Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: broker can encode btc smart contract call #5329

Merged
merged 10 commits into from
Oct 22, 2024
48 changes: 47 additions & 1 deletion api/bin/chainflip-broker-api/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
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,
SwapDepositAddress, SwapPayload, WithdrawFeesDetail,
};
use clap::Parser;
use custom_rpc::to_rpc_error;
Expand Down Expand Up @@ -48,6 +50,20 @@ pub trait Rpc {
asset: Asset,
destination_address: AddressString,
) -> RpcResult<WithdrawFeesDetail>;

#[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,
retry_duration: u32,
min_output_amount: NumberOrHex,
dca_parameters: Option<DcaParameters>,
boost_fee: Option<BasisPoints>,
affiliate_fees: Option<Affiliates<AccountId32>>,
) -> RpcResult<SwapPayload>;
}

pub struct RpcServerImpl {
Expand Down Expand Up @@ -120,6 +136,36 @@ impl RpcServer for RpcServerImpl {
.await
.map_err(to_rpc_error)?)
}

async fn request_swap_parameter_encoding(
&self,
source_asset: Asset,
destination_asset: Asset,
destination_address: AddressString,
broker_commission: BasisPoints,
retry_duration: u32,
min_output_amount: NumberOrHex,
dca_parameters: Option<DcaParameters>,
boost_fee: Option<BasisPoints>,
affiliate_fees: Option<Affiliates<AccountId32>>,
) -> RpcResult<SwapPayload> {
Ok(self
.api
.broker_api()
.request_swap_parameter_encoding(
source_asset,
destination_asset,
destination_address,
retry_duration,
try_parse_number_or_hex(min_output_amount).map_err(to_rpc_error)?,
boost_fee,
dca_parameters,
broker_commission,
affiliate_fees,
)
.await
.map_err(to_rpc_error)?)
}
}

#[derive(Parser, Debug, Clone, Default)]
Expand Down
86 changes: 83 additions & 3 deletions api/lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ use anyhow::{anyhow, bail, Context, Result};
use async_trait::async_trait;
use cf_chains::{
address::{try_from_encoded_address, EncodedAddress},
btc::smart_contract_encoding::{
encode_swap_params_in_nulldata_utxo, SharedCfParameters, UtxoEncodedData,
},
dot::PolkadotAccountId,
evm::to_evm_address,
sol::SolAddress,
CcmChannelMetadata, ChannelRefundParametersGeneric, ForeignChain, ForeignChainAddress,
};
pub use cf_primitives::{AccountRole, Affiliates, Asset, BasisPoints, ChannelId, SemVer};
use cf_primitives::{BlockNumber, DcaParameters, NetworkEnvironment, Price};
use cf_primitives::{AssetAmount, BlockNumber, DcaParameters, NetworkEnvironment, Price};
use pallet_cf_account_roles::MAX_LENGTH_FOR_VANITY_NAME;
use pallet_cf_governance::ExecutionMode;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -107,6 +110,11 @@ 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<StateChainClient>,
}
Expand Down Expand Up @@ -321,8 +329,8 @@ impl AddressString {
.map_err(|_| anyhow!("Failed to parse address"))
}

