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",
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",
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};
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
///
#[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
/// 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 {
/// 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 {
)]
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> {
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)?;
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)?;
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 }))
+ }
}
}
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};
#[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",
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]
&"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#"cHNidP8BAKcBAAAAA31Ko7U8mQMXxjrKhYvd5N06BrT2dBPwWVhZQYABZbdZAAAAAAD/////mAqA48Jx/UDORZswhCLAQiyCxhu4IZMXzWRUMx5PVIUAAAAAAP////+YCoDjwnH9QM5FmzCEIsBCLILGG7ghkxfNZFQzHk9UhQEAAAAA/////wHo7zMDAAAAABl2qRSff9CW037SwOP38M/JJL7vT/zraIisAAAAAAABAQoAAAAAAAAAAAFRAQMEAQAAAAEHAAABASAQJwAAAAAAABepFBCNSAfpaNUWLsnOLKCLqO4EAl4UhyICAyS3XurSwfnGDoretecAn+x6Ka/Nsw2CnYLQlWL+i66FRzBEAiA3wllP5sFLWtT5NOthk2OaD42fNATjDzBVL4dPsG538QIgC7r4Hs2qQrKzY/WJOl2Idx7KAEY+J5xniJfEB1D7TzsBIgIDdGj46pm2xkeIOYta0lSAytCPSw1lvlTOOlX9IGta5HJIMEUCIQDETYrRs/Lamq1zew92oa2zFUFBeaWADxcKXmMf8/pMgAIgeQCUTF6jvi5iD9LxD54YKD3STmWy/Y4WwtVebZJWeh4BIgID9y09lmY7DqmbCusNfyc8qxGo3jeIXx3dyNkRKtuHFpNHMEQCIEIkdGA0m2sxDlRArMN5cVflkK3OZt0thfgntyqv8PuoAiBjtkZejhZ2YgB/C3oiGjZM2L7QA+QoXc7Ma677P7+87wEBBCIAIHQQ4qnMe1dC7RoA6/AqOG53jareHaC0Fbqu6vBAL08NAQXxUyECL1M7Zn4uo7NuIZYcn+nco0D74K9SEBc6g64DN6sgpXYhAmu1OpjoEL0O5hoO0RZLpsAkeG12VU55PiAtxs6ceMTqIQLVuKfWakH/229MU9YZlAIuiGtPRQAfsVi5XJFk1F+MoyEDJLde6tLB+cYOit615wCf7Hopr82zDYKdgtCVYv6LroUhAy00+JMiAIM0h70pSqIZ3L4AC5+bPYJHmVQUMACfD6VRIQN0aPjqmbbGR4g5i1rSVIDK0I9LDWW+VM46Vf0ga1rkciED9y09lmY7DqmbCusNfyc8qxGo3jeIXx3dyNkRKtuHFpNXrgEHIyIAIHQQ4qnMe1dC7RoA6/AqOG53jareHaC0Fbqu6vBAL08NAQj9zQEFAEcwRAIgN8JZT+bBS1rU+TTrYZNjmg+NnzQE4w8wVS+HT7Bud/ECIAu6+B7NqkKys2P1iTpdiHceygBGPiecZ4iXxAdQ+087AUgwRQIhAMRNitGz8tqarXN7D3ahrbMVQUF5pYAPFwpeYx/z+kyAAiB5AJRMXqO+LmIP0vEPnhgoPdJOZbL9jhbC1V5tklZ6HgFHMEQCIEIkdGA0m2sxDlRArMN5cVflkK3OZt0thfgntyqv8PuoAiBjtkZejhZ2YgB/C3oiGjZM2L7QA+QoXc7Ma677P7+87wHxUyECL1M7Zn4uo7NuIZYcn+nco0D74K9SEBc6g64DN6sgpXYhAmu1OpjoEL0O5hoO0RZLpsAkeG12VU55PiAtxs6ceMTqIQLVuKfWakH/229MU9YZlAIuiGtPRQAfsVi5XJFk1F+MoyEDJLde6tLB+cYOit615wCf7Hopr82zDYKdgtCVYv6LroUhAy00+JMiAIM0h70pSqIZ3L4AC5+bPYJHmVQUMACfD6VRIQN0aPjqmbbGR4g5i1rSVIDK0I9LDWW+VM46Vf0ga1rkciED9y09lmY7DqmbCusNfyc8qxGo3jeIXx3dyNkRKtuHFpNXrgABASDYyDMDAAAAABepFBCNSAfpaNUWLsnOLKCLqO4EAl4UhyICAyS3XurSwfnGDoretecAn+x6Ka/Nsw2CnYLQlWL+i66FRzBEAiBER55YOumAJFkXvTrb1GSuXxYfenIqK+LRx7PPvoKGLQIgVp0yY/2YB63O2tzzjtEZpI+GVkHblhI/dWASuoKTUt4BIgIDdGj46pm2xkeIOYta0lSAytCPSw1lvlTOOlX9IGta5HJHMEQCIGjiLiZbmAJB6+x2D2K6FYWczwRx4XCKaBIsvvdyt1ouAiBTlhGF+7tXHXRWv4pWisXPlJ8oBvUN8c+CbdNxsfB8oQEiAgP3LT2WZjsOqZsK6w1/JzyrEajeN4hfHd3I2REq24cWk0gwRQIhAKxzC4IYfuSVMbIk1dkOgi+xCg/zEh7Drie9E1r0KKUPAiAEJM+oGgJw5CTKiLoO80uyWlHnNYXRt0bDLaM0OaoVtgEBBCIAIHQQ4qnMe1dC7RoA6/AqOG53jareHaC0Fbqu6vBAL08NAQXxUyECL1M7Zn4uo7NuIZYcn+nco0D74K9SEBc6g64DN6sgpXYhAmu1OpjoEL0O5hoO0RZLpsAkeG12VU55PiAtxs6ceMTqIQLVuKfWakH/229MU9YZlAIuiGtPRQAfsVi5XJFk1F+MoyEDJLde6tLB+cYOit615wCf7Hopr82zDYKdgtCVYv6LroUhAy00+JMiAIM0h70pSqIZ3L4AC5+bPYJHmVQUMACfD6VRIQN0aPjqmbbGR4g5i1rSVIDK0I9LDWW+VM46Vf0ga1rkciED9y09lmY7DqmbCusNfyc8qxGo3jeIXx3dyNkRKtuHFpNXrgEHIyIAIHQQ4qnMe1dC7RoA6/AqOG53jareHaC0Fbqu6vBAL08NAQj9zQEFAEcwRAIgREeeWDrpgCRZF70629Rkrl8WH3pyKivi0cezz76Chi0CIFadMmP9mAetztrc847RGaSPhlZB25YSP3VgErqCk1LeAUcwRAIgaOIuJluYAkHr7HYPYroVhZzPBHHhcIpoEiy+93K3Wi4CIFOWEYX7u1cddFa/ilaKxc+UnygG9Q3xz4Jt03Gx8HyhAUgwRQIhAKxzC4IYfuSVMbIk1dkOgi+xCg/zEh7Drie9E1r0KKUPAiAEJM+oGgJw5CTKiLoO80uyWlHnNYXRt0bDLaM0OaoVtgHxUyECL1M7Zn4uo7NuIZYcn+nco0D74K9SEBc6g64DN6sgpXYhAmu1OpjoEL0O5hoO0RZLpsAkeG12VU55PiAtxs6ceMTqIQLVuKfWakH/229MU9YZlAIuiGtPRQAfsVi5XJFk1F+MoyEDJLde6tLB+cYOit615wCf7Hopr82zDYKdgtCVYv6LroUhAy00+JMiAIM0h70pSqIZ3L4AC5+bPYJHmVQUMACfD6VRIQN0aPjqmbbGR4g5i1rSVIDK0I9LDWW+VM46Vf0ga1rkciED9y09lmY7DqmbCusNfyc8qxGo3jeIXx3dyNkRKtuHFpNXrgAA"#;
+ 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);
+ }
}