From 40c9ec683bd641871098669184cf3c918b829a5e Mon Sep 17 00:00:00 2001 From: horologger Date: Sun, 14 Dec 2025 11:23:03 -0500 Subject: [PATCH] Add fee estimation RPC method and CLI command - Add estimatefee RPC method to expose Bitcoin fee estimation through spaced - Add FeeEstimateResponse struct with feerate_sat_vb and blocks fields - Implement estimate_fee in RpcServerImpl using Bitcoin Core's estimatesmartfee - Add estimatefee subcommand to space-cli for easy command-line access - Support confirmation targets (1-1008 blocks) and estimation modes (unset/conservative/economical) - Convert fee rates from BTC/kB to sat/vB for consistent output format --- client/src/bin/space-cli.rs | 22 ++++++++++++++ client/src/rpc.rs | 59 +++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/client/src/bin/space-cli.rs b/client/src/bin/space-cli.rs index 0118960..109256a 100644 --- a/client/src/bin/space-cli.rs +++ b/client/src/bin/space-cli.rs @@ -180,6 +180,16 @@ enum Commands { #[arg(default_value = "0")] target: usize, }, + /// Estimate fee rate for a given confirmation target + #[command(name = "estimatefee")] + EstimateFee { + /// Target number of blocks for confirmation (1-1008) + #[arg(default_value = "6")] + conf_target: u32, + /// Fee estimation mode: unset, conservative, or economical + #[arg(long, short)] + mode: Option, + }, /// Send the specified amount of BTC to the given name or address #[command( name = "send", @@ -575,6 +585,18 @@ async fn handle_commands(cli: &SpaceCli, command: Commands) -> Result<(), Client let response = cli.client.estimate_bid(target).await?; println!("{} sat", Amount::from_sat(response).to_sat()); } + Commands::EstimateFee { conf_target, mode } => { + let response = cli.client.estimate_fee(conf_target, mode.clone()).await?; + match cli.format { + Format::Text => { + println!("Fee rate: {} sat/vB", response.feerate_sat_vb); + println!("Blocks: {}", response.blocks); + } + Format::Json => { + println!("{}", serde_json::to_string_pretty(&response)?); + } + } + } Commands::GetSpace { space } => { let space = normalize_space(&space); let response = cli.client.get_space(&space).await?; diff --git a/client/src/rpc.rs b/client/src/rpc.rs index a3be3fa..bd07e2b 100644 --- a/client/src/rpc.rs +++ b/client/src/rpc.rs @@ -340,6 +340,13 @@ pub trait Rpc { #[method(name = "walletgetbalance")] async fn wallet_get_balance(&self, wallet: &str) -> Result; + + #[method(name = "estimatefee")] + async fn estimate_fee( + &self, + conf_target: u32, + estimate_mode: Option, + ) -> Result; } #[derive(Clone, Serialize, Deserialize)] @@ -433,6 +440,12 @@ pub struct ProofResult { pub proof: Vec, } +#[derive(Clone, Serialize, Deserialize)] +pub struct FeeEstimateResponse { + pub feerate_sat_vb: u64, + pub blocks: u64, +} + fn serialize_hash( bytes: &spaces_protocol::hasher::Hash, serializer: S, @@ -1123,6 +1136,52 @@ impl RpcServer for RpcServerImpl { .await .map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::)) } + + async fn estimate_fee( + &self, + conf_target: u32, + estimate_mode: Option, + ) -> Result { + let mode = estimate_mode.unwrap_or_else(|| "unset".to_string()); + let params = serde_json::json!([conf_target, mode]); + let rpc = self.wallet_manager.rpc.clone(); + + let estimate_req = rpc.make_request("estimatesmartfee", params); + + // Use spawn_blocking to handle the blocking RPC call in async context + let result = tokio::task::spawn_blocking(move || { + let blocking_client = reqwest::blocking::Client::new(); + rpc.send_json_blocking::(&blocking_client, &estimate_req) + }) + .await + .map_err(|e| ErrorObjectOwned::owned(-1, format!("Task join error: {}", e), None::))?; + + match result { + Ok(res) => { + if let Some(fee_rate) = res["feerate"].as_f64() { + // Convert BTC/kB to sat/vB + let fee_rate_sat_vb = (fee_rate * 100_000.0).ceil() as u64; + let blocks = res["blocks"].as_u64().unwrap_or(conf_target as u64); + + Ok(FeeEstimateResponse { + feerate_sat_vb: fee_rate_sat_vb, + blocks, + }) + } else { + Err(ErrorObjectOwned::owned( + -1, + "Fee estimation unavailable: no feerate in response".to_string(), + None::, + )) + } + } + Err(e) => Err(ErrorObjectOwned::owned( + -1, + format!("RPC error: {}", e), + None::, + )), + } + } } impl AsyncChainState {