diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index e817bf1..570ccc3 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -19,6 +19,10 @@ jobs: - esplora-reqwest - compiler - compact_filters + - reserves + - reserves,electrum + - reserves,esplora-ureq + - reserves,compact_filters steps: - name: Checkout uses: actions/checkout@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8191e71..6924377 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - Replace `wallet bump_fee` command `--send_all` with new `--shrink` option +- Add 'reserve' feature to enable proof of reserve ## [0.3.0] diff --git a/Cargo.lock b/Cargo.lock index f11d6cb..2abd305 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,6 +114,7 @@ dependencies = [ "base64 0.11.0", "bdk", "bdk-macros", + "bdk-reserves", "clap", "dirs-next", "env_logger", @@ -137,6 +138,18 @@ dependencies = [ "syn", ] +[[package]] +name = "bdk-reserves" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e5def53134305f5c44dfd5b1b94609fc194d983d0b816e5ca7635d44a9d6dd" +dependencies = [ + "base64 0.11.0", + "bdk", + "bitcoinconsensus", + "log", +] + [[package]] name = "bech32" version = "0.8.1" @@ -202,6 +215,16 @@ dependencies = [ "serde", ] +[[package]] +name = "bitcoinconsensus" +version = "0.19.0-3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a8aa43b5cd02f856cb126a9af819e77b8910fdd74dd1407be649f2f5fe3a1b5" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "bitcoincore-rpc" version = "0.14.0" diff --git a/Cargo.toml b/Cargo.toml index 17ed9ee..2abd9d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ env_logger = { version = "0.7", optional = true } clap = { version = "2.33", optional = true } regex = { version = "1", optional = true } fd-lock = { version = "=3.0.0", optional = true } +bdk-reserves = { version = "0.14.2", optional = true} [features] default = ["cli", "repl"] @@ -39,6 +40,7 @@ esplora-reqwest = ["esplora", "bdk/use-esplora-reqwest"] compiler = ["bdk/compiler"] compact_filters = ["bdk/compact_filters"] rpc = ["bdk/rpc"] +reserves = ["bdk-reserves"] [[bin]] name = "bdk-cli" diff --git a/src/bdk_cli.rs b/src/bdk_cli.rs index 4c39ff9..f2eac5e 100644 --- a/src/bdk_cli.rs +++ b/src/bdk_cli.rs @@ -407,6 +407,24 @@ fn handle_command(cli_opts: CliOpts, network: Network) -> Result // rl.save_history("history.txt").unwrap(); "Exiting REPL".to_string() } + #[cfg(all(feature = "reserves", feature = "electrum"))] + CliSubCommand::ExternalReserves { + message, + psbt, + confirmations, + addresses, + electrum_opts, + } => { + let result = bdk_cli::handle_ext_reserves_subcommand( + network, + message, + psbt, + confirmations, + addresses, + electrum_opts, + )?; + serde_json::to_string_pretty(&result)? + } }; Ok(result) } diff --git a/src/lib.rs b/src/lib.rs index 27287d9..0959af2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -122,6 +122,8 @@ use crate::OfflineWalletSubCommand::*; feature = "rpc" ))] use crate::OnlineWalletSubCommand::*; +#[cfg(all(feature = "reserves", feature = "electrum"))] +use bdk::bitcoin::blockdata::transaction::TxOut; use bdk::bitcoin::consensus::encode::{deserialize, serialize, serialize_hex}; #[cfg(any( feature = "electrum", @@ -134,6 +136,8 @@ use bdk::bitcoin::secp256k1::Secp256k1; use bdk::bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey, KeySource}; use bdk::bitcoin::util::psbt::PartiallySignedTransaction; use bdk::bitcoin::{Address, Network, OutPoint, Script, Txid}; +#[cfg(feature = "reserves")] +use bdk::blockchain::Capability; #[cfg(any( feature = "electrum", feature = "esplora", @@ -145,6 +149,8 @@ use bdk::database::BatchDatabase; use bdk::descriptor::Segwitv0; #[cfg(feature = "compiler")] use bdk::descriptor::{Descriptor, Legacy, Miniscript}; +#[cfg(all(feature = "reserves", feature = "electrum"))] +use bdk::electrum_client::{Client, ElectrumApi}; use bdk::keys::bip39::{Language, Mnemonic, WordCount}; use bdk::keys::DescriptorKey::Secret; use bdk::keys::KeyError::{InvalidNetwork, Message}; @@ -156,6 +162,16 @@ use bdk::wallet::AddressIndex; use bdk::Error; use bdk::SignOptions; use bdk::{FeeRate, KeychainKind, Wallet}; +#[cfg(all(feature = "reserves", feature = "electrum"))] +use bdk_reserves::reserves::verify_proof; +#[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "compact_filters", + feature = "rpc" +))] +#[cfg(feature = "reserves")] +use bdk_reserves::reserves::ProofOfReserves; /// Global options /// @@ -308,6 +324,25 @@ pub enum CliSubCommand { #[structopt(flatten)] wallet_opts: WalletOpts, }, + /// Proof of reserves external sub-commands + #[cfg(all(feature = "reserves", feature = "electrum"))] + #[structopt(long_about = "Proof of reserves external verification")] + ExternalReserves { + /// Sets the challenge message with which the proof was produced + #[structopt(name = "MESSAGE", required = true, index = 1)] + message: String, + /// Sets the proof in form of a PSBT to verify + #[structopt(name = "PSBT", required = true, index = 2)] + psbt: String, + /// Sets the number of block confirmations for UTXOs to be considered. + #[structopt(name = "CONFIRMATIONS", required = true, index = 3)] + confirmations: usize, + /// Sets the addresses for which the proof was produced + #[structopt(name = "ADDRESSES", required = true, index = 4)] + addresses: Vec, + #[structopt(flatten)] + electrum_opts: ElectrumOpts, + }, } /// Wallet sub-commands @@ -740,6 +775,9 @@ pub enum OfflineWalletSubCommand { /// Assume the blockchain has reached a specific height. This affects the transaction finalization, if there are timelocks in the descriptor #[structopt(name = "HEIGHT", long = "assume_height")] assume_height: Option, + /// Whether the signer should trust the witness_utxo, if the non_witness_utxo hasn’t been provided + #[structopt(name = "WITNESS", long = "trust_witness_utxo")] + trust_witness_utxo: Option, }, /// Extracts a raw transaction from a PSBT ExtractPsbt { @@ -755,6 +793,9 @@ pub enum OfflineWalletSubCommand { /// Assume the blockchain has reached a specific height #[structopt(name = "HEIGHT", long = "assume_height")] assume_height: Option, + /// Whether the signer should trust the witness_utxo, if the non_witness_utxo hasn’t been provided + #[structopt(name = "WITNESS", long = "trust_witness_utxo")] + trust_witness_utxo: Option, }, /// Combines multiple PSBTs into one CombinePsbt { @@ -809,6 +850,26 @@ pub enum OnlineWalletSubCommand { )] tx: Option, }, + /// Produce a proof of reserves + #[cfg(feature = "reserves")] + ProduceProof { + /// Sets the message + #[structopt(name = "MESSAGE", long = "message")] + msg: String, + }, + /// Verify a proof of reserves for our wallet + #[cfg(feature = "reserves")] + VerifyProof { + /// Sets the PSBT to verify + #[structopt(name = "BASE64_PSBT", long = "psbt")] + psbt: String, + /// Sets the message to verify + #[structopt(name = "MESSAGE", long = "message")] + msg: String, + /// Sets the number of block confirmations for UTXOs to be considered. If nothing is specified, 6 is used. + #[structopt(name = "CONFIRMATIONS", long = "confirmations", default_value = "6")] + confirmations: u32, + }, } fn parse_recipient(s: &str) -> Result<(Script, u64), String> { @@ -971,11 +1032,14 @@ where Sign { psbt, assume_height, + trust_witness_utxo, } => { - let psbt = base64::decode(&psbt).unwrap(); - let mut psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap(); + let psbt = base64::decode(&psbt) + .map_err(|e| Error::Generic(format!("Base64 decode error: {:?}", e)))?; + let mut psbt: PartiallySignedTransaction = deserialize(&psbt)?; let signopt = SignOptions { assume_height, + trust_witness_utxo: trust_witness_utxo.unwrap_or(false), ..Default::default() }; let finalized = wallet.sign(&mut psbt, signopt)?; @@ -995,12 +1059,14 @@ where FinalizePsbt { psbt, assume_height, + trust_witness_utxo, } => { let psbt = base64::decode(&psbt).unwrap(); let mut psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap(); let signopt = SignOptions { assume_height, + trust_witness_utxo: trust_witness_utxo.unwrap_or(false), ..Default::default() }; let finalized = wallet.finalize_psbt(&mut psbt, signopt)?; @@ -1076,6 +1142,52 @@ where let txid = maybe_await!(wallet.broadcast(&tx))?; Ok(json!({ "txid": txid })) } + #[cfg(feature = "reserves")] + ProduceProof { msg } => { + let mut psbt = maybe_await!(wallet.create_proof(&msg))?; + + let _finalized = wallet.sign( + &mut psbt, + SignOptions { + trust_witness_utxo: true, + ..Default::default() + }, + )?; + + let psbt_ser = serialize(&psbt); + let psbt_b64 = base64::encode(&psbt_ser); + + Ok(json!({ "psbt": psbt , "psbt_base64" : psbt_b64})) + } + #[cfg(feature = "reserves")] + VerifyProof { + psbt, + msg, + confirmations, + } => { + let psbt = base64::decode(&psbt).unwrap(); + let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap(); + let current_height = wallet.client().get_height()?; + let max_confirmation_height = if confirmations == 0 { + None + } else { + if !wallet + .client() + .get_capabilities() + .contains(&Capability::GetAnyTx) + { + return Err(Error::Generic( + "For validating a proof with a certain number of confirmations, we need a Blockchain with the GetAnyTx capability." + .to_string() + )); + } + Some(current_height - confirmations) + }; + + let spendable = + maybe_await!(wallet.verify_proof(&psbt, &msg, max_confirmation_height))?; + Ok(json!({ "spendable": spendable })) + } } } @@ -1224,6 +1336,81 @@ pub fn handle_compile_subcommand( Ok(json!({"descriptor": descriptor.to_string()})) } +/// Proof of reserves verification sub-command +/// +/// Proof of reserves options are described in [`CliSubCommand::ExternalReserves`]. +#[cfg(all(feature = "reserves", feature = "electrum"))] +pub fn handle_ext_reserves_subcommand( + network: Network, + message: String, + psbt: String, + confirmations: usize, + addresses: Vec, + electrum_opts: ElectrumOpts, +) -> Result { + let psbt = base64::decode(&psbt) + .map_err(|e| Error::Generic(format!("Base64 decode error: {:?}", e)))?; + let psbt: PartiallySignedTransaction = deserialize(&psbt)?; + let client = Client::new(&electrum_opts.server)?; + + let current_block_height = client.block_headers_subscribe().map(|data| data.height)?; + let max_confirmation_height = Some(current_block_height - confirmations); + + let outpoints_per_addr = addresses + .iter() + .map(|address| { + let address = Address::from_str(&address) + .map_err(|e| Error::Generic(format!("Invalid address: {:?}", e)))?; + get_outpoints_for_address(address, &client, max_confirmation_height) + }) + .collect::>, Error>>()?; + let outpoints_combined = outpoints_per_addr + .iter() + .fold(Vec::new(), |mut outpoints, outs| { + outpoints.append(&mut outs.clone()); + outpoints + }); + + let spendable = verify_proof(&psbt, &message, outpoints_combined, network) + .map_err(|e| Error::Generic(format!("{:?}", e)))?; + + Ok(json!({ "spendable": spendable })) +} + +#[cfg(all(feature = "reserves", feature = "electrum"))] +pub fn get_outpoints_for_address( + address: Address, + client: &Client, + max_confirmation_height: Option, +) -> Result, Error> { + let unspents = client + .script_list_unspent(&address.script_pubkey()) + .map_err(Error::Electrum)?; + + unspents + .iter() + .filter(|utxo| { + utxo.height > 0 && utxo.height <= max_confirmation_height.unwrap_or(usize::MAX) + }) + .map(|utxo| { + let tx = match client.transaction_get(&utxo.tx_hash) { + Ok(tx) => tx, + Err(e) => { + return Err(e).map_err(Error::Electrum); + } + }; + + Ok(( + OutPoint { + txid: utxo.tx_hash, + vout: utxo.tx_pos as u32, + }, + tx.output[utxo.tx_pos].clone(), + )) + }) + .collect() +} + #[cfg(test)] mod test { use super::{CliOpts, WalletOpts}; @@ -1236,6 +1423,10 @@ mod test { #[cfg(feature = "esplora")] use crate::EsploraOpts; use crate::OfflineWalletSubCommand::{BumpFee, CreateTx, GetNewAddress}; + #[cfg(all(feature = "reserves", feature = "compact_filters"))] + use crate::OnlineWalletSubCommand::ProduceProof; + #[cfg(all(feature = "reserves", feature = "esplora-ureq"))] + use crate::OnlineWalletSubCommand::VerifyProof; #[cfg(any( feature = "electrum", feature = "esplora", @@ -1247,12 +1438,22 @@ mod test { use crate::ProxyOpts; #[cfg(feature = "rpc")] use crate::RpcOpts; + #[cfg(all(feature = "reserves", feature = "electrum"))] + use crate::{handle_ext_reserves_subcommand, handle_online_wallet_subcommand}; use crate::{handle_key_subcommand, CliSubCommand, KeySubCommand, WalletSubCommand}; - use bdk::bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey}; + #[cfg(all(feature = "reserves", feature = "electrum"))] + use bdk::bitcoin::{consensus::Encodable, util::psbt::PartiallySignedTransaction}; use bdk::bitcoin::{Address, Network, OutPoint}; use bdk::miniscript::bitcoin::network::constants::Network::Testnet; - use std::str::FromStr; + #[cfg(all(feature = "reserves", feature = "electrum"))] + use bdk::{ + blockchain::{noop_progress, ElectrumBlockchain}, + database::MemoryDatabase, + electrum_client::Client, + Wallet, + }; + use std::str::{self, FromStr}; use structopt::StructOpt; #[test] @@ -1941,4 +2142,353 @@ mod test { &"sh(wsh(thresh(3,pk(Alice),s:pk(Bob),s:pk(Carol),sdv:older(2))))#l4qaawgv" ); } + + #[cfg(all(feature = "reserves", feature = "compact_filters"))] + #[test] + fn test_parse_produce_proof() { + let message = "Those coins belong to Satoshi Nakamoto"; + let cli_args = vec![ + "bdk-cli", + "--network", + "bitcoin", + "wallet", + "--descriptor", + "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)", + "produce_proof", + "--message", + message.clone(), + ]; + + let cli_opts = CliOpts::from_iter(&cli_args); + + let expected_cli_opts = CliOpts { + network: Network::Bitcoin, + subcommand: CliSubCommand::Wallet { + wallet_opts: WalletOpts { + wallet: "main".to_string(), + verbose: false, + descriptor: "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)" + .to_string(), + change_descriptor: None, + compactfilter_opts: CompactFilterOpts { + address: vec!["127.0.0.1:18444".to_string()], + conn_count: 4, + skip_blocks: 0, + }, + proxy_opts: ProxyOpts { + proxy: None, + proxy_auth: None, + retries: 5, + }, + }, + subcommand: WalletSubCommand::OnlineWalletSubCommand(ProduceProof { + msg: message.to_string(), + }), + }, + }; + + assert_eq!(expected_cli_opts, cli_opts); + } + + #[cfg(all(feature = "reserves", feature = "esplora-ureq"))] + #[test] + fn test_parse_verify_proof_internal() { + let psbt = r#"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"#; + let message = "Those coins belong to Satoshi Nakamoto"; + let cli_args = vec![ + "bdk-cli", + "--network", + "bitcoin", + "wallet", + "--descriptor", + "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)", + "verify_proof", + "--psbt", + psbt.clone(), + "--message", + message.clone(), + ]; + + let cli_opts = CliOpts::from_iter(&cli_args); + + let expected_cli_opts = CliOpts { + network: Network::Bitcoin, + subcommand: CliSubCommand::Wallet { + wallet_opts: WalletOpts { + wallet: "main".to_string(), + verbose: false, + descriptor: "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)" + .to_string(), + change_descriptor: None, + esplora_opts: EsploraOpts { + server: "https://blockstream.info/testnet/api/".to_string(), + read_timeout: 5, + write_timeout: 5, + stop_gap: 10, + }, + proxy_opts: ProxyOpts { + proxy: None, + proxy_auth: None, + retries: 5, + }, + }, + subcommand: WalletSubCommand::OnlineWalletSubCommand(VerifyProof { + psbt: psbt.to_string(), + msg: message.to_string(), + confirmations: 6, + }), + }, + }; + + assert_eq!(expected_cli_opts, cli_opts); + } + + #[cfg(all(feature = "reserves", feature = "esplora-ureq"))] + #[test] + fn test_parse_verify_proof_internal_confirmation() { + let psbt = r#"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"#; + let message = "Those coins belong to Satoshi Nakamoto"; + let cli_args = vec![ + "bdk-cli", + "--network", + "bitcoin", + "wallet", + "--descriptor", + "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)", + "verify_proof", + "--psbt", + psbt.clone(), + "--message", + message.clone(), + "--confirmations", + "0", + ]; + + let cli_opts = CliOpts::from_iter(&cli_args); + + let expected_cli_opts = CliOpts { + network: Network::Bitcoin, + subcommand: CliSubCommand::Wallet { + wallet_opts: WalletOpts { + wallet: "main".to_string(), + verbose: false, + descriptor: "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)" + .to_string(), + change_descriptor: None, + esplora_opts: EsploraOpts { + server: "https://blockstream.info/testnet/api/".to_string(), + read_timeout: 5, + write_timeout: 5, + stop_gap: 10, + }, + proxy_opts: ProxyOpts { + proxy: None, + proxy_auth: None, + retries: 5, + }, + }, + subcommand: WalletSubCommand::OnlineWalletSubCommand(VerifyProof { + psbt: psbt.to_string(), + msg: message.to_string(), + confirmations: 0, + }), + }, + }; + + assert_eq!(expected_cli_opts, cli_opts); + } + + #[cfg(all(feature = "reserves", feature = "electrum"))] + #[test] + fn test_parse_verify_proof_external() { + let psbt = r#"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"#.to_string(); + let address = "tb1qanjjv4cs20dgv32vncrxw702l8g4qtn2m9wn7d".to_string(); + let message = "Those coins belong to Satoshi Nakamoto".to_string(); + let cli_args = vec![ + "bdk-cli", + "--network", + "bitcoin", + "external_reserves", + &message, + &psbt, + "6", + &address, + "--server", + "ssl://electrum.blockstream.info:60002", + ]; + + let cli_opts = CliOpts::from_iter(&cli_args); + + let expected_cli_opts = CliOpts { + network: Network::Bitcoin, + subcommand: CliSubCommand::ExternalReserves { + message, + psbt, + confirmations: 6, + addresses: [address].to_vec(), + electrum_opts: ElectrumOpts { + timeout: None, + server: "ssl://electrum.blockstream.info:60002".to_string(), + stop_gap: 10, + }, + }, + }; + + assert_eq!(expected_cli_opts, cli_opts); + } + + /// Encodes a partially signed transaction as base64 and returns the bytes of the resulting string. + #[cfg(all(feature = "reserves", feature = "electrum"))] + fn encode_psbt(psbt: PartiallySignedTransaction) -> Vec { + let mut encoded = Vec::::new(); + psbt.consensus_encode(&mut encoded).unwrap(); + let base64_psbt = base64::encode(&encoded); + + base64_psbt.as_bytes().to_vec() + } + + #[cfg(all(feature = "reserves", feature = "electrum"))] + #[test] + fn test_proof_of_reserves_wallet() { + let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)".to_string(); + let message = "Those coins belong to Satoshi Nakamoto"; + + let client = Client::new("ssl://electrum.blockstream.info:60002").unwrap(); + let wallet = Wallet::new( + &descriptor, + None, + Network::Testnet, + MemoryDatabase::default(), + ElectrumBlockchain::from(client), + ) + .unwrap(); + + wallet.sync(noop_progress(), None).unwrap(); + let balance = wallet.get_balance().unwrap(); + + let addr = wallet.get_address(bdk::wallet::AddressIndex::New).unwrap(); + assert_eq!( + "tb1qanjjv4cs20dgv32vncrxw702l8g4qtn2m9wn7d", + addr.to_string() + ); + + let cli_args = vec![ + "bdk-cli", + "--network", + "bitcoin", + "wallet", + "--descriptor", + &descriptor, + "produce_proof", + "--message", + message.clone(), + ]; + let cli_opts = CliOpts::from_iter(&cli_args); + + let wallet_subcmd = match cli_opts.subcommand { + CliSubCommand::Wallet { + wallet_opts: _, + subcommand: WalletSubCommand::OnlineWalletSubCommand(online_subcommand), + } => online_subcommand, + _ => panic!("unexpected subcommand"), + }; + let result = handle_online_wallet_subcommand(&wallet, wallet_subcmd).unwrap(); + let psbt: PartiallySignedTransaction = + serde_json::from_str(&result.as_object().unwrap().get("psbt").unwrap().to_string()) + .unwrap(); + let psbt = encode_psbt(psbt); + let psbt = str::from_utf8(&psbt).unwrap(); + assert_eq!(format!("{}", psbt), "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"); + + let psbt_b64 = &result + .as_object() + .unwrap() + .get("psbt_base64") + .unwrap() + .to_string(); + assert_eq!(&format!("{}", psbt), psbt_b64.trim_matches('\"')); + + let cli_args = vec![ + "bdk-cli", + "--network", + "bitcoin", + "wallet", + "--descriptor", + &descriptor, + "verify_proof", + "--psbt", + psbt, + "--message", + message.clone(), + ]; + let cli_opts = CliOpts::from_iter(&cli_args); + + let wallet_subcmd = match cli_opts.subcommand { + CliSubCommand::Wallet { + wallet_opts: _, + subcommand: WalletSubCommand::OnlineWalletSubCommand(online_subcommand), + } => online_subcommand, + _ => panic!("unexpected subcommand"), + }; + let result = handle_online_wallet_subcommand(&wallet, wallet_subcmd).unwrap(); + let spendable = result + .as_object() + .unwrap() + .get("spendable") + .unwrap() + .as_u64() + .unwrap(); + assert_eq!(spendable, balance); + } + + #[cfg(all(feature = "reserves", feature = "electrum"))] + #[test] + fn test_proof_of_reserves_veryfy() { + let message = "Those coins belong to Satoshi Nakamoto"; + let address = "tb1qanjjv4cs20dgv32vncrxw702l8g4qtn2m9wn7d"; + let psbt = "cHNidP8BAKcBAAAAA9A7AuVMuwIrkAuXtKDqw3BruovK2PH7E90MzCbyaAMlAAAAAAD/////sq+79fxJ+5F8951Xcbp8N5VYVJoesG1zd3AxStcGwPYAAAAAAP/////AUS4n/SkO9SSvIv8MI2Xdm3xdn77Sud6uX5PyqCganAEAAAAA/////wGwrQEAAAAAABl2qRSff9CW037SwOP38M/JJL7vT/zraIisAAAAAAABAQoAAAAAAAAAAAFRAQcAAAEBHxAnAAAAAAAAFgAU7OUmVxBT2oZFTJ4GZ3nq+dFQLmoiAgMrBVgHi+w4aUqEkz1lkwPiV12ufpFoWRFFQRW/1kSH40gwRQIhAPgByvkajQrNeQDSGik2gnxpo/P/owiEHR+0nWefkXurAiBgrAlDvwuTiaGEEWQW/Kd7L7u7YOQnqvrd46DR0A8yPgEBBwABCGwCSDBFAiEA+AHK+RqNCs15ANIaKTaCfGmj8/+jCIQdH7SdZ5+Re6sCIGCsCUO/C5OJoYQRZBb8p3svu7tg5Ceq+t3joNHQDzI+ASEDKwVYB4vsOGlKhJM9ZZMD4lddrn6RaFkRRUEVv9ZEh+MAAQEfoIYBAAAAAAAWABTs5SZXEFPahkVMngZneer50VAuaiICAysFWAeL7DhpSoSTPWWTA+JXXa5+kWhZEUVBFb/WRIfjRzBEAiBSfiX0qP7vR+2Qx/mRJS8pwma8nTfOWKerzo6c0iSAfwIgEfX4Wt7YXd8MkKUEY627GWYCmKfMsJGcIC0U1wgc1vUBAQcAAQhrAkcwRAIgUn4l9Kj+70ftkMf5kSUvKcJmvJ03zlinq86OnNIkgH8CIBH1+Fre2F3fDJClBGOtuxlmApinzLCRnCAtFNcIHNb1ASEDKwVYB4vsOGlKhJM9ZZMD4lddrn6RaFkRRUEVv9ZEh+MAAA=="; + + let cli_args = vec![ + "bdk-cli", + "--network", + "bitcoin", + "external_reserves", + message, + psbt, + "6", + address, + address, // passing the address twice on purpose, to test passing of multiple addresses + "--server", + "ssl://electrum.blockstream.info:60002", + ]; + let cli_opts = CliOpts::from_iter(&cli_args); + + let (message, psbt, confirmations, addresses, electrum_opts) = match cli_opts.subcommand { + CliSubCommand::ExternalReserves { + message, + psbt, + confirmations, + addresses, + electrum_opts, + } => (message, psbt, confirmations, addresses, electrum_opts), + _ => panic!("unexpected subcommand"), + }; + let result = handle_ext_reserves_subcommand( + Network::Bitcoin, + message, + psbt, + confirmations, + addresses, + electrum_opts, + ) + .unwrap(); + let spendable = result + .as_object() + .unwrap() + .get("spendable") + .unwrap() + .as_u64() + .unwrap(); + assert!(spendable > 0); + } }