pub fn from_encoded_address(address: &EncodedAddress) -> Self {
Self(address.to_string())
pub fn from_encoded_address<T: std::borrow::Borrow<EncodedAddress>>(address: T) -> Self {
Self(address.borrow().to_string())
dandanlen marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down Expand Up @@ -515,6 +523,78 @@ 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,
input_asset: Asset,
output_asset: Asset,
output_address: AddressString,
retry_duration: BlockNumber,
min_output_amount: AssetAmount,
boost_fee: Option<BasisPoints>,
dca_parameters: Option<DcaParameters>,
broker_commission: BasisPoints,
affiliate_fees: Option<Affiliates<AccountId32>>,
) -> Result<SwapPayload> {
// Check if safe mode is active
let block_hash = self.base_rpc_api().latest_finalized_block_hash().await?;
let safe_mode = self
.storage_value::<pallet_cf_environment::RuntimeSafeMode<state_chain_runtime::Runtime>>(
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(input_asset) {
ForeignChain::Bitcoin => {
let params = UtxoEncodedData {
output_asset,
output_address: output_address
.try_parse_to_encoded_address(output_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]
Expand Down
36 changes: 36 additions & 0 deletions engine/src/state_chain_observer/client/base_rpc_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,19 @@ pub trait BaseRpcApi {
params: Option<Box<RawValue>>,
unsub: &str,
) -> RpcResult<Subscription<Box<RawValue>>>;

async fn validate_refund_params(
&self,
retry_duration: u32,
block_hash: Option<state_chain_runtime::Hash>,
) -> RpcResult<()>;

async fn validate_dca_params(
&self,
number_of_chunks: u32,
chunk_interval: u32,
block_hash: Option<state_chain_runtime::Hash>,
) -> RpcResult<()>;
}

pub struct BaseRpcClient<RawRpcClient> {
Expand Down Expand Up @@ -321,6 +334,29 @@ impl<RawRpcClient: RawRpcApi + Send + Sync> BaseRpcApi for BaseRpcClient<RawRpcC
.await
.map_err(to_rpc_error)
}

async fn validate_refund_params(
&self,
retry_duration: u32,
block_hash: Option<state_chain_runtime::Hash>,
) -> RpcResult<()> {
self.raw_rpc_client
.cf_validate_refund_params(retry_duration, block_hash)
.await
.map_err(to_rpc_error)
}

async fn validate_dca_params(
&self,
number_of_chunks: u32,
chunk_interval: u32,
block_hash: Option<state_chain_runtime::Hash>,
) -> RpcResult<()> {
self.raw_rpc_client
.cf_validate_dca_params(number_of_chunks, chunk_interval, block_hash)
.await
.map_err(to_rpc_error)
}
}

struct Params(Option<Box<RawValue>>);
Expand Down
2 changes: 1 addition & 1 deletion engine/src/witness/btc/smart_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ mod tests {
n: 0,
script_pubkey: vault_script.clone(),
},
// A nulddata UTXO encoding some swap parameters:
// A nulldata UTXO encoding some swap parameters:
VerboseTxOut {
value: Amount::from_sat(0),
n: 1,
Expand Down
1 change: 0 additions & 1 deletion state-chain/chains/src/btc/smart_contract_encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ pub struct SharedCfParameters {
pub boost_fee: u8,
}

#[allow(dead_code)]
pub fn encode_data_in_nulldata_utxo(data: &[u8]) -> Option<BitcoinScript> {
if data.len() > MAX_NULLDATA_LENGTH {
return None;
Expand Down
40 changes: 40 additions & 0 deletions state-chain/custom-rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,21 @@ pub trait CustomApi {
proposed_votes: Vec<u8>,
at: Option<state_chain_runtime::Hash>,
) -> RpcResult<Vec<u8>>;

#[method(name = "validate_dca_params")]
fn cf_validate_dca_params(
&self,
number_of_chunks: u32,
chunk_interval: u32,
at: Option<state_chain_runtime::Hash>,
) -> RpcResult<()>;

#[method(name = "validate_refund_params")]
fn cf_validate_refund_params(
&self,
retry_duration: u32,
at: Option<state_chain_runtime::Hash>,
) -> RpcResult<()>;
}

/// An RPC extension for the state chain node.
Expand Down Expand Up @@ -1973,6 +1988,31 @@ where
)
.map_err(to_rpc_error)
}

fn cf_validate_dca_params(
&self,
number_of_chunks: u32,
chunk_interval: u32,
at: Option<state_chain_runtime::Hash>,
) -> RpcResult<()> {
self.client
.runtime_api()
.cf_validate_dca_params(self.unwrap_or_best(at), number_of_chunks, chunk_interval)
.map_err(to_rpc_error)
.and_then(|result| result.map_err(map_dispatch_error))
}

fn cf_validate_refund_params(
&self,
retry_duration: u32,
at: Option<state_chain_runtime::Hash>,
) -> RpcResult<()> {
self.client
.runtime_api()
.cf_validate_refund_params(self.unwrap_or_best(at), retry_duration)
.map_err(to_rpc_error)
.and_then(|result| result.map_err(map_dispatch_error))
}
}

impl<C, B> CustomRpc<C, B>
Expand Down
31 changes: 3 additions & 28 deletions state-chain/pallets/cf-ingress-egress/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ use cf_chains::{
use cf_primitives::{
Asset, AssetAmount, BasisPoints, Beneficiaries, BoostPoolTier, BroadcastId, ChannelId,
DcaParameters, EgressCounter, EgressId, EpochIndex, ForeignChain, PrewitnessedDepositId,
SwapRequestId, ThresholdSignatureRequestId, TransactionHash, SWAP_DELAY_BLOCKS,
SwapRequestId, ThresholdSignatureRequestId, TransactionHash,
};
use cf_runtime_utilities::log_or_panic;
use cf_traits::{
Expand Down Expand Up @@ -694,11 +694,6 @@ pub mod pallet {
DepositChannelCreationDisabled,
/// The specified boost pool does not exist.
BoostPoolDoesNotExist,
/// Swap Retry duration is set above the max allowed.
SwapRetryDurationTooLong,
/// The number of chunks must be greater than 0, the interval must be greater than 2 and
/// the total duration of the swap request must be less then the max allowed.
InvalidDcaParameters,
/// CCM parameters from a contract swap failed validity check.
InvalidCcm,
}
Expand Down Expand Up @@ -2217,31 +2212,11 @@ impl<T: Config<I>, I: 'static> DepositApi<T::TargetChain> for Pallet<T, I> {
(ChannelId, ForeignChainAddress, <T::TargetChain as Chain>::ChainBlockNumber, Self::Amount),
DispatchError,
> {
let swap_limits = T::SwapLimitsProvider::get_swap_limits();
if let Some(params) = &refund_params {
ensure!(
params.retry_duration <= swap_limits.max_swap_retry_duration_blocks,
DispatchError::from(Error::<T, I>::SwapRetryDurationTooLong)
);
T::SwapLimitsProvider::validate_refund_params(params.retry_duration)?;
}

if let Some(params) = &dca_params {
if params.number_of_chunks != 1 {
ensure!(
params.number_of_chunks > 0 && params.chunk_interval >= SWAP_DELAY_BLOCKS,
DispatchError::from(Error::<T, I>::InvalidDcaParameters)
);
let total_swap_request_duration = params
.number_of_chunks
.saturating_sub(1)
.checked_mul(params.chunk_interval)
.ok_or(Error::<T, I>::InvalidDcaParameters)?;

ensure!(
total_swap_request_duration <= swap_limits.max_swap_request_duration_blocks,
DispatchError::from(Error::<T, I>::InvalidDcaParameters)
);
}
T::SwapLimitsProvider::validate_dca_params(params)?;
}

let (channel_id, deposit_address, expiry_height, channel_opening_fee) = Self::open_channel(
Expand Down
Loading
Loading