commit 92c4d2638fd272de0b60d759bdbc9f1ada01e27c
parent 1be9da930bd1d532109490a15fb97c27c52ce3ce
Author: Antoine A <>
Date: Thu, 16 Dec 2021 17:35:19 +0100
Use custom simple RPC client
Diffstat:
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