]> Untitled Git - bdk-cli/commitdiff
adding the commands for proof-of-reserves using the separate repository
authorRichard Ulrich <richi@paraeasy.ch>
Wed, 26 May 2021 14:07:46 +0000 (16:07 +0200)
committerRichard Ulrich <richi@paraeasy.ch>
Fri, 3 Dec 2021 06:23:37 +0000 (07:23 +0100)
.github/workflows/cont_integration.yml
CHANGELOG.md
Cargo.lock
Cargo.toml
src/bdk_cli.rs
src/lib.rs

index e817bf1893435876e0d416c61350fbe9e074766c..570ccc33c3aa088ca94c5059756522b38323a629 100644 (file)
@@ -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
index 8191e71beec76ad6aeeb843dc42093d4f8974123..6924377b54de36cdbb54ccb0bf4a4b31aa32a721 100644 (file)
@@ -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]
 
index f11d6cba84928956810beada2086db0dfc7ff226..2abd305642d2f8aa17d34f0d67a14de69b102cb7 100644 (file)
@@ -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"
index 17ed9ee0bb854e074a96dbb91dcd3b1d4d58d9db..2abd9d51f71beed2d2ee6f12f3ba220dbcda45b1 100644 (file)
@@ -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"
index 4c39ff92ca3483c329703303919a136b4e495927..f2eac5e9dbdf8e3a79e2eee6fdea9951d27e9666 100644 (file)
@@ -407,6 +407,24 @@ fn handle_command(cli_opts: CliOpts, network: Network) -> Result<String, Error>
             // 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)
 }
index 27287d9a26af25d3d4f92ce1573356bc7a675316..0959af2ee3bc6d69c59f218644b6940959956a6c 100644 (file)
@@ -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<String>,
+        #[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<u32>,
+        /// 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<bool>,
     },
     /// 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<u32>,
+        /// 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<bool>,
     },
     /// Combines multiple PSBTs into one
     CombinePsbt {
@@ -809,6 +850,26 @@ pub enum OnlineWalletSubCommand {
         )]
         tx: Option<String>,
     },
+    /// 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<String>,
+    electrum_opts: ElectrumOpts,
+) -> Result<serde_json::Value, Error> {
+    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::<Result<Vec<Vec<_>>, 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<usize>,
+) -> Result<Vec<(OutPoint, TxOut)>, 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<u8> {
+        let mut encoded = Vec::<u8>::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);
+    }
 }