diff --git a/src/cli/deploy.rs b/src/cli/deploy.rs index 2ffda00..06b9a18 100644 --- a/src/cli/deploy.rs +++ b/src/cli/deploy.rs @@ -74,7 +74,7 @@ impl Args { NetworkInfo::new(self.ckb.network, self.ckb.ckb_endpoint.as_str().to_owned()); let configuration = { let mut tmp = TransactionBuilderConfiguration::new_with_network(network_info.clone())?; - tmp.fee_rate = self.ckb.fee_rate; + tmp.fee_rate = self.ckb.fee_rate()?; tmp }; diff --git a/src/cli/init.rs b/src/cli/init.rs index 97f2f7b..9612273 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -134,7 +134,7 @@ impl Args { NetworkInfo::new(self.ckb.network, self.ckb.ckb_endpoint.as_str().to_owned()); let configuration = { let mut tmp = TransactionBuilderConfiguration::new_with_network(network_info.clone())?; - tmp.fee_rate = self.ckb.fee_rate; + tmp.fee_rate = self.ckb.fee_rate()?; tmp }; diff --git a/src/cli/mod.rs b/src/cli/mod.rs index fa2e143..96f3d80 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,12 +1,14 @@ //! The command line argument. use ckb_sdk::{rpc::CkbRpcClient, types::NetworkType}; -use clap::{Parser, Subcommand}; +use ckb_types::core::FeeRate; +use clap::{Args, Parser, Subcommand}; use clap_verbosity_flag::{InfoLevel, Verbosity}; use url::Url; use crate::{ components::BitcoinClient, + prelude::*, result::Result, utilities::{value_parsers, Key256Bits}, }; @@ -66,11 +68,10 @@ pub struct CkbArgs { value_parser = value_parsers::NetworkTypeValueParser, default_value = "testnet" )] - pub network: NetworkType, + pub(crate) network: NetworkType, - /// The fee rate for CKB transactions. - #[arg(long = "ckb-fee-rate", default_value = "1000")] - pub(crate) fee_rate: u64, + #[command(flatten)] + pub(crate) fee_rate: FeeRateArgs, /// A binary file, which contains a secp256k1 private key. /// This private key will be used to provide all CKBytes. @@ -92,7 +93,39 @@ pub struct CkbRoArgs { value_parser = value_parsers::NetworkTypeValueParser, default_value = "testnet" )] - pub network: NetworkType, + pub(crate) network: NetworkType, +} + +#[derive(Args)] +#[group(multiple = false)] +pub struct FeeRateArgs { + /// The fixed fee rate for CKB transactions. + #[arg( + group = "fixed-fee-rate", + conflicts_with = "dynamic-fee-rate", + long = "ckb-fee-rate", + default_value = "1000" + )] + fixed_value: u64, + + /// [Experimental] Enable dynamic fee rate for CKB transactions. + /// + /// The actual fee rate will be the `median` fee rate which is fetched through the CKB RPC method `get_fee_rate_statistics`. + /// + /// For security, a hard limit is required. + /// When the returned dynamic fee rate is larger than the hard limit, the hard limit will be used. + /// + /// ### Warning + /// + /// Users have to make sure the remote CKB node they used are trustsed. + /// + /// Ref: + #[arg( + group = "dynamic-fee-rate", + conflicts_with = "fixed-fee-rate", + long = "enable-dynamic-ckb-fee-rate-with-limit" + )] + limit_for_dynamic: Option, } #[derive(Parser)] @@ -160,6 +193,29 @@ impl CkbArgs { pub fn client(&self) -> CkbRpcClient { CkbRpcClient::new(self.ckb_endpoint.as_str()) } + + pub fn fee_rate(&self) -> Result { + let value = if let Some(limit) = self.fee_rate.limit_for_dynamic { + let dynamic = self.client().dynamic_fee_rate()?; + log::info!("CKB fee rate: {} (dynamic)", FeeRate(dynamic)); + if dynamic > limit { + log::warn!( + "dynamic CKB fee rate {} is too large, it seems unreasonable;\ + so the upper limit {} will be used", + FeeRate(dynamic), + FeeRate(limit) + ); + limit + } else { + dynamic + } + } else { + let fixed = self.fee_rate.fixed_value; + log::info!("CKB fee rate: {} (fixed)", FeeRate(fixed)); + fixed + }; + Ok(value) + } } impl CkbRoArgs { diff --git a/src/cli/serve.rs b/src/cli/serve.rs index d789d4c..4177a8e 100644 --- a/src/cli/serve.rs +++ b/src/cli/serve.rs @@ -185,7 +185,7 @@ impl Args { NetworkInfo::new(self.ckb.network, self.ckb.ckb_endpoint.as_str().to_owned()); let configuration = { let mut tmp = TransactionBuilderConfiguration::new_with_network(network_info.clone())?; - tmp.fee_rate = self.ckb.fee_rate; + tmp.fee_rate = self.ckb.fee_rate()?; tmp }; @@ -357,7 +357,7 @@ impl Args { NetworkInfo::new(self.ckb.network, self.ckb.ckb_endpoint.as_str().to_owned()); let configuration = { let mut tmp = TransactionBuilderConfiguration::new_with_network(network_info.clone())?; - tmp.fee_rate = self.ckb.fee_rate; + tmp.fee_rate = self.ckb.fee_rate()?; tmp }; diff --git a/src/components/ckb_client.rs b/src/components/ckb_client.rs index b236c32..20a5df5 100644 --- a/src/components/ckb_client.rs +++ b/src/components/ckb_client.rs @@ -62,6 +62,7 @@ impl SpvInfoCell { } pub trait CkbRpcClientExtension { + fn dynamic_fee_rate(&self) -> Result; fn send_transaction_ext(&self, tx_json: TransactionView, dry_run: bool) -> Result; fn find_raw_spv_cells(&self, spv_type_script: Script) -> Result>; @@ -109,6 +110,17 @@ pub trait CkbRpcClientExtension { } impl CkbRpcClientExtension for CkbRpcClient { + fn dynamic_fee_rate(&self) -> Result { + self.get_fee_rate_statistics(None)? + .ok_or_else(|| { + let msg = "remote server replied null for \ + RPC method get_fee_rate_statistics(null)"; + Error::other(msg) + }) + .map(|resp| resp.median) + .map(Into::into) + } + fn send_transaction_ext(&self, tx_json: TransactionView, dry_run: bool) -> Result { if log::log_enabled!(log::Level::Trace) { match serde_json::to_string_pretty(&tx_json) { @@ -147,6 +159,8 @@ impl CkbRpcClientExtension for CkbRpcClient { })? .unpack(); + log::trace!("the type script of SPV cell is {spv_type_script}"); + let query = CellQueryOptions::new(spv_type_script, PrimaryScriptType::Type); let order = Order::Desc; let search_key = SearchKey::from(query);