depolymerization

wire gateway for Bitcoin/Ethereum
Log | Files | Refs | Submodules | README | LICENSE

commit 92c4d2638fd272de0b60d759bdbc9f1ada01e27c
parent 1be9da930bd1d532109490a15fb97c27c52ce3ce
Author: Antoine A <>
Date:   Thu, 16 Dec 2021 17:35:19 +0100

Use custom simple RPC client

Diffstat:
MCargo.lock | 149++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mbtc-wire/Cargo.toml | 8++++++--
Mbtc-wire/src/bin/btc-wire-cli.rs | 32+++++++++++++-------------------
Mbtc-wire/src/bin/test.rs | 234+++++++++++++++++++++++++------------------------------------------------------
Mbtc-wire/src/lib.rs | 142+++++++++++++++++++++++++++++--------------------------------------------------
Mbtc-wire/src/main.rs | 55++++++++++++++++++++++++++-----------------------------
Abtc-wire/src/rpc.rs | 384+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dbtc-wire/src/rpc_patch.rs | 192-------------------------------------------------------------------------------
Mbtc-wire/src/rpc_utils.rs | 112+++++--------------------------------------------------------------------------
Mscript/setup.sh | 3+--
Mscript/test_btc_fail.sh | 2+-
Mscript/test_btc_stress.sh | 6++++--
12 files changed, 680 insertions(+), 639 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -93,15 +93,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] -name = "base64-compat" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a8d4d2746f89841e49230dd26917df1876050f95abafafbe34f47cb534b88d7" -dependencies = [ - "byteorder", -] - -[[package]] name = "bech32" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -129,19 +120,6 @@ dependencies = [ ] [[package]] -name = "bitcoincore-rpc" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b8d99d58466295cb2bf72c6959b784d59f8f0d6977458d2ba3eb75c834f36c3" -dependencies = [ - "bitcoincore-rpc-json", - "jsonrpc", - "log", - "serde", - "serde_json", -] - -[[package]] name = "bitcoincore-rpc-json" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -184,18 +162,22 @@ name = "btc-wire" version = "0.1.0" dependencies = [ "argh", + "base64", "bech32", - "bitcoincore-rpc", + "bitcoincore-rpc-json", "criterion", "fastrand", "owo-colors", "postgres", "rand", "serde", + "serde_json", + "serde_repr", "taler-api", "taler-config", "taler-log", "thiserror", + "ureq", "uri-pack", "url", ] @@ -240,6 +222,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] +name = "chunked_transfer" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" + +[[package]] name = "clap" version = "2.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -797,18 +785,6 @@ dependencies = [ ] [[package]] -name = "jsonrpc" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad24d69a8a0698db8ffb9048e937e8ae3ee3bc45772a5d7b6979b1d2d5b6a9f7" -dependencies = [ - "base64-compat", - "serde", - "serde_derive", - "serde_json", -] - -[[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -939,6 +915,12 @@ dependencies = [ ] [[package]] +name = "once_cell" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" + +[[package]] name = "oorandom" version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1248,6 +1230,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" [[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] name = "rust-ini" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1267,6 +1264,18 @@ dependencies = [ ] [[package]] +name = "rustls" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d37e5e2290f3e040b594b1a9e04377c2c671f1a1cfd9bfdef82106ac1c113f84" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] name = "rustversion" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1294,6 +1303,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] name = "secp256k1" version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1360,6 +1379,17 @@ dependencies = [ ] [[package]] +name = "serde_repr" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98d0516900518c29efa217c298fa1f4e6c6ffc85ae29fd7f4ee48f176e1a9ed5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "serde_urlencoded" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1434,6 +1464,12 @@ dependencies = [ ] [[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] name = "stringprep" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1702,6 +1738,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" [[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "ureq" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5c448dcb78ec38c7d59ec61f87f70a98ea19171e06c139357e012ee226fec90" +dependencies = [ + "base64", + "chunked_transfer", + "log", + "once_cell", + "rustls", + "serde", + "serde_json", + "url", + "webpki", + "webpki-roots", +] + +[[package]] name = "uri-pack" version = "0.1.0" dependencies = [ @@ -1832,6 +1892,25 @@ dependencies = [ ] [[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c475786c6f47219345717a043a37ec04cb4bc185e28853adcc4fa0a947eba630" +dependencies = [ + "webpki", +] + +[[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/btc-wire/Cargo.toml b/btc-wire/Cargo.toml @@ -8,8 +8,8 @@ edition = "2021" fail = [] [dependencies] -# Typed bitcoin json-rpc library -bitcoincore-rpc = "0.14.0" +# Typed bitcoin rpc types +bitcoincore-rpc-json = "0.14.0" # Cli args argh = "0.1.6" # Bech32 encoding and decoding @@ -20,6 +20,8 @@ rand = { version = "0.8.4", features = ["getrandom"] } fastrand = "1.5.0" # Serialization library serde = { version = "1.0.130", features = ["derive"] } +serde_json = "1.0.66" +serde_repr = "0.1" # Error macros thiserror = "1.0.30" # Postgres client @@ -30,6 +32,8 @@ uri-pack = { path = "../uri-pack" } url = { version = "2.2.2", features = ["serde"] } # Ansi color owo-colors = "3.1.1" +ureq = { version = "2.3.1", features = ["json"] } +base64 = "0.13.0" # Taler libs taler-api = { path = "../taler-api" } taler-config = { path = "../taler-config" } diff --git a/btc-wire/src/bin/btc-wire-cli.rs b/btc-wire/src/bin/btc-wire-cli.rs @@ -1,13 +1,10 @@ use std::path::PathBuf; -use bitcoincore_rpc::{ - bitcoin::{Address, Amount}, - Client, RpcApi, -}; +use bitcoincore_rpc_json::bitcoin::{Address, Amount}; use btc_wire::{ - rpc_utils::{common_rpc, default_data_dir, dirty_guess_network, wallet_rpc, Network}, + rpc::BtcRpc, + rpc_utils::{default_data_dir, Network}, test::rand_key, - ClientExtended, }; #[derive(argh::FromArgs)] @@ -72,15 +69,14 @@ struct ResetCmd {} struct App { data_dir: PathBuf, network: Network, - client: Client, + client: BtcRpc, } impl App { pub fn start(data_dir: Option<PathBuf>) -> Self { let data_dir = data_dir.unwrap_or_else(default_data_dir); - let network = dirty_guess_network(&data_dir); - let client = - common_rpc(&data_dir, network).expect("Failed to connect to bitcoin core server"); + let network = Network::RegTest; + let client = BtcRpc::common(&data_dir, network); Self { data_dir, @@ -89,12 +85,12 @@ impl App { } } - pub fn auto_wallet(&self, name: &str) -> (Client, Address) { + pub fn auto_wallet(&self, name: &str) -> (BtcRpc, Address) { // Auto load self.client.load_wallet(name).ok(); - let wallet = wallet_rpc(&self.data_dir, self.network, name); + let wallet = BtcRpc::wallet(&self.data_dir, self.network, name); let addr = wallet - .get_new_address(None, None) + .get_new_address() .expect(&format!("Failed to get wallet address {}", name)); (wallet, addr) } @@ -104,7 +100,7 @@ impl App { Network::RegTest => { // Manually mine a block let (_, addr) = self.auto_wallet(wallet); - self.client.generate_to_address(1, &addr).unwrap(); + self.client.generate(1, &addr).unwrap(); } _ => { // Wait for next network block @@ -115,13 +111,11 @@ impl App { pub fn init(&self) { for wallet in ["wire", "client", "reserve"] { - self.client - .create_wallet(wallet, None, None, None, None) - .ok(); + self.client.create_wallet(wallet).ok(); } if self.network == Network::RegTest { let (client, client_addr) = self.auto_wallet("client"); - client.generate_to_address(101, &client_addr).unwrap(); + client.generate(101, &client_addr).unwrap(); } } } @@ -149,7 +143,7 @@ fn main() { let (client, _) = app.auto_wallet(&from); let (_, to) = app.auto_wallet(&to); client - .send_segwit_key(&to, Amount::from_btc(amount).unwrap(), &rand_key()) + .send_segwit_key(&to, &Amount::from_btc(amount).unwrap(), &rand_key()) .unwrap(); } Cmd::NextBlock(NextBlockCmd { to }) => { diff --git a/btc-wire/src/bin/test.rs b/btc-wire/src/bin/test.rs @@ -1,22 +1,15 @@ use core::panic; use std::{collections::HashSet, iter::repeat_with, panic::AssertUnwindSafe}; -use bitcoincore_rpc::{ +use bitcoincore_rpc_json::{ bitcoin::{Address, Amount, Txid}, - json::GetTransactionResultDetailCategory as Category, - jsonrpc::{ - error::RpcError, - serde_json::{json, Value}, - }, - Client, RpcApi, + GetTransactionResultDetailCategory as Category, }; use btc_wire::{ - rpc_patch::RpcErrorCode, - rpc_utils::{ - common_rpc, default_data_dir, dirty_guess_network, wallet_rpc, Network, CLIENT, WIRE, - }, + rpc::{self, BtcRpc}, + rpc_utils::{default_data_dir, Network, CLIENT, WIRE}, test::rand_key, - BounceErr, ClientExtended, + BounceErr, }; use owo_colors::OwoColorize; @@ -27,7 +20,7 @@ pub fn main() { let test_amount = Amount::from_sat(1500); let data_dir = default_data_dir(); // Network check - let network = dirty_guess_network(&data_dir); + let network = Network::RegTest; match network { Network::MainNet => { panic!("Do not run tests on the mainnet, you are going to loose money") @@ -45,38 +38,45 @@ pub fn main() { .map(|it| it.file_name().to_string_lossy().to_string()) .collect(); - let rpc = common_rpc(&data_dir, network).expect("Failed to open common client"); + let wallets = [CLIENT, WIRE, RESERVE]; + + let rpc = BtcRpc::common(&data_dir, network); if !existing_wallets.contains(CLIENT) || !existing_wallets.contains(WIRE) || !existing_wallets.contains(RESERVE) { println!("Generate tests wallets"); // Create wallets - rpc.create_wallet(WIRE, None, None, None, None).ok(); - rpc.create_wallet(CLIENT, None, None, None, None).ok(); - rpc.create_wallet(RESERVE, None, None, None, None).ok(); + for wallet in &wallets { + rpc.create_wallet(wallet).unwrap(); + } } // Load wallets - - rpc.load_wallet(WIRE).ok(); - rpc.load_wallet(CLIENT).ok(); - rpc.load_wallet(RESERVE).ok(); + for wallet in &wallets { + if let Err(e) = rpc.load_wallet(wallet) { + match e { + rpc::Error::RPC { code, .. } + if code == rpc::ErrorCode::RpcWalletAlreadyLoaded => {} + e => Err(e).unwrap(), + } + } + } } // Client initialization - let client_rpc = wallet_rpc(&data_dir, network, CLIENT); - let wire_rpc = wallet_rpc(&data_dir, network, WIRE); - let reserve_rpc = wallet_rpc(&data_dir, network, RESERVE); - let client_addr = client_rpc.get_new_address(None, None).unwrap(); - let wire_addr = wire_rpc.get_new_address(None, None).unwrap(); - let reserve_addr = reserve_rpc.get_new_address(None, None).unwrap(); + let client_rpc = BtcRpc::wallet(&data_dir, network, CLIENT); + let wire_rpc = BtcRpc::wallet(&data_dir, network, WIRE); + let reserve_rpc = BtcRpc::wallet(&data_dir, network, RESERVE); + let client_addr = client_rpc.get_new_address().unwrap(); + let wire_addr = wire_rpc.get_new_address().unwrap(); + let reserve_addr = reserve_rpc.get_new_address().unwrap(); let next_block = || { match network { Network::RegTest => { // Manually mine a block - reserve_rpc.generate_to_address(1, &reserve_addr).unwrap(); + reserve_rpc.generate(1, &reserve_addr).unwrap(); } _ => { // Wait for next network block @@ -85,11 +85,11 @@ pub fn main() { } }; - let wait_for_tx = |rpc: &Client, txs: &[Txid]| { + let wait_for_tx = |rpc: &BtcRpc, txs: &[Txid]| { let mut count = 0; while txs .iter() - .any(|id| rpc.get_transaction(id, None).unwrap().info.confirmations <= 0) + .any(|id| rpc.get_tx(id).unwrap().tx.info.confirmations <= 0) { next_block(); if count > 3 { @@ -100,38 +100,17 @@ pub fn main() { }; // Balance check + { // Transfer all wire money to client - let wire_balance = wire_rpc.get_balance(None, None).unwrap(); - wire_rpc - .send_to_address( - &client_addr, - wire_balance, - None, - None, - Some(true), - None, - None, - None, - ) - .ok(); + let wire_balance = wire_rpc.get_balance().unwrap(); + wire_rpc.send(&client_addr, &wire_balance, true).ok(); // Transfer all wire money to client - let reserve_balance = reserve_rpc.get_balance(None, None).unwrap(); - reserve_rpc - .send_to_address( - &client_addr, - reserve_balance, - None, - None, - Some(true), - None, - None, - None, - ) - .ok(); + let reserve_balance = reserve_rpc.get_balance().unwrap(); + reserve_rpc.send(&client_addr, &reserve_balance, true).ok(); next_block(); - let balance = client_rpc.get_balance(None, None).unwrap(); + let balance = client_rpc.get_balance().unwrap(); let min_balance = test_amount * 3; if balance < min_balance { println!( @@ -150,18 +129,19 @@ pub fn main() { println!("Request coins from a faucet such as https://bitcoinfaucet.uo1.net/send.php to this address: {}", client_addr); } println!("Waiting for the transaction..."); - while client_rpc.get_balance(None, None).unwrap() < min_balance { + while client_rpc.get_balance().unwrap() < min_balance { client_rpc.wait_for_new_block(0).ok(); } } Network::RegTest => { println!("Add 50B to client wallet"); client_rpc - .generate_to_address( - 101, /* Need 100 blocks to validate */ - &client_addr, - ) + .generate(101 /* Need 100 blocks to validate */, &reserve_addr) .unwrap(); + reserve_rpc + .send(&client_addr, &Amount::from_sat(5_000_000_000), true) + .unwrap(); + next_block(); } } } @@ -169,9 +149,9 @@ pub fn main() { println!( "Initial state:\n{} {}\n{} {}", WIRE, - wire_rpc.get_balance(None, None).unwrap(), + wire_rpc.get_balance().unwrap(), CLIENT, - client_rpc.get_balance(None, None).unwrap() + client_rpc.get_balance().unwrap() ); } @@ -181,7 +161,7 @@ pub fn main() { // Send metadata let msg = "J'aime le chocolat".as_bytes(); let id = client_rpc - .send_op_return(&wire_addr, test_amount, msg) + .send_op_return(&wire_addr, &test_amount, msg) .unwrap(); // Check in mempool assert!( @@ -202,7 +182,7 @@ pub fn main() { // Send metadata let key = rand_key(); let id = client_rpc - .send_segwit_key(&wire_addr, test_amount, &key) + .send_segwit_key(&wire_addr, &test_amount, &key) .unwrap(); // Check in mempool assert!( @@ -222,44 +202,33 @@ pub fn main() { let bounce_fee = Amount::from_sat(300); runner.test("Bounce simple", || { - let before = client_rpc.get_balance(None, None).unwrap(); - let send_id = client_rpc - .send_to_address(&wire_addr, test_amount, None, None, None, None, None, None) - .unwrap(); + let before = client_rpc.get_balance().unwrap(); + let send_id = client_rpc.send(&wire_addr, &test_amount, false).unwrap(); wait_for_tx(&client_rpc, &[send_id]); - let bounce_id = wire_rpc.bounce(&send_id, bounce_fee).unwrap(); + let bounce_id = wire_rpc.bounce(&send_id, &bounce_fee).unwrap(); wait_for_tx(&wire_rpc, &[bounce_id]); - let bounce_tx_fee = wire_rpc.get_transaction(&bounce_id, None).unwrap().details[0] + let bounce_tx_fee = wire_rpc.get_tx(&bounce_id).unwrap().tx.details[0] .fee .unwrap() .abs() .to_unsigned() .unwrap(); - let send_tx_fee = client_rpc.get_transaction(&send_id, None).unwrap().details[0] + let send_tx_fee = client_rpc.get_tx(&send_id).unwrap().tx.details[0] .fee .unwrap() .abs() .to_unsigned() .unwrap(); - let after = client_rpc.get_balance(None, None).unwrap(); + let after = client_rpc.get_balance().unwrap(); assert!(before >= after); assert_eq!(before - after, bounce_tx_fee + bounce_fee + send_tx_fee); }); runner.test("Bounce minimal amount", || { let send_id = client_rpc - .send_to_address( - &wire_addr, - Amount::from_sat(294), - None, - None, - None, - None, - None, - None, - ) + .send(&wire_addr, &Amount::from_sat(294), false) .unwrap(); wait_for_tx(&client_rpc, &[send_id]); - assert!(match wire_rpc.bounce(&send_id, bounce_fee) { + assert!(match wire_rpc.bounce(&send_id, &bounce_fee) { Ok(_) => false, Err(err) => match err { BounceErr::AmountLessThanFee => true, @@ -269,100 +238,50 @@ pub fn main() { }); runner.test("Bounce too small amount", || { let send_id = client_rpc - .send_to_address( - &wire_addr, - Amount::from_sat(294) + bounce_fee, - None, - None, - None, - None, - None, - None, - ) + .send(&wire_addr, &(Amount::from_sat(294) + bounce_fee), false) .unwrap(); wait_for_tx(&client_rpc, &[send_id]); - assert!(match wire_rpc.bounce(&send_id, bounce_fee) { + assert!(match wire_rpc.bounce(&send_id, &bounce_fee) { Ok(_) => false, Err(err) => match err { - BounceErr::RPC(bitcoincore_rpc::Error::JsonRpc( - bitcoincore_rpc::jsonrpc::Error::Rpc(RpcError { code, .. }), - )) => code == RpcErrorCode::RpcWalletInsufficientFunds as i32, + BounceErr::RPC(rpc::Error::RPC { code, .. }) => + code == rpc::ErrorCode::RpcWalletInsufficientFunds, _ => false, }, }); }); runner.test("Bounce complex", || { // Generate 6 new addresses - let addresses: Vec<Address> = - repeat_with(|| client_rpc.get_new_address(None, None).unwrap()) - .take(6) - .collect(); + let addresses: Vec<Address> = repeat_with(|| client_rpc.get_new_address().unwrap()) + .take(6) + .collect(); // Send transaction to self with 1, 2 and 3 outputs let txs: Vec<Txid> = [&addresses[0..1], &addresses[1..3], &addresses[3..]] .into_iter() .map(|addresses| { - let hex: String = client_rpc - .call( - "createrawtransaction", - &[ - Value::Array(vec![]), - Value::Array( - addresses - .iter() - .map(|addr| json!({&addr.to_string(): test_amount.as_btc()})) - .collect(), - ), - ], - ) - .unwrap(); - let funded = client_rpc.fund_raw_transaction(hex, None, None).unwrap(); - let signed = client_rpc - .sign_raw_transaction_with_wallet(&funded.hex, None, None) - .unwrap(); - client_rpc.send_raw_transaction(&signed.hex).unwrap() + client_rpc + .send_custom(&[], addresses.iter().map(|addr| (addr, &test_amount)), None) + .unwrap() }) .collect(); wait_for_tx(&client_rpc, txs.as_slice()); - let before = client_rpc.get_balance(None, None).unwrap(); + let before = client_rpc.get_balance().unwrap(); // Send a transaction with multiple input from multiple transaction of different outputs len - let hex: String = client_rpc - .call( - "createrawtransaction", - &[ - Value::Array( - txs.into_iter() - .enumerate() - .map(|(i, tx)| { - json!({ - "txid": tx.to_string(), - "vout": i, - }) - }) - .collect(), - ), - json!([ - {&wire_addr.to_string(): (test_amount*3).as_btc()} - ]), - ], - ) - .unwrap(); - let funded = client_rpc.fund_raw_transaction(hex, None, None).unwrap(); - let signed = client_rpc - .sign_raw_transaction_with_wallet(&funded.hex, None, None) + let send_id = client_rpc + .send_custom(&txs, [(&wire_addr, &(test_amount * 3))], None) .unwrap(); - let send_id = client_rpc.send_raw_transaction(&signed.hex).unwrap(); wait_for_tx(&client_rpc, &[send_id]); - let bounce_id = wire_rpc.bounce(&send_id, bounce_fee).unwrap(); + let bounce_id = wire_rpc.bounce(&send_id, &bounce_fee).unwrap(); wait_for_tx(&wire_rpc, &[bounce_id]); - let after = client_rpc.get_balance(None, None).unwrap(); - let bounce_tx_fee = wire_rpc.get_transaction(&bounce_id, None).unwrap().details[0] + let after = client_rpc.get_balance().unwrap(); + let bounce_tx_fee = wire_rpc.get_tx(&bounce_id).unwrap().tx.details[0] .fee .unwrap() .abs() .to_unsigned() .unwrap(); - let send_tx_fee = client_rpc.get_transaction(&send_id, None).unwrap().details[0] + let send_tx_fee = client_rpc.get_tx(&send_id).unwrap().tx.details[0] .fee .unwrap() .abs() @@ -376,14 +295,9 @@ pub fn main() { } /// Check a specific transaction exist in a wallet historic -fn tx_exist( - rpc: &Client, - id: &Txid, - min_confirmation: i32, - detail: Category, -) -> bitcoincore_rpc::Result<bool> { - let txs = rpc.list_transactions(None, None, None, None).unwrap(); - let found = txs.into_iter().any(|tx| { +fn tx_exist(rpc: &BtcRpc, id: &Txid, min_confirmation: i32, detail: Category) -> rpc::Result<bool> { + let result = rpc.list_since_block(None, 1, false).unwrap(); + let found = result.transactions.into_iter().any(|tx| { tx.detail.category == detail && tx.info.confirmations >= min_confirmation && tx.info.txid == *id diff --git a/btc-wire/src/lib.rs b/btc-wire/src/lib.rs @@ -1,18 +1,14 @@ -pub use bitcoincore_rpc; -use bitcoincore_rpc::{ - bitcoin::{ - hashes::hex::{FromHex, ToHex}, - Address, Amount, Network, Txid, - }, - json::{GetTransactionResultDetailCategory as Category, ScriptPubkeyType}, - jsonrpc::serde_json::{json, Value}, - Client, RpcApi, +use std::str::FromStr; + +use bitcoincore_rpc_json::{ + bitcoin::{hashes::hex::FromHex, Address, Amount, Network, Txid}, + GetTransactionResultDetailCategory, ScriptPubkeyType, }; -use rpc_patch::{ClientPatched, GetTransactionFull}; -use rpc_utils::{segwit_min_amount, send_many, sender_address}; +use rpc::{BtcRpc, GetTransactionFull}; +use rpc_utils::{segwit_min_amount, sender_address}; use segwit::{decode_segwit_msg, encode_segwit_key}; -pub mod rpc_patch; +pub mod rpc; pub mod rpc_utils; pub mod segwit; pub mod test; @@ -24,7 +20,7 @@ pub enum BounceErr { #[error("Transaction amount less than bounce fee")] AmountLessThanFee, #[error(transparent)] - RPC(#[from] bitcoincore_rpc::Error), + RPC(#[from] rpc::Error), } #[derive(Debug, thiserror::Error)] @@ -32,7 +28,7 @@ pub enum GetSegwitErr { #[error(transparent)] Decode(#[from] segwit::DecodeSegWitErr), #[error(transparent)] - RPC(#[from] bitcoincore_rpc::Error), + RPC(#[from] rpc::Error), } #[derive(Debug, thiserror::Error)] @@ -40,65 +36,40 @@ pub enum GetOpReturnErr { #[error("Missing opreturn")] MissingOpReturn, #[error(transparent)] - RPC(#[from] bitcoincore_rpc::Error), + RPC(#[from] rpc::Error), } /// An extended bitcoincore JSON-RPC api client who can send and retrieve metadata with their transaction -pub trait ClientExtended { +impl BtcRpc { /// Send a transaction with a 32B key as metadata encoded using fake segwit addresses - fn send_segwit_key( - &self, - to: &Address, - amount: Amount, - metadata: &[u8; 32], - ) -> bitcoincore_rpc::Result<Txid>; - - /// Get detailed information about an in-wallet transaction and it's 32B metadata key encoded using fake segwit addresses - fn get_tx_segwit_key(&self, id: &Txid) -> Result<(GetTransactionFull, [u8; 32]), GetSegwitErr>; - - /// Send a transaction with metadata encoded using OP_RETURN - fn send_op_return( - &self, - to: &Address, - amount: Amount, - metadata: &[u8], - ) -> bitcoincore_rpc::Result<Txid>; - - /// Get detailed information about an in-wallet transaction and its op_return metadata - fn get_tx_op_return(&self, id: &Txid) -> Result<(GetTransactionFull, Vec<u8>), GetOpReturnErr>; - - /// Bounce a transaction bask to its sender - /// - /// There is no reliable way to bounce a transaction as you cannot know if the addresses - /// used are shared or come from a third-party service. We only send back to the first input - /// address as a best-effort gesture. - fn bounce(&self, id: &Txid, bounce_fee: Amount) -> Result<Txid, BounceErr>; -} - -impl ClientExtended for Client { - fn send_segwit_key( + pub fn send_segwit_key( &self, to: &Address, - amount: Amount, + amount: &Amount, metadata: &[u8; 32], - ) -> bitcoincore_rpc::Result<Txid> { + ) -> rpc::Result<Txid> { let hrp = match to.network { Network::Bitcoin => "bc", Network::Testnet | Network::Signet => "tb", Network::Regtest => "bcrt", }; let addresses = encode_segwit_key(hrp, metadata); - let mut recipients = vec![(to.to_string(), amount)]; - recipients.extend( - addresses - .into_iter() - .map(|addr| (addr, segwit_min_amount())), - ); - send_many(self, recipients, Vec::new()) + let addresses = [ + Address::from_str(&addresses[0]).unwrap(), + Address::from_str(&addresses[1]).unwrap(), + ]; + let mut recipients = vec![(to, amount)]; + let min = segwit_min_amount(); + recipients.extend(addresses.iter().map(|addr| (addr, &min))); + self.send_many(recipients) } - fn get_tx_segwit_key(&self, id: &Txid) -> Result<(GetTransactionFull, [u8; 32]), GetSegwitErr> { - let full = self.get_transaction_full(id)?; + /// Get detailed information about an in-wallet transaction and it's 32B metadata key encoded using fake segwit addresses + pub fn get_tx_segwit_key( + &self, + id: &Txid, + ) -> Result<(GetTransactionFull, [u8; 32]), GetSegwitErr> { + let full = self.get_tx(id)?; let addresses: Vec<String> = full .decoded @@ -117,36 +88,24 @@ impl ClientExtended for Client { Ok((full, metadata)) } - fn send_op_return( + /// Send a transaction with metadata encoded using OP_RETURN + pub fn send_op_return( &self, to: &Address, - amount: Amount, + amount: &Amount, metadata: &[u8], - ) -> bitcoincore_rpc::Result<Txid> { + ) -> rpc::Result<Txid> { assert!(metadata.len() > 0, "No medatata"); assert!(metadata.len() <= 80, "Max 80 bytes"); - - // Create a raw transaction with the recipient and the metadata - let hex: String = self.call( - "createrawtransaction", - &[ - Value::Array(vec![]), - // Recipient - json!( - [ - {&to.to_string(): amount.as_btc()}, - {"data": metadata.to_hex()} - ]), - ], - )?; - // Let bitcoincore handle the funding logic - let funded = self.fund_raw_transaction(hex, None, None)?; - let signed = self.sign_raw_transaction_with_wallet(&funded.hex, None, None)?; - self.send_raw_transaction(&signed.hex) + self.send_custom(&[], [(to, amount)], Some(metadata)) } - fn get_tx_op_return(&self, id: &Txid) -> Result<(GetTransactionFull, Vec<u8>), GetOpReturnErr> { - let full = self.get_transaction_full(id)?; + /// Get detailed information about an in-wallet transaction and its op_return metadata + pub fn get_tx_op_return( + &self, + id: &Txid, + ) -> Result<(GetTransactionFull, Vec<u8>), GetOpReturnErr> { + let full = self.get_tx(id)?; let op_return_out = full .decoded @@ -162,26 +121,27 @@ impl ClientExtended for Client { Ok((full, metadata)) } - fn bounce(&self, id: &Txid, bounce_fee: Amount) -> Result<Txid, BounceErr> { - let full = self.get_transaction_full(id)?; + /// Bounce a transaction bask to its sender + /// + /// There is no reliable way to bounce a transaction as you cannot know if the addresses + /// used are shared or come from a third-party service. We only send back to the first input + /// address as a best-effort gesture. + pub fn bounce(&self, id: &Txid, bounce_fee: &Amount) -> Result<Txid, BounceErr> { + let full = self.get_tx(id)?; let detail = &full.tx.details[0]; - if detail.category != Category::Receive { + if detail.category != GetTransactionResultDetailCategory::Receive { return Err(BounceErr::NotAReceiveTransaction); } let amount = detail.amount.to_unsigned().unwrap(); - if amount <= bounce_fee { + if amount <= *bounce_fee { return Err(BounceErr::AmountLessThanFee); } let sender = sender_address(self, &full)?; - let bounce_amount = amount - bounce_fee; + let bounce_amount = amount - *bounce_fee; // Send refund making recipient pay the transaction fees - let id = send_many( - self, - vec![(sender.to_string(), bounce_amount)], - vec![sender.to_string()], - )?; + let id = self.send(&sender, &bounce_amount, true)?; Ok(id) } } diff --git a/btc-wire/src/main.rs b/btc-wire/src/main.rs @@ -1,12 +1,10 @@ -use bitcoincore_rpc::{ - bitcoin::{hashes::Hash, Address, Amount as BtcAmount, BlockHash, SignedAmount, Txid}, - json::GetTransactionResultDetailCategory as Category, - Client as RPC, RpcApi, -}; + +use bitcoincore_rpc_json::{bitcoin::{Address, SignedAmount, Amount as BtcAmount, BlockHash, Txid, hashes::Hash}, GetTransactionResultDetailCategory}; use btc_wire::{ - rpc_utils::{common_rpc, default_data_dir, sender_address, wallet_rpc, Network}, + rpc::BtcRpc, + rpc_utils::{default_data_dir, sender_address, Network}, segwit::DecodeSegWitErr, - ClientExtended, GetOpReturnErr, GetSegwitErr, + GetOpReturnErr, GetSegwitErr, }; use postgres::{fallible_iterator::FallibleIterator, Client, NoTls}; use rand::{rngs::OsRng, RngCore}; @@ -171,7 +169,7 @@ fn last_hash(db: &mut Client) -> Result<Option<BlockHash>, postgres::Error> { } /// Listen for new proposed transactions and announce them on the bitcoin network -fn sender(rpc: RPC, mut db: AutoReloadDb, _config: &Config) { +fn sender(rpc: BtcRpc, mut db: AutoReloadDb, _config: &Config) { // List all transactions waiting to be sent fn list_waiting(db: &mut Client) -> Result<Vec<(i32, Status)>, postgres::Error> { let mut iter = db.query_raw( @@ -202,7 +200,11 @@ fn sender(rpc: RPC, mut db: AutoReloadDb, _config: &Config) { // Perform a transaction on the blockchain // The transaction must be in the manual state - fn perform_send(db: &mut Client, rpc: &RPC, id: i32) -> Result<(), Box<dyn std::error::Error>> { + fn perform_send( + db: &mut Client, + rpc: &BtcRpc, + id: i32, + ) -> Result<(), Box<dyn std::error::Error>> { let mut tx = db.transaction()?; // We lock the row with FOR UPDATE to prevent sending same transaction multiple time let iter = tx.query_opt( @@ -217,9 +219,9 @@ fn sender(rpc: RPC, mut db: AutoReloadDb, _config: &Config) { let metadata = encode_info(reserve_pub.try_into()?, &exchange_base_url); fail_point("Skip send_op_return", 0.3)?; - match rpc.send_op_return(&addr, amount, &metadata) { + match rpc.send_op_return(&addr, &amount, &metadata) { Ok(tx_id) => { - fail_point("Fail update db", 0.4)?; + fail_point("Fail update db", 0.3)?; tx.execute( "UPDATE tx_out SET status=$1, txid=$2 WHERE id=$3", &[&(Status::Pending as i16), &tx_id.as_ref(), &id], @@ -228,7 +230,7 @@ fn sender(rpc: RPC, mut db: AutoReloadDb, _config: &Config) { } Err(e) => { info!("sender: RPC - {}", e); - fail_point("Fail update db", 0.4)?; + fail_point("Fail update db", 0.3)?; tx.execute( "UPDATE tx_out SET status=$1 WHERE id=$2", &[&(Status::Delayed as i16), &id], @@ -263,10 +265,10 @@ fn sender(rpc: RPC, mut db: AutoReloadDb, _config: &Config) { let mut manuals = list_manual(db)?; if !manuals.is_empty() { let last_hash = last_hash(db)?; - let txs = rpc.list_since_block(last_hash.as_ref(), None, None, None)?; + let txs = rpc.list_since_block(last_hash.as_ref(), 1, false)?; // Search for a matching unconfirmed transactions for tx in txs.transactions { - if tx.detail.category == Category::Send { + if tx.detail.category == GetTransactionResultDetailCategory::Send { if let Ok((_, bytes)) = rpc.get_tx_op_return(&tx.info.txid) { let (wtid, _) = decode_info(&bytes); if let Some(pos) = manuals.iter().position(|(_, it)| it == &wtid) { @@ -301,7 +303,7 @@ fn sender(rpc: RPC, mut db: AutoReloadDb, _config: &Config) { } /// Listen for mined block and index confirmed transactions into the database -fn watcher(rpc: RPC, mut db: AutoReloadDb, config: &Config) { +fn watcher(rpc: BtcRpc, mut db: AutoReloadDb, config: &Config) { loop { let db = db.client(); @@ -309,27 +311,22 @@ fn watcher(rpc: RPC, mut db: AutoReloadDb, config: &Config) { // Get stored last_hash let last_hash = last_hash(db)?; // Get all transactions made since this block - let list = rpc.list_since_block( - last_hash.as_ref(), - Some(config.confirmation as usize), - None, - Some(true), - )?; + let list = rpc.list_since_block(last_hash.as_ref(), config.confirmation, true)?; // Keep only confirmed send and receive transactions - let txs: HashMap<Txid, Category> = list + let txs: HashMap<Txid, GetTransactionResultDetailCategory> = list .transactions .into_iter() .filter_map(|tx| { let cat = tx.detail.category; (tx.info.confirmations >= config.confirmation as i32 - && (cat == Category::Send || cat == Category::Receive)) + && (cat == GetTransactionResultDetailCategory::Send || cat == GetTransactionResultDetailCategory::Receive)) .then(|| (tx.info.txid, cat)) }) .collect(); for (id, category) in txs { match category { - Category::Send => match rpc.get_tx_op_return(&id) { + GetTransactionResultDetailCategory::Send => match rpc.get_tx_op_return(&id) { Ok((full, bytes)) => { let (wtid, url) = decode_info(&bytes); let mut tx = db.transaction()?; @@ -382,7 +379,7 @@ fn watcher(rpc: RPC, mut db: AutoReloadDb, config: &Config) { err => warn!("send: {} {}", id, err), }, }, - Category::Receive => match rpc.get_tx_segwit_key(&id) { + GetTransactionResultDetailCategory::Receive => match rpc.get_tx_segwit_key(&id) { Ok((full, reserve_pub)) => { let debit_addr = sender_address(&rpc, &full)?; let credit_addr = full.tx.details[0].address.as_ref().unwrap(); @@ -404,7 +401,7 @@ fn watcher(rpc: RPC, mut db: AutoReloadDb, config: &Config) { err => warn!("receive: {} {}", id, err), }, }, - Category::Generate | Category::Immature | Category::Orphan => {} + GetTransactionResultDetailCategory::Generate | GetTransactionResultDetailCategory::Immature | GetTransactionResultDetailCategory::Orphan => {} } } // Move last_hash forward if no error have been caught @@ -466,10 +463,10 @@ fn main() { std::process::exit(1); } }; - let rpc = common_rpc(&data_dir, network).unwrap(); + let rpc = BtcRpc::common(&data_dir, network); rpc.load_wallet(&config.btc_wallet).ok(); - let rpc_watcher = wallet_rpc(&data_dir, network, &config.btc_wallet); - let rpc_sender = wallet_rpc(&data_dir, network, &config.btc_wallet); + let rpc_watcher = BtcRpc::wallet(&data_dir, network, &config.btc_wallet); + let rpc_sender = BtcRpc::wallet(&data_dir, network, &config.btc_wallet); let db_watcher = AutoReloadDb::new(&config.db_url, Duration::from_secs(5)); let db_sender = AutoReloadDb::new(&config.db_url, Duration::from_secs(5)); diff --git a/btc-wire/src/rpc.rs b/btc-wire/src/rpc.rs @@ -0,0 +1,384 @@ +use crate::rpc_utils::{rpc_url, Network}; +use bitcoincore_rpc_json::{ + bitcoin::{hashes::hex::ToHex, Address, Amount, BlockHash, Txid, Wtxid}, + BlockRef, FundRawTransactionResult, GetRawTransactionResultVin, GetTransactionResult, + ListSinceBlockResult, LoadWalletResult, ScriptPubkeyType, SignRawTransactionResult, +}; +use serde_json::{json, Value}; +use std::{ + fmt::Debug, + path::Path, + sync::atomic::{AtomicU64, Ordering}, + time::Duration, +}; + +// This is a very simple RPC client designed only for a specific bitcoincore version +// and to un on a secure localhost + +#[derive(Debug, serde::Serialize)] +struct BtcRequest<'a, T: serde::Serialize> { + method: &'a str, + id: u64, + params: &'a T, +} + +#[derive(Debug, serde::Deserialize)] +struct BtcResponse<T> { + result: Option<T>, + error: Option<BtcErr>, + id: u64, +} + +#[derive(Debug, serde::Deserialize)] +struct BtcErr { + code: ErrorCode, + message: String, +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Transport(#[from] ureq::Transport), + #[error("{code:?} - {msg}")] + RPC { code: ErrorCode, msg: String }, + #[error(transparent)] + Json(#[from] serde_json::Error), +} + +pub type Result<T> = std::result::Result<T, Error>; + +pub struct BtcRpc { + path: String, + agent: ureq::Agent, + id: AtomicU64, + cookie: String, +} + +impl BtcRpc { + pub fn common(data_dir: &Path, network: Network) -> Self { + let path = data_dir.join(network.dir()).join(".cookie"); + + let cookie = std::fs::read(path).unwrap(); + let agent = ureq::builder() + .redirects(0) + .timeout_connect(Duration::from_secs(5)) + .timeout_write(Duration::from_secs(5)) + .timeout_read(Duration::from_secs(5)) + .build(); + Self { + path: rpc_url(network), + agent, + id: AtomicU64::new(0), + cookie: format!("Basic {}", base64::encode(&cookie)), + } + } + + pub fn wallet(data_dir: &Path, network: Network, wallet: &str) -> Self { + let path = data_dir.join(network.dir()).join(".cookie"); + + let cookie = std::fs::read(path).unwrap(); + let agent = ureq::builder() + .redirects(0) + .timeout_connect(Duration::from_secs(5)) + .timeout_write(Duration::from_secs(5)) + .timeout_read(Duration::from_secs(5)) + .build(); + Self { + path: format!("{}/wallet/{}", rpc_url(network), wallet), + agent, + id: AtomicU64::new(0), + cookie: format!("Basic {}", base64::encode(&cookie)), + } + } + + fn call<T>(&self, method: &str, params: &impl serde::Serialize) -> Result<T> + where + T: serde::de::DeserializeOwned + Debug, + { + let id = self.id.fetch_add(1, Ordering::SeqCst); + let request = BtcRequest { method, id, params }; + let body = + serde_json::to_vec(&request).expect("Serialization into a vec should never fail"); + let result = self + .agent + .post(&self.path) + .set("Authorization", &self.cookie) + .set("Content-Type", "application/json-rpc") + .set("Accept", "application/json-rpc") + .timeout(Duration::from_secs(10)) + .send_bytes(&body); + let response = match result { + Ok(it) => it, + Err(it) => match it { + ureq::Error::Status(_, it) => it, + ureq::Error::Transport(it) => return Err(it.into()), + }, + }; + let response: BtcResponse<T> = serde_json::from_reader(response.into_reader())?; + assert_eq!(id, response.id); + if let Some(ok) = response.result { + Ok(ok) + } else { + let err = response.error.unwrap(); + Err(Error::RPC { + code: err.code, + msg: err.message, + }) + } + } + + pub fn load_wallet(&self, name: &str) -> Result<LoadWalletResult> { + self.call("loadwallet", &[name]) + } + + pub fn create_wallet(&self, name: &str) -> Result<LoadWalletResult> { + self.call("createwallet", &[name]) + } + + pub fn get_new_address(&self) -> Result<Address> { + self.call("getnewaddress", &[()]) + } + + pub fn generate(&self, nb: u16, address: &Address) -> Result<Vec<BlockHash>> { + self.call("generatetoaddress", &(nb, address)) + } + + pub fn wait_for_new_block(&self, timeout: u64) -> Result<BlockRef> { + self.call("waitfornewblock", &[timeout]) + } + + pub fn get_balance(&self) -> Result<Amount> { + let btc: f64 = self.call("getbalance", &[()])?; + Ok(Amount::from_btc(btc).unwrap()) + } + + pub fn send(&self, address: &Address, amount: &Amount, subtract_fee: bool) -> Result<Txid> { + let btc = amount.as_btc(); + self.call("sendtoaddress", &(address, btc, (), (), subtract_fee)) + } + + /// Send transaction to multiple recipients + pub fn send_many<'a, 'b>( + &self, + recipients: impl IntoIterator<Item = (&'a Address, &'b Amount)>, + ) -> Result<Txid> { + let amounts = Value::Object( + recipients + .into_iter() + .map(|(addr, amount)| (addr.to_string(), amount.as_btc().into())) + .collect(), + ); + self.call("sendmany", &("", amounts)) + } + + pub fn send_custom<'a, 'b, 'c>( + &self, + inputs: impl IntoIterator<Item = &'a Txid>, + outputs: impl IntoIterator<Item = (&'b Address, &'c Amount)>, + data: Option<&[u8]>, + ) -> Result<Txid> { + let hex: String = self + .call( + "createrawtransaction", + &[ + Value::Array( + inputs + .into_iter() + .enumerate() + .map(|(i, id)| json!({"txid": id.to_string(), "vout": i})) + .collect(), + ), + Value::Array({ + let mut vec: Vec<Value> = outputs + .into_iter() + .map(|(addr, amount)| json!({&addr.to_string(): amount.as_btc()})) + .collect(); + if let Some(data) = data { + vec.push(json!({ "data".to_string(): data.to_hex() })); + } + vec + }), + ], + ) + .unwrap(); + let funded: FundRawTransactionResult = self.call("fundrawtransaction", &[hex]).unwrap(); + let signed: SignRawTransactionResult = self + .call("signrawtransactionwithwallet", &[&funded.hex.to_hex()]) + .unwrap(); + self.call("sendrawtransaction", &[&signed.hex.to_hex()]) + } + + pub fn list_since_block( + &self, + hash: Option<&BlockHash>, + confirmation: u8, + include_remove: bool, + ) -> Result<ListSinceBlockResult> { + self.call("listsinceblock", &(hash, confirmation, (), include_remove)) + } + + pub fn get_tx(&self, id: &Txid) -> Result<GetTransactionFull> { + self.call("gettransaction", &(id, (), true)) + } + + pub fn get_raw(&self, id: &Txid) -> Result<GetRawTransactionResult22> { + self.call("getrawtransaction", &(id, true)) + } +} +/// v22.0 replace "reqSigs" and "addresses" for the saner "address" +#[derive(Clone, PartialEq, Eq, Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetRawTransactionResultVoutScriptPubKey22 { + pub asm: String, + #[serde(with = "bitcoincore_rpc_json::serde_hex")] + pub hex: Vec<u8>, + #[serde(rename = "type")] + pub type_: ScriptPubkeyType, + pub address: Option<Address>, +} + +#[derive(Clone, PartialEq, Eq, Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetRawTransactionResultVout22 { + #[serde(with = "bitcoincore_rpc_json::bitcoin::util::amount::serde::as_btc")] + pub value: Amount, + pub n: u32, + pub script_pub_key: GetRawTransactionResultVoutScriptPubKey22, +} + +#[derive(Clone, PartialEq, Eq, Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetRawTransactionResult22 { + #[serde(rename = "in_active_chain")] + pub in_active_chain: Option<bool>, + #[serde(with = "bitcoincore_rpc_json::serde_hex")] + pub hex: Vec<u8>, + pub txid: Txid, + pub hash: Wtxid, + pub size: usize, + pub vsize: usize, + pub version: u32, + pub locktime: u32, + pub vin: Vec<GetRawTransactionResultVin>, + pub vout: Vec<GetRawTransactionResultVout22>, + pub blockhash: Option<BlockHash>, + pub confirmations: Option<u32>, + pub time: Option<usize>, + pub blocktime: Option<usize>, +} + +/// Decoded raw transtion from"gettransaction" verbose does not return field already given in the simple form +#[derive(Clone, PartialEq, Eq, Debug, serde::Deserialize)] +pub struct TransactionDecoded { + pub txid: Txid, + pub hash: Wtxid, + pub size: usize, + pub vsize: usize, + pub version: u32, + pub locktime: u32, + pub vin: Vec<GetRawTransactionResultVin>, + pub vout: Vec<GetRawTransactionResultVout22>, +} + +/// "gettransaction" with decoded raw transaction +#[derive(Clone, PartialEq, Eq, Debug, serde::Deserialize)] +pub struct GetTransactionFull { + #[serde(flatten)] + pub tx: GetTransactionResult, + pub decoded: TransactionDecoded, +} + +/// Bitcoin RPC error codes <https://github.com/bitcoin/bitcoin/blob/master/src/rpc/protocol.h> +#[derive( + Debug, Clone, Copy, PartialEq, Eq, serde_repr::Serialize_repr, serde_repr::Deserialize_repr, +)] +#[repr(i32)] +pub enum ErrorCode { + // Standard JSON-RPC 2.0 errors + // RPC_INVALID_REQUEST is internally mapped to HTTP_BAD_REQUEST (400). + // It should not be used for application-layer errors. + RpcInvalidRequest = -32600, + // RPC_METHOD_NOT_FOUND is internally mapped to HTTP_NOT_FOUND (404). + // It should not be used for application-layer errors. + RpcMethodNotFound = -32601, + RpcInvalidParams = -32602, + // RPC_INTERNAL_ERROR should only be used for genuine errors in bitcoind + // (for example datadir corruption). + RpcInternalError = -32603, + RpcParseError = -32700, + + // General application defined errors + /// std::exception thrown in command handling + RpcMiscError = -1, + + /// Unexpected type was passed as parameter + RpcTypeError = -3, + /// Invalid address or key + RpcInvalidAddressOrKey = -5, + /// Ran out of memory during operation + RpcOutOfMemory = -7, + /// Invalid, missing or duplicate parameter + RpcInvalidParameter = -8, + /// Database error + RpcDatabaseError = -20, + /// Error parsing or validating structure in raw format + RpcDeserializationError = -22, + /// General error during transaction or block submission + RpcVerifyError = -25, + /// Transaction or block was rejected by network rules + RpcVerifyRejected = -26, + /// Transaction already in chain + RpcVerifyAlreadyInChain = -27, + /// Client still warming up + RpcInWarmup = -28, + /// RPC method is deprecated + RpcMethodDeprecated = -32, + // P2P client errors + /// Bitcoin is not connected + RpcClientNotConnected = -9, + /// Still downloading initial blocks + RpcClientInInitialDownload = -10, + /// Node is already added + RpcClientNodeAlreadyAdded = -23, + /// Node has not been added before + RpcClientNodeNotAdded = -24, + /// Node to disconnect not found in connected nodes + RpcClientNodeNotConnected = -29, + /// Invalid IP/Subnet + RpcClientInvalidIpOrSubnet = -30, + /// No valid connection manager instance found + RpcClientP2pDisabled = -31, + /// Max number of outbound or block-relay connections already open + RpcClientNodeCapacityReached = -34, + // Chain errors + RpcClientMempoolDisabled = -33, + /// No mempool instance found + // Wallet errors + /// Unspecified problem with wallet (key not found etc.) + RpcWalletError = -4, + /// Not enough funds in wallet or account + RpcWalletInsufficientFunds = -6, + /// Invalid label name + RpcWalletInvalidLabelName = -11, + /// Keypool ran out, call keypoolrefill first + RpcWalletKeypoolRanOut = -12, + /// Enter the wallet passphrase with walletpassphrase first + RpcWalletUnlockNeeded = -13, + /// The wallet passphrase entered was incorrect + RpcWalletPassphraseIncorrect = -14, + /// Command given in wrong wallet encryption state (encrypting an encrypted wallet etc.) + RpcWalletWrongEncState = -15, + /// Failed to encrypt the wallet + RpcWalletEncryptionFailed = -16, + /// Wallet is already unlocked + RpcWalletAlreadyUnlocked = -17, + /// Invalid wallet specified + RpcWalletNotFound = -18, + /// No wallet specified (error when there are multiple wallets loaded) + RpcWalletNotSpecified = -19, + /// This same wallet is already loaded + RpcWalletAlreadyLoaded = -35, + // Unused reserved codes, kept around for backwards compatibility. Do not reuse. + /// Server is in safe mode, and command is not allowed in safe mode + RpcForbiddenBySafeMode = -2, +} diff --git a/btc-wire/src/rpc_patch.rs b/btc-wire/src/rpc_patch.rs @@ -1,192 +0,0 @@ -//! bitcoincore-rpc does not handle all the command we need and is not compatible with bitcoincore v22.0 that we use. -//! We add additional typed command with a custom trait - -use bitcoincore_rpc::{ - bitcoin::{Address, Amount, BlockHash, Txid, Wtxid}, - json::{GetRawTransactionResultVin, GetTransactionResult, ScriptPubkeyType}, - jsonrpc::serde_json::Value, - Client, RpcApi, -}; - -pub trait ClientPatched { - fn get_transaction_full(&self, id: &Txid) -> bitcoincore_rpc::Result<GetTransactionFull>; - fn get_raw_tx(&self, id: &Txid) -> bitcoincore_rpc::Result<GetRawTransactionResult22>; -} - -impl ClientPatched for Client { - fn get_transaction_full(&self, id: &Txid) -> bitcoincore_rpc::Result<GetTransactionFull> { - self.call( - "gettransaction", - &[id.to_string().into(), Value::Null, true.into()], - ) - } - fn get_raw_tx(&self, id: &Txid) -> bitcoincore_rpc::Result<GetRawTransactionResult22> { - self.call("getrawtransaction", &[id.to_string().into(), true.into()]) - } -} - -/// v22.0 replace "reqSigs" and "addresses" for the saner "address" -#[derive(Clone, PartialEq, Eq, Debug, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetRawTransactionResultVoutScriptPubKey22 { - pub asm: String, - #[serde(with = "bitcoincore_rpc::bitcoincore_rpc_json::serde_hex")] - pub hex: Vec<u8>, - #[serde(rename = "type")] - pub type_: ScriptPubkeyType, - pub address: Option<Address>, -} - -#[derive(Clone, PartialEq, Eq, Debug, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetRawTransactionResultVout22 { - #[serde(with = "bitcoincore_rpc::bitcoin::util::amount::serde::as_btc")] - pub value: Amount, - pub n: u32, - pub script_pub_key: GetRawTransactionResultVoutScriptPubKey22, -} - -#[derive(Clone, PartialEq, Eq, Debug, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetRawTransactionResult22 { - #[serde(rename = "in_active_chain")] - pub in_active_chain: Option<bool>, - #[serde(with = "bitcoincore_rpc::bitcoincore_rpc_json::serde_hex")] - pub hex: Vec<u8>, - pub txid: Txid, - pub hash: Wtxid, - pub size: usize, - pub vsize: usize, - pub version: u32, - pub locktime: u32, - pub vin: Vec<GetRawTransactionResultVin>, - pub vout: Vec<GetRawTransactionResultVout22>, - pub blockhash: Option<BlockHash>, - pub confirmations: Option<u32>, - pub time: Option<usize>, - pub blocktime: Option<usize>, -} - -/// Decoded raw transtion from"gettransaction" verbose does not return field already given in the simple form -#[derive(Clone, PartialEq, Eq, Debug, serde::Deserialize)] -pub struct TransactionDecoded { - pub txid: Txid, - pub hash: Wtxid, - pub size: usize, - pub vsize: usize, - pub version: u32, - pub locktime: u32, - pub vin: Vec<GetRawTransactionResultVin>, - pub vout: Vec<GetRawTransactionResultVout22>, -} - -/// "gettransaction" with decoded raw transaction -#[derive(Clone, PartialEq, Eq, Debug, serde::Deserialize)] -pub struct GetTransactionFull { - #[serde(flatten)] - pub tx: GetTransactionResult, - pub decoded: TransactionDecoded, -} - -pub fn rpc_error(code: RpcErrorCode, msg: impl Into<String>) -> bitcoincore_rpc::Error { - bitcoincore_rpc::Error::JsonRpc(bitcoincore_rpc::jsonrpc::Error::Rpc( - bitcoincore_rpc::jsonrpc::error::RpcError { - code: code as i32, - message: msg.into(), - data: None, - }, - )) -} - -/// Bitcoin RPC error codes <https://github.com/bitcoin/bitcoin/blob/master/src/rpc/protocol.h> -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(i32)] -pub enum RpcErrorCode { - // Standard JSON-RPC 2.0 errors - // RPC_INVALID_REQUEST is internally mapped to HTTP_BAD_REQUEST (400). - // It should not be used for application-layer errors. - RpcInvalidRequest = -32600, - // RPC_METHOD_NOT_FOUND is internally mapped to HTTP_NOT_FOUND (404). - // It should not be used for application-layer errors. - RpcMethodNotFound = -32601, - RpcInvalidParams = -32602, - // RPC_INTERNAL_ERROR should only be used for genuine errors in bitcoind - // (for example datadir corruption). - RpcInternalError = -32603, - RpcParseError = -32700, - - // General application defined errors - /// std::exception thrown in command handling - RpcMiscError = -1, - - /// Unexpected type was passed as parameter - RpcTypeError = -3, - /// Invalid address or key - RpcInvalidAddressOrKey = -5, - /// Ran out of memory during operation - RpcOutOfMemory = -7, - /// Invalid, missing or duplicate parameter - RpcInvalidParameter = -8, - /// Database error - RpcDatabaseError = -20, - /// Error parsing or validating structure in raw format - RpcDeserializationError = -22, - /// General error during transaction or block submission - RpcVerifyError = -25, - /// Transaction or block was rejected by network rules - RpcVerifyRejected = -26, - /// Transaction already in chain - RpcVerifyAlreadyInChain = -27, - /// Client still warming up - RpcInWarmup = -28, - /// RPC method is deprecated - RpcMethodDeprecated = -32, - // P2P client errors - /// Bitcoin is not connected - RpcClientNotConnected = -9, - /// Still downloading initial blocks - RpcClientInInitialDownload = -10, - /// Node is already added - RpcClientNodeAlreadyAdded = -23, - /// Node has not been added before - RpcClientNodeNotAdded = -24, - /// Node to disconnect not found in connected nodes - RpcClientNodeNotConnected = -29, - /// Invalid IP/Subnet - RpcClientInvalidIpOrSubnet = -30, - /// No valid connection manager instance found - RpcClientP2pDisabled = -31, - /// Max number of outbound or block-relay connections already open - RpcClientNodeCapacityReached = -34, - // Chain errors - RpcClientMempoolDisabled = -33, - /// No mempool instance found - // Wallet errors - /// Unspecified problem with wallet (key not found etc.) - RpcWalletError = -4, - /// Not enough funds in wallet or account - RpcWalletInsufficientFunds = -6, - /// Invalid label name - RpcWalletInvalidLabelName = -11, - /// Keypool ran out, call keypoolrefill first - RpcWalletKeypoolRanOut = -12, - /// Enter the wallet passphrase with walletpassphrase first - RpcWalletUnlockNeeded = -13, - /// The wallet passphrase entered was incorrect - RpcWalletPassphraseIncorrect = -14, - /// Command given in wrong wallet encryption state (encrypting an encrypted wallet etc.) - RpcWalletWrongEncState = -15, - /// Failed to encrypt the wallet - RpcWalletEncryptionFailed = -16, - /// Wallet is already unlocked - RpcWalletAlreadyUnlocked = -17, - /// Invalid wallet specified - RpcWalletNotFound = -18, - /// No wallet specified (error when there are multiple wallets loaded) - RpcWalletNotSpecified = -19, - /// This same wallet is already loaded - RpcWalletAlreadyLoaded = -35, - // Unused reserved codes, kept around for backwards compatibility. Do not reuse. - /// Server is in safe mode, and command is not allowed in safe mode - RpcForbiddenBySafeMode = -2, -} diff --git a/btc-wire/src/rpc_utils.rs b/btc-wire/src/rpc_utils.rs @@ -1,13 +1,8 @@ -use std::{path::{PathBuf, Path}, str::FromStr}; +use std::{path::PathBuf, str::FromStr}; -use bitcoincore_rpc::{ - bitcoin::{Address, Amount, BlockHash, Txid}, - json::{GetTransactionResultDetailCategory, ListTransactionResult}, - jsonrpc::serde_json::Value, - Auth, Client, RpcApi, -}; +use bitcoincore_rpc_json::bitcoin::{Address, Amount}; -use crate::rpc_patch::{ClientPatched, GetTransactionFull}; +use crate::rpc::{self, BtcRpc, GetTransactionFull}; pub const CLIENT: &str = "client"; pub const WIRE: &str = "wire"; @@ -36,7 +31,7 @@ pub fn rpc_url(network: Network) -> String { Network::TestNet => 18332, Network::RegTest => 18443, }; - format!("https://127.0.0.1:{}", port) + format!("http://127.0.0.1:{}", port) } pub fn default_data_dir() -> PathBuf { @@ -57,72 +52,6 @@ pub fn default_data_dir() -> PathBuf { unimplemented!("Only windows, linux or macos") } } - - -pub fn common_rpc(data_dir: &Path, network: Network) -> bitcoincore_rpc::Result<Client> { - Client::new( - &rpc_url(network), - Auth::CookieFile(data_dir.join(network.dir()).join(".cookie")), - ) -} - -pub fn wallet_rpc(data_dir: &Path, network: Network, wallet: &str) -> Client { - Client::new( - &format!("{}/wallet/{}", rpc_url(network), wallet), - Auth::CookieFile(data_dir.join(network.dir()).join(".cookie")), - ) - .expect(&format!("Failed to open wallet '{}' client", wallet)) -} - -pub fn last_transaction(rpc: &Client) -> bitcoincore_rpc::Result<Txid> { - Ok(rpc - .list_transactions(None, None, None, None)? - .last() - .unwrap() - .info - .txid) -} - -pub fn received_since( - rpc: &Client, - hash: Option<&BlockHash>, -) -> bitcoincore_rpc::Result<(Vec<Txid>, BlockHash)> { - let result = rpc.list_since_block(hash, Some(1), None, None)?; - let mut received: Vec<&ListTransactionResult> = result - .transactions - .iter() - .filter(|it| { - it.info.confirmations > 0 - && it.detail.category == GetTransactionResultDetailCategory::Receive - }) - .collect(); - received.sort_unstable_by_key(|it| it.info.time); - received.reverse(); - Ok(( - received.into_iter().map(|it| it.info.txid).collect(), - result.lastblock, - )) -} - -pub fn dirty_guess_network(data_dir: &Path) -> Network { - let result_reg = common_rpc(data_dir, Network::RegTest).and_then(|rpc| rpc.get_network_info()); - if result_reg.is_ok() { - return Network::RegTest; - } - let result_test = common_rpc(data_dir, Network::TestNet).and_then(|rpc| rpc.get_network_info()); - if result_test.is_ok() { - return Network::TestNet; - } - let result_main = common_rpc(data_dir, Network::MainNet).and_then(|rpc| rpc.get_network_info()); - if result_main.is_ok() { - return Network::MainNet; - } - unreachable!( - "Failed to connect to any chain\nreg: {:?}\ntest: {:?}\nmain: {:?}", - result_reg, result_main, result_test - ); -} - /// Minimum dust amount to perform a transaction to a segwit address pub fn segwit_min_amount() -> Amount { // https://github.com/bitcoin/bitcoin/blob/master/src/policy/policy.cpp @@ -133,39 +62,10 @@ pub fn check_address(addr: &str) -> bool { Address::from_str(addr).is_ok() } -/// Send transaction to multiple recipients -pub fn send_many( - client: &Client, - recipients: Vec<(String, Amount)>, - fee_from: Vec<String>, -) -> bitcoincore_rpc::Result<Txid> { - let amounts = Value::Object( - recipients - .into_iter() - .map(|(addr, amount)| (addr, amount.as_btc().into())) - .collect(), - ); - client.call( - "sendmany", - &[ - "".into(), // dummy - amounts, // amounts - 0.into(), // minconf - "".into(), // comment - fee_from.into(), // substractfeefrom - false.into(), // replaceable - Value::Null, // conf_target - Value::Null, // estimate mode - 1.into(), // fee rate - false.into(), // verbose - ], - ) -} - /// Get the first sender address from a raw transaction -pub fn sender_address(rpc: &Client, full: &GetTransactionFull) -> bitcoincore_rpc::Result<Address> { +pub fn sender_address(rpc: &BtcRpc, full: &GetTransactionFull) -> rpc::Result<Address> { let first = &full.decoded.vin[0]; - let tx = rpc.get_raw_tx(&first.txid.unwrap())?; + let tx = rpc.get_raw(&first.txid.unwrap())?; Ok(tx .vout .into_iter() diff --git a/script/setup.sh b/script/setup.sh @@ -71,7 +71,7 @@ function check_balance() { CLIENT_BALANCE=`$BTC_CLI -rpcwallet=client getbalance` WIRE_BALANCE=`$BTC_CLI -rpcwallet=wire getbalance` if [ "$CLIENT_BALANCE" != "$1" ] || [ "$WIRE_BALANCE" != "${2:-$WIRE_BALANCE}" ]; then - echo "expected: client $1 wire $2 got: client $CLIENT_BALANCE wire $WIRE_BALANCE" + echo "expected: client $1 wire ${2:-$WIRE_BALANCE} got: client $CLIENT_BALANCE wire $WIRE_BALANCE" exit 1 fi } @@ -93,7 +93,6 @@ function stressed_btc_wire() { cargo build --bin btc-wire --release &> /dev/null target/release/btc-wire $BTC_DIR &> btc_wire.log & target/release/btc-wire $BTC_DIR &> btc_wire1.log & - target/release/btc-wire $BTC_DIR &> btc_wire2.log & } # Start wire_gateway in test mode diff --git a/script/test_btc_fail.sh b/script/test_btc_fail.sh @@ -18,7 +18,7 @@ trap cleanup EXIT source "${BASH_SOURCE%/*}/setup.sh" -echo "---- Setup stressed -----" +echo "---- Setup fail -----" echo "Load config file" load_config echo "Reset database" diff --git a/script/test_btc_stress.sh b/script/test_btc_stress.sh @@ -72,7 +72,9 @@ done next_btc # Mine transactions sleep 5 next_btc # Trigger watcher twice (for sure) -sleep 3 +sleep 5 +next_btc # Trigger watcher twice (for sure) +sleep 5 echo " OK" echo -n "Requesting exchange outgoing transaction list:" @@ -88,7 +90,7 @@ echo "----- Recover DB -----" echo "Reset database" reset_db # Clear database tables next_btc # Trigger watcher -sleep 2 +sleep 10 echo -n "Requesting exchange incoming transaction list:" check incoming