From 6401c482df8e5827cbf23574e05026eda83299b1 Mon Sep 17 00:00:00 2001 From: Jamie Ford Date: Tue, 5 Nov 2024 15:18:16 +1100 Subject: [PATCH 01/15] feat: new vault swap encoding rpc --- api/bin/chainflip-broker-api/src/main.rs | 47 +------ api/lib/src/lib.rs | 82 +----------- state-chain/custom-rpc/src/lib.rs | 90 ++++++++++++- .../pallets/cf-ingress-egress/src/lib.rs | 32 +++-- state-chain/pallets/cf-swapping/src/lib.rs | 8 ++ state-chain/runtime/src/lib.rs | 123 ++++++++++++++++-- state-chain/runtime/src/runtime_apis.rs | 29 ++++- 7 files changed, 254 insertions(+), 157 deletions(-) diff --git a/api/bin/chainflip-broker-api/src/main.rs b/api/bin/chainflip-broker-api/src/main.rs index 8a80b4aee1..5ac5c842ff 100644 --- a/api/bin/chainflip-broker-api/src/main.rs +++ b/api/bin/chainflip-broker-api/src/main.rs @@ -1,15 +1,13 @@ use cf_utilities::{ health::{self, HealthCheckOptions}, - rpc::NumberOrHex, task_scope::{task_scope, Scope}, - try_parse_number_or_hex, }; use chainflip_api::{ self, primitives::{AccountRole, Affiliates, Asset, BasisPoints, CcmChannelMetadata, DcaParameters}, settings::StateChain, AccountId32, AddressString, BrokerApi, OperatorApi, RefundParameters, StateChainApi, - SwapDepositAddress, SwapPayload, WithdrawFeesDetail, + SwapDepositAddress, WithdrawFeesDetail, }; use clap::Parser; use futures::FutureExt; @@ -86,20 +84,6 @@ pub trait Rpc { asset: Asset, destination_address: AddressString, ) -> RpcResult; - - #[method(name = "request_swap_parameter_encoding", aliases = ["broker_requestSwapParameterEncoding"])] - async fn request_swap_parameter_encoding( - &self, - source_asset: Asset, - destination_asset: Asset, - destination_address: AddressString, - broker_commission: BasisPoints, - min_output_amount: NumberOrHex, - retry_duration: u32, - boost_fee: Option, - affiliate_fees: Option>, - dca_parameters: Option, - ) -> RpcResult; } pub struct RpcServerImpl { @@ -165,35 +149,6 @@ impl RpcServer for RpcServerImpl { ) -> RpcResult { Ok(self.api.broker_api().withdraw_fees(asset, destination_address).await?) } - - async fn request_swap_parameter_encoding( - &self, - source_asset: Asset, - destination_asset: Asset, - destination_address: AddressString, - broker_commission: BasisPoints, - min_output_amount: NumberOrHex, - retry_duration: u32, - boost_fee: Option, - affiliate_fees: Option>, - dca_parameters: Option, - ) -> RpcResult { - Ok(self - .api - .broker_api() - .request_swap_parameter_encoding( - source_asset, - destination_asset, - destination_address, - broker_commission, - try_parse_number_or_hex(min_output_amount)?, - retry_duration, - boost_fee, - affiliate_fees, - dca_parameters, - ) - .await?) - } } #[derive(Parser, Debug, Clone, Default)] diff --git a/api/lib/src/lib.rs b/api/lib/src/lib.rs index 1c0e5c33f2..97c252bec6 100644 --- a/api/lib/src/lib.rs +++ b/api/lib/src/lib.rs @@ -4,9 +4,6 @@ use anyhow::{anyhow, bail, Context, Result}; use async_trait::async_trait; use cf_chains::{ address::{try_from_encoded_address, EncodedAddress}, - btc::vault_swap_encoding::{ - encode_swap_params_in_nulldata_utxo, SharedCfParameters, UtxoEncodedData, - }, dot::PolkadotAccountId, evm::to_evm_address, sol::SolAddress, @@ -14,7 +11,7 @@ use cf_chains::{ ForeignChain, ForeignChainAddress, }; pub use cf_primitives::{AccountRole, Affiliates, Asset, BasisPoints, ChannelId, SemVer}; -use cf_primitives::{AssetAmount, BlockNumber, DcaParameters, NetworkEnvironment}; +use cf_primitives::{DcaParameters, NetworkEnvironment}; use pallet_cf_account_roles::MAX_LENGTH_FOR_VANITY_NAME; use pallet_cf_governance::ExecutionMode; use serde::{Deserialize, Serialize}; @@ -111,11 +108,6 @@ pub async fn request_block( .ok_or_else(|| anyhow!("unknown block hash")) } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum SwapPayload { - Bitcoin { nulldata_utxo: Bytes }, -} - pub struct StateChainApi { pub state_chain_client: Arc, } @@ -510,78 +502,6 @@ pub trait BrokerApi: SignedExtrinsicApi + StorageApi + Sized + Send + Sync + 'st self.simple_submission_with_dry_run(pallet_cf_swapping::Call::deregister_as_broker {}) .await } - - async fn request_swap_parameter_encoding( - &self, - source_asset: Asset, - destination_asset: Asset, - destination_address: AddressString, - broker_commission: BasisPoints, - min_output_amount: AssetAmount, - retry_duration: BlockNumber, - boost_fee: Option, - affiliate_fees: Option>, - dca_parameters: Option, - ) -> Result { - // Check if safe mode is active - let block_hash = self.base_rpc_api().latest_finalized_block_hash().await?; - let safe_mode = self - .storage_value::>( - block_hash, - ) - .await?; - if !safe_mode.swapping.swaps_enabled { - bail!("Safe mode is active. Swaps are disabled."); - } - - // Validate params - frame_support::ensure!( - broker_commission == 0 && affiliate_fees.map_or(true, |fees| fees.is_empty()), - anyhow!("Broker/Affi fees are not yet supported for vault swaps. Request a deposit address or remove the broker fees.") - ); - self.base_rpc_api() - .validate_refund_params(retry_duration, Some(block_hash)) - .await?; - if let Some(params) = dca_parameters.as_ref() { - self.base_rpc_api() - .validate_dca_params( - params.number_of_chunks, - params.chunk_interval, - Some(block_hash), - ) - .await?; - } - - // Encode swap - match ForeignChain::from(source_asset) { - ForeignChain::Bitcoin => { - let params = UtxoEncodedData { - output_asset: destination_asset, - output_address: destination_address - .try_parse_to_encoded_address(destination_asset.into())?, - parameters: SharedCfParameters { - retry_duration: retry_duration.try_into()?, - min_output_amount, - number_of_chunks: dca_parameters - .as_ref() - .map(|params| params.number_of_chunks) - .unwrap_or(1) - .try_into()?, - chunk_interval: dca_parameters - .as_ref() - .map(|params| params.chunk_interval) - .unwrap_or(2) - .try_into()?, - boost_fee: boost_fee.unwrap_or_default().try_into()?, - }, - }; - Ok(SwapPayload::Bitcoin { - nulldata_utxo: encode_swap_params_in_nulldata_utxo(params).raw().into(), - }) - }, - _ => bail!("Unsupported input asset"), - } - } } #[async_trait] diff --git a/state-chain/custom-rpc/src/lib.rs b/state-chain/custom-rpc/src/lib.rs index a84e888a1f..0d6c8eda4b 100644 --- a/state-chain/custom-rpc/src/lib.rs +++ b/state-chain/custom-rpc/src/lib.rs @@ -9,12 +9,12 @@ use cf_chains::{ dot::PolkadotAccountId, eth::Address as EthereumAddress, sol::SolAddress, - Chain, + Chain, ForeignChainAddress, }; use cf_primitives::{ chains::assets::any::{self, AssetMap}, - AccountRole, Asset, AssetAmount, BlockNumber, BroadcastId, EpochIndex, ForeignChain, - NetworkEnvironment, SemVer, SwapId, SwapRequestId, + AccountRole, Affiliates, Asset, AssetAmount, BasisPoints, BlockNumber, BroadcastId, + DcaParameters, EpochIndex, ForeignChain, NetworkEnvironment, SemVer, SwapId, SwapRequestId, }; use cf_utilities::rpc::NumberOrHex; use core::ops::Range; @@ -37,7 +37,7 @@ use pallet_cf_swapping::SwapLegInfo; use sc_client_api::{BlockchainEvents, HeaderBackend}; use serde::{Deserialize, Serialize}; use sp_api::{ApiError, CallApiAt}; -use sp_core::U256; +use sp_core::{Bytes, U256}; use sp_runtime::{ traits::{Block as BlockT, Header as HeaderT, UniqueSaturatedInto}, Permill, @@ -53,8 +53,9 @@ use state_chain_runtime::{ }, runtime_apis::{ AuctionState, BoostPoolDepth, BoostPoolDetails, BrokerInfo, CustomRuntimeApi, - DispatchErrorWithMessage, ElectoralRuntimeApi, FailingWitnessValidators, - LiquidityProviderBoostPoolInfo, LiquidityProviderInfo, RuntimeApiPenalty, ValidatorInfo, + DispatchErrorWithMessage, ElectoralRuntimeApi, EncodedVaultSwapParams, + FailingWitnessValidators, LiquidityProviderBoostPoolInfo, LiquidityProviderInfo, + RuntimeApiPenalty, ValidatorInfo, VaultSwapDetails, }, safe_mode::RuntimeSafeMode, Hash, NetworkFee, SolanaInstance, @@ -68,6 +69,33 @@ use std::{ pub mod monitoring; pub mod order_fills; +#[derive(Clone, Serialize, Deserialize)] +pub struct VaultSwapDetailsHumanreadable { + pub deposit_address: ForeignChainAddressHumanreadable, + pub encoded_params: EncodedVaultSwapParamsBytes, +} + +#[derive(Clone, Serialize, Deserialize)] +pub enum EncodedVaultSwapParamsBytes { + Bitcoin { nulldata_utxo: Bytes }, +} + +impl VaultSwapDetailsHumanreadable { + fn from( + details: VaultSwapDetails, + network: NetworkEnvironment, + ) -> VaultSwapDetailsHumanreadable { + match details.encoded_params { + EncodedVaultSwapParams::Bitcoin { nulldata_utxo } => VaultSwapDetailsHumanreadable { + deposit_address: details.deposit_address.to_humanreadable(network), + encoded_params: EncodedVaultSwapParamsBytes::Bitcoin { + nulldata_utxo: nulldata_utxo.into(), + }, + }, + } + } +} + #[derive(Serialize, Deserialize, Clone)] pub struct RpcEpochState { pub blocks_per_epoch: u32, @@ -953,6 +981,22 @@ pub trait CustomApi { retry_duration: u32, at: Option, ) -> RpcResult<()>; + + #[method(name = "get_vault_swap_details")] + fn cf_get_vault_swap_details( + &self, + broker: state_chain_runtime::AccountId, + source_asset: Asset, + destination_asset: Asset, + destination_address: ForeignChainAddress, + broker_commission: BasisPoints, + min_output_amount: AssetAmount, + retry_duration: u32, + boost_fee: Option, + affiliate_fees: Option>, + dca_parameters: Option, + at: Option, + ) -> RpcResult; } /// An RPC extension for the state chain node. @@ -1742,6 +1786,40 @@ where ) -> RpcResult> { self.with_runtime_api(at, |api, hash| api.cf_filter_votes(hash, validator, proposed_votes)) } + + fn cf_get_vault_swap_details( + &self, + broker: state_chain_runtime::AccountId, + source_asset: Asset, + destination_asset: Asset, + destination_address: ForeignChainAddress, + broker_commission: BasisPoints, + min_output_amount: AssetAmount, + retry_duration: u32, + boost_fee: Option, + affiliate_fees: Option>, + dca_parameters: Option, + at: Option, + ) -> RpcResult { + self.with_runtime_api(at, |api, hash| { + Ok::<_, CfApiError>(VaultSwapDetailsHumanreadable::from( + api.cf_get_vault_swap_details( + hash, + broker, + source_asset, + destination_asset, + destination_address, + broker_commission, + min_output_amount, + retry_duration, + boost_fee, + affiliate_fees, + dca_parameters, + )??, + api.cf_network_environment(hash)?, + )) + }) + } } impl CustomRpc diff --git a/state-chain/pallets/cf-ingress-egress/src/lib.rs b/state-chain/pallets/cf-ingress-egress/src/lib.rs index 345639bcde..882cecabf1 100644 --- a/state-chain/pallets/cf-ingress-egress/src/lib.rs +++ b/state-chain/pallets/cf-ingress-egress/src/lib.rs @@ -1290,21 +1290,25 @@ pub mod pallet { dca_params: Option, boost_fee: BasisPoints, ) -> DispatchResult { - T::EnsureWitnessed::ensure_origin(origin)?; + if T::EnsurePrewitnessed::ensure_origin(origin.clone()).is_ok() { + // Pre-witnessed vault swaps are not supported yet. + } else { + T::EnsureWitnessed::ensure_origin(origin)?; - Self::process_vault_swap_request( - input_asset, - deposit_amount, - output_asset, - destination_address, - deposit_metadata, - tx_hash, - *deposit_details, - broker_fees, - refund_params.map(|boxed| *boxed), - dca_params, - boost_fee, - ); + Self::process_vault_swap_request( + input_asset, + deposit_amount, + output_asset, + destination_address, + deposit_metadata, + tx_hash, + *deposit_details, + broker_fees, + refund_params.map(|boxed| *boxed), + dca_params, + boost_fee, + ); + } Ok(()) } diff --git a/state-chain/pallets/cf-swapping/src/lib.rs b/state-chain/pallets/cf-swapping/src/lib.rs index 051ffc0cbd..8a82801639 100644 --- a/state-chain/pallets/cf-swapping/src/lib.rs +++ b/state-chain/pallets/cf-swapping/src/lib.rs @@ -685,6 +685,14 @@ pub mod pallet { InvalidDcaParameters, /// The provided Refund address cannot be decoded into ForeignChainAddress. InvalidRefundAddress, + /// The given boost fee is too large to fit in a u8. + BoostFeeTooHigh, + /// Destination address and asset are incompatible. + IncompatibleDestinationAddress, + /// Broker/Affiliate fees are not yet supported for vault swaps + VaultSwapBrokerFeesNotSupported, + /// Unsupported source asset for vault swap + UnsupportedSourceAsset, } #[pallet::genesis_config] diff --git a/state-chain/runtime/src/lib.rs b/state-chain/runtime/src/lib.rs index cc73d6b6bf..779e0f406d 100644 --- a/state-chain/runtime/src/lib.rs +++ b/state-chain/runtime/src/lib.rs @@ -26,9 +26,10 @@ use crate::{ }, runtime_apis::{ runtime_decl_for_custom_runtime_api::CustomRuntimeApiV1, AuctionState, BoostPoolDepth, - BoostPoolDetails, BrokerInfo, DispatchErrorWithMessage, FailingWitnessValidators, - LiquidityProviderBoostPoolInfo, LiquidityProviderInfo, RuntimeApiPenalty, - SimulateSwapAdditionalOrder, SimulatedSwapInformation, ValidatorInfo, + BoostPoolDetails, BrokerInfo, DispatchErrorWithMessage, EncodedVaultSwapParams, + FailingWitnessValidators, LiquidityProviderBoostPoolInfo, LiquidityProviderInfo, + RuntimeApiPenalty, SimulateSwapAdditionalOrder, SimulatedSwapInformation, ValidatorInfo, + VaultSwapDetails, }, }; use cf_amm::{ @@ -40,16 +41,27 @@ pub use cf_chains::instances::{ SolanaInstance, }; use cf_chains::{ + address::to_encoded_address, arb::api::ArbitrumApi, assets::any::{AssetMap, ForeignChainAndAsset}, - btc::{api::BitcoinApi, BitcoinCrypto, BitcoinRetryPolicy, ScriptPubkey}, + btc::{ + api::BitcoinApi, + vault_swap_encoding::{ + encode_swap_params_in_nulldata_utxo, SharedCfParameters, UtxoEncodedData, + }, + BitcoinCrypto, BitcoinRetryPolicy, ScriptPubkey, + }, dot::{self, PolkadotAccountId, PolkadotCrypto}, eth::{self, api::EthereumApi, Address as EthereumAddress, Ethereum}, evm::EvmCrypto, sol::{SolAddress, SolanaCrypto}, - Arbitrum, Bitcoin, DefaultRetryPolicy, ForeignChain, Polkadot, Solana, TransactionBuilder, + Arbitrum, Bitcoin, DefaultRetryPolicy, ForeignChain, ForeignChainAddress, Polkadot, Solana, + TransactionBuilder, +}; +use cf_primitives::{ + Affiliates, BasisPoints, BroadcastId, DcaParameters, EpochIndex, NetworkEnvironment, + STABLE_ASSET, }; -use cf_primitives::{BroadcastId, EpochIndex, NetworkEnvironment, STABLE_ASSET}; use cf_runtime_utilities::NoopRuntimeUpgrade; use cf_traits::{ AdjustedFeeEstimationApi, AssetConverter, BalanceApi, DummyEgressSuccessWitnesser, @@ -110,7 +122,7 @@ use sp_runtime::{ BlakeTwo256, Block as BlockT, ConvertInto, IdentifyAccount, NumberFor, One, OpaqueKeys, Saturating, UniqueSaturatedInto, Verify, }, - BoundedVec, + BoundedVec, DispatchError, }; use frame_support::genesis_builder_helper::build_state; @@ -2094,12 +2106,107 @@ 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> { pallet_cf_swapping::Pallet::::validate_refund_params(retry_duration).map_err(Into::into) } + + fn cf_get_vault_swap_details( + _broker: AccountId, + source_asset: Asset, + destination_asset: Asset, + destination_address: ForeignChainAddress, + broker_commission: BasisPoints, + min_output_amount: AssetAmount, + retry_duration: u32, + boost_fee: Option, + affiliate_fees: Option>, + dca_parameters: Option, + ) -> Result { + // Validate params + if broker_commission != 0 || affiliate_fees.map_or(false, |fees| !fees.is_empty()) { + return Err(Into::::into(DispatchError::from( + pallet_cf_swapping::Error::::VaultSwapBrokerFeesNotSupported, + ))); + } + pallet_cf_swapping::Pallet::::validate_refund_params(retry_duration) + .map_err(Into::::into)?; + if let Some(params) = dca_parameters.as_ref() { + pallet_cf_swapping::Pallet::::validate_dca_params(params) + .map_err(Into::::into)?; + } + if ForeignChain::from(destination_asset) != destination_address.chain() { + return Err(Into::::into(DispatchError::from( + pallet_cf_swapping::Error::::IncompatibleDestinationAddress, + ))); + } + + // Encode swap + match ForeignChain::from(source_asset) { + ForeignChain::Bitcoin => { + let params = UtxoEncodedData { + output_asset: destination_asset, + output_address: to_encoded_address( + destination_address, + Environment::network_environment, + ), + parameters: SharedCfParameters { + retry_duration: retry_duration.try_into().map_err(|_| { + Into::::into(DispatchError::from( + pallet_cf_swapping::Error::::SwapRequestDurationTooLong, + )) + })?, + min_output_amount, + number_of_chunks: dca_parameters + .as_ref() + .map(|params| params.number_of_chunks) + .unwrap_or(1) + .try_into() + .map_err(|_| { + Into::::into(DispatchError::from( + pallet_cf_swapping::Error::::InvalidDcaParameters, + )) + })?, + chunk_interval: dca_parameters + .as_ref() + .map(|params| params.chunk_interval) + .unwrap_or(2) + .try_into() + .map_err(|_| { + Into::::into(DispatchError::from( + pallet_cf_swapping::Error::::InvalidDcaParameters, + )) + })?, + boost_fee: boost_fee.unwrap_or_default().try_into().map_err(|_| { + Into::::into(DispatchError::from( + pallet_cf_swapping::Error::::BoostFeeTooHigh, + )) + })?, + }, + }; + + // TODO: get private channel address. For now just return the btc vault address. + let btc_key = pallet_cf_threshold_signature::Pallet::::keys( + pallet_cf_threshold_signature::Pallet::::current_key_epoch().unwrap()).unwrap(); + let deposit_address = ForeignChainAddress::Btc( + cf_chains::btc::deposit_address::DepositAddress::new(btc_key.current, 0) + .script_pubkey(), + ); + + Ok(VaultSwapDetails { + deposit_address, + encoded_params: EncodedVaultSwapParams::Bitcoin { + nulldata_utxo: encode_swap_params_in_nulldata_utxo(params).raw().into(), + }, + }) + }, + _ => Err(Into::::into(DispatchError::from( + pallet_cf_swapping::Error::::UnsupportedSourceAsset, + ))), + } + } } impl monitoring_apis::MonitoringRuntimeApi for Runtime { diff --git a/state-chain/runtime/src/runtime_apis.rs b/state-chain/runtime/src/runtime_apis.rs index 536ebc0bfd..a6a58d6663 100644 --- a/state-chain/runtime/src/runtime_apis.rs +++ b/state-chain/runtime/src/runtime_apis.rs @@ -7,8 +7,9 @@ use cf_chains::{ assets::any::AssetMap, eth::Address as EthereumAddress, Chain, ForeignChainAddress, }; use cf_primitives::{ - AccountRole, Asset, AssetAmount, BlockNumber, BroadcastId, EpochIndex, FlipBalance, - ForeignChain, NetworkEnvironment, PrewitnessedDepositId, SemVer, + AccountRole, Affiliates, Asset, AssetAmount, BasisPoints, BlockNumber, BroadcastId, + DcaParameters, EpochIndex, FlipBalance, ForeignChain, NetworkEnvironment, + PrewitnessedDepositId, SemVer, }; use cf_traits::SwapLimits; use codec::{Decode, Encode}; @@ -30,8 +31,20 @@ use sp_std::{ collections::{btree_map::BTreeMap, btree_set::BTreeSet}, vec::Vec, }; + type VanityName = Vec; +#[derive(PartialEq, Eq, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize)] +pub struct VaultSwapDetails { + pub deposit_address: ForeignChainAddress, + pub encoded_params: EncodedVaultSwapParams, +} + +#[derive(PartialEq, Eq, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize)] +pub enum EncodedVaultSwapParams { + Bitcoin { nulldata_utxo: Vec }, +} + #[derive(PartialEq, Eq, Clone, Encode, Decode, Copy, TypeInfo, Serialize, Deserialize)] pub enum BackupOrPassive { Backup, @@ -306,6 +319,18 @@ decl_runtime_apis!( chunk_interval: u32, ) -> Result<(), DispatchErrorWithMessage>; fn cf_validate_refund_params(retry_duration: u32) -> Result<(), DispatchErrorWithMessage>; + fn cf_get_vault_swap_details( + broker: AccountId32, + source_asset: Asset, + destination_asset: Asset, + destination_address: ForeignChainAddress, + broker_commission: BasisPoints, + min_output_amount: AssetAmount, + retry_duration: u32, + boost_fee: Option, + affiliate_fees: Option>, + dca_parameters: Option, + ) -> Result; } ); From 8d9ff2a8ba6dd41e61bc7b512f79acb179c9b4c5 Mon Sep 17 00:00:00 2001 From: Jamie Ford Date: Tue, 5 Nov 2024 15:22:50 +1100 Subject: [PATCH 02/15] test: Btc vault swap bouncer test --- bouncer/commands/create_raw_btc_tx.ts | 39 ++------- bouncer/commands/run_test.ts | 2 + bouncer/shared/send_btc.ts | 59 +++++++++---- bouncer/tests/btc_vault_swap.ts | 121 ++++++++++++++++++++++++++ 4 files changed, 173 insertions(+), 48 deletions(-) create mode 100644 bouncer/tests/btc_vault_swap.ts diff --git a/bouncer/commands/create_raw_btc_tx.ts b/bouncer/commands/create_raw_btc_tx.ts index 2922e96c78..b4377fe920 100755 --- a/bouncer/commands/create_raw_btc_tx.ts +++ b/bouncer/commands/create_raw_btc_tx.ts @@ -2,49 +2,26 @@ // Constructs a very simple Raw BTC transaction. Can be used for manual testing a raw broadcast for example. // Usage: ./commands/create_raw_btc_tx.ts -import Client from 'bitcoin-core'; +import { BTC_ENDPOINT, btcClient, selectInputs } from '../shared/send_btc'; -const BTC_ENDPOINT = process.env.BTC_ENDPOINT || 'http://127.0.0.1:8332'; -console.log(`BTC_ENDPOINT is set to '${BTC_ENDPOINT}'`); - -const client = new Client({ - host: BTC_ENDPOINT.split(':')[1].slice(2), - port: Number(BTC_ENDPOINT.split(':')[2]), - username: 'flip', - password: 'flip', - wallet: 'whale', -}); +console.log(`Btc endpoint is set to '${BTC_ENDPOINT}'`); const createRawTransaction = async (toAddress: string, amountInBtc: number | string) => { try { + // Inputs and outputs const feeInBtc = 0.00001; - - // List unspent UTXOs - const utxos = await client.listUnspent(); - - const utxo = utxos.find((u) => u.amount >= Number(amountInBtc) + feeInBtc); - if (!utxo) throw new Error('Insufficient funds'); - - // Prepare the transaction inputs and outputs - const inputs = [ - { - txid: utxo.txid, - vout: utxo.vout, - }, - ]; - - const changeAmount = utxo.amount - Number(amountInBtc) - feeInBtc; - const changeAddress = await client.getNewAddress(); + const { inputs, change } = await selectInputs(Number(amountInBtc) + feeInBtc); + const changeAddress = await btcClient.getNewAddress(); const outputs = { [toAddress]: amountInBtc, - [changeAddress]: changeAmount, + [changeAddress]: change, }; // Create the raw transaction - const rawTx = await client.createRawTransaction(inputs, outputs); + const rawTx = await btcClient.createRawTransaction(inputs, outputs); // Sign the raw transaction - const signedTx = await client.signRawTransactionWithWallet(rawTx); + const signedTx = await btcClient.signRawTransactionWithWallet(rawTx); // Here's your raw signed transaction console.log('Raw signed transaction:', signedTx.hex); diff --git a/bouncer/commands/run_test.ts b/bouncer/commands/run_test.ts index fe73e8618d..f13caed00f 100755 --- a/bouncer/commands/run_test.ts +++ b/bouncer/commands/run_test.ts @@ -40,6 +40,7 @@ import { testDeltaBasedIngress } from '../tests/delta_based_ingress'; import { testCancelOrdersBatch } from '../tests/create_and_delete_multiple_orders'; import { depositChannelCreation } from '../tests/request_swap_deposit_address_with_affiliates'; import { testDCASwaps } from '../tests/DCA_test'; +import { testBtcVaultSwap } from '../tests/btc_vault_swap'; async function main() { const testName = process.argv[2]; @@ -67,6 +68,7 @@ async function main() { testDeltaBasedIngress, testCancelOrdersBatch, depositChannelCreation, + testBtcVaultSwap, ]; // Help message diff --git a/bouncer/shared/send_btc.ts b/bouncer/shared/send_btc.ts index 8f674dfdd1..92b4502c47 100644 --- a/bouncer/shared/send_btc.ts +++ b/bouncer/shared/send_btc.ts @@ -1,30 +1,55 @@ import Client from 'bitcoin-core'; import { sleep, btcClientMutex } from './utils'; -export async function sendBtc(address: string, amount: number | string) { - const BTC_ENDPOINT = process.env.BTC_ENDPOINT || 'http://127.0.0.1:8332'; - const client = new Client({ - host: BTC_ENDPOINT.split(':')[1].slice(2), - port: Number(BTC_ENDPOINT.split(':')[2]), - username: 'flip', - password: 'flip', - wallet: 'whale', - }); +export const BTC_ENDPOINT = process.env.BTC_ENDPOINT || 'http://127.0.0.1:8332'; - // Btc client has a limit on the number of concurrent requests - const txid = await btcClientMutex.runExclusive(async () => - client.sendToAddress(address, amount, '', '', false, true, null, 'unset', null, 1), - ); +export const btcClient = new Client({ + host: BTC_ENDPOINT.split(':')[1].slice(2), + port: Number(BTC_ENDPOINT.split(':')[2]), + username: 'flip', + password: 'flip', + wallet: 'whale', +}); - for (let i = 0; i < 50; i++) { - const transactionDetails = await client.getTransaction(txid); +export async function selectInputs(amount: number) { + // List unspent UTXOs + const utxos = await btcClient.listUnspent(); + + // Find a UTXO with enough funds + const utxo = utxos.find((u) => u.amount >= amount); + if (!utxo) throw new Error('Insufficient funds'); + // TODO: be able to select more than one UTXO + + const change = utxo.amount - amount; + + // Prepare the transaction inputs and outputs + const inputs = [ + { + txid: utxo.txid, + vout: utxo.vout, + }, + ]; + + return { inputs, change }; +} - const confirmations = transactionDetails.confirmations; +export async function waitForBtcTransaction(txid: string, confirmations = 1) { + for (let i = 0; i < 50; i++) { + const transactionDetails = await btcClient.getTransaction(txid); - if (confirmations < 1) { + if (transactionDetails.confirmations < confirmations) { await sleep(1000); } else { return; } } } + +export async function sendBtc(address: string, amount: number | string) { + // Btc client has a limit on the number of concurrent requests + const txid = (await btcClientMutex.runExclusive(async () => + btcClient.sendToAddress(address, amount, '', '', false, true, null, 'unset', null, 1), + )) as string; + + await waitForBtcTransaction(txid); +} diff --git a/bouncer/tests/btc_vault_swap.ts b/bouncer/tests/btc_vault_swap.ts new file mode 100644 index 0000000000..64fe486f67 --- /dev/null +++ b/bouncer/tests/btc_vault_swap.ts @@ -0,0 +1,121 @@ +import { ExecutableTest } from '../shared/executable_test'; +import { BTC_ENDPOINT, selectInputs, waitForBtcTransaction, btcClient } from '../shared/send_btc'; +import { + amountToFineAmount, + Asset, + assetDecimals, + btcClientMutex, + createStateChainKeypair, + newAddress, + shortChainFromAsset, +} from '../shared/utils'; +import { getChainflipApi, observeEvent } from '../shared/utils/substrate'; + +/* eslint-disable @typescript-eslint/no-use-before-define */ +export const testBtcVaultSwap = new ExecutableTest('Btc-Vault-Swap', main, 60); + +interface EncodedSwapRequest { + Bitcoin: { + nulldata_utxo: string; + }; +} + +interface VaultSwapDetails { + deposit_address: string; + encoded_params: EncodedSwapRequest; +} + +async function buildAndSendBtcVaultSwap( + depositAmountBtc: number, + brokerUri: string, + destinationAsset: Asset, + destinationAddress: string, + refundAddress: string, +) { + await using chainflip = await getChainflipApi(); + + const broker = createStateChainKeypair(brokerUri); + testBtcVaultSwap.debugLog(`Btc endpoint is set to`, BTC_ENDPOINT); + + const feeBtc = 0.00001; + const { inputs, change } = await selectInputs(Number(depositAmountBtc) + feeBtc); + + const vaultSwapDetails = (await chainflip.rpc( + `cf_get_vault_swap_details`, + broker.address, + 'BTC', // source_asset + destinationAsset.toUpperCase(), + { [shortChainFromAsset(destinationAsset)]: destinationAddress }, + 0, // broker_commission + 0, // min_output_amount + 0, // retry_duration + )) as unknown as VaultSwapDetails; + testBtcVaultSwap.debugLog( + 'nulldata_utxo:', + vaultSwapDetails.encoded_params.Bitcoin.nulldata_utxo, + ); + + // The `createRawTransaction` function will add the op codes, so we have to remove them here. + const nullDataWithoutOpCodes = vaultSwapDetails.encoded_params.Bitcoin.nulldata_utxo + .replace('0x', '') + .substring(4); + + const outputs = [ + { + [vaultSwapDetails.deposit_address]: depositAmountBtc, + }, + { + data: nullDataWithoutOpCodes, + }, + { + [refundAddress]: change, + }, + ]; + + const rawTx = await btcClient.createRawTransaction(inputs, outputs, 0, false); + const signedTx = await btcClient.signRawTransactionWithWallet(rawTx); + const txid = await btcClientMutex.runExclusive(async () => + btcClient.sendRawTransaction(signedTx.hex), + ); + if (!txid) { + throw new Error('Broadcast failed'); + } else { + testBtcVaultSwap.log('Broadcast successful, txid:', txid); + } + + await waitForBtcTransaction(txid as string); + testBtcVaultSwap.debugLog('Transaction confirmed'); +} + +async function testVaultSwap(depositAmountBtc: number, brokerUri: string, destinationAsset: Asset) { + const destinationAddress = await newAddress(destinationAsset, 'BTC_VAULT_SWAP'); + testBtcVaultSwap.debugLog('destinationAddress:', destinationAddress); + const refundAddress = await newAddress('Btc', 'BTC_VAULT_SWAP_REFUND'); + testBtcVaultSwap.debugLog('Refund address:', refundAddress); + + const observeSwapExecutedEvent = observeEvent(`swapping:SwapExecuted`, { + test: (event) => + event.data.inputAsset === 'Btc' && + event.data.outputAsset === destinationAsset && + event.data.inputAmount.replace(/,/g, '') === + amountToFineAmount(depositAmountBtc.toString(), assetDecimals('Btc')), + }).event; + + await buildAndSendBtcVaultSwap( + depositAmountBtc, + brokerUri, + destinationAsset, + destinationAddress, + refundAddress, + ); + + testBtcVaultSwap.debugLog('Waiting for swap executed event'); + await observeSwapExecutedEvent; + testBtcVaultSwap.log(`✅ Btc -> ${destinationAsset} Vault Swap executed`); +} + +async function main() { + const depositAmount = 0.1; + + await testVaultSwap(depositAmount, '//BROKER_1', 'Flip'); +} From 58dbf0929a44f760bff4c6886422820af9f7928d Mon Sep 17 00:00:00 2001 From: Jamie Ford Date: Wed, 6 Nov 2024 16:11:43 +1100 Subject: [PATCH 03/15] refactor: address some PR comments --- state-chain/custom-rpc/src/lib.rs | 26 ++++------ state-chain/pallets/cf-swapping/src/lib.rs | 2 - state-chain/runtime/src/lib.rs | 55 ++++++++++------------ state-chain/runtime/src/runtime_apis.rs | 15 +++--- 4 files changed, 43 insertions(+), 55 deletions(-) diff --git a/state-chain/custom-rpc/src/lib.rs b/state-chain/custom-rpc/src/lib.rs index 0d6c8eda4b..c8a2728092 100644 --- a/state-chain/custom-rpc/src/lib.rs +++ b/state-chain/custom-rpc/src/lib.rs @@ -53,9 +53,9 @@ use state_chain_runtime::{ }, runtime_apis::{ AuctionState, BoostPoolDepth, BoostPoolDetails, BrokerInfo, CustomRuntimeApi, - DispatchErrorWithMessage, ElectoralRuntimeApi, EncodedVaultSwapParams, - FailingWitnessValidators, LiquidityProviderBoostPoolInfo, LiquidityProviderInfo, - RuntimeApiPenalty, ValidatorInfo, VaultSwapDetails, + DispatchErrorWithMessage, ElectoralRuntimeApi, FailingWitnessValidators, + LiquidityProviderBoostPoolInfo, LiquidityProviderInfo, RuntimeApiPenalty, ValidatorInfo, + VaultSwapDetails, }, safe_mode::RuntimeSafeMode, Hash, NetworkFee, SolanaInstance, @@ -70,14 +70,9 @@ pub mod monitoring; pub mod order_fills; #[derive(Clone, Serialize, Deserialize)] -pub struct VaultSwapDetailsHumanreadable { - pub deposit_address: ForeignChainAddressHumanreadable, - pub encoded_params: EncodedVaultSwapParamsBytes, -} - -#[derive(Clone, Serialize, Deserialize)] -pub enum EncodedVaultSwapParamsBytes { - Bitcoin { nulldata_utxo: Bytes }, +#[serde(tag = "chain")] +pub enum VaultSwapDetailsHumanreadable { + Bitcoin { nulldata_utxo: Bytes, deposit_address: ForeignChainAddressHumanreadable }, } impl VaultSwapDetailsHumanreadable { @@ -85,13 +80,12 @@ impl VaultSwapDetailsHumanreadable { details: VaultSwapDetails, network: NetworkEnvironment, ) -> VaultSwapDetailsHumanreadable { - match details.encoded_params { - EncodedVaultSwapParams::Bitcoin { nulldata_utxo } => VaultSwapDetailsHumanreadable { - deposit_address: details.deposit_address.to_humanreadable(network), - encoded_params: EncodedVaultSwapParamsBytes::Bitcoin { + match details { + VaultSwapDetails::Bitcoin { nulldata_utxo, deposit_address } => + VaultSwapDetailsHumanreadable::Bitcoin { nulldata_utxo: nulldata_utxo.into(), + deposit_address: deposit_address.to_humanreadable(network), }, - }, } } } diff --git a/state-chain/pallets/cf-swapping/src/lib.rs b/state-chain/pallets/cf-swapping/src/lib.rs index 1e5dfb3e28..5c745b3854 100644 --- a/state-chain/pallets/cf-swapping/src/lib.rs +++ b/state-chain/pallets/cf-swapping/src/lib.rs @@ -701,8 +701,6 @@ pub mod pallet { InvalidRefundAddress, /// The given boost fee is too large to fit in a u8. BoostFeeTooHigh, - /// Destination address and asset are incompatible. - IncompatibleDestinationAddress, /// Broker/Affiliate fees are not yet supported for vault swaps VaultSwapBrokerFeesNotSupported, /// Unsupported source asset for vault swap diff --git a/state-chain/runtime/src/lib.rs b/state-chain/runtime/src/lib.rs index 7341c39cc0..7bfa95ead7 100644 --- a/state-chain/runtime/src/lib.rs +++ b/state-chain/runtime/src/lib.rs @@ -26,10 +26,9 @@ use crate::{ }, runtime_apis::{ runtime_decl_for_custom_runtime_api::CustomRuntimeApiV1, AuctionState, BoostPoolDepth, - BoostPoolDetails, BrokerInfo, DispatchErrorWithMessage, EncodedVaultSwapParams, - FailingWitnessValidators, LiquidityProviderBoostPoolInfo, LiquidityProviderInfo, - RuntimeApiPenalty, SimulateSwapAdditionalOrder, SimulatedSwapInformation, ValidatorInfo, - VaultSwapDetails, + BoostPoolDetails, BrokerInfo, DispatchErrorWithMessage, FailingWitnessValidators, + LiquidityProviderBoostPoolInfo, LiquidityProviderInfo, RuntimeApiPenalty, + SimulateSwapAdditionalOrder, SimulatedSwapInformation, ValidatorInfo, VaultSwapDetails, }, }; use cf_amm::{ @@ -41,7 +40,7 @@ pub use cf_chains::instances::{ SolanaInstance, }; use cf_chains::{ - address::to_encoded_address, + address::{AddressConverter, EncodedAddress}, arb::api::ArbitrumApi, assets::any::{AssetMap, ForeignChainAndAsset}, btc::{ @@ -60,7 +59,7 @@ use cf_chains::{ }; use cf_primitives::{ Affiliates, BasisPoints, BroadcastId, DcaParameters, EpochIndex, NetworkEnvironment, - STABLE_ASSET, + STABLE_ASSET, SWAP_DELAY_BLOCKS, }; use cf_runtime_utilities::NoopRuntimeUpgrade; use cf_traits::{ @@ -122,7 +121,7 @@ use sp_runtime::{ BlakeTwo256, Block as BlockT, ConvertInto, IdentifyAccount, NumberFor, One, OpaqueKeys, Saturating, UniqueSaturatedInto, Verify, }, - BoundedVec, DispatchError, + BoundedVec, }; use frame_support::genesis_builder_helper::build_state; @@ -2128,9 +2127,9 @@ impl_runtime_apis! { ) -> Result { // Validate params if broker_commission != 0 || affiliate_fees.map_or(false, |fees| !fees.is_empty()) { - return Err(Into::::into(DispatchError::from( + return Err(DispatchErrorWithMessage::from_pallet_error( pallet_cf_swapping::Error::::VaultSwapBrokerFeesNotSupported, - ))); + )); } pallet_cf_swapping::Pallet::::validate_refund_params(retry_duration) .map_err(Into::::into)?; @@ -2139,25 +2138,25 @@ impl_runtime_apis! { .map_err(Into::::into)?; } if ForeignChain::from(destination_asset) != destination_address.chain() { - return Err(Into::::into(DispatchError::from( - pallet_cf_swapping::Error::::IncompatibleDestinationAddress, - ))); + return Err(DispatchErrorWithMessage::from_pallet_error( + pallet_cf_swapping::Error::::InvalidDestinationAddress, + )); } + // Encode swap match ForeignChain::from(source_asset) { ForeignChain::Bitcoin => { let params = UtxoEncodedData { output_asset: destination_asset, - output_address: to_encoded_address( + output_address: ChainAddressConverter::to_encoded_address( destination_address, - Environment::network_environment, ), parameters: SharedCfParameters { retry_duration: retry_duration.try_into().map_err(|_| { - Into::::into(DispatchError::from( + DispatchErrorWithMessage::from_pallet_error( pallet_cf_swapping::Error::::SwapRequestDurationTooLong, - )) + ) })?, min_output_amount, number_of_chunks: dca_parameters @@ -2166,24 +2165,24 @@ impl_runtime_apis! { .unwrap_or(1) .try_into() .map_err(|_| { - Into::::into(DispatchError::from( + DispatchErrorWithMessage::from_pallet_error( pallet_cf_swapping::Error::::InvalidDcaParameters, - )) + ) })?, chunk_interval: dca_parameters .as_ref() .map(|params| params.chunk_interval) - .unwrap_or(2) + .unwrap_or(SWAP_DELAY_BLOCKS) .try_into() .map_err(|_| { - Into::::into(DispatchError::from( + DispatchErrorWithMessage::from_pallet_error( pallet_cf_swapping::Error::::InvalidDcaParameters, - )) + ) })?, boost_fee: boost_fee.unwrap_or_default().try_into().map_err(|_| { - Into::::into(DispatchError::from( + DispatchErrorWithMessage::from_pallet_error( pallet_cf_swapping::Error::::BoostFeeTooHigh, - )) + ) })?, }, }; @@ -2196,16 +2195,14 @@ impl_runtime_apis! { .script_pubkey(), ); - Ok(VaultSwapDetails { + Ok(VaultSwapDetails::Bitcoin { + nulldata_utxo: encode_swap_params_in_nulldata_utxo(params).raw().into(), deposit_address, - encoded_params: EncodedVaultSwapParams::Bitcoin { - nulldata_utxo: encode_swap_params_in_nulldata_utxo(params).raw().into(), - }, }) }, - _ => Err(Into::::into(DispatchError::from( + _ => Err(DispatchErrorWithMessage::from_pallet_error( pallet_cf_swapping::Error::::UnsupportedSourceAsset, - ))), + )), } } } diff --git a/state-chain/runtime/src/runtime_apis.rs b/state-chain/runtime/src/runtime_apis.rs index a6a58d6663..b672b619d6 100644 --- a/state-chain/runtime/src/runtime_apis.rs +++ b/state-chain/runtime/src/runtime_apis.rs @@ -35,14 +35,8 @@ use sp_std::{ type VanityName = Vec; #[derive(PartialEq, Eq, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize)] -pub struct VaultSwapDetails { - pub deposit_address: ForeignChainAddress, - pub encoded_params: EncodedVaultSwapParams, -} - -#[derive(PartialEq, Eq, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize)] -pub enum EncodedVaultSwapParams { - Bitcoin { nulldata_utxo: Vec }, +pub enum VaultSwapDetails { + Bitcoin { nulldata_utxo: Vec, deposit_address: ForeignChainAddress }, } #[derive(PartialEq, Eq, Clone, Encode, Decode, Copy, TypeInfo, Serialize, Deserialize)] @@ -174,6 +168,11 @@ impl From for DispatchErrorWithMessage { } } } +impl DispatchErrorWithMessage { + pub fn from_pallet_error(e: impl Into) -> Self { + Self::from(e.into()) + } +} #[cfg(feature = "std")] impl core::fmt::Display for DispatchErrorWithMessage { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { From 5e2870ebc6264a535b89e3587738d0d8c76aad26 Mon Sep 17 00:00:00 2001 From: Jamie Ford Date: Wed, 6 Nov 2024 16:12:17 +1100 Subject: [PATCH 04/15] chore: rename bouncer evm vault swap file --- .../{vault_swap.ts => evm_vault_swap.ts} | 2 +- bouncer/shared/swapping.ts | 2 +- bouncer/tests/DCA_test.ts | 2 +- bouncer/tests/all_swaps.ts | 2 +- bouncer/tests/btc_vault_swap.ts | 45 +++++++++---------- bouncer/tests/fill_or_kill.ts | 2 +- 6 files changed, 25 insertions(+), 30 deletions(-) rename bouncer/shared/{vault_swap.ts => evm_vault_swap.ts} (99%) diff --git a/bouncer/shared/vault_swap.ts b/bouncer/shared/evm_vault_swap.ts similarity index 99% rename from bouncer/shared/vault_swap.ts rename to bouncer/shared/evm_vault_swap.ts index 2ee750c526..b18f754df1 100644 --- a/bouncer/shared/vault_swap.ts +++ b/bouncer/shared/evm_vault_swap.ts @@ -22,7 +22,7 @@ import { newAddress, } from './utils'; import { getBalance } from './get_balance'; -import { CcmDepositMetadata, DcaParams, FillOrKillParamsX128 } from '../shared/new_swap'; +import { CcmDepositMetadata, DcaParams, FillOrKillParamsX128 } from './new_swap'; import { SwapContext, SwapStatus } from './swap_context'; const erc20Assets: Asset[] = ['Flip', 'Usdc', 'Usdt', 'ArbUsdc']; diff --git a/bouncer/shared/swapping.ts b/bouncer/shared/swapping.ts index a77112c384..c7e9520493 100644 --- a/bouncer/shared/swapping.ts +++ b/bouncer/shared/swapping.ts @@ -16,7 +16,7 @@ import { } from '../shared/utils'; import { BtcAddressType } from '../shared/new_btc_address'; import { CcmDepositMetadata } from '../shared/new_swap'; -import { performVaultSwap } from '../shared/vault_swap'; +import { performVaultSwap } from './evm_vault_swap'; import { SwapContext, SwapStatus } from './swap_context'; enum SolidityType { diff --git a/bouncer/tests/DCA_test.ts b/bouncer/tests/DCA_test.ts index 71ee0070af..57b5d8c8d0 100644 --- a/bouncer/tests/DCA_test.ts +++ b/bouncer/tests/DCA_test.ts @@ -13,7 +13,7 @@ import { getBalance } from '../shared/get_balance'; import { ExecutableTest } from '../shared/executable_test'; import { requestNewSwap } from '../shared/perform_swap'; import { DcaParams, FillOrKillParamsX128 } from '../shared/new_swap'; -import { executeVaultSwap } from '../shared/vault_swap'; +import { executeVaultSwap } from '../shared/evm_vault_swap'; /* eslint-disable @typescript-eslint/no-use-before-define */ export const testDCASwaps = new ExecutableTest('DCA-Swaps', main, 150); diff --git a/bouncer/tests/all_swaps.ts b/bouncer/tests/all_swaps.ts index 21708fc17c..9f777ff8ef 100644 --- a/bouncer/tests/all_swaps.ts +++ b/bouncer/tests/all_swaps.ts @@ -1,5 +1,5 @@ import { InternalAsset as Asset, InternalAssets as Assets } from '@chainflip/cli'; -import { VaultSwapParams } from '../shared/vault_swap'; +import { VaultSwapParams } from '../shared/evm_vault_swap'; import { ExecutableTest } from '../shared/executable_test'; import { SwapParams } from '../shared/perform_swap'; import { newCcmMetadata, testSwap, testVaultSwap } from '../shared/swapping'; diff --git a/bouncer/tests/btc_vault_swap.ts b/bouncer/tests/btc_vault_swap.ts index 64fe486f67..c0fb5a2a8a 100644 --- a/bouncer/tests/btc_vault_swap.ts +++ b/bouncer/tests/btc_vault_swap.ts @@ -14,15 +14,13 @@ import { getChainflipApi, observeEvent } from '../shared/utils/substrate'; /* eslint-disable @typescript-eslint/no-use-before-define */ export const testBtcVaultSwap = new ExecutableTest('Btc-Vault-Swap', main, 60); -interface EncodedSwapRequest { - Bitcoin: { - nulldata_utxo: string; - }; -} - interface VaultSwapDetails { - deposit_address: string; - encoded_params: EncodedSwapRequest; + chain: { + Bitcoin: { + nulldata_utxo: string; + deposit_address: string; + }; + }; } async function buildAndSendBtcVaultSwap( @@ -40,25 +38,22 @@ async function buildAndSendBtcVaultSwap( const feeBtc = 0.00001; const { inputs, change } = await selectInputs(Number(depositAmountBtc) + feeBtc); - const vaultSwapDetails = (await chainflip.rpc( - `cf_get_vault_swap_details`, - broker.address, - 'BTC', // source_asset - destinationAsset.toUpperCase(), - { [shortChainFromAsset(destinationAsset)]: destinationAddress }, - 0, // broker_commission - 0, // min_output_amount - 0, // retry_duration - )) as unknown as VaultSwapDetails; - testBtcVaultSwap.debugLog( - 'nulldata_utxo:', - vaultSwapDetails.encoded_params.Bitcoin.nulldata_utxo, - ); + const vaultSwapDetails = ( + (await chainflip.rpc( + `cf_get_vault_swap_details`, + broker.address, + 'BTC', // source_asset + destinationAsset.toUpperCase(), + { [shortChainFromAsset(destinationAsset)]: destinationAddress }, + 0, // broker_commission + 0, // min_output_amount + 0, // retry_duration + )) as unknown as VaultSwapDetails + ).chain.Bitcoin; + testBtcVaultSwap.debugLog('nulldata_utxo:', vaultSwapDetails.nulldata_utxo); // The `createRawTransaction` function will add the op codes, so we have to remove them here. - const nullDataWithoutOpCodes = vaultSwapDetails.encoded_params.Bitcoin.nulldata_utxo - .replace('0x', '') - .substring(4); + const nullDataWithoutOpCodes = vaultSwapDetails.nulldata_utxo.replace('0x', '').substring(4); const outputs = [ { diff --git a/bouncer/tests/fill_or_kill.ts b/bouncer/tests/fill_or_kill.ts index 9c31a9177e..84b40b2dfb 100644 --- a/bouncer/tests/fill_or_kill.ts +++ b/bouncer/tests/fill_or_kill.ts @@ -15,7 +15,7 @@ import { getBalance } from '../shared/get_balance'; import { observeEvent } from '../shared/utils/substrate'; import { CcmDepositMetadata, FillOrKillParamsX128 } from '../shared/new_swap'; import { ExecutableTest } from '../shared/executable_test'; -import { executeVaultSwap } from '../shared/vault_swap'; +import { executeVaultSwap } from '../shared/evm_vault_swap'; import { newCcmMetadata } from '../shared/swapping'; /* eslint-disable @typescript-eslint/no-use-before-define */ From 124e5e12c0d53ac490ce3b8ca0536ff2dc3d9274 Mon Sep 17 00:00:00 2001 From: Jamie Ford Date: Thu, 7 Nov 2024 14:52:35 +1100 Subject: [PATCH 05/15] feat: RPC in broker api and address some PR comments --- api/bin/chainflip-broker-api/src/main.rs | 50 +++++- api/bin/chainflip-cli/src/main.rs | 2 +- api/lib/src/lib.rs | 145 ++++++++---------- api/lib/src/lp.rs | 14 +- bouncer/tests/btc_vault_swap.ts | 50 +++--- .../client/base_rpc_api.rs | 50 +++++- state-chain/chains/src/address.rs | 77 +++++++++- state-chain/custom-rpc/src/lib.rs | 14 +- state-chain/runtime/src/lib.rs | 24 +-- state-chain/runtime/src/runtime_apis.rs | 5 +- 10 files changed, 300 insertions(+), 131 deletions(-) diff --git a/api/bin/chainflip-broker-api/src/main.rs b/api/bin/chainflip-broker-api/src/main.rs index 5ac5c842ff..de71aec1ad 100644 --- a/api/bin/chainflip-broker-api/src/main.rs +++ b/api/bin/chainflip-broker-api/src/main.rs @@ -1,15 +1,18 @@ use cf_utilities::{ health::{self, HealthCheckOptions}, + rpc::NumberOrHex, task_scope::{task_scope, Scope}, + try_parse_number_or_hex, }; use chainflip_api::{ self, primitives::{AccountRole, Affiliates, Asset, BasisPoints, CcmChannelMetadata, DcaParameters}, settings::StateChain, - AccountId32, AddressString, BrokerApi, OperatorApi, RefundParameters, StateChainApi, - SwapDepositAddress, WithdrawFeesDetail, + AccountId32, AddressString, OperatorApi, RefundParameters, StateChainApi, SwapDepositAddress, + WithdrawFeesDetail, }; use clap::Parser; +use custom_rpc::VaultSwapDetailsHumanreadable; use futures::FutureExt; use jsonrpsee::{ core::{async_trait, ClientError}, @@ -84,6 +87,20 @@ pub trait Rpc { asset: Asset, destination_address: AddressString, ) -> RpcResult; + + #[method(name = "request_swap_parameter_encoding", aliases = ["broker_requestSwapParameterEncoding"])] + async fn request_swap_parameter_encoding( + &self, + source_asset: Asset, + destination_asset: Asset, + destination_address: AddressString, + broker_commission: BasisPoints, + min_output_amount: NumberOrHex, + retry_duration: u32, + boost_fee: Option, + affiliate_fees: Option>, + dca_parameters: Option, + ) -> RpcResult; } pub struct RpcServerImpl { @@ -149,6 +166,35 @@ impl RpcServer for RpcServerImpl { ) -> RpcResult { Ok(self.api.broker_api().withdraw_fees(asset, destination_address).await?) } + + async fn request_swap_parameter_encoding( + &self, + source_asset: Asset, + destination_asset: Asset, + destination_address: AddressString, + broker_commission: BasisPoints, + min_output_amount: NumberOrHex, + retry_duration: u32, + boost_fee: Option, + affiliate_fees: Option>, + dca_parameters: Option, + ) -> RpcResult { + Ok(self + .api + .broker_api() + .request_swap_parameter_encoding( + source_asset, + destination_asset, + destination_address, + broker_commission, + try_parse_number_or_hex(min_output_amount)?, + retry_duration, + boost_fee, + affiliate_fees, + dca_parameters, + ) + .await?) + } } #[derive(Parser, Debug, Clone, Default)] diff --git a/api/bin/chainflip-cli/src/main.rs b/api/bin/chainflip-cli/src/main.rs index c63ceb0f59..9061d60f56 100644 --- a/api/bin/chainflip-cli/src/main.rs +++ b/api/bin/chainflip-cli/src/main.rs @@ -7,7 +7,7 @@ use api::{ lp::LpApi, primitives::{EpochIndex, RedemptionAmount, FLIP_DECIMALS}, queries::QueryApi, - AccountId32, BrokerApi, GovernanceApi, KeyPair, OperatorApi, StateChainApi, ValidatorApi, + AccountId32, GovernanceApi, KeyPair, OperatorApi, StateChainApi, ValidatorApi, }; use bigdecimal::BigDecimal; use cf_chains::eth::Address as EthereumAddress; diff --git a/api/lib/src/lib.rs b/api/lib/src/lib.rs index 97c252bec6..955a6b0957 100644 --- a/api/lib/src/lib.rs +++ b/api/lib/src/lib.rs @@ -1,17 +1,15 @@ -use std::{fmt, str::FromStr, sync::Arc}; +use std::{fmt, sync::Arc}; use anyhow::{anyhow, bail, Context, Result}; use async_trait::async_trait; +pub use cf_chains::address::AddressString; use cf_chains::{ - address::{try_from_encoded_address, EncodedAddress}, - dot::PolkadotAccountId, - evm::to_evm_address, - sol::SolAddress, - CcmChannelMetadata, ChannelRefundParametersEncoded, ChannelRefundParametersGeneric, - ForeignChain, ForeignChainAddress, + evm::to_evm_address, CcmChannelMetadata, ChannelRefundParametersEncoded, + ChannelRefundParametersGeneric, ForeignChain, }; pub use cf_primitives::{AccountRole, Affiliates, Asset, BasisPoints, ChannelId, SemVer}; -use cf_primitives::{DcaParameters, NetworkEnvironment}; +use cf_primitives::{AssetAmount, DcaParameters}; +use custom_rpc::VaultSwapDetailsHumanreadable; use pallet_cf_account_roles::MAX_LENGTH_FOR_VANITY_NAME; use pallet_cf_governance::ExecutionMode; use serde::{Deserialize, Serialize}; @@ -49,7 +47,7 @@ pub mod queries; pub use chainflip_node::chain_spec::use_chainflip_account_id_encoding; -use cf_utilities::{clean_hex_address, rpc::NumberOrHex, task_scope::Scope}; +use cf_utilities::{rpc::NumberOrHex, task_scope::Scope}; use chainflip_engine::state_chain_observer::client::{ base_rpc_api::BaseRpcClient, extrinsic_api::signed::UntilInBlock, DefaultRpcClient, StateChainClient, @@ -143,8 +141,8 @@ impl StateChainApi { self.state_chain_client.clone() } - pub fn broker_api(&self) -> Arc { - self.state_chain_client.clone() + pub fn broker_api(&self) -> BrokerApi { + BrokerApi { state_chain_client: self.state_chain_client.clone() } } pub fn lp_api(&self) -> Arc { @@ -159,12 +157,6 @@ impl StateChainApi { #[async_trait] impl GovernanceApi for StateChainClient {} #[async_trait] -impl BrokerApi for StateChainClient { - fn base_rpc_api(&self) -> Arc { - self.base_rpc_client.clone() - } -} -#[async_trait] impl OperatorApi for StateChainClient {} #[async_trait] impl ValidatorApi for StateChainClient {} @@ -297,42 +289,6 @@ pub trait GovernanceApi: SignedExtrinsicApi { } } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AddressString(String); - -impl FromStr for AddressString { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - Ok(Self(s.to_string())) - } -} - -impl AddressString { - pub fn try_parse_to_encoded_address(self, chain: ForeignChain) -> Result { - clean_foreign_chain_address(chain, self.0.as_str()) - } - - pub fn try_parse_to_foreign_chain_address( - self, - chain: ForeignChain, - network: NetworkEnvironment, - ) -> Result { - try_from_encoded_address(self.try_parse_to_encoded_address(chain)?, move || network) - .map_err(|_| anyhow!("Failed to parse address")) - } - - pub fn from_encoded_address>(address: T) -> Self { - Self(address.borrow().to_string()) - } -} - -impl fmt::Display for AddressString { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - write!(f, "{}", self.0) - } -} - pub type RefundParameters = ChannelRefundParametersGeneric; #[derive(Serialize, Deserialize, Debug, Clone)] @@ -374,10 +330,12 @@ impl fmt::Display for WithdrawFeesDetail { } } -#[async_trait] -pub trait BrokerApi: SignedExtrinsicApi + StorageApi + Sized + Send + Sync + 'static { - fn base_rpc_api(&self) -> Arc; - async fn request_swap_deposit_address( +pub struct BrokerApi { + pub(crate) state_chain_client: Arc, +} + +impl BrokerApi { + pub async fn request_swap_deposit_address( &self, source_asset: Asset, destination_asset: Asset, @@ -389,9 +347,11 @@ pub trait BrokerApi: SignedExtrinsicApi + StorageApi + Sized + Send + Sync + 'st refund_parameters: Option, dca_parameters: Option, ) -> Result { - let destination_address = - destination_address.try_parse_to_encoded_address(destination_asset.into())?; + let destination_address = destination_address + .try_parse_to_encoded_address(destination_asset.into()) + .map_err(anyhow::Error::msg)?; let (_tx_hash, events, header, ..) = self + .state_chain_client .submit_signed_extrinsic_with_dry_run( pallet_cf_swapping::Call::request_swap_deposit_address_with_affiliates { source_asset, @@ -407,7 +367,8 @@ pub trait BrokerApi: SignedExtrinsicApi + StorageApi + Sized + Send + Sync + 'st retry_duration: rpc_params.retry_duration, refund_address: rpc_params .refund_address - .try_parse_to_encoded_address(source_asset.into())?, + .try_parse_to_encoded_address(source_asset.into()) + .map_err(anyhow::Error::msg)?, min_price: rpc_params.min_price, }) }) @@ -452,16 +413,18 @@ pub trait BrokerApi: SignedExtrinsicApi + StorageApi + Sized + Send + Sync + 'st bail!("No SwapDepositAddressReady event was found"); } } - async fn withdraw_fees( + pub async fn withdraw_fees( &self, asset: Asset, destination_address: AddressString, ) -> Result { let (tx_hash, events, ..) = self + .state_chain_client .submit_signed_extrinsic(RuntimeCall::from(pallet_cf_swapping::Call::withdraw { asset, destination_address: destination_address - .try_parse_to_encoded_address(asset.into())?, + .try_parse_to_encoded_address(asset.into()) + .map_err(anyhow::Error::msg)?, })) .await .until_in_block() @@ -494,14 +457,47 @@ pub trait BrokerApi: SignedExtrinsicApi + StorageApi + Sized + Send + Sync + 'st bail!("No WithdrawalRequested event was found"); } } - async fn register_account(&self) -> Result { - self.simple_submission_with_dry_run(pallet_cf_swapping::Call::register_as_broker {}) + pub async fn register_account(&self) -> Result { + self.state_chain_client + .simple_submission_with_dry_run(pallet_cf_swapping::Call::register_as_broker {}) .await } - async fn deregister_account(&self) -> Result { - self.simple_submission_with_dry_run(pallet_cf_swapping::Call::deregister_as_broker {}) + pub async fn deregister_account(&self) -> Result { + self.state_chain_client + .simple_submission_with_dry_run(pallet_cf_swapping::Call::deregister_as_broker {}) .await } + + pub async fn request_swap_parameter_encoding( + &self, + source_asset: Asset, + destination_asset: Asset, + destination_address: AddressString, + broker_commission: BasisPoints, + min_output_amount: AssetAmount, + retry_duration: u32, + boost_fee: Option, + affiliate_fees: Option>, + dca_parameters: Option, + ) -> Result { + Ok(self + .state_chain_client + .base_rpc_client + .cf_get_vault_swap_details( + self.state_chain_client.account_id(), + source_asset, + destination_asset, + destination_address, + broker_commission, + min_output_amount, + retry_duration, + boost_fee, + affiliate_fees, + dca_parameters, + None, + ) + .await?) + } } #[async_trait] @@ -519,22 +515,6 @@ pub trait SimpleSubmissionApi: SignedExtrinsicApi { #[async_trait] impl SimpleSubmissionApi for T {} -/// Sanitize the given address (hex or base58) and turn it into a EncodedAddress of the given -/// chain. -pub fn clean_foreign_chain_address(chain: ForeignChain, address: &str) -> Result { - Ok(match chain { - ForeignChain::Ethereum => EncodedAddress::Eth(clean_hex_address(address)?), - ForeignChain::Polkadot => - EncodedAddress::Dot(PolkadotAccountId::from_str(address).map(|id| *id.aliased_ref())?), - ForeignChain::Bitcoin => EncodedAddress::Btc(address.as_bytes().to_vec()), - ForeignChain::Arbitrum => EncodedAddress::Arb(clean_hex_address(address)?), - ForeignChain::Solana => match SolAddress::from_str(address) { - Ok(sol_address) => EncodedAddress::Sol(sol_address.into()), - Err(_) => EncodedAddress::Sol(clean_hex_address(address)?), - }, - }) -} - #[derive(Debug, Zeroize, PartialEq, Eq)] /// Public and Secret keys as bytes pub struct KeyPair { @@ -640,6 +620,7 @@ mod tests { mod key_generation { use super::*; + use cf_chains::address::clean_foreign_chain_address; use sp_core::crypto::Ss58Codec; #[test] diff --git a/api/lib/src/lp.rs b/api/lib/src/lp.rs index 282874161e..c47197adbd 100644 --- a/api/lib/src/lp.rs +++ b/api/lib/src/lp.rs @@ -1,5 +1,3 @@ -use crate::AddressString; - use super::SimpleSubmissionApi; use anyhow::{bail, Result}; use async_trait::async_trait; @@ -7,7 +5,10 @@ pub use cf_amm::{ common::{Amount, PoolPairsMap, Side, Tick}, range_orders::Liquidity, }; -use cf_chains::{address::EncodedAddress, ForeignChain}; +use cf_chains::{ + address::{AddressString, EncodedAddress}, + ForeignChain, +}; use cf_primitives::{AccountId, Asset, AssetAmount, BasisPoints, BlockNumber, EgressId}; use chainflip_engine::state_chain_observer::client::{ extrinsic_api::signed::{SignedExtrinsicApi, UntilInBlock, WaitFor, WaitForResult}, @@ -197,7 +198,9 @@ pub trait LpApi: SignedExtrinsicApi + Sized + Send + Sync + 'static { let (tx_hash, ..) = self .submit_signed_extrinsic(RuntimeCall::from( pallet_cf_lp::Call::register_liquidity_refund_address { - address: address.try_parse_to_encoded_address(chain)?, + address: address + .try_parse_to_encoded_address(chain) + .map_err(anyhow::Error::msg)?, }, )) .await @@ -263,7 +266,8 @@ pub trait LpApi: SignedExtrinsicApi + Sized + Send + Sync + 'static { amount, asset, destination_address: destination_address - .try_parse_to_encoded_address(asset.into())?, + .try_parse_to_encoded_address(asset.into()) + .map_err(anyhow::Error::msg)?, }, wait_for, ) diff --git a/bouncer/tests/btc_vault_swap.ts b/bouncer/tests/btc_vault_swap.ts index c0fb5a2a8a..69cd237c68 100644 --- a/bouncer/tests/btc_vault_swap.ts +++ b/bouncer/tests/btc_vault_swap.ts @@ -1,3 +1,4 @@ +import assert from 'assert'; import { ExecutableTest } from '../shared/executable_test'; import { BTC_ENDPOINT, selectInputs, waitForBtcTransaction, btcClient } from '../shared/send_btc'; import { @@ -7,20 +8,18 @@ import { btcClientMutex, createStateChainKeypair, newAddress, - shortChainFromAsset, + observeBalanceIncrease, } from '../shared/utils'; import { getChainflipApi, observeEvent } from '../shared/utils/substrate'; +import { getBalance } from '../shared/get_balance'; /* eslint-disable @typescript-eslint/no-use-before-define */ -export const testBtcVaultSwap = new ExecutableTest('Btc-Vault-Swap', main, 60); +export const testBtcVaultSwap = new ExecutableTest('Btc-Vault-Swap', main, 100); interface VaultSwapDetails { - chain: { - Bitcoin: { - nulldata_utxo: string; - deposit_address: string; - }; - }; + chain: string; + nulldata_utxo: string; + deposit_address: string; } async function buildAndSendBtcVaultSwap( @@ -33,23 +32,24 @@ async function buildAndSendBtcVaultSwap( await using chainflip = await getChainflipApi(); const broker = createStateChainKeypair(brokerUri); + testBtcVaultSwap.debugLog('Broker:', broker.address); testBtcVaultSwap.debugLog(`Btc endpoint is set to`, BTC_ENDPOINT); const feeBtc = 0.00001; const { inputs, change } = await selectInputs(Number(depositAmountBtc) + feeBtc); - const vaultSwapDetails = ( - (await chainflip.rpc( - `cf_get_vault_swap_details`, - broker.address, - 'BTC', // source_asset - destinationAsset.toUpperCase(), - { [shortChainFromAsset(destinationAsset)]: destinationAddress }, - 0, // broker_commission - 0, // min_output_amount - 0, // retry_duration - )) as unknown as VaultSwapDetails - ).chain.Bitcoin; + const vaultSwapDetails = (await chainflip.rpc( + `cf_get_vault_swap_details`, + broker.address, + 'BTC', // source_asset + destinationAsset.toUpperCase(), + destinationAddress, + 0, // broker_commission + 0, // min_output_amount + 0, // retry_duration + )) as unknown as VaultSwapDetails; + + assert.strictEqual(vaultSwapDetails.chain, 'Bitcoin'); testBtcVaultSwap.debugLog('nulldata_utxo:', vaultSwapDetails.nulldata_utxo); // The `createRawTransaction` function will add the op codes, so we have to remove them here. @@ -87,6 +87,7 @@ async function testVaultSwap(depositAmountBtc: number, brokerUri: string, destin testBtcVaultSwap.debugLog('destinationAddress:', destinationAddress); const refundAddress = await newAddress('Btc', 'BTC_VAULT_SWAP_REFUND'); testBtcVaultSwap.debugLog('Refund address:', refundAddress); + const destinationAmountBeforeSwap = await getBalance(destinationAsset, destinationAddress); const observeSwapExecutedEvent = observeEvent(`swapping:SwapExecuted`, { test: (event) => @@ -106,11 +107,14 @@ async function testVaultSwap(depositAmountBtc: number, brokerUri: string, destin testBtcVaultSwap.debugLog('Waiting for swap executed event'); await observeSwapExecutedEvent; - testBtcVaultSwap.log(`✅ Btc -> ${destinationAsset} Vault Swap executed`); + testBtcVaultSwap.log(`Btc -> ${destinationAsset} Vault Swap executed`); + + await observeBalanceIncrease(destinationAsset, destinationAddress, destinationAmountBeforeSwap); + testBtcVaultSwap.log(`Balance increased, Vault Swap Complete`); } async function main() { - const depositAmount = 0.1; + const btcDepositAmount = 0.1; - await testVaultSwap(depositAmount, '//BROKER_1', 'Flip'); + await testVaultSwap(btcDepositAmount, '//BROKER_1', 'Flip'); } diff --git a/engine/src/state_chain_observer/client/base_rpc_api.rs b/engine/src/state_chain_observer/client/base_rpc_api.rs index 73072e964b..559b4dc9e8 100644 --- a/engine/src/state_chain_observer/client/base_rpc_api.rs +++ b/engine/src/state_chain_observer/client/base_rpc_api.rs @@ -1,5 +1,7 @@ use async_trait::async_trait; +use cf_chains::address::AddressString; +use cf_primitives::{Affiliates, Asset, AssetAmount, BasisPoints, DcaParameters}; use jsonrpsee::core::client::{ClientT, Subscription, SubscriptionClientT}; use sc_transaction_pool_api::TransactionStatus; use sp_core::{ @@ -10,7 +12,7 @@ use sp_version::RuntimeVersion; use state_chain_runtime::SignedBlock; use codec::Encode; -use custom_rpc::CustomApiClient; +use custom_rpc::{CustomApiClient, VaultSwapDetailsHumanreadable}; use sc_rpc_api::{ author::AuthorApiClient, chain::ChainApiClient, @@ -181,6 +183,21 @@ pub trait BaseRpcApi { chunk_interval: u32, block_hash: Option, ) -> RpcResult<()>; + + async fn cf_get_vault_swap_details( + &self, + broker: state_chain_runtime::AccountId, + source_asset: Asset, + destination_asset: Asset, + destination_address: AddressString, + broker_commission: BasisPoints, + min_output_amount: AssetAmount, + retry_duration: u32, + boost_fee: Option, + affiliate_fees: Option>, + dca_parameters: Option, + block_hash: Option, + ) -> RpcResult; } pub struct BaseRpcClient { @@ -337,6 +354,37 @@ impl BaseRpcApi for BaseRpcClient, + affiliate_fees: Option>, + dca_parameters: Option, + block_hash: Option, + ) -> RpcResult { + self.raw_rpc_client + .cf_get_vault_swap_details( + broker, + source_asset, + destination_asset, + destination_address, + broker_commission, + min_output_amount, + retry_duration, + boost_fee, + affiliate_fees, + dca_parameters, + block_hash, + ) + .await + } } struct Params(Option>); diff --git a/state-chain/chains/src/address.rs b/state-chain/chains/src/address.rs index 2cddfa239e..f2a2d56543 100644 --- a/state-chain/chains/src/address.rs +++ b/state-chain/chains/src/address.rs @@ -97,7 +97,9 @@ impl ForeignChainAddress { } } -#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, TypeInfo, PartialOrd, Ord)] +#[derive( + Clone, Debug, PartialEq, Eq, Encode, Decode, TypeInfo, PartialOrd, Ord, Serialize, Deserialize, +)] pub enum EncodedAddress { Eth([u8; 20]), Dot([u8; 32]), @@ -383,6 +385,79 @@ impl ToHumanreadableAddress for ForeignChainAddress { } } +#[cfg(feature = "std")] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AddressString(String); + +#[cfg(feature = "std")] +impl sp_std::str::FromStr for AddressString { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + Ok(Self(s.to_string())) + } +} + +#[cfg(feature = "std")] +impl AddressString { + pub fn try_parse_to_encoded_address( + self, + chain: ForeignChain, + ) -> Result { + clean_foreign_chain_address(chain, self.0.as_str()) + } + + pub fn try_parse_to_foreign_chain_address( + self, + chain: ForeignChain, + network: NetworkEnvironment, + ) -> Result { + try_from_encoded_address(self.try_parse_to_encoded_address(chain)?, move || network) + .map_err(|_| "Failed to parse address".into()) + } + + pub fn from_encoded_address>(address: T) -> Self { + Self(address.borrow().to_string()) + } +} + +#[cfg(feature = "std")] +impl sp_std::fmt::Display for AddressString { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Sanitize the given address (hex or base58) and turn it into a EncodedAddress of the given +/// chain. +#[cfg(feature = "std")] +pub fn clean_foreign_chain_address( + chain: ForeignChain, + address: &str, +) -> Result { + use core::str::FromStr; + + use cf_utilities::clean_hex_address; + + Ok(match chain { + ForeignChain::Ethereum => + EncodedAddress::Eth(clean_hex_address(address).map_err(|_| "Invalid address")?), + ForeignChain::Polkadot => EncodedAddress::Dot( + PolkadotAccountId::from_str(address) + .map(|id| *id.aliased_ref()) + .map_err(|_| "Invalid address")?, + ), + ForeignChain::Bitcoin => EncodedAddress::Btc(address.as_bytes().to_vec()), + ForeignChain::Arbitrum => + EncodedAddress::Arb(clean_hex_address(address).map_err(|_| "Invalid address")?), + ForeignChain::Solana => match SolAddress::from_str(address) { + Ok(sol_address) => EncodedAddress::Sol(sol_address.into()), + Err(_) => + EncodedAddress::Sol(clean_hex_address(address).map_err(|_| "Invalid address")?), + }, + }) +} + #[test] fn encode_and_decode_address() { #[track_caller] diff --git a/state-chain/custom-rpc/src/lib.rs b/state-chain/custom-rpc/src/lib.rs index c8a2728092..d6707cd510 100644 --- a/state-chain/custom-rpc/src/lib.rs +++ b/state-chain/custom-rpc/src/lib.rs @@ -5,11 +5,11 @@ use cf_amm::{ range_orders::Liquidity, }; use cf_chains::{ - address::{ForeignChainAddressHumanreadable, ToHumanreadableAddress}, + address::{AddressString, ForeignChainAddressHumanreadable, ToHumanreadableAddress}, dot::PolkadotAccountId, eth::Address as EthereumAddress, sol::SolAddress, - Chain, ForeignChainAddress, + Chain, }; use cf_primitives::{ chains::assets::any::{self, AssetMap}, @@ -982,7 +982,7 @@ pub trait CustomApi { broker: state_chain_runtime::AccountId, source_asset: Asset, destination_asset: Asset, - destination_address: ForeignChainAddress, + destination_address: AddressString, broker_commission: BasisPoints, min_output_amount: AssetAmount, retry_duration: u32, @@ -1786,7 +1786,7 @@ where broker: state_chain_runtime::AccountId, source_asset: Asset, destination_asset: Asset, - destination_address: ForeignChainAddress, + destination_address: AddressString, broker_commission: BasisPoints, min_output_amount: AssetAmount, retry_duration: u32, @@ -1802,7 +1802,11 @@ where broker, source_asset, destination_asset, - destination_address, + destination_address + .try_parse_to_encoded_address(destination_asset.into()) + .map_err(|e| { + ErrorObject::owned(ErrorCode::InvalidParams.code(), e, None::<()>) + })?, broker_commission, min_output_amount, retry_duration, diff --git a/state-chain/runtime/src/lib.rs b/state-chain/runtime/src/lib.rs index 7bfa95ead7..b3625ba146 100644 --- a/state-chain/runtime/src/lib.rs +++ b/state-chain/runtime/src/lib.rs @@ -2117,7 +2117,7 @@ impl_runtime_apis! { _broker: AccountId, source_asset: Asset, destination_asset: Asset, - destination_address: ForeignChainAddress, + destination_address: EncodedAddress, broker_commission: BasisPoints, min_output_amount: AssetAmount, retry_duration: u32, @@ -2137,11 +2137,19 @@ impl_runtime_apis! { pallet_cf_swapping::Pallet::::validate_dca_params(params) .map_err(Into::::into)?; } - if ForeignChain::from(destination_asset) != destination_address.chain() { - return Err(DispatchErrorWithMessage::from_pallet_error( - pallet_cf_swapping::Error::::InvalidDestinationAddress, - )); - } + ChainAddressConverter::try_from_encoded_address(destination_address.clone()) + .and_then(|address| { + if ForeignChain::from(destination_asset) != address.chain() { + Err(()) + } else { + Ok(()) + } + }) + .map_err(|_| { + DispatchErrorWithMessage::from_pallet_error( + pallet_cf_swapping::Error::::InvalidDestinationAddress, + ) + })?; // Encode swap @@ -2149,9 +2157,7 @@ impl_runtime_apis! { ForeignChain::Bitcoin => { let params = UtxoEncodedData { output_asset: destination_asset, - output_address: ChainAddressConverter::to_encoded_address( - destination_address, - ), + output_address: destination_address, parameters: SharedCfParameters { retry_duration: retry_duration.try_into().map_err(|_| { DispatchErrorWithMessage::from_pallet_error( diff --git a/state-chain/runtime/src/runtime_apis.rs b/state-chain/runtime/src/runtime_apis.rs index b672b619d6..598ebbf6e6 100644 --- a/state-chain/runtime/src/runtime_apis.rs +++ b/state-chain/runtime/src/runtime_apis.rs @@ -4,7 +4,8 @@ use cf_amm::{ range_orders::Liquidity, }; use cf_chains::{ - assets::any::AssetMap, eth::Address as EthereumAddress, Chain, ForeignChainAddress, + address::EncodedAddress, assets::any::AssetMap, eth::Address as EthereumAddress, Chain, + ForeignChainAddress, }; use cf_primitives::{ AccountRole, Affiliates, Asset, AssetAmount, BasisPoints, BlockNumber, BroadcastId, @@ -322,7 +323,7 @@ decl_runtime_apis!( broker: AccountId32, source_asset: Asset, destination_asset: Asset, - destination_address: ForeignChainAddress, + destination_address: EncodedAddress, broker_commission: BasisPoints, min_output_amount: AssetAmount, retry_duration: u32, From 9b304ab84881b0e8d388cb1dd395a32d439ebedb Mon Sep 17 00:00:00 2001 From: Jamie Ford Date: Thu, 7 Nov 2024 16:31:50 +1100 Subject: [PATCH 06/15] refactor: go back to better error types on encoded address --- api/lib/src/lib.rs | 3 +-- state-chain/chains/src/address.rs | 24 +++++++++--------------- state-chain/custom-rpc/src/lib.rs | 6 +++++- state-chain/runtime/src/lib.rs | 2 +- 4 files changed, 16 insertions(+), 19 deletions(-) diff --git a/api/lib/src/lib.rs b/api/lib/src/lib.rs index 765a36cbe5..5867c4b5d8 100644 --- a/api/lib/src/lib.rs +++ b/api/lib/src/lib.rs @@ -373,8 +373,7 @@ impl BrokerApi { retry_duration: rpc_params.retry_duration, refund_address: rpc_params .refund_address - .try_parse_to_encoded_address(source_asset.into()) - .map_err(anyhow::Error::msg)?, + .try_parse_to_encoded_address(source_asset.into())?, min_price: rpc_params.min_price, }) }) diff --git a/state-chain/chains/src/address.rs b/state-chain/chains/src/address.rs index f2a2d56543..1359a11925 100644 --- a/state-chain/chains/src/address.rs +++ b/state-chain/chains/src/address.rs @@ -403,7 +403,7 @@ impl AddressString { pub fn try_parse_to_encoded_address( self, chain: ForeignChain, - ) -> Result { + ) -> anyhow::Result { clean_foreign_chain_address(chain, self.0.as_str()) } @@ -411,9 +411,9 @@ impl AddressString { self, chain: ForeignChain, network: NetworkEnvironment, - ) -> Result { + ) -> anyhow::Result { try_from_encoded_address(self.try_parse_to_encoded_address(chain)?, move || network) - .map_err(|_| "Failed to parse address".into()) + .map_err(|_| anyhow::anyhow!("Failed to parse address")) } pub fn from_encoded_address>(address: T) -> Self { @@ -434,26 +434,20 @@ impl sp_std::fmt::Display for AddressString { pub fn clean_foreign_chain_address( chain: ForeignChain, address: &str, -) -> Result { +) -> anyhow::Result { use core::str::FromStr; use cf_utilities::clean_hex_address; Ok(match chain { - ForeignChain::Ethereum => - EncodedAddress::Eth(clean_hex_address(address).map_err(|_| "Invalid address")?), - ForeignChain::Polkadot => EncodedAddress::Dot( - PolkadotAccountId::from_str(address) - .map(|id| *id.aliased_ref()) - .map_err(|_| "Invalid address")?, - ), + ForeignChain::Ethereum => EncodedAddress::Eth(clean_hex_address(address)?), + ForeignChain::Polkadot => + EncodedAddress::Dot(PolkadotAccountId::from_str(address).map(|id| *id.aliased_ref())?), ForeignChain::Bitcoin => EncodedAddress::Btc(address.as_bytes().to_vec()), - ForeignChain::Arbitrum => - EncodedAddress::Arb(clean_hex_address(address).map_err(|_| "Invalid address")?), + ForeignChain::Arbitrum => EncodedAddress::Arb(clean_hex_address(address)?), ForeignChain::Solana => match SolAddress::from_str(address) { Ok(sol_address) => EncodedAddress::Sol(sol_address.into()), - Err(_) => - EncodedAddress::Sol(clean_hex_address(address).map_err(|_| "Invalid address")?), + Err(_) => EncodedAddress::Sol(clean_hex_address(address)?), }, }) } diff --git a/state-chain/custom-rpc/src/lib.rs b/state-chain/custom-rpc/src/lib.rs index e7e1297e85..255acaf963 100644 --- a/state-chain/custom-rpc/src/lib.rs +++ b/state-chain/custom-rpc/src/lib.rs @@ -1819,7 +1819,11 @@ where destination_address .try_parse_to_encoded_address(destination_asset.into()) .map_err(|e| { - ErrorObject::owned(ErrorCode::InvalidParams.code(), e, None::<()>) + ErrorObject::owned( + ErrorCode::InvalidParams.code(), + format!("{e}"), + None::<()>, + ) })?, broker_commission, min_output_amount, diff --git a/state-chain/runtime/src/lib.rs b/state-chain/runtime/src/lib.rs index 3f23b68683..874ab0944d 100644 --- a/state-chain/runtime/src/lib.rs +++ b/state-chain/runtime/src/lib.rs @@ -2151,7 +2151,7 @@ impl_runtime_apis! { ); Ok(VaultSwapDetails::Bitcoin { - nulldata_utxo: encode_swap_params_in_nulldata_utxo(params).raw().into(), + nulldata_utxo: encode_swap_params_in_nulldata_utxo(params).raw(), deposit_address, }) }, From e2327ca47f432d2a81d1e69b9136141c163183fa Mon Sep 17 00:00:00 2001 From: Jamie Ford Date: Fri, 8 Nov 2024 15:13:02 +1100 Subject: [PATCH 07/15] chore: change prewitness origin logic get test to pass --- state-chain/pallets/cf-ingress-egress/src/lib.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/state-chain/pallets/cf-ingress-egress/src/lib.rs b/state-chain/pallets/cf-ingress-egress/src/lib.rs index b904be235a..606564b032 100644 --- a/state-chain/pallets/cf-ingress-egress/src/lib.rs +++ b/state-chain/pallets/cf-ingress-egress/src/lib.rs @@ -1290,11 +1290,7 @@ pub mod pallet { dca_params: Option, boost_fee: BasisPoints, ) -> DispatchResult { - if T::EnsurePrewitnessed::ensure_origin(origin.clone()).is_ok() { - // Pre-witnessed vault swaps are not supported yet. - } else { - T::EnsureWitnessed::ensure_origin(origin)?; - + if T::EnsureWitnessed::ensure_origin(origin.clone()).is_ok() { Self::process_vault_swap_request( input_asset, deposit_amount, @@ -1308,6 +1304,9 @@ pub mod pallet { dca_params, boost_fee, ); + } else { + T::EnsurePrewitnessed::ensure_origin(origin)?; + // Pre-witnessed vault swaps are not supported yet. } Ok(()) From bf8460c42fab7ea63bed7bfd80d25982b1b01c8d Mon Sep 17 00:00:00 2001 From: Jamie Ford Date: Mon, 11 Nov 2024 10:37:17 +1100 Subject: [PATCH 08/15] chore: Address PR comments for bouncer test --- bouncer/shared/send_btc.ts | 11 +++++++++-- bouncer/tests/btc_vault_swap.ts | 3 +-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/bouncer/shared/send_btc.ts b/bouncer/shared/send_btc.ts index fb443cbb5e..f79f53e090 100644 --- a/bouncer/shared/send_btc.ts +++ b/bouncer/shared/send_btc.ts @@ -43,15 +43,22 @@ export async function waitForBtcTransaction(txid: string, confirmations = 1) { return; } } + throw new Error(`Timeout waiting for Btc transaction to be confirmed, txid: ${txid}`); } -export async function sendBtc(address: string, amount: number | string): Promise { +export async function sendBtc( + address: string, + amount: number | string, + confirmations = 1, +): Promise { // Btc client has a limit on the number of concurrent requests const txid = (await btcClientMutex.runExclusive(async () => btcClient.sendToAddress(address, amount, '', '', false, true, null, 'unset', null, 1), )) as string; - await waitForBtcTransaction(txid); + if (confirmations > 0) { + await waitForBtcTransaction(txid, confirmations); + } return txid; } diff --git a/bouncer/tests/btc_vault_swap.ts b/bouncer/tests/btc_vault_swap.ts index 69cd237c68..dd491288b7 100644 --- a/bouncer/tests/btc_vault_swap.ts +++ b/bouncer/tests/btc_vault_swap.ts @@ -74,9 +74,8 @@ async function buildAndSendBtcVaultSwap( ); if (!txid) { throw new Error('Broadcast failed'); - } else { - testBtcVaultSwap.log('Broadcast successful, txid:', txid); } + testBtcVaultSwap.log('Broadcast successful, txid:', txid); await waitForBtcTransaction(txid as string); testBtcVaultSwap.debugLog('Transaction confirmed'); From 3399d75d6a5a214cdeff3745051ddc8a12229f88 Mon Sep 17 00:00:00 2001 From: Jamie Ford Date: Mon, 11 Nov 2024 10:38:52 +1100 Subject: [PATCH 09/15] chore: use BlockNumber instead of u32 for retry duration --- api/bin/chainflip-broker-api/src/main.rs | 7 ++++--- api/lib/src/lib.rs | 2 +- engine/src/state_chain_observer/client/base_rpc_api.rs | 10 +++++----- state-chain/custom-rpc/src/lib.rs | 8 ++++---- state-chain/pallets/cf-swapping/src/lib.rs | 2 +- state-chain/pallets/cf-swapping/src/tests.rs | 6 ++++-- state-chain/runtime/src/lib.rs | 4 ++-- state-chain/runtime/src/runtime_apis.rs | 6 ++++-- state-chain/traits/src/lib.rs | 2 +- state-chain/traits/src/mocks/swap_limits_provider.rs | 3 ++- 10 files changed, 28 insertions(+), 22 deletions(-) diff --git a/api/bin/chainflip-broker-api/src/main.rs b/api/bin/chainflip-broker-api/src/main.rs index 589a8d7d52..ac8a1baea4 100644 --- a/api/bin/chainflip-broker-api/src/main.rs +++ b/api/bin/chainflip-broker-api/src/main.rs @@ -8,7 +8,8 @@ use chainflip_api::{ self, primitives::{ state_chain_runtime::runtime_apis::{ChainAccounts, TaintedTransactionEvents}, - AccountRole, Affiliates, Asset, BasisPoints, CcmChannelMetadata, DcaParameters, + AccountRole, Affiliates, Asset, BasisPoints, BlockNumber, CcmChannelMetadata, + DcaParameters, }, settings::StateChain, AccountId32, AddressString, BlockUpdate, ChainApi, DepositMonitorApi, OperatorApi, @@ -108,7 +109,7 @@ pub trait Rpc { destination_address: AddressString, broker_commission: BasisPoints, min_output_amount: NumberOrHex, - retry_duration: u32, + retry_duration: BlockNumber, boost_fee: Option, affiliate_fees: Option>, dca_parameters: Option, @@ -198,7 +199,7 @@ impl RpcServer for RpcServerImpl { destination_address: AddressString, broker_commission: BasisPoints, min_output_amount: NumberOrHex, - retry_duration: u32, + retry_duration: BlockNumber, boost_fee: Option, affiliate_fees: Option>, dca_parameters: Option, diff --git a/api/lib/src/lib.rs b/api/lib/src/lib.rs index 5867c4b5d8..32952ed0d3 100644 --- a/api/lib/src/lib.rs +++ b/api/lib/src/lib.rs @@ -480,7 +480,7 @@ impl BrokerApi { destination_address: AddressString, broker_commission: BasisPoints, min_output_amount: AssetAmount, - retry_duration: u32, + retry_duration: cf_primitives::BlockNumber, boost_fee: Option, affiliate_fees: Option>, dca_parameters: Option, diff --git a/engine/src/state_chain_observer/client/base_rpc_api.rs b/engine/src/state_chain_observer/client/base_rpc_api.rs index 559b4dc9e8..30682f06a2 100644 --- a/engine/src/state_chain_observer/client/base_rpc_api.rs +++ b/engine/src/state_chain_observer/client/base_rpc_api.rs @@ -1,7 +1,7 @@ use async_trait::async_trait; use cf_chains::address::AddressString; -use cf_primitives::{Affiliates, Asset, AssetAmount, BasisPoints, DcaParameters}; +use cf_primitives::{Affiliates, Asset, AssetAmount, BasisPoints, BlockNumber, DcaParameters}; use jsonrpsee::core::client::{ClientT, Subscription, SubscriptionClientT}; use sc_transaction_pool_api::TransactionStatus; use sp_core::{ @@ -173,7 +173,7 @@ pub trait BaseRpcApi { async fn validate_refund_params( &self, - retry_duration: u32, + retry_duration: BlockNumber, block_hash: Option, ) -> RpcResult<()>; @@ -192,7 +192,7 @@ pub trait BaseRpcApi { destination_address: AddressString, broker_commission: BasisPoints, min_output_amount: AssetAmount, - retry_duration: u32, + retry_duration: BlockNumber, boost_fee: Option, affiliate_fees: Option>, dca_parameters: Option, @@ -338,7 +338,7 @@ impl BaseRpcApi for BaseRpcClient, ) -> RpcResult<()> { self.raw_rpc_client.cf_validate_refund_params(retry_duration, block_hash).await @@ -363,7 +363,7 @@ impl BaseRpcApi for BaseRpcClient, affiliate_fees: Option>, dca_parameters: Option, diff --git a/state-chain/custom-rpc/src/lib.rs b/state-chain/custom-rpc/src/lib.rs index cdd79d35cb..eab24b896b 100644 --- a/state-chain/custom-rpc/src/lib.rs +++ b/state-chain/custom-rpc/src/lib.rs @@ -984,7 +984,7 @@ pub trait CustomApi { #[method(name = "validate_refund_params")] fn cf_validate_refund_params( &self, - retry_duration: u32, + retry_duration: BlockNumber, at: Option, ) -> RpcResult<()>; @@ -997,7 +997,7 @@ pub trait CustomApi { destination_address: AddressString, broker_commission: BasisPoints, min_output_amount: AssetAmount, - retry_duration: u32, + retry_duration: BlockNumber, boost_fee: Option, affiliate_fees: Option>, dca_parameters: Option, @@ -1284,7 +1284,7 @@ where liquidity: Liquidity, ) -> PoolPairsMap, cf_validate_dca_params(number_of_chunks: u32, chunk_interval: u32) -> (), - cf_validate_refund_params(retry_duration: u32) -> (), + cf_validate_refund_params(retry_duration: BlockNumber) -> (), } fn cf_current_compatibility_version(&self) -> RpcResult { @@ -1843,7 +1843,7 @@ where destination_address: AddressString, broker_commission: BasisPoints, min_output_amount: AssetAmount, - retry_duration: u32, + retry_duration: BlockNumber, boost_fee: Option, affiliate_fees: Option>, dca_parameters: Option, diff --git a/state-chain/pallets/cf-swapping/src/lib.rs b/state-chain/pallets/cf-swapping/src/lib.rs index 8d9c9ca36e..253d98a4e0 100644 --- a/state-chain/pallets/cf-swapping/src/lib.rs +++ b/state-chain/pallets/cf-swapping/src/lib.rs @@ -2363,7 +2363,7 @@ impl SwapLimitsProvider for Pallet { } } - fn validate_refund_params(retry_duration: u32) -> Result<(), DispatchError> { + fn validate_refund_params(retry_duration: BlockNumber) -> Result<(), DispatchError> { let max_swap_retry_duration_blocks = MaxSwapRetryDurationBlocks::::get(); if retry_duration > max_swap_retry_duration_blocks { return Err(DispatchError::from(Error::::RetryDurationTooHigh)); diff --git a/state-chain/pallets/cf-swapping/src/tests.rs b/state-chain/pallets/cf-swapping/src/tests.rs index beed1924d4..a2ef8d75dd 100644 --- a/state-chain/pallets/cf-swapping/src/tests.rs +++ b/state-chain/pallets/cf-swapping/src/tests.rs @@ -19,7 +19,9 @@ use cf_chains::{ dot::PolkadotAccountId, AnyChain, CcmChannelMetadata, CcmDepositMetadata, Ethereum, }; -use cf_primitives::{Asset, AssetAmount, BasisPoints, Beneficiary, DcaParameters, ForeignChain}; +use cf_primitives::{ + Asset, AssetAmount, BasisPoints, Beneficiary, BlockNumber, DcaParameters, ForeignChain, +}; use cf_test_utilities::{assert_event_sequence, assert_events_eq, assert_has_matching_event}; use cf_traits::{ mocks::{ @@ -90,7 +92,7 @@ impl TestSwapParams { // with min output rather than min price: #[derive(Debug, Clone)] struct TestRefundParams { - retry_duration: u32, + retry_duration: BlockNumber, min_output: AssetAmount, } diff --git a/state-chain/runtime/src/lib.rs b/state-chain/runtime/src/lib.rs index c96dfc6995..f48298a293 100644 --- a/state-chain/runtime/src/lib.rs +++ b/state-chain/runtime/src/lib.rs @@ -2094,7 +2094,7 @@ impl_runtime_apis! { 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> { + fn cf_validate_refund_params(retry_duration: BlockNumber) -> Result<(), DispatchErrorWithMessage> { pallet_cf_swapping::Pallet::::validate_refund_params(retry_duration).map_err(Into::into) } @@ -2105,7 +2105,7 @@ impl_runtime_apis! { destination_address: EncodedAddress, broker_commission: BasisPoints, min_output_amount: AssetAmount, - retry_duration: u32, + retry_duration: BlockNumber, boost_fee: Option, affiliate_fees: Option>, dca_parameters: Option, diff --git a/state-chain/runtime/src/runtime_apis.rs b/state-chain/runtime/src/runtime_apis.rs index 458631344a..796e33cf8b 100644 --- a/state-chain/runtime/src/runtime_apis.rs +++ b/state-chain/runtime/src/runtime_apis.rs @@ -376,7 +376,9 @@ decl_runtime_apis!( number_of_chunks: u32, chunk_interval: u32, ) -> Result<(), DispatchErrorWithMessage>; - fn cf_validate_refund_params(retry_duration: u32) -> Result<(), DispatchErrorWithMessage>; + fn cf_validate_refund_params( + retry_duration: BlockNumber, + ) -> Result<(), DispatchErrorWithMessage>; fn cf_get_vault_swap_details( broker: AccountId32, source_asset: Asset, @@ -384,7 +386,7 @@ decl_runtime_apis!( destination_address: EncodedAddress, broker_commission: BasisPoints, min_output_amount: AssetAmount, - retry_duration: u32, + retry_duration: BlockNumber, boost_fee: Option, affiliate_fees: Option>, dca_parameters: Option, diff --git a/state-chain/traits/src/lib.rs b/state-chain/traits/src/lib.rs index 0c8ae01d9b..2d8ee0e346 100644 --- a/state-chain/traits/src/lib.rs +++ b/state-chain/traits/src/lib.rs @@ -993,7 +993,7 @@ pub trait SwapLimitsProvider { fn get_swap_limits() -> SwapLimits; fn validate_dca_params(dca_params: &DcaParameters) -> Result<(), DispatchError>; - fn validate_refund_params(retry_duration: u32) -> Result<(), DispatchError>; + fn validate_refund_params(retry_duration: BlockNumber) -> Result<(), DispatchError>; fn validate_broker_fees( broker_fees: &Beneficiaries, ) -> Result<(), DispatchError>; diff --git a/state-chain/traits/src/mocks/swap_limits_provider.rs b/state-chain/traits/src/mocks/swap_limits_provider.rs index 66a24c9ba1..48834e6867 100644 --- a/state-chain/traits/src/mocks/swap_limits_provider.rs +++ b/state-chain/traits/src/mocks/swap_limits_provider.rs @@ -1,3 +1,4 @@ +use cf_primitives::BlockNumber; use frame_support::sp_runtime::DispatchError; use crate::{SwapLimits, SwapLimitsProvider}; @@ -14,7 +15,7 @@ impl SwapLimitsProvider for MockSwapLimitsProvider { } } - fn validate_refund_params(retry_duration: u32) -> Result<(), DispatchError> { + fn validate_refund_params(retry_duration: BlockNumber) -> Result<(), DispatchError> { let limits = Self::get_swap_limits(); if retry_duration > limits.max_swap_retry_duration_blocks { return Err(DispatchError::Other("Retry duration too high")); From e16a11075fb5145b0e0674f13a66989bdf88d646 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 7 Nov 2024 10:08:31 +0100 Subject: [PATCH 10/15] feat: simplified error handling --- state-chain/runtime/src/lib.rs | 52 ++++++------------------- state-chain/runtime/src/runtime_apis.rs | 14 +++---- 2 files changed, 17 insertions(+), 49 deletions(-) diff --git a/state-chain/runtime/src/lib.rs b/state-chain/runtime/src/lib.rs index f48298a293..13c541bfc4 100644 --- a/state-chain/runtime/src/lib.rs +++ b/state-chain/runtime/src/lib.rs @@ -2112,15 +2112,12 @@ impl_runtime_apis! { ) -> Result { // Validate params if broker_commission != 0 || affiliate_fees.map_or(false, |affiliate| !affiliate.is_empty()) { - return Err(DispatchErrorWithMessage::from_pallet_error( - pallet_cf_swapping::Error::::VaultSwapBrokerFeesNotSupported, - )); + return Err( + pallet_cf_swapping::Error::::VaultSwapBrokerFeesNotSupported.into()); } - pallet_cf_swapping::Pallet::::validate_refund_params(retry_duration) - .map_err(Into::::into)?; + pallet_cf_swapping::Pallet::::validate_refund_params(retry_duration)?; if let Some(params) = dca_parameters.as_ref() { - pallet_cf_swapping::Pallet::::validate_dca_params(params) - .map_err(Into::::into)?; + pallet_cf_swapping::Pallet::::validate_dca_params(params)?; } ChainAddressConverter::try_from_encoded_address(destination_address.clone()) .and_then(|address| { @@ -2130,11 +2127,7 @@ impl_runtime_apis! { Ok(()) } }) - .map_err(|_| { - DispatchErrorWithMessage::from_pallet_error( - pallet_cf_swapping::Error::::InvalidDestinationAddress, - ) - })?; + .map_err(|()| pallet_cf_swapping::Error::::InvalidDestinationAddress)?; // Encode swap @@ -2144,42 +2137,22 @@ impl_runtime_apis! { output_asset: destination_asset, output_address: destination_address, parameters: SharedCfParameters { - retry_duration: retry_duration.try_into().map_err(|_| { - DispatchErrorWithMessage::from_pallet_error( - pallet_cf_swapping::Error::::SwapRequestDurationTooLong, - ) - })?, + retry_duration: retry_duration.try_into().map_err(|_| pallet_cf_swapping::Error::::SwapRequestDurationTooLong)?, min_output_amount, number_of_chunks: dca_parameters .as_ref() .map(|params| params.number_of_chunks) .unwrap_or(1) .try_into() - .map_err(|_| { - DispatchErrorWithMessage::from_pallet_error( - pallet_cf_swapping::Error::::InvalidDcaParameters, - ) - })?, + .map_err(|_| pallet_cf_swapping::Error::::InvalidDcaParameters)?, chunk_interval: dca_parameters .as_ref() .map(|params| params.chunk_interval) .unwrap_or(SWAP_DELAY_BLOCKS) .try_into() - .map_err(|_| { - DispatchErrorWithMessage::from_pallet_error( - pallet_cf_swapping::Error::::InvalidDcaParameters, - ) - })?, - boost_fee: boost_fee.unwrap_or_default().try_into().map_err(|_| { - DispatchErrorWithMessage::from_pallet_error( - pallet_cf_swapping::Error::::BoostFeeTooHigh, - ) - })?, - broker_fee: broker_commission.try_into().map_err(|_| { - DispatchErrorWithMessage::from_pallet_error( - pallet_cf_swapping::Error::::BrokerFeeTooHigh, - ) - })?, + .map_err(|_| pallet_cf_swapping::Error::::InvalidDcaParameters)?, + boost_fee: boost_fee.unwrap_or_default().try_into().map_err(|_| pallet_cf_swapping::Error::::BoostFeeTooHigh)?, + broker_fee: broker_commission.try_into().map_err(|_| pallet_cf_swapping::Error::::BrokerFeeTooHigh)?, // TODO: lookup affiliate mapping to convert affiliate ids and use them here affiliates: Default::default(), }, @@ -2198,9 +2171,8 @@ impl_runtime_apis! { deposit_address, }) }, - _ => Err(DispatchErrorWithMessage::from_pallet_error( - pallet_cf_swapping::Error::::UnsupportedSourceAsset, - )), + _ => Err( + pallet_cf_swapping::Error::::UnsupportedSourceAsset.into()), } } diff --git a/state-chain/runtime/src/runtime_apis.rs b/state-chain/runtime/src/runtime_apis.rs index 796e33cf8b..f7e832cae5 100644 --- a/state-chain/runtime/src/runtime_apis.rs +++ b/state-chain/runtime/src/runtime_apis.rs @@ -161,20 +161,16 @@ pub enum DispatchErrorWithMessage { Module(Vec), Other(DispatchError), } -impl From for DispatchErrorWithMessage { - fn from(value: DispatchError) -> Self { - match value { +impl> From for DispatchErrorWithMessage { + fn from(error: E) -> Self { + match error.into() { DispatchError::Module(sp_runtime::ModuleError { message: Some(message), .. }) => DispatchErrorWithMessage::Module(message.as_bytes().to_vec()), - value => DispatchErrorWithMessage::Other(value), + error => DispatchErrorWithMessage::Other(error), } } } -impl DispatchErrorWithMessage { - pub fn from_pallet_error(e: impl Into) -> Self { - Self::from(e.into()) - } -} + #[cfg(feature = "std")] impl core::fmt::Display for DispatchErrorWithMessage { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { From 3422e8a7cd85e936cba61b8149392be314696718 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 8 Nov 2024 15:28:02 +0100 Subject: [PATCH 11/15] fix: pass-through base_rpc --- api/bin/chainflip-broker-api/src/main.rs | 7 ++- api/bin/chainflip-cli/src/main.rs | 1 + api/lib/src/lib.rs | 68 +++++++----------------- 3 files changed, 24 insertions(+), 52 deletions(-) diff --git a/api/bin/chainflip-broker-api/src/main.rs b/api/bin/chainflip-broker-api/src/main.rs index ac8a1baea4..42ecd450d0 100644 --- a/api/bin/chainflip-broker-api/src/main.rs +++ b/api/bin/chainflip-broker-api/src/main.rs @@ -12,7 +12,7 @@ use chainflip_api::{ DcaParameters, }, settings::StateChain, - AccountId32, AddressString, BlockUpdate, ChainApi, DepositMonitorApi, OperatorApi, + AccountId32, AddressString, BlockUpdate, BrokerApi, ChainApi, DepositMonitorApi, OperatorApi, RefundParameters, SignedExtrinsicApi, StateChainApi, SwapDepositAddress, TransactionInId, WithdrawFeesDetail, }; @@ -207,7 +207,9 @@ impl RpcServer for RpcServerImpl { Ok(self .api .broker_api() - .request_swap_parameter_encoding( + .base_rpc_api() + .cf_get_vault_swap_details( + self.api.state_chain_client.account_id(), source_asset, destination_asset, destination_address, @@ -217,6 +219,7 @@ impl RpcServer for RpcServerImpl { boost_fee, affiliate_fees, dca_parameters, + None, ) .await?) } diff --git a/api/bin/chainflip-cli/src/main.rs b/api/bin/chainflip-cli/src/main.rs index 9061d60f56..4134a33a93 100644 --- a/api/bin/chainflip-cli/src/main.rs +++ b/api/bin/chainflip-cli/src/main.rs @@ -15,6 +15,7 @@ use cf_utilities::{clean_hex_address, round_f64, task_scope::task_scope}; use chainflip_api::{ self as api, primitives::{state_chain_runtime, FLIPPERINOS_PER_FLIP}, + BrokerApi, }; use clap::Parser; use futures::FutureExt; diff --git a/api/lib/src/lib.rs b/api/lib/src/lib.rs index 32952ed0d3..03dfe2a2ae 100644 --- a/api/lib/src/lib.rs +++ b/api/lib/src/lib.rs @@ -7,9 +7,8 @@ use cf_chains::{ evm::to_evm_address, CcmChannelMetadata, Chain, ChainCrypto, ChannelRefundParametersEncoded, ChannelRefundParametersGeneric, ForeignChain, }; +use cf_primitives::DcaParameters; pub use cf_primitives::{AccountRole, Affiliates, Asset, BasisPoints, ChannelId, SemVer}; -use cf_primitives::{AssetAmount, DcaParameters}; -use custom_rpc::VaultSwapDetailsHumanreadable; use pallet_cf_account_roles::MAX_LENGTH_FOR_VANITY_NAME; use pallet_cf_governance::ExecutionMode; use serde::{Deserialize, Serialize}; @@ -141,8 +140,8 @@ impl StateChainApi { self.state_chain_client.clone() } - pub fn broker_api(&self) -> BrokerApi { - BrokerApi { state_chain_client: self.state_chain_client.clone() } + pub fn broker_api(&self) -> Arc { + self.state_chain_client.clone() } pub fn lp_api(&self) -> Arc { @@ -161,6 +160,12 @@ impl StateChainApi { #[async_trait] impl GovernanceApi for StateChainClient {} #[async_trait] +impl BrokerApi for StateChainClient { + fn base_rpc_api(&self) -> Arc { + self.base_rpc_client.clone() + } +} +#[async_trait] impl OperatorApi for StateChainClient {} #[async_trait] impl ValidatorApi for StateChainClient {} @@ -336,12 +341,10 @@ impl fmt::Display for WithdrawFeesDetail { } } -pub struct BrokerApi { - pub(crate) state_chain_client: Arc, -} - -impl BrokerApi { - pub async fn request_swap_deposit_address( +#[async_trait] +pub trait BrokerApi: SignedExtrinsicApi + StorageApi + Sized + Send + Sync + 'static { + fn base_rpc_api(&self) -> Arc; + async fn request_swap_deposit_address( &self, source_asset: Asset, destination_asset: Asset, @@ -357,7 +360,6 @@ impl BrokerApi { .try_parse_to_encoded_address(destination_asset.into()) .map_err(anyhow::Error::msg)?; let (_tx_hash, events, header, ..) = self - .state_chain_client .submit_signed_extrinsic_with_dry_run( pallet_cf_swapping::Call::request_swap_deposit_address_with_affiliates { source_asset, @@ -418,13 +420,12 @@ impl BrokerApi { bail!("No SwapDepositAddressReady event was found"); } } - pub async fn withdraw_fees( + async fn withdraw_fees( &self, asset: Asset, destination_address: AddressString, ) -> Result { let (tx_hash, events, ..) = self - .state_chain_client .submit_signed_extrinsic(RuntimeCall::from(pallet_cf_swapping::Call::withdraw { asset, destination_address: destination_address @@ -462,47 +463,14 @@ impl BrokerApi { bail!("No WithdrawalRequested event was found"); } } - pub async fn register_account(&self) -> Result { - self.state_chain_client - .simple_submission_with_dry_run(pallet_cf_swapping::Call::register_as_broker {}) + async fn register_account(&self) -> Result { + self.simple_submission_with_dry_run(pallet_cf_swapping::Call::register_as_broker {}) .await } - pub async fn deregister_account(&self) -> Result { - self.state_chain_client - .simple_submission_with_dry_run(pallet_cf_swapping::Call::deregister_as_broker {}) + async fn deregister_account(&self) -> Result { + self.simple_submission_with_dry_run(pallet_cf_swapping::Call::deregister_as_broker {}) .await } - - pub async fn request_swap_parameter_encoding( - &self, - source_asset: Asset, - destination_asset: Asset, - destination_address: AddressString, - broker_commission: BasisPoints, - min_output_amount: AssetAmount, - retry_duration: cf_primitives::BlockNumber, - boost_fee: Option, - affiliate_fees: Option>, - dca_parameters: Option, - ) -> Result { - Ok(self - .state_chain_client - .base_rpc_client - .cf_get_vault_swap_details( - self.state_chain_client.account_id(), - source_asset, - destination_asset, - destination_address, - broker_commission, - min_output_amount, - retry_duration, - boost_fee, - affiliate_fees, - dca_parameters, - None, - ) - .await?) - } } #[async_trait] From afc539ca61add40e07bab5df73de1b7d055905c3 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 11 Nov 2024 10:23:47 +0100 Subject: [PATCH 12/15] feat: use direct access to node rpcs --- api/bin/chainflip-broker-api/src/main.rs | 13 ++--- api/lib/src/lib.rs | 15 +++--- .../client/base_rpc_api.rs | 50 +------------------ 3 files changed, 17 insertions(+), 61 deletions(-) diff --git a/api/bin/chainflip-broker-api/src/main.rs b/api/bin/chainflip-broker-api/src/main.rs index 42ecd450d0..3b70c4c529 100644 --- a/api/bin/chainflip-broker-api/src/main.rs +++ b/api/bin/chainflip-broker-api/src/main.rs @@ -7,7 +7,9 @@ use cf_utilities::{ use chainflip_api::{ self, primitives::{ - state_chain_runtime::runtime_apis::{ChainAccounts, TaintedTransactionEvents}, + state_chain_runtime::runtime_apis::{ + ChainAccounts, TaintedTransactionEvents, VaultSwapDetails, + }, AccountRole, Affiliates, Asset, BasisPoints, BlockNumber, CcmChannelMetadata, DcaParameters, }, @@ -17,7 +19,7 @@ use chainflip_api::{ WithdrawFeesDetail, }; use clap::Parser; -use custom_rpc::{CustomApiClient, VaultSwapDetailsHumanreadable}; +use custom_rpc::CustomApiClient; use futures::{FutureExt, StreamExt}; use jsonrpsee::{ core::{async_trait, ClientError, SubscriptionResult}, @@ -113,7 +115,7 @@ pub trait Rpc { boost_fee: Option, affiliate_fees: Option>, dca_parameters: Option, - ) -> RpcResult; + ) -> RpcResult>; #[method(name = "mark_transaction_as_tainted", aliases = ["broker_markTransactionAsTainted"])] async fn mark_transaction_as_tainted(&self, tx_id: TransactionInId) -> RpcResult<()>; @@ -203,11 +205,10 @@ impl RpcServer for RpcServerImpl { boost_fee: Option, affiliate_fees: Option>, dca_parameters: Option, - ) -> RpcResult { + ) -> RpcResult> { Ok(self .api - .broker_api() - .base_rpc_api() + .raw_client() .cf_get_vault_swap_details( self.api.state_chain_client.account_id(), source_asset, diff --git a/api/lib/src/lib.rs b/api/lib/src/lib.rs index 03dfe2a2ae..20e2e1e0a1 100644 --- a/api/lib/src/lib.rs +++ b/api/lib/src/lib.rs @@ -155,16 +155,20 @@ impl StateChainApi { pub fn query_api(&self) -> queries::QueryApi { queries::QueryApi { state_chain_client: self.state_chain_client.clone() } } + + pub fn base_rpc_api(&self) -> Arc { + self.state_chain_client.base_rpc_client.clone() + } + + pub fn raw_client(&self) -> &jsonrpsee::ws_client::WsClient { + &self.state_chain_client.base_rpc_client.raw_rpc_client + } } #[async_trait] impl GovernanceApi for StateChainClient {} #[async_trait] -impl BrokerApi for StateChainClient { - fn base_rpc_api(&self) -> Arc { - self.base_rpc_client.clone() - } -} +impl BrokerApi for StateChainClient {} #[async_trait] impl OperatorApi for StateChainClient {} #[async_trait] @@ -343,7 +347,6 @@ impl fmt::Display for WithdrawFeesDetail { #[async_trait] pub trait BrokerApi: SignedExtrinsicApi + StorageApi + Sized + Send + Sync + 'static { - fn base_rpc_api(&self) -> Arc; async fn request_swap_deposit_address( &self, source_asset: Asset, diff --git a/engine/src/state_chain_observer/client/base_rpc_api.rs b/engine/src/state_chain_observer/client/base_rpc_api.rs index 30682f06a2..d909339af4 100644 --- a/engine/src/state_chain_observer/client/base_rpc_api.rs +++ b/engine/src/state_chain_observer/client/base_rpc_api.rs @@ -1,7 +1,5 @@ use async_trait::async_trait; -use cf_chains::address::AddressString; -use cf_primitives::{Affiliates, Asset, AssetAmount, BasisPoints, BlockNumber, DcaParameters}; use jsonrpsee::core::client::{ClientT, Subscription, SubscriptionClientT}; use sc_transaction_pool_api::TransactionStatus; use sp_core::{ @@ -12,7 +10,7 @@ use sp_version::RuntimeVersion; use state_chain_runtime::SignedBlock; use codec::Encode; -use custom_rpc::{CustomApiClient, VaultSwapDetailsHumanreadable}; +use custom_rpc::CustomApiClient; use sc_rpc_api::{ author::AuthorApiClient, chain::ChainApiClient, @@ -183,21 +181,6 @@ pub trait BaseRpcApi { chunk_interval: u32, block_hash: Option, ) -> RpcResult<()>; - - async fn cf_get_vault_swap_details( - &self, - broker: state_chain_runtime::AccountId, - source_asset: Asset, - destination_asset: Asset, - destination_address: AddressString, - broker_commission: BasisPoints, - min_output_amount: AssetAmount, - retry_duration: BlockNumber, - boost_fee: Option, - affiliate_fees: Option>, - dca_parameters: Option, - block_hash: Option, - ) -> RpcResult; } pub struct BaseRpcClient { @@ -354,37 +337,6 @@ impl BaseRpcApi for BaseRpcClient, - affiliate_fees: Option>, - dca_parameters: Option, - block_hash: Option, - ) -> RpcResult { - self.raw_rpc_client - .cf_get_vault_swap_details( - broker, - source_asset, - destination_asset, - destination_address, - broker_commission, - min_output_amount, - retry_duration, - boost_fee, - affiliate_fees, - dca_parameters, - block_hash, - ) - .await - } } struct Params(Option>); From c8f64dc40eb56dbdfd6d2e4b4a983922f2ff2782 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 11 Nov 2024 10:27:34 +0100 Subject: [PATCH 13/15] chore: remove options --- state-chain/custom-rpc/src/lib.rs | 4 ++-- state-chain/runtime/src/lib.rs | 8 ++++---- state-chain/runtime/src/runtime_apis.rs | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/state-chain/custom-rpc/src/lib.rs b/state-chain/custom-rpc/src/lib.rs index eab24b896b..72c42003cc 100644 --- a/state-chain/custom-rpc/src/lib.rs +++ b/state-chain/custom-rpc/src/lib.rs @@ -1868,8 +1868,8 @@ where broker_commission, min_output_amount, retry_duration, - boost_fee, - affiliate_fees, + boost_fee.unwrap_or_default(), + affiliate_fees.unwrap_or_default(), dca_parameters, )??, api.cf_network_environment(hash)?, diff --git a/state-chain/runtime/src/lib.rs b/state-chain/runtime/src/lib.rs index 13c541bfc4..c445d94eec 100644 --- a/state-chain/runtime/src/lib.rs +++ b/state-chain/runtime/src/lib.rs @@ -2106,12 +2106,12 @@ impl_runtime_apis! { broker_commission: BasisPoints, min_output_amount: AssetAmount, retry_duration: BlockNumber, - boost_fee: Option, - affiliate_fees: Option>, + boost_fee: BasisPoints, + affiliate_fees: Affiliates, dca_parameters: Option, ) -> Result { // Validate params - if broker_commission != 0 || affiliate_fees.map_or(false, |affiliate| !affiliate.is_empty()) { + if broker_commission != 0 || !affiliate_fees.is_empty() { return Err( pallet_cf_swapping::Error::::VaultSwapBrokerFeesNotSupported.into()); } @@ -2151,7 +2151,7 @@ impl_runtime_apis! { .unwrap_or(SWAP_DELAY_BLOCKS) .try_into() .map_err(|_| pallet_cf_swapping::Error::::InvalidDcaParameters)?, - boost_fee: boost_fee.unwrap_or_default().try_into().map_err(|_| pallet_cf_swapping::Error::::BoostFeeTooHigh)?, + boost_fee: boost_fee.try_into().map_err(|_| pallet_cf_swapping::Error::::BoostFeeTooHigh)?, broker_fee: broker_commission.try_into().map_err(|_| pallet_cf_swapping::Error::::BrokerFeeTooHigh)?, // TODO: lookup affiliate mapping to convert affiliate ids and use them here affiliates: Default::default(), diff --git a/state-chain/runtime/src/runtime_apis.rs b/state-chain/runtime/src/runtime_apis.rs index f7e832cae5..a8e908e1a4 100644 --- a/state-chain/runtime/src/runtime_apis.rs +++ b/state-chain/runtime/src/runtime_apis.rs @@ -383,8 +383,8 @@ decl_runtime_apis!( broker_commission: BasisPoints, min_output_amount: AssetAmount, retry_duration: BlockNumber, - boost_fee: Option, - affiliate_fees: Option>, + boost_fee: BasisPoints, + affiliate_fees: Affiliates, dca_parameters: Option, ) -> Result; fn cf_get_open_deposit_channels(account_id: Option) -> ChainAccounts; From 611ce467a6c76e86951ead7aa7c41295fce1e39b Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 11 Nov 2024 10:28:06 +0100 Subject: [PATCH 14/15] fix: simplify bitcoin address return type --- state-chain/chains/src/address.rs | 9 +++- state-chain/custom-rpc/src/lib.rs | 48 +++++----------------- state-chain/pallets/cf-swapping/src/lib.rs | 2 +- state-chain/runtime/src/lib.rs | 30 +++++++++----- state-chain/runtime/src/runtime_apis.rs | 23 +++++++++-- 5 files changed, 59 insertions(+), 53 deletions(-) diff --git a/state-chain/chains/src/address.rs b/state-chain/chains/src/address.rs index 1359a11925..07801bd0a6 100644 --- a/state-chain/chains/src/address.rs +++ b/state-chain/chains/src/address.rs @@ -389,9 +389,16 @@ impl ToHumanreadableAddress for ForeignChainAddress { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AddressString(String); +#[cfg(feature = "std")] +impl From for AddressString { + fn from(s: String) -> Self { + Self(s) + } +} + #[cfg(feature = "std")] impl sp_std::str::FromStr for AddressString { - type Err = &'static str; + type Err = frame_support::Never; fn from_str(s: &str) -> Result { Ok(Self(s.to_string())) diff --git a/state-chain/custom-rpc/src/lib.rs b/state-chain/custom-rpc/src/lib.rs index 72c42003cc..5e6212323e 100644 --- a/state-chain/custom-rpc/src/lib.rs +++ b/state-chain/custom-rpc/src/lib.rs @@ -37,7 +37,7 @@ use pallet_cf_swapping::SwapLegInfo; use sc_client_api::{BlockchainEvents, HeaderBackend}; use serde::{Deserialize, Serialize}; use sp_api::{ApiError, CallApiAt}; -use sp_core::{Bytes, U256}; +use sp_core::U256; use sp_runtime::{ traits::{Block as BlockT, Header as HeaderT, UniqueSaturatedInto}, Permill, @@ -69,27 +69,6 @@ use std::{ pub mod monitoring; pub mod order_fills; -#[derive(Clone, Serialize, Deserialize)] -#[serde(tag = "chain")] -pub enum VaultSwapDetailsHumanreadable { - Bitcoin { nulldata_utxo: Bytes, deposit_address: ForeignChainAddressHumanreadable }, -} - -impl VaultSwapDetailsHumanreadable { - fn from( - details: VaultSwapDetails, - network: NetworkEnvironment, - ) -> VaultSwapDetailsHumanreadable { - match details { - VaultSwapDetails::Bitcoin { nulldata_utxo, deposit_address } => - VaultSwapDetailsHumanreadable::Bitcoin { - nulldata_utxo: nulldata_utxo.into(), - deposit_address: deposit_address.to_humanreadable(network), - }, - } - } -} - #[derive(Serialize, Deserialize, Clone)] pub struct RpcEpochState { pub blocks_per_epoch: u32, @@ -1002,7 +981,7 @@ pub trait CustomApi { affiliate_fees: Option>, dca_parameters: Option, at: Option, - ) -> RpcResult; + ) -> RpcResult>; #[method(name = "get_open_deposit_channels")] fn cf_get_open_deposit_channels( @@ -1128,6 +1107,8 @@ pub enum CfApiError { RuntimeApiError(#[from] ApiError), #[error(transparent)] ErrorObject(#[from] ErrorObjectOwned), + #[error(transparent)] + OtherError(#[from] anyhow::Error), } pub type RpcResult = Result; @@ -1167,6 +1148,7 @@ impl From for ErrorObjectOwned { other => internal_error(format!("Unexpected ApiError: {other}")), }, CfApiError::ErrorObject(object) => object, + CfApiError::OtherError(error) => internal_error(error), } } } @@ -1848,32 +1830,24 @@ where affiliate_fees: Option>, dca_parameters: Option, at: Option, - ) -> RpcResult { + ) -> RpcResult> { self.with_runtime_api(at, |api, hash| { - Ok::<_, CfApiError>(VaultSwapDetailsHumanreadable::from( + Ok::<_, CfApiError>( api.cf_get_vault_swap_details( hash, broker, source_asset, destination_asset, - destination_address - .try_parse_to_encoded_address(destination_asset.into()) - .map_err(|e| { - ErrorObject::owned( - ErrorCode::InvalidParams.code(), - format!("{e}"), - None::<()>, - ) - })?, + destination_address.try_parse_to_encoded_address(destination_asset.into())?, broker_commission, min_output_amount, retry_duration, boost_fee.unwrap_or_default(), affiliate_fees.unwrap_or_default(), dca_parameters, - )??, - api.cf_network_environment(hash)?, - )) + )?? + .map_btc_address(Into::into), + ) }) } diff --git a/state-chain/pallets/cf-swapping/src/lib.rs b/state-chain/pallets/cf-swapping/src/lib.rs index 253d98a4e0..b337f07ae8 100644 --- a/state-chain/pallets/cf-swapping/src/lib.rs +++ b/state-chain/pallets/cf-swapping/src/lib.rs @@ -709,7 +709,7 @@ pub mod pallet { UnsupportedSourceAsset, /// Broker cannot deregister or open a new private channel because one already exists. PrivateChannelExistsForBroker, - /// Cannot close a private channel for a broker because it does not exist. + /// The Broker does not have an open private channel. NoPrivateChannelExistsForBroker, } diff --git a/state-chain/runtime/src/lib.rs b/state-chain/runtime/src/lib.rs index c445d94eec..cc2604330a 100644 --- a/state-chain/runtime/src/lib.rs +++ b/state-chain/runtime/src/lib.rs @@ -54,8 +54,7 @@ use cf_chains::{ eth::{self, api::EthereumApi, Address as EthereumAddress, Ethereum}, evm::EvmCrypto, sol::{SolAddress, SolanaCrypto}, - Arbitrum, Bitcoin, DefaultRetryPolicy, ForeignChain, ForeignChainAddress, Polkadot, Solana, - TransactionBuilder, + Arbitrum, Bitcoin, DefaultRetryPolicy, ForeignChain, Polkadot, Solana, TransactionBuilder, }; use cf_primitives::{ Affiliates, BasisPoints, Beneficiary, BroadcastId, DcaParameters, EpochIndex, @@ -2099,7 +2098,7 @@ impl_runtime_apis! { } fn cf_get_vault_swap_details( - _broker: AccountId, + broker_id: AccountId, source_asset: Asset, destination_asset: Asset, destination_address: EncodedAddress, @@ -2109,7 +2108,7 @@ impl_runtime_apis! { boost_fee: BasisPoints, affiliate_fees: Affiliates, dca_parameters: Option, - ) -> Result { + ) -> Result, DispatchErrorWithMessage> { // Validate params if broker_commission != 0 || !affiliate_fees.is_empty() { return Err( @@ -2133,6 +2132,11 @@ impl_runtime_apis! { // Encode swap match ForeignChain::from(source_asset) { ForeignChain::Bitcoin => { + use cf_traits::{KeyProvider, EpochKey}; + use cf_chains::btc::deposit_address::DepositAddress; + + let private_channel_id = pallet_cf_swapping::BrokerPrivateBtcChannels::::get(&broker_id) + .ok_or(pallet_cf_swapping::Error::::NoPrivateChannelExistsForBroker)?; let params = UtxoEncodedData { output_asset: destination_asset, output_address: destination_address, @@ -2158,13 +2162,17 @@ impl_runtime_apis! { }, }; - // TODO: get private channel address. For now just return the btc vault address. - let btc_key = pallet_cf_threshold_signature::Pallet::::keys( - pallet_cf_threshold_signature::Pallet::::current_key_epoch().unwrap()).unwrap(); - let deposit_address = ForeignChainAddress::Btc( - cf_chains::btc::deposit_address::DepositAddress::new(btc_key.current, 0) - .script_pubkey(), - ); + let EpochKey { key, .. } = BitcoinThresholdSigner::active_epoch_key() + .expect("We should always have a for the current epoch."); + let deposit_address = DepositAddress::new( + key.current, + private_channel_id.try_into().map_err( + // TODO: Ensure this can't happen. + |_| DispatchErrorWithMessage::Other("Private channel id out of bounds.".into()) + )? + ) + .script_pubkey() + .to_address(&Environment::network_environment().into()); Ok(VaultSwapDetails::Bitcoin { nulldata_utxo: encode_swap_params_in_nulldata_utxo(params).raw(), diff --git a/state-chain/runtime/src/runtime_apis.rs b/state-chain/runtime/src/runtime_apis.rs index a8e908e1a4..b92a4638e1 100644 --- a/state-chain/runtime/src/runtime_apis.rs +++ b/state-chain/runtime/src/runtime_apis.rs @@ -36,8 +36,25 @@ use sp_std::{ type VanityName = Vec; #[derive(PartialEq, Eq, Clone, Encode, Decode, TypeInfo, Serialize, Deserialize)] -pub enum VaultSwapDetails { - Bitcoin { nulldata_utxo: Vec, deposit_address: ForeignChainAddress }, +#[serde(tag = "chain")] +pub enum VaultSwapDetails { + Bitcoin { + #[serde(with = "sp_core::bytes")] + nulldata_utxo: Vec, + deposit_address: BtcAddress, + }, +} + +impl VaultSwapDetails { + pub fn map_btc_address(self, f: F) -> VaultSwapDetails + where + F: FnOnce(BtcAddress) -> T, + { + match self { + VaultSwapDetails::Bitcoin { nulldata_utxo, deposit_address } => + VaultSwapDetails::Bitcoin { nulldata_utxo, deposit_address: f(deposit_address) }, + } + } } #[derive(PartialEq, Eq, Clone, Encode, Decode, Copy, TypeInfo, Serialize, Deserialize)] @@ -386,7 +403,7 @@ decl_runtime_apis!( boost_fee: BasisPoints, affiliate_fees: Affiliates, dca_parameters: Option, - ) -> Result; + ) -> Result, DispatchErrorWithMessage>; fn cf_get_open_deposit_channels(account_id: Option) -> ChainAccounts; fn cf_tainted_transaction_events() -> TaintedTransactionEvents; } From 6a6d84e2c60f9eebf6141d7cfcd6d89436485707 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 11 Nov 2024 12:28:27 +0100 Subject: [PATCH 15/15] fix: comments + clippy --- .../state_chain_observer/client/base_rpc_api.rs | 2 +- state-chain/runtime/src/lib.rs | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/engine/src/state_chain_observer/client/base_rpc_api.rs b/engine/src/state_chain_observer/client/base_rpc_api.rs index d909339af4..7eade20f09 100644 --- a/engine/src/state_chain_observer/client/base_rpc_api.rs +++ b/engine/src/state_chain_observer/client/base_rpc_api.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; - +use cf_primitives::BlockNumber; use jsonrpsee::core::client::{ClientT, Subscription, SubscriptionClientT}; use sc_transaction_pool_api::TransactionStatus; use sp_core::{ diff --git a/state-chain/runtime/src/lib.rs b/state-chain/runtime/src/lib.rs index cc2604330a..ad72c347ae 100644 --- a/state-chain/runtime/src/lib.rs +++ b/state-chain/runtime/src/lib.rs @@ -62,7 +62,8 @@ use cf_primitives::{ }; use cf_traits::{ AdjustedFeeEstimationApi, AssetConverter, BalanceApi, DummyEgressSuccessWitnesser, - DummyIngressSource, GetBlockHeight, NoLimit, SwapLimits, SwapLimitsProvider, + DummyIngressSource, EpochKey, GetBlockHeight, KeyProvider, NoLimit, SwapLimits, + SwapLimitsProvider, }; use codec::{alloc::string::ToString, Decode, Encode}; use core::ops::Range; @@ -2132,7 +2133,6 @@ impl_runtime_apis! { // Encode swap match ForeignChain::from(source_asset) { ForeignChain::Bitcoin => { - use cf_traits::{KeyProvider, EpochKey}; use cf_chains::btc::deposit_address::DepositAddress; let private_channel_id = pallet_cf_swapping::BrokerPrivateBtcChannels::::get(&broker_id) @@ -2163,7 +2163,7 @@ impl_runtime_apis! { }; let EpochKey { key, .. } = BitcoinThresholdSigner::active_epoch_key() - .expect("We should always have a for the current epoch."); + .expect("We should always have a key for the current epoch."); let deposit_address = DepositAddress::new( key.current, private_channel_id.try_into().map_err( @@ -2261,16 +2261,15 @@ impl_runtime_apis! { let btc_ceremonies = pallet_cf_threshold_signature::PendingCeremonies::::iter_values().map(|ceremony|{ ceremony.request_context.request_id }).collect::>(); - let btc_key = pallet_cf_threshold_signature::Pallet::::keys( - pallet_cf_threshold_signature::Pallet::::current_key_epoch() - .expect("We should always have an epoch set")).expect("We should always have a key set for the current epoch"); + let EpochKey { key, .. } = pallet_cf_threshold_signature::Pallet::::active_epoch_key() + .expect("We should always have a key for the current epoch"); for ceremony in btc_ceremonies { if let RuntimeCall::BitcoinBroadcaster(pallet_cf_broadcast::pallet::Call::on_signature_ready{ api_call, ..}) = pallet_cf_threshold_signature::RequestCallback::::get(ceremony).unwrap() { if let BitcoinApi::BatchTransfer(batch_transfer) = *api_call { for output in batch_transfer.bitcoin_transaction.outputs { if [ - ScriptPubkey::Taproot(btc_key.previous.unwrap_or_default()), - ScriptPubkey::Taproot(btc_key.current), + ScriptPubkey::Taproot(key.previous.unwrap_or_default()), + ScriptPubkey::Taproot(key.current), ] .contains(&output.script_pubkey) {