Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions client/src/bin/space-cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
},
/// Send the specified amount of BTC to the given name or address
#[command(
name = "send",
Expand Down Expand Up @@ -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?;
Expand Down
59 changes: 59 additions & 0 deletions client/src/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,13 @@ pub trait Rpc {

#[method(name = "walletgetbalance")]
async fn wallet_get_balance(&self, wallet: &str) -> Result<Balance, ErrorObjectOwned>;

#[method(name = "estimatefee")]
async fn estimate_fee(
&self,
conf_target: u32,
estimate_mode: Option<String>,
) -> Result<FeeEstimateResponse, ErrorObjectOwned>;
}

#[derive(Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -433,6 +440,12 @@ pub struct ProofResult {
pub proof: Vec<u8>,
}

#[derive(Clone, Serialize, Deserialize)]
pub struct FeeEstimateResponse {
pub feerate_sat_vb: u64,
pub blocks: u64,
}

fn serialize_hash<S>(
bytes: &spaces_protocol::hasher::Hash,
serializer: S,
Expand Down Expand Up @@ -1123,6 +1136,52 @@ impl RpcServer for RpcServerImpl {
.await
.map_err(|error| ErrorObjectOwned::owned(-1, error.to_string(), None::<String>))
}

async fn estimate_fee(
&self,
conf_target: u32,
estimate_mode: Option<String>,
) -> Result<FeeEstimateResponse, ErrorObjectOwned> {
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::<serde_json::Value>(&blocking_client, &estimate_req)
})
.await
.map_err(|e| ErrorObjectOwned::owned(-1, format!("Task join error: {}", e), None::<String>))?;

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::<String>,
))
}
}
Err(e) => Err(ErrorObjectOwned::owned(
-1,
format!("RPC error: {}", e),
None::<String>,
)),
}
}
}

impl AsyncChainState {
Expand Down
Loading