diff --git a/state-chain/custom-rpc/src/lib.rs b/state-chain/custom-rpc/src/lib.rs index 35bc0567c7..f93866d063 100644 --- a/state-chain/custom-rpc/src/lib.rs +++ b/state-chain/custom-rpc/src/lib.rs @@ -13,8 +13,8 @@ use cf_chains::{ }; use cf_primitives::{ chains::assets::any::{self, AssetMap}, - AccountRole, Asset, AssetAmount, BlockNumber, BroadcastId, EpochIndex, ForeignChain, - NetworkEnvironment, SemVer, SwapId, SwapRequestId, + AccountRole, Asset, AssetAmount, BasisPoints, BlockNumber, BroadcastId, DcaParameters, + EpochIndex, ForeignChain, NetworkEnvironment, SemVer, SwapId, SwapRequestId, }; use cf_utilities::rpc::NumberOrHex; use core::ops::Range; @@ -420,6 +420,7 @@ pub struct RpcSwapOutputV2 { pub network_fee: RpcFee, pub ingress_fee: RpcFee, pub egress_fee: RpcFee, + pub broker_commission: RpcFee, } #[derive(Serialize, Deserialize, Clone)] @@ -747,6 +748,17 @@ pub trait CustomApi { additional_orders: Option>, at: Option, ) -> RpcResult; + #[method(name = "swap_rate_v3")] + fn cf_pool_swap_rate_v3( + &self, + from_asset: Asset, + to_asset: Asset, + amount: U256, + broker_commission: BasisPoints, + dca_parameters: Option, + additional_orders: Option>, + at: Option, + ) -> RpcResult; #[method(name = "required_asset_ratio_for_range_order")] fn cf_required_asset_ratio_for_range_order( &self, @@ -1370,6 +1382,28 @@ where amount: U256, additional_orders: Option>, at: Option, + ) -> RpcResult { + self.cf_pool_swap_rate_v3( + from_asset, + to_asset, + amount, + Default::default(), + None, + additional_orders, + at, + ) + .map(Into::into) + } + + fn cf_pool_swap_rate_v3( + &self, + from_asset: Asset, + to_asset: Asset, + amount: U256, + broker_commission: BasisPoints, + dca_parameters: Option, + additional_orders: Option>, + at: Option, ) -> RpcResult { self.with_runtime_api(at, |api, hash| { Ok::<_, CfApiError>( @@ -1380,7 +1414,7 @@ where amount .try_into() .map_err(|_| "Swap input amount too large.") - .and_then(|amount| { + .and_then(|amount: u128| { if amount == 0 { Err("Swap input amount cannot be zero.") } else { @@ -1390,43 +1424,49 @@ where .map_err(|s| { ErrorObject::owned(ErrorCode::InvalidParams.code(), s, None::<()>) })?, + broker_commission, + dca_parameters, additional_orders.map(|additional_orders| { additional_orders .into_iter() .map(|additional_order| { match additional_order { - SwapRateV2AdditionalOrder::LimitOrder { - base_asset, - quote_asset, - side, - tick, - sell_amount, - } => state_chain_runtime::runtime_apis::SimulateSwapAdditionalOrder::LimitOrder { - base_asset, - quote_asset, - side, - tick, - sell_amount: sell_amount.unique_saturated_into(), + SwapRateV2AdditionalOrder::LimitOrder { + base_asset, + quote_asset, + side, + tick, + sell_amount, + } => state_chain_runtime::runtime_apis::SimulateSwapAdditionalOrder::LimitOrder { + base_asset, + quote_asset, + side, + tick, + sell_amount: sell_amount.unique_saturated_into(), + } } - } }) .collect() }), )? - .map(|simulated_swap_info| RpcSwapOutputV2 { - intermediary: simulated_swap_info.intermediary.map(Into::into), - output: simulated_swap_info.output.into(), + .map(|simulated_swap_info_v2| RpcSwapOutputV2 { + intermediary: simulated_swap_info_v2.intermediary.map(Into::into), + output: simulated_swap_info_v2.output.into(), network_fee: RpcFee { asset: cf_primitives::STABLE_ASSET, - amount: simulated_swap_info.network_fee.into(), + amount: simulated_swap_info_v2.network_fee.into(), }, ingress_fee: RpcFee { asset: from_asset, - amount: simulated_swap_info.ingress_fee.into(), + amount: simulated_swap_info_v2.ingress_fee.into(), }, egress_fee: RpcFee { asset: to_asset, - amount: simulated_swap_info.egress_fee.into(), + amount: simulated_swap_info_v2.egress_fee.into(), + }, + broker_commission: RpcFee { + asset: cf_primitives::STABLE_ASSET, + amount: simulated_swap_info_v2.broker_fee.into(), }, })?, ) @@ -2244,6 +2284,7 @@ mod test { network_fee: RpcFee { asset: Asset::Usdc, amount: 1_000u128.into() }, ingress_fee: RpcFee { asset: Asset::Flip, amount: 500u128.into() }, egress_fee: RpcFee { asset: Asset::Eth, amount: 1_000_000u128.into() }, + broker_commission: RpcFee { asset: Asset::Usdc, amount: 100u128.into() }, }) .unwrap()); } diff --git a/state-chain/custom-rpc/src/snapshots/custom_rpc__test__swap_output_serialization.snap b/state-chain/custom-rpc/src/snapshots/custom_rpc__test__swap_output_serialization.snap index 157a5cc001..40e56c40d8 100644 --- a/state-chain/custom-rpc/src/snapshots/custom_rpc__test__swap_output_serialization.snap +++ b/state-chain/custom-rpc/src/snapshots/custom_rpc__test__swap_output_serialization.snap @@ -1,5 +1,5 @@ --- source: state-chain/custom-rpc/src/lib.rs -expression: "serde_json::to_value(swap_output).unwrap()" +expression: "serde_json::to_value(RpcSwapOutputV2\n{\n output: 1_000_000_000_000_000_000u128.into(), intermediary:\n Some(1_000_000u128.into()), network_fee: RpcFee\n { asset: Asset::Usdc, amount: 1_000u128.into() }, ingress_fee: RpcFee\n { asset: Asset::Flip, amount: 500u128.into() }, egress_fee: RpcFee\n { asset: Asset::Eth, amount: 1_000_000u128.into() }, broker_commission:\n RpcFee { asset: Asset::Usdc, amount: 100u128.into() },\n}).unwrap()" --- -{"egress_fee":{"amount":"0xf4240","asset":"ETH","chain":"Ethereum"},"ingress_fee":{"amount":"0x1f4","asset":"FLIP","chain":"Ethereum"},"intermediary":"0xf4240","network_fee":{"amount":"0x3e8","asset":"USDC","chain":"Ethereum"},"output":"0xde0b6b3a7640000"} +{"broker_commission":{"amount":"0x64","asset":"USDC","chain":"Ethereum"},"egress_fee":{"amount":"0xf4240","asset":"ETH","chain":"Ethereum"},"ingress_fee":{"amount":"0x1f4","asset":"FLIP","chain":"Ethereum"},"intermediary":"0xf4240","network_fee":{"amount":"0x3e8","asset":"USDC","chain":"Ethereum"},"output":"0xde0b6b3a7640000"} diff --git a/state-chain/pallets/cf-swapping/src/lib.rs b/state-chain/pallets/cf-swapping/src/lib.rs index 9187c3a9a1..1edce0d5d2 100644 --- a/state-chain/pallets/cf-swapping/src/lib.rs +++ b/state-chain/pallets/cf-swapping/src/lib.rs @@ -70,12 +70,12 @@ struct FeeTaken { } #[derive(CloneNoBound, DebugNoBound)] -struct SwapState { - swap: Swap, - network_fee_taken: Option, - broker_fee_taken: Option, - stable_amount: Option, - final_output: Option, +pub struct SwapState { + pub swap: Swap, + pub network_fee_taken: Option, + pub broker_fee_taken: Option, + pub stable_amount: Option, + pub final_output: Option, } impl SwapState { @@ -149,7 +149,7 @@ impl SwapState { #[repr(u8)] #[derive(Clone, DebugNoBound, PartialEq, Eq, Encode, Decode, TypeInfo)] #[scale_info(skip_type_params(T))] -enum FeeType { +pub enum FeeType { NetworkFee = 0, BrokerFee(Beneficiaries), } @@ -181,7 +181,7 @@ pub struct SwapLegInfo { } impl Swap { - fn new( + pub fn new( swap_id: SwapId, swap_request_id: SwapRequestId, from: Asset, @@ -202,7 +202,7 @@ impl Swap { } } -enum BatchExecutionError { +pub enum BatchExecutionError { SwapLegFailed { asset: Asset, direction: SwapLeg, @@ -1281,7 +1281,7 @@ pub mod pallet { } #[transactional] - fn try_execute_without_violations( + pub fn try_execute_without_violations( swaps: Vec>, ) -> Result>, BatchExecutionError> { let mut swaps: Vec<_> = swaps.into_iter().map(SwapState::new).collect(); diff --git a/state-chain/runtime/src/lib.rs b/state-chain/runtime/src/lib.rs index c70a746158..096d45023f 100644 --- a/state-chain/runtime/src/lib.rs +++ b/state-chain/runtime/src/lib.rs @@ -24,7 +24,7 @@ use crate::{ PendingBroadcasts, PendingTssCeremonies, RedemptionsInfo, SolanaNonces, }, runtime_apis::{ - runtime_decl_for_custom_runtime_api::CustomRuntimeApiV1, AuctionState, BoostPoolDepth, + runtime_decl_for_custom_runtime_api::CustomRuntimeApi, AuctionState, BoostPoolDepth, BoostPoolDetails, BrokerInfo, DispatchErrorWithMessage, FailingWitnessValidators, LiquidityProviderBoostPoolInfo, LiquidityProviderInfo, RuntimeApiPenalty, SimulateSwapAdditionalOrder, SimulatedSwapInformation, TaintedTransactionEvents, @@ -49,7 +49,10 @@ use cf_chains::{ sol::{SolAddress, SolanaCrypto}, Arbitrum, Bitcoin, DefaultRetryPolicy, ForeignChain, Polkadot, Solana, TransactionBuilder, }; -use cf_primitives::{BroadcastId, EpochIndex, NetworkEnvironment, STABLE_ASSET}; +use cf_primitives::{ + BasisPoints, Beneficiary, BroadcastId, DcaParameters, EpochIndex, NetworkEnvironment, + STABLE_ASSET, +}; use cf_traits::{ AdjustedFeeEstimationApi, AssetConverter, BalanceApi, DummyEgressSuccessWitnesser, DummyIngressSource, GetBlockHeight, NoLimit, SwapLimits, SwapLimitsProvider, @@ -66,6 +69,7 @@ use pallet_cf_pools::{ AskBidMap, AssetPair, HistoricalEarnedFees, OrderId, PoolLiquidity, PoolOrderbook, PoolPriceV1, PoolPriceV2, UnidirectionalPoolDepth, }; +use pallet_cf_swapping::{BatchExecutionError, FeeType, Swap}; use runtime_apis::ChainAccounts; use crate::{chainflip::EvmLimit, runtime_apis::TaintedTransactionEvent}; @@ -115,7 +119,7 @@ pub use sp_runtime::BuildStorage; use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, transaction_validity::{TransactionSource, TransactionValidity}, - ApplyExtrinsicResult, MultiSignature, + ApplyExtrinsicResult, DispatchError, MultiSignature, }; pub use sp_runtime::{Perbill, Permill}; use sp_std::prelude::*; @@ -1495,6 +1499,8 @@ impl_runtime_apis! { from: Asset, to: Asset, amount: AssetAmount, + broker_commission: BasisPoints, + dca_parameters: Option, additional_orders: Option>, ) -> Result { if let Some(additional_orders) = additional_orders { @@ -1570,20 +1576,62 @@ impl_runtime_apis! { let (amount_to_swap, ingress_fee) = remove_fees(IngressOrEgress::Ingress, from, amount); - let swap_output = Swapping::swap_with_network_fee( - from, - to, - amount_to_swap, - )?; + // Estimate swap result for a chunk, then extrapolate the result. + // If no DCA parameter is given, swap the entire amount with 1 chunk. + let number_of_chunks: u128 = dca_parameters.map(|dca|dca.number_of_chunks).unwrap_or(1u32).into(); + let amount_per_chunk = amount_to_swap / number_of_chunks; + + let swap_output_per_chunk = Swapping::try_execute_without_violations( + vec![ + Swap::new( + Default::default(), + Default::default(), + from, + to, + amount_per_chunk, + None, + vec![ + FeeType::NetworkFee, + FeeType::BrokerFee( + vec![Beneficiary { + account: AccountId::new([0xbb; 32]), + bps: broker_commission, + }] + .try_into() + .expect("Beneficiary with a length of 1 must be within length bound.") + ) + ], + ) + ], + ).map_err(|e| DispatchErrorWithMessage::Other(match e { + BatchExecutionError::SwapLegFailed { .. } => DispatchError::Other("Swap leg failed."), + BatchExecutionError::PriceViolation { .. } => DispatchError::Other("Price Violation: Some swaps failed due to Price Impact Limitations."), + BatchExecutionError::DispatchError { error } => error, + }))?; + + let ( + network_fee, + broker_fee, + intermediary, + output, + ) = { + ( + swap_output_per_chunk[0].network_fee_taken.unwrap_or_default() * number_of_chunks, + swap_output_per_chunk[0].broker_fee_taken.unwrap_or_default() * number_of_chunks, + swap_output_per_chunk[0].stable_amount.map(|amount| amount * number_of_chunks), + swap_output_per_chunk[0].final_output.unwrap_or_default() * number_of_chunks, + ) + }; - let (output, egress_fee) = remove_fees(IngressOrEgress::Egress, to, swap_output.output); + let (output, egress_fee) = remove_fees(IngressOrEgress::Egress, to, output); Ok(SimulatedSwapInformation { - intermediary: swap_output.intermediary, + intermediary, output, - network_fee: swap_output.network_fee, + network_fee, ingress_fee, egress_fee, + broker_fee, }) } @@ -1592,9 +1640,7 @@ impl_runtime_apis! { } fn cf_lp_events() -> Vec> { - System::read_events_no_consensus().filter_map(|event_record| { - if let RuntimeEvent::LiquidityPools(pools_event) = event_record.event { Some(pools_event) } else { @@ -2037,7 +2083,7 @@ impl_runtime_apis! { } fn cf_validate_dca_params(number_of_chunks: u32, chunk_interval: u32) -> Result<(), DispatchErrorWithMessage> { - pallet_cf_swapping::Pallet::::validate_dca_params(&cf_primitives::DcaParameters{number_of_chunks, chunk_interval}).map_err(Into::into) + pallet_cf_swapping::Pallet::::validate_dca_params(&DcaParameters{number_of_chunks, chunk_interval}).map_err(Into::into) } fn cf_validate_refund_params(retry_duration: u32) -> Result<(), DispatchErrorWithMessage> { diff --git a/state-chain/runtime/src/runtime_apis.rs b/state-chain/runtime/src/runtime_apis.rs index 05bd242d00..df3b9e1521 100644 --- a/state-chain/runtime/src/runtime_apis.rs +++ b/state-chain/runtime/src/runtime_apis.rs @@ -8,8 +8,8 @@ use cf_chains::{ ForeignChainAddress, }; use cf_primitives::{ - AccountRole, Asset, AssetAmount, BlockNumber, BroadcastId, EpochIndex, FlipBalance, - ForeignChain, NetworkEnvironment, PrewitnessedDepositId, SemVer, + AccountRole, Asset, AssetAmount, BasisPoints, BlockNumber, BroadcastId, DcaParameters, + EpochIndex, FlipBalance, ForeignChain, NetworkEnvironment, PrewitnessedDepositId, SemVer, }; use cf_traits::SwapLimits; use codec::{Decode, Encode}; @@ -146,6 +146,7 @@ pub struct SimulatedSwapInformation { pub network_fee: AssetAmount, pub ingress_fee: AssetAmount, pub egress_fee: AssetAmount, + pub broker_fee: AssetAmount, } #[derive(Debug, Decode, Encode, TypeInfo)] @@ -217,8 +218,23 @@ pub struct TaintedTransactionEvents { pub btc_events: Vec>, } +// READ THIS BEFORE UPDATING THIS TRAIT: +// +// ## When changing an existing method: +// - Bump the api_version of the trait, for example from #[api_version(2)] to #[api_version(3)]. +// - Annotate the old method with #[changed_in($VERSION)] where $VERSION is the *new* api_version, +// for example #[changed_in(3)]. +// - Handle the old method in the custom rpc implementation using runtime_api().api_version(). +// +// ## When adding a new method: +// - Bump the api_version of the trait, for example from #[api_version(2)] to #[api_version(3)]. +// - Create a dummy method with the same name, but no args and no return value. +// - Annotate the dummy method with #[changed_in($VERSION)] where $VERSION is the *new* +// api_version. +// - Handle the dummy method gracefully in the custom rpc implementation using +// runtime_api().api_version(). decl_runtime_apis!( - /// Definition for all runtime API interfaces. + #[api_version(2)] pub trait CustomRuntimeApi { /// Returns true if the current phase is the auction phase. fn cf_is_auction_phase() -> bool; @@ -252,12 +268,21 @@ decl_runtime_apis!( base_asset: Asset, quote_asset: Asset, ) -> Result; + #[changed_in(2)] fn cf_pool_simulate_swap( from: Asset, to: Asset, amount: AssetAmount, additional_limit_orders: Option>, ) -> Result; + fn cf_pool_simulate_swap( + from: Asset, + to: Asset, + amount: AssetAmount, + broker_commission: BasisPoints, + dca_parameters: Option, + additional_limit_orders: Option>, + ) -> Result; fn cf_pool_info( base_asset: Asset, quote_asset: Asset,