From ed0baffe897f123372fe5fc953bb61dd18216775 Mon Sep 17 00:00:00 2001 From: Andrew Dibble Date: Thu, 26 Oct 2023 17:25:53 +0200 Subject: [PATCH] feat(custom-rpc): improve environment RPC (#4154) * feat(custom-rpc): improve environment RPC * redo * add serialization test * update type * fix clippy * rename * chore: use serde(flatten), more explicit conversion * untagged * unimplement deserialization * fixes * snapshot * add snapshots * fix clippy * serialize to string * change serialization * delete file * safer serialization * safer number or hex --------- Co-authored-by: Daniel --- Cargo.lock | 23 ++ state-chain/chains/src/address.rs | 18 +- state-chain/custom-rpc/Cargo.toml | 3 +- state-chain/custom-rpc/src/lib.rs | 305 +++++++++++++----- ...ustom_rpc__test__broker_serialization.snap | 5 + ..._rpc__test__environment_serialization.snap | 5 + .../custom_rpc__test__lp_serialization.snap | 5 + ...m_rpc__test__no_account_serialization.snap | 5 + ...om_rpc__test__validator_serialization.snap | 5 + state-chain/runtime/src/lib.rs | 33 +- state-chain/runtime/src/runtime_apis.rs | 13 +- utilities/Cargo.toml | 3 + utilities/src/with_std.rs | 1 + utilities/src/with_std/rpc.rs | 91 ++++++ 14 files changed, 424 insertions(+), 91 deletions(-) create mode 100644 state-chain/custom-rpc/src/snapshots/custom_rpc__test__broker_serialization.snap create mode 100644 state-chain/custom-rpc/src/snapshots/custom_rpc__test__environment_serialization.snap create mode 100644 state-chain/custom-rpc/src/snapshots/custom_rpc__test__lp_serialization.snap create mode 100644 state-chain/custom-rpc/src/snapshots/custom_rpc__test__no_account_serialization.snap create mode 100644 state-chain/custom-rpc/src/snapshots/custom_rpc__test__validator_serialization.snap create mode 100644 utilities/src/with_std/rpc.rs diff --git a/Cargo.lock b/Cargo.lock index f6a2824d39..5605fd7e06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2338,6 +2338,7 @@ dependencies = [ "cf-primitives", "futures", "hex", + "insta", "jsonrpsee 0.16.2", "pallet-cf-governance", "pallet-cf-pools", @@ -4863,6 +4864,20 @@ dependencies = [ "generic-array 0.14.7", ] +[[package]] +name = "insta" +version = "1.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d64600be34b2fcfc267740a243fa7744441bb4947a619ac4e5bb6507f35fbfc" +dependencies = [ + "console", + "lazy_static", + "linked-hash-map", + "serde", + "similar", + "yaml-rust", +] + [[package]] name = "instant" version = "0.1.12" @@ -10495,6 +10510,12 @@ dependencies = [ "wide", ] +[[package]] +name = "similar" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aeaf503862c419d66959f5d7ca015337d864e9c49485d771b732e2a20453597" + [[package]] name = "siphasher" version = "0.3.10" @@ -13156,6 +13177,8 @@ dependencies = [ "reqwest", "scopeguard", "serde", + "serde_json", + "sp-core 21.0.0 (git+https://github.com/chainflip-io/substrate.git?tag=chainflip-monthly-2023-08+2)", "sp-rpc", "tempfile", "tokio", diff --git a/state-chain/chains/src/address.rs b/state-chain/chains/src/address.rs index 8e8ad5cdf6..ea462a36bb 100644 --- a/state-chain/chains/src/address.rs +++ b/state-chain/chains/src/address.rs @@ -245,13 +245,29 @@ impl ToHumanreadableAddress for PolkadotAccountId { } #[cfg(feature = "std")] -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] +#[serde(untagged)] +/// A type that serializes the address in a human-readable way. This can only be +/// serialized and not deserialized. +/// `deserialize` is not implemented for ForeignChainAddressHumanreadable +/// because it is not possible to deserialize a human-readable address without +/// further context around the asset and chain. pub enum ForeignChainAddressHumanreadable { Eth(::Humanreadable), Dot(::Humanreadable), Btc(::Humanreadable), } +#[cfg(feature = "std")] +impl<'de> Deserialize<'de> for ForeignChainAddressHumanreadable { + fn deserialize(_deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + unimplemented!("Deserialization of ForeignChainAddressHumanreadable is not implemented") + } +} + impl ToHumanreadableAddress for ForeignChainAddress { #[cfg(feature = "std")] type Humanreadable = ForeignChainAddressHumanreadable; diff --git a/state-chain/custom-rpc/Cargo.toml b/state-chain/custom-rpc/Cargo.toml index ac1fcefb93..cc6b7406a4 100644 --- a/state-chain/custom-rpc/Cargo.toml +++ b/state-chain/custom-rpc/Cargo.toml @@ -29,4 +29,5 @@ sp-runtime = { git = "https://github.com/chainflip-io/substrate.git", tag = "cha sc-client-api = { git = "https://github.com/chainflip-io/substrate.git", tag = "chainflip-monthly-2023-08+2" } [dev-dependencies] -serde_json = "1.0.107" +insta = { version = "1.34.0", features = ["json"] } +serde_json = "1.0" diff --git a/state-chain/custom-rpc/src/lib.rs b/state-chain/custom-rpc/src/lib.rs index 10c8a503e4..77a2076837 100644 --- a/state-chain/custom-rpc/src/lib.rs +++ b/state-chain/custom-rpc/src/lib.rs @@ -4,13 +4,12 @@ use cf_amm::{ }; use cf_chains::{ address::{ForeignChainAddressHumanreadable, ToHumanreadableAddress}, - btc::BitcoinNetwork, - dot::PolkadotHash, eth::Address as EthereumAddress, }; use cf_primitives::{ AccountRole, Asset, AssetAmount, ForeignChain, NetworkEnvironment, SemVer, SwapOutput, }; +use cf_utilities::rpc::NumberOrHex; use core::ops::Range; use jsonrpsee::{ core::RpcResult, @@ -23,12 +22,11 @@ use pallet_cf_pools::{AssetsMap, PoolInfo, PoolLiquidity, PoolOrders, Unidirecti use sc_client_api::{BlockchainEvents, HeaderBackend}; use serde::{Deserialize, Serialize}; use sp_api::BlockT; -use sp_rpc::number::NumberOrHex; use sp_runtime::DispatchError; use state_chain_runtime::{ chainflip::Offence, constants::common::TX_FEE_MULTIPLIER, - runtime_apis::{CustomRuntimeApi, Environment, LiquidityProviderInfo, RuntimeApiAccountInfoV2}, + runtime_apis::{CustomRuntimeApi, LiquidityProviderInfo, RuntimeApiAccountInfoV2}, }; use std::{ collections::{BTreeMap, HashMap}, @@ -165,29 +163,67 @@ pub struct RpcSwapOutput { impl From for RpcSwapOutput { fn from(swap_output: SwapOutput) -> Self { Self { - intermediary: swap_output.intermediary.map(NumberOrHex::from), - output: NumberOrHex::from(swap_output.output), + intermediary: swap_output.intermediary.map(Into::into), + output: swap_output.output.into(), } } } #[derive(Serialize, Deserialize)] -pub struct RpcEnvironment { - bitcoin_network: BitcoinNetwork, - ethereum_chain_id: cf_chains::evm::api::EvmChainId, - polkadot_genesis_hash: PolkadotHash, +#[serde(untagged)] +pub enum RpcAsset { + ImplicitChain(Asset), + ExplicitChain { chain: ForeignChain, asset: Asset }, } -impl From for RpcEnvironment { - fn from(environment: Environment) -> Self { - Self { - bitcoin_network: environment.network.into(), - ethereum_chain_id: environment.ethereum_chain_id, - polkadot_genesis_hash: environment.polkadot_genesis_hash, - } +impl From for RpcAsset { + fn from(asset: Asset) -> Self { + RpcAsset::ExplicitChain { asset, chain: asset.into() } + } +} + +#[derive(Serialize, Deserialize)] +pub struct RpcPoolInfo { + #[serde(flatten)] + pub pool_info: PoolInfo, + pub pair_asset: RpcAsset, +} + +impl From for RpcPoolInfo { + fn from(pool_info: PoolInfo) -> Self { + Self { pool_info, pair_asset: Asset::Usdc.into() } } } +#[derive(Serialize, Deserialize)] +pub struct PoolsEnvironment { + pub fees: HashMap>>, +} + +#[derive(Serialize, Deserialize)] +pub struct IngressEgressEnvironment { + pub minimum_deposit_amounts: HashMap>, +} + +#[derive(Serialize, Deserialize)] +pub struct FundingEnvironment { + pub redemption_tax: NumberOrHex, + pub minimum_funding_amount: NumberOrHex, +} + +#[derive(Serialize, Deserialize)] +pub struct SwappingEnvironment { + minimum_swap_amounts: HashMap>, +} + +#[derive(Serialize, Deserialize)] +pub struct RpcEnvironment { + ingress_egress: IngressEgressEnvironment, + swapping: SwappingEnvironment, + funding: FundingEnvironment, + pools: PoolsEnvironment, +} + #[rpc(server, client, namespace = "cf")] /// The custom RPC endpoints for the state chain node. pub trait CustomApi { @@ -336,6 +372,26 @@ pub trait CustomApi { liquidity: Liquidity, at: Option, ) -> RpcResult>>; + #[method(name = "funding_environment")] + fn cf_funding_environment( + &self, + at: Option, + ) -> RpcResult; + #[method(name = "swapping_environment")] + fn cf_swapping_environment( + &self, + at: Option, + ) -> RpcResult; + #[method(name = "ingress_egress_environment")] + fn cf_ingress_egress_environment( + &self, + at: Option, + ) -> RpcResult; + #[method(name = "pool_environment")] + fn cf_pool_environment( + &self, + at: Option, + ) -> RpcResult; #[method(name = "environment")] fn cf_environment(&self, at: Option) -> RpcResult; #[deprecated(note = "Use direct storage access of `CurrentReleaseVersion` instead.")] @@ -553,7 +609,7 @@ where RpcAccountInfo::lp( info, - api.cf_environment(hash).map_err(to_rpc_error)?.network, + api.cf_network_environment(hash).map_err(to_rpc_error)?, balance, ) }, @@ -674,7 +730,7 @@ where self.unwrap_or_best(at), from, to, - cf_utilities::try_parse_number_or_hex(amount).and_then(|amount| { + amount.try_into().and_then(|amount| { if amount == 0 { Err(anyhow::anyhow!("Swap input amount cannot be zero.")) } else { @@ -783,12 +839,85 @@ where .map_err(map_dispatch_error) } + fn cf_ingress_egress_environment( + &self, + at: Option, + ) -> RpcResult { + let runtime_api = &self.client.runtime_api(); + let hash = self.unwrap_or_best(at); + let mut minimum_deposit_amounts = HashMap::new(); + + for asset in Asset::all() { + minimum_deposit_amounts + .entry(ForeignChain::from(asset)) + .or_insert_with(HashMap::new) + .insert( + asset, + runtime_api.cf_min_deposit_amount(hash, asset).map_err(to_rpc_error)?.into(), + ); + } + + Ok(IngressEgressEnvironment { minimum_deposit_amounts }) + } + + fn cf_swapping_environment( + &self, + at: Option, + ) -> RpcResult { + let runtime_api = &self.client.runtime_api(); + let hash = self.unwrap_or_best(at); + let mut minimum_swap_amounts = HashMap::new(); + + for asset in Asset::all() { + let swap_amount = runtime_api.cf_min_swap_amount(hash, asset).map_err(to_rpc_error)?; + minimum_swap_amounts + .entry(asset.into()) + .or_insert_with(HashMap::new) + .insert(asset, swap_amount.into()); + } + + Ok(SwappingEnvironment { minimum_swap_amounts }) + } + + fn cf_funding_environment( + &self, + at: Option, + ) -> RpcResult { + let runtime_api = &self.client.runtime_api(); + let hash = self.unwrap_or_best(at); + + Ok(FundingEnvironment { + redemption_tax: runtime_api.cf_redemption_tax(hash).map_err(to_rpc_error)?.into(), + minimum_funding_amount: runtime_api.cf_min_funding(hash).map_err(to_rpc_error)?.into(), + }) + } + + fn cf_pool_environment( + &self, + at: Option, + ) -> RpcResult { + let mut fees = HashMap::new(); + + for asset in Asset::all() { + if asset == Asset::Usdc { + continue + } + + let info = self.cf_pool_info(asset, Asset::Usdc, at)?.map(Into::into); + + fees.entry(asset.into()).or_insert_with(HashMap::new).insert(asset, info); + } + + Ok(PoolsEnvironment { fees }) + } + fn cf_environment(&self, at: Option) -> RpcResult { - self.client - .runtime_api() - .cf_environment(self.unwrap_or_best(at)) - .map_err(to_rpc_error) - .map(RpcEnvironment::from) + Ok(RpcEnvironment { + ingress_egress: self.cf_ingress_egress_environment(at)?, + swapping: self.cf_swapping_environment(at)?, + funding: self.cf_funding_environment(at)?, + pools: self.cf_pool_environment(at)?, + }) } fn cf_current_compatibility_version(&self) -> RpcResult { @@ -944,20 +1073,29 @@ where mod test { use super::*; - use serde_json::json; use sp_core::H160; + /* + changing any of these serialization tests signifies a breaking change in the + API. please make sure to get approval from the product team before merging + any changes that break a serialization test. + + if approval is received and a new breaking change is introduced, please + stale the review and get a new review from someone on product. + */ + #[test] - fn test_account_info_serialization() { - assert_eq!( - serde_json::to_value(RpcAccountInfo::none(0)).unwrap(), - json!({ "role": "none", "flip_balance": "0x0" }) - ); - assert_eq!( - serde_json::to_value(RpcAccountInfo::broker(0)).unwrap(), - json!({ "role":"broker", "flip_balance": "0x0" }) - ); + fn test_no_account_serialization() { + insta::assert_display_snapshot!(serde_json::to_value(RpcAccountInfo::none(0)).unwrap()); + } + + #[test] + fn test_broker_serialization() { + insta::assert_display_snapshot!(serde_json::to_value(RpcAccountInfo::broker(0)).unwrap()); + } + #[test] + fn test_lp_serialization() { let lp = RpcAccountInfo::lp( LiquidityProviderInfo { refund_addresses: vec![ @@ -981,26 +1119,11 @@ mod test { 0, ); - assert_eq!( - serde_json::to_value(lp).unwrap(), - json!({ - "role": "liquidity_provider", - "flip_balance": "0x0", - "balances": { - "Ethereum": { - "Flip": "0x7fffffffffffffffffffffffffffffff", - "Eth": "0xffffffffffffffffffffffffffffffff" - }, - "Bitcoin": { "Btc": "0x0" }, - }, - "refund_addresses": { - "Ethereum": { "Eth" : "0x0101010101010101010101010101010101010101" }, - "Bitcoin": null, - "Polkadot": { "Dot": "111111111111111111111111111111111HC1" } - } - }) - ); + insta::assert_display_snapshot!(serde_json::to_value(lp).unwrap()); + } + #[test] + fn test_validator_serialization() { let validator = RpcAccountInfo::validator(RuntimeApiAccountInfoV2 { balance: 10u128.pow(18), bond: 10u128.pow(18), @@ -1016,26 +1139,60 @@ mod test { apy_bp: Some(100u32), restricted_balances: BTreeMap::from_iter(vec![(H160::from([1; 20]), 10u128.pow(18))]), }); - assert_eq!( - serde_json::to_value(validator).unwrap(), - json!({ - "flip_balance": "0xde0b6b3a7640000", - "bond": "0xde0b6b3a7640000", - "bound_redeem_address": "0x0101010101010101010101010101010101010101", - "is_bidding": false, - "is_current_authority": true, - "is_current_backup": false, - "is_online": true, - "is_qualified": true, - "keyholder_epochs": [123], - "last_heartbeat": 0, - "reputation_points": 0, - "role": "validator", - "apy_bp": 100, - "restricted_balances": { - "0x0101010101010101010101010101010101010101": "0xde0b6b3a7640000" - } - }) - ); + + insta::assert_display_snapshot!(serde_json::to_value(validator).unwrap()); + } + + #[test] + fn test_environment_serialization() { + let env = RpcEnvironment { + swapping: SwappingEnvironment { + minimum_swap_amounts: HashMap::from([ + (ForeignChain::Bitcoin, HashMap::from([(Asset::Btc, 0u32.into())])), + ( + ForeignChain::Ethereum, + HashMap::from([ + (Asset::Flip, u64::MAX.into()), + (Asset::Usdc, (u64::MAX / 2 - 1).into()), + (Asset::Eth, 0u32.into()), + ]), + ), + ]), + }, + ingress_egress: IngressEgressEnvironment { + minimum_deposit_amounts: HashMap::from([ + (ForeignChain::Bitcoin, HashMap::from([(Asset::Btc, 0u32.into())])), + ( + ForeignChain::Ethereum, + HashMap::from([ + (Asset::Flip, u64::MAX.into()), + (Asset::Usdc, (u64::MAX / 2 - 1).into()), + (Asset::Eth, 0u32.into()), + ]), + ), + ]), + }, + funding: FundingEnvironment { + redemption_tax: 0u32.into(), + minimum_funding_amount: 0u32.into(), + }, + pools: PoolsEnvironment { + fees: HashMap::from([( + ForeignChain::Ethereum, + HashMap::from([( + Asset::Flip, + Some( + PoolInfo { + limit_order_fee_hundredth_pips: 0, + range_order_fee_hundredth_pips: 100, + } + .into(), + ), + )]), + )]), + }, + }; + + insta::assert_display_snapshot!(serde_json::to_value(env).unwrap()); } } diff --git a/state-chain/custom-rpc/src/snapshots/custom_rpc__test__broker_serialization.snap b/state-chain/custom-rpc/src/snapshots/custom_rpc__test__broker_serialization.snap new file mode 100644 index 0000000000..39803b3c8c --- /dev/null +++ b/state-chain/custom-rpc/src/snapshots/custom_rpc__test__broker_serialization.snap @@ -0,0 +1,5 @@ +--- +source: state-chain/custom-rpc/src/lib.rs +expression: "serde_json::to_value(RpcAccountInfo::broker(0)).unwrap()" +--- +{"flip_balance":"0x0","role":"broker"} diff --git a/state-chain/custom-rpc/src/snapshots/custom_rpc__test__environment_serialization.snap b/state-chain/custom-rpc/src/snapshots/custom_rpc__test__environment_serialization.snap new file mode 100644 index 0000000000..436ee8b949 --- /dev/null +++ b/state-chain/custom-rpc/src/snapshots/custom_rpc__test__environment_serialization.snap @@ -0,0 +1,5 @@ +--- +source: state-chain/custom-rpc/src/lib.rs +expression: "serde_json::to_value(env).unwrap()" +--- +{"funding":{"minimum_funding_amount":0,"redemption_tax":0},"ingress_egress":{"minimum_deposit_amounts":{"Bitcoin":{"Btc":0},"Ethereum":{"Eth":0,"Flip":"0xffffffffffffffff","Usdc":"0x7ffffffffffffffe"}}},"pools":{"fees":{"Ethereum":{"Flip":{"limit_order_fee_hundredth_pips":0,"pair_asset":{"asset":"Usdc","chain":"Ethereum"},"range_order_fee_hundredth_pips":100}}}},"swapping":{"minimum_swap_amounts":{"Bitcoin":{"Btc":0},"Ethereum":{"Eth":0,"Flip":"0xffffffffffffffff","Usdc":"0x7ffffffffffffffe"}}}} diff --git a/state-chain/custom-rpc/src/snapshots/custom_rpc__test__lp_serialization.snap b/state-chain/custom-rpc/src/snapshots/custom_rpc__test__lp_serialization.snap new file mode 100644 index 0000000000..e42c71605f --- /dev/null +++ b/state-chain/custom-rpc/src/snapshots/custom_rpc__test__lp_serialization.snap @@ -0,0 +1,5 @@ +--- +source: state-chain/custom-rpc/src/lib.rs +expression: "serde_json::to_value(lp).unwrap()" +--- +{"balances":{"Bitcoin":{"Btc":"0x0"},"Ethereum":{"Eth":"0xffffffffffffffffffffffffffffffff","Flip":"0x7fffffffffffffffffffffffffffffff"}},"flip_balance":"0x0","refund_addresses":{"Bitcoin":null,"Ethereum":"0x0101010101010101010101010101010101010101","Polkadot":"111111111111111111111111111111111HC1"},"role":"liquidity_provider"} diff --git a/state-chain/custom-rpc/src/snapshots/custom_rpc__test__no_account_serialization.snap b/state-chain/custom-rpc/src/snapshots/custom_rpc__test__no_account_serialization.snap new file mode 100644 index 0000000000..c360439549 --- /dev/null +++ b/state-chain/custom-rpc/src/snapshots/custom_rpc__test__no_account_serialization.snap @@ -0,0 +1,5 @@ +--- +source: state-chain/custom-rpc/src/lib.rs +expression: "serde_json::to_value(RpcAccountInfo::none(0)).unwrap()" +--- +{"flip_balance":"0x0","role":"none"} diff --git a/state-chain/custom-rpc/src/snapshots/custom_rpc__test__validator_serialization.snap b/state-chain/custom-rpc/src/snapshots/custom_rpc__test__validator_serialization.snap new file mode 100644 index 0000000000..8634b5ed60 --- /dev/null +++ b/state-chain/custom-rpc/src/snapshots/custom_rpc__test__validator_serialization.snap @@ -0,0 +1,5 @@ +--- +source: state-chain/custom-rpc/src/lib.rs +expression: "serde_json::to_value(validator).unwrap()" +--- +{"apy_bp":100,"bond":"0xde0b6b3a7640000","bound_redeem_address":"0x0101010101010101010101010101010101010101","flip_balance":"0xde0b6b3a7640000","is_bidding":false,"is_current_authority":true,"is_current_backup":false,"is_online":true,"is_qualified":true,"keyholder_epochs":[123],"last_heartbeat":0,"reputation_points":0,"restricted_balances":{"0x0101010101010101010101010101010101010101":"0xde0b6b3a7640000"},"role":"validator"} diff --git a/state-chain/runtime/src/lib.rs b/state-chain/runtime/src/lib.rs index b84ac9e394..696a299747 100644 --- a/state-chain/runtime/src/lib.rs +++ b/state-chain/runtime/src/lib.rs @@ -23,6 +23,7 @@ use cf_chains::{ evm::EvmCrypto, Bitcoin, CcmChannelMetadata, ForeignChain, Polkadot, }; +use cf_primitives::NetworkEnvironment; use core::ops::Range; pub use frame_system::Call as SystemCall; use pallet_cf_governance::GovCallHash; @@ -1039,18 +1040,34 @@ impl_runtime_apis! { LiquidityPools::pool_range_order_liquidity_value(base_asset, pair_asset, tick_range, liquidity) } - fn cf_environment() -> runtime_apis::Environment { - runtime_apis::Environment { - network: Environment::network_environment(), - ethereum_chain_id: Environment::ethereum_chain_id(), - polkadot_genesis_hash: Environment::polkadot_genesis_hash(), - } + fn cf_network_environment() -> NetworkEnvironment { + Environment::network_environment() } fn cf_min_swap_amount(asset: Asset) -> AssetAmount { Swapping::minimum_swap_amount(asset) } + fn cf_min_deposit_amount(asset: Asset) -> AssetAmount { + use pallet_cf_ingress_egress::MinimumDeposit; + use cf_chains::assets::{eth, dot, btc}; + + match ForeignChain::from(asset) { + ForeignChain::Ethereum => MinimumDeposit::::get( + eth::Asset::try_from(asset) + .expect("Conversion must succeed: ForeignChain checked in match clause.") + ), + ForeignChain::Polkadot => MinimumDeposit::::get( + dot::Asset::try_from(asset) + .expect("Conversion must succeed: ForeignChain checked in match clause.") + ), + ForeignChain::Bitcoin => MinimumDeposit::::get( + btc::Asset::try_from(asset) + .expect("Conversion must succeed: ForeignChain checked in match clause.") + ).into(), + } + } + fn cf_liquidity_provider_info( account_id: AccountId, ) -> Option { @@ -1079,6 +1096,10 @@ impl_runtime_apis! { pallet_cf_account_roles::AccountRoles::::get(account_id) } + fn cf_redemption_tax() -> AssetAmount { + pallet_cf_funding::RedemptionTax::::get() + } + /// This should *not* be fully trusted as if the deposits that are pre-witnessed will definitely go through. /// This returns a list of swaps in the requested direction that are pre-witnessed in the current block. fn cf_prewitness_swaps(from: Asset, to: Asset) -> Option> { diff --git a/state-chain/runtime/src/runtime_apis.rs b/state-chain/runtime/src/runtime_apis.rs index 7938f65179..1f084c9a6b 100644 --- a/state-chain/runtime/src/runtime_apis.rs +++ b/state-chain/runtime/src/runtime_apis.rs @@ -3,7 +3,7 @@ use cf_amm::{ common::{Amount, Price, Tick}, range_orders::Liquidity, }; -use cf_chains::{dot::PolkadotHash, eth::Address as EthereumAddress, ForeignChainAddress}; +use cf_chains::{eth::Address as EthereumAddress, ForeignChainAddress}; use cf_primitives::{ AccountRole, Asset, AssetAmount, EpochIndex, ForeignChain, NetworkEnvironment, SemVer, SwapOutput, @@ -66,13 +66,6 @@ pub struct AuctionState { pub auction_size_range: (u32, u32), } -#[derive(Encode, Decode, Eq, PartialEq, TypeInfo)] -pub struct Environment { - pub network: NetworkEnvironment, - pub ethereum_chain_id: cf_chains::evm::api::EvmChainId, - pub polkadot_genesis_hash: PolkadotHash, -} - #[derive(Encode, Decode, Eq, PartialEq, TypeInfo)] pub struct LiquidityProviderInfo { pub refund_addresses: Vec<(ForeignChain, Option)>, @@ -131,10 +124,12 @@ decl_runtime_apis!( tick_range: Range, liquidity: Liquidity, ) -> Option, DispatchError>>; - fn cf_environment() -> Environment; fn cf_min_swap_amount(asset: Asset) -> AssetAmount; + fn cf_min_deposit_amount(asset: Asset) -> AssetAmount; fn cf_prewitness_swaps(from: Asset, to: Asset) -> Option>; fn cf_liquidity_provider_info(account_id: AccountId32) -> Option; fn cf_account_role(account_id: AccountId32) -> Option; + fn cf_redemption_tax() -> AssetAmount; + fn cf_network_environment() -> NetworkEnvironment; } ); diff --git a/utilities/Cargo.toml b/utilities/Cargo.toml index 8097be5312..68b22e350a 100644 --- a/utilities/Cargo.toml +++ b/utilities/Cargo.toml @@ -31,6 +31,7 @@ tracing-subscriber = { version = "0.3", features = [ ], optional = true } pin-project = { version = "1.0.12", optional = true } warp = { version = "0.3.5", optional = true } +sp-core = { git = "https://github.com/chainflip-io/substrate.git", tag = "chainflip-monthly-2023-08+2", optional = true } sp-rpc = { git = "https://github.com/chainflip-io/substrate.git", tag = "chainflip-monthly-2023-08+2", optional = true } num-traits = { version = "0.2", optional = true } scopeguard = { version = "1.2.0" } @@ -43,6 +44,7 @@ regex = { version = "1", optional = true } url = { version = "2.4", optional = true } [dev-dependencies] +serde_json = "1.0" tempfile = "3.7.0" reqwest = { version = "0.11.4", features = ["rustls-tls"] } @@ -65,6 +67,7 @@ std = [ 'dep:tracing', 'dep:tracing-subscriber', 'dep:warp', + 'dep:sp-core', 'dep:sp-rpc', 'dep:num-traits', 'dep:jsonrpsee', diff --git a/utilities/src/with_std.rs b/utilities/src/with_std.rs index 6951598a11..ef51b74e11 100644 --- a/utilities/src/with_std.rs +++ b/utilities/src/with_std.rs @@ -16,6 +16,7 @@ pub mod unending_stream; pub use unending_stream::UnendingStream; pub mod logging; pub mod redact_endpoint_secret; +pub mod rpc; pub mod serde_helpers; mod cached_stream; diff --git a/utilities/src/with_std/rpc.rs b/utilities/src/with_std/rpc.rs new file mode 100644 index 0000000000..57ae093aee --- /dev/null +++ b/utilities/src/with_std/rpc.rs @@ -0,0 +1,91 @@ +use anyhow::anyhow; +use serde::{Deserialize, Serialize}; +use sp_core::U256; + +#[derive(Debug, PartialEq, Eq, Deserialize)] +#[serde(untagged)] +pub enum NumberOrHex { + Number(u64), + Hex(U256), +} + +impl Serialize for NumberOrHex { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + // JS numbers are 64-bit floats, so we need to use a string for numbers larger than 2^53 + &Self::Number(n) if n >= 2u64.pow(53) => U256::from(n).serialize(serializer), + Self::Number(n) => n.serialize(serializer), + Self::Hex(n) => n.serialize(serializer), + } + } +} + +macro_rules! impl_safe_number { + ( $( $int:ident ),+ ) => { + $( + impl From<$int> for NumberOrHex { + fn from(value: $int) -> Self { + Self::Number(value.into()) + } + } + )+ + } +} + +impl_safe_number!(u32, u64); + +macro_rules! impl_safe_hex { + ( $( $int:ident ),+ ) => { + $( + impl From<$int> for NumberOrHex { + fn from(value: $int) -> Self { + Self::Hex(value.into()) + } + } + )+ + } +} + +impl_safe_hex!(u128, U256); + +impl TryInto for NumberOrHex { + type Error = anyhow::Error; + + fn try_into(self) -> Result { + match self { + Self::Number(n) => Ok(n.into()), + Self::Hex(n) => n.try_into().map_err(|_| { + anyhow!("Error parsing amount. Please use a valid number or hex string as input.") + }), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + fn assert_deser(string: &str, value: NumberOrHex) { + assert_eq!(serde_json::to_string(&value).unwrap(), string); + assert_eq!(serde_json::from_str::(string).unwrap(), value); + } + + #[test] + fn test_serialization() { + assert_deser("\"0x20000000000000\"", NumberOrHex::Hex(2u64.pow(53).into())); + assert_deser("9007199254740991", NumberOrHex::Number(2u64.pow(53) - 1)); + assert_deser( + "\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"", + NumberOrHex::Hex(U256::MAX), + ); + assert_deser(r#""0x1234""#, NumberOrHex::Hex(0x1234.into())); + assert_deser(r#""0x0""#, NumberOrHex::Hex(0.into())); + assert_deser(r#"5"#, NumberOrHex::Number(5)); + assert_deser(r#"10000"#, NumberOrHex::Number(10000)); + assert_deser(r#"0"#, NumberOrHex::Number(0)); + assert_deser(r#"1000000000000"#, NumberOrHex::Number(1000000000000)); + } +}