diff --git a/core/lib/via_btc_client/src/client/mod.rs b/core/lib/via_btc_client/src/client/mod.rs index 3316d9f290bb..827bc310ddd3 100644 --- a/core/lib/via_btc_client/src/client/mod.rs +++ b/core/lib/via_btc_client/src/client/mod.rs @@ -1,5 +1,6 @@ use async_trait::async_trait; use bitcoin::{address::NetworkUnchecked, Address, Network, Txid}; +use bitcoincore_rpc::json::EstimateMode; use crate::{ traits::{BitcoinOps, BitcoinRpc}, @@ -10,6 +11,8 @@ mod rpc_client; #[allow(unused)] pub use rpc_client::BitcoinRpcClient; +use crate::types::BitcoinError; + #[allow(unused)] pub struct BitcoinClient { rpc: Box, @@ -87,4 +90,22 @@ impl BitcoinOps for BitcoinClient { let _block = self.rpc.get_block(block_height).await?; todo!() } + + async fn estimate_fee(&self, conf_target: u16) -> BitcoinClientResult { + let estimation = self + .rpc + .estimate_smart_fee(conf_target, Some(EstimateMode::Economical)) + .await?; + + match estimation.fee_rate { + Some(fee_rate) => Ok(fee_rate.to_sat()), + None => { + let err = match estimation.errors { + Some(errors) => errors.join(", "), + None => "Unknown error during fee estimation".to_string(), + }; + Err(BitcoinError::FeeEstimationFailed(err)) + } + } + } } diff --git a/core/lib/via_btc_client/src/client/rpc_client.rs b/core/lib/via_btc_client/src/client/rpc_client.rs index 24d150f014d2..91649edf5707 100644 --- a/core/lib/via_btc_client/src/client/rpc_client.rs +++ b/core/lib/via_btc_client/src/client/rpc_client.rs @@ -1,6 +1,8 @@ use async_trait::async_trait; use bitcoin::{Address, Block, OutPoint, Transaction, Txid}; -use bitcoincore_rpc::{Auth, Client, RpcApi}; +use bitcoincore_rpc::{ + bitcoincore_rpc_json::EstimateMode, json::EstimateSmartFeeResult, Auth, Client, RpcApi, +}; use crate::{traits::BitcoinRpc, types::BitcoinRpcResult}; @@ -79,50 +81,30 @@ impl BitcoinRpc for BitcoinRpcClient { .get_raw_transaction_info(txid, block_hash) .map_err(|e| e.into()) } + + async fn estimate_smart_fee( + &self, + conf_target: u16, + estimate_mode: Option, + ) -> BitcoinRpcResult { + self.client + .estimate_smart_fee(conf_target, estimate_mode) + .map_err(|e| e.into()) + } } #[cfg(test)] mod tests { use std::str::FromStr; - use bitcoin::Network; use tokio; use super::*; - use crate::{regtest::BitcoinRegtest, traits::BitcoinRpc}; - - struct TestContext { - _regtest: BitcoinRegtest, - client: BitcoinRpcClient, - test_address: Address, - } - - impl TestContext { - async fn setup() -> Self { - let regtest = BitcoinRegtest::new().expect("Failed to create BitcoinRegtest"); - regtest.run().expect("Failed to run Bitcoin regtest"); - - let url = regtest.get_url(); - let client = BitcoinRpcClient::new(&url, "rpcuser", "rpcpassword") - .expect("Failed to create BitcoinRpcClient"); - - // some random address - let test_address = Address::from_str("bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080") - .unwrap() - .require_network(Network::Regtest) - .unwrap(); - - Self { - _regtest: regtest, - client, - test_address, - } - } - } + use crate::{regtest::TestContext, traits::BitcoinRpc}; #[tokio::test] async fn test_get_balance() { - let context = TestContext::setup().await; + let context = TestContext::setup(None).await; let balance = context .client .get_balance(&context.test_address) @@ -133,21 +115,21 @@ mod tests { #[tokio::test] async fn test_get_block_count() { - let context = TestContext::setup().await; + let context = TestContext::setup(None).await; let block_count = context.client.get_block_count().await.unwrap(); assert!(block_count > 100, "Block count should be greater than 100"); } #[tokio::test] async fn test_get_block_size() { - let context = TestContext::setup().await; + let context = TestContext::setup(None).await; let block = context.client.get_block(1).await.unwrap(); assert_eq!(block.total_size(), 248usize, "Block version should be 1"); } #[tokio::test] async fn test_list_unspent() { - let context = TestContext::setup().await; + let context = TestContext::setup(None).await; let unspent = context .client .list_unspent(&context.test_address) @@ -161,7 +143,7 @@ mod tests { #[tokio::test] async fn test_send_raw_transaction() { - let context = TestContext::setup().await; + let context = TestContext::setup(None).await; let dummy_tx_hex = "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0502cd010101ffffffff0200f2052a01000000160014a8a01aa2b9c7f00bbd59aabfb047e8f3a181d7ed0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000"; let result = context.client.send_raw_transaction(dummy_tx_hex).await; assert!(result.is_err(), "Sending invalid transaction should fail"); @@ -169,7 +151,7 @@ mod tests { #[tokio::test] async fn test_get_transaction() { - let context = TestContext::setup().await; + let context = TestContext::setup(None).await; let dummy_txid = Txid::from_str("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b") .unwrap(); diff --git a/core/lib/via_btc_client/src/regtest.rs b/core/lib/via_btc_client/src/regtest.rs index 2447fb472373..5b19b765e513 100644 --- a/core/lib/via_btc_client/src/regtest.rs +++ b/core/lib/via_btc_client/src/regtest.rs @@ -1,16 +1,19 @@ use std::{env, fs, path::PathBuf, process::Command, thread, time::Duration}; +use bitcoin::{address::NetworkUnchecked, Address, Network}; use bitcoincore_rpc::{Auth, Client}; use rand::Rng; use tempfile::TempDir; +use crate::client::BitcoinRpcClient; + const COMPOSE_TEMPLATE_PATH: &str = concat!( env!("CARGO_MANIFEST_DIR"), "/tests/docker-compose-btc-template.yml" ); #[allow(unused)] -pub struct BitcoinRegtest { +pub(crate) struct BitcoinRegtest { temp_dir: TempDir, compose_file: PathBuf, rpc_port: u16, @@ -30,13 +33,13 @@ impl BitcoinRegtest { }) } - pub fn generate_compose_file(&self) -> std::io::Result<()> { + fn generate_compose_file(&self) -> std::io::Result<()> { let template = fs::read_to_string(COMPOSE_TEMPLATE_PATH)?; let compose_content = template.replace("{RPC_PORT}", &self.rpc_port.to_string()); fs::write(&self.compose_file, compose_content) } - pub fn run(&self) -> std::io::Result<()> { + fn run(&self) -> std::io::Result<()> { self.generate_compose_file()?; Command::new("docker") @@ -56,7 +59,7 @@ impl BitcoinRegtest { Ok(()) } - pub fn stop(&self) -> std::io::Result<()> { + fn stop(&self) -> std::io::Result<()> { Command::new("docker") .args(&[ "compose", @@ -72,7 +75,7 @@ impl BitcoinRegtest { Ok(()) } - pub fn get_url(&self) -> String { + fn get_url(&self) -> String { format!("http://127.0.0.1:{}", self.rpc_port) } } @@ -85,6 +88,37 @@ impl Drop for BitcoinRegtest { } } +pub(crate) struct TestContext { + pub(crate) _regtest: BitcoinRegtest, + pub(crate) client: BitcoinRpcClient, + pub(crate) test_address: Address, +} + +impl TestContext { + pub(crate) async fn setup(address: Option<&str>) -> Self { + let regtest = BitcoinRegtest::new().expect("Failed to create BitcoinRegtest"); + regtest.run().expect("Failed to run Bitcoin regtest"); + + let url = regtest.get_url(); + let client = BitcoinRpcClient::new(&url, "rpcuser", "rpcpassword") + .expect("Failed to create BitcoinRpcClient"); + + // some random address + let test_address = address + .unwrap_or("bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080") + .parse::>() + .unwrap() + .require_network(Network::Regtest) + .unwrap(); + + Self { + _regtest: regtest, + client, + test_address, + } + } +} + #[cfg(test)] mod tests { use bitcoincore_rpc::RpcApi; diff --git a/core/lib/via_btc_client/src/traits.rs b/core/lib/via_btc_client/src/traits.rs index 225569dd8c41..e7588a7004cd 100644 --- a/core/lib/via_btc_client/src/traits.rs +++ b/core/lib/via_btc_client/src/traits.rs @@ -22,6 +22,7 @@ pub trait BitcoinOps: Send + Sync { async fn check_tx_confirmation(&self, txid: &str) -> types::BitcoinClientResult; async fn fetch_block_height(&self) -> types::BitcoinClientResult; async fn fetch_and_parse_block(&self, block_height: u128) -> types::BitcoinClientResult<&str>; + async fn estimate_fee(&self, conf_target: u16) -> types::BitcoinClientResult; } #[allow(dead_code)] @@ -42,6 +43,11 @@ pub trait BitcoinRpc: Send + Sync { txid: &Txid, block_hash: Option<&bitcoin::BlockHash>, ) -> types::BitcoinRpcResult; + async fn estimate_smart_fee( + &self, + conf_target: u16, + estimate_mode: Option, + ) -> types::BitcoinRpcResult; } #[allow(dead_code)] diff --git a/core/lib/via_btc_client/src/types.rs b/core/lib/via_btc_client/src/types.rs index 110bc661dfc7..07dd025409fc 100644 --- a/core/lib/via_btc_client/src/types.rs +++ b/core/lib/via_btc_client/src/types.rs @@ -24,6 +24,9 @@ pub enum BitcoinError { #[error("Transaction building error: {0}")] TransactionBuildingError(String), + #[error("Fee estimation error: {0}")] + FeeEstimationFailed(String), + #[error("Other error: {0}")] Other(String), }