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:
Daniel Karzel 2020-12-02 19:11:12 +11:00
parent 3dd8817884
commit 3e60a514c4
11 changed files with 1017 additions and 30 deletions

89
Cargo.lock generated
View File

@ -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",

View File

@ -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
View 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

View 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 = []

View 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();
}
}

View 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
View 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),
}

View 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();
}
}

View File

@ -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"] }

View File

@ -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]

View File

@ -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 }