mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-04-21 16:29:20 -04:00
Pull in bitcoin harness to be able to use @teizinger JSON RPC client
@d4nte adapted the harness, but I was not able to use that commit straight away because of bitcoin version conflicts. Pull the harness in for now until the mainnet swap is done, then potentially upgrade bitcoin.
This commit is contained in:
parent
3dd8817884
commit
3e60a514c4
89
Cargo.lock
generated
89
Cargo.lock
generated
@ -246,22 +246,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a32c9d2fa897cfbb0db45d71e3d2838666194abc4828c0f994e4b5c3bf85ba4"
|
||||
dependencies = [
|
||||
"bech32",
|
||||
"bitcoin_hashes",
|
||||
"bitcoin_hashes 0.7.6",
|
||||
"hex 0.3.2",
|
||||
"secp256k1",
|
||||
"secp256k1 0.17.2",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin"
|
||||
version = "0.25.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aefc9be9f17185f4ebccae6575d342063f775924d57df0000edb1880c0fb7095"
|
||||
dependencies = [
|
||||
"bech32",
|
||||
"bitcoin_hashes 0.9.4",
|
||||
"secp256k1 0.19.0",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin-harness"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/coblox/bitcoin-harness-rs?rev=3be644cd9512c157d3337a189298b8257ed54d04#3be644cd9512c157d3337a189298b8257ed54d04"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.12.3",
|
||||
"bitcoin",
|
||||
"bitcoin 0.23.0",
|
||||
"bitcoincore-rpc-json",
|
||||
"futures",
|
||||
"hex 0.4.2",
|
||||
"jsonrpc_client",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -282,12 +295,21 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoincore-rpc-json"
|
||||
version = "0.11.0"
|
||||
name = "bitcoin_hashes"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea6d55f23cd516d515ae10911164c603ea1040024670ec109715a20d7f6c9d0c"
|
||||
checksum = "0aaf87b776808e26ae93289bc7d025092b6d909c193f0cdee0b3a86e7bd3c776"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoincore-rpc-json"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76d488ec31e9cb6726c361be5160f7d2aaace89a0681acf1f476b8fada770b6e"
|
||||
dependencies = [
|
||||
"bitcoin 0.25.2",
|
||||
"hex 0.3.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -1551,6 +1573,28 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonrpc_client"
|
||||
version = "0.3.0"
|
||||
source = "git+https://github.com/thomaseizinger/rust-jsonrpc-client?rev=c7010817e0f86ab24b3dc10d6bb0463faa0aace4#c7010817e0f86ab24b3dc10d6bb0463faa0aace4"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"jsonrpc_client_macro",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonrpc_client_macro"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/thomaseizinger/rust-jsonrpc-client?rev=c7010817e0f86ab24b3dc10d6bb0463faa0aace4#c7010817e0f86ab24b3dc10d6bb0463faa0aace4"
|
||||
dependencies = [
|
||||
"quote 1.0.7",
|
||||
"syn 1.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keccak"
|
||||
version = "0.1.0"
|
||||
@ -1944,7 +1988,7 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58f196216a07ade53d91a263b3721057f1f0cfd6f95f3d324626f3f9cf7cdfce"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"bitcoin 0.23.0",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@ -2932,7 +2976,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2932dc07acd2066ff2e3921a4419606b220ba6cd03a9935123856cc534877056"
|
||||
dependencies = [
|
||||
"rand 0.6.5",
|
||||
"secp256k1-sys",
|
||||
"secp256k1-sys 0.1.2",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secp256k1"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6179428c22c73ac0fbb7b5579a56353ce78ba29759b3b8575183336ea74cdfb"
|
||||
dependencies = [
|
||||
"secp256k1-sys 0.3.0",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@ -2945,6 +2999,15 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secp256k1-sys"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11553d210db090930f4432bea123b31f70bbf693ace14504ea2a35e796c28dd2"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secp256kfun"
|
||||
version = "0.3.2-alpha.0"
|
||||
@ -2952,7 +3015,7 @@ source = "git+https://github.com/LLFourn/secp256kfun?rev=510d48ef6a2b19805f7f5c7
|
||||
dependencies = [
|
||||
"digest 0.9.0",
|
||||
"rand_core 0.5.1",
|
||||
"secp256k1",
|
||||
"secp256k1 0.17.2",
|
||||
"secp256kfun_parity_backend",
|
||||
"serde",
|
||||
"subtle 2.3.0",
|
||||
@ -3368,7 +3431,7 @@ dependencies = [
|
||||
"atty",
|
||||
"backoff",
|
||||
"base64 0.12.3",
|
||||
"bitcoin",
|
||||
"bitcoin 0.23.0",
|
||||
"bitcoin-harness",
|
||||
"conquer-once",
|
||||
"derivative",
|
||||
@ -4146,7 +4209,7 @@ dependencies = [
|
||||
"async-trait",
|
||||
"backoff",
|
||||
"base64 0.12.3",
|
||||
"bitcoin",
|
||||
"bitcoin 0.23.0",
|
||||
"bitcoin-harness",
|
||||
"cross-curve-dleq",
|
||||
"curve25519-dalek 2.1.0",
|
||||
|
@ -1,2 +1,2 @@
|
||||
[workspace]
|
||||
members = ["monero-harness", "xmr-btc", "swap"]
|
||||
members = ["monero-harness", "bitcoin-harness", "xmr-btc", "swap"]
|
||||
|
10
bitcoin-harness/.gitignore
vendored
Normal file
10
bitcoin-harness/.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
26
bitcoin-harness/Cargo.toml
Normal file
26
bitcoin-harness/Cargo.toml
Normal file
@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "bitcoin-harness"
|
||||
version = "0.1.0"
|
||||
authors = ["CoBloX Team <team@coblox.tech>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1"
|
||||
base64 = "0.12.3"
|
||||
bitcoin = { version = "0.23", features = ["use-serde"] }
|
||||
bitcoincore-rpc-json = "0.12"
|
||||
futures = "0.3.5"
|
||||
hex = "0.4.2"
|
||||
jsonrpc_client = { git = "https://github.com/thomaseizinger/rust-jsonrpc-client", rev = "c7010817e0f86ab24b3dc10d6bb0463faa0aace4", features = ["reqwest"] }
|
||||
reqwest = { version = "0.10", default-features = false, features = ["json", "native-tls"] }
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
testcontainers = "0.11"
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "0.2", default-features = false, features = ["blocking", "macros", "rt-core", "time"] }
|
||||
tracing = "0.1"
|
||||
url = "2"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
test-docker = []
|
255
bitcoin-harness/src/bitcoind_rpc.rs
Normal file
255
bitcoin-harness/src/bitcoind_rpc.rs
Normal file
@ -0,0 +1,255 @@
|
||||
//! An incomplete async bitcoind rpc client that supports multi-wallet features
|
||||
|
||||
use crate::bitcoind_rpc_api::{BitcoindRpcApi, PsbtBase64, WalletProcessPsbtResponse};
|
||||
use ::bitcoin::{hashes::hex::FromHex, Address, Amount, Network, Transaction, Txid};
|
||||
use bitcoincore_rpc_json::{FinalizePsbtResult, GetAddressInfoResult};
|
||||
use jsonrpc_client::{JsonRpcError, ResponsePayload, SendRequest};
|
||||
use reqwest::Url;
|
||||
use serde::{de::DeserializeOwned, Deserialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub const JSONRPC_VERSION: &str = "1.0";
|
||||
|
||||
#[jsonrpc_client::implement(BitcoindRpcApi)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Client {
|
||||
inner: reqwest::Client,
|
||||
base_url: reqwest::Url,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(url: Url) -> Self {
|
||||
Client {
|
||||
inner: reqwest::Client::new(),
|
||||
base_url: url,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_wallet(&self, wallet_name: &str) -> Result<Self> {
|
||||
Ok(Self {
|
||||
base_url: self
|
||||
.base_url
|
||||
.join(format!("/wallet/{}", wallet_name).as_str())?,
|
||||
..self.clone()
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn network(&self) -> Result<Network> {
|
||||
let blockchain_info = self.getblockchaininfo().await?;
|
||||
|
||||
let network = match blockchain_info.chain.as_str() {
|
||||
"main" => Network::Bitcoin,
|
||||
"test" => Network::Testnet,
|
||||
"regtest" => Network::Regtest,
|
||||
_ => return Err(Error::UnexpectedResponse),
|
||||
};
|
||||
|
||||
Ok(network)
|
||||
}
|
||||
|
||||
pub async fn median_time(&self) -> Result<u64> {
|
||||
let blockchain_info = self.getblockchaininfo().await?;
|
||||
|
||||
Ok(blockchain_info.median_time)
|
||||
}
|
||||
|
||||
pub async fn set_hd_seed(
|
||||
&self,
|
||||
wallet_name: &str,
|
||||
new_key_pool: Option<bool>,
|
||||
wif_private_key: Option<String>,
|
||||
) -> Result<()> {
|
||||
self.with_wallet(wallet_name)?
|
||||
.sethdseed(new_key_pool, wif_private_key)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_to_address(
|
||||
&self,
|
||||
wallet_name: &str,
|
||||
address: Address,
|
||||
amount: Amount,
|
||||
) -> Result<Txid> {
|
||||
let txid = self
|
||||
.with_wallet(wallet_name)?
|
||||
.sendtoaddress(address, amount.as_btc())
|
||||
.await?;
|
||||
let txid = Txid::from_hex(&txid)?;
|
||||
|
||||
Ok(txid)
|
||||
}
|
||||
|
||||
pub async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
|
||||
let hex: String = self.get_raw_transaction_rpc(txid, false).await?;
|
||||
let bytes: Vec<u8> = FromHex::from_hex(&hex)?;
|
||||
let transaction = bitcoin::consensus::encode::deserialize(&bytes)?;
|
||||
|
||||
Ok(transaction)
|
||||
}
|
||||
|
||||
pub async fn get_raw_transaction_verbose(
|
||||
&self,
|
||||
txid: Txid,
|
||||
) -> Result<GetRawTransactionVerboseResponse> {
|
||||
let res = self.get_raw_transaction_rpc(txid, true).await?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
async fn get_raw_transaction_rpc<R>(&self, txid: Txid, is_verbose: bool) -> Result<R>
|
||||
where
|
||||
R: std::fmt::Debug + DeserializeOwned,
|
||||
{
|
||||
let body = jsonrpc_client::Request::new_v2("getrawtransaction")
|
||||
.with_argument(txid)?
|
||||
.with_argument(is_verbose)?
|
||||
.serialize()?;
|
||||
|
||||
let payload: ResponsePayload<R> = self
|
||||
.inner
|
||||
.send_request::<R>(self.base_url.clone(), body)
|
||||
.await
|
||||
.map_err(::jsonrpc_client::Error::Client)?
|
||||
.payload;
|
||||
let response: std::result::Result<R, JsonRpcError> = payload.into();
|
||||
|
||||
Ok(response.map_err(::jsonrpc_client::Error::JsonRpc)?)
|
||||
}
|
||||
|
||||
pub async fn fund_psbt(
|
||||
&self,
|
||||
wallet_name: &str,
|
||||
inputs: &[bitcoincore_rpc_json::CreateRawTransactionInput],
|
||||
address: Address,
|
||||
amount: Amount,
|
||||
) -> Result<String> {
|
||||
let mut outputs_converted = HashMap::new();
|
||||
outputs_converted.insert(address.to_string(), amount.as_btc());
|
||||
let psbt = self
|
||||
.with_wallet(wallet_name)?
|
||||
.walletcreatefundedpsbt(inputs, outputs_converted)
|
||||
.await?;
|
||||
Ok(psbt.psbt)
|
||||
}
|
||||
|
||||
pub async fn join_psbts(&self, wallet_name: &str, psbts: &[String]) -> Result<PsbtBase64> {
|
||||
let psbt = self.with_wallet(wallet_name)?.joinpsbts(psbts).await?;
|
||||
Ok(psbt)
|
||||
}
|
||||
pub async fn wallet_process_psbt(
|
||||
&self,
|
||||
wallet_name: &str,
|
||||
psbt: PsbtBase64,
|
||||
) -> Result<WalletProcessPsbtResponse> {
|
||||
let psbt = self
|
||||
.with_wallet(wallet_name)?
|
||||
.walletprocesspsbt(psbt)
|
||||
.await?;
|
||||
Ok(psbt)
|
||||
}
|
||||
|
||||
pub async fn finalize_psbt(
|
||||
&self,
|
||||
wallet_name: &str,
|
||||
psbt: PsbtBase64,
|
||||
) -> Result<FinalizePsbtResult> {
|
||||
let psbt = self.with_wallet(wallet_name)?.finalizepsbt(psbt).await?;
|
||||
Ok(psbt)
|
||||
}
|
||||
|
||||
pub async fn address_info(
|
||||
&self,
|
||||
wallet_name: &str,
|
||||
address: &Address,
|
||||
) -> Result<GetAddressInfoResult> {
|
||||
let address_info = self
|
||||
.with_wallet(wallet_name)?
|
||||
.getaddressinfo(address)
|
||||
.await?;
|
||||
Ok(address_info)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("JSON Rpc Client: ")]
|
||||
JsonRpcClient(#[from] jsonrpc_client::Error<reqwest::Error>),
|
||||
#[error("Serde JSON: ")]
|
||||
SerdeJson(#[from] serde_json::Error),
|
||||
#[error("Parse amount: ")]
|
||||
ParseAmount(#[from] bitcoin::util::amount::ParseAmountError),
|
||||
#[error("Hex decode: ")]
|
||||
Hex(#[from] bitcoin::hashes::hex::Error),
|
||||
#[error("Bitcoin decode: ")]
|
||||
BitcoinDecode(#[from] bitcoin::consensus::encode::Error),
|
||||
// TODO: add more info to error
|
||||
#[error("Unexpected response: ")]
|
||||
UnexpectedResponse,
|
||||
#[error("Parse url: ")]
|
||||
ParseUrl(#[from] url::ParseError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct BlockchainInfo {
|
||||
chain: Network,
|
||||
mediantime: u32,
|
||||
}
|
||||
|
||||
/// Response to the RPC command `getrawtransaction`, when the second
|
||||
/// argument is set to `true`.
|
||||
///
|
||||
/// It only defines one field, but can be expanded to include all the
|
||||
/// fields returned by `bitcoind` (see:
|
||||
/// https://bitcoincore.org/en/doc/0.19.0/rpc/rawtransactions/getrawtransaction/)
|
||||
#[derive(Clone, Copy, Debug, Deserialize)]
|
||||
pub struct GetRawTransactionVerboseResponse {
|
||||
#[serde(rename = "blockhash")]
|
||||
pub block_hash: Option<bitcoin::BlockHash>,
|
||||
}
|
||||
|
||||
/// Response to the RPC command `getblock`.
|
||||
///
|
||||
/// It only defines one field, but can be expanded to include all the
|
||||
/// fields returned by `bitcoind` (see:
|
||||
/// https://bitcoincore.org/en/doc/0.19.0/rpc/blockchain/getblock/)
|
||||
#[derive(Copy, Clone, Debug, Deserialize)]
|
||||
pub struct GetBlockResponse {
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "test-docker"))]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::Bitcoind;
|
||||
use testcontainers::clients;
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_network_info() {
|
||||
let tc_client = clients::Cli::default();
|
||||
let (client, _container) = {
|
||||
let blockchain = Bitcoind::new(&tc_client, "0.19.1").unwrap();
|
||||
|
||||
(Client::new(blockchain.node_url.clone()), blockchain)
|
||||
};
|
||||
|
||||
let network = client.network().await.unwrap();
|
||||
|
||||
assert_eq!(network, Network::Regtest)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_median_time() {
|
||||
let tc_client = clients::Cli::default();
|
||||
let (client, _container) = {
|
||||
let blockchain = Bitcoind::new(&tc_client, "0.19.1").unwrap();
|
||||
|
||||
(Client::new(blockchain.node_url.clone()), blockchain)
|
||||
};
|
||||
|
||||
let _mediant_time = client.median_time().await.unwrap();
|
||||
}
|
||||
}
|
126
bitcoin-harness/src/bitcoind_rpc_api.rs
Normal file
126
bitcoin-harness/src/bitcoind_rpc_api.rs
Normal file
@ -0,0 +1,126 @@
|
||||
use bitcoin::{Address, BlockHash, Transaction, Txid};
|
||||
use bitcoincore_rpc_json::{
|
||||
FinalizePsbtResult, GetAddressInfoResult, GetBlockResult, GetBlockchainInfoResult,
|
||||
GetDescriptorInfoResult, GetTransactionResult, GetWalletInfoResult, ListUnspentResultEntry,
|
||||
LoadWalletResult, WalletCreateFundedPsbtResult,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[jsonrpc_client::api]
|
||||
#[async_trait::async_trait]
|
||||
pub trait BitcoindRpcApi {
|
||||
async fn createwallet(
|
||||
&self,
|
||||
wallet_name: &str,
|
||||
disable_private_keys: Option<bool>,
|
||||
blank: Option<bool>,
|
||||
passphrase: Option<String>,
|
||||
avoid_reuse: Option<bool>,
|
||||
) -> LoadWalletResult;
|
||||
|
||||
async fn deriveaddresses(&self, descriptor: &str, range: Option<[u64; 2]>) -> Vec<Address>;
|
||||
|
||||
async fn dumpwallet(&self, filename: &std::path::Path) -> DumpWalletResponse;
|
||||
|
||||
async fn finalizepsbt(&self, psbt: PsbtBase64) -> FinalizePsbtResult;
|
||||
|
||||
async fn generatetoaddress(
|
||||
&self,
|
||||
nblocks: u32,
|
||||
address: Address,
|
||||
max_tries: Option<u32>,
|
||||
) -> Vec<BlockHash>;
|
||||
|
||||
async fn getaddressinfo(&self, address: &Address) -> GetAddressInfoResult;
|
||||
|
||||
// TODO: Manual implementation to avoid odd "account" parameter
|
||||
async fn getbalance(
|
||||
&self,
|
||||
account: Account,
|
||||
minimum_confirmation: Option<u32>,
|
||||
include_watch_only: Option<bool>,
|
||||
avoid_reuse: Option<bool>,
|
||||
) -> f64;
|
||||
|
||||
async fn getblock(&self, block_hash: &bitcoin::BlockHash) -> GetBlockResult;
|
||||
|
||||
async fn getblockchaininfo(&self) -> GetBlockchainInfoResult;
|
||||
|
||||
async fn getblockcount(&self) -> u32;
|
||||
|
||||
async fn getdescriptorinfo(&self, descriptor: &str) -> GetDescriptorInfoResult;
|
||||
|
||||
async fn getnewaddress(&self, label: Option<String>, address_type: Option<String>) -> Address;
|
||||
|
||||
async fn gettransaction(&self, txid: Txid) -> GetTransactionResult;
|
||||
|
||||
async fn getwalletinfo(&self) -> GetWalletInfoResult;
|
||||
|
||||
async fn joinpsbts(&self, psbts: &[String]) -> PsbtBase64;
|
||||
|
||||
async fn listunspent(
|
||||
&self,
|
||||
min_conf: Option<u32>,
|
||||
max_conf: Option<u32>,
|
||||
addresses: Option<Vec<Address>>,
|
||||
include_unsafe: Option<bool>,
|
||||
) -> Vec<ListUnspentResultEntry>;
|
||||
|
||||
async fn listwallets(&self) -> Vec<String>;
|
||||
|
||||
async fn sendrawtransaction(&self, transaction: TransactionHex) -> String;
|
||||
|
||||
/// amount is btc
|
||||
async fn sendtoaddress(&self, address: Address, amount: f64) -> String;
|
||||
|
||||
async fn sethdseed(&self, new_key_pool: Option<bool>, wif_private_key: Option<String>) -> ();
|
||||
|
||||
/// Outputs are {address, btc amount}
|
||||
async fn walletcreatefundedpsbt(
|
||||
&self,
|
||||
inputs: &[bitcoincore_rpc_json::CreateRawTransactionInput],
|
||||
outputs: HashMap<String, f64>,
|
||||
) -> WalletCreateFundedPsbtResult;
|
||||
|
||||
async fn walletprocesspsbt(&self, psbt: PsbtBase64) -> WalletProcessPsbtResponse;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
pub struct DumpWalletResponse {
|
||||
pub filename: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct PsbtBase64(pub String);
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct WalletProcessPsbtResponse {
|
||||
psbt: String,
|
||||
complete: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename = "*")]
|
||||
pub struct Account;
|
||||
|
||||
impl From<WalletProcessPsbtResponse> for PsbtBase64 {
|
||||
fn from(processed_psbt: WalletProcessPsbtResponse) -> Self {
|
||||
Self(processed_psbt.psbt)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for PsbtBase64 {
|
||||
fn from(base64_string: String) -> Self {
|
||||
Self(base64_string)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct TransactionHex(String);
|
||||
|
||||
impl From<Transaction> for TransactionHex {
|
||||
fn from(tx: Transaction) -> Self {
|
||||
Self(bitcoin::consensus::encode::serialize_hex(&tx))
|
||||
}
|
||||
}
|
183
bitcoin-harness/src/lib.rs
Normal file
183
bitcoin-harness/src/lib.rs
Normal file
@ -0,0 +1,183 @@
|
||||
#![warn(
|
||||
unused_extern_crates,
|
||||
missing_debug_implementations,
|
||||
missing_copy_implementations,
|
||||
rust_2018_idioms,
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::fallible_impl_from,
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_possible_wrap,
|
||||
clippy::dbg_macro
|
||||
)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! # bitcoin-harness
|
||||
//! A simple lib to start a bitcoind container, generate blocks and funds
|
||||
//! addresses. Note: It uses tokio.
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ## Just connect to bitcoind and get the network
|
||||
//!
|
||||
//! ```rust
|
||||
//! use bitcoin_harness::{bitcoind_rpc, Bitcoind, Client};
|
||||
//!
|
||||
//! # #[tokio::main]
|
||||
//! # async fn main() {
|
||||
//! let tc_client = testcontainers::clients::Cli::default();
|
||||
//! let bitcoind = Bitcoind::new(&tc_client, "0.20.0").unwrap();
|
||||
//! let client = Client::new(bitcoind.node_url);
|
||||
//! let network = client.network().await.unwrap();
|
||||
//!
|
||||
//! assert_eq!(network, bitcoin::Network::Regtest)
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Create a wallet, fund it and get a UTXO
|
||||
//!
|
||||
//! ```rust
|
||||
//! use bitcoin_harness::{bitcoind_rpc, Bitcoind, Client, Wallet};
|
||||
//!
|
||||
//! # #[tokio::main]
|
||||
//! # async fn main() {
|
||||
//! let tc_client = testcontainers::clients::Cli::default();
|
||||
//! let bitcoind = Bitcoind::new(&tc_client, "0.19.1").unwrap();
|
||||
//! let client = Client::new(bitcoind.node_url.clone());
|
||||
//!
|
||||
//! bitcoind.init(5).await.unwrap();
|
||||
//!
|
||||
//! let wallet = Wallet::new("my_wallet", bitcoind.node_url.clone())
|
||||
//! .await
|
||||
//! .unwrap();
|
||||
//! let address = wallet.new_address().await.unwrap();
|
||||
//! let amount = bitcoin::Amount::from_btc(3.0).unwrap();
|
||||
//!
|
||||
//! bitcoind.mint(address, amount).await.unwrap();
|
||||
//!
|
||||
//! let balance = wallet.balance().await.unwrap();
|
||||
//!
|
||||
//! assert_eq!(balance, amount);
|
||||
//!
|
||||
//! let utxos = wallet.list_unspent().await.unwrap();
|
||||
//!
|
||||
//! assert_eq!(utxos.get(0).unwrap().amount, amount);
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
pub mod bitcoind_rpc;
|
||||
pub mod bitcoind_rpc_api;
|
||||
pub mod wallet;
|
||||
|
||||
use crate::bitcoind_rpc_api::BitcoindRpcApi;
|
||||
use reqwest::Url;
|
||||
use std::time::Duration;
|
||||
use testcontainers::{clients, images::coblox_bitcoincore::BitcoinCore, Container, Docker};
|
||||
|
||||
pub use crate::{bitcoind_rpc::Client, wallet::Wallet};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
const BITCOIND_RPC_PORT: u16 = 18443;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Bitcoind<'c> {
|
||||
pub container: Container<'c, clients::Cli, BitcoinCore>,
|
||||
pub node_url: Url,
|
||||
pub wallet_name: String,
|
||||
}
|
||||
|
||||
impl<'c> Bitcoind<'c> {
|
||||
/// Starts a new regtest bitcoind container
|
||||
pub fn new(client: &'c clients::Cli, tag: &str) -> Result<Self> {
|
||||
let container = client.run(BitcoinCore::default().with_tag(tag));
|
||||
let port = container
|
||||
.get_host_port(BITCOIND_RPC_PORT)
|
||||
.ok_or(Error::PortNotExposed(BITCOIND_RPC_PORT))?;
|
||||
|
||||
let auth = container.image().auth();
|
||||
let url = format!(
|
||||
"http://{}:{}@localhost:{}",
|
||||
&auth.username, &auth.password, port
|
||||
);
|
||||
let url = Url::parse(&url)?;
|
||||
|
||||
let wallet_name = String::from("testwallet");
|
||||
|
||||
Ok(Self {
|
||||
container,
|
||||
node_url: url,
|
||||
wallet_name,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a test wallet, generate enough block to fund it and activate
|
||||
/// segwit. Generate enough blocks to make the passed
|
||||
/// `spendable_quantity` spendable. Spawn a tokio thread to mine a new
|
||||
/// block every second.
|
||||
pub async fn init(&self, spendable_quantity: u32) -> Result<()> {
|
||||
let bitcoind_client = Client::new(self.node_url.clone());
|
||||
|
||||
bitcoind_client
|
||||
.createwallet(&self.wallet_name, None, None, None, None)
|
||||
.await?;
|
||||
|
||||
let reward_address = bitcoind_client
|
||||
.with_wallet(&self.wallet_name)?
|
||||
.getnewaddress(None, None)
|
||||
.await?;
|
||||
|
||||
bitcoind_client
|
||||
.generatetoaddress(101 + spendable_quantity, reward_address.clone(), None)
|
||||
.await?;
|
||||
let _ = tokio::spawn(mine(bitcoind_client, reward_address));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send Bitcoin to the specified address, limited to the spendable bitcoin
|
||||
/// quantity.
|
||||
pub async fn mint(&self, address: bitcoin::Address, amount: bitcoin::Amount) -> Result<()> {
|
||||
let bitcoind_client = Client::new(self.node_url.clone());
|
||||
|
||||
bitcoind_client
|
||||
.send_to_address(&self.wallet_name, address.clone(), amount)
|
||||
.await?;
|
||||
|
||||
// Confirm the transaction
|
||||
let reward_address = bitcoind_client
|
||||
.with_wallet(&self.wallet_name)?
|
||||
.getnewaddress(None, None)
|
||||
.await?;
|
||||
bitcoind_client
|
||||
.generatetoaddress(1, reward_address, None)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn container_id(&self) -> &str {
|
||||
self.container.id()
|
||||
}
|
||||
}
|
||||
|
||||
async fn mine(bitcoind_client: Client, reward_address: bitcoin::Address) -> Result<()> {
|
||||
loop {
|
||||
tokio::time::delay_for(Duration::from_secs(1)).await;
|
||||
bitcoind_client
|
||||
.generatetoaddress(1, reward_address.clone(), None)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Bitcoin Rpc: ")]
|
||||
BitcoindRpc(#[from] bitcoind_rpc::Error),
|
||||
#[error("Json Rpc: ")]
|
||||
JsonRpc(#[from] jsonrpc_client::Error<reqwest::Error>),
|
||||
#[error("Url Parsing: ")]
|
||||
UrlParseError(#[from] url::ParseError),
|
||||
#[error("Docker port not exposed: ")]
|
||||
PortNotExposed(u16),
|
||||
}
|
335
bitcoin-harness/src/wallet.rs
Normal file
335
bitcoin-harness/src/wallet.rs
Normal file
@ -0,0 +1,335 @@
|
||||
use crate::{
|
||||
bitcoind_rpc::{Client, Result},
|
||||
bitcoind_rpc_api::{Account, BitcoindRpcApi, PsbtBase64, WalletProcessPsbtResponse},
|
||||
};
|
||||
use bitcoin::{hashes::hex::FromHex, Address, Amount, Transaction, Txid};
|
||||
use bitcoincore_rpc_json::{
|
||||
FinalizePsbtResult, GetAddressInfoResult, GetTransactionResult, GetWalletInfoResult,
|
||||
ListUnspentResultEntry,
|
||||
};
|
||||
use std::convert::TryFrom;
|
||||
use url::Url;
|
||||
|
||||
/// A wrapper to bitcoind wallet
|
||||
#[derive(Debug)]
|
||||
pub struct Wallet {
|
||||
name: String,
|
||||
bitcoind_client: Client,
|
||||
}
|
||||
|
||||
impl Wallet {
|
||||
/// Create a wallet on the bitcoind instance or use the wallet with the same
|
||||
/// name if it exists.
|
||||
pub async fn new(name: &str, url: Url) -> Result<Self> {
|
||||
let bitcoind_client = Client::new(url);
|
||||
|
||||
let wallet = Self {
|
||||
name: name.to_string(),
|
||||
bitcoind_client,
|
||||
};
|
||||
|
||||
wallet.init().await?;
|
||||
|
||||
Ok(wallet)
|
||||
}
|
||||
|
||||
async fn init(&self) -> Result<()> {
|
||||
match self.info().await {
|
||||
Err(_) => {
|
||||
self.bitcoind_client
|
||||
.createwallet(&self.name, None, None, None, None)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
Ok(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn info(&self) -> Result<GetWalletInfoResult> {
|
||||
Ok(self
|
||||
.bitcoind_client
|
||||
.with_wallet(&self.name)?
|
||||
.getwalletinfo()
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn median_time(&self) -> Result<u64> {
|
||||
Ok(self.bitcoind_client.median_time().await?)
|
||||
}
|
||||
|
||||
pub async fn block_height(&self) -> Result<u32> {
|
||||
Ok(self.bitcoind_client.getblockcount().await?)
|
||||
}
|
||||
|
||||
pub async fn new_address(&self) -> Result<Address> {
|
||||
Ok(self
|
||||
.bitcoind_client
|
||||
.with_wallet(&self.name)?
|
||||
.getnewaddress(None, Some("bech32".into()))
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn balance(&self) -> Result<Amount> {
|
||||
let response = self
|
||||
.bitcoind_client
|
||||
.with_wallet(&self.name)?
|
||||
.getbalance(Account, None, None, None)
|
||||
.await?;
|
||||
let amount = Amount::from_btc(response)?;
|
||||
Ok(amount)
|
||||
}
|
||||
|
||||
pub async fn send_to_address(&self, address: Address, amount: Amount) -> Result<Txid> {
|
||||
let txid = self
|
||||
.bitcoind_client
|
||||
.with_wallet(&self.name)?
|
||||
.sendtoaddress(address, amount.as_btc())
|
||||
.await?;
|
||||
let txid = Txid::from_hex(&txid)?;
|
||||
|
||||
Ok(txid)
|
||||
}
|
||||
|
||||
pub async fn send_raw_transaction(&self, transaction: Transaction) -> Result<Txid> {
|
||||
let txid = self
|
||||
.bitcoind_client
|
||||
.with_wallet(&self.name)?
|
||||
.sendrawtransaction(transaction.into())
|
||||
.await?;
|
||||
let txid = Txid::from_hex(&txid)?;
|
||||
Ok(txid)
|
||||
}
|
||||
|
||||
pub async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
|
||||
self.bitcoind_client.get_raw_transaction(txid).await
|
||||
}
|
||||
|
||||
pub async fn get_wallet_transaction(&self, txid: Txid) -> Result<GetTransactionResult> {
|
||||
let res = self
|
||||
.bitcoind_client
|
||||
.with_wallet(&self.name)?
|
||||
.gettransaction(txid)
|
||||
.await?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn address_info(&self, address: &Address) -> Result<GetAddressInfoResult> {
|
||||
self.bitcoind_client.address_info(&self.name, address).await
|
||||
}
|
||||
|
||||
pub async fn list_unspent(&self) -> Result<Vec<ListUnspentResultEntry>> {
|
||||
let unspents = self
|
||||
.bitcoind_client
|
||||
.with_wallet(&self.name)?
|
||||
.listunspent(None, None, None, None)
|
||||
.await?;
|
||||
Ok(unspents)
|
||||
}
|
||||
|
||||
pub async fn fund_psbt(&self, address: Address, amount: Amount) -> Result<String> {
|
||||
self.bitcoind_client
|
||||
.fund_psbt(&self.name, &[], address, amount)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn join_psbts(&self, psbts: &[String]) -> Result<PsbtBase64> {
|
||||
self.bitcoind_client.join_psbts(&self.name, psbts).await
|
||||
}
|
||||
|
||||
pub async fn wallet_process_psbt(&self, psbt: PsbtBase64) -> Result<WalletProcessPsbtResponse> {
|
||||
self.bitcoind_client
|
||||
.wallet_process_psbt(&self.name, psbt)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn finalize_psbt(&self, psbt: PsbtBase64) -> Result<FinalizePsbtResult> {
|
||||
self.bitcoind_client.finalize_psbt(&self.name, psbt).await
|
||||
}
|
||||
|
||||
pub async fn transaction_block_height(&self, txid: Txid) -> Result<Option<u32>> {
|
||||
let res = self
|
||||
.bitcoind_client
|
||||
.get_raw_transaction_verbose(txid)
|
||||
.await?;
|
||||
|
||||
let block_hash = match res.block_hash {
|
||||
Some(block_hash) => block_hash,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let res = self.bitcoind_client.getblock(&block_hash).await?;
|
||||
|
||||
// TODO: This was changed to u32 because down the road we needed it as u32 (and
|
||||
// the height should be sufficient as u32)
|
||||
Ok(Some(
|
||||
u32::try_from(res.height).expect("can cast block-height to u32"),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{Bitcoind, Wallet};
|
||||
use bitcoin::{util::psbt::PartiallySignedTransaction, Amount, Transaction, TxOut};
|
||||
use tokio::time::delay_for;
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_wallet_transaction() {
|
||||
let tc_client = testcontainers::clients::Cli::default();
|
||||
let bitcoind = Bitcoind::new(&tc_client, "0.19.1").unwrap();
|
||||
bitcoind.init(5).await.unwrap();
|
||||
|
||||
let wallet = Wallet::new("wallet", bitcoind.node_url.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
let mint_address = wallet.new_address().await.unwrap();
|
||||
let mint_amount = bitcoin::Amount::from_btc(3.0).unwrap();
|
||||
bitcoind.mint(mint_address, mint_amount).await.unwrap();
|
||||
|
||||
let pay_address = wallet.new_address().await.unwrap();
|
||||
let pay_amount = bitcoin::Amount::from_btc(1.0).unwrap();
|
||||
let txid = wallet
|
||||
.send_to_address(pay_address, pay_amount)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let _res = wallet.get_wallet_transaction(txid).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn two_party_psbt_test() {
|
||||
let tc_client = testcontainers::clients::Cli::default();
|
||||
let bitcoind = Bitcoind::new(&tc_client, "0.19.1").unwrap();
|
||||
bitcoind.init(5).await.unwrap();
|
||||
|
||||
let alice = Wallet::new("alice", bitcoind.node_url.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
let address = alice.new_address().await.unwrap();
|
||||
let amount = bitcoin::Amount::from_btc(3.0).unwrap();
|
||||
bitcoind.mint(address, amount).await.unwrap();
|
||||
let joined_address = alice.new_address().await.unwrap();
|
||||
let alice_result = alice
|
||||
.fund_psbt(joined_address.clone(), Amount::from_btc(1.0).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let bob = Wallet::new("bob", bitcoind.node_url.clone()).await.unwrap();
|
||||
let address = bob.new_address().await.unwrap();
|
||||
let amount = bitcoin::Amount::from_btc(3.0).unwrap();
|
||||
bitcoind.mint(address, amount).await.unwrap();
|
||||
let bob_psbt = bob
|
||||
.fund_psbt(joined_address.clone(), Amount::from_btc(1.0).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let joined_psbts = alice
|
||||
.join_psbts(&[alice_result.clone(), bob_psbt.clone()])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let partial_signed_bitcoin_transaction: PartiallySignedTransaction = {
|
||||
let as_hex = base64::decode(joined_psbts.0).unwrap();
|
||||
bitcoin::consensus::deserialize(&as_hex).unwrap()
|
||||
};
|
||||
|
||||
let transaction = partial_signed_bitcoin_transaction.extract_tx();
|
||||
let mut outputs = vec![];
|
||||
|
||||
transaction.output.iter().for_each(|output| {
|
||||
// filter out shared output
|
||||
if output.script_pubkey != joined_address.clone().script_pubkey() {
|
||||
outputs.push(output.clone());
|
||||
}
|
||||
});
|
||||
// add shared output with twice the btc to fit change addresses
|
||||
outputs.push(TxOut {
|
||||
value: Amount::from_btc(2.0).unwrap().as_sat(),
|
||||
script_pubkey: joined_address.clone().script_pubkey(),
|
||||
});
|
||||
|
||||
let transaction = Transaction {
|
||||
output: outputs,
|
||||
..transaction
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
transaction.input.len(),
|
||||
2,
|
||||
"We expect 2 inputs, one from alice, one from bob"
|
||||
);
|
||||
assert_eq!(
|
||||
transaction.output.len(),
|
||||
3,
|
||||
"We expect 3 outputs, change for alice, change for bob and shared address"
|
||||
);
|
||||
|
||||
let psbt = {
|
||||
let partial_signed_bitcoin_transaction =
|
||||
PartiallySignedTransaction::from_unsigned_tx(transaction).unwrap();
|
||||
let hex_vec = bitcoin::consensus::serialize(&partial_signed_bitcoin_transaction);
|
||||
base64::encode(hex_vec).into()
|
||||
};
|
||||
|
||||
let alice_signed_psbt = alice.wallet_process_psbt(psbt).await.unwrap();
|
||||
let bob_signed_psbt = bob
|
||||
.wallet_process_psbt(alice_signed_psbt.into())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let alice_finalized_psbt = alice.finalize_psbt(bob_signed_psbt.into()).await.unwrap();
|
||||
|
||||
let transaction = alice_finalized_psbt.transaction().unwrap().unwrap();
|
||||
let txid = alice.send_raw_transaction(transaction).await.unwrap();
|
||||
println!("Final tx_id: {:?}", txid);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn block_height() {
|
||||
let tc_client = testcontainers::clients::Cli::default();
|
||||
let bitcoind = Bitcoind::new(&tc_client, "0.19.1").unwrap();
|
||||
bitcoind.init(5).await.unwrap();
|
||||
|
||||
let wallet = Wallet::new("wallet", bitcoind.node_url.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let height_0 = wallet.block_height().await.unwrap();
|
||||
delay_for(Duration::from_secs(2)).await;
|
||||
|
||||
let height_1 = wallet.block_height().await.unwrap();
|
||||
|
||||
assert!(height_1 > height_0)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn transaction_block_height() {
|
||||
let tc_client = testcontainers::clients::Cli::default();
|
||||
let bitcoind = Bitcoind::new(&tc_client, "0.19.1").unwrap();
|
||||
bitcoind.init(5).await.unwrap();
|
||||
|
||||
let wallet = Wallet::new("wallet", bitcoind.node_url.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
let mint_address = wallet.new_address().await.unwrap();
|
||||
let mint_amount = bitcoin::Amount::from_btc(3.0).unwrap();
|
||||
bitcoind.mint(mint_address, mint_amount).await.unwrap();
|
||||
|
||||
let pay_address = wallet.new_address().await.unwrap();
|
||||
let pay_amount = bitcoin::Amount::from_btc(1.0).unwrap();
|
||||
let txid = wallet
|
||||
.send_to_address(pay_address, pay_amount)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// wait for the transaction to be included in a block, so that
|
||||
// it has a block height field assigned to it when calling
|
||||
// `getrawtransaction`
|
||||
delay_for(Duration::from_secs(2)).await;
|
||||
|
||||
let _res = wallet.transaction_block_height(txid).await.unwrap();
|
||||
}
|
||||
}
|
@ -13,7 +13,7 @@ atty = "0.2"
|
||||
backoff = { version = "0.2", features = ["tokio"] }
|
||||
base64 = "0.12"
|
||||
bitcoin = { version = "0.23", features = ["rand", "use-serde"] } # TODO: Upgrade other crates in this repo to use this version.
|
||||
bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "3be644cd9512c157d3337a189298b8257ed54d04" }
|
||||
bitcoin-harness = { path = "../bitcoin-harness" }
|
||||
conquer-once = "0.3"
|
||||
derivative = "2"
|
||||
ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", rev = "510d48ef6a2b19805f7f5c70c598e5b03f668e7a", features = ["libsecp_compat", "serde", "serialization"] }
|
||||
|
@ -1,11 +1,10 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
|
||||
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation};
|
||||
use bitcoin::util::psbt::PartiallySignedTransaction;
|
||||
use bitcoin_harness::bitcoind_rpc::PsbtBase64;
|
||||
use bitcoin_harness::bitcoind_rpc_api::PsbtBase64;
|
||||
use reqwest::Url;
|
||||
use std::time::Duration;
|
||||
use xmr_btc::bitcoin::{
|
||||
BlockHeight, BroadcastSignedTransaction, BuildTxLockPsbt, SignTxLock, TransactionBlockHeight,
|
||||
WatchForRawTransaction,
|
||||
@ -34,16 +33,6 @@ impl Wallet {
|
||||
pub async fn new_address(&self) -> Result<Address> {
|
||||
self.0.new_address().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn transaction_fee(&self, txid: Txid) -> Result<Amount> {
|
||||
let fee = self
|
||||
.0
|
||||
.get_wallet_transaction(txid)
|
||||
.await
|
||||
.map(|res| bitcoin::Amount::from_btc(-res.fee))??;
|
||||
|
||||
Ok(fee)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
@ -28,7 +28,7 @@ tracing = "0.1"
|
||||
[dev-dependencies]
|
||||
backoff = { version = "0.2", features = ["tokio"] }
|
||||
base64 = "0.12"
|
||||
bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "3be644cd9512c157d3337a189298b8257ed54d04" }
|
||||
bitcoin-harness = { path = "../bitcoin-harness" }
|
||||
futures = "0.3"
|
||||
monero-harness = { path = "../monero-harness" }
|
||||
reqwest = { version = "0.10", default-features = false }
|
||||
|
Loading…
x
Reference in New Issue
Block a user