diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 5c41512e..6bc1c462 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -10,14 +10,8 @@ jobs: matrix: include: - rust: stable - env: - RUSTFMTCHK: true - rust: nightly - env: - RUSTFMTCHK: false - - rust: 1.41.1 - env: - RUSTFMTCHK: false + - rust: 1.48.0 steps: - name: Checkout Crate uses: actions/checkout@v2 @@ -27,9 +21,10 @@ jobs: profile: minimal toolchain: ${{ matrix.rust }} override: true - - run: cargo update -p serde --precise 1.0.152 + - run: cargo update --package backtrace --precise 0.3.50 + - run: cargo update --package async-trait --precise 0.1.53 + - run: cargo update --package serde --precise 1.0.152 - name: Running test script - env: ${{ matrix.env }} run: ./contrib/test.sh integrations-tests: diff --git a/Cargo.toml b/Cargo.toml index df0a30c2..d9802d78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,3 +5,7 @@ members = [ "client", "integration_test", ] + +[patch."crates-io"] +bitcoin = { git = "https://github.com/stevenroose/rust-bitcoin", branch = "dev" } +bitcoin_hashes = { git = "https://github.com/stevenroose/rust-bitcoin", branch = "dev" } diff --git a/README.md b/README.md index 9cda95c3..e07e9395 100644 --- a/README.md +++ b/README.md @@ -47,4 +47,4 @@ The following versions are officially supported and automatically tested: * 0.21.0 # Minimum Supported Rust Version (MSRV) -This library should always compile with any combination of features on **Rust 1.41.1**. +This library should always compile with any combination of features on **Rust 1.48.0**. diff --git a/client/Cargo.toml b/client/Cargo.toml index 1728b77a..855b98cd 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bitcoincore-rpc" -version = "0.17.0" +version = "1.0.0-rc0" authors = [ "Steven Roose ", "Jean Pierre Dudey ", @@ -18,13 +18,18 @@ edition = "2018" name = "bitcoincore_rpc" path = "src/lib.rs" +[features] +default = [ "async" ] +async = [ "async-trait" ] + [dependencies] -bitcoincore-rpc-json = { version = "0.17.0", path = "../json" } +bitcoincore-rpc-json = { version = "1.0.0-rc0", path = "../json" } log = "0.4.5" -jsonrpc = "0.14.0" +jsonrpc = { git = "https://github.com/stevenroose/rust-jsonrpc", branch = "request" } # Used for deserialization of JSON. serde = "1" serde_json = "1" -bitcoin-private = "0.1.0" + +async-trait = { version = "0.1.53", optional = true } diff --git a/client/examples/retry_client.rs b/client/examples/retry_client.rs index 10b2fed3..78ed0e42 100644 --- a/client/examples/retry_client.rs +++ b/client/examples/retry_client.rs @@ -1,47 +1,47 @@ -// To the extent possible under law, the author(s) have dedicated all -// copyright and related and neighboring rights to this software to -// the public domain worldwide. This software is distributed without -// any warranty. -// -// You should have received a copy of the CC0 Public Domain Dedication -// along with this software. -// If not, see . -// +//// To the extent possible under law, the author(s) have dedicated all +//// copyright and related and neighboring rights to this software to +//// the public domain worldwide. This software is distributed without +//// any warranty. +//// +//// You should have received a copy of the CC0 Public Domain Dedication +//// along with this software. +//// If not, see . +//// -extern crate bitcoincore_rpc; -extern crate jsonrpc; -extern crate serde; -extern crate serde_json; +//extern crate bitcoincore_rpc; +//extern crate jsonrpc; +//extern crate serde; +//extern crate serde_json; -use bitcoincore_rpc::{Client, Error, Result, RpcApi}; +//use bitcoincore_rpc::{Client, Error, Result, RpcApi}; -pub struct RetryClient { - client: Client, -} +//pub struct RetryClient { +// client: Client, +//} -const INTERVAL: u64 = 1000; -const RETRY_ATTEMPTS: u8 = 10; +//const INTERVAL: u64 = 1000; +//const RETRY_ATTEMPTS: u8 = 10; -impl RpcApi for RetryClient { - fn call serde::de::Deserialize<'a>>( - &self, - cmd: &str, - args: &[serde_json::Value], - ) -> Result { - for _ in 0..RETRY_ATTEMPTS { - match self.client.call(cmd, args) { - Ok(ret) => return Ok(ret), - Err(Error::JsonRpc(jsonrpc::error::Error::Rpc(ref rpcerr))) - if rpcerr.code == -28 => - { - ::std::thread::sleep(::std::time::Duration::from_millis(INTERVAL)); - continue; - } - Err(e) => return Err(e), - } - } - self.client.call(cmd, args) - } -} +//impl RpcApi for RetryClient { +// fn call serde::de::Deserialize<'a>>( +// &self, +// cmd: &str, +// args: &[serde_json::Value], +// ) -> Result { +// for _ in 0..RETRY_ATTEMPTS { +// match self.client.call(cmd, args) { +// Ok(ret) => return Ok(ret), +// Err(Error::JsonRpc(jsonrpc::error::Error::Rpc(ref rpcerr))) +// if rpcerr.code == -28 => +// { +// ::std::thread::sleep(::std::time::Duration::from_millis(INTERVAL)); +// continue; +// } +// Err(e) => return Err(e), +// } +// } +// self.client.call(cmd, args) +// } +//} fn main() {} diff --git a/client/examples/test_against_node.rs b/client/examples/test_against_node.rs index e658e781..942341e5 100644 --- a/client/examples/test_against_node.rs +++ b/client/examples/test_against_node.rs @@ -1,48 +1,50 @@ -// To the extent possible under law, the author(s) have dedicated all -// copyright and related and neighboring rights to this software to -// the public domain worldwide. This software is distributed without -// any warranty. -// -// You should have received a copy of the CC0 Public Domain Dedication -// along with this software. -// If not, see . -// - -//! A very simple example used as a self-test of this library against a Bitcoin -//! Core node. -extern crate bitcoincore_rpc; - -use bitcoincore_rpc::{bitcoin, Auth, Client, Error, RpcApi}; - -fn main_result() -> Result<(), Error> { - let mut args = std::env::args(); - - let _exe_name = args.next().unwrap(); - - let url = args.next().expect("Usage: "); - let user = args.next().expect("no user given"); - let pass = args.next().expect("no pass given"); - - let rpc = Client::new(&url, Auth::UserPass(user, pass)).unwrap(); - - let _blockchain_info = rpc.get_blockchain_info()?; - - let best_block_hash = rpc.get_best_block_hash()?; - println!("best block hash: {}", best_block_hash); - let bestblockcount = rpc.get_block_count()?; - println!("best block height: {}", bestblockcount); - let best_block_hash_by_height = rpc.get_block_hash(bestblockcount)?; - println!("best block hash by height: {}", best_block_hash_by_height); - assert_eq!(best_block_hash_by_height, best_block_hash); - - let bitcoin_block: bitcoin::Block = rpc.get_by_id(&best_block_hash)?; - println!("best block hash by `get`: {}", bitcoin_block.header.prev_blockhash); - let bitcoin_tx: bitcoin::Transaction = rpc.get_by_id(&bitcoin_block.txdata[0].txid())?; - println!("tx by `get`: {}", bitcoin_tx.txid()); - - Ok(()) -} - -fn main() { - main_result().unwrap(); -} +//// To the extent possible under law, the author(s) have dedicated all +//// copyright and related and neighboring rights to this software to +//// the public domain worldwide. This software is distributed without +//// any warranty. +//// +//// You should have received a copy of the CC0 Public Domain Dedication +//// along with this software. +//// If not, see . +//// + +////! A very simple example used as a self-test of this library against a Bitcoin +////! Core node. +//extern crate bitcoincore_rpc; + +//use bitcoincore_rpc::{bitcoin, Auth, Client, Error, RpcApi}; + +//fn main_result() -> Result<(), Error> { +// let mut args = std::env::args(); + +// let _exe_name = args.next().unwrap(); + +// let url = args.next().expect("Usage: "); +// let user = args.next().expect("no user given"); +// let pass = args.next().expect("no pass given"); + +// let rpc = Client::new(&url, Auth::UserPass(user, pass)).unwrap(); + +// let _blockchain_info = rpc.get_blockchain_info()?; + +// let best_block_hash = rpc.get_best_block_hash()?; +// println!("best block hash: {}", best_block_hash); +// let bestblockcount = rpc.get_block_count()?; +// println!("best block height: {}", bestblockcount); +// let best_block_hash_by_height = rpc.get_block_hash(bestblockcount)?; +// println!("best block hash by height: {}", best_block_hash_by_height); +// assert_eq!(best_block_hash_by_height, best_block_hash); + +// let bitcoin_block: bitcoin::Block = rpc.get_by_id(&best_block_hash)?; +// println!("best block hash by `get`: {}", bitcoin_block.header.prev_blockhash); +// let bitcoin_tx: bitcoin::Transaction = rpc.get_by_id(&bitcoin_block.txdata[0].txid())?; +// println!("tx by `get`: {}", bitcoin_tx.txid()); + +// Ok(()) +//} + +//fn main() { +// main_result().unwrap(); +//} + +fn main() {} diff --git a/client/src/async_client.rs b/client/src/async_client.rs new file mode 100644 index 00000000..fb5b2112 --- /dev/null +++ b/client/src/async_client.rs @@ -0,0 +1,887 @@ + +use std::sync::atomic; + +use async_trait::async_trait; +use crate::bitcoin::secp256k1::ecdsa::Signature; +use crate::bitcoin::{ + self, Address, Amount, Block, OutPoint, PrivateKey, PublicKey, + ScriptBuf, Transaction, FeeRate, +}; +use crate::bitcoin::psbt::PartiallySignedTransaction; +use crate::bitcoin::block::Header as BlockHeader; +type UncheckedAddress = Address; + +use jsonrpc::client::{Param, List, Request, Params}; + +use crate::{ + json, requests, Client, AddressParam, Error, Result, BlockParam, BlockRef, PsbtParam, + SighashParam, TxParam, +}; +use crate::serialize::{ + HexSerializeWrapper, HexListSerializeWrapper, + OutPointListObjectSerializeWrapper, + StringListSerializeWrapper, StringSerializeWrapper, +}; + +#[async_trait(?Send)] +pub trait AsyncClient { + /// The internal method to make a request. + async fn handle_request<'r, T>(&self, req: Request<'r, T>) -> Result; + + /// Make a manual call. + async fn call serde::de::Deserialize<'a> + 'static>( + &self, + method: &str, + params: &[serde_json::Value], + ) -> Result { + let params = params.iter().map(|v| Param::ByRef(v)).collect::>(); + self.handle_request( + Request { + method: method.into(), + params: Params::ByPosition(List::Slice(¶ms[..])), + converter: &|raw| requests::converter_json(raw), + } + ).await + } + + /// Get cached version of the version. + async fn version(&self) -> Result; + + /// Refresh the cached version by asking the server again. + async fn refresh_version(&self) -> Result<()>; + + async fn get_version(&self) -> Result { + self.handle_request(requests::version()).await + } + + async fn get_network_info(&self) -> Result { + self.handle_request(requests::get_network_info()).await + } + + async fn get_index_info(&self) -> Result { + self.handle_request(requests::get_index_info()).await + } + + async fn add_multisig_address( + &self, + nrequired: usize, + keys: &[json::PubKeyOrAddress], + label: Option<&str>, + address_type: Option, + ) -> Result { + self.handle_request(requests::add_multisig_address( + nrequired, &keys, label.as_ref(), address_type.as_ref(), + )).await + } + + async fn load_wallet(&self, wallet: &str) -> Result { + self.handle_request(requests::load_wallet(&wallet)).await + } + + async fn unload_wallet(&self, wallet: Option<&str>) -> Result { + self.handle_request(requests::unload_wallet(wallet.as_ref())).await + } + + async fn create_wallet( + &self, + wallet: &str, + disable_private_keys: Option, + blank: Option, + passphrase: Option<&str>, + avoid_reuse: Option, + ) -> Result { + self.handle_request(requests::create_wallet( + &wallet, disable_private_keys, blank, passphrase.as_ref(), avoid_reuse, + )).await + } + + async fn list_wallets(&self) -> Result> { + self.handle_request(requests::list_wallets()).await + } + + async fn list_wallet_dir(&self) -> Result> { + self.handle_request(requests::list_wallet_dir()).await + } + + async fn get_wallet_info(&self) -> Result { + self.handle_request(requests::get_wallet_info()).await + } + + async fn backup_wallet(&self, destination: &str) -> Result<()> { + self.handle_request(requests::backup_wallet(&destination)).await + } + + async fn dump_private_key( + &self, + address: &(impl AddressParam + ?Sized), + ) -> Result { + self.handle_request(requests::dump_private_key(&StringSerializeWrapper(address))).await + } + + async fn encrypt_wallet(&self, passphrase: &str) -> Result { + self.handle_request(requests::encrypt_wallet(&passphrase)).await + } + + async fn get_difficulty(&self) -> Result { + self.handle_request(requests::get_difficulty()).await + } + + async fn get_connection_count(&self) -> Result { + self.handle_request(requests::get_connection_count()).await + } + + async fn get_block(&self, hash: &bitcoin::BlockHash) -> Result { + self.handle_request(requests::get_block(hash)).await + } + + async fn get_block_hex(&self, hash: &bitcoin::BlockHash) -> Result { + self.handle_request(requests::get_block_hex(hash)).await + } + + async fn get_block_info(&self, hash: &bitcoin::BlockHash) -> Result { + self.handle_request(requests::get_block_info(hash)).await + } + //TODO(stevenroose) add getblock_txs + + async fn get_block_header(&self, hash: &bitcoin::BlockHash) -> Result { + self.handle_request(requests::get_block_header(hash)).await + } + + async fn get_block_header_info( + &self, + hash: &bitcoin::BlockHash, + ) -> Result { + self.handle_request(requests::get_block_header_info(hash)).await + } + + async fn get_mining_info(&self) -> Result { + self.handle_request(requests::get_mining_info()).await + } + + async fn get_block_template( + &self, + mode: json::GetBlockTemplateModes, + rules: &[json::GetBlockTemplateRules], + capabilities: &[json::GetBlockTemplateCapabilities], + ) -> Result { + self.handle_request(requests::get_block_template(&mode, &rules, &capabilities)).await + } + + async fn get_blockchain_info(&self) -> Result { + self.handle_request(requests::get_blockchain_info()).await + } + + async fn get_block_count(&self) -> Result { + self.handle_request(requests::get_block_count()).await + } + + async fn get_best_block_hash(&self) -> Result { + self.handle_request(requests::get_best_block_hash()).await + } + + async fn get_block_hash(&self, height: u64) -> Result { + self.handle_request(requests::get_block_hash(height)).await + } + + async fn get_block_stats( + &self, + block_ref: impl BlockRef + 'async_trait, + ) -> Result { + self.handle_request(requests::get_block_stats(&block_ref)).await + } + + async fn get_block_stats_fields( + &self, + block_ref: impl BlockRef + 'async_trait, + fields: &[json::BlockStatsFields], + ) -> Result { + self.handle_request(requests::get_block_stats_fields(&block_ref, &fields)).await + } + + async fn get_raw_transaction( + &self, + txid: &bitcoin::Txid, + block_hash: Option<&bitcoin::BlockHash>, + ) -> Result { + self.handle_request(requests::get_raw_transaction(txid, block_hash)).await + } + + async fn get_raw_transaction_hex( + &self, + txid: &bitcoin::Txid, + block_hash: Option<&bitcoin::BlockHash>, + ) -> Result { + self.handle_request(requests::get_raw_transaction_hex(txid, block_hash)).await + } + + async fn get_raw_transaction_info( + &self, + txid: &bitcoin::Txid, + block_hash: Option<&bitcoin::BlockHash>, + ) -> Result { + self.handle_request(requests::get_raw_transaction_info(txid, block_hash)).await + } + + async fn get_block_filter( + &self, + block_hash: &bitcoin::BlockHash, + ) -> Result { + self.handle_request(requests::get_block_filter(block_hash)).await + } + + async fn get_balance( + &self, + minconf: Option, + include_watchonly: Option, + ) -> Result { + self.handle_request(requests::get_balance(minconf, include_watchonly)).await + } + + async fn get_balances(&self) -> Result { + self.handle_request(requests::get_balances()).await + } + + async fn get_received_by_address( + &self, + address: &(impl AddressParam + ?Sized), + minconf: Option, + ) -> Result { + self.handle_request(requests::get_received_by_address( + &StringSerializeWrapper(address), minconf, + )).await + } + + async fn get_transaction( + &self, + txid: &bitcoin::Txid, + include_watchonly: Option, + ) -> Result { + let support_verbose = self.version().await? >= 19_00_00; + + self.handle_request(requests::get_transaction(txid, include_watchonly, support_verbose)).await + } + + async fn list_transactions( + &self, + label: Option<&str>, + count: Option, + skip: Option, + include_watchonly: Option, + ) -> Result> { + self.handle_request(requests::list_transactions( + label.as_ref(), count, skip, include_watchonly, + )).await + } + + async fn list_since_block( + &self, + block_hash: Option<&bitcoin::BlockHash>, + target_confirmations: Option, + include_watchonly: Option, + include_removed: Option, + ) -> Result { + self.handle_request(requests::list_since_block( + block_hash, target_confirmations, include_watchonly, include_removed, + )).await + } + + async fn get_tx_out( + &self, + txid: &bitcoin::Txid, + vout: u32, + include_mempool: Option, + ) -> Result> { + self.handle_request(requests::get_tx_out(txid, vout, include_mempool)).await + } + + async fn get_tx_out_proof( + &self, + txids: &[bitcoin::Txid], + block_hash: Option<&bitcoin::BlockHash>, + ) -> Result> { + self.handle_request(requests::get_tx_out_proof(&txids, block_hash)).await + } + + async fn import_public_key( + &self, + public_key: &PublicKey, + label: Option<&str>, + rescan: Option, + ) -> Result<()> { + self.handle_request(requests::import_public_key(public_key, label.as_ref(), rescan)).await + } + + async fn import_private_key( + &self, + private_key: &PrivateKey, + label: Option<&str>, + rescan: Option, + ) -> Result<()> { + self.handle_request(requests::import_private_key(private_key, label.as_ref(), rescan)).await + } + + async fn import_address( + &self, + address: &(impl AddressParam + ?Sized), + label: Option<&str>, + rescan: Option, + ) -> Result<()> { + self.handle_request(requests::import_address( + &StringSerializeWrapper(address), label.as_ref(), rescan, + )).await + } + + async fn import_address_script( + &self, + script: &ScriptBuf, + label: Option<&str>, + rescan: Option, + p2sh: Option, + ) -> Result<()> { + self.handle_request(requests::import_address_script(script, label.as_ref(), rescan, p2sh)).await + } + + async fn import_multi( + &self, + requests: &[json::ImportMultiRequest], + options: Option<&json::ImportMultiOptions>, + ) -> Result> { + self.handle_request(requests::import_multi(&requests, options)).await + } + + async fn import_descriptors( + &self, + requests: &[json::ImportDescriptors], + ) -> Result> { + self.handle_request(requests::import_descriptors(&requests)).await + } + + async fn set_label( + &self, + address: &(impl AddressParam + ?Sized), + label: &str, + ) -> Result<()> { + self.handle_request(requests::set_label(&StringSerializeWrapper(address), &label)).await + } + + async fn key_pool_refill(&self, new_size: Option) -> Result<()> { + self.handle_request(requests::key_pool_refill(new_size)).await + } + + async fn list_unspent( + &self, + minconf: Option, + maxconf: Option, + addresses: Option<&[impl AddressParam]>, + include_unsafe: Option, + query_options: Option<&json::ListUnspentQueryOptions>, + ) -> Result> { + self.handle_request(requests::list_unspent( + minconf, maxconf, addresses.map(|a| StringListSerializeWrapper(a)).as_ref(), + include_unsafe, query_options, + )).await + } + + /// To unlock, use [unlock_unspent]. + async fn lock_unspent(&self, outputs: &[OutPoint]) -> Result { + self.handle_request(requests::lock_unspent( + &OutPointListObjectSerializeWrapper(outputs), + )).await + } + + async fn unlock_unspent(&self, outputs: &[OutPoint]) -> Result { + self.handle_request(requests::unlock_unspent( + &OutPointListObjectSerializeWrapper(outputs), + )).await + } + + async fn unlock_unspent_all(&self) -> Result { + self.handle_request(requests::unlock_unspent_all()).await + } + + async fn list_received_by_address( + &self, + address_filter: Option<&(impl AddressParam + ?Sized)>, + minconf: Option, + include_empty: Option, + include_watchonly: Option, + ) -> Result> { + self.handle_request(requests::list_received_by_address( + address_filter.map(|a| StringSerializeWrapper(a)).as_ref(), minconf, include_empty, + include_watchonly, + )).await + } + + async fn create_raw_transaction_hex( + &self, + inputs: &[json::CreateRawTransactionInput], + outputs: &[json::CreateRawTransactionOutput], + locktime: Option, + replaceable: Option, + ) -> Result { + self.handle_request(requests::create_raw_transaction_hex( + &inputs, &outputs, locktime, replaceable, + )).await + } + + async fn create_raw_transaction( + &self, + inputs: &[json::CreateRawTransactionInput], + outputs: &[json::CreateRawTransactionOutput], + locktime: Option, + replaceable: Option, + ) -> Result { + self.handle_request(requests::create_raw_transaction( + &inputs, &outputs, locktime, replaceable, + )).await + } + + async fn decode_raw_transaction( + &self, + tx: &(impl TxParam + ?Sized), + is_witness: Option, + ) -> Result { + self.handle_request(requests::decode_raw_transaction( + &HexSerializeWrapper(tx), is_witness, + )).await + } + + async fn fund_raw_transaction( + &self, + tx: &(impl TxParam + ?Sized), + options: Option<&json::FundRawTransactionOptions>, + is_witness: Option, + ) -> Result { + self.handle_request(requests::fund_raw_transaction( + &HexSerializeWrapper(tx), options, is_witness, + )).await + } + + async fn sign_raw_transaction_with_wallet( + &self, + tx: &(impl TxParam + ?Sized), + inputs: Option<&[json::SignRawTransactionInput]>, + sighash_type: Option<&(impl SighashParam + 'async_trait)>, + ) -> Result { + let sighash = sighash_type.as_ref().map(|v| StringSerializeWrapper(*v)); + self.handle_request(requests::sign_raw_transaction_with_wallet( + &HexSerializeWrapper(tx), + inputs.as_ref(), + sighash.as_ref(), + )).await + } + + async fn sign_raw_transaction_with_key( + &self, + tx: &(impl TxParam + ?Sized), + private_keys: &[PrivateKey], + inputs: Option<&[json::SignRawTransactionInput]>, + sighash_type: Option<&(impl SighashParam + 'async_trait)>, + ) -> Result { + let sighash = sighash_type.as_ref().map(|v| StringSerializeWrapper(*v)); + self.handle_request(requests::sign_raw_transaction_with_key( + &HexSerializeWrapper(tx), &private_keys, inputs.as_ref(), sighash.as_ref(), + )).await + } + + /// Fee rate per kvb. + async fn test_mempool_accept<'r>( + &self, + raw_txs: &[impl TxParam], + max_fee_rate: Option, + ) -> Result> { + let fr_btc_per_kvb = max_fee_rate.map(|fr| { + let per_kvb = fr.to_per_kvb() + .ok_or_else(|| Error::InvalidArguments("fee rate overflow".into()))?; + Result::<_>::Ok(per_kvb.to_btc()) + }).transpose()?; + + self.handle_request(requests::test_mempool_accept( + &HexListSerializeWrapper(raw_txs), fr_btc_per_kvb, + )).await + } + + async fn stop(&self) -> Result { + self.handle_request(requests::stop()).await + } + + async fn verify_message( + &self, + address: &(impl AddressParam + ?Sized), + signature: &Signature, + message: &str, + ) -> Result { + self.handle_request(requests::verify_message( + &StringSerializeWrapper(address), signature, &message, + )).await + } + + async fn get_new_address( + &self, + label: Option<&str>, + address_type: Option, + ) -> Result { + self.handle_request(requests::get_new_address(label.as_ref(), address_type.as_ref())).await + } + + async fn get_raw_change_address( + &self, + address_type: Option, + ) -> Result { + self.handle_request(requests::get_raw_change_address(address_type.as_ref())).await + } + + async fn get_address_info( + &self, + address: &(impl AddressParam + ?Sized), + ) -> Result { + self.handle_request(requests::get_address_info(&StringSerializeWrapper(address))).await + } + + async fn generate_to_address( + &self, + block_num: u64, + address: &(impl AddressParam + ?Sized), + max_tries: Option, + ) -> Result> { + self.handle_request(requests::generate_to_address( + block_num, &StringSerializeWrapper(address), max_tries, + )).await + } + + // NB This call is no longer available on recent Bitcoin Core versions. + #[deprecated] + async fn generate( + &self, + block_num: u64, + max_tries: Option, + ) -> Result> { + self.handle_request(requests::generate(block_num, max_tries)).await + } + + async fn invalidate_block(&self, block_hash: &bitcoin::BlockHash) -> Result<()> { + self.handle_request(requests::invalidate_block(block_hash)).await + } + + async fn reconsider_block(&self, block_hash: &bitcoin::BlockHash) -> Result<()> { + self.handle_request(requests::reconsider_block(block_hash)).await + } + + async fn get_mempool_info(&self) -> Result { + self.handle_request(requests::get_mempool_info()).await + } + + async fn get_raw_mempool(&self) -> Result> { + self.handle_request(requests::get_raw_mempool()).await + } + + async fn get_mempool_entry(&self, txid: &bitcoin::Txid) -> Result { + self.handle_request(requests::get_mempool_entry(txid)).await + } + + async fn get_chain_tips(&self) -> Result { + self.handle_request(requests::get_chain_tips()).await + } + + //TODO(stevenroose) make this call more ergonomic, it's getting insane + // NB the [avoid_reuse] argument is not supported for Bitcoin Core version 0.18 and older. + // NB the [fee_rate] argument is not supported for Bitcoin Core versions 0.19 and older. + async fn send_to_address( + &self, + address: &(impl AddressParam + ?Sized), + amount: Amount, + comment: Option<&str>, + comment_to: Option<&str>, + subtract_fee: Option, + replaceable: Option, + confirmation_target: Option, + estimate_mode: Option, + avoid_reuse: Option, + fee_rate: Option, + ) -> Result { + let support_verbose = self.version().await? >= 21_00_00; + + self.handle_request(requests::send_to_address( + &StringSerializeWrapper(address), amount, comment.as_ref(), comment_to.as_ref(), + subtract_fee, replaceable, confirmation_target, estimate_mode.as_ref(), avoid_reuse, + support_verbose, fee_rate.map(|fr| fr.to_per_vb_ceil().to_sat()), + )).await + } + + async fn add_node(&self, addr: &str) -> Result<()> { + self.handle_request(requests::add_node(&addr)).await + } + + async fn add_node_onetry(&self, addr: &str) -> Result<()> { + self.handle_request(requests::add_node_onetry(&addr)).await + } + + async fn remove_node(&self, addr: &str) -> Result<()> { + self.handle_request(requests::remove_node(&addr)).await + } + + async fn disconnect_node(&self, addr: &str) -> Result<()> { + self.handle_request(requests::disconnect_node(&addr)).await + } + + async fn disconnect_node_by_id(&self, node_id: u32) -> Result<()> { + self.handle_request(requests::disconnect_node_by_id(node_id)).await + } + + async fn get_added_node_info( + &self, + node: &str, + ) -> Result> { + self.handle_request(requests::get_added_node_info(&node)).await + } + + async fn get_added_nodes_info(&self) -> Result> { + self.handle_request(requests::get_added_nodes_info()).await + } + + async fn get_node_addresses( + &self, + count: Option, + ) -> Result> { + self.handle_request(requests::get_node_addresses(count)).await + } + + async fn list_banned(&self) -> Result> { + self.handle_request(requests::list_banned()).await + } + + async fn clear_banned(&self) -> Result<()> { + self.handle_request(requests::clear_banned()).await + } + + async fn add_ban(&self, subnet: &str, bantime: u64, absolute: bool) -> Result<()> { + self.handle_request(requests::add_ban(&subnet, bantime, absolute)).await + } + + async fn remove_ban(&self, subnet: &str) -> Result<()> { + self.handle_request(requests::remove_ban(&subnet)).await + } + + async fn set_network_active(&self, state: bool) -> Result { + self.handle_request(requests::set_network_active(state)).await + } + + async fn get_peer_info(&self) -> Result> { + self.handle_request(requests::get_peer_info()).await + } + + async fn ping(&self) -> Result<()> { + self.handle_request(requests::ping()).await + } + + async fn send_raw_transaction( + &self, + tx: &(impl TxParam + ?Sized), + ) -> Result { + self.handle_request(requests::send_raw_transaction(&HexSerializeWrapper(tx))).await + } + + async fn estimate_smart_fee( + &self, + conf_target: u16, + estimate_mode: Option, + ) -> Result { + self.handle_request(requests::estimate_smart_fee(conf_target, estimate_mode.as_ref())).await + } + + async fn wait_for_new_block(&self, timeout: Option) -> Result { + self.handle_request(requests::wait_for_new_block(timeout)).await + } + + async fn wait_for_block( + &self, + block_hash: &bitcoin::BlockHash, + timeout: Option, + ) -> Result { + self.handle_request(requests::wait_for_block(block_hash, timeout)).await + } + + async fn get_descriptor_info( + &self, + descriptor: &str, + ) -> Result { + self.handle_request(requests::get_descriptor_info(&descriptor)).await + } + + async fn derive_addresses( + &self, + descriptor: &str, + range: Option<&[u32; 2]>, + ) -> Result> { + self.handle_request(requests::derive_addresses(&descriptor, range)).await + } + + async fn create_psbt_raw( + &self, + inputs: &[json::CreateRawTransactionInput], + outputs: &[json::CreateRawTransactionOutput], + locktime: Option, + replaceable: Option, + ) -> Result { + self.handle_request(requests::create_psbt_raw( + &inputs, &outputs, locktime, replaceable, + )).await + } + + async fn create_psbt( + &self, + inputs: &[json::CreateRawTransactionInput], + outputs: &[json::CreateRawTransactionOutput], + locktime: Option, + replaceable: Option, + ) -> Result { + self.handle_request(requests::create_psbt( + &inputs, &outputs, locktime, replaceable, + )).await + } + + async fn wallet_create_funded_psbt( + &self, + inputs: &[json::CreateRawTransactionInput], + outputs: &[json::CreateRawTransactionOutput], + locktime: Option, + options: Option<&json::WalletCreateFundedPsbtOptions>, + include_bip32_derivations: Option, + ) -> Result { + self.handle_request(requests::wallet_create_funded_psbt( + &inputs, &outputs, locktime, options, include_bip32_derivations, + )).await + } + + /// NB the [sighash_type] argument is not optional in all version of Bitcoin Core. + async fn wallet_process_psbt( + &self, + psbt: &(impl PsbtParam + ?Sized), + sign: Option, + sighash_type: Option<&json::SigHashType>, + include_bip32_derivations: Option, + ) -> Result { + // Somehow if the bip32derivs parameter is set, the sighashtype is not optional. + let version = self.version().await?; + if version >= 18_00_00 && version <= 22_00_00 + && include_bip32_derivations.is_some() && sighash_type.is_none() + { + return Err(Error::InvalidArguments(format!( + "the `sighash_type` argument is required when the `include_bip32_derivations` \ + argument is provided" + ))); + } + + self.handle_request(requests::wallet_process_psbt( + &StringSerializeWrapper(psbt), sign, sighash_type, include_bip32_derivations, + )).await + } + + async fn join_psbts_raw(&self, psbts: &[impl PsbtParam]) -> Result { + self.handle_request(requests::join_psbts_raw(&StringListSerializeWrapper(psbts))).await + } + + async fn join_psbts(&self, psbts: &[impl PsbtParam]) -> Result { + self.handle_request(requests::join_psbts(&StringListSerializeWrapper(psbts))).await + } + + async fn combine_psbt_raw(&self, psbts: &[impl PsbtParam]) -> Result { + self.handle_request(requests::combine_psbt_raw(&StringListSerializeWrapper(psbts))).await + } + + async fn combine_psbt(&self, psbts: &[impl PsbtParam]) -> Result { + self.handle_request(requests::combine_psbt(&StringListSerializeWrapper(psbts))).await + } + + async fn combine_raw_transaction_hex(&self, txs: &[impl TxParam]) -> Result { + self.handle_request( + requests::combine_raw_transaction_hex(&HexListSerializeWrapper(txs)), + ).await + } + + async fn combine_raw_transaction(&self, txs: &[impl TxParam]) -> Result { + self.handle_request( + requests::combine_raw_transaction(&HexListSerializeWrapper(txs)), + ).await + } + + async fn finalize_psbt( + &self, + psbt: &(impl PsbtParam + ?Sized), + extract: Option, + ) -> Result { + self.handle_request(requests::finalize_psbt(&StringSerializeWrapper(psbt), extract)).await + } + + async fn rescan_blockchain( + &self, + start_height: Option, + stop_height: Option, + ) -> Result<(usize, Option)> { + self.handle_request(requests::rescan_blockchain(start_height, stop_height)).await + } + + async fn get_tx_out_set_info( + &self, + hash_type: Option, + target_block_ref: Option, + use_index: Option, + ) -> Result { + self.handle_request(requests::get_tx_out_set_info( + hash_type.as_ref(), target_block_ref.as_ref(), use_index, + )).await + } + + async fn get_net_totals(&self) -> Result { + self.handle_request(requests::get_net_totals()).await + } + + async fn get_network_hash_ps( + &self, + nb_blocks: Option, + height: Option, + ) -> Result { + self.handle_request(requests::get_network_hash_ps(nb_blocks, height)).await + } + + async fn uptime(&self) -> Result { + self.handle_request(requests::uptime()).await + } + + async fn submit_block(&self, block: &(impl BlockParam + ?Sized)) -> Result<()> { + self.handle_request(requests::submit_block(&HexSerializeWrapper(block))).await + } + + async fn scan_tx_out_set_blocking( + &self, + descriptors: &[json::ScanTxOutRequest], + ) -> Result { + self.handle_request(requests::scan_tx_out_set_blocking(&descriptors)).await + } + +} + +#[async_trait(?Send)] +impl AsyncClient for Client { + async fn version(&self) -> Result { + let ver = self.version.load(atomic::Ordering::Relaxed); + + if ver > 0 { + Ok(ver) + } else { + self.refresh_version().await?; + Ok(self.version.load(atomic::Ordering::Relaxed)) + } + } + + async fn refresh_version(&self) -> Result<()> { + let ver = self.get_version().await?; + self.version.store(ver, atomic::Ordering::Relaxed); + Ok(()) + } + + #[inline(always)] + async fn handle_request<'r, R>(&self, req: Request<'r, R>) -> Result { + Ok(req.get_async(&self.client).await?) + } +} diff --git a/client/src/client.rs b/client/src/client.rs index edb71e30..99e04848 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -8,187 +8,17 @@ // If not, see . // -use std::collections::HashMap; +use std::fmt; use std::fs::File; -use std::iter::FromIterator; +use std::io::Read; use std::path::PathBuf; -use std::{fmt, result}; +use std::sync::atomic; -use crate::{bitcoin, deserialize_hex}; -use bitcoin_private::hex::exts::DisplayHex; use jsonrpc; -use serde; -use serde_json; - -use crate::bitcoin::address::{NetworkUnchecked, NetworkChecked}; -use crate::bitcoin::hashes::hex::FromHex; -use crate::bitcoin::secp256k1::ecdsa::Signature; -use crate::bitcoin::{ - Address, Amount, Block, OutPoint, PrivateKey, PublicKey, Script, Transaction, -}; use log::Level::{Debug, Trace, Warn}; +use serde_json; -use crate::error::*; -use crate::json; -use crate::queryable; - -/// Crate-specific Result type, shorthand for `std::result::Result` with our -/// crate-specific Error type; -pub type Result = result::Result; - -/// Outpoint that serializes and deserializes as a map, instead of a string, -/// for use as RPC arguments -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct JsonOutPoint { - pub txid: bitcoin::Txid, - pub vout: u32, -} - -impl From for JsonOutPoint { - fn from(o: OutPoint) -> JsonOutPoint { - JsonOutPoint { - txid: o.txid, - vout: o.vout, - } - } -} - -impl Into for JsonOutPoint { - fn into(self) -> OutPoint { - OutPoint { - txid: self.txid, - vout: self.vout, - } - } -} - -/// Shorthand for converting a variable into a serde_json::Value. -fn into_json(val: T) -> Result -where - T: serde::ser::Serialize, -{ - Ok(serde_json::to_value(val)?) -} - -/// Shorthand for converting an Option into an Option. -fn opt_into_json(opt: Option) -> Result -where - T: serde::ser::Serialize, -{ - match opt { - Some(val) => Ok(into_json(val)?), - None => Ok(serde_json::Value::Null), - } -} - -/// Shorthand for `serde_json::Value::Null`. -fn null() -> serde_json::Value { - serde_json::Value::Null -} - -/// Shorthand for an empty serde_json::Value array. -fn empty_arr() -> serde_json::Value { - serde_json::Value::Array(vec![]) -} - -/// Shorthand for an empty serde_json object. -fn empty_obj() -> serde_json::Value { - serde_json::Value::Object(Default::default()) -} - -/// Handle default values in the argument list -/// -/// Substitute `Value::Null`s with corresponding values from `defaults` table, -/// except when they are trailing, in which case just skip them altogether -/// in returned list. -/// -/// Note, that `defaults` corresponds to the last elements of `args`. -/// -/// ```norust -/// arg1 arg2 arg3 arg4 -/// def1 def2 -/// ``` -/// -/// Elements of `args` without corresponding `defaults` value, won't -/// be substituted, because they are required. -fn handle_defaults<'a, 'b>( - args: &'a mut [serde_json::Value], - defaults: &'b [serde_json::Value], -) -> &'a [serde_json::Value] { - assert!(args.len() >= defaults.len()); - - // Pass over the optional arguments in backwards order, filling in defaults after the first - // non-null optional argument has been observed. - let mut first_non_null_optional_idx = None; - for i in 0..defaults.len() { - let args_i = args.len() - 1 - i; - let defaults_i = defaults.len() - 1 - i; - if args[args_i] == serde_json::Value::Null { - if first_non_null_optional_idx.is_some() { - if defaults[defaults_i] == serde_json::Value::Null { - panic!("Missing `default` for argument idx {}", args_i); - } - args[args_i] = defaults[defaults_i].clone(); - } - } else if first_non_null_optional_idx.is_none() { - first_non_null_optional_idx = Some(args_i); - } - } - - let required_num = args.len() - defaults.len(); - - if let Some(i) = first_non_null_optional_idx { - &args[..i + 1] - } else { - &args[..required_num] - } -} - -/// Convert a possible-null result into an Option. -fn opt_result serde::de::Deserialize<'a>>( - result: serde_json::Value, -) -> Result> { - if result == serde_json::Value::Null { - Ok(None) - } else { - Ok(serde_json::from_value(result)?) - } -} - -/// Used to pass raw txs into the API. -pub trait RawTx: Sized + Clone { - fn raw_hex(self) -> String; -} - -impl<'a> RawTx for &'a Transaction { - fn raw_hex(self) -> String { - bitcoin::consensus::encode::serialize_hex(self) - } -} - -impl<'a> RawTx for &'a [u8] { - fn raw_hex(self) -> String { - self.to_lower_hex_string() - } -} - -impl<'a> RawTx for &'a Vec { - fn raw_hex(self) -> String { - self.to_lower_hex_string() - } -} - -impl<'a> RawTx for &'a str { - fn raw_hex(self) -> String { - self.to_owned() - } -} - -impl RawTx for String { - fn raw_hex(self) -> String { - self - } -} +use crate::{Error, Result}; /// The different authentication methods for the client. #[derive(Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] @@ -201,7 +31,6 @@ pub enum Auth { impl Auth { /// Convert into the arguments that jsonrpc::Client needs. pub fn get_user_pass(self) -> Result<(Option, Option)> { - use std::io::Read; match self { Auth::None => Ok((None, None)), Auth::UserPass(u, p) => Ok((Some(u), Some(p))), @@ -219,1116 +48,49 @@ impl Auth { } } -pub trait RpcApi: Sized { - /// Call a `cmd` rpc with given `args` list - fn call serde::de::Deserialize<'a>>( - &self, - cmd: &str, - args: &[serde_json::Value], - ) -> Result; - - /// Query an object implementing `Querable` type - fn get_by_id>( - &self, - id: &>::Id, - ) -> Result { - T::query(&self, &id) - } - - fn get_network_info(&self) -> Result { - self.call("getnetworkinfo", &[]) - } - - fn get_index_info(&self) -> Result { - self.call("getindexinfo", &[]) - } - - fn version(&self) -> Result { - #[derive(Deserialize)] - struct Response { - pub version: usize, - } - let res: Response = self.call("getnetworkinfo", &[])?; - Ok(res.version) - } - - fn add_multisig_address( - &self, - nrequired: usize, - keys: &[json::PubKeyOrAddress], - label: Option<&str>, - address_type: Option, - ) -> Result { - let mut args = [ - into_json(nrequired)?, - into_json(keys)?, - opt_into_json(label)?, - opt_into_json(address_type)?, - ]; - self.call("addmultisigaddress", handle_defaults(&mut args, &[into_json("")?, null()])) - } - - fn load_wallet(&self, wallet: &str) -> Result { - self.call("loadwallet", &[wallet.into()]) - } - - fn unload_wallet(&self, wallet: Option<&str>) -> Result> { - let mut args = [opt_into_json(wallet)?]; - self.call("unloadwallet", handle_defaults(&mut args, &[null()])) - } - - fn create_wallet( - &self, - wallet: &str, - disable_private_keys: Option, - blank: Option, - passphrase: Option<&str>, - avoid_reuse: Option, - ) -> Result { - let mut args = [ - wallet.into(), - opt_into_json(disable_private_keys)?, - opt_into_json(blank)?, - opt_into_json(passphrase)?, - opt_into_json(avoid_reuse)?, - ]; - self.call( - "createwallet", - handle_defaults(&mut args, &[false.into(), false.into(), into_json("")?, false.into()]), - ) - } - - fn list_wallets(&self) -> Result> { - self.call("listwallets", &[]) - } - - fn list_wallet_dir(&self) -> Result> { - let result: json::ListWalletDirResult = self.call("listwalletdir", &[])?; - let names = result.wallets.into_iter().map(|x| x.name).collect(); - Ok(names) - } - - fn get_wallet_info(&self) -> Result { - self.call("getwalletinfo", &[]) - } - - fn backup_wallet(&self, destination: Option<&str>) -> Result<()> { - let mut args = [opt_into_json(destination)?]; - self.call("backupwallet", handle_defaults(&mut args, &[null()])) - } - - fn dump_private_key(&self, address: &Address) -> Result { - self.call("dumpprivkey", &[address.to_string().into()]) - } - - fn encrypt_wallet(&self, passphrase: &str) -> Result<()> { - self.call("encryptwallet", &[into_json(passphrase)?]) - } - - fn get_difficulty(&self) -> Result { - self.call("getdifficulty", &[]) - } - - fn get_connection_count(&self) -> Result { - self.call("getconnectioncount", &[]) - } - - fn get_block(&self, hash: &bitcoin::BlockHash) -> Result { - let hex: String = self.call("getblock", &[into_json(hash)?, 0.into()])?; - deserialize_hex(&hex) - } - - fn get_block_hex(&self, hash: &bitcoin::BlockHash) -> Result { - self.call("getblock", &[into_json(hash)?, 0.into()]) - } - - fn get_block_info(&self, hash: &bitcoin::BlockHash) -> Result { - self.call("getblock", &[into_json(hash)?, 1.into()]) - } - //TODO(stevenroose) add getblock_txs - - fn get_block_header(&self, hash: &bitcoin::BlockHash) -> Result { - let hex: String = self.call("getblockheader", &[into_json(hash)?, false.into()])?; - deserialize_hex(&hex) - } - - fn get_block_header_info( - &self, - hash: &bitcoin::BlockHash, - ) -> Result { - self.call("getblockheader", &[into_json(hash)?, true.into()]) - } - - fn get_mining_info(&self) -> Result { - self.call("getmininginfo", &[]) - } - - fn get_block_template( - &self, - mode: json::GetBlockTemplateModes, - rules: &[json::GetBlockTemplateRules], - capabilities: &[json::GetBlockTemplateCapabilities], - ) -> Result { - #[derive(Serialize)] - struct Argument<'a> { - mode: json::GetBlockTemplateModes, - rules: &'a [json::GetBlockTemplateRules], - capabilities: &'a [json::GetBlockTemplateCapabilities], - } - - self.call( - "getblocktemplate", - &[into_json(Argument { - mode: mode, - rules: rules, - capabilities: capabilities, - })?], - ) - } - - /// Returns a data structure containing various state info regarding - /// blockchain processing. - fn get_blockchain_info(&self) -> Result { - let mut raw: serde_json::Value = self.call("getblockchaininfo", &[])?; - // The softfork fields are not backwards compatible: - // - 0.18.x returns a "softforks" array and a "bip9_softforks" map. - // - 0.19.x returns a "softforks" map. - Ok(if self.version()? < 190000 { - use crate::Error::UnexpectedStructure as err; - - // First, remove both incompatible softfork fields. - // We need to scope the mutable ref here for v1.29 borrowck. - let (bip9_softforks, old_softforks) = { - let map = raw.as_object_mut().ok_or(err)?; - let bip9_softforks = map.remove("bip9_softforks").ok_or(err)?; - let old_softforks = map.remove("softforks").ok_or(err)?; - // Put back an empty "softforks" field. - map.insert("softforks".into(), serde_json::Map::new().into()); - (bip9_softforks, old_softforks) - }; - let mut ret: json::GetBlockchainInfoResult = serde_json::from_value(raw)?; - - // Then convert both softfork types and add them. - for sf in old_softforks.as_array().ok_or(err)?.iter() { - let json = sf.as_object().ok_or(err)?; - let id = json.get("id").ok_or(err)?.as_str().ok_or(err)?; - let reject = json.get("reject").ok_or(err)?.as_object().ok_or(err)?; - let active = reject.get("status").ok_or(err)?.as_bool().ok_or(err)?; - ret.softforks.insert( - id.into(), - json::Softfork { - type_: json::SoftforkType::Buried, - bip9: None, - height: None, - active: active, - }, - ); - } - for (id, sf) in bip9_softforks.as_object().ok_or(err)?.iter() { - #[derive(Deserialize)] - struct OldBip9SoftFork { - pub status: json::Bip9SoftforkStatus, - pub bit: Option, - #[serde(rename = "startTime")] - pub start_time: i64, - pub timeout: u64, - pub since: u32, - pub statistics: Option, - } - let sf: OldBip9SoftFork = serde_json::from_value(sf.clone())?; - ret.softforks.insert( - id.clone(), - json::Softfork { - type_: json::SoftforkType::Bip9, - bip9: Some(json::Bip9SoftforkInfo { - status: sf.status, - bit: sf.bit, - start_time: sf.start_time, - timeout: sf.timeout, - since: sf.since, - statistics: sf.statistics, - }), - height: None, - active: sf.status == json::Bip9SoftforkStatus::Active, - }, - ); - } - ret - } else { - serde_json::from_value(raw)? - }) - } - - /// Returns the numbers of block in the longest chain. - fn get_block_count(&self) -> Result { - self.call("getblockcount", &[]) - } - - /// Returns the hash of the best (tip) block in the longest blockchain. - fn get_best_block_hash(&self) -> Result { - self.call("getbestblockhash", &[]) - } - - /// Get block hash at a given height - fn get_block_hash(&self, height: u64) -> Result { - self.call("getblockhash", &[height.into()]) - } - - fn get_block_stats(&self, height: u64) -> Result { - self.call("getblockstats", &[height.into()]) - } - - fn get_block_stats_fields( - &self, - height: u64, - fields: &[json::BlockStatsFields], - ) -> Result { - self.call("getblockstats", &[height.into(), fields.into()]) - } - - fn get_raw_transaction( - &self, - txid: &bitcoin::Txid, - block_hash: Option<&bitcoin::BlockHash>, - ) -> Result { - let mut args = [into_json(txid)?, into_json(false)?, opt_into_json(block_hash)?]; - let hex: String = self.call("getrawtransaction", handle_defaults(&mut args, &[null()]))?; - deserialize_hex(&hex) - } - - fn get_raw_transaction_hex( - &self, - txid: &bitcoin::Txid, - block_hash: Option<&bitcoin::BlockHash>, - ) -> Result { - let mut args = [into_json(txid)?, into_json(false)?, opt_into_json(block_hash)?]; - self.call("getrawtransaction", handle_defaults(&mut args, &[null()])) - } - - fn get_raw_transaction_info( - &self, - txid: &bitcoin::Txid, - block_hash: Option<&bitcoin::BlockHash>, - ) -> Result { - let mut args = [into_json(txid)?, into_json(true)?, opt_into_json(block_hash)?]; - self.call("getrawtransaction", handle_defaults(&mut args, &[null()])) - } - - fn get_block_filter( - &self, - block_hash: &bitcoin::BlockHash, - ) -> Result { - self.call("getblockfilter", &[into_json(block_hash)?]) - } - - fn get_balance( - &self, - minconf: Option, - include_watchonly: Option, - ) -> Result { - let mut args = ["*".into(), opt_into_json(minconf)?, opt_into_json(include_watchonly)?]; - Ok(Amount::from_btc( - self.call("getbalance", handle_defaults(&mut args, &[0.into(), null()]))?, - )?) - } - - fn get_balances(&self) -> Result { - Ok(self.call("getbalances", &[])?) - } - - fn get_received_by_address(&self, address: &Address, minconf: Option) -> Result { - let mut args = [address.to_string().into(), opt_into_json(minconf)?]; - Ok(Amount::from_btc( - self.call("getreceivedbyaddress", handle_defaults(&mut args, &[null()]))?, - )?) - } - - fn get_transaction( - &self, - txid: &bitcoin::Txid, - include_watchonly: Option, - ) -> Result { - let mut args = [into_json(txid)?, opt_into_json(include_watchonly)?]; - self.call("gettransaction", handle_defaults(&mut args, &[null()])) - } - - fn list_transactions( - &self, - label: Option<&str>, - count: Option, - skip: Option, - include_watchonly: Option, - ) -> Result> { - let mut args = [ - label.unwrap_or("*").into(), - opt_into_json(count)?, - opt_into_json(skip)?, - opt_into_json(include_watchonly)?, - ]; - self.call("listtransactions", handle_defaults(&mut args, &[10.into(), 0.into(), null()])) - } - - fn list_since_block( - &self, - blockhash: Option<&bitcoin::BlockHash>, - target_confirmations: Option, - include_watchonly: Option, - include_removed: Option, - ) -> Result { - let mut args = [ - opt_into_json(blockhash)?, - opt_into_json(target_confirmations)?, - opt_into_json(include_watchonly)?, - opt_into_json(include_removed)?, - ]; - self.call("listsinceblock", handle_defaults(&mut args, &[null()])) - } - - fn get_tx_out( - &self, - txid: &bitcoin::Txid, - vout: u32, - include_mempool: Option, - ) -> Result> { - let mut args = [into_json(txid)?, into_json(vout)?, opt_into_json(include_mempool)?]; - opt_result(self.call("gettxout", handle_defaults(&mut args, &[null()]))?) - } - - fn get_tx_out_proof( - &self, - txids: &[bitcoin::Txid], - block_hash: Option<&bitcoin::BlockHash>, - ) -> Result> { - let mut args = [into_json(txids)?, opt_into_json(block_hash)?]; - let hex: String = self.call("gettxoutproof", handle_defaults(&mut args, &[null()]))?; - Ok(FromHex::from_hex(&hex)?) - } - - fn import_public_key( - &self, - pubkey: &PublicKey, - label: Option<&str>, - rescan: Option, - ) -> Result<()> { - let mut args = [pubkey.to_string().into(), opt_into_json(label)?, opt_into_json(rescan)?]; - self.call("importpubkey", handle_defaults(&mut args, &[into_json("")?, null()])) - } - - fn import_private_key( - &self, - privkey: &PrivateKey, - label: Option<&str>, - rescan: Option, - ) -> Result<()> { - let mut args = [privkey.to_string().into(), opt_into_json(label)?, opt_into_json(rescan)?]; - self.call("importprivkey", handle_defaults(&mut args, &[into_json("")?, null()])) - } - - fn import_address( - &self, - address: &Address, - label: Option<&str>, - rescan: Option, - ) -> Result<()> { - let mut args = [address.to_string().into(), opt_into_json(label)?, opt_into_json(rescan)?]; - self.call("importaddress", handle_defaults(&mut args, &[into_json("")?, null()])) - } - - fn import_address_script( - &self, - script: &Script, - label: Option<&str>, - rescan: Option, - p2sh: Option, - ) -> Result<()> { - let mut args = [ - script.to_hex_string().into(), - opt_into_json(label)?, - opt_into_json(rescan)?, - opt_into_json(p2sh)?, - ]; - self.call( - "importaddress", - handle_defaults(&mut args, &[into_json("")?, true.into(), null()]), - ) - } - - fn import_multi( - &self, - requests: &[json::ImportMultiRequest], - options: Option<&json::ImportMultiOptions>, - ) -> Result> { - let mut json_requests = Vec::with_capacity(requests.len()); - for req in requests { - json_requests.push(serde_json::to_value(req)?); - } - let mut args = [json_requests.into(), opt_into_json(options)?]; - self.call("importmulti", handle_defaults(&mut args, &[null()])) - } - - fn import_descriptors( - &self, - req: json::ImportDescriptors, - ) -> Result> { - let json_request = vec![serde_json::to_value(req)?]; - self.call("importdescriptors", handle_defaults(&mut [json_request.into()], &[null()])) - } - - fn set_label(&self, address: &Address, label: &str) -> Result<()> { - self.call("setlabel", &[address.to_string().into(), label.into()]) - } - - fn key_pool_refill(&self, new_size: Option) -> Result<()> { - let mut args = [opt_into_json(new_size)?]; - self.call("keypoolrefill", handle_defaults(&mut args, &[null()])) - } - - fn list_unspent( - &self, - minconf: Option, - maxconf: Option, - addresses: Option<&[&Address]>, - include_unsafe: Option, - query_options: Option, - ) -> Result> { - let mut args = [ - opt_into_json(minconf)?, - opt_into_json(maxconf)?, - opt_into_json(addresses)?, - opt_into_json(include_unsafe)?, - opt_into_json(query_options)?, - ]; - let defaults = [into_json(0)?, into_json(9999999)?, empty_arr(), into_json(true)?, null()]; - self.call("listunspent", handle_defaults(&mut args, &defaults)) - } - - /// To unlock, use [unlock_unspent]. - fn lock_unspent(&self, outputs: &[OutPoint]) -> Result { - let outputs: Vec<_> = outputs - .into_iter() - .map(|o| serde_json::to_value(JsonOutPoint::from(*o)).unwrap()) - .collect(); - self.call("lockunspent", &[false.into(), outputs.into()]) - } - - fn unlock_unspent(&self, outputs: &[OutPoint]) -> Result { - let outputs: Vec<_> = outputs - .into_iter() - .map(|o| serde_json::to_value(JsonOutPoint::from(*o)).unwrap()) - .collect(); - self.call("lockunspent", &[true.into(), outputs.into()]) - } - - /// Unlock all unspent UTXOs. - fn unlock_unspent_all(&self) -> Result { - self.call("lockunspent", &[true.into()]) - } - - fn list_received_by_address( - &self, - address_filter: Option<&Address>, - minconf: Option, - include_empty: Option, - include_watchonly: Option, - ) -> Result> { - let mut args = [ - opt_into_json(minconf)?, - opt_into_json(include_empty)?, - opt_into_json(include_watchonly)?, - opt_into_json(address_filter)?, - ]; - let defaults = [1.into(), false.into(), false.into(), null()]; - self.call("listreceivedbyaddress", handle_defaults(&mut args, &defaults)) - } - - fn create_psbt( - &self, - inputs: &[json::CreateRawTransactionInput], - outputs: &HashMap, - locktime: Option, - replaceable: Option, - ) -> Result { - let outs_converted = serde_json::Map::from_iter( - outputs.iter().map(|(k, v)| (k.clone(), serde_json::Value::from(v.to_btc()))), - ); - self.call( - "createpsbt", - &[ - into_json(inputs)?, - into_json(outs_converted)?, - into_json(locktime)?, - into_json(replaceable)?, - ], - ) - } - - fn create_raw_transaction_hex( - &self, - utxos: &[json::CreateRawTransactionInput], - outs: &HashMap, - locktime: Option, - replaceable: Option, - ) -> Result { - let outs_converted = serde_json::Map::from_iter( - outs.iter().map(|(k, v)| (k.clone(), serde_json::Value::from(v.to_btc()))), - ); - let mut args = [ - into_json(utxos)?, - into_json(outs_converted)?, - opt_into_json(locktime)?, - opt_into_json(replaceable)?, - ]; - let defaults = [into_json(0i64)?, null()]; - self.call("createrawtransaction", handle_defaults(&mut args, &defaults)) - } - - fn create_raw_transaction( - &self, - utxos: &[json::CreateRawTransactionInput], - outs: &HashMap, - locktime: Option, - replaceable: Option, - ) -> Result { - let hex: String = self.create_raw_transaction_hex(utxos, outs, locktime, replaceable)?; - deserialize_hex(&hex) - } - - fn decode_raw_transaction( - &self, - tx: R, - is_witness: Option, - ) -> Result { - let mut args = [tx.raw_hex().into(), opt_into_json(is_witness)?]; - let defaults = [null()]; - self.call("decoderawtransaction", handle_defaults(&mut args, &defaults)) - } - - fn fund_raw_transaction( - &self, - tx: R, - options: Option<&json::FundRawTransactionOptions>, - is_witness: Option, - ) -> Result { - let mut args = [tx.raw_hex().into(), opt_into_json(options)?, opt_into_json(is_witness)?]; - let defaults = [empty_obj(), null()]; - self.call("fundrawtransaction", handle_defaults(&mut args, &defaults)) - } - - #[deprecated] - fn sign_raw_transaction( - &self, - tx: R, - utxos: Option<&[json::SignRawTransactionInput]>, - private_keys: Option<&[PrivateKey]>, - sighash_type: Option, - ) -> Result { - let mut args = [ - tx.raw_hex().into(), - opt_into_json(utxos)?, - opt_into_json(private_keys)?, - opt_into_json(sighash_type)?, - ]; - let defaults = [empty_arr(), empty_arr(), null()]; - self.call("signrawtransaction", handle_defaults(&mut args, &defaults)) - } - - fn sign_raw_transaction_with_wallet( - &self, - tx: R, - utxos: Option<&[json::SignRawTransactionInput]>, - sighash_type: Option, - ) -> Result { - let mut args = [tx.raw_hex().into(), opt_into_json(utxos)?, opt_into_json(sighash_type)?]; - let defaults = [empty_arr(), null()]; - self.call("signrawtransactionwithwallet", handle_defaults(&mut args, &defaults)) - } - - fn sign_raw_transaction_with_key( - &self, - tx: R, - privkeys: &[PrivateKey], - prevtxs: Option<&[json::SignRawTransactionInput]>, - sighash_type: Option, - ) -> Result { - let mut args = [ - tx.raw_hex().into(), - into_json(privkeys)?, - opt_into_json(prevtxs)?, - opt_into_json(sighash_type)?, - ]; - let defaults = [empty_arr(), null()]; - self.call("signrawtransactionwithkey", handle_defaults(&mut args, &defaults)) - } - - fn test_mempool_accept( - &self, - rawtxs: &[R], - ) -> Result> { - let hexes: Vec = - rawtxs.to_vec().into_iter().map(|r| r.raw_hex().into()).collect(); - self.call("testmempoolaccept", &[hexes.into()]) - } - - fn stop(&self) -> Result { - self.call("stop", &[]) - } - - fn verify_message( - &self, - address: &Address, - signature: &Signature, - message: &str, - ) -> Result { - let args = [address.to_string().into(), signature.to_string().into(), into_json(message)?]; - self.call("verifymessage", &args) - } - - /// Generate new address under own control - fn get_new_address( - &self, - label: Option<&str>, - address_type: Option, - ) -> Result> { - self.call("getnewaddress", &[opt_into_json(label)?, opt_into_json(address_type)?]) - } - - /// Generate new address for receiving change - fn get_raw_change_address(&self, address_type: Option) -> Result> { - self.call("getrawchangeaddress", &[opt_into_json(address_type)?]) - } - - fn get_address_info(&self, address: &Address) -> Result { - self.call("getaddressinfo", &[address.to_string().into()]) - } - - /// Mine `block_num` blocks and pay coinbase to `address` - /// - /// Returns hashes of the generated blocks - fn generate_to_address( - &self, - block_num: u64, - address: &Address, - ) -> Result> { - self.call("generatetoaddress", &[block_num.into(), address.to_string().into()]) - } - - /// Mine up to block_num blocks immediately (before the RPC call returns) - /// to an address in the wallet. - fn generate(&self, block_num: u64, maxtries: Option) -> Result> { - self.call("generate", &[block_num.into(), opt_into_json(maxtries)?]) - } - - /// Mark a block as invalid by `block_hash` - fn invalidate_block(&self, block_hash: &bitcoin::BlockHash) -> Result<()> { - self.call("invalidateblock", &[into_json(block_hash)?]) - } - - /// Mark a block as valid by `block_hash` - fn reconsider_block(&self, block_hash: &bitcoin::BlockHash) -> Result<()> { - self.call("reconsiderblock", &[into_json(block_hash)?]) - } - - /// Returns details on the active state of the TX memory pool - fn get_mempool_info(&self) -> Result { - self.call("getmempoolinfo", &[]) - } - - /// Get txids of all transactions in a memory pool - fn get_raw_mempool(&self) -> Result> { - self.call("getrawmempool", &[]) - } - - /// Get details for the transactions in a memory pool - fn get_raw_mempool_verbose( - &self, - ) -> Result> { - self.call("getrawmempool", &[into_json(true)?]) - } - - /// Get mempool data for given transaction - fn get_mempool_entry(&self, txid: &bitcoin::Txid) -> Result { - self.call("getmempoolentry", &[into_json(txid)?]) - } - - /// Get information about all known tips in the block tree, including the - /// main chain as well as stale branches. - fn get_chain_tips(&self) -> Result { - self.call("getchaintips", &[]) - } - - fn send_to_address( - &self, - address: &Address, - amount: Amount, - comment: Option<&str>, - comment_to: Option<&str>, - subtract_fee: Option, - replaceable: Option, - confirmation_target: Option, - estimate_mode: Option, - ) -> Result { - let mut args = [ - address.to_string().into(), - into_json(amount.to_btc())?, - opt_into_json(comment)?, - opt_into_json(comment_to)?, - opt_into_json(subtract_fee)?, - opt_into_json(replaceable)?, - opt_into_json(confirmation_target)?, - opt_into_json(estimate_mode)?, - ]; - self.call( - "sendtoaddress", - handle_defaults( - &mut args, - &["".into(), "".into(), false.into(), false.into(), 6.into(), null()], - ), - ) - } - - /// Attempts to add a node to the addnode list. - /// Nodes added using addnode (or -connect) are protected from DoS disconnection and are not required to be full nodes/support SegWit as other outbound peers are (though such peers will not be synced from). - fn add_node(&self, addr: &str) -> Result<()> { - self.call("addnode", &[into_json(&addr)?, into_json("add")?]) - } - - /// Attempts to remove a node from the addnode list. - fn remove_node(&self, addr: &str) -> Result<()> { - self.call("addnode", &[into_json(&addr)?, into_json("remove")?]) - } - - /// Attempts to connect to a node without permanently adding it to the addnode list. - fn onetry_node(&self, addr: &str) -> Result<()> { - self.call("addnode", &[into_json(&addr)?, into_json("onetry")?]) - } - - /// Immediately disconnects from the specified peer node. - fn disconnect_node(&self, addr: &str) -> Result<()> { - self.call("disconnectnode", &[into_json(&addr)?]) - } - - fn disconnect_node_by_id(&self, node_id: u32) -> Result<()> { - self.call("disconnectnode", &[into_json("")?, into_json(node_id)?]) - } - - /// Returns information about the given added node, or all added nodes (note that onetry addnodes are not listed here) - fn get_added_node_info(&self, node: Option<&str>) -> Result> { - if let Some(addr) = node { - self.call("getaddednodeinfo", &[into_json(&addr)?]) - } else { - self.call("getaddednodeinfo", &[]) - } - } - - /// Return known addresses which can potentially be used to find new nodes in the network - fn get_node_addresses( - &self, - count: Option, - ) -> Result> { - let cnt = count.unwrap_or(1); - self.call("getnodeaddresses", &[into_json(&cnt)?]) - } - - /// List all banned IPs/Subnets. - fn list_banned(&self) -> Result> { - self.call("listbanned", &[]) - } - - /// Clear all banned IPs. - fn clear_banned(&self) -> Result<()> { - self.call("clearbanned", &[]) - } - - /// Attempts to add an IP/Subnet to the banned list. - fn add_ban(&self, subnet: &str, bantime: u64, absolute: bool) -> Result<()> { - self.call( - "setban", - &[into_json(&subnet)?, into_json("add")?, into_json(&bantime)?, into_json(&absolute)?], - ) - } - - /// Attempts to remove an IP/Subnet from the banned list. - fn remove_ban(&self, subnet: &str) -> Result<()> { - self.call("setban", &[into_json(&subnet)?, into_json("remove")?]) - } - - /// Disable/enable all p2p network activity. - fn set_network_active(&self, state: bool) -> Result { - self.call("setnetworkactive", &[into_json(&state)?]) - } - - /// Returns data about each connected network node as an array of - /// [`PeerInfo`][] - /// - /// [`PeerInfo`]: net/struct.PeerInfo.html - fn get_peer_info(&self) -> Result> { - self.call("getpeerinfo", &[]) - } - - /// Requests that a ping be sent to all other nodes, to measure ping - /// time. - /// - /// Results provided in `getpeerinfo`, `pingtime` and `pingwait` fields - /// are decimal seconds. - /// - /// Ping command is handled in queue with all other commands, so it - /// measures processing backlog, not just network ping. - fn ping(&self) -> Result<()> { - self.call("ping", &[]) - } - - fn send_raw_transaction(&self, tx: R) -> Result { - self.call("sendrawtransaction", &[tx.raw_hex().into()]) - } - - fn estimate_smart_fee( - &self, - conf_target: u16, - estimate_mode: Option, - ) -> Result { - let mut args = [into_json(conf_target)?, opt_into_json(estimate_mode)?]; - self.call("estimatesmartfee", handle_defaults(&mut args, &[null()])) - } - - /// Waits for a specific new block and returns useful info about it. - /// Returns the current block on timeout or exit. - /// - /// # Arguments - /// - /// 1. `timeout`: Time in milliseconds to wait for a response. 0 - /// indicates no timeout. - fn wait_for_new_block(&self, timeout: u64) -> Result { - self.call("waitfornewblock", &[into_json(timeout)?]) - } - - /// Waits for a specific new block and returns useful info about it. - /// Returns the current block on timeout or exit. - /// - /// # Arguments - /// - /// 1. `blockhash`: Block hash to wait for. - /// 2. `timeout`: Time in milliseconds to wait for a response. 0 - /// indicates no timeout. - fn wait_for_block( - &self, - blockhash: &bitcoin::BlockHash, - timeout: u64, - ) -> Result { - let args = [into_json(blockhash)?, into_json(timeout)?]; - self.call("waitforblock", &args) - } - - fn wallet_create_funded_psbt( - &self, - inputs: &[json::CreateRawTransactionInput], - outputs: &HashMap, - locktime: Option, - options: Option, - bip32derivs: Option, - ) -> Result { - let outputs_converted = serde_json::Map::from_iter( - outputs.iter().map(|(k, v)| (k.clone(), serde_json::Value::from(v.to_btc()))), - ); - let mut args = [ - into_json(inputs)?, - into_json(outputs_converted)?, - opt_into_json(locktime)?, - opt_into_json(options)?, - opt_into_json(bip32derivs)?, - ]; - self.call( - "walletcreatefundedpsbt", - handle_defaults(&mut args, &[0.into(), serde_json::Map::new().into(), false.into()]), - ) - } - - fn wallet_process_psbt( - &self, - psbt: &str, - sign: Option, - sighash_type: Option, - bip32derivs: Option, - ) -> Result { - let mut args = [ - into_json(psbt)?, - opt_into_json(sign)?, - opt_into_json(sighash_type)?, - opt_into_json(bip32derivs)?, - ]; - let defaults = [ - true.into(), - into_json(json::SigHashType::from(bitcoin::sighash::EcdsaSighashType::All))?, - true.into(), - ]; - self.call("walletprocesspsbt", handle_defaults(&mut args, &defaults)) - } - - fn get_descriptor_info(&self, desc: &str) -> Result { - self.call("getdescriptorinfo", &[desc.to_string().into()]) - } - - fn join_psbt(&self, psbts: &[String]) -> Result { - self.call("joinpsbts", &[into_json(psbts)?]) - } - - fn combine_psbt(&self, psbts: &[String]) -> Result { - self.call("combinepsbt", &[into_json(psbts)?]) - } - - fn combine_raw_transaction(&self, hex_strings: &[String]) -> Result { - self.call("combinerawtransaction", &[into_json(hex_strings)?]) - } - - fn finalize_psbt(&self, psbt: &str, extract: Option) -> Result { - let mut args = [into_json(psbt)?, opt_into_json(extract)?]; - self.call("finalizepsbt", handle_defaults(&mut args, &[true.into()])) - } - - fn derive_addresses(&self, descriptor: &str, range: Option<[u32; 2]>) -> Result>> { - let mut args = [into_json(descriptor)?, opt_into_json(range)?]; - self.call("deriveaddresses", handle_defaults(&mut args, &[null()])) - } - - fn rescan_blockchain( - &self, - start_from: Option, - stop_height: Option, - ) -> Result<(usize, Option)> { - let mut args = [opt_into_json(start_from)?, opt_into_json(stop_height)?]; - - #[derive(Deserialize)] - struct Response { - pub start_height: usize, - pub stop_height: Option, - } - let res: Response = - self.call("rescanblockchain", handle_defaults(&mut args, &[0.into(), null()]))?; - Ok((res.start_height, res.stop_height)) - } - - /// Returns statistics about the unspent transaction output set. - /// Note this call may take some time if you are not using coinstatsindex. - fn get_tx_out_set_info( - &self, - hash_type: Option, - hash_or_height: Option, - use_index: Option, - ) -> Result { - let mut args = - [opt_into_json(hash_type)?, opt_into_json(hash_or_height)?, opt_into_json(use_index)?]; - self.call("gettxoutsetinfo", handle_defaults(&mut args, &[null(), null(), null()])) - } - - /// Returns information about network traffic, including bytes in, bytes out, - /// and current time. - fn get_net_totals(&self) -> Result { - self.call("getnettotals", &[]) - } - - /// Returns the estimated network hashes per second based on the last n blocks. - fn get_network_hash_ps(&self, nblocks: Option, height: Option) -> Result { - let mut args = [opt_into_json(nblocks)?, opt_into_json(height)?]; - self.call("getnetworkhashps", handle_defaults(&mut args, &[null(), null()])) - } - - /// Returns the total uptime of the server in seconds - fn uptime(&self) -> Result { - self.call("uptime", &[]) - } - - /// Submit a block - fn submit_block(&self, block: &bitcoin::Block) -> Result<()> { - let block_hex: String = bitcoin::consensus::encode::serialize_hex(block); - self.submit_block_hex(&block_hex) - } - - /// Submit a raw block - fn submit_block_bytes(&self, block_bytes: &[u8]) -> Result<()> { - let block_hex: String = block_bytes.to_lower_hex_string(); - self.submit_block_hex(&block_hex) - } - - /// Submit a block as a hex string - fn submit_block_hex(&self, block_hex: &str) -> Result<()> { - match self.call("submitblock", &[into_json(&block_hex)?]) { - Ok(serde_json::Value::Null) => Ok(()), - Ok(res) => Err(Error::ReturnedError(res.to_string())), - Err(err) => Err(err.into()), - } - } - - fn scan_tx_out_set_blocking( - &self, - descriptors: &[json::ScanTxOutRequest], - ) -> Result { - self.call("scantxoutset", &["start".into(), into_json(descriptors)?]) - } -} - /// Client implements a JSON-RPC client for the Bitcoin Core daemon or compatible APIs. -pub struct Client { - client: jsonrpc::client::Client, +pub struct Client { + pub(crate) client: jsonrpc::Client, + pub(crate) version: atomic::AtomicUsize, } -impl fmt::Debug for Client { +impl fmt::Debug for Client { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "bitcoincore_rpc::Client({:?})", self.client) } } -impl Client { - /// Creates a client to a bitcoind JSON-RPC server. - /// - /// Can only return [Err] when using cookie authentication. - pub fn new(url: &str, auth: Auth) -> Result { - let (user, pass) = auth.get_user_pass()?; - jsonrpc::client::Client::simple_http(url, user, pass) - .map(|client| Client { - client, - }) - .map_err(|e| super::error::Error::JsonRpc(e.into())) - } - +impl Client { /// Create a new Client using the given [jsonrpc::Client]. - pub fn from_jsonrpc(client: jsonrpc::client::Client) -> Client { + pub fn from_jsonrpc(client: jsonrpc::Client) -> Client { Client { client, + version: atomic::AtomicUsize::new(0), } } /// Get the underlying JSONRPC client. - pub fn get_jsonrpc_client(&self) -> &jsonrpc::client::Client { + pub fn jsonrpc_client(&self) -> &jsonrpc::Client { &self.client } } -impl RpcApi for Client { - /// Call an `cmd` rpc with given `args` list - fn call serde::de::Deserialize<'a>>( - &self, - cmd: &str, - args: &[serde_json::Value], - ) -> Result { - let raw_args: Vec<_> = args - .iter() - .map(|a| { - let json_string = serde_json::to_string(a)?; - serde_json::value::RawValue::from_string(json_string) // we can't use to_raw_value here due to compat with Rust 1.29 +impl Client { + /// Creates a client to a bitcoind JSON-RPC server. + /// + /// Can only return [Err] when using cookie authentication. + pub fn with_simple_http(url: &str, auth: Auth) -> Result { + let (user, pass) = auth.get_user_pass()?; + jsonrpc::Client::with_simple_http(url, user, pass) + .map(|client| Client { + client: client, + version: atomic::AtomicUsize::new(0), }) - .map(|a| a.map_err(|e| Error::Json(e))) - .collect::>>()?; - let req = self.client.build_request(&cmd, &raw_args); - if log_enabled!(Debug) { - debug!(target: "bitcoincore_rpc", "JSON-RPC request: {} {}", cmd, serde_json::Value::from(args)); - } - - let resp = self.client.send_request(req).map_err(Error::from); - log_response(cmd, &resp); - Ok(resp?.result()?) + .map_err(|e| super::error::Error::JsonRpc(e.into())) } } -fn log_response(cmd: &str, resp: &Result) { +fn log_response(cmd: &str, resp: &Result) { if log_enabled!(Warn) || log_enabled!(Debug) || log_enabled!(Trace) { match resp { Err(ref e) => { @@ -1358,75 +120,19 @@ fn log_response(cmd: &str, resp: &Result) { #[cfg(test)] mod tests { use super::*; + use crate::SyncClient; use crate::bitcoin; - use serde_json; + use crate::bitcoin::hashes::hex::FromHex; #[test] fn test_raw_tx() { use crate::bitcoin::consensus::encode; - let client = Client::new("http://localhost/".into(), Auth::None).unwrap(); + let client = Client::with_simple_http("http://localhost/".into(), Auth::None).unwrap(); let tx: bitcoin::Transaction = encode::deserialize(&Vec::::from_hex("0200000001586bd02815cf5faabfec986a4e50d25dbee089bd2758621e61c5fab06c334af0000000006b483045022100e85425f6d7c589972ee061413bcf08dc8c8e589ce37b217535a42af924f0e4d602205c9ba9cb14ef15513c9d946fa1c4b797883e748e8c32171bdf6166583946e35c012103dae30a4d7870cd87b45dd53e6012f71318fdd059c1c2623b8cc73f8af287bb2dfeffffff021dc4260c010000001976a914f602e88b2b5901d8aab15ebe4a97cf92ec6e03b388ac00e1f505000000001976a914687ffeffe8cf4e4c038da46a9b1d37db385a472d88acfd211500").unwrap()).unwrap(); assert!(client.send_raw_transaction(&tx).is_err()); assert!(client.send_raw_transaction(&encode::serialize(&tx)).is_err()); assert!(client.send_raw_transaction("deadbeef").is_err()); - assert!(client.send_raw_transaction("deadbeef".to_owned()).is_err()); - } - - fn test_handle_defaults_inner() -> Result<()> { - { - let mut args = [into_json(0)?, null(), null()]; - let defaults = [into_json(1)?, into_json(2)?]; - let res = [into_json(0)?]; - assert_eq!(handle_defaults(&mut args, &defaults), &res); - } - { - let mut args = [into_json(0)?, into_json(1)?, null()]; - let defaults = [into_json(2)?]; - let res = [into_json(0)?, into_json(1)?]; - assert_eq!(handle_defaults(&mut args, &defaults), &res); - } - { - let mut args = [into_json(0)?, null(), into_json(5)?]; - let defaults = [into_json(2)?, into_json(3)?]; - let res = [into_json(0)?, into_json(2)?, into_json(5)?]; - assert_eq!(handle_defaults(&mut args, &defaults), &res); - } - { - let mut args = [into_json(0)?, null(), into_json(5)?, null()]; - let defaults = [into_json(2)?, into_json(3)?, into_json(4)?]; - let res = [into_json(0)?, into_json(2)?, into_json(5)?]; - assert_eq!(handle_defaults(&mut args, &defaults), &res); - } - { - let mut args = [null(), null()]; - let defaults = [into_json(2)?, into_json(3)?]; - let res: [serde_json::Value; 0] = []; - assert_eq!(handle_defaults(&mut args, &defaults), &res); - } - { - let mut args = [null(), into_json(1)?]; - let defaults = []; - let res = [null(), into_json(1)?]; - assert_eq!(handle_defaults(&mut args, &defaults), &res); - } - { - let mut args = []; - let defaults = []; - let res: [serde_json::Value; 0] = []; - assert_eq!(handle_defaults(&mut args, &defaults), &res); - } - { - let mut args = [into_json(0)?]; - let defaults = [into_json(2)?]; - let res = [into_json(0)?]; - assert_eq!(handle_defaults(&mut args, &defaults), &res); - } - Ok(()) - } - - #[test] - fn test_handle_defaults() { - test_handle_defaults_inner().unwrap(); + assert!(client.send_raw_transaction(&"deadbeef".to_owned()).is_err()); } } diff --git a/client/src/error.rs b/client/src/error.rs index 9ac04fea..26a060d7 100644 --- a/client/src/error.rs +++ b/client/src/error.rs @@ -10,11 +10,11 @@ use std::{error, fmt, io}; +use jsonrpc; +use serde_json; use crate::bitcoin; use crate::bitcoin::hashes::hex; use crate::bitcoin::secp256k1; -use jsonrpc; -use serde_json; /// The error type for errors produced in this library. #[derive(Debug)] @@ -27,6 +27,7 @@ pub enum Error { Io(io::Error), InvalidAmount(bitcoin::amount::ParseAmountError), InvalidCookieFile, + InvalidArguments(String), /// The JSON result had an unexpected structure. UnexpectedStructure, /// The daemon returned an error string. @@ -86,6 +87,7 @@ impl fmt::Display for Error { Error::Io(ref e) => write!(f, "I/O error: {}", e), Error::InvalidAmount(ref e) => write!(f, "invalid amount: {}", e), Error::InvalidCookieFile => write!(f, "invalid cookie file"), + Error::InvalidArguments(ref e) => write!(f, "invalid arguments: {}", e), Error::UnexpectedStructure => write!(f, "the JSON result had an unexpected structure"), Error::ReturnedError(ref s) => write!(f, "the daemon returned an error string: {}", s), } diff --git a/client/src/lib.rs b/client/src/lib.rs index 4cc5f0d2..1f46cfd1 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -18,34 +18,32 @@ #[macro_use] extern crate log; -#[allow(unused)] -#[macro_use] // `macro_use` is needed for v1.24.0 compilation. -extern crate serde; pub extern crate jsonrpc; pub extern crate bitcoincore_rpc_json; -pub use crate::json::bitcoin; pub use bitcoincore_rpc_json as json; -use json::bitcoin::consensus::{Decodable, ReadExt}; -use json::bitcoin::hashes::hex::HexIterator; + +pub use crate::json::bitcoin; mod client; +pub use client::*; + +mod params; +pub use params::*; + +mod sync_client; +pub use sync_client::SyncClient; + +mod async_client; +pub use async_client::AsyncClient; + mod error; -mod queryable; - -pub use crate::client::*; -pub use crate::error::Error; -pub use crate::queryable::*; - -fn deserialize_hex(hex: &str) -> Result { - let mut reader = HexIterator::new(&hex)?; - let object = Decodable::consensus_decode(&mut reader)?; - if reader.read_u8().is_ok() { - Err(Error::BitcoinSerialization(bitcoin::consensus::encode::Error::ParseFailed( - "data not consumed entirely when explicitly deserializing", - ))) - } else { - Ok(object) - } -} +pub use error::Error; + +pub mod requests; +pub mod serialize; + +/// Crate-specific Result type, shorthand for `std::result::Result` with our +/// crate-specific Error type; +pub type Result = std::result::Result; diff --git a/client/src/params.rs b/client/src/params.rs new file mode 100644 index 00000000..5fcdd0dc --- /dev/null +++ b/client/src/params.rs @@ -0,0 +1,475 @@ +//! Some types that are used are parameters to function calls. + +// DEVELOPER NOTES +// +// There are quite some subtleties here in this module. +// * It's best to always look around closely to other implementations and +// do as they do. +// +// * The `impl X for &T` impls are for params that are used in slices, +// so that you can both do `&[&my_tx]` but also `&my_txs`. +// * Params that include `str`, must have `+ ?Sized` in these &T trait impls. + +use std::fmt; + +use crate::bitcoin::hashes::sha256d; +use crate::bitcoin::{self, Address, Block, Transaction}; +use crate::bitcoin::psbt::PartiallySignedTransaction; +use crate::bitcoin::sighash::EcdsaSighashType; + +/// Outputs hex into an object implementing `fmt::Write`. +/// +/// This is usually more efficient than going through a `String` using [`ToHex`]. +// NB taken from bitcoin_hashes::hex +fn format_hex(data: &[u8], f: &mut fmt::Formatter) -> fmt::Result { + let prec = f.precision().unwrap_or(2 * data.len()); + let width = f.width().unwrap_or(2 * data.len()); + for _ in (2 * data.len())..width { + f.write_str("0")?; + } + for ch in data.iter().take(prec / 2) { + write!(f, "{:02x}", *ch)?; + } + if prec < 2 * data.len() && prec % 2 == 1 { + write!(f, "{:x}", data[prec / 2] / 16)?; + } + Ok(()) +} + +/// Marker trait for arguments that identify a block by +/// either its hash or block height. +/// +/// In the RPC documentation these are generally named hash_or_height. +pub trait BlockRef: serde::Serialize + Sync {} + +impl BlockRef for bitcoin::BlockHash {} +impl BlockRef for sha256d::Hash {} +impl BlockRef for String {} +impl<'a> BlockRef for &'a String {} +impl<'a> BlockRef for &'a str {} +impl BlockRef for usize {} +impl BlockRef for u64 {} +impl BlockRef for u32 {} +impl BlockRef for u16 {} +impl BlockRef for u8 {} +impl BlockRef for isize {} +impl BlockRef for i64 {} +impl BlockRef for i32 {} +impl BlockRef for i16 {} +impl BlockRef for i8 {} + +/// Trait for parameter types that are serializable as a string. +pub trait StringParam { + fn write_string(&self, f: &mut fmt::Formatter) -> fmt::Result; +} + +impl StringParam for str { + fn write_string(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + +impl StringParam for String { + fn write_string(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + +impl StringParam for &T { + fn write_string(&self, f: &mut fmt::Formatter) -> fmt::Result { + StringParam::write_string(*self, f) + } +} + +/// General trait for any object that can be accepted as an argument +/// that should be serialized as hex. +pub trait HexParam { + fn write_hex(&self, f: &mut fmt::Formatter) -> fmt::Result; +} + +impl HexParam for str { + fn write_hex(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + +impl HexParam for String { + fn write_hex(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + +impl HexParam for [u8] { + fn write_hex(&self, f: &mut fmt::Formatter) -> fmt::Result { + format_hex(self, f) + } +} + +impl HexParam for Vec { + fn write_hex(&self, f: &mut fmt::Formatter) -> fmt::Result { + format_hex(self, f) + } +} + +impl HexParam for Transaction { + fn write_hex(&self, f: &mut fmt::Formatter) -> fmt::Result { + let bytes = bitcoin::consensus::encode::serialize(self); + HexParam::write_hex(&bytes[..], f) + } +} + +impl HexParam for Block { + fn write_hex(&self, f: &mut fmt::Formatter) -> fmt::Result { + let bytes = bitcoin::consensus::encode::serialize(self); + HexParam::write_hex(&bytes[..], f) + } +} + +impl HexParam for &T { + fn write_hex(&self, f: &mut fmt::Formatter) -> fmt::Result { + HexParam::write_hex(*self, f) + } +} + +/// A marker trait for parameters that represent transactions. +pub trait TxParam: HexParam + Sync {} + +impl TxParam for str {} +impl TxParam for String {} +impl TxParam for [u8] {} +impl TxParam for Vec {} +impl TxParam for Transaction {} +impl TxParam for &T {} + +/// A marker trait for parameters that represent blocks. +pub trait BlockParam: HexParam + Sync {} + +impl BlockParam for str {} +impl BlockParam for String {} +impl BlockParam for [u8] {} +impl BlockParam for Vec {} +impl BlockParam for Block {} + +/// A marker trait for parameters that represent Bitcoin addresses. +pub trait AddressParam: StringParam + Sync {} + +impl StringParam for Address { + fn write_string(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self) + } +} +impl StringParam for Address { + fn write_string(&self, f: &mut fmt::Formatter) -> fmt::Result { + //TODO(stevenroose) await landing of fmt for all addresses + write!(f, "{}", self.clone().assume_checked()) + } +} + +impl AddressParam for Address {} +impl AddressParam for Address {} +impl AddressParam for str {} +impl AddressParam for String {} +impl AddressParam for &T {} + +/// A marker trait for parameters that represent sighash flags. +pub trait SighashParam: StringParam + Sync {} + +impl StringParam for EcdsaSighashType { + fn write_string(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match self { + EcdsaSighashType::All => "ALL", + EcdsaSighashType::None => "NONE", + EcdsaSighashType::Single => "SINGLE", + EcdsaSighashType::AllPlusAnyoneCanPay => "ALL|ANYONECANPAY", + EcdsaSighashType::NonePlusAnyoneCanPay => "NONE|ANYONECANPAY", + EcdsaSighashType::SinglePlusAnyoneCanPay => "SINGLE|ANYONECANPAY", + }) + } +} +impl SighashParam for EcdsaSighashType {} +impl SighashParam for &T {} + +/// A marker trait for parameters that represent partially signed Bitcoin transactions. +pub trait PsbtParam: StringParam + Sync {} + +impl StringParam for PartiallySignedTransaction { + fn write_string(&self, f: &mut fmt::Formatter) -> fmt::Result { + // ideally we won't rely on fmt::Display for this, but have an explicit base64 getter + fmt::Display::fmt(self, f) + } +} + +impl PsbtParam for PartiallySignedTransaction {} +impl PsbtParam for str {} +impl PsbtParam for String {} +impl PsbtParam for &T {} + +#[cfg(test)] +mod test { + use super::*; + + use std::str::FromStr; + + use async_trait::async_trait; + use bitcoin::BlockHash; + use bitcoin::hash_types::TxMerkleNode; + use bitcoin::hashes::Hash; + use bitcoin::blockdata::locktime::absolute::LockTime; + use jsonrpc::Request; + + use crate::{AsyncClient, Error, Result, SyncClient}; + + struct SyncDummy; + + #[async_trait(?Send)] + impl SyncClient for SyncDummy { + fn handle_request<'r, T>(&self, _: Request<'r, T>) -> Result { + Err(Error::ReturnedError("dummy".into())) + } + + fn version(&self) -> Result { Ok(0) } + fn refresh_version(&self) -> Result<()> { Ok(()) } + } + + struct AsyncDummy; + + #[async_trait(?Send)] + impl AsyncClient for AsyncDummy { + async fn handle_request<'r, T>(&self, _: Request<'r, T>) -> Result { + Err(Error::ReturnedError("dummy".into())) + } + + async fn version(&self) -> Result { Ok(0) } + async fn refresh_version(&self) -> Result<()> { Ok(()) } + } + + fn new_tx() -> Transaction { + Transaction { + version: 0, + lock_time: LockTime::ZERO, + input: vec![], + output: vec![], + } + } + + fn new_psbt() -> PartiallySignedTransaction { + PartiallySignedTransaction { + unsigned_tx: new_tx(), + version: 0, + xpub: Default::default(), + proprietary: Default::default(), + unknown: Default::default(), + inputs: vec![], + outputs: vec![], + } + } + + fn new_block() -> Block { + Block { + header: bitcoin::block::Header { + version: Default::default(), + prev_blockhash: BlockHash::all_zeros(), + merkle_root: TxMerkleNode::all_zeros(), + time: 0, + bits: Default::default(), + nonce: 0, + }, + txdata: vec![], + } + } + + // What follows are some tests that simply check if the compiler allows + // all the different method argument passings that we intend. + // These tests only test a single method for each case, so they rely on + // all similar methods in the APIs to have similar semantics. + // We test both sync and async because async often has more subtle + // semantics. + + #[test] + fn test_tx_param_sync() { + let c = SyncDummy; + + let bytes = vec![1u8, 2]; + let string = "deadbeef".to_owned(); + let strr = "deadbeef"; + let tx = new_tx(); + let txs = vec![ new_tx(), new_tx() ]; + + let _ = c.decode_raw_transaction("deadbeef", None); + let _ = c.decode_raw_transaction(strr, None); + let _ = c.decode_raw_transaction(&string, None); + let _ = c.decode_raw_transaction(&bytes, None); + let _ = c.decode_raw_transaction(&bytes[..], None); + let _ = c.decode_raw_transaction(&tx, None); + // slice + let _ = c.test_mempool_accept(&["deadbeef"], None); + let _ = c.test_mempool_accept(&[&string], None); + let _ = c.test_mempool_accept(&[string], None); + let _ = c.test_mempool_accept(&[&bytes], None); + let _ = c.test_mempool_accept(&[&bytes[..]], None); + let _ = c.test_mempool_accept(&[bytes], None); + let _ = c.test_mempool_accept(&[&tx], None); + let _ = c.test_mempool_accept(&[tx], None); + let _ = c.test_mempool_accept(&txs, None); + let _ = c.test_mempool_accept(&txs[..], None); + } + + #[test] + fn test_tx_param_async() { + let c = AsyncDummy; + + let bytes = vec![1u8, 2]; + let string = "deadbeef".to_owned(); + let strr = "deadbeef"; + let tx = new_tx(); + let txs = vec![ new_tx(), new_tx() ]; + + let _ = c.decode_raw_transaction("deadbeef", None); + let _ = c.decode_raw_transaction(strr, None); + let _ = c.decode_raw_transaction(&string, None); + let _ = c.decode_raw_transaction(&bytes, None); + let _ = c.decode_raw_transaction(&bytes[..], None); + let _ = c.decode_raw_transaction(&tx, None); + // slice + let _ = c.test_mempool_accept(&["deadbeef"], None); + let _ = c.test_mempool_accept(&[&string], None); + let _ = c.test_mempool_accept(&[string], None); + let _ = c.test_mempool_accept(&[&bytes], None); + let _ = c.test_mempool_accept(&[&bytes[..]], None); + let _ = c.test_mempool_accept(&[bytes], None); + let _ = c.test_mempool_accept(&[&tx], None); + let _ = c.test_mempool_accept(&[tx], None); + let _ = c.test_mempool_accept(&txs, None); + let _ = c.test_mempool_accept(&txs[..], None); + } + + #[test] + fn test_block_param_sync() { + let c = SyncDummy; + + let bytes = vec![1u8, 2]; + let string = "deadbeef".to_owned(); + let strr = "deadbeef"; + let block = new_block(); + + let _ = c.submit_block("deadbeef"); + let _ = c.submit_block(strr); + let _ = c.submit_block(&string); + let _ = c.submit_block(&bytes); + let _ = c.submit_block(&bytes[..]); + let _ = c.submit_block(&block); + } + + #[test] + fn test_block_param_async() { + let c = AsyncDummy; + + let bytes = vec![1u8, 2]; + let string = "deadbeef".to_owned(); + let strr = "deadbeef"; + let block = new_block(); + + let _ = c.submit_block("deadbeef"); + let _ = c.submit_block(strr); + let _ = c.submit_block(&string); + let _ = c.submit_block(&bytes); + let _ = c.submit_block(&bytes[..]); + let _ = c.submit_block(&block); + } + + #[test] + fn test_address_param_sync() { + let c = SyncDummy; + + let string = "deadbeef".to_owned(); + let strr = "deadbeef"; + let addr = Address::from_str("1HYjUtfC5KGrB1QrDzbWXjam5dw1VofKf2").unwrap().assume_checked(); + let addr_uc = Address::from_str("1HYjUtfC5KGrB1QrDzbWXjam5dw1VofKf2").unwrap(); + let addrs = vec![addr.clone(), addr.clone()]; + + let _ = c.get_address_info("deadbeef"); + let _ = c.get_address_info(strr); + let _ = c.get_address_info(&string); + let _ = c.get_address_info(&addr); + let _ = c.get_address_info(&addr_uc); + // slice + let _ = c.list_unspent(None, None, Some(&["deadbeef"]), None, None); + let _ = c.list_unspent(None, None, Some(&[&string]), None, None); + let _ = c.list_unspent(None, None, Some(&[string]), None, None); + let _ = c.list_unspent(None, None, Some(&[&addr]), None, None); + let _ = c.list_unspent(None, None, Some(&[addr]), None, None); + let _ = c.list_unspent(None, None, Some(&addrs), None, None); + let _ = c.list_unspent(None, None, Some(&addrs[..]), None, None); + } + + #[test] + fn test_address_param_async() { + let c = AsyncDummy; + + let string = "deadbeef".to_owned(); + let strr = "deadbeef"; + let addr = Address::from_str("1HYjUtfC5KGrB1QrDzbWXjam5dw1VofKf2").unwrap().assume_checked(); + let addr_uc = Address::from_str("1HYjUtfC5KGrB1QrDzbWXjam5dw1VofKf2").unwrap(); + let addrs = vec![addr.clone(), addr.clone()]; + + let _ = c.get_address_info("deadbeef"); + let _ = c.get_address_info(strr); + let _ = c.get_address_info(&string); + let _ = c.get_address_info(&addr); + let _ = c.get_address_info(&addr_uc); + // slice + let _ = c.list_unspent(None, None, Some(&["deadbeef"]), None, None); + let _ = c.list_unspent(None, None, Some(&[&string]), None, None); + let _ = c.list_unspent(None, None, Some(&[string]), None, None); + let _ = c.list_unspent(None, None, Some(&[&addr]), None, None); + let _ = c.list_unspent(None, None, Some(&[addr]), None, None); + let _ = c.list_unspent(None, None, Some(&addrs), None, None); + let _ = c.list_unspent(None, None, Some(&addrs[..]), None, None); + } + + #[test] + fn test_psbt_param_sync() { + let c = SyncDummy; + + let string = "deadbeef".to_owned(); + let strr = "deadbeef"; + let psbt = new_psbt(); + let psbts = vec![ new_psbt(), new_psbt() ]; + + let _ = c.finalize_psbt("deadbeef", None); + let _ = c.finalize_psbt(strr, None); + let _ = c.finalize_psbt(&string, None); + let _ = c.finalize_psbt(&psbt, None); + // slice + let _ = c.join_psbts(&["deadbeef"]); + let _ = c.join_psbts(&[&string]); + let _ = c.join_psbts(&[string]); + let _ = c.join_psbts(&[&psbt]); + let _ = c.join_psbts(&[psbt]); + let _ = c.join_psbts(&psbts); + let _ = c.join_psbts(&psbts[..]); + } + + #[test] + fn test_psbt_param_async() { + let c = AsyncDummy; + + let string = "deadbeef".to_owned(); + let strr = "deadbeef"; + let psbt = new_psbt(); + let psbts = vec![ new_psbt(), new_psbt() ]; + + let _ = c.finalize_psbt("deadbeef", None); + let _ = c.finalize_psbt(strr, None); + let _ = c.finalize_psbt(&string, None); + let _ = c.finalize_psbt(&psbt, None); + // slice + let _ = c.join_psbts(&["deadbeef"]); + let _ = c.join_psbts(&[&string]); + let _ = c.join_psbts(&[string]); + let _ = c.join_psbts(&[&psbt]); + let _ = c.join_psbts(&[psbt]); + let _ = c.join_psbts(&psbts); + let _ = c.join_psbts(&psbts[..]); + } +} diff --git a/client/src/queryable.rs b/client/src/queryable.rs deleted file mode 100644 index 696a37d2..00000000 --- a/client/src/queryable.rs +++ /dev/null @@ -1,53 +0,0 @@ -// To the extent possible under law, the author(s) have dedicated all -// copyright and related and neighboring rights to this software to -// the public domain worldwide. This software is distributed without -// any warranty. -// -// You should have received a copy of the CC0 Public Domain Dedication -// along with this software. -// If not, see . -// - -use crate::bitcoin; -use serde_json; - -use crate::client::Result; -use crate::client::RpcApi; - -/// A type that can be queried from Bitcoin Core. -pub trait Queryable: Sized { - /// Type of the ID used to query the item. - type Id; - /// Query the item using `rpc` and convert to `Self`. - fn query(rpc: &C, id: &Self::Id) -> Result; -} - -impl Queryable for bitcoin::block::Block { - type Id = bitcoin::BlockHash; - - fn query(rpc: &C, id: &Self::Id) -> Result { - let rpc_name = "getblock"; - let hex: String = rpc.call(rpc_name, &[serde_json::to_value(id)?, 0.into()])?; - let bytes: Vec = bitcoin::hashes::hex::FromHex::from_hex(&hex)?; - Ok(bitcoin::consensus::encode::deserialize(&bytes)?) - } -} - -impl Queryable for bitcoin::transaction::Transaction { - type Id = bitcoin::Txid; - - fn query(rpc: &C, id: &Self::Id) -> Result { - let rpc_name = "getrawtransaction"; - let hex: String = rpc.call(rpc_name, &[serde_json::to_value(id)?])?; - let bytes: Vec = bitcoin::hashes::hex::FromHex::from_hex(&hex)?; - Ok(bitcoin::consensus::encode::deserialize(&bytes)?) - } -} - -impl Queryable for Option { - type Id = bitcoin::OutPoint; - - fn query(rpc: &C, id: &Self::Id) -> Result { - rpc.get_tx_out(&id.txid, id.vout, Some(true)) - } -} diff --git a/client/src/requests.rs b/client/src/requests.rs new file mode 100644 index 00000000..b640bac2 --- /dev/null +++ b/client/src/requests.rs @@ -0,0 +1,1879 @@ + +use std::str::FromStr; + +use jsonrpc::client::Request;//, Batch}; +use jsonrpc::client::{ConverterError, List, Param, Params}; +use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue; + +use crate::bitcoin::hashes::hex::{HexIterator, FromHex}; +use crate::bitcoin::secp256k1::ecdsa::Signature; +use crate::bitcoin::{ + self, Address, Amount, Block, PrivateKey, PublicKey, ScriptBuf, Transaction, +}; +use crate::bitcoin::block::Header as BlockHeader; +use crate::bitcoin::psbt::PartiallySignedTransaction; +type UncheckedAddress = Address; + +use crate::serialize::{ + HexListSerializeWrapper, HexSerializeWrapper, + OutPointListObjectSerializeWrapper, + StringListSerializeWrapper, StringSerializeWrapper, +}; +use crate::{json, AddressParam, BlockRef, BlockParam, Error, PsbtParam, SighashParam, TxParam}; + +fn converter_hex(raw: Box) -> Result +where + T: bitcoin::consensus::encode::Decodable, +{ + let hex: &str = serde_json::from_str(raw.get())?; + let mut bytes = HexIterator::new(hex)?; + Ok(T::consensus_decode(&mut bytes)?) +} + +fn converter_raw_hex(raw: Box) -> Result, ConverterError> { + let hex: &str = serde_json::from_str(raw.get())?; + Ok(Vec::::from_hex(&hex)?) +} + +pub(crate) fn converter_json(raw: Box) -> Result +where + T: serde::de::DeserializeOwned, +{ + Ok(serde_json::from_str(raw.get())?) +} + +fn converter_btc(raw: Box) -> Result { + let btc = serde_json::from_str::(raw.get())?; + Ok(Amount::from_btc(btc)?) +} + +fn converter_psbt(raw: Box) -> Result { + let b64 = serde_json::from_str::<&str>(raw.get())?; + Ok(PartiallySignedTransaction::from_str(b64)?) +} + +/// Converter for calls that expect an empty result and a string indicates an error. +fn converter_expect_null(raw: Box) -> Result<(), ConverterError> { + match serde_json::from_str::(raw.get()) { + Ok(serde_json::Value::Null) => Ok(()), + Ok(res) => Err(Error::ReturnedError(res.to_string()).into()), + Err(err) => Err(err.into()), + } +} + +// The following methods are shorthands to create [Param] objects. + +trait IntoJsonValue { + fn into_value(&self) -> serde_json::Value; +} + +macro_rules! into_json_from { + ($t:ty) => { + impl IntoJsonValue for $t { + fn into_value(&self) -> serde_json::Value { + serde_json::Value::from(*self) + } + } + }; +} +into_json_from!(bool); +into_json_from!(usize); +into_json_from!(u16); +into_json_from!(u32); +into_json_from!(u64); +into_json_from!(i32); +into_json_from!(i64); +into_json_from!(f64); + +/// Convert a basic type into a value parameter. +#[inline] +fn v(v: T) -> Param<'static> { + Param::Value(v.into_value()) +} + +/// Same as [v], but for options. +#[inline] +fn ov(o: Option) -> Option> { + o.map(v) +} + +/// Allocate the given variable on the heap into a boxed parameter. +#[inline] +fn b(v: T) -> Param<'static> where T: serde::Serialize + Sync + 'static { + Param::InBox(Box::new(v)) +} + +/// Same as [b] but for options. +#[allow(unused)] +#[inline] +fn ob(o: Option) -> Option> where T: serde::Serialize + Sync + 'static { + o.map(b) +} + +/// Convert a reference into a reference parameter. +#[inline] +fn r<'a, T>(p: &'a T) -> Param<'a> where T: serde::Serialize + Sync { + Param::ByRef(p) +} + +/// Same as for [r] but for options. +#[inline] +fn or<'a, T>(o: Option<&'a T>) -> Option> where T: serde::Serialize + Sync { + o.map(r) +} + +/// Create a boxed pre-allocated boxed parameter. +#[inline] +fn raw(v: T) -> Param<'static> where T: serde::Serialize { + let serialized = serde_json::to_string(&v).expect("serializer shoudln't fail"); + let raw = serde_json::value::RawValue::from_string(serialized).expect("valid utf8"); + Param::Raw(raw) +} + +/// Macro to generate a Params value. +/// +/// Params are passed as either a regular (key, value) tuple, +/// or as a ?-prefixed tuple where the value is an Option. +/// The optional parameters must always succeed the non-optional ones. +/// +/// Example: +/// params![ ("key", value), ?("key", Some(value)) ] +macro_rules! params { + () => {{ Params::ByName(List::Slice(&[])) }}; + // Special case for only optional ones. + ($(?($k2:expr, $v2:expr)),* $(,)?) => ( params![, $( ?($k2, $v2), )* ] ); + ($(($k1:expr, $v1:expr)),*, $(?($k2:expr, $v2:expr)),* $(,)?) => {{ + let mut n = 0; + $( let _ = $k1; n += 1; )* + // For optional params we could check if $v2 is some, + // but currently $v2 is an expression and we don't want to + // evaluate it twice. It's not too bad like this. + $( let _ = $k2; n += 1; )* + //TODO(stevenroose) use smallvec + let mut ret = Vec::<(&str, Param)>::with_capacity(n); + $( + ret.push(($k1, $v1)); + )* + $( + if let Some(v) = $v2 { + ret.push(($k2, v)); + } + )* + Params::from(ret) + }}; +} + +// ************* +// * MAIN PART * +// ************* + +#[inline(always)] +pub fn get_network_info() -> Request<'static, json::GetNetworkInfoResult> { + Request { + method: "getnetworkinfo".into(), + params: params![], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn version() -> Request<'static, usize> { + Request { + method: "getnetworkinfo".into(), + params: params![], + converter: &|raw| { + #[derive(Deserialize)] + struct Response { + pub version: usize, + } + let ret = serde_json::from_str::(raw.get())?; + Ok(ret.version) + }, + } +} + +#[inline(always)] +pub fn get_index_info() -> Request<'static, json::GetIndexInfoResult> { + Request { + method: "getindexinfo".into(), + params: params![], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn add_multisig_address<'r>( + nrequired: usize, + keys: &'r &'r [json::PubKeyOrAddress], + label: Option<&'r &'r str>, + address_type: Option<&'r json::AddressType>, +) -> Request<'r, json::AddMultiSigAddressResult> { + Request { + method: "addmultisigaddress".into(), + params: params![ + ("nrequired", v(nrequired)), + ("keys", r(keys)), + ?("label", or(label)), + ?("address_type", or(address_type)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn load_wallet<'r>(wallet: &'r &'r str) -> Request<'r, json::LoadWalletResult> { + Request { + method: "loadwallet".into(), + params: params![ + ("filename", r(wallet)), + //TODO(stevenroose) missing param + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn unload_wallet<'r>(wallet: Option<&'r &'r str>) -> Request<'r, json::UnloadWalletResult> { + Request { + method: "unloadwallet".into(), + params: params![ + ?("wallet_name", or(wallet)), + //TODO(stevenroose) missing param + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn create_wallet<'r>( + wallet: &'r &'r str, + disable_private_keys: Option, + blank: Option, + passphrase: Option<&'r &'r str>, + avoid_reuse: Option, +) -> Request<'r, json::LoadWalletResult> { + Request { + method: "createwallet".into(), + params: params![ + ("wallet_name", r(wallet)), + ?("disable_private_keys", ov(disable_private_keys)), + ?("blank", ov(blank)), + ?("passphrase", or(passphrase)), + ?("avoid_reuse", ov(avoid_reuse)), + //TODO(stevenroose) missing params + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn list_wallets() -> Request<'static, Vec> { + Request { + method: "listwallets".into(), + params: params![], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn list_wallet_dir() -> Request<'static, Vec> { + Request { + method: "listwalletdir".into(), + params: params![], + converter: &|raw| { + let ret: json::ListWalletDirResult = converter_json(raw)?; + Ok(ret.wallets.into_iter().map(|x| x.name).collect()) + }, + } +} + +#[inline(always)] +pub fn get_wallet_info() -> Request<'static, json::GetWalletInfoResult> { + Request { + method: "getwalletinfo".into(), + params: params![], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn backup_wallet<'r>(destination: &'r &'r str) -> Request<'r, ()> { + Request { + method: "backupwallet".into(), + params: params![ + ("destination", r(destination)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn dump_private_key<'r>( + address: &'r StringSerializeWrapper<'r, impl AddressParam + ?Sized>, +) -> Request<'r, PrivateKey> { + Request { + method: "dumpprivkey".into(), + params: params![ + ("address", r(address)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn encrypt_wallet<'r>(passphrase: &'r &'r str) -> Request<'r, String> { + Request { + method: "encryptwallet".into(), + params: params![ + ("passphrase", r(passphrase)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_difficulty() -> Request<'static, f64> { + Request { + method: "getdifficulty".into(), + params: params![], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_connection_count() -> Request<'static, usize> { + Request { + method: "getconnectioncount".into(), + params: params![], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_block<'r>(hash: &'r bitcoin::BlockHash) -> Request<'r, Block> { + Request { + method: "getblock".into(), + params: params![ + ("blockhash", r(hash)), + ("verbose", v(0)), + ], + converter: &|raw| converter_hex(raw), + } +} + +#[inline(always)] +pub fn get_block_hex<'r>(hash: &'r bitcoin::BlockHash) -> Request<'r, String> { + Request { + method: "getblock".into(), + params: params![ + ("blockhash", r(hash)), + ("verbose", v(0)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_block_info<'r>(hash: &'r bitcoin::BlockHash) -> Request<'r, json::GetBlockResult> { + Request { + method: "getblock".into(), + params: params![ + ("blockhash", r(hash)), + ("verbose", v(1)), + ], + converter: &|raw| converter_json(raw), + } +} +//TODO(stevenroose) add getblock_txs + +#[inline(always)] +pub fn get_block_header<'r>(hash: &'r bitcoin::BlockHash) -> Request<'r, BlockHeader> { + Request { + method: "getblockheader".into(), + params: params![ + ("blockhash", r(hash)), + ("verbose", v(false)), + ], + converter: &|raw| converter_hex(raw), + } +} + +#[inline(always)] +pub fn get_block_header_info<'r>( + hash: &'r bitcoin::BlockHash, +) -> Request<'r, json::GetBlockHeaderResult> { + Request { + method: "getblockheader".into(), + params: params![ + ("blockhash", r(hash)), + ("verbose", v(true)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_mining_info() -> Request<'static, json::GetMiningInfoResult> { + Request { + method: "getmininginfo".into(), + params: params![], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_block_template<'r>( + mode: &'r json::GetBlockTemplateModes, + rules: &'r &'r [json::GetBlockTemplateRules], + capabilities: &'r &'r [json::GetBlockTemplateCapabilities], +) -> Request<'r, json::GetBlockTemplateResult> { + #[derive(Serialize)] + struct Argument<'a> { + mode: &'a json::GetBlockTemplateModes, + rules: &'a [json::GetBlockTemplateRules], + capabilities: &'a [json::GetBlockTemplateCapabilities], + } + + Request { + method: "getblocktemplate".into(), + params: params![ + ("template_request", raw(Argument { + mode: mode, + rules: rules, + capabilities: capabilities, + })), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_blockchain_info() -> Request<'static, json::GetBlockchainInfoResult> { + Request { + method: "getblockchaininfo".into(), + params: params![], + converter: &|raw| { + use Error::UnexpectedStructure as err; + + let mut raw = serde_json::from_str::(raw.get())?; + + // The softfork fields are not backwards compatible: + // - 0.18.x returns a "softforks" array and a "bip9_softforks" map. + // - 0.19.x returns a "softforks" map. + let is_legacy = { + let obj = raw.as_object().ok_or(err)?; + obj.contains_key("bip9_softforks") + }; + + if is_legacy { + // First, remove both incompatible softfork fields. + // We need to scope the mutable ref here for v1.29 borrowck. + let (bip9_softforks, old_softforks) = { + let map = raw.as_object_mut().ok_or(err)?; + let bip9_softforks = map.remove("bip9_softforks").ok_or(err)?; + let old_softforks = map.remove("softforks").ok_or(err)?; + // Put back an empty "softforks" field. + map.insert("softforks".into(), serde_json::Map::new().into()); + (bip9_softforks, old_softforks) + }; + let mut ret = serde_json::from_value::(raw)?; + + // Then convert both softfork types and add them. + for sf in old_softforks.as_array().ok_or(err)?.iter() { + let json = sf.as_object().ok_or(err)?; + let id = json.get("id").ok_or(err)?.as_str().ok_or(err)?; + let reject = json.get("reject").ok_or(err)?.as_object().ok_or(err)?; + let active = reject.get("status").ok_or(err)?.as_bool().ok_or(err)?; + ret.softforks.insert( + id.into(), + json::Softfork { + type_: json::SoftforkType::Buried, + bip9: None, + height: None, + active: active, + }, + ); + } + for (id, sf) in bip9_softforks.as_object().ok_or(err)?.iter() { + #[derive(Deserialize)] + struct OldBip9SoftFork { + pub status: json::Bip9SoftforkStatus, + pub bit: Option, + #[serde(rename = "startTime")] + pub start_time: i64, + pub timeout: u64, + pub since: u32, + pub statistics: Option, + } + let sf: OldBip9SoftFork = serde_json::from_value(sf.clone())?; + ret.softforks.insert( + id.clone(), + json::Softfork { + type_: json::SoftforkType::Bip9, + bip9: Some(json::Bip9SoftforkInfo { + status: sf.status, + bit: sf.bit, + start_time: sf.start_time, + timeout: sf.timeout, + since: sf.since, + statistics: sf.statistics, + }), + height: None, + active: sf.status == json::Bip9SoftforkStatus::Active, + }, + ); + } + Ok(ret) + } else { + Ok(serde_json::from_value(raw)?) + } + }, + } +} + +#[inline(always)] +pub fn get_block_count() -> Request<'static, u64> { + Request { + method: "getblockcount".into(), + params: params![], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_best_block_hash() -> Request<'static, bitcoin::BlockHash> { + Request { + method: "getbestblockhash".into(), + params: params![], + converter: &|raw| converter_json(raw), + } +} + +/// Get block hash at a given height +#[inline(always)] +pub fn get_block_hash(height: u64) -> Request<'static, bitcoin::BlockHash> { + Request { + method: "getblockhash".into(), + params: params![ + ("height", v(height)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_block_stats<'r>( + block_ref: &'r impl BlockRef, +) -> Request<'r, json::GetBlockStatsResult> { + Request { + method: "getblockstats".into(), + params: params![ + ("hash_or_height", r(block_ref)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_block_stats_fields<'r>( + block_ref: &'r impl BlockRef, + fields: &'r &'r [json::BlockStatsFields], +) -> Request<'r, json::GetBlockStatsResultPartial> { + Request { + method: "getblockstats".into(), + params: params![ + ("hash_or_height", r(block_ref)), + ("stats", r(fields)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_raw_transaction<'r>( + txid: &'r bitcoin::Txid, + block_hash: Option<&'r bitcoin::BlockHash>, +) -> Request<'r, Transaction> { + Request { + method: "getrawtransaction".into(), + params: params![ + ("txid", r(txid)), + ("verbose", v(false)), + ?("blockhash", or(block_hash)), + ], + converter: &|raw| converter_hex(raw), + } +} + +#[inline(always)] +pub fn get_raw_transaction_hex<'r>( + txid: &'r bitcoin::Txid, + block_hash: Option<&'r bitcoin::BlockHash>, +) -> Request<'r, String> { + Request { + method: "getrawtransaction".into(), + params: params![ + ("txid", r(txid)), + ("verbose", v(false)), + ?("blockhash", or(block_hash)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_raw_transaction_info<'r>( + txid: &'r bitcoin::Txid, + block_hash: Option<&'r bitcoin::BlockHash>, +) -> Request<'r, json::GetRawTransactionResult> { + Request { + method: "getrawtransaction".into(), + params: params![ + ("txid", r(txid)), + ("verbose", v(true)), + ?("blockhash", or(block_hash)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_block_filter<'r>( + block_hash: &'r bitcoin::BlockHash, +) -> Request<'r, json::GetBlockFilterResult> { + Request { + method: "getblockfilter".into(), + params: params![ + ("blockhash", r(block_hash)), + // filtertype? + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_balance( + minconf: Option, + include_watchonly: Option, +) -> Request<'static, Amount> { + Request { + method: "getbalance".into(), + params: params![ + // we don't need to provide the dummy argument because we use named args + ?("minconf", ov(minconf)), + ?("include_watchonly", ov(include_watchonly)), + //TODO(stevenroose) missing avoid_reuse + ], + converter: &|raw| converter_btc(raw), + } +} + +#[inline(always)] +pub fn get_balances() -> Request<'static, json::GetBalancesResult> { + Request { + method: "getbalances".into(), + params: params![], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_received_by_address<'r>( + address: &'r StringSerializeWrapper<'r, impl AddressParam + ?Sized>, + minconf: Option, +) -> Request<'r, Amount> { + Request { + method: "getreceivedbyaddress".into(), + params: params![ + ("address", r(address)), + ?("minconf", ov(minconf)), + ], + converter: &|raw| converter_btc(raw), + } +} + +#[inline(always)] +pub fn get_transaction<'r>( + txid: &'r bitcoin::Txid, + include_watchonly: Option, + support_verbose: bool, +) -> Request<'r, json::GetTransactionResult> { + Request { + method: "gettransaction".into(), + params: if support_verbose { + params![ + ("txid", r(txid)), + ("verbose", v(false)), + ?("include_watchonly", ov(include_watchonly)), + ] + } else { + params![ + ("txid", r(txid)), + ?("include_watchonly", ov(include_watchonly)), + ] + }, + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn list_transactions<'r>( + label: Option<&'r &'r str>, + count: Option, + skip: Option, + include_watchonly: Option, +) -> Request<'r, Vec> { + Request { + method: "listtransactions".into(), + params: params![ + ?("label", or(label)), + ?("count", ov(count)), + ?("skip", ov(skip)), + ?("include_watchonly", ov(include_watchonly)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn list_since_block<'r>( + block_hash: Option<&'r bitcoin::BlockHash>, + target_confirmations: Option, + include_watchonly: Option, + include_removed: Option, +) -> Request<'r, json::ListSinceBlockResult> { + Request { + method: "listsinceblock".into(), + params: params![ + ?("blockhash", or(block_hash)), + ?("target_confirmations", ov(target_confirmations)), + ?("include_watchonly", ov(include_watchonly)), + ?("include_removed", ov(include_removed)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_tx_out<'r>( + txid: &'r bitcoin::Txid, + vout: u32, + include_mempool: Option, +) -> Request<'r, Option> { + Request { + method: "gettxout".into(), + params: params![ + ("txid", r(txid)), + ("n", v(vout)), + ?("include_mempool", ov(include_mempool)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_tx_out_proof<'r>( + txids: &'r &'r [bitcoin::Txid], + block_hash: Option<&'r bitcoin::BlockHash>, +) -> Request<'r, Vec> { + Request { + method: "gettxoutproof".into(), + params: params![ + ("txids", r(txids)), + ?("blockhash", or(block_hash)), + ], + converter: &|raw| converter_raw_hex(raw), + } +} + +#[inline(always)] +pub fn import_public_key<'r>( + public_key: &'r PublicKey, + label: Option<&'r &'r str>, + rescan: Option, +) -> Request<'r, ()> { + Request { + method: "importpubkey".into(), + params: params![ + ("pubkey", r(public_key)), + ?("label", or(label)), + ?("rescan", ov(rescan)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn import_private_key<'r>( + private_key: &'r PrivateKey, + label: Option<&'r &'r str>, + rescan: Option, +) -> Request<'r, ()> { + Request { + method: "importprivkey".into(), + params: params![ + ("privkey", r(private_key)), + ?("label", or(label)), + ?("rescan", ov(rescan)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn import_address<'r>( + address: &'r StringSerializeWrapper<'r, impl AddressParam + ?Sized>, + label: Option<&'r &'r str>, + rescan: Option, +) -> Request<'r, ()> { + Request { + method: "importaddress".into(), + params: params![ + ("address", r(address)), + ?("label", or(label)), + ?("rescan", ov(rescan)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn import_address_script<'r>( + script: &'r ScriptBuf, + label: Option<&'r &'r str>, + rescan: Option, + p2sh: Option, +) -> Request<'r, ()> { + Request { + method: "importaddress".into(), + params: params![ + ("address", r(script)), + ?("label", or(label)), + ?("rescan", ov(rescan)), + ?("p2sh", ov(p2sh)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn import_multi<'r>( + requests: &'r &'r [json::ImportMultiRequest], + options: Option<&'r json::ImportMultiOptions>, +) -> Request<'r, Vec> { + Request { + method: "importmulti".into(), + params: params![ + ("requests", r(requests)), + ?("options", or(options)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn import_descriptors<'r>( + requests: &'r &'r [json::ImportDescriptors], +) -> Request<'r, Vec> { + Request { + method: "importdescriptors".into(), + params: params![ + ("requests", r(requests)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn set_label<'r>( + address: &'r StringSerializeWrapper<'r, impl AddressParam + ?Sized>, + label: &'r &'r str, +) -> Request<'r, ()> { + Request { + method: "setlabel".into(), + params: params![ + ("address", r(address)), + ("label", r(label)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn key_pool_refill(new_size: Option) -> Request<'static, ()> { + Request { + method: "keypoolrefill".into(), + params: params![ + ?("newsize", ov(new_size)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn list_unspent<'r>( + minconf: Option, + maxconf: Option, + addresses: Option<&'r StringListSerializeWrapper<'r, impl AddressParam>>, + include_unsafe: Option, + query_options: Option<&'r json::ListUnspentQueryOptions>, +) -> Request<'r, Vec> { + Request { + method: "listunspent".into(), + params: params![ + ?("minconf", ov(minconf)), + ?("maxconf", ov(maxconf)), + ?("addresses", or(addresses)), + ?("include_unsafe", ov(include_unsafe)), + ?("query_options", or(query_options)), + ], + converter: &|raw| converter_json(raw), + } +} + +/// To unlock, use [unlock_unspent]. +#[inline(always)] +pub fn lock_unspent<'r>(outputs: &'r OutPointListObjectSerializeWrapper) -> Request<'r, bool> { + Request { + method: "lockunspent".into(), + params: params![ + ("unlock", v(false)), + ("transactions", r(outputs)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn unlock_unspent<'r>(outputs: &'r OutPointListObjectSerializeWrapper) -> Request<'r, bool> { + Request { + method: "lockunspent".into(), + params: params![ + ("unlock", v(true)), + ("transactions", r(outputs)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn unlock_unspent_all() -> Request<'static, bool> { + Request { + method: "lockunspent".into(), + params: params![ + ("unlock", v(true)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn list_received_by_address<'r>( + address_filter: Option<&'r StringSerializeWrapper<'r, impl AddressParam + ?Sized>>, + minconf: Option, + include_empty: Option, + include_watchonly: Option, +) -> Request<'r, Vec> { + Request { + method: "listreceivedbyaddress".into(), + params: params![ + ?("minconf", ov(minconf)), + ?("include_empty", ov(include_empty)), + ?("include_watchonly", ov(include_watchonly)), + ?("address_filter", or(address_filter)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn create_raw_transaction_hex<'r>( + inputs: &'r &'r [json::CreateRawTransactionInput], + outputs: &'r &'r [json::CreateRawTransactionOutput], + locktime: Option, + replaceable: Option, +) -> Request<'r, String> { + Request { + method: "createrawtransaction".into(), + params: params![ + ("inputs", r(inputs)), + ("outputs", r(outputs)), + ?("locktime", ov(locktime)), + ?("replaceable", ov(replaceable)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn create_raw_transaction<'r>( + inputs: &'r &'r [json::CreateRawTransactionInput], + outputs: &'r &'r [json::CreateRawTransactionOutput], + locktime: Option, + replaceable: Option, +) -> Request<'r, Transaction> { + Request { + method: "createrawtransaction".into(), + params: params![ + ("inputs", r(inputs)), + ("outputs", r(outputs)), + ?("locktime", ov(locktime)), + ?("replaceable", ov(replaceable)), + ], + converter: &|raw| converter_hex(raw), + } +} + +#[inline(always)] +pub fn decode_raw_transaction<'r>( + tx: &'r HexSerializeWrapper<'r, impl TxParam + ?Sized>, + is_witness: Option, +) -> Request<'r, json::DecodeRawTransactionResult> { + Request { + method: "decoderawtransaction".into(), + params: params![ + ("hexstring", r(tx)), + ?("iswitness", ov(is_witness)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn fund_raw_transaction<'r>( + tx: &'r HexSerializeWrapper<'r, impl TxParam + ?Sized>, + options: Option<&'r json::FundRawTransactionOptions>, + is_witness: Option, +) -> Request<'r, json::FundRawTransactionResult> { + Request { + method: "fundrawtransaction".into(), + params: params![ + ("hexstring", r(tx)), + ?("options", or(options)), + ?("iswitness", ov(is_witness)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn sign_raw_transaction_with_wallet<'r>( + tx: &'r HexSerializeWrapper<'r, impl TxParam + ?Sized>, + inputs: Option<&'r &'r [json::SignRawTransactionInput]>, + sighash_type: Option<&'r StringSerializeWrapper>, +) -> Request<'r, json::SignRawTransactionResult> { + Request { + method: "signrawtransactionwithwallet".into(), + params: params![ + ("hexstring", r(tx)), + ?("prevtxs", or(inputs)), + ?("sighashtype", or(sighash_type)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn sign_raw_transaction_with_key<'r>( + tx: &'r HexSerializeWrapper<'r, impl TxParam + ?Sized>, + privkeys: &'r &'r [PrivateKey], + inputs: Option<&'r &'r [json::SignRawTransactionInput]>, + sighash_type: Option<&'r StringSerializeWrapper>, +) -> Request<'r, json::SignRawTransactionResult> { + Request { + method: "signrawtransactionwithkey".into(), + params: params![ + ("hexstring", r(tx)), + ("privkeys", r(privkeys)), + ?("prevtxs", or(inputs)), + // avoid allocation + ?("sighashtype", or(sighash_type)), + ], + converter: &|raw| converter_json(raw), + } +} + +/// Fee rate per kvb. +#[inline(always)] +pub fn test_mempool_accept<'r>( + raw_txs: &'r HexListSerializeWrapper<'r, impl TxParam>, + max_fee_rate_btc_per_kvb: Option, +) -> Request<'r, Vec> { + Request { + method: "testmempoolaccept".into(), + params: params![ + ("rawtxs", r(raw_txs)), + ?("maxfeerate", ov(max_fee_rate_btc_per_kvb)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn stop() -> Request<'static, String> { + Request { + method: "stop".into(), + params: params![], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn verify_message<'r>( + address: &'r StringSerializeWrapper<'r, impl AddressParam + ?Sized>, + signature: &'r Signature, + message: &'r &'r str, +) -> Request<'r, bool> { + Request { + method: "verifymessage".into(), + params: params![ + ("address", r(address)), + ("signature", r(signature)), + ("message", r(message)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_new_address<'r>( + label: Option<&'r &'r str>, + address_type: Option<&'r json::AddressType>, +) -> Request<'r, UncheckedAddress> { + Request { + method: "getnewaddress".into(), + params: params![ + ?("label", or(label)), + ?("address_type", or(address_type)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_raw_change_address<'r>( + address_type: Option<&'r json::AddressType>, +) -> Request<'r, UncheckedAddress> { + Request { + method: "getrawchangeaddress".into(), + params: params![ + ?("address_type", or(address_type)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_address_info<'r>( + address: &'r StringSerializeWrapper<'r, impl AddressParam + ?Sized>, +) -> Request<'r, json::GetAddressInfoResult> { + Request { + method: "getaddressinfo".into(), + params: params![ + ("address", r(address)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn generate_to_address<'r>( + block_num: u64, + address: &'r StringSerializeWrapper<'r, impl AddressParam + ?Sized>, + max_tries: Option, +) -> Request<'r, Vec> { + Request { + method: "generatetoaddress".into(), + params: params![ + ("nblocks", v(block_num)), + ("address", r(address)), + ?("maxtries", ov(max_tries)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn generate( + block_num: u64, + max_tries: Option, +) -> Request<'static, Vec> { + // Special case for generate we use positional arguments. + // This is a deprecated call. + + let params = if let Some(max_tries) = max_tries { + vec![v(block_num), v(max_tries)] + } else { + vec![v(block_num)] + }; + Request { + method: "generate".into(), + params: Params::ByPosition(List::Boxed(params.into())), + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn invalidate_block<'r>(block_hash: &'r bitcoin::BlockHash) -> Request<'r, ()> { + Request { + method: "invalidateblock".into(), + params: params![ + ("blockhash", r(block_hash)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn reconsider_block<'r>(block_hash: &'r bitcoin::BlockHash) -> Request<'r, ()> { + Request { + method: "reconsiderblock".into(), + params: params![ + ("blockhash", r(block_hash)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_mempool_info() -> Request<'static, json::GetMempoolInfoResult> { + Request { + method: "getmempoolinfo".into(), + params: params![], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_raw_mempool() -> Request<'static, Vec> { + Request { + method: "getrawmempool".into(), + params: params![], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_mempool_entry<'r>(txid: &'r bitcoin::Txid) -> Request<'r, json::GetMempoolEntryResult> { + Request { + method: "getmempoolentry".into(), + params: params![ + ("txid", r(txid)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_chain_tips() -> Request<'static, json::GetChainTipsResult> { + Request { + method: "getchaintips".into(), + params: params![], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn send_to_address<'r>( + address: &'r StringSerializeWrapper<'r, impl AddressParam + ?Sized>, + amount: Amount, + comment: Option<&'r &'r str>, + comment_to: Option<&'r &'r str>, + subtract_fee: Option, + replaceable: Option, + confirmation_target: Option, + estimate_mode: Option<&'r json::EstimateMode>, + avoid_reuse: Option, + support_verbose: bool, + fee_rate_sat_per_vb: Option, +) -> Request<'r, bitcoin::Txid> { + Request { + method: "sendtoaddress".into(), + params: if support_verbose { + params![ + ("address", r(address)), + ("amount", v(amount.to_btc())), + ("verbose", v(false)), + ?("comment", or(comment)), + ?("comment_to", or(comment_to)), + ?("subtractfeefromamount", ov(subtract_fee)), + ?("replaceable", ov(replaceable)), + ?("conf_target", ov(confirmation_target)), + ?("estimate_mode", or(estimate_mode)), + ?("avoid_reuse", ov(avoid_reuse)), + ?("fee_rate", ov(fee_rate_sat_per_vb)), + ] + } else { + params![ + ("address", r(address)), + ("amount", v(amount.to_btc())), + ?("comment", or(comment)), + ?("comment_to", or(comment_to)), + ?("subtractfeefromamount", ov(subtract_fee)), + ?("replaceable", ov(replaceable)), + ?("conf_target", ov(confirmation_target)), + ?("estimate_mode", or(estimate_mode)), + ?("avoid_reuse", ov(avoid_reuse)), + // pre-verbose also doesn't support feerate, but let's let core + // produce an error in case the user thinks it's setting a fee + // rate instead of silently dropping it + ?("fee_rate", ov(fee_rate_sat_per_vb)), + ] + }, + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn add_node<'r>(addr: &'r &'r str) -> Request<'r, ()> { + Request { + method: "addnode".into(), + params: params![ + ("node", r(addr)), + ("command", r(&"add")), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn add_node_onetry<'r>(addr: &'r &'r str) -> Request<'r, ()> { + Request { + method: "addnode".into(), + params: params![ + ("node", r(addr)), + ("command", r(&"onetry")), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn remove_node<'r>(addr: &'r &'r str) -> Request<'r, ()> { + Request { + method: "addnode".into(), + params: params![ + ("node", r(addr)), + ("command", r(&"remove")), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn disconnect_node<'r>(addr: &'r &'r str) -> Request<'r, ()> { + Request { + method: "disconnectnode".into(), + params: params![ + ("address", r(addr)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn disconnect_node_by_id(node_id: u32) -> Request<'static, ()> { + Request { + method: "disconnectnode".into(), + params: params![ + ("nodeid", v(node_id)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_added_node_info<'r>( + node: &'r &'r str, +) -> Request<'r, Vec> { + Request { + method: "getaddednodeinfo".into(), + params: params![ + ("node", r(node)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_added_nodes_info() -> Request<'static, Vec> { + Request { + method: "getaddednodeinfo".into(), + params: params![], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_node_addresses( + count: Option, +) -> Request<'static, Vec> { + Request { + method: "getnodeaddresses".into(), + params: params![ + ?("count", ov(count)), + ], + converter: &|raw| converter_json(raw), + } +} + +/// List all banned IPs/Subnets. +#[inline(always)] +pub fn list_banned() -> Request<'static, Vec> { + Request { + method: "listbanned".into(), + params: params![], + converter: &|raw| converter_json(raw), + } +} + +/// Clear all banned IPs. +#[inline(always)] +pub fn clear_banned() -> Request<'static, ()> { + Request { + method: "clearbanned".into(), + params: params![], + converter: &|raw| converter_json(raw), + } +} + +/// Attempts to add an IP/Subnet to the banned list. +#[inline(always)] +pub fn add_ban<'r>(subnet: &'r &'r str, bantime: u64, absolute: bool) -> Request<'r, ()> { + Request { + method: "setban".into(), + params: params![ + ("subnet", r(subnet)), + ("command", r(&"add")), + ("bantime", v(bantime)), + ("absolute", v(absolute)), + ], + converter: &|raw| converter_json(raw), + } +} + +/// Attempts to remove an IP/Subnet from the banned list. +#[inline(always)] +pub fn remove_ban<'r>(subnet: &'r &'r str) -> Request<'r, ()> { + Request { + method: "setban".into(), + params: params![ + ("subnet", r(subnet)), + ("command", r(&"remove")), + ], + converter: &|raw| converter_json(raw), + } +} + +/// Disable/enable all p2p network activity. +#[inline(always)] +pub fn set_network_active(state: bool) -> Request<'static, bool> { + Request { + method: "setnetworkactive".into(), + params: params![ + ("state", v(state)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_peer_info() -> Request<'static, Vec> { + Request { + method: "getpeerinfo".into(), + params: params![], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn ping() -> Request<'static, ()> { + Request { + method: "ping".into(), + params: params![], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn send_raw_transaction<'r>( + tx: &'r HexSerializeWrapper<'r, impl TxParam + ?Sized>, +) -> Request<'r, bitcoin::Txid> { + Request { + method: "sendrawtransaction".into(), + params: params![ + ("hexstring", r(tx)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn estimate_smart_fee<'r>( + conf_target: u16, + estimate_mode: Option<&'r json::EstimateMode>, +) -> Request<'r, json::EstimateSmartFeeResult> { + Request { + method: "estimatesmartfee".into(), + params: params![ + ("conf_target", v(conf_target)), + ?("estimate_mode", or(estimate_mode)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn wait_for_new_block(timeout: Option) -> Request<'static, json::BlockRef> { + Request { + method: "waitfornewblock".into(), + params: params![ + ?("timeout", ov(timeout)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn wait_for_block<'r>( + block_hash: &'r bitcoin::BlockHash, + timeout: Option, +) -> Request<'r, json::BlockRef> { + Request { + method: "waitforblock".into(), + params: params![ + ("blockhash", r(block_hash)), + ?("timeout", ov(timeout)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn create_psbt_raw<'r>( + inputs: &'r &'r [json::CreateRawTransactionInput], + outputs: &'r &'r [json::CreateRawTransactionOutput], + locktime: Option, + replaceable: Option, +) -> Request<'r, String> { + Request { + method: "createpsbt".into(), + params: params![ + ("inputs", r(inputs)), + ("outputs", r(outputs)), + ?("locktime", ov(locktime)), + ?("replaceable", ov(replaceable)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn create_psbt<'r>( + inputs: &'r &'r [json::CreateRawTransactionInput], + outputs: &'r &'r [json::CreateRawTransactionOutput], + locktime: Option, + replaceable: Option, +) -> Request<'r, PartiallySignedTransaction> { + Request { + method: "createpsbt".into(), + params: params![ + ("inputs", r(inputs)), + ("outputs", r(outputs)), + ?("locktime", ov(locktime)), + ?("replaceable", ov(replaceable)), + ], + converter: &|raw| converter_psbt(raw), + } +} + +#[inline(always)] +pub fn wallet_create_funded_psbt<'r>( + inputs: &'r &'r [json::CreateRawTransactionInput], + outputs: &'r &'r [json::CreateRawTransactionOutput], + locktime: Option, + options: Option<&'r json::WalletCreateFundedPsbtOptions>, + include_bip32_derivations: Option, +) -> Request<'r, json::WalletCreateFundedPsbtResult> { + Request { + method: "walletcreatefundedpsbt".into(), + params: params![ + ("inputs", r(inputs)), + ("outputs", r(outputs)), + ?("locktime", ov(locktime)), + ?("options", or(options)), + ?("bip32derivs", ov(include_bip32_derivations)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn wallet_process_psbt<'r>( + psbt: &'r StringSerializeWrapper, + sign: Option, + sighash_type: Option<&'r json::SigHashType>, + include_bip32_derivations: Option, +) -> Request<'r, json::WalletProcessPsbtResult> { + Request { + method: "walletprocesspsbt".into(), + params: params![ + ("psbt", r(psbt)), + ?("sign", ov(sign)), + ?("sighashtype", or(sighash_type)), + ?("bip32derivs", ov(include_bip32_derivations)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn join_psbts_raw<'r>( + psbts: &'r StringListSerializeWrapper<'r, impl PsbtParam>, +) -> Request<'r, String> { + Request { + method: "joinpsbts".into(), + params: params![ + ("txs", r(psbts)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn join_psbts<'r>( + psbts: &'r StringListSerializeWrapper<'r, impl PsbtParam>, +) -> Request<'r, PartiallySignedTransaction> { + Request { + method: "joinpsbts".into(), + params: params![ + ("txs", r(psbts)), + ], + converter: &|raw| converter_psbt(raw), + } +} + +#[inline(always)] +pub fn combine_psbt_raw<'r>( + psbts: &'r StringListSerializeWrapper<'r, impl PsbtParam>, +) -> Request<'r, String> { + Request { + method: "combinepsbt".into(), + params: params![ + ("txs", r(psbts)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn combine_psbt<'r>( + psbts: &'r StringListSerializeWrapper<'r, impl PsbtParam>, +) -> Request<'r, PartiallySignedTransaction> { + Request { + method: "combinepsbt".into(), + params: params![ + ("txs", r(psbts)), + ], + converter: &|raw| converter_psbt(raw), + } +} + +#[inline(always)] +pub fn combine_raw_transaction_hex<'r>( + txs: &'r HexListSerializeWrapper<'r, impl TxParam>, +) -> Request<'r, String> { + Request { + method: "combinerawtransaction".into(), + params: params![ + ("txs", r(txs)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn combine_raw_transaction<'r>( + txs: &'r HexListSerializeWrapper<'r, impl TxParam>, +) -> Request<'r, Transaction> { + Request { + method: "combinerawtransaction".into(), + params: params![ + ("txs", r(txs)), + ], + converter: &|raw| converter_hex(raw), + } +} + +#[inline(always)] +pub fn finalize_psbt<'r>( + psbt: &'r StringSerializeWrapper, + extract: Option, +) -> Request<'r, json::FinalizePsbtResult> { + Request { + method: "finalizepsbt".into(), + params: params![ + ("psbt", r(psbt)), + ?("extract", ov(extract)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_descriptor_info<'r>( + descriptor: &'r &'r str, +) -> Request<'r, json::GetDescriptorInfoResult> { + Request { + method: "getdescriptorinfo".into(), + params: params![ + ("descriptor", r(descriptor)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn derive_addresses<'r>( + descriptor: &'r &'r str, + range: Option<&'r [u32; 2]>, +) -> Request<'r, Vec> { + Request { + method: "deriveaddresses".into(), + params: params![ + ("descriptor", r(descriptor)), + ?("range", or(range)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn rescan_blockchain( + start_height: Option, + stop_height: Option, +) -> Request<'static, (usize, Option)> { + Request { + method: "rescanblockchain".into(), + params: params![ + ?("start_height", ov(start_height)), + ?("stop_height", ov(stop_height)), + ], + converter: &|raw| { + #[derive(Deserialize)] + struct Response { + start_height: usize, + stop_height: Option, + } + let ret = serde_json::from_str::(raw.get())?; + Ok((ret.start_height, ret.stop_height)) + }, + } +} + +#[inline(always)] +pub fn get_tx_out_set_info<'r>( + hash_type: Option<&'r json::TxOutSetHashType>, + target_block_ref: Option<&'r impl BlockRef>, + use_index: Option, +) -> Request<'r, json::GetTxOutSetInfoResult> { + Request { + method: "gettxoutsetinfo".into(), + params: params![ + ?("hash_type", or(hash_type)), + ?("hash_or_height", or(target_block_ref)), + ?("use_index", ov(use_index)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_net_totals() -> Request<'static, json::GetNetTotalsResult> { + Request { + method: "getnettotals".into(), + params: params![], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn get_network_hash_ps( + nb_blocks: Option, + height: Option, +) -> Request<'static, f64> { + Request { + method: "getnetworkhashps".into(), + params: params![ + ?("nblocks", ov(nb_blocks)), + ?("height", ov(height)), + ], + converter: &|raw| converter_json(raw), + } +} + +/// Returns the total uptime of the server in seconds +#[inline(always)] +pub fn uptime() -> Request<'static, u64> { + Request { + method: "uptime".into(), + params: params![], + converter: &|raw| converter_json(raw), + } +} + +#[inline(always)] +pub fn submit_block<'r>( + block: &'r HexSerializeWrapper<'r, impl BlockParam + ?Sized>, +) -> Request<'r, ()> { + Request { + method: "submitblock".into(), + params: params![ + ("block", r(block)), + ], + converter: &|raw| converter_expect_null(raw), + } +} + +#[inline(always)] +pub fn scan_tx_out_set_blocking<'r>( + descriptors: &'r &'r [json::ScanTxOutRequest], +) -> Request<'r, json::ScanTxOutResult> { + Request { + method: "scantxoutset".into(), + params: params![ + ("action", r(&"start")), + ("scanobjects", r(descriptors)), + ], + converter: &|raw| converter_json(raw), + } +} + +#[cfg(test)] +mod test { + use super::*; + use serde_json::Value; + + /// Shorthand for `serde_json::Value::Null`. + fn null() -> Param<'static> { + Param::Value(Value::Null) + } + + #[test] + fn test_params_macro() { + let params = params![ + ("test1", null()), + ?("test2", Some(null())), + ?("test3", None), + ]; + match params { + Params::ByPosition(_) => panic!(), + Params::ByName(p) => { + assert_eq!(p.as_slice().len(), 2); + assert_eq!(serde_json::to_string(&p.as_slice()[0].1).unwrap(), "null"); + assert_eq!(serde_json::to_string(&p.as_slice()[1].1).unwrap(), "null"); + } + } + } +} diff --git a/client/src/serialize.rs b/client/src/serialize.rs new file mode 100644 index 00000000..b862d346 --- /dev/null +++ b/client/src/serialize.rs @@ -0,0 +1,143 @@ + +use std::cell::RefCell; +use std::fmt; + +use serde::ser::{Serialize, Serializer, SerializeSeq}; + +use crate::json::bitcoin::{OutPoint, Txid}; +use crate::{HexParam, StringParam}; + +/// Wrapper for types with a custom string-based serialization. +pub struct StringSerializeWrapper<'a, T: ?Sized>(pub &'a T); + +impl<'a, T: StringParam + ?Sized> serde::Serialize for StringSerializeWrapper<'a, T> { + fn serialize(&self, s: S) -> std::result::Result { + struct Fmt<'a, T: ?Sized>(&'a T); + impl<'a, T: StringParam + ?Sized> fmt::Display for Fmt<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + StringParam::write_string(self.0, f) + } + } + + s.collect_str(&Fmt(self.0)) + } +} + +/// A wrapper for an argument to be serialized as a list of strings. +pub struct StringListSerializeWrapper<'a, T>(pub &'a [T]); + +impl<'a, T: StringParam> Serialize for StringListSerializeWrapper<'a, T> { + fn serialize(&self, serializer: S) -> Result { + let mut seq = serializer.serialize_seq(None)?; + for e in self.0.iter() { + SerializeSeq::serialize_element(&mut seq, &StringSerializeWrapper(e))?; + } + SerializeSeq::end(seq) + } +} + +/// A wrapper for an argument to be serialized as hex. +pub struct HexSerializeWrapper<'a, T: ?Sized>(pub &'a T); + +impl<'a, T: HexParam + ?Sized> Serialize for HexSerializeWrapper<'a, T> { + fn serialize(&self, serializer: S) -> Result { + struct Fmt<'a, T: ?Sized>(&'a T); + impl<'a, T: HexParam + ?Sized> fmt::Display for Fmt<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + HexParam::write_hex(self.0, f) + } + } + + serializer.collect_str(&Fmt(self.0)) + } +} + +/// A wrapper for an argument to be serialized as a list of hex objects. +pub struct HexListSerializeWrapper<'a, T>(pub &'a [T]); + +impl<'a, T: HexParam> Serialize for HexListSerializeWrapper<'a, T> { + fn serialize(&self, serializer: S) -> Result { + let mut seq = serializer.serialize_seq(None)?; + for e in self.0.iter() { + SerializeSeq::serialize_element(&mut seq, &HexSerializeWrapper(e))?; + } + SerializeSeq::end(seq) + } +} + +/// A wrapper type to serialize a series of objects as a JSON list. +/// Implemented for Iterators over serializable objects. +pub(crate) struct ListSerializeWrapper(RefCell>); + +impl From for ListSerializeWrapper { + fn from(v: T) -> ListSerializeWrapper { + ListSerializeWrapper(RefCell::new(Some(v))) + } +} + +impl serde::Serialize for ListSerializeWrapper +where + E: serde::Serialize, + I: IntoIterator, +{ + fn serialize(&self, serializer: S) -> Result { + let mut seq = serializer.serialize_seq(None)?; + for e in self.0.borrow_mut().take().unwrap().into_iter() { + serde::ser::SerializeSeq::serialize_element(&mut seq, &e)?; + } + serde::ser::SerializeSeq::end(seq) + } +} + +/// A wrapper type to serialize something as a JSON map. +/// Implemented for Iterators over key-value pairs. +pub struct MapSerializeWrapper(RefCell); + +impl From for MapSerializeWrapper { + fn from(v: T) -> MapSerializeWrapper { + MapSerializeWrapper(RefCell::new(v)) + } +} + +impl serde::Serialize for MapSerializeWrapper +where + K: serde::Serialize, + V: serde::Serialize, + I: Iterator, +{ + fn serialize(&self, serializer: S) -> Result { + let mut map = serializer.serialize_map(None)?; + for (k, v) in &mut *self.0.borrow_mut() { + serde::ser::SerializeMap::serialize_entry(&mut map, &k, &v)?; + } + serde::ser::SerializeMap::end(map) + } +} + +/// A wrapper type to serialize OutPoint as JSON objects. +pub struct OutPointListObjectSerializeWrapper<'a>(pub &'a [OutPoint]); + +impl<'a> serde::Serialize for OutPointListObjectSerializeWrapper<'a> { + fn serialize(&self, serializer: S) -> Result { + #[derive(serde::Serialize)] + struct JsonOutPoint { + pub txid: Txid, + pub vout: u32, + } + + impl From for JsonOutPoint { + fn from(o: OutPoint) -> JsonOutPoint { + JsonOutPoint { + txid: o.txid, + vout: o.vout, + } + } + } + + let mut seq = serializer.serialize_seq(None)?; + for e in self.0.iter() { + SerializeSeq::serialize_element(&mut seq, &JsonOutPoint::from(*e))?; + } + SerializeSeq::end(seq) + } +} diff --git a/client/src/sync_client.rs b/client/src/sync_client.rs new file mode 100644 index 00000000..3982612d --- /dev/null +++ b/client/src/sync_client.rs @@ -0,0 +1,873 @@ + +use std::sync::atomic; + +use crate::bitcoin::secp256k1::ecdsa::Signature; +use crate::bitcoin::{ + self, Address, Amount, Block, OutPoint, PrivateKey, PublicKey, + ScriptBuf, Transaction, FeeRate, +}; +use crate::bitcoin::block::Header as BlockHeader; +use crate::bitcoin::psbt::PartiallySignedTransaction; +type UncheckedAddress = Address; + +use jsonrpc::client::{List, Param, Params, Request}; + +use crate::{ + json, requests, AddressParam, Client, BlockRef, BlockParam, Error, + Result, PsbtParam, SighashParam, TxParam, +}; +use crate::serialize::{ + HexListSerializeWrapper, HexSerializeWrapper, + OutPointListObjectSerializeWrapper, + StringListSerializeWrapper, StringSerializeWrapper, +}; + +/// Synchronous client API. +pub trait SyncClient { + /// The internal method to make a request. + fn handle_request<'r, T>(&self, req: Request<'r, T>) -> Result; + + /// Make a manual call. + fn call serde::de::Deserialize<'a> + 'static>( + &self, + method: &str, + params: &[serde_json::Value], + ) -> Result { + let params = params.iter().map(|v| Param::ByRef(v)).collect::>(); + self.handle_request( + Request { + method: method.into(), + params: Params::ByPosition(List::Slice(¶ms[..])), + converter: &|raw| requests::converter_json(raw), + } + ) + } + + /// Get cached version of the version. + fn version(&self) -> Result; + + /// Refresh the cached version by asking the server again. + fn refresh_version(&self) -> Result<()>; + + fn get_version(&self) -> Result { + self.handle_request(requests::version()) + } + + fn get_network_info(&self) -> Result { + self.handle_request(requests::get_network_info()) + } + + fn get_index_info(&self) -> Result { + self.handle_request(requests::get_index_info()) + } + + fn add_multisig_address( + &self, + nrequired: usize, + keys: &[json::PubKeyOrAddress], + label: Option<&str>, + address_type: Option, + ) -> Result { + self.handle_request(requests::add_multisig_address( + nrequired, &keys, label.as_ref(), address_type.as_ref(), + )) + } + + fn load_wallet(&self, wallet: &str) -> Result { + self.handle_request(requests::load_wallet(&wallet)) + } + + fn unload_wallet(&self, wallet: Option<&str>) -> Result { + self.handle_request(requests::unload_wallet(wallet.as_ref())) + } + + fn create_wallet( + &self, + wallet: &str, + disable_private_keys: Option, + blank: Option, + passphrase: Option<&str>, + avoid_reuse: Option, + ) -> Result { + self.handle_request(requests::create_wallet( + &wallet, disable_private_keys, blank, passphrase.as_ref(), avoid_reuse, + )) + } + + fn list_wallets(&self) -> Result> { + self.handle_request(requests::list_wallets()) + } + + fn list_wallet_dir(&self) -> Result> { + self.handle_request(requests::list_wallet_dir()) + } + + fn get_wallet_info(&self) -> Result { + self.handle_request(requests::get_wallet_info()) + } + + fn backup_wallet(&self, destination: &str) -> Result<()> { + self.handle_request(requests::backup_wallet(&destination)) + } + + fn dump_private_key( + &self, + address: &(impl AddressParam + ?Sized), + ) -> Result { + self.handle_request(requests::dump_private_key(&StringSerializeWrapper(address))) + } + + fn encrypt_wallet(&self, passphrase: &str) -> Result { + self.handle_request(requests::encrypt_wallet(&passphrase)) + } + + fn get_difficulty(&self) -> Result { + self.handle_request(requests::get_difficulty()) + } + + fn get_connection_count(&self) -> Result { + self.handle_request(requests::get_connection_count()) + } + + fn get_block(&self, hash: &bitcoin::BlockHash) -> Result { + self.handle_request(requests::get_block(hash)) + } + + fn get_block_hex(&self, hash: &bitcoin::BlockHash) -> Result { + self.handle_request(requests::get_block_hex(hash)) + } + + fn get_block_info(&self, hash: &bitcoin::BlockHash) -> Result { + self.handle_request(requests::get_block_info(hash)) + } + //TODO(stevenroose) add getblock_txs + + fn get_block_header(&self, hash: &bitcoin::BlockHash) -> Result { + self.handle_request(requests::get_block_header(hash)) + } + + fn get_block_header_info( + &self, + hash: &bitcoin::BlockHash, + ) -> Result { + self.handle_request(requests::get_block_header_info(hash)) + } + + fn get_mining_info(&self) -> Result { + self.handle_request(requests::get_mining_info()) + } + + fn get_block_template( + &self, + mode: json::GetBlockTemplateModes, + rules: &[json::GetBlockTemplateRules], + capabilities: &[json::GetBlockTemplateCapabilities], + ) -> Result { + self.handle_request(requests::get_block_template(&mode, &rules, &capabilities)) + } + + fn get_blockchain_info(&self) -> Result { + self.handle_request(requests::get_blockchain_info()) + } + + fn get_block_count(&self) -> Result { + self.handle_request(requests::get_block_count()) + } + + fn get_best_block_hash(&self) -> Result { + self.handle_request(requests::get_best_block_hash()) + } + + fn get_block_hash(&self, height: u64) -> Result { + self.handle_request(requests::get_block_hash(height)) + } + + fn get_block_stats(&self, block_ref: impl BlockRef) -> Result { + self.handle_request(requests::get_block_stats(&block_ref)) + } + + fn get_block_stats_fields( + &self, + block_ref: impl BlockRef, + fields: &[json::BlockStatsFields], + ) -> Result { + self.handle_request(requests::get_block_stats_fields(&block_ref, &fields)) + } + + fn get_raw_transaction( + &self, + txid: &bitcoin::Txid, + block_hash: Option<&bitcoin::BlockHash>, + ) -> Result { + self.handle_request(requests::get_raw_transaction(txid, block_hash)) + } + + fn get_raw_transaction_hex( + &self, + txid: &bitcoin::Txid, + block_hash: Option<&bitcoin::BlockHash>, + ) -> Result { + self.handle_request(requests::get_raw_transaction_hex(txid, block_hash)) + } + + fn get_raw_transaction_info( + &self, + txid: &bitcoin::Txid, + block_hash: Option<&bitcoin::BlockHash>, + ) -> Result { + self.handle_request(requests::get_raw_transaction_info(txid, block_hash)) + } + + fn get_block_filter( + &self, + block_hash: &bitcoin::BlockHash, + ) -> Result { + self.handle_request(requests::get_block_filter(block_hash)) + } + + fn get_balance( + &self, + minconf: Option, + include_watchonly: Option, + ) -> Result { + self.handle_request(requests::get_balance(minconf, include_watchonly)) + } + + fn get_balances(&self) -> Result { + self.handle_request(requests::get_balances()) + } + + fn get_received_by_address( + &self, + address: &(impl AddressParam + ?Sized), + minconf: Option, + ) -> Result { + self.handle_request(requests::get_received_by_address( + &StringSerializeWrapper(address), minconf, + )) + } + + fn get_transaction( + &self, + txid: &bitcoin::Txid, + include_watchonly: Option, + ) -> Result { + let support_verbose = self.version()? >= 19_00_00; + + self.handle_request(requests::get_transaction(txid, include_watchonly, support_verbose)) + } + + fn list_transactions( + &self, + label: Option<&str>, + count: Option, + skip: Option, + include_watchonly: Option, + ) -> Result> { + self.handle_request(requests::list_transactions( + label.as_ref(), count, skip, include_watchonly, + )) + } + + fn list_since_block( + &self, + block_hash: Option<&bitcoin::BlockHash>, + target_confirmations: Option, + include_watchonly: Option, + include_removed: Option, + ) -> Result { + self.handle_request(requests::list_since_block( + block_hash, target_confirmations, include_watchonly, include_removed, + )) + } + + fn get_tx_out( + &self, + txid: &bitcoin::Txid, + vout: u32, + include_mempool: Option, + ) -> Result> { + self.handle_request(requests::get_tx_out(txid, vout, include_mempool)) + } + + fn get_tx_out_proof( + &self, + txids: &[bitcoin::Txid], + block_hash: Option<&bitcoin::BlockHash>, + ) -> Result> { + self.handle_request(requests::get_tx_out_proof(&txids, block_hash)) + } + + fn import_public_key( + &self, + public_key: &PublicKey, + label: Option<&str>, + rescan: Option, + ) -> Result<()> { + self.handle_request(requests::import_public_key(public_key, label.as_ref(), rescan)) + } + + fn import_private_key( + &self, + private_key: &PrivateKey, + label: Option<&str>, + rescan: Option, + ) -> Result<()> { + self.handle_request(requests::import_private_key(private_key, label.as_ref(), rescan)) + } + + fn import_address( + &self, + address: &(impl AddressParam + ?Sized), + label: Option<&str>, + rescan: Option, + ) -> Result<()> { + self.handle_request(requests::import_address( + &StringSerializeWrapper(address), label.as_ref(), rescan, + )) + } + + fn import_address_script( + &self, + script: &ScriptBuf, + label: Option<&str>, + rescan: Option, + p2sh: Option, + ) -> Result<()> { + self.handle_request(requests::import_address_script(script, label.as_ref(), rescan, p2sh)) + } + + fn import_multi( + &self, + requests: &[json::ImportMultiRequest], + options: Option<&json::ImportMultiOptions>, + ) -> Result> { + self.handle_request(requests::import_multi(&requests, options)) + } + + fn import_descriptors( + &self, + requests: &[json::ImportDescriptors], + ) -> Result> { + self.handle_request(requests::import_descriptors(&requests)) + } + + fn set_label( + &self, + address: &(impl AddressParam + ?Sized), + label: &str, + ) -> Result<()> { + self.handle_request(requests::set_label(&StringSerializeWrapper(address), &label)) + } + + fn key_pool_refill(&self, new_size: Option) -> Result<()> { + self.handle_request(requests::key_pool_refill(new_size)) + } + + fn list_unspent( + &self, + minconf: Option, + maxconf: Option, + addresses: Option<&[impl AddressParam]>, + include_unsafe: Option, + query_options: Option<&json::ListUnspentQueryOptions>, + ) -> Result> { + self.handle_request(requests::list_unspent( + minconf, maxconf, addresses.map(|a| StringListSerializeWrapper(a)).as_ref(), + include_unsafe, query_options, + )) + } + + /// To unlock, use [unlock_unspent]. + fn lock_unspent(&self, outputs: &[OutPoint]) -> Result { + self.handle_request(requests::lock_unspent(&OutPointListObjectSerializeWrapper(outputs))) + } + + fn unlock_unspent(&self, outputs: &[OutPoint]) -> Result { + self.handle_request(requests::unlock_unspent(&OutPointListObjectSerializeWrapper(outputs))) + } + + fn unlock_unspent_all(&self) -> Result { + self.handle_request(requests::unlock_unspent_all()) + } + + fn list_received_by_address( + &self, + address_filter: Option<&(impl AddressParam + ?Sized)>, + minconf: Option, + include_empty: Option, + include_watchonly: Option, + ) -> Result> { + self.handle_request(requests::list_received_by_address( + address_filter.map(|a| StringSerializeWrapper(a)).as_ref(), minconf, include_empty, + include_watchonly, + )) + } + + fn create_raw_transaction_hex( + &self, + inputs: &[json::CreateRawTransactionInput], + outputs: &[json::CreateRawTransactionOutput], + locktime: Option, + replaceable: Option, + ) -> Result { + self.handle_request(requests::create_raw_transaction_hex( + &inputs, &outputs, locktime, replaceable, + )) + } + + fn create_raw_transaction( + &self, + inputs: &[json::CreateRawTransactionInput], + outputs: &[json::CreateRawTransactionOutput], + locktime: Option, + replaceable: Option, + ) -> Result { + self.handle_request(requests::create_raw_transaction( + &inputs, &outputs, locktime, replaceable, + )) + } + + fn decode_raw_transaction( + &self, + tx: &(impl TxParam + ?Sized), + is_witness: Option, + ) -> Result { + self.handle_request(requests::decode_raw_transaction( + &HexSerializeWrapper(tx), is_witness, + )) + } + + fn fund_raw_transaction( + &self, + tx: &(impl TxParam + ?Sized), + options: Option<&json::FundRawTransactionOptions>, + is_witness: Option, + ) -> Result { + self.handle_request(requests::fund_raw_transaction( + &HexSerializeWrapper(tx), options, is_witness, + )) + } + + fn sign_raw_transaction_with_wallet( + &self, + tx: &(impl TxParam + ?Sized), + inputs: Option<&[json::SignRawTransactionInput]>, + sighash_type: Option<&impl SighashParam>, + ) -> Result { + let sighash = sighash_type.as_ref().map(|v| StringSerializeWrapper(*v)); + self.handle_request(requests::sign_raw_transaction_with_wallet( + &HexSerializeWrapper(tx), + inputs.as_ref(), + sighash.as_ref(), + )) + } + + fn sign_raw_transaction_with_key( + &self, + tx: &(impl TxParam + ?Sized), + private_keys: &[PrivateKey], + inputs: Option<&[json::SignRawTransactionInput]>, + sighash_type: Option<&impl SighashParam>, + ) -> Result { + let sighash = sighash_type.as_ref().map(|v| StringSerializeWrapper(*v)); + self.handle_request(requests::sign_raw_transaction_with_key( + &HexSerializeWrapper(tx), &private_keys, inputs.as_ref(), sighash.as_ref(), + )) + } + + /// Fee rate per kvb. + fn test_mempool_accept<'r>( + &self, + raw_txs: &[impl TxParam], + max_fee_rate: Option, + ) -> Result> { + let fr_btc_per_kvb = max_fee_rate.map(|fr| { + let per_kvb = fr.to_per_kvb() + .ok_or_else(|| Error::InvalidArguments("fee rate overflow".into()))?; + Result::<_>::Ok(per_kvb.to_btc()) + }).transpose()?; + + self.handle_request(requests::test_mempool_accept( + &HexListSerializeWrapper(raw_txs), fr_btc_per_kvb, + )) + } + + fn stop(&self) -> Result { + self.handle_request(requests::stop()) + } + + fn verify_message( + &self, + address: &(impl AddressParam + ?Sized), + signature: &Signature, + message: &str, + ) -> Result { + self.handle_request(requests::verify_message( + &StringSerializeWrapper(address), signature, &message, + )) + } + + fn get_new_address( + &self, + label: Option<&str>, + address_type: Option, + ) -> Result { + self.handle_request(requests::get_new_address(label.as_ref(), address_type.as_ref())) + } + + fn get_raw_change_address( + &self, + address_type: Option, + ) -> Result { + self.handle_request(requests::get_raw_change_address(address_type.as_ref())) + } + + fn get_address_info( + &self, + address: &(impl AddressParam + ?Sized), + ) -> Result { + self.handle_request(requests::get_address_info(&StringSerializeWrapper(address))) + } + + fn generate_to_address( + &self, + block_num: u64, + address: &(impl AddressParam + ?Sized), + max_tries: Option, + ) -> Result> { + self.handle_request(requests::generate_to_address( + block_num, &StringSerializeWrapper(address), max_tries, + )) + } + + // NB This call is no longer available on recent Bitcoin Core versions. + #[deprecated] + fn generate( + &self, + block_num: u64, + max_tries: Option, + ) -> Result> { + self.handle_request(requests::generate(block_num, max_tries)) + } + + fn invalidate_block(&self, block_hash: &bitcoin::BlockHash) -> Result<()> { + self.handle_request(requests::invalidate_block(block_hash)) + } + + fn reconsider_block(&self, block_hash: &bitcoin::BlockHash) -> Result<()> { + self.handle_request(requests::reconsider_block(block_hash)) + } + + fn get_mempool_info(&self) -> Result { + self.handle_request(requests::get_mempool_info()) + } + + fn get_raw_mempool(&self) -> Result> { + self.handle_request(requests::get_raw_mempool()) + } + + fn get_mempool_entry(&self, txid: &bitcoin::Txid) -> Result { + self.handle_request(requests::get_mempool_entry(txid)) + } + + fn get_chain_tips(&self) -> Result { + self.handle_request(requests::get_chain_tips()) + } + + //TODO(stevenroose) make this call more ergonomic, it's getting insane + // NB the [avoid_reuse] argument is not supported for Bitcoin Core version 0.18 and older. + // NB the [fee_rate] argument is not supported for Bitcoin Core versions 0.19 and older. + fn send_to_address( + &self, + address: &(impl AddressParam + ?Sized), + amount: Amount, + comment: Option<&str>, + comment_to: Option<&str>, + subtract_fee: Option, + replaceable: Option, + confirmation_target: Option, + estimate_mode: Option, + avoid_reuse: Option, + fee_rate: Option, + ) -> Result { + let support_verbose = self.version()? >= 21_00_00; + + self.handle_request(requests::send_to_address( + &StringSerializeWrapper(address), amount, comment.as_ref(), comment_to.as_ref(), + subtract_fee, replaceable, confirmation_target, estimate_mode.as_ref(), avoid_reuse, + support_verbose, fee_rate.map(|fr| fr.to_per_vb_ceil().to_sat()), + )) + } + + fn add_node(&self, addr: &str) -> Result<()> { + self.handle_request(requests::add_node(&addr)) + } + + fn add_node_onetry(&self, addr: &str) -> Result<()> { + self.handle_request(requests::add_node_onetry(&addr)) + } + + fn remove_node(&self, addr: &str) -> Result<()> { + self.handle_request(requests::remove_node(&addr)) + } + + fn disconnect_node(&self, addr: &str) -> Result<()> { + self.handle_request(requests::disconnect_node(&addr)) + } + + fn disconnect_node_by_id(&self, node_id: u32) -> Result<()> { + self.handle_request(requests::disconnect_node_by_id(node_id)) + } + + fn get_added_node_info( + &self, + node: &str, + ) -> Result> { + self.handle_request(requests::get_added_node_info(&node)) + } + + fn get_added_nodes_info(&self) -> Result> { + self.handle_request(requests::get_added_nodes_info()) + } + + fn get_node_addresses( + &self, + count: Option, + ) -> Result> { + self.handle_request(requests::get_node_addresses(count)) + } + + fn list_banned(&self) -> Result> { + self.handle_request(requests::list_banned()) + } + + fn clear_banned(&self) -> Result<()> { + self.handle_request(requests::clear_banned()) + } + + fn add_ban(&self, subnet: &str, bantime: u64, absolute: bool) -> Result<()> { + self.handle_request(requests::add_ban(&subnet, bantime, absolute)) + } + + fn remove_ban(&self, subnet: &str) -> Result<()> { + self.handle_request(requests::remove_ban(&subnet)) + } + + fn set_network_active(&self, state: bool) -> Result { + self.handle_request(requests::set_network_active(state)) + } + + fn get_peer_info(&self) -> Result> { + self.handle_request(requests::get_peer_info()) + } + + fn ping(&self) -> Result<()> { + self.handle_request(requests::ping()) + } + + fn send_raw_transaction( + &self, + tx: &(impl TxParam + ?Sized), + ) -> Result { + self.handle_request(requests::send_raw_transaction(&HexSerializeWrapper(tx))) + } + + fn estimate_smart_fee( + &self, + conf_target: u16, + estimate_mode: Option, + ) -> Result { + self.handle_request(requests::estimate_smart_fee(conf_target, estimate_mode.as_ref())) + } + + fn wait_for_new_block(&self, timeout: Option) -> Result { + self.handle_request(requests::wait_for_new_block(timeout)) + } + + fn wait_for_block( + &self, + block_hash: &bitcoin::BlockHash, + timeout: Option, + ) -> Result { + self.handle_request(requests::wait_for_block(block_hash, timeout)) + } + + fn get_descriptor_info( + &self, + descriptor: &str, + ) -> Result { + self.handle_request(requests::get_descriptor_info(&descriptor)) + } + + fn derive_addresses( + &self, + descriptor: &str, + range: Option<&[u32; 2]>, + ) -> Result> { + self.handle_request(requests::derive_addresses(&descriptor, range)) + } + + fn create_psbt_raw( + &self, + inputs: &[json::CreateRawTransactionInput], + outputs: &[json::CreateRawTransactionOutput], + locktime: Option, + replaceable: Option, + ) -> Result { + self.handle_request(requests::create_psbt_raw( + &inputs, &outputs, locktime, replaceable, + )) + } + + fn create_psbt( + &self, + inputs: &[json::CreateRawTransactionInput], + outputs: &[json::CreateRawTransactionOutput], + locktime: Option, + replaceable: Option, + ) -> Result { + self.handle_request(requests::create_psbt( + &inputs, &outputs, locktime, replaceable, + )) + } + + fn wallet_create_funded_psbt( + &self, + inputs: &[json::CreateRawTransactionInput], + outputs: &[json::CreateRawTransactionOutput], + locktime: Option, + options: Option<&json::WalletCreateFundedPsbtOptions>, + include_bip32_derivations: Option, + ) -> Result { + self.handle_request(requests::wallet_create_funded_psbt( + &inputs, &outputs, locktime, options, include_bip32_derivations, + )) + } + + /// NB the [sighash_type] argument is not optional in all version of Bitcoin Core. + fn wallet_process_psbt( + &self, + psbt: &(impl PsbtParam + ?Sized), + sign: Option, + sighash_type: Option<&json::SigHashType>, + include_bip32_derivations: Option, + ) -> Result { + // Somehow if the bip32derivs parameter is set, the sighashtype is not optional. + let version = self.version()?; + if version >= 18_00_00 && version <= 22_00_00 + && include_bip32_derivations.is_some() && sighash_type.is_none() + { + return Err(Error::InvalidArguments(format!( + "the `sighash_type` argument is required when the `include_bip32_derivations` \ + argument is provided" + ))); + } + + self.handle_request(requests::wallet_process_psbt( + &StringSerializeWrapper(psbt), sign, sighash_type, include_bip32_derivations, + )) + } + + fn join_psbts_raw(&self, psbts: &[impl PsbtParam]) -> Result { + self.handle_request(requests::join_psbts_raw(&StringListSerializeWrapper(psbts))) + } + + fn join_psbts(&self, psbts: &[impl PsbtParam]) -> Result { + self.handle_request(requests::join_psbts(&StringListSerializeWrapper(psbts))) + } + + fn combine_psbt_raw(&self, psbts: &[impl PsbtParam]) -> Result { + self.handle_request(requests::combine_psbt_raw(&StringListSerializeWrapper(psbts))) + } + + fn combine_psbt(&self, psbts: &[impl PsbtParam]) -> Result { + self.handle_request(requests::combine_psbt(&StringListSerializeWrapper(psbts))) + } + + fn combine_raw_transaction_hex(&self, txs: &[impl TxParam]) -> Result { + self.handle_request(requests::combine_raw_transaction_hex(&HexListSerializeWrapper(txs))) + } + + fn combine_raw_transaction(&self, txs: &[impl TxParam]) -> Result { + self.handle_request(requests::combine_raw_transaction(&HexListSerializeWrapper(txs))) + } + + fn finalize_psbt( + &self, + psbt: &(impl PsbtParam + ?Sized), + extract: Option, + ) -> Result { + self.handle_request(requests::finalize_psbt(&StringSerializeWrapper(psbt), extract)) + } + + fn rescan_blockchain( + &self, + start_height: Option, + stop_height: Option, + ) -> Result<(usize, Option)> { + self.handle_request(requests::rescan_blockchain(start_height, stop_height)) + } + + fn get_tx_out_set_info( + &self, + hash_type: Option, + target_block_ref: Option, + use_index: Option, + ) -> Result { + self.handle_request(requests::get_tx_out_set_info( + hash_type.as_ref(), target_block_ref.as_ref(), use_index, + )) + } + + fn get_net_totals(&self) -> Result { + self.handle_request(requests::get_net_totals()) + } + + fn get_network_hash_ps( + &self, + nb_blocks: Option, + height: Option, + ) -> Result { + self.handle_request(requests::get_network_hash_ps(nb_blocks, height)) + } + + fn uptime(&self) -> Result { + self.handle_request(requests::uptime()) + } + + fn submit_block(&self, block: &(impl BlockParam + ?Sized)) -> Result<()> { + self.handle_request(requests::submit_block(&HexSerializeWrapper(block))) + } + + fn scan_tx_out_set_blocking( + &self, + descriptors: &[json::ScanTxOutRequest], + ) -> Result { + self.handle_request(requests::scan_tx_out_set_blocking(&descriptors)) + } +} + +impl SyncClient for Client { + fn version(&self) -> Result { + let ver = self.version.load(atomic::Ordering::Relaxed); + + if ver > 0 { + Ok(ver) + } else { + self.refresh_version()?; + Ok(self.version.load(atomic::Ordering::Relaxed)) + } + } + + fn refresh_version(&self) -> Result<()> { + let ver = self.get_version()?; + self.version.store(ver, atomic::Ordering::Relaxed); + Ok(()) + } + + #[inline(always)] + fn handle_request<'r, R>(&self, req: Request<'r, R>) -> Result { + Ok(req.get_sync(&self.client)?) + } +} diff --git a/contrib/test.sh b/contrib/test.sh index 9c0e908f..21b359e4 100755 --- a/contrib/test.sh +++ b/contrib/test.sh @@ -2,15 +2,9 @@ set -xe # Just echo all the relevant env vars to help debug Travis. -echo "RUSTFMTCHECK: \"$RUSTFMTCHECK\"" echo "BITCOINVERSION: \"$BITCOINVERSION\"" echo "PATH: \"$PATH\"" -if [ -n "$RUSTFMTCHECK" ]; then - rustup component add rustfmt - cargo fmt --all -- --check -fi - # Integration test. if [ -n "$BITCOINVERSION" ]; then wget https://bitcoincore.org/bin/bitcoin-core-$BITCOINVERSION/bitcoin-$BITCOINVERSION-x86_64-linux-gnu.tar.gz diff --git a/integration_test/Cargo.toml b/integration_test/Cargo.toml index c0ef00a0..c8cd3bd2 100644 --- a/integration_test/Cargo.toml +++ b/integration_test/Cargo.toml @@ -9,3 +9,4 @@ bitcoincore-rpc = { path = "../client" } bitcoin = { version = "0.30.0", features = ["serde", "rand"]} lazy_static = "1.4.0" log = "0.4" +backtrace = "0.3.50" diff --git a/integration_test/run.sh b/integration_test/run.sh index be15d005..90e28721 100755 --- a/integration_test/run.sh +++ b/integration_test/run.sh @@ -1,5 +1,6 @@ #!/bin/sh +BITCOIND_PATH="${BITCOIND_PATH:-bitcoind}" TESTDIR=/tmp/rust_bitcoincore_rpc_test rm -rf ${TESTDIR} @@ -8,7 +9,7 @@ mkdir -p ${TESTDIR}/1 ${TESTDIR}/2 # To kill any remaining open bitcoind. killall -9 bitcoind -bitcoind -regtest \ +${BITCOIND_PATH} -regtest \ -datadir=${TESTDIR}/1 \ -port=12348 \ -server=0 \ @@ -19,16 +20,16 @@ PID1=$! sleep 3 BLOCKFILTERARG="" -if bitcoind -version | grep -q "v0\.\(19\|2\)"; then +if ${BITCOIND_PATH} -version | grep -q "v\(2\|0\.19\|0.2\)"; then BLOCKFILTERARG="-blockfilterindex=1" fi FALLBACKFEEARG="" -if bitcoind -version | grep -q "v0\.2"; then +if ${BITCOIND_PATH} -version | grep -q "v\(2\|0\.2\)"; then FALLBACKFEEARG="-fallbackfee=0.00001000" fi -bitcoind -regtest $BLOCKFILTERARG $FALLBACKFEEARG \ +${BITCOIND_PATH} -regtest $BLOCKFILTERARG $FALLBACKFEEARG \ -datadir=${TESTDIR}/2 \ -connect=127.0.0.1:12348 \ -rpcport=12349 \ diff --git a/integration_test/src/main.rs b/integration_test/src/main.rs index d9f4dca7..b77a2d02 100644 --- a/integration_test/src/main.rs +++ b/integration_test/src/main.rs @@ -9,37 +9,44 @@ //! #![deny(unused)] +#![allow(deprecated)] #[macro_use] extern crate lazy_static; -use std::collections::HashMap; +use std::cell::RefCell; +use std::{mem, panic}; use std::str::FromStr; -use bitcoin::absolute::LockTime; -use bitcoin::address::NetworkChecked; +use backtrace::Backtrace; use bitcoincore_rpc::json; +use bitcoincore_rpc::jsonrpc; use bitcoincore_rpc::jsonrpc::error::Error as JsonRpcError; -use bitcoincore_rpc::{Auth, Client, Error, RpcApi}; +use bitcoincore_rpc::{Auth, Error, SyncClient}; -use crate::json::BlockStatsFields as BsFields; +use crate::json::{BlockStatsFields as BsFields}; use bitcoin::consensus::encode::{deserialize, serialize_hex}; use bitcoin::hashes::hex::FromHex; use bitcoin::hashes::Hash; -use bitcoin::{secp256k1, ScriptBuf, sighash}; +use bitcoin::sighash::EcdsaSighashType; +use bitcoin::{secp256k1, ScriptBuf}; use bitcoin::{ - Address, Amount, Network, OutPoint, PrivateKey, + Address, Amount, FeeRate, Network, OutPoint, PrivateKey, Sequence, SignedAmount, Transaction, TxIn, TxOut, Txid, Witness, }; +use bitcoin::blockdata::locktime::absolute::LockTime; use bitcoincore_rpc::bitcoincore_rpc_json::{ GetBlockTemplateModes, GetBlockTemplateRules, ScanTxOutRequest, }; +type UncheckedAddress = Address; lazy_static! { static ref SECP: secp256k1::Secp256k1 = secp256k1::Secp256k1::new(); static ref NET: Network = Network::Regtest; /// A random address not owned by the node. - static ref RANDOM_ADDRESS: Address = Address::from_str("mgR9fN5UzZ64mSUUtk6NwxxS6kwVfoEtPG").unwrap().assume_checked(); + static ref RANDOM_ADDRESS: Address = Address::from_str("mgR9fN5UzZ64mSUUtk6NwxxS6kwVfoEtPG").unwrap().assume_checked(); + /// The default fee rate to use when needed. + static ref FEERATE: FeeRate = FeeRate::from_per_vb(Amount::from_sat(1)).unwrap(); /// The default fee amount to use when needed. static ref FEE: Amount = Amount::from_btc(0.001).unwrap(); } @@ -93,12 +100,6 @@ macro_rules! assert_error_message { }; } -static mut VERSION: usize = 0; -/// Get the version of the node that is running. -fn version() -> usize { - unsafe { VERSION } -} - /// Quickly create a BTC amount. fn btc>(btc: F) -> Amount { Amount::from_btc(btc.into()).unwrap() @@ -107,6 +108,10 @@ fn btc>(btc: F) -> Amount { fn sbtc>(btc: F) -> SignedAmount { SignedAmount::from_btc(btc.into()).unwrap() } +/// Quickly create a feerate in sat_per_vb. +fn sat_per_vb(v: u64) -> FeeRate { + FeeRate::from_per_vb(Amount::from_sat(v)).unwrap() +} fn get_testdir() -> String { return std::env::var("TESTDIR").expect("TESTDIR must be set"); @@ -126,107 +131,186 @@ fn get_auth() -> bitcoincore_rpc::Auth { }; } +type Client = bitcoincore_rpc::Client; +fn make_client(url: &str) -> Client { + Client::with_simple_http(url, get_auth()).unwrap() +} + fn new_wallet_client(wallet_name: &str) -> Client { - let url = format!("{}{}{}", get_rpc_url(), "/wallet/", wallet_name); - Client::new(&url, get_auth()).unwrap() + make_client(&format!("{}{}{}", get_rpc_url(), "/wallet/", wallet_name)) +} + +lazy_static! { + static ref CLIENT: Client = { + let rpc_url = format!("{}/wallet/testwallet", get_rpc_url()); + make_client(&rpc_url) + }; +} + +/// Get the version of the node that is running. +fn version() -> usize { + static mut VERSION: Option = None; + + match unsafe { VERSION } { + Some(v) => v, + None => { + let v = CLIENT.version().unwrap(); + unsafe { + VERSION = Some(v); + } + v + } + } +} + +thread_local! { + static LAST_PANIC: RefCell> = RefCell::new(None); +} + +/// Here we will collect all the results of the individual tests, +/// preserving ordering. +/// Ideally this would be preset with capacity, but static prevents this. +static mut RESULTS: Vec<(&'static str, bool)> = Vec::new(); + +macro_rules! run_test { + ($method:ident) => { + println!("Running {}...", stringify!($method)); + let result = panic::catch_unwind(|| { + $method(&*CLIENT); + }); + if result.is_err() { + let (msg, bt) = LAST_PANIC.with(|b| b.borrow_mut().take()).unwrap(); + println!("{}", msg); + println!("{:?}", bt); + println!("--"); + } + + unsafe { RESULTS.push((stringify!($method), result.is_ok())); } + } } fn main() { log::set_logger(&LOGGER).map(|()| log::set_max_level(log::LevelFilter::max())).unwrap(); - let cl = new_wallet_client("testwallet"); + // let default_hook = std::panic::take_hook()g + std::panic::set_hook(Box::new(|panic_info| { + let bt = Backtrace::new(); + LAST_PANIC.with(move |b| b.borrow_mut().replace((panic_info.to_string(), bt))); + })); - test_get_network_info(&cl); - unsafe { VERSION = cl.version().unwrap() }; + run_test!(test_get_network_info); println!("Version: {}", version()); - cl.create_wallet("testwallet", None, None, None, None).unwrap(); - - test_get_mining_info(&cl); - test_get_blockchain_info(&cl); - test_get_new_address(&cl); - test_get_raw_change_address(&cl); - test_dump_private_key(&cl); - test_generate(&cl); - test_get_balance_generate_to_address(&cl); - test_get_balances_generate_to_address(&cl); - test_get_best_block_hash(&cl); - test_get_block_count(&cl); - test_get_block_hash(&cl); - test_get_block(&cl); - test_get_block_header_get_block_header_info(&cl); - test_get_block_stats(&cl); - test_get_block_stats_fields(&cl); - test_get_address_info(&cl); - test_set_label(&cl); - test_send_to_address(&cl); - test_get_received_by_address(&cl); - test_list_unspent(&cl); - test_get_difficulty(&cl); - test_get_connection_count(&cl); - test_get_raw_transaction(&cl); - test_get_raw_mempool(&cl); - test_get_raw_mempool_verbose(&cl); - test_get_transaction(&cl); - test_list_transactions(&cl); - test_list_since_block(&cl); - test_get_tx_out(&cl); - test_get_tx_out_proof(&cl); - test_get_mempool_entry(&cl); - test_lock_unspent_unlock_unspent(&cl); - test_get_block_filter(&cl); - test_sign_raw_transaction_with_send_raw_transaction(&cl); - test_invalidate_block_reconsider_block(&cl); - test_key_pool_refill(&cl); - test_create_raw_transaction(&cl); - test_decode_raw_transaction(&cl); - test_fund_raw_transaction(&cl); - test_test_mempool_accept(&cl); - test_wallet_create_funded_psbt(&cl); - test_wallet_process_psbt(&cl); - test_join_psbt(&cl); - test_combine_psbt(&cl); - test_combine_raw_transaction(&cl); - test_create_psbt(&cl); - test_finalize_psbt(&cl); - test_list_received_by_address(&cl); - test_scantxoutset(&cl); - test_import_public_key(&cl); - test_import_priv_key(&cl); - test_import_address(&cl); - test_import_address_script(&cl); - test_estimate_smart_fee(&cl); - test_ping(&cl); - test_get_peer_info(&cl); - test_rescan_blockchain(&cl); - test_create_wallet(&cl); - test_get_tx_out_set_info(&cl); - test_get_chain_tips(&cl); - test_get_net_totals(&cl); - test_get_network_hash_ps(&cl); - test_uptime(&cl); - test_getblocktemplate(&cl); - test_unloadwallet(&cl); - test_loadwallet(&cl); - test_backupwallet(&cl); - test_wait_for_new_block(&cl); - test_wait_for_block(&cl); - test_get_descriptor_info(&cl); - test_derive_addresses(&cl); - test_get_mempool_info(&cl); - test_add_multisig_address(&cl); + CLIENT.create_wallet("testwallet", None, None, None, None).unwrap(); + + run_test!(test_get_mining_info); + run_test!(test_get_blockchain_info); + run_test!(test_get_new_address); + run_test!(test_get_raw_change_address); + run_test!(test_dump_private_key); + run_test!(test_generate); + run_test!(test_get_balance_generate_to_address); + run_test!(test_get_balances_generate_to_address); + run_test!(test_get_best_block_hash); + run_test!(test_get_block_count); + run_test!(test_get_block_hash); + run_test!(test_get_block); + run_test!(test_get_block_header_get_block_header_info); + run_test!(test_get_block_stats); + run_test!(test_get_block_stats_fields); + run_test!(test_get_address_info); + run_test!(test_set_label); + run_test!(test_send_to_address); + run_test!(test_get_received_by_address); + run_test!(test_list_unspent); + run_test!(test_get_difficulty); + run_test!(test_get_connection_count); + run_test!(test_get_raw_transaction); + run_test!(test_get_raw_mempool); + run_test!(test_get_raw_mempool_verbose); + run_test!(test_get_transaction); + run_test!(test_list_transactions); + run_test!(test_list_since_block); + run_test!(test_get_tx_out); + run_test!(test_get_tx_out_proof); + run_test!(test_get_mempool_entry); + run_test!(test_lock_unspent_unlock_unspent); + run_test!(test_get_block_filter); + run_test!(test_sign_raw_transaction_with_send_raw_transaction); + run_test!(test_invalidate_block_reconsider_block); + run_test!(test_key_pool_refill); + run_test!(test_create_raw_transaction); + run_test!(test_decode_raw_transaction); + run_test!(test_fund_raw_transaction); + run_test!(test_test_mempool_accept); + run_test!(test_wallet_create_funded_psbt); + run_test!(test_wallet_process_psbt); + run_test!(test_join_psbts); + run_test!(test_combine_psbt); + run_test!(test_combine_raw_transaction); + run_test!(test_create_psbt); + run_test!(test_finalize_psbt); + run_test!(test_list_received_by_address); + run_test!(test_scantxoutset); + run_test!(test_import_public_key); + run_test!(test_import_priv_key); + run_test!(test_import_address); + run_test!(test_import_address_script); + run_test!(test_estimate_smart_fee); + run_test!(test_ping); + run_test!(test_get_peer_info); + run_test!(test_rescan_blockchain); + run_test!(test_create_wallet); + run_test!(test_get_tx_out_set_info); + run_test!(test_get_chain_tips); + run_test!(test_get_net_totals); + run_test!(test_get_network_hash_ps); + run_test!(test_uptime); + run_test!(test_getblocktemplate); + run_test!(test_unloadwallet); + run_test!(test_loadwallet); + run_test!(test_backupwallet); + run_test!(test_wait_for_new_block); + run_test!(test_wait_for_block); + run_test!(test_get_descriptor_info); + run_test!(test_derive_addresses); + run_test!(test_get_mempool_info); + run_test!(test_add_multisig_address); //TODO import_multi( //TODO verify_message( //TODO encrypt_wallet(&self, passphrase: &str) -> Result<()> { //TODO get_by_id>( - test_add_node(&cl); - test_get_added_node_info(&cl); - test_get_node_addresses(&cl); - test_disconnect_node(&cl); - test_add_ban(&cl); - test_set_network_active(&cl); - test_get_index_info(&cl); - test_stop(cl); + run_test!(test_add_node); + run_test!(test_get_added_node_info); + run_test!(test_get_node_addresses); + run_test!(test_disconnect_node); + run_test!(test_add_ban); + run_test!(test_set_network_active); + run_test!(test_get_index_info); + run_test!(test_stop); + + // Print results + println!(""); + println!(""); + println!("Summary:"); + let mut error_count = 0; + for (name, success) in mem::replace(unsafe { &mut RESULTS }, Vec::new()).into_iter() { + if !success { + println!(" - {}: FAILED", name); + error_count += 1; + } else { + println!(" - {}: PASSED", name); + } + } + + println!(""); + + if error_count == 0 { + println!("All tests succesful!"); + } else { + println!("{} tests failed", error_count); + std::process::exit(1); + } } fn test_get_network_info(cl: &Client) { @@ -290,7 +374,8 @@ fn test_generate(cl: &Client) { fn test_get_balance_generate_to_address(cl: &Client) { let initial = cl.get_balance(None, None).unwrap(); - let blocks = cl.generate_to_address(500, &cl.get_new_address(None, None).unwrap().assume_checked()).unwrap(); + let addr = cl.get_new_address(None, None).unwrap().assume_checked(); + let blocks = cl.generate_to_address(500, &addr, None).unwrap(); assert_eq!(blocks.len(), 500); assert_ne!(cl.get_balance(None, None).unwrap(), initial); } @@ -299,7 +384,8 @@ fn test_get_balances_generate_to_address(cl: &Client) { if version() >= 190000 { let initial = cl.get_balances().unwrap(); - let blocks = cl.generate_to_address(500, &cl.get_new_address(None, None).unwrap().assume_checked()).unwrap(); + let addr = cl.get_new_address(None, None).unwrap().assume_checked(); + let blocks = cl.generate_to_address(500, &addr, None).unwrap(); assert_eq!(blocks.len(), 500); assert_ne!(cl.get_balances().unwrap(), initial); } @@ -418,20 +504,45 @@ fn test_set_label(cl: &Client) { fn test_send_to_address(cl: &Client) { let addr = cl.get_new_address(None, None).unwrap().assume_checked(); let est = json::EstimateMode::Conservative; - let _ = cl.send_to_address(&addr, btc(1), Some("cc"), None, None, None, None, None).unwrap(); - let _ = cl.send_to_address(&addr, btc(1), None, Some("tt"), None, None, None, None).unwrap(); - let _ = cl.send_to_address(&addr, btc(1), None, None, Some(true), None, None, None).unwrap(); - let _ = cl.send_to_address(&addr, btc(1), None, None, None, Some(true), None, None).unwrap(); - let _ = cl.send_to_address(&addr, btc(1), None, None, None, None, Some(3), None).unwrap(); - let _ = cl.send_to_address(&addr, btc(1), None, None, None, None, None, Some(est)).unwrap(); + let _ = cl.send_to_address( + &addr, btc(1), Some("cc"), None, None, None, None, None, None, None, + ).unwrap(); + let _ = cl.send_to_address( + &addr, btc(1), None, Some("tt"), None, None, None, None, None, None, + ).unwrap(); + let _ = cl.send_to_address( + &addr, btc(1), None, None, Some(true), None, None, None, None, None, + ).unwrap(); + let _ = cl.send_to_address( + &addr, btc(1), None, None, None, Some(true), None, None, None, None, + ).unwrap(); + let _ = cl.send_to_address( + &addr, btc(1), None, None, None, None, Some(3), None, None, None, + ).unwrap(); + let _ = cl.send_to_address( + &addr, btc(1), None, None, None, None, None, Some(est), None, None, + ).unwrap(); + if version() >= 19_00_00 { + let _ = cl.send_to_address( + &addr, btc(1), None, None, None, None, None, None, Some(false), None, + ).unwrap(); + } + if version() >= 21_00_00 { + let _ = cl.send_to_address( + &addr, btc(1), None, None, None, None, None, None, None, Some(sat_per_vb(1)), + ).unwrap(); + } } fn test_get_received_by_address(cl: &Client) { let addr = cl.get_new_address(None, None).unwrap().assume_checked(); - let _ = cl.send_to_address(&addr, btc(1), None, None, None, None, None, None).unwrap(); + let _ = cl.send_to_address( + &addr, btc(1), None, None, None, None, None, None, None, None, + ).unwrap(); assert_eq!(cl.get_received_by_address(&addr, Some(0)).unwrap(), btc(1)); assert_eq!(cl.get_received_by_address(&addr, Some(1)).unwrap(), btc(0)); - let _ = cl.generate_to_address(7, &cl.get_new_address(None, None).unwrap().assume_checked()).unwrap(); + let addr2 = cl.get_new_address(None, None).unwrap().assume_checked(); + let _ = cl.generate_to_address(7, &addr2, None).unwrap(); assert_eq!(cl.get_received_by_address(&addr, Some(6)).unwrap(), btc(1)); assert_eq!(cl.get_received_by_address(&addr, None).unwrap(), btc(1)); } @@ -439,19 +550,23 @@ fn test_get_received_by_address(cl: &Client) { fn test_list_unspent(cl: &Client) { let addr = cl.get_new_address(None, None).unwrap(); let addr_checked = addr.clone().assume_checked(); - let txid = cl.send_to_address(&addr.clone().assume_checked(), btc(1), None, None, None, None, None, None).unwrap(); - let unspent = cl.list_unspent(Some(0), None, Some(&[ &addr_checked]), None, None).unwrap(); + let txid = cl.send_to_address( + &addr.clone().assume_checked(), btc(1), None, None, None, None, None, None, None, None, + ).unwrap(); + let unspent = cl.list_unspent(Some(0), None, Some(&[addr_checked.clone()]), None, None).unwrap(); assert_eq!(unspent[0].txid, txid); assert_eq!(unspent[0].address.as_ref(), Some(&addr)); assert_eq!(unspent[0].amount, btc(1)); - let txid = cl.send_to_address(&addr_checked, btc(7), None, None, None, None, None, None).unwrap(); + let txid = cl.send_to_address( + &addr_checked, btc(7), None, None, None, None, None, None, None, None, + ).unwrap(); let options = json::ListUnspentQueryOptions { minimum_amount: Some(btc(7)), maximum_amount: Some(btc(7)), ..Default::default() }; - let unspent = cl.list_unspent(Some(0), None, Some(&[&addr_checked]), None, Some(options)).unwrap(); + let unspent = cl.list_unspent(Some(0), None, Some(&[addr_checked]), None, Some(&options)).unwrap(); assert_eq!(unspent.len(), 1); assert_eq!(unspent[0].txid, txid); assert_eq!(unspent[0].address.as_ref(), Some(&addr)); @@ -468,7 +583,9 @@ fn test_get_connection_count(cl: &Client) { fn test_get_raw_transaction(cl: &Client) { let addr = cl.get_new_address(None, None).unwrap().assume_checked(); - let txid = cl.send_to_address(&addr, btc(1), None, None, None, None, None, None).unwrap(); + let txid = cl.send_to_address( + &addr, btc(1), None, None, None, None, None, None, None, None, + ).unwrap(); let tx = cl.get_raw_transaction(&txid, None).unwrap(); let hex = cl.get_raw_transaction_hex(&txid, None).unwrap(); assert_eq!(tx, deserialize(&Vec::::from_hex(&hex).unwrap()).unwrap()); @@ -477,7 +594,8 @@ fn test_get_raw_transaction(cl: &Client) { let info = cl.get_raw_transaction_info(&txid, None).unwrap(); assert_eq!(info.txid, txid); - let blocks = cl.generate_to_address(7, &cl.get_new_address(None, None).unwrap().assume_checked()).unwrap(); + let addr = cl.get_new_address(None, None).unwrap().assume_checked(); + let blocks = cl.generate_to_address(7, &addr, None).unwrap(); let _ = cl.get_raw_transaction_info(&txid, Some(&blocks[0])).unwrap(); } @@ -486,16 +604,19 @@ fn test_get_raw_mempool(cl: &Client) { } fn test_get_raw_mempool_verbose(cl: &Client) { - cl.send_to_address(&RANDOM_ADDRESS, btc(1), None, None, None, None, None, None).unwrap(); - let _ = cl.get_raw_mempool_verbose().unwrap(); + cl.send_to_address( + &*RANDOM_ADDRESS, btc(1), None, None, None, None, None, None, None, None, + ).unwrap(); + let _ = cl.get_raw_mempool().unwrap(); // cleanup mempool transaction - cl.generate_to_address(2, &RANDOM_ADDRESS).unwrap(); + cl.generate_to_address(2, &*RANDOM_ADDRESS, None).unwrap(); } fn test_get_transaction(cl: &Client) { - let txid = - cl.send_to_address(&RANDOM_ADDRESS, btc(1), None, None, None, None, None, None).unwrap(); + let txid = cl.send_to_address( + &*RANDOM_ADDRESS, btc(1), None, None, None, None, None, None, None, None, + ).unwrap(); let tx = cl.get_transaction(&txid, None).unwrap(); assert_eq!(tx.amount, sbtc(-1.0)); assert_eq!(tx.info.txid, txid); @@ -519,8 +640,9 @@ fn test_list_since_block(cl: &Client) { } fn test_get_tx_out(cl: &Client) { - let txid = - cl.send_to_address(&RANDOM_ADDRESS, btc(1), None, None, None, None, None, None).unwrap(); + let txid = cl.send_to_address( + &*RANDOM_ADDRESS, btc(1), None, None, None, None, None, None, None, None, + ).unwrap(); let out = cl.get_tx_out(&txid, 0, Some(false)).unwrap(); assert!(out.is_none()); let out = cl.get_tx_out(&txid, 0, Some(true)).unwrap(); @@ -529,18 +651,22 @@ fn test_get_tx_out(cl: &Client) { } fn test_get_tx_out_proof(cl: &Client) { - let txid1 = - cl.send_to_address(&RANDOM_ADDRESS, btc(1), None, None, None, None, None, None).unwrap(); - let txid2 = - cl.send_to_address(&RANDOM_ADDRESS, btc(1), None, None, None, None, None, None).unwrap(); - let blocks = cl.generate_to_address(7, &cl.get_new_address(None, None).unwrap().assume_checked()).unwrap(); + let txid1 = cl.send_to_address( + &*RANDOM_ADDRESS, btc(1), None, None, None, None, None, None, None, None, + ).unwrap(); + let txid2 = cl.send_to_address( + &*RANDOM_ADDRESS, btc(1), None, None, None, None, None, None, None, None, + ).unwrap(); + let addr = cl.get_new_address(None, None).unwrap().assume_checked(); + let blocks = cl.generate_to_address(7, &addr, None).unwrap(); let proof = cl.get_tx_out_proof(&[txid1, txid2], Some(&blocks[0])).unwrap(); assert!(!proof.is_empty()); } fn test_get_mempool_entry(cl: &Client) { - let txid = - cl.send_to_address(&RANDOM_ADDRESS, btc(1), None, None, None, None, None, None).unwrap(); + let txid = cl.send_to_address( + &*RANDOM_ADDRESS, btc(1), None, None, None, None, None, None, None, None, + ).unwrap(); let entry = cl.get_mempool_entry(&txid).unwrap(); assert!(entry.spent_by.is_empty()); @@ -550,7 +676,9 @@ fn test_get_mempool_entry(cl: &Client) { fn test_lock_unspent_unlock_unspent(cl: &Client) { let addr = cl.get_new_address(None, None).unwrap().assume_checked(); - let txid = cl.send_to_address(&addr, btc(1), None, None, None, None, None, None).unwrap(); + let txid = cl.send_to_address( + &addr, btc(1), None, None, None, None, None, None, None, None, + ).unwrap(); assert!(cl.lock_unspent(&[OutPoint::new(txid, 0)]).unwrap()); assert!(cl.unlock_unspent(&[OutPoint::new(txid, 0)]).unwrap()); @@ -560,7 +688,8 @@ fn test_lock_unspent_unlock_unspent(cl: &Client) { } fn test_get_block_filter(cl: &Client) { - let blocks = cl.generate_to_address(7, &cl.get_new_address(None, None).unwrap().assume_checked()).unwrap(); + let addr = cl.get_new_address(None, None).unwrap().assume_checked(); + let blocks = cl.generate_to_address(7, &addr, None).unwrap(); if version() >= 190000 { let _ = cl.get_block_filter(&blocks[0]).unwrap(); } else { @@ -580,7 +709,7 @@ fn test_sign_raw_transaction_with_send_raw_transaction(cl: &Client) { minimum_amount: Some(btc(2)), ..Default::default() }; - let unspent = cl.list_unspent(Some(6), None, None, None, Some(options)).unwrap(); + let unspent = cl.list_unspent(Some(6), None, None::<&[Address]>, None, Some(&options)).unwrap(); let unspent = unspent.into_iter().nth(0).unwrap(); let tx = Transaction { @@ -608,7 +737,9 @@ fn test_sign_raw_transaction_with_send_raw_transaction(cl: &Client) { redeem_script: None, amount: Some(unspent.amount), }; - let res = cl.sign_raw_transaction_with_wallet(&tx, Some(&[input]), None).unwrap(); + let res = cl.sign_raw_transaction_with_wallet( + &tx, Some(&[input]), None::<&EcdsaSighashType>, + ).unwrap(); assert!(res.complete); let txid = cl.send_raw_transaction(&res.transaction().unwrap()).unwrap(); @@ -631,7 +762,7 @@ fn test_sign_raw_transaction_with_send_raw_transaction(cl: &Client) { }; let res = cl - .sign_raw_transaction_with_key(&tx, &[sk], None, Some(sighash::EcdsaSighashType::All.into())) + .sign_raw_transaction_with_key(&tx, &[sk], None, Some(&EcdsaSighashType::All)) .unwrap(); assert!(res.complete); let _ = cl.send_raw_transaction(&res.transaction().unwrap()).unwrap(); @@ -653,7 +784,7 @@ fn test_create_raw_transaction(cl: &Client) { minimum_amount: Some(btc(2)), ..Default::default() }; - let unspent = cl.list_unspent(Some(6), None, None, None, Some(options)).unwrap(); + let unspent = cl.list_unspent(Some(6), None, None::<&[Address]>, None, Some(&options)).unwrap(); let unspent = unspent.into_iter().nth(0).unwrap(); let input = json::CreateRawTransactionInput { @@ -661,12 +792,14 @@ fn test_create_raw_transaction(cl: &Client) { vout: unspent.vout, sequence: None, }; - let mut output = HashMap::new(); - output.insert(RANDOM_ADDRESS.to_string(), btc(1)); - - let tx = - cl.create_raw_transaction(&[input.clone()], &output, Some(500_000), Some(true)).unwrap(); - let hex = cl.create_raw_transaction_hex(&[input], &output, Some(500_000), Some(true)).unwrap(); + let output = (RANDOM_ADDRESS.clone(), btc(1)); + + let tx = cl.create_raw_transaction( + &[input.clone()], &[output.clone().into()], Some(500_000), Some(true), + ).unwrap(); + let hex = cl.create_raw_transaction_hex( + &[input], &[output.into()], Some(500_000), Some(true), + ).unwrap(); assert_eq!(tx, deserialize(&Vec::::from_hex(&hex).unwrap()).unwrap()); assert_eq!(hex, serialize_hex(&tx)); } @@ -676,7 +809,7 @@ fn test_decode_raw_transaction(cl: &Client) { minimum_amount: Some(btc(2)), ..Default::default() }; - let unspent = cl.list_unspent(Some(6), None, None, None, Some(options)).unwrap(); + let unspent = cl.list_unspent(Some(6), None, None::<&[Address]>, None, Some(&options)).unwrap(); let unspent = unspent.into_iter().nth(0).unwrap(); let input = json::CreateRawTransactionInput { @@ -684,14 +817,16 @@ fn test_decode_raw_transaction(cl: &Client) { vout: unspent.vout, sequence: None, }; - let mut output = HashMap::new(); - output.insert(RANDOM_ADDRESS.to_string(), btc(1)); + let output = (RANDOM_ADDRESS.clone(), btc(1)); - let tx = - cl.create_raw_transaction(&[input.clone()], &output, Some(500_000), Some(true)).unwrap(); - let hex = cl.create_raw_transaction_hex(&[input], &output, Some(500_000), Some(true)).unwrap(); + let tx = cl.create_raw_transaction( + &[input.clone()], &[output.clone().into()], Some(500_000), Some(true), + ).unwrap(); + let hex = cl.create_raw_transaction_hex( + &[input], &[output.clone().into()], Some(500_000), Some(true), + ).unwrap(); - let decoded_transaction = cl.decode_raw_transaction(hex, None).unwrap(); + let decoded_transaction = cl.decode_raw_transaction(&hex, None).unwrap(); assert_eq!(tx.txid(), decoded_transaction.txid); assert_eq!(500_000, decoded_transaction.locktime); @@ -702,8 +837,7 @@ fn test_decode_raw_transaction(cl: &Client) { fn test_fund_raw_transaction(cl: &Client) { let addr = cl.get_new_address(None, None).unwrap().assume_checked(); - let mut output = HashMap::new(); - output.insert(RANDOM_ADDRESS.to_string(), btc(1)); + let output = (RANDOM_ADDRESS.clone(), btc(1)); let options = json::FundRawTransactionOptions { add_inputs: None, @@ -712,14 +846,16 @@ fn test_fund_raw_transaction(cl: &Client) { change_type: None, include_watching: Some(true), lock_unspents: Some(true), - fee_rate: Some(*FEE), + fee_rate: Some(*FEERATE), subtract_fee_from_outputs: Some(vec![0]), replaceable: Some(true), conf_target: None, estimate_mode: None, }; - let tx = cl.create_raw_transaction_hex(&[], &output, Some(500_000), Some(true)).unwrap(); - let funded = cl.fund_raw_transaction(tx, Some(&options), Some(false)).unwrap(); + let tx = cl.create_raw_transaction_hex( + &[], &[output.clone().into()], Some(500_000), Some(true), + ).unwrap(); + let funded = cl.fund_raw_transaction(&tx, Some(&options), Some(false)).unwrap(); let _ = funded.transaction().unwrap(); let options = json::FundRawTransactionOptions { @@ -735,8 +871,10 @@ fn test_fund_raw_transaction(cl: &Client) { conf_target: Some(2), estimate_mode: Some(json::EstimateMode::Conservative), }; - let tx = cl.create_raw_transaction_hex(&[], &output, Some(500_000), Some(true)).unwrap(); - let funded = cl.fund_raw_transaction(tx, Some(&options), Some(false)).unwrap(); + let tx = cl.create_raw_transaction_hex( + &[], &[output.into()], Some(500_000), Some(true), + ).unwrap(); + let funded = cl.fund_raw_transaction(&tx, Some(&options), Some(false)).unwrap(); let _ = funded.transaction().unwrap(); } @@ -745,7 +883,7 @@ fn test_test_mempool_accept(cl: &Client) { minimum_amount: Some(btc(2)), ..Default::default() }; - let unspent = cl.list_unspent(Some(6), None, None, None, Some(options)).unwrap(); + let unspent = cl.list_unspent(Some(6), None, None::<&[Address]>, None, Some(&options)).unwrap(); let unspent = unspent.into_iter().nth(0).unwrap(); let input = json::CreateRawTransactionInput { @@ -753,17 +891,18 @@ fn test_test_mempool_accept(cl: &Client) { vout: unspent.vout, sequence: Some(0xFFFFFFFF), }; - let mut output = HashMap::new(); - output.insert(RANDOM_ADDRESS.to_string(), unspent.amount - *FEE); + let output = (RANDOM_ADDRESS.clone(), unspent.amount - *FEE); - let tx = - cl.create_raw_transaction(&[input.clone()], &output, Some(500_000), Some(false)).unwrap(); - let res = cl.test_mempool_accept(&[&tx]).unwrap(); + let tx = cl.create_raw_transaction( + &[input.clone()], &[output.into()], Some(500_000), Some(false) + ).unwrap(); + let res = cl.test_mempool_accept(&[tx.clone()], None).unwrap(); assert!(!res[0].allowed); assert!(res[0].reject_reason.is_some()); - let signed = - cl.sign_raw_transaction_with_wallet(&tx, None, None).unwrap().transaction().unwrap(); - let res = cl.test_mempool_accept(&[&signed]).unwrap(); + let signed =cl.sign_raw_transaction_with_wallet( + &tx, None, None::<&EcdsaSighashType>, + ).unwrap().transaction().unwrap(); + let res = cl.test_mempool_accept(&[signed], None).unwrap(); assert!(res[0].allowed, "not allowed: {:?}", res[0].reject_reason); } @@ -773,16 +912,15 @@ fn test_wallet_create_funded_psbt(cl: &Client) { minimum_amount: Some(btc(2)), ..Default::default() }; - let unspent = cl.list_unspent(Some(6), None, None, None, Some(options)).unwrap(); - let unspent = unspent.into_iter().nth(0).unwrap(); + let unspent = cl.list_unspent(Some(6), None, None::<&[Address]>, None, Some(&options)).unwrap(); + let first_unspent = unspent.into_iter().nth(0).unwrap(); let input = json::CreateRawTransactionInput { - txid: unspent.txid, - vout: unspent.vout, + txid: first_unspent.txid, + vout: first_unspent.vout, sequence: None, }; - let mut output = HashMap::new(); - output.insert(RANDOM_ADDRESS.to_string(), btc(1)); + let output = (RANDOM_ADDRESS.clone(), first_unspent.amount - *FEE); let options = json::WalletCreateFundedPsbtOptions { add_inputs: None, @@ -791,21 +929,24 @@ fn test_wallet_create_funded_psbt(cl: &Client) { change_type: Some(json::AddressType::Legacy), include_watching: Some(true), lock_unspent: Some(true), - fee_rate: Some(*FEE), + fee_rate: if version() >= 21_00_00 { + Some(*FEERATE) + } else { + None + }, subtract_fee_from_outputs: vec![0], replaceable: Some(true), conf_target: None, estimate_mode: None, }; - let _ = cl - .wallet_create_funded_psbt( - &[input.clone()], - &output, - Some(500_000), - Some(options), - Some(true), - ) - .unwrap(); + let _ = cl.wallet_create_funded_psbt( + &[input.clone()], + &[output.clone().into()], + Some(500_000), + Some(&options), + Some(true), + ) + .unwrap(); let options = json::WalletCreateFundedPsbtOptions { add_inputs: None, @@ -820,9 +961,9 @@ fn test_wallet_create_funded_psbt(cl: &Client) { conf_target: Some(3), estimate_mode: Some(json::EstimateMode::Conservative), }; - let psbt = cl - .wallet_create_funded_psbt(&[input], &output, Some(500_000), Some(options), Some(true)) - .unwrap(); + let psbt = cl.wallet_create_funded_psbt( + &[input], &[output.into()], Some(500_000), Some(&options), Some(true), + ).unwrap(); assert!(!psbt.psbt.is_empty()); } @@ -831,40 +972,56 @@ fn test_wallet_process_psbt(cl: &Client) { minimum_amount: Some(btc(2)), ..Default::default() }; - let unspent = cl.list_unspent(Some(6), None, None, None, Some(options)).unwrap(); + let unspent = cl.list_unspent(Some(6), None, None::<&[Address]>, None, Some(&options)).unwrap(); let unspent = unspent.into_iter().nth(0).unwrap(); let input = json::CreateRawTransactionInput { txid: unspent.txid, vout: unspent.vout, sequence: None, }; - let mut output = HashMap::new(); - output.insert(RANDOM_ADDRESS.to_string(), btc(1)); - let psbt = cl - .wallet_create_funded_psbt(&[input.clone()], &output, Some(500_000), None, Some(true)) - .unwrap(); + let output = (RANDOM_ADDRESS.clone(), unspent.amount - *FEE); + let psbt = cl.wallet_create_funded_psbt( + &[input.clone()], &[output.clone().into()], Some(500_000), None, Some(true), + ).unwrap(); + println!("psbt: {:?}", psbt); + + // Check that the API doesn't allow empty sighashtype when bip32derivs is set. + //TODO(stevenroose) this seems like a bug in core, it's supposed to be optional + let res = cl.wallet_process_psbt( + &psbt.psbt, Some(true), None, Some(true), + ); + assert!(match res.unwrap_err() { + Error::InvalidArguments(_) => true, + _ => false, + }); - let res = cl.wallet_process_psbt(&psbt.psbt, Some(true), None, Some(true)).unwrap(); + let sighashtype = if version() >= 18_00_00 && version() <= 22_00_00 { + Some(EcdsaSighashType::All.into()) + } else { + None + }; + let res = cl.wallet_process_psbt( + &psbt.psbt, Some(true), sighashtype.as_ref(), Some(true), + ).unwrap(); assert!(res.complete); } -fn test_join_psbt(cl: &Client) { +fn test_join_psbts(cl: &Client) { let options = json::ListUnspentQueryOptions { minimum_amount: Some(btc(2)), ..Default::default() }; - let unspent = cl.list_unspent(Some(6), None, None, None, Some(options)).unwrap(); - let unspent1 = unspent[0].clone(); + let unspent = cl.list_unspent(Some(6), None, None::<&[Address]>, None, Some(&options)).unwrap(); + let first_unspent = unspent[0].clone(); let input = json::CreateRawTransactionInput { - txid: unspent1.txid, - vout: unspent1.vout, + txid: first_unspent.txid, + vout: first_unspent.vout, sequence: None, }; - let mut output = HashMap::new(); - output.insert(RANDOM_ADDRESS.to_string(), btc(1)); - let psbt1 = cl - .wallet_create_funded_psbt(&[input.clone()], &output, Some(500_000), None, Some(true)) - .unwrap(); + let output = (RANDOM_ADDRESS.clone(), first_unspent.amount - *FEE); + let psbt1 = cl.wallet_create_funded_psbt( + &[input.clone()], &[output.clone().into()], Some(500_000), None, Some(true), + ).unwrap(); let unspent = unspent.into_iter().nth(1).unwrap(); let input2 = json::CreateRawTransactionInput { @@ -872,14 +1029,14 @@ fn test_join_psbt(cl: &Client) { vout: unspent.vout, sequence: None, }; - let mut output2 = HashMap::new(); - output2.insert(RANDOM_ADDRESS.to_string(), btc(1)); - let psbt2 = cl - .wallet_create_funded_psbt(&[input2.clone()], &output, Some(500_000), None, Some(true)) - .unwrap(); + let output2 = (RANDOM_ADDRESS.clone(), unspent.amount - *FEE); + let psbt2 = cl.wallet_create_funded_psbt( + &[input2.clone()], &[output2.clone().into()], Some(500_000), None, Some(true), + ).unwrap(); - let psbt = cl.join_psbt(&[psbt1.psbt, psbt2.psbt]).unwrap(); + let psbt = cl.join_psbts_raw(&[psbt1.psbt.clone(), psbt2.psbt.clone()]).unwrap(); assert!(!psbt.is_empty()); + let _ = cl.join_psbts(&[psbt1.psbt, psbt2.psbt]).unwrap(); } fn test_combine_psbt(cl: &Client) { @@ -887,21 +1044,21 @@ fn test_combine_psbt(cl: &Client) { minimum_amount: Some(btc(2)), ..Default::default() }; - let unspent = cl.list_unspent(Some(6), None, None, None, Some(options)).unwrap(); - let unspent = unspent.into_iter().nth(0).unwrap(); + let unspent = cl.list_unspent(Some(6), None, None::<&[Address]>, None, Some(&options)).unwrap(); + let first_unspent = unspent.into_iter().nth(0).unwrap(); let input = json::CreateRawTransactionInput { - txid: unspent.txid, - vout: unspent.vout, + txid: first_unspent.txid, + vout: first_unspent.vout, sequence: None, }; - let mut output = HashMap::new(); - output.insert(RANDOM_ADDRESS.to_string(), btc(1)); - let psbt1 = cl - .wallet_create_funded_psbt(&[input.clone()], &output, Some(500_000), None, Some(true)) - .unwrap(); + let output = (RANDOM_ADDRESS.clone(), first_unspent.amount - *FEE); + let psbt1 = cl.wallet_create_funded_psbt( + &[input.clone()], &[output.clone().into()], Some(500_000), None, Some(true), + ).unwrap(); - let psbt = cl.combine_psbt(&[psbt1.psbt.clone(), psbt1.psbt]).unwrap(); + let psbt = cl.combine_psbt_raw(&[psbt1.psbt.clone(), psbt1.psbt.clone()]).unwrap(); assert!(!psbt.is_empty()); + let _ = cl.combine_psbt(&[psbt1.psbt.clone(), psbt1.psbt.clone()]).unwrap(); } fn test_combine_raw_transaction(cl: &Client) { @@ -909,20 +1066,21 @@ fn test_combine_raw_transaction(cl: &Client) { minimum_amount: Some(btc(2)), ..Default::default() }; - let unspent = cl.list_unspent(Some(6), None, None, None, Some(options)).unwrap(); - let unspent = unspent.into_iter().nth(0).unwrap(); + let unspent = cl.list_unspent(Some(6), None, None::<&[Address]>, None, Some(&options)).unwrap(); + let first_unspent = unspent.into_iter().nth(0).unwrap(); let input = json::CreateRawTransactionInput { - txid: unspent.txid, - vout: unspent.vout, + txid: first_unspent.txid, + vout: first_unspent.vout, sequence: None, }; - let mut output = HashMap::new(); - output.insert(RANDOM_ADDRESS.to_string(), btc(1)); - let tx = cl.create_raw_transaction_hex(&[input.clone()], &output, Some(500_000), None).unwrap(); - - let transaction = cl.combine_raw_transaction(&[tx.clone(), tx]).unwrap(); + let output = (RANDOM_ADDRESS.clone(), btc(1)); + let tx = cl.create_raw_transaction_hex( + &[input.clone()], &[output.clone().into()], Some(500_000), None, + ).unwrap(); + let transaction = cl.combine_raw_transaction_hex(&[tx.clone(), tx.clone()]).unwrap(); assert!(!transaction.is_empty()); + let _ = cl.combine_raw_transaction(&[tx.clone(), tx.clone()]).unwrap(); } fn test_create_psbt(cl: &Client) { @@ -930,7 +1088,7 @@ fn test_create_psbt(cl: &Client) { minimum_amount: Some(btc(2)), ..Default::default() }; - let unspent = cl.list_unspent(Some(6), None, None, None, Some(options)).unwrap(); + let unspent = cl.list_unspent(Some(6), None, None::<&[Address]>, None, Some(&options)).unwrap(); let unspent = unspent.into_iter().nth(0).unwrap(); let input = json::CreateRawTransactionInput { @@ -938,10 +1096,11 @@ fn test_create_psbt(cl: &Client) { vout: unspent.vout, sequence: None, }; - let mut output = HashMap::new(); - output.insert(RANDOM_ADDRESS.to_string(), btc(1)); + let output = (RANDOM_ADDRESS.clone(), btc(1)); - let _ = cl.create_psbt(&[input], &output, Some(500_000), Some(true)).unwrap(); + let _ = cl.create_psbt( + &[input], &[output.clone().into()], Some(500_000), Some(true), + ).unwrap(); } fn test_finalize_psbt(cl: &Client) { @@ -949,18 +1108,17 @@ fn test_finalize_psbt(cl: &Client) { minimum_amount: Some(btc(2)), ..Default::default() }; - let unspent = cl.list_unspent(Some(6), None, None, None, Some(options)).unwrap(); + let unspent = cl.list_unspent(Some(6), None, None::<&[Address]>, None, Some(&options)).unwrap(); let unspent = unspent.into_iter().nth(0).unwrap(); let input = json::CreateRawTransactionInput { txid: unspent.txid, vout: unspent.vout, sequence: None, }; - let mut output = HashMap::new(); - output.insert(RANDOM_ADDRESS.to_string(), btc(1)); - let psbt = cl - .wallet_create_funded_psbt(&[input.clone()], &output, Some(500_000), None, Some(true)) - .unwrap(); + let output = (RANDOM_ADDRESS.clone(), unspent.amount - *FEE); + let psbt = cl.wallet_create_funded_psbt( + &[input.clone()], &[output.clone().into()], Some(500_000), None, Some(true), + ).unwrap(); let res = cl.finalize_psbt(&psbt.psbt, Some(true)).unwrap(); assert!(!res.complete); @@ -970,12 +1128,14 @@ fn test_finalize_psbt(cl: &Client) { fn test_list_received_by_address(cl: &Client) { let addr = cl.get_new_address(None, None).unwrap().assume_checked(); - let txid = cl.send_to_address(&addr, btc(1), None, None, None, None, None, None).unwrap(); + let txid = cl.send_to_address( + &addr, btc(1), None, None, None, None, None, None, None, None, + ).unwrap(); let _ = cl.list_received_by_address(Some(&addr), None, None, None).unwrap(); let _ = cl.list_received_by_address(Some(&addr), None, Some(true), None).unwrap(); let _ = cl.list_received_by_address(Some(&addr), None, None, Some(true)).unwrap(); - let _ = cl.list_received_by_address(None, Some(200), None, None).unwrap(); + let _ = cl.list_received_by_address(None::<&Address>, Some(200), None, None).unwrap(); let res = cl.list_received_by_address(Some(&addr), Some(0), None, None).unwrap(); assert_eq!(res[0].txids, vec![txid]); @@ -1043,7 +1203,7 @@ fn test_estimate_smart_fee(cl: &Client) { } assert!(res.fee_rate.is_some(), "no fee estimate available: {:?}", res.errors); - assert!(res.fee_rate.unwrap() >= btc(0)); + assert!(res.fee_rate.unwrap() >= sat_per_vb(1)); } fn test_ping(cl: &Client) { @@ -1118,26 +1278,28 @@ fn test_create_wallet(cl: &Client) { } for wallet_param in wallet_params { - let result = cl - .create_wallet( - wallet_param.name, - wallet_param.disable_private_keys, - wallet_param.blank, - wallet_param.passphrase, - wallet_param.avoid_reuse, - ) - .unwrap(); + let result = cl.create_wallet( + wallet_param.name, + wallet_param.disable_private_keys, + wallet_param.blank, + wallet_param.passphrase, + wallet_param.avoid_reuse, + ) + .unwrap(); assert_eq!(result.name, wallet_param.name); let expected_warning = match (wallet_param.passphrase, wallet_param.avoid_reuse) { - (None, Some(true)) => { + (None, Some(true)) => if version() < 19_00_00 { Some("Empty string given as passphrase, wallet will not be encrypted.".to_string()) - } + } else { + Some("".to_string()) + }, _ => Some("".to_string()), }; assert_eq!(result.warning, expected_warning); - let wallet_client = new_wallet_client(wallet_param.name); + let wallet_client_url = format!("{}{}{}", get_rpc_url(), "/wallet/", wallet_param.name); + let wallet_client = make_client(&wallet_client_url); let wallet_info = wallet_client.get_wallet_info().unwrap(); assert_eq!(wallet_info.wallet_name, wallet_param.name); @@ -1174,7 +1336,7 @@ fn test_create_wallet(cl: &Client) { } fn test_get_tx_out_set_info(cl: &Client) { - cl.get_tx_out_set_info(None, None, None).unwrap(); + cl.get_tx_out_set_info(None, None::, None).unwrap(); } fn test_get_chain_tips(cl: &Client) { @@ -1186,17 +1348,17 @@ fn test_add_node(cl: &Client) { cl.add_node("127.0.0.1:1234").unwrap(); assert_error_message!(cl.add_node("127.0.0.1:1234"), -23, "Error: Node already added"); cl.remove_node("127.0.0.1:1234").unwrap(); - cl.onetry_node("127.0.0.1:1234").unwrap(); + cl.add_node_onetry("127.0.0.1:1234").unwrap(); } fn test_get_added_node_info(cl: &Client) { cl.add_node("127.0.0.1:1234").unwrap(); cl.add_node("127.0.0.1:4321").unwrap(); - assert!(cl.get_added_node_info(Some("127.0.0.1:1111")).is_err()); - assert_eq!(cl.get_added_node_info(None).unwrap().len(), 2); - assert_eq!(cl.get_added_node_info(Some("127.0.0.1:1234")).unwrap().len(), 1); - assert_eq!(cl.get_added_node_info(Some("127.0.0.1:4321")).unwrap().len(), 1); + assert!(cl.get_added_node_info("127.0.0.1:1111").is_err()); + assert_eq!(cl.get_added_nodes_info().unwrap().len(), 2); + assert_eq!(cl.get_added_node_info("127.0.0.1:1234").unwrap().len(), 1); + assert_eq!(cl.get_added_node_info("127.0.0.1:4321").unwrap().len(), 1); } fn test_get_node_addresses(cl: &Client) { @@ -1251,9 +1413,9 @@ fn test_uptime(cl: &Client) { fn test_scantxoutset(cl: &Client) { let addr = cl.get_new_address(None, None).unwrap().assume_checked(); - - cl.generate_to_address(2, &addr).unwrap(); - cl.generate_to_address(7, &cl.get_new_address(None, None).unwrap().assume_checked()).unwrap(); + cl.generate_to_address(2, &addr, None).unwrap(); + let addr2 = cl.get_new_address(None, None).unwrap().assume_checked(); + cl.generate_to_address(7, &addr2, None).unwrap(); let utxos = cl .scan_tx_out_set_blocking(&[ScanTxOutRequest::Single(format!("addr({})", addr))]) @@ -1268,13 +1430,13 @@ fn test_getblocktemplate(cl: &Client) { // contains an entry in the vector of GetBlockTemplateResultTransaction. // Otherwise the GetBlockTemplateResultTransaction deserialization wouldn't // be tested. - cl.send_to_address(&RANDOM_ADDRESS, btc(1), None, None, None, None, None, None).unwrap(); + cl.send_to_address(&*RANDOM_ADDRESS, btc(1), None, None, None, None, None, None, None, None).unwrap(); cl.get_block_template(GetBlockTemplateModes::Template, &[GetBlockTemplateRules::SegWit], &[]) .unwrap(); // cleanup mempool transaction - cl.generate_to_address(2, &RANDOM_ADDRESS).unwrap(); + cl.generate_to_address(2, &*RANDOM_ADDRESS, None).unwrap(); } fn test_unloadwallet(cl: &Client) { @@ -1285,9 +1447,9 @@ fn test_unloadwallet(cl: &Client) { .unwrap(); if version() >= 210000 { - assert!(res.is_some()); + assert!(res.warning.is_some()); } else { - assert!(res.is_none()); + assert!(res.warning.is_none()); } } @@ -1309,27 +1471,27 @@ fn test_backupwallet(_: &Client) { let wallet_client = new_wallet_client("testbackupwallet"); let backup_path = format!("{}/testbackupwallet.dat", get_testdir()); - assert!(wallet_client.backup_wallet(None).is_err()); - assert!(wallet_client.backup_wallet(Some(&backup_path)).is_err()); + // assert!(wallet_client.backup_wallet(None).is_err()); + assert!(wallet_client.backup_wallet(&backup_path).is_err()); wallet_client.create_wallet("testbackupwallet", None, None, None, None).unwrap(); - assert!(wallet_client.backup_wallet(None).is_err()); - assert!(wallet_client.backup_wallet(Some(&backup_path)).is_ok()); + // assert!(wallet_client.backup_wallet(None).is_err()); + assert!(wallet_client.backup_wallet(&backup_path).is_ok()); } fn test_wait_for_new_block(cl: &Client) { let height = cl.get_block_count().unwrap(); let hash = cl.get_block_hash(height).unwrap(); - assert!(cl.wait_for_new_block(std::u64::MAX).is_err()); // JSON integer out of range - assert_eq!(cl.wait_for_new_block(100).unwrap(), json::BlockRef{hash, height}); + assert!(cl.wait_for_new_block(Some(std::u64::MAX)).is_err()); // JSON integer out of range + assert_eq!(cl.wait_for_new_block(Some(100)).unwrap(), json::BlockRef{hash, height}); } fn test_wait_for_block(cl: &Client) { let height = cl.get_block_count().unwrap(); let hash = cl.get_block_hash(height).unwrap(); - assert!(cl.wait_for_block(&hash, std::u64::MAX).is_err()); // JSON integer out of range - assert_eq!(cl.wait_for_block(&hash, 0).unwrap(), json::BlockRef{hash, height}); + assert!(cl.wait_for_block(&hash, Some(std::u64::MAX)).is_err()); // JSON integer out of range + assert_eq!(cl.wait_for_block(&hash, Some(0)).unwrap(), json::BlockRef{hash, height}); } fn test_get_descriptor_info(cl: &Client) { @@ -1369,15 +1531,17 @@ fn test_add_multisig_address(cl: &Client) { fn test_derive_addresses(cl: &Client) { let descriptor = r"pkh(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)#62k9sn4x"; - assert_eq!(cl.derive_addresses(descriptor, None).unwrap(), vec!["mrkwtj5xpYQjHeJe5wsweNjVeTKkvR5fCr".parse().unwrap()]); - assert!(cl.derive_addresses(descriptor, Some([0, 1])).is_err()); // Range should not be specified for an unranged descriptor + assert_eq!(cl.derive_addresses(descriptor, None).unwrap(), vec![ + "mrkwtj5xpYQjHeJe5wsweNjVeTKkvR5fCr".parse::().unwrap(), + ]); + assert!(cl.derive_addresses(descriptor, Some(&[0, 1])).is_err()); // Range should not be specified for an unranged descriptor let descriptor = std::concat!( r"wpkh([1004658e/84'/1'/0']tpubDCBEcmVKbfC9KfdydyLbJ2gfNL88grZu1XcWSW9ytTM6fi", r"tvaRmVyr8Ddf7SjZ2ZfMx9RicjYAXhuh3fmLiVLPodPEqnQQURUfrBKiiVZc8/0/*)#g8l47ngv", ); - assert_eq!(cl.derive_addresses(descriptor, Some([0, 1])).unwrap(), vec![ - "bcrt1q5n5tjkpva8v5s0uadu2y5f0g7pn4h5eqaq2ux2".parse().unwrap(), + assert_eq!(cl.derive_addresses(descriptor, Some(&[0, 1])).unwrap(), vec![ + "bcrt1q5n5tjkpva8v5s0uadu2y5f0g7pn4h5eqaq2ux2".parse::().unwrap(), "bcrt1qcgl303ht03ja2e0hudpwk7ypcxk5t478wspzlt".parse().unwrap(), ]); assert!(cl.derive_addresses(descriptor, None).is_err()); // Range must be specified for a ranged descriptor @@ -1422,6 +1586,6 @@ fn test_get_index_info(cl: &Client) { } } -fn test_stop(cl: Client) { +fn test_stop(cl: &Client) { println!("Stopping: '{}'", cl.stop().unwrap()); } diff --git a/json/Cargo.toml b/json/Cargo.toml index 109ab2d8..eb22b9ae 100644 --- a/json/Cargo.toml +++ b/json/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bitcoincore-rpc-json" -version = "0.17.0" +version = "1.0.0-rc0" authors = [ "Steven Roose ", "Jean Pierre Dudey ", @@ -22,5 +22,5 @@ path = "src/lib.rs" serde = { version = "1", features = [ "derive" ] } serde_json = "1" -bitcoin = { version = "0.30.0", features = ["serde", "rand-std"]} +bitcoin = { version = "0.30.0", features = [ "base64", "serde", "rand-std" ]} bitcoin-private = "0.1.0" diff --git a/json/src/lib.rs b/json/src/lib.rs index f6b13675..c067143a 100644 --- a/json/src/lib.rs +++ b/json/src/lib.rs @@ -22,55 +22,133 @@ pub extern crate bitcoin; extern crate serde; extern crate serde_json; +use std::fmt; +use std::borrow::Cow; use std::collections::HashMap; use bitcoin::address::NetworkUnchecked; use bitcoin::block::Version; -use bitcoin::consensus::encode; -use bitcoin::hashes::hex::FromHex; +use bitcoin::consensus::encode::{self, Decodable}; +use bitcoin::hashes::hex::HexIterator; use bitcoin::hashes::sha256; -use bitcoin::{Address, Amount, PrivateKey, PublicKey, SignedAmount, Transaction, ScriptBuf, Script, bip158, bip32, Network}; -use serde::de::Error as SerdeError; -use serde::{Deserialize, Serialize}; -use std::fmt; +use bitcoin::psbt::{PartiallySignedTransaction, PsbtParseError}; +use bitcoin::{bip158, bip32}; +use bitcoin::{ + Address, Amount, PrivateKey, PublicKey, Script, ScriptBuf, SignedAmount, + Transaction, FeeRate, Network, +}; +use bitcoin::blockdata::fee_rate::serde as serde_feerate; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use serde::ser::SerializeMap; //TODO(stevenroose) consider using a Time type +/// Outputs hex into an object implementing `fmt::Write`. +/// +/// This is usually more efficient than going through a `String` using [`ToHex`]. +// NB taken from bitcoin_hashes::hex +fn format_hex(data: &[u8], f: &mut fmt::Formatter) -> fmt::Result { + let prec = f.precision().unwrap_or(2 * data.len()); + let width = f.width().unwrap_or(2 * data.len()); + for _ in (2 * data.len())..width { + f.write_str("0")?; + } + for ch in data.iter().take(prec / 2) { + write!(f, "{:02x}", *ch)?; + } + if prec < 2 * data.len() && prec % 2 == 1 { + write!(f, "{:x}", data[prec / 2] / 16)?; + } + Ok(()) +} + +/// A wrapper for an argument to be serialized as hex. +struct HexSerializeWrapper<'a>(pub &'a [u8]); + +impl<'a> fmt::Display for HexSerializeWrapper<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + format_hex(self.0, f) + } +} + +impl<'a> Serialize for HexSerializeWrapper<'a> { + fn serialize(&self, serializer: S) -> Result { + serializer.collect_str(&self) + } +} + /// A module used for serde serialization of bytes in hexadecimal format. /// /// The module is compatible with the serde attribute. pub mod serde_hex { use bitcoin::hashes::hex::FromHex; - use bitcoin_private::hex::exts::DisplayHex; - use serde::de::Error; - use serde::{Deserializer, Serializer}; + use serde::de::{Error as SerdeError, Deserialize, Deserializer}; + use serde::ser::Serializer; + + use super::HexSerializeWrapper; + + struct HexVisitor; + + impl<'de> serde::de::Visitor<'de> for HexVisitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { + formatter.write_str("an ASCII hex string") + } + + fn visit_str(self, s: &str) -> Result { + FromHex::from_hex(s).map_err(E::custom) + } + + fn visit_string(self, s: String) -> Result { + FromHex::from_hex(&s).map_err(E::custom) + } + } pub fn serialize(b: &Vec, s: S) -> Result { - s.serialize_str(&b.to_lower_hex_string()) + s.collect_str(&HexSerializeWrapper(&b[..])) } pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { - let hex_str: String = ::serde::Deserialize::deserialize(d)?; - Ok(FromHex::from_hex(&hex_str).map_err(D::Error::custom)?) + d.deserialize_any(HexVisitor) } pub mod opt { - use bitcoin::hashes::hex::FromHex; - use bitcoin_private::hex::exts::DisplayHex; - use serde::de::Error; - use serde::{Deserializer, Serializer}; + use super::*; pub fn serialize(b: &Option>, s: S) -> Result { match *b { None => s.serialize_none(), - Some(ref b) => s.serialize_str(&b.to_lower_hex_string()), + Some(ref b) => s.collect_str(&HexSerializeWrapper(&b[..])), } } pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result>, D::Error> { - let hex_str: String = ::serde::Deserialize::deserialize(d)?; - Ok(Some(FromHex::from_hex(&hex_str).map_err(D::Error::custom)?)) + Ok(Some(d.deserialize_any(HexVisitor)?)) + } + } + + pub mod vec { + use super::*; + + pub mod opt { + use super::*; + + pub fn deserialize<'de, D>(d: D) -> Result>>, D::Error> + where + D: serde::Deserializer<'de>, + { + //TODO(stevenroose) Revisit when issue is fixed: + // https://github.com/serde-rs/serde/issues/723 + + let v: Vec = Vec::deserialize(d)?; + let mut res = Vec::new(); + for h in v.into_iter() { + res.push(FromHex::from_hex(&h).map_err(D::Error::custom)?); + } + Ok(Some(res)) + } } } } @@ -135,11 +213,55 @@ pub struct LoadWalletResult { pub warning: Option, } -#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +#[derive(Clone, PartialEq, Eq, Debug, Serialize)] pub struct UnloadWalletResult { pub warning: Option, } +// implement custom parsing to support legacy result that was just a string or null +impl<'de> Deserialize<'de> for UnloadWalletResult { + fn deserialize>(deserializer: D) -> Result { + struct Visitor; + impl<'de> de::Visitor<'de> for Visitor { + type Value = UnloadWalletResult; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "result for unloadwallet rpc") + } + + fn visit_unit(self) -> Result { + Ok(UnloadWalletResult { + warning: None, + }) + } + + fn visit_string(self, v: String) -> Result { + Ok(UnloadWalletResult { + warning: Some(v), + }) + } + + fn visit_str(self, v: &str) -> Result { + Ok(UnloadWalletResult { + warning: Some(v.to_owned()), + }) + } + + fn visit_map>(self, mut map: A) -> Result { + // only look for the "warning" field + let mut warning = None; + while let Some(key) = map.next_key::<&str>()? { + if key == "warning" { + warning = Some(map.next_value()?); + } + } + Ok(UnloadWalletResult { warning }) + } + } + deserializer.deserialize_any(Visitor) + } +} + #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] pub struct ListWalletDirResult { pub wallets: Vec, @@ -249,8 +371,8 @@ pub struct GetBlockHeaderResult { pub struct GetBlockStatsResult { #[serde(rename = "avgfee", with = "bitcoin::amount::serde::as_sat")] pub avg_fee: Amount, - #[serde(rename = "avgfeerate", with = "bitcoin::amount::serde::as_sat")] - pub avg_fee_rate: Amount, + #[serde(rename = "avgfeerate", with = "serde_feerate::sat_per_vb")] + pub avg_fee_rate: FeeRate, #[serde(rename = "avgtxsize")] pub avg_tx_size: u32, #[serde(rename = "blockhash")] @@ -261,8 +383,8 @@ pub struct GetBlockStatsResult { pub ins: usize, #[serde(rename = "maxfee", with = "bitcoin::amount::serde::as_sat")] pub max_fee: Amount, - #[serde(rename = "maxfeerate", with = "bitcoin::amount::serde::as_sat")] - pub max_fee_rate: Amount, + #[serde(rename = "maxfeerate", with = "serde_feerate::sat_per_vb")] + pub max_fee_rate: FeeRate, #[serde(rename = "maxtxsize")] pub max_tx_size: u32, #[serde(rename = "medianfee", with = "bitcoin::amount::serde::as_sat")] @@ -273,8 +395,8 @@ pub struct GetBlockStatsResult { pub median_tx_size: u32, #[serde(rename = "minfee", with = "bitcoin::amount::serde::as_sat")] pub min_fee: Amount, - #[serde(rename = "minfeerate", with = "bitcoin::amount::serde::as_sat")] - pub min_fee_rate: Amount, + #[serde(rename = "minfeerate", with = "serde_feerate::sat_per_vb")] + pub min_fee_rate: FeeRate, #[serde(rename = "mintxsize")] pub min_tx_size: u32, pub outs: usize, @@ -310,10 +432,10 @@ pub struct GetBlockStatsResultPartial { #[serde( default, rename = "avgfeerate", - with = "bitcoin::amount::serde::as_sat::opt", + with = "serde_feerate::sat_per_vb::opt", skip_serializing_if = "Option::is_none" )] - pub avg_fee_rate: Option, + pub avg_fee_rate: Option, #[serde(default, rename = "avgtxsize", skip_serializing_if = "Option::is_none")] pub avg_tx_size: Option, #[serde(default, rename = "blockhash", skip_serializing_if = "Option::is_none")] @@ -334,10 +456,10 @@ pub struct GetBlockStatsResultPartial { #[serde( default, rename = "maxfeerate", - with = "bitcoin::amount::serde::as_sat::opt", + with = "serde_feerate::sat_per_vb::opt", skip_serializing_if = "Option::is_none" )] - pub max_fee_rate: Option, + pub max_fee_rate: Option, #[serde(default, rename = "maxtxsize", skip_serializing_if = "Option::is_none")] pub max_tx_size: Option, #[serde( @@ -361,10 +483,10 @@ pub struct GetBlockStatsResultPartial { #[serde( default, rename = "minfeerate", - with = "bitcoin::amount::serde::as_sat::opt", + with = "serde_feerate::sat_per_vb::opt", skip_serializing_if = "Option::is_none" )] - pub min_fee_rate: Option, + pub min_fee_rate: Option, #[serde(default, rename = "mintxsize", skip_serializing_if = "Option::is_none")] pub min_tx_size: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -410,16 +532,16 @@ pub struct GetBlockStatsResultPartial { #[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] pub struct FeeRatePercentiles { - #[serde(with = "bitcoin::amount::serde::as_sat")] - pub fr_10th: Amount, - #[serde(with = "bitcoin::amount::serde::as_sat")] - pub fr_25th: Amount, - #[serde(with = "bitcoin::amount::serde::as_sat")] - pub fr_50th: Amount, - #[serde(with = "bitcoin::amount::serde::as_sat")] - pub fr_75th: Amount, - #[serde(with = "bitcoin::amount::serde::as_sat")] - pub fr_90th: Amount, + #[serde(with = "serde_feerate::sat_per_vb")] + pub fr_10th: FeeRate, + #[serde(with = "serde_feerate::sat_per_vb")] + pub fr_25th: FeeRate, + #[serde(with = "serde_feerate::sat_per_vb")] + pub fr_50th: FeeRate, + #[serde(with = "serde_feerate::sat_per_vb")] + pub fr_75th: FeeRate, + #[serde(with = "serde_feerate::sat_per_vb")] + pub fr_90th: FeeRate, } #[derive(Clone)] @@ -497,9 +619,9 @@ impl fmt::Display for BlockStatsFields { } } -impl From for serde_json::Value { - fn from(bsf: BlockStatsFields) -> Self { - Self::from(bsf.to_string()) +impl Serialize for BlockStatsFields { + fn serialize(&self, s: S) -> Result { + s.collect_str(&self) } } @@ -535,7 +657,7 @@ impl GetRawTransactionResultVinScriptSig { } } -#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +#[derive(Clone, PartialEq, Eq, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetRawTransactionResultVin { pub sequence: u32, @@ -549,7 +671,7 @@ pub struct GetRawTransactionResultVin { /// The scriptSig in case of a non-coinbase tx. pub script_sig: Option, /// Not provided for coinbase txs. - #[serde(default, deserialize_with = "deserialize_hex_array_opt")] + #[serde(default, with = "serde_hex::vec::opt")] pub txinwitness: Option>>, } @@ -594,7 +716,7 @@ pub struct GetRawTransactionResultVout { pub script_pub_key: GetRawTransactionResultVoutScriptPubKey, } -#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +#[derive(Clone, PartialEq, Eq, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct GetRawTransactionResult { #[serde(rename = "in_active_chain")] @@ -1231,7 +1353,6 @@ impl<'de> serde::Deserialize<'de> for Timestamp { where D: serde::Deserializer<'de>, { - use serde::de; struct Visitor; impl<'de> de::Visitor<'de> for Visitor { type Value = Timestamp; @@ -1480,10 +1601,10 @@ pub struct EstimateSmartFeeResult { #[serde( default, rename = "feerate", - skip_serializing_if = "Option::is_none", - with = "bitcoin::amount::serde::as_btc::opt" + with = "serde_feerate::btc_per_kvb::opt", + skip_serializing_if = "Option::is_none" )] - pub fee_rate: Option, + pub fee_rate: Option, /// Errors encountered during processing. pub errors: Option>, /// Block number where estimate was found. @@ -1712,6 +1833,12 @@ pub struct WalletCreateFundedPsbtResult { pub change_position: i32, } +impl WalletCreateFundedPsbtResult { + pub fn psbt(&self) -> Result { + self.psbt.parse() + } +} + /// Models the result of "walletprocesspsbt" #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] pub struct WalletProcessPsbtResult { @@ -1719,6 +1846,12 @@ pub struct WalletProcessPsbtResult { pub complete: bool, } +impl WalletProcessPsbtResult { + pub fn psbt(&self) -> Result { + self.psbt.parse() + } +} + /// Models the request for "walletcreatefundedpsbt" #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize, Default)] pub struct WalletCreateFundedPsbtOptions { @@ -1737,11 +1870,10 @@ pub struct WalletCreateFundedPsbtOptions { #[serde(rename = "lockUnspents", skip_serializing_if = "Option::is_none")] pub lock_unspent: Option, #[serde( - rename = "feeRate", skip_serializing_if = "Option::is_none", - with = "bitcoin::amount::serde::as_btc::opt" + with = "serde_feerate::sat_per_vb::opt" )] - pub fee_rate: Option, + pub fee_rate: Option, #[serde(rename = "subtractFeeFromOutputs", skip_serializing_if = "Vec::is_empty")] pub subtract_fee_from_outputs: Vec, #[serde(skip_serializing_if = "Option::is_none")] @@ -1756,13 +1888,20 @@ pub struct WalletCreateFundedPsbtOptions { #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] pub struct FinalizePsbtResult { pub psbt: Option, - #[serde(default, with = "crate::serde_hex::opt")] - pub hex: Option>, + pub hex: Option, pub complete: bool, } +impl FinalizePsbtResult { + pub fn transaction(&self) -> Option> { + self.hex.as_ref().map(|h| Transaction::consensus_decode( + &mut HexIterator::new(h).map_err(|_| encode::Error::ParseFailed("invalid hex"))? + )) + } +} + /// Model for decode transaction -#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +#[derive(Clone, PartialEq, Eq, Debug, Deserialize)] pub struct DecodeRawTransactionResult { pub txid: bitcoin::Txid, pub hash: bitcoin::Wtxid, @@ -1810,12 +1949,6 @@ pub enum GetChainTipsResultStatus { Active, } -impl FinalizePsbtResult { - pub fn transaction(&self) -> Option> { - self.hex.as_ref().map(|h| encode::deserialize(h)) - } -} - // Custom types for input arguments. #[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, PartialEq, Hash)] @@ -1828,7 +1961,8 @@ pub enum EstimateMode { /// A wrapper around bitcoin::EcdsaSighashType that will be serialized /// according to what the RPC expects. -pub struct SigHashType(bitcoin::sighash::EcdsaSighashType); +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct SigHashType(pub bitcoin::sighash::EcdsaSighashType); impl From for SigHashType { fn from(sht: bitcoin::sighash::EcdsaSighashType) -> SigHashType { @@ -1852,7 +1986,7 @@ impl serde::Serialize for SigHashType { } } -// Used for createrawtransaction argument. +/// Used for createrawtransaction input arguments. #[derive(Serialize, Clone, PartialEq, Eq, Debug)] #[serde(rename_all = "camelCase")] pub struct CreateRawTransactionInput { @@ -1862,6 +1996,55 @@ pub struct CreateRawTransactionInput { pub sequence: Option, } +/// Used for createrawtransaction output arguments. +/// +/// Has implementations of [From] for the following types: +/// - `(Address, Amount)` for regular outputs +/// - `Vec` for nulldata/OP_RETURN outputs +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum CreateRawTransactionOutput<'a> { + Address(Address, Amount), + Data(Cow<'a, [u8]>), +} + +impl<'a> From<(Address, Amount)> for CreateRawTransactionOutput<'a> { + fn from(pair: (Address, Amount)) -> CreateRawTransactionOutput<'a> { + CreateRawTransactionOutput::Address(pair.0, pair.1) + } +} + +impl<'a> From<&'a [u8]> for CreateRawTransactionOutput<'a> { + fn from(data: &'a [u8]) -> CreateRawTransactionOutput<'a> { + CreateRawTransactionOutput::Data(Cow::Borrowed(data)) + } +} + +impl<'a> From> for CreateRawTransactionOutput<'a> { + fn from(data: Vec) -> CreateRawTransactionOutput<'a> { + CreateRawTransactionOutput::Data(Cow::Owned(data)) + } +} + +impl<'a> serde::Serialize for CreateRawTransactionOutput<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut map = serializer.serialize_map(None)?; + match self { + CreateRawTransactionOutput::Address(addr, amt) => { + map.serialize_key(addr)?; + map.serialize_value(&amt.to_btc())?; + } + CreateRawTransactionOutput::Data(data) => { + map.serialize_key("data")?; + map.serialize_value(&HexSerializeWrapper(data.as_ref()))?; + } + } + map.end() + } +} + #[derive(Serialize, Clone, PartialEq, Eq, Debug, Default)] #[serde(rename_all = "camelCase")] pub struct FundRawTransactionOptions { @@ -1880,10 +2063,11 @@ pub struct FundRawTransactionOptions { #[serde(skip_serializing_if = "Option::is_none")] pub lock_unspents: Option, #[serde( - with = "bitcoin::amount::serde::as_btc::opt", + rename = "feeRate", + with = "serde_feerate::btc_per_kvb::opt", skip_serializing_if = "Option::is_none" )] - pub fee_rate: Option, + pub fee_rate: Option, #[serde(skip_serializing_if = "Option::is_none")] pub subtract_fee_from_outputs: Option>, #[serde(skip_serializing_if = "Option::is_none")] @@ -1954,14 +2138,6 @@ pub enum TxOutSetHashType { None, } -/// Used to specify a block hash or a height -#[derive(Clone, Serialize, PartialEq, Eq, Debug)] -#[serde(untagged)] -pub enum HashOrHeight { - BlockHash(bitcoin::BlockHash), - Height(u64), -} - #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] pub struct GetTxOutSetInfoResult { /// The block height (index) of the returned statistics @@ -2156,22 +2332,6 @@ impl<'a> serde::Serialize for PubKeyOrAddress<'a> { // Custom deserializer functions. -/// deserialize_hex_array_opt deserializes a vector of hex-encoded byte arrays. -fn deserialize_hex_array_opt<'de, D>(deserializer: D) -> Result>>, D::Error> -where - D: serde::Deserializer<'de>, -{ - //TODO(stevenroose) Revisit when issue is fixed: - // https://github.com/serde-rs/serde/issues/723 - - let v: Vec = Vec::deserialize(deserializer)?; - let mut res = Vec::new(); - for h in v.into_iter() { - res.push(FromHex::from_hex(&h).map_err(D::Error::custom)?); - } - Ok(Some(res)) -} - /// deserialize_bip70_network deserializes a Bitcoin Core network according to BIP70 /// The accepted input variants are: {"main", "test", "signet", "regtest"} fn deserialize_bip70_network<'de, D>(deserializer: D) -> Result diff --git a/todo.md b/todo.md new file mode 100644 index 00000000..e6cb8aa5 --- /dev/null +++ b/todo.md @@ -0,0 +1,11 @@ + + + +- Generic optional arguments are quite inergonomic because the [None] variant requires a generic argument: + F.e. `Option::<&bitcoin::EcdsaSighashType>::None`. + This counts for all `Option` in arguments. + (Sighash type is more often used optionally, maybe we can make our own type for that and not need generics?) + +- Fix logging + +- Should hashes arguments be references or values? diff --git a/usage_examples/Cargo.toml b/usage_examples/Cargo.toml new file mode 100644 index 00000000..410f29ba --- /dev/null +++ b/usage_examples/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "usage_examples" +version = "0.0.0" +edition = "2021" + +[dependencies] +bitcoincore_rpc = { path = "../client" } +jsonrpc = { git = "https://github.com/stevenroose/rust-jsonrpc", branch = "request", features = [ "tp-hyper" ] } +hyper = { version = "0.14.0", features = [ "client", "http1", "tcp" ] } diff --git a/usage_examples/src/main.rs b/usage_examples/src/main.rs new file mode 100644 index 00000000..bece2c7f --- /dev/null +++ b/usage_examples/src/main.rs @@ -0,0 +1,20 @@ + +use bitcoincore_rpc::{Client, AsyncClient}; + +#[allow(unused)] +async fn test() { + let jsonrpc = jsonrpc::Client::with_hyper( + hyper::Client::new(), + "http://localhost".into(), + Some("user".into()), + Some("pass".into()), + ); + let client = Client::from_jsonrpc(jsonrpc); + + + let ret = client.get_blockchain_info().await.unwrap(); + println!("{}", ret.blocks); +} + +fn main() { +}