#![cfg_attr(not(feature = "std"), no_std)]
mod mock;
mod regex_offsets;
mod tests;
extern crate alloc;
use crate::regex_offsets::{get_index_offsets, get_url_offset};
use alloc::format;
use alloc::string::{FromUtf8Error, String, ToString};
use core::str::FromStr;
use eq_primitives::currency::*;
use eq_utils::log::eq_log;
use eq_utils::{eq_ensure, ok_or_error};
use eq_whitelists::CheckWhitelisted;
use frame_support::{
codec::{Decode, Encode},
debug, decl_error, decl_event, decl_module, decl_storage,
dispatch::DispatchResult,
traits::{Get, UnixTime},
};
use impl_trait_for_tuples::impl_for_tuples;
use serde_json as json;
use sp_arithmetic::{FixedI64, FixedPointNumber};
use sp_core::crypto::KeyTypeId;
use sp_runtime::{
offchain::{http, Duration, StorageKind},
traits::IdentifyAccount,
RuntimeAppPublic, RuntimeDebug,
};
use sp_std::iter::Iterator;
use sp_std::prelude::*;
use system as frame_system;
use system::{
self as system, ensure_root, ensure_signed,
offchain::{
AppCrypto, CreateSignedTransaction,
SendSignedTransaction,
Signer,
},
};
pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"orac");
pub mod crypto {
use super::KEY_TYPE;
use sp_runtime::MultiSignature;
use sp_runtime::{
app_crypto::{app_crypto, sr25519},
traits::Verify,
};
app_crypto!(sr25519, KEY_TYPE);
pub struct AuthId;
impl system::offchain::AppCrypto<<MultiSignature as Verify>::Signer, MultiSignature> for AuthId {
type RuntimeAppPublic = Public;
type GenericSignature = sp_core::sr25519::Signature;
type GenericPublic = sp_core::sr25519::Public;
}
use sp_core::sr25519::Signature as Sr25519Signature;
pub struct TestAuthId;
impl system::offchain::AppCrypto<<Sr25519Signature as Verify>::Signer, Sr25519Signature>
for TestAuthId
{
type RuntimeAppPublic = Public;
type GenericSignature = sp_core::sr25519::Signature;
type GenericPublic = sp_core::sr25519::Public;
}
}
#[impl_for_tuples(5)]
pub trait OnNewPrice {
fn on_new_price(currency: &eq_primitives::currency::Currency, price: FixedI64);
}
pub trait Trait: CreateSignedTransaction<Call<Self>> {
type AuthorityId: AppCrypto<Self::Public, Self::Signature>;
type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
type Call: From<Call<Self>>;
type UnixTime: UnixTime;
type Whitelist: CheckWhitelisted<Self::AccountId>;
type MedianPriceTimeout: Get<u64>;
type PriceTimeout: Get<u64>;
type OnNewPrice: OnNewPrice;
}
trait WithUrl {
fn get_url(&self, url_template: &str, path_template: &str) -> (String, String);
}
impl WithUrl for Currency {
fn get_url(&self, url_template: &str, path_template: &str) -> (String, String) {
let is_upper_case = match url_template.find("USD") {
Some(_) => true,
None => false,
};
let is_kraken = url_template.contains("api.kraken.com");
let symbol = match self {
Currency::Unknown => panic!("Nope!"),
Currency::Usd => "usd",
Currency::Eq => "eq",
Currency::Eth => {
if is_kraken {
"XETHZ"
} else {
"eth"
}
}
Currency::Btc => {
if is_kraken {
"XXBTZ"
} else {
"btc"
}
}
Currency::Eos => "eos",
Currency::Dot => "dot",
};
let upper_case_symbol = symbol.to_uppercase();
(
url_template.replace(
"{$}",
if is_upper_case {
upper_case_symbol.as_str()
} else {
symbol
},
),
path_template.replace(
"{$}",
if is_upper_case {
upper_case_symbol.as_str()
} else {
symbol
},
),
)
}
}
#[derive(Encode, Decode, Clone, Default, PartialEq, RuntimeDebug)]
pub struct DataPoint<AccountId, BlockNumber> {
price: FixedI64,
account_id: AccountId,
block_number: BlockNumber,
timestamp: u64,
}
impl<AccountId, BlockNumber> DataPoint<AccountId, BlockNumber> {
pub fn get(&self) -> (&AccountId, i64) {
(&self.account_id, self.price.into_inner())
}
}
#[derive(Encode, Decode, Clone, Default, PartialEq, RuntimeDebug)]
pub struct PricePoint<AccountId, BlockNumber> {
block_number: BlockNumber,
timestamp: u64,
price: FixedI64,
data_points: Vec<DataPoint<AccountId, BlockNumber>>,
}
impl<AccountId, BlockNumber> PricePoint<AccountId, BlockNumber> {
pub fn get_block_number(&self) -> &BlockNumber {
&self.block_number
}
pub fn get_timestamp(&self) -> u64 {
self.timestamp
}
pub fn get_price(&self) -> i64 {
self.price.into_inner()
}
pub fn get_data_points(&self) -> &Vec<DataPoint<AccountId, BlockNumber>> {
&self.data_points
}
}
#[derive(Debug)]
pub enum PriceSource {
Custom,
Cryptocompare,
}
impl PriceSource {
pub fn get(resource_type: String) -> Option<PriceSource> {
match resource_type.as_str() {
"custom" => Some(PriceSource::Custom),
"cryptocompare" => Some(PriceSource::Cryptocompare),
_ => Option::None,
}
}
pub fn get_param_key(&self) -> &[u8] {
match self {
PriceSource::Custom => b"oracle::custom_query",
PriceSource::Cryptocompare => b"oracle::cryptocompare_api_key",
}
}
pub fn get_query(&self) -> Result<Option<String>, FromUtf8Error> {
let query_param_raw =
sp_io::offchain::local_storage_get(StorageKind::PERSISTENT, self.get_param_key());
if let Some(query_param) = query_param_raw {
let param = String::from_utf8(query_param)?;
match self {
PriceSource::Custom => return Ok(Some(param)),
PriceSource::Cryptocompare => {
return Ok(Some(format!("json(https://min-api.cryptocompare.com/data/price?fsym={{$}}&tsyms=USD&api_key={}).USD", param)))
},
};
}
Ok(Option::None)
}
}
decl_storage! {
trait Store for Module<T: Trait> as Oracle
{
PricePoints get(fn price_points): map hasher(identity) Currency => PricePoint<<T as system::Trait>::AccountId, <T as system::Trait>::BlockNumber>;
}
add_extra_genesis {
config(prices): Vec<(u8, u64, u64)>;
config(update_date): u64;
build(|config: &GenesisConfig| {
let default_price_point = PricePoint {
block_number: system::Module::<T>::block_number(),
timestamp: 0,
price: FixedI64::saturating_from_integer(-1),
data_points: Vec::<DataPoint<T::AccountId, T::BlockNumber>>::new()
};
for currency in Currency::iterator() {
<PricePoints<T>>::insert(currency, default_price_point.clone());
}
for &(currency, nom, denom) in config.prices.iter() {
let currency_typed: Currency = currency.into();
let price: FixedI64 = FixedI64::saturating_from_rational(nom, denom);
<PricePoints<T>>::mutate(¤cy_typed, |price_point| {
price_point.timestamp = config.update_date;
price_point.price = price;
});
}
});
}
}
decl_event!(
pub enum Event<T>
where
AccountId = <T as system::Trait>::AccountId,
Currency = eq_primitives::currency::Currency,
{
NewPrice(Currency, FixedI64, FixedI64, AccountId),
}
);
decl_error! {
pub enum Error for Module<T: Trait> {
NotAllowedToSubmitPrice,
PriceAlreadyAdded,
CurrencyNotFound,
WrongCurrency,
PriceIsZero,
PriceIsNegative,
PriceTimeout,
}
}
decl_module! {
pub struct Module<T: Trait> for enum Call where origin: T::Origin {
fn deposit_event() = default;
#[weight = 10_000]
pub fn set_price(origin, currency: Currency, price: FixedI64) -> DispatchResult
{
let who = ensure_signed(origin)?;
eq_ensure!(T::Whitelist::in_whitelist(&who), Error::<T>::NotAllowedToSubmitPrice,
"{}:{}. Account not in whitelist. Who: {:?}.", file!(), line!(), who);
eq_ensure!(price != FixedI64::from_inner(0), Error::<T>::PriceIsZero,
"{}:{}. Price is equal to zero. Who: {:?}, price: {:?}, currency: {:?}.",
file!(), line!(), who, price, currency);
eq_ensure!(!price.is_negative(), Error::<T>::PriceIsNegative,
"{}:{}. Price is negative. Who: {:?}, price: {:?}, currency: {:?}.",
file!(), line!(), who, price, currency);
eq_ensure!(currency != Currency::Usd, Error::<T>::WrongCurrency,
"{}:{}. 'USD' is not allowed to set price. Who: {:?}, price: {:?}, currency: {:?}.",
file!(), line!(), who, price, currency);
eq_ensure!(currency != Currency::Unknown, Error::<T>::WrongCurrency,
"{}:{}. 'Unknown' is not allowed to set price. Who: {:?}, price: {:?}, currency: {:?}.",
file!(), line!(), who, price, currency);
let mut new_price = price;
<PricePoints<T>>::mutate(¤cy, |price_point| {
let current_block = system::Module::<T>::block_number();
let current_time = T::UnixTime::now().as_secs();
if price_point.block_number == current_block {
if price_point.data_points.iter().any(|x| x.account_id == who && x.block_number == current_block) {
debug::error!("{}:{}. Account already setted price. Who: {:?}, price: {:?}, block: {:?}, timestamp: {:?}.",
file!(), line!(), who, price_point.price, price_point.block_number, price_point.timestamp);
return Err(Error::<T>::PriceAlreadyAdded);
}
}
price_point.block_number = current_block;
price_point.timestamp = current_time;
let dp = DataPoint { account_id: who.clone(), price: price, block_number: current_block, timestamp: current_time };
let white_list = T::Whitelist::accounts();
let mut actual_data_points: Vec<_> = price_point.data_points.iter().filter(|x| current_time - x.timestamp < T::PriceTimeout::get()).collect();
actual_data_points.push(&dp);
let mut last_data_points: Vec<_> = Vec::new();
for acc_id in white_list {
if let Some(i) = actual_data_points.iter().rposition(|x| x.account_id == acc_id) {
last_data_points.push(actual_data_points[i].clone());
}
}
last_data_points.sort_by(|a, b| a.price.cmp(&b.price));
let len = last_data_points.len();
if len % 2 == 0
{
new_price= (last_data_points[len/2-1].price+last_data_points[len/2].price)/(FixedI64::one()+FixedI64::one());
}
else
{
new_price= last_data_points[len/2].price;
}
price_point.price = new_price;
price_point.data_points = last_data_points;
eq_log!("Med(Avg) calc price:{:?} new_price:{:?} {:?}", price , new_price, ¤cy);
Ok(())
})?;
T::OnNewPrice::on_new_price(¤cy, price);
Self::deposit_event(RawEvent::NewPrice(currency, price, new_price, who));
Ok(())
}
fn offchain_worker(_block_number: T::BlockNumber) {
let publics = <T::AuthorityId as AppCrypto<T::Public, T::Signature>>::RuntimeAppPublic::all()
.into_iter()
.enumerate()
.filter_map(|(_index, key)| {
let generic_public = <T::AuthorityId as AppCrypto<T::Public, T::Signature>>::GenericPublic::from(key);
let public = generic_public.into();
let account_id = public.clone().into_account();
if T::Whitelist::in_whitelist(&account_id) {
Option::Some(public.clone())
} else {
Option::None
}
})
.collect();
let signer = Signer::<T, T::AuthorityId>::all_accounts().with_filter(publics);
if !signer.can_sign() {
return;
}
let price_periodicity_o = Self::get_local_storage_val::<u32>("oracle::price_periodicity");
if let Some(price_periodicity) = price_periodicity_o {
if price_periodicity < 1 {
debug::warn!("Unexpected price periodicity {:?}, should be more than 1", price_periodicity);
return;
}
let counter = Self::get_local_storage_val::<u32>("oracle::counter");
let counter = counter.unwrap_or(0_u32);
let counter_next = counter + 1;
if counter_next == price_periodicity {
sp_io::offchain::local_storage_set(StorageKind::PERSISTENT, b"oracle::counter", 0_u32.to_string().as_bytes());
let resource_type_o = Self::get_local_storage_val::<String>("oracle::resource_type");
if let Some(resource_type) = resource_type_o {
let rsrc = PriceSource::get(resource_type);
if let Some(resource) = rsrc {
match resource.get_query() {
Ok(qry) => {
if let Some(query) = qry {
for currency in Currency::iterator() {
if currency == &Currency::Unknown || currency == &Currency::Usd {continue;}
if currency == &Currency::Eq {
continue;
}
let price = Self::fetch_price(currency, &query);
if price.is_err()
{
debug::warn!("Can't fetch {:?} price from {}", currency, query);
continue;
}
let price_unwrapped = price.unwrap();
if currency == &Currency::Dot {
let eq_price = price_unwrapped * FixedI64::saturating_from_rational(17, 100);
signer.send_signed_transaction(|_account| {
Call::set_price(Currency::Eq, eq_price)
});
}
signer.send_signed_transaction(|_account| {
Call::set_price(currency.clone(), price_unwrapped)
});
}
}
},
Err(_e) => {
debug::warn!("Can't decode {:?} price query", resource);
return;
}
}
} else {
debug::warn!("Unexpected price resource type {:?}", rsrc);
return;
}
}
} else if counter_next > price_periodicity {
sp_io::offchain::local_storage_set(StorageKind::PERSISTENT, b"oracle::counter", 0_u32.to_string().as_bytes());
} else {
sp_io::offchain::local_storage_set(StorageKind::PERSISTENT, b"oracle::counter", counter_next.to_string().as_bytes());
}
}
}
#[weight = 10_000]
pub fn print_node_message(origin, message: Vec<u8>) -> DispatchResult
{
ensure_root(origin)?;
eq_log!("|{:?}", String::from_utf8(message));
Ok(())
}
}
}
impl<T: Trait> Module<T> {
fn exec_query(url: String) -> Result<String, http::Error> {
let request = http::Request::get(url.as_str());
let deadline = sp_io::offchain::timestamp().add(Duration::from_millis(2_000));
let pending = request.deadline(deadline).send().map_err(|_| {
debug::error!(
"{}:{}. Error sending request. Request: {:?}, deadline: {:?}.",
file!(),
line!(),
url.as_str(),
deadline
);
http::Error::IoError
})?;
let response = pending.try_wait(deadline).map_err(|_| {
debug::error!(
"{}:{}. Didn't receive response. Deadline: {:?}.",
file!(),
line!(),
deadline
);
http::Error::DeadlineReached
})??;
if response.code != 200 {
debug::error!(
"{}:{}. Unexpected status code: {}",
file!(),
line!(),
response.code
);
return Err(http::Error::Unknown);
}
let body = response.body();
let str = String::from_utf8(body.collect()).unwrap_or(String::new());
Ok(str)
}
fn fetch_price_from_json(body: String, path: &str) -> Result<FixedI64, http::Error> {
let err = http::Error::Unknown;
let mut val: &json::Value = &json::from_str(&body).or_else(|_| {
debug::warn!(
"{:?}:{:?} {:?}. Cannot deserialize an instance from a string of JSON text.",
file!(),
line!(),
body
);
Err(err.clone())
})?;
let indices = path.split(".");
for index in indices {
let offsets = get_index_offsets(index.as_bytes());
if offsets.len() == 0 {
let option_value = val.get(index);
val = ok_or_error!(
option_value,
err.clone(),
"{}:{}. Couldn't access a value in a map. Json: {:?}, index: {:?}.",
file!(),
line!(),
val,
index
)?;
} else {
for (start, end) in offsets {
if start != 0 {
let option_value = val.get(&index[..start]);
val = ok_or_error!(option_value, err.clone(), "{}:{}. Couldn't access an element of an array. Json: {:?}, index: {:?}.",
file!(), line!(), val, &index[..start])?;
}
let i = &index[start + 1..end - 1]
.parse::<usize>()
.expect("Expect a number as array index");
let option_value = val.get(i);
val = ok_or_error!(
option_value,
err.clone(),
"{}:{}. Couldn't access an element of an array. Json: {:?}, index: {:?}.",
file!(),
line!(),
val,
i
)?;
}
}
}
let option_price = match val {
json::Value::Number(v) => Ok(v.as_f64()),
json::Value::String(v) => Ok(v.parse::<f64>().ok()),
_ => Err({
debug::error!(
"{}:{}. Value received from json not number or string. Value: {:?}.",
file!(),
line!(),
val
);
http::Error::Unknown
}),
}?;
let price = ok_or_error!(
option_price,
err,
"{}:{}. Couldn't get value as f64. Value: {:?}.",
file!(),
line!(),
val
)?;
Ok(FixedI64::from_inner(
(price * (FixedI64::accuracy() as f64)) as i64,
))
}
fn fetch_price(currency: &Currency, query: &String) -> Result<FixedI64, http::Error> {
let url_bytes_offset = get_url_offset(query.as_bytes());
if let Some((start, end)) = url_bytes_offset {
let url_template = &query[start + 1..end - 2];
if !url_template.contains("{$}") {
debug::error!("{}:{}. Incorrect query format, doesn't have {{$}}. Query: {}, url template: {:?}.",
file!(), line!(), query, url_template);
Err(http::Error::Unknown)
} else {
let path_template = &query[end..];
let (url, path) = currency.get_url(url_template, path_template);
let s = Self::exec_query(url)?;
Self::fetch_price_from_json(s, path.as_str())
}
} else {
debug::error!(
"{}:{}. Incorrect query format, can't parse. Query: {}",
file!(),
line!(),
query
);
Err(http::Error::Unknown)
}
}
fn get_local_storage_val<R: FromStr>(key: &str) -> Option<R> {
let raw_val = sp_io::offchain::local_storage_get(StorageKind::PERSISTENT, key.as_bytes());
match raw_val {
Some(val_bytes) => match String::from_utf8(val_bytes.clone()) {
Ok(val_decoded) => match val_decoded.parse::<R>() {
Ok(val) => return Some(val),
Err(_e) => {
debug::warn!("Can't parse local storage value {:?}", val_decoded);
return None;
}
},
Err(_e) => {
debug::warn!("Can't decode local storage key {:?}: {:?}", key, val_bytes);
return None;
}
},
None => {
debug::warn!("Uninitialized local storage key: {:?}", key);
return None;
}
}
}
}
pub trait PriceGetter {
fn get_price(currency: &Currency) -> Result<FixedI64, sp_runtime::DispatchError>;
}
impl<T: Trait> PriceGetter for Module<T> {
fn get_price(currency: &Currency) -> Result<FixedI64, sp_runtime::DispatchError> {
if currency == &Currency::Usd {
return Ok(FixedI64::saturating_from_integer(1));
}
eq_ensure!(
<PricePoints<T>>::contains_key(¤cy),
Error::<T>::CurrencyNotFound,
"{}:{}. Currency not found in PricePoints. Currency: {:?}.",
file!(),
line!(),
currency
);
let item = <PricePoints<T>>::get(¤cy);
let price = item.price;
eq_ensure!(
price != FixedI64::from_inner(0),
Error::<T>::PriceIsZero,
"{}:{}. Price is equal to zero. Price: {:?}, currency: {:?}.",
file!(),
line!(),
price,
currency
);
eq_ensure!(
!price.is_negative(),
Error::<T>::PriceIsNegative,
"{}:{}. Price is negative. Price: {:?}, currency: {:?}.",
file!(),
line!(),
price,
currency
);
let current_time = T::UnixTime::now().as_secs();
eq_ensure!(
(current_time < item.timestamp + T::MedianPriceTimeout::get()),
Error::<T>::PriceTimeout,
"{}:{}. Price received after time is out. Current time: {:?}, price_point timestamp + {:?} seconds: {:?}.",
file!(), line!(), current_time, T::MedianPriceTimeout::get(), item.timestamp + T::MedianPriceTimeout::get()
);
Ok(price)
}
}