diff --git a/Cargo.lock b/Cargo.lock index e0933eff..66273d71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -247,7 +247,6 @@ name = "bdk-cli" version = "3.0.0" dependencies = [ "assert_cmd", - "bdk_bip322", "bdk_bitcoind_rpc", "bdk_electrum", "bdk_esplora", @@ -257,7 +256,6 @@ dependencies = [ "bdk_testenv", "bdk_wallet", "bitcoin-payment-instructions", - "claims", "clap", "clap_complete", "cli-table", @@ -280,16 +278,6 @@ dependencies = [ "url", ] -[[package]] -name = "bdk_bip322" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "262ee5f0c1056400b354391cabc541cb732c10488679de6389641e37f7e6c698" -dependencies = [ - "bdk_wallet", - "bitcoin", -] - [[package]] name = "bdk_bitcoind_rpc" version = "0.22.0" @@ -349,9 +337,9 @@ dependencies = [ [[package]] name = "bdk_kyoto" -version = "0.15.4" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f35b9f8b3aa8c4647bec7a92b050c496742d955e0ac1edcb4e7c2deabf63c54" +checksum = "346c1d502ee4613cf2201db9c3b93a2317772d8eaa372114f34bf5f374be94fd" dependencies = [ "bdk_wallet", "bip157", @@ -359,9 +347,9 @@ dependencies = [ [[package]] name = "bdk_redb" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c21112531eccdcb15c120d6c39f704b0c733b12a3dff6bcce106984ca9f1011" +checksum = "69ac8b88c9594c615fcb5b763e7afa16937e94f7a7f5ca334bf59a7fee69fea1" dependencies = [ "bdk_chain", "bdk_wallet", @@ -390,9 +378,9 @@ dependencies = [ [[package]] name = "bdk_wallet" -version = "2.4.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c99821af39c7df004bd411ece2ef22d9fba5a631b4489f3d9a0ac0d19637e2d0" +checksum = "1284fb23acc3e3022673712b55f4d5ce7e38aadc2c49bbef830dc3935f0a3289" dependencies = [ "bdk_chain", "bip39", @@ -420,13 +408,14 @@ dependencies = [ [[package]] name = "bip157" -version = "0.3.4" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88df5c18baaea9be4219679afbd4fc26491606f89f6ecdaffcdcabd67635b07b" +checksum = "14485468baff5ec4114d186f1683635f3094de9f5a4b3953bf82ac6cccba40e8" dependencies = [ "bip324", "bitcoin", "bitcoin-address-book", + "bitcoin_hashes 0.20.0", "tokio", ] @@ -456,13 +445,14 @@ dependencies = [ [[package]] name = "bitcoin" -version = "0.32.101" +version = "0.32.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed8ccb78a9ff7a6fbb90e2fb9b8588b4a9928d49d8af3cb789108a84ea6b0ce" +checksum = "1e499f9fc0407f50fe98af744ab44fa67d409f76b6772e1689ec8485eb0c0f66" dependencies = [ "base58ck", "base64 0.21.7", "bech32", + "bitcoin-internals 0.3.0", "bitcoin-io 0.1.101", "bitcoin-units", "bitcoin_hashes 0.14.101", @@ -481,6 +471,15 @@ dependencies = [ "bitcoin", ] +[[package]] +name = "bitcoin-consensus-encoding" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d7ca3dc8ff835693ad73bf1596240c06f974a31eeb3f611aaedf855f1f2725" +dependencies = [ + "bitcoin-internals 0.5.0", +] + [[package]] name = "bitcoin-consensus-encoding" version = "1.0.0" @@ -509,6 +508,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bitcoin-internals" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" +dependencies = [ + "serde", +] + [[package]] name = "bitcoin-internals" version = "0.4.2" @@ -530,7 +538,7 @@ version = "0.1.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb5de036369d1ac59d3c1819ebc4d850f89466f5401c571a285b6ed564a4cb78" dependencies = [ - "bitcoin-consensus-encoding", + "bitcoin-consensus-encoding 1.0.0", ] [[package]] @@ -584,7 +592,7 @@ version = "0.1.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cb95693f371d089a4b5b6fc41c6f3ea6e01ee8c15388335dfac8ea685173b51" dependencies = [ - "bitcoin-consensus-encoding", + "bitcoin-consensus-encoding 1.0.0", "serde", ] @@ -609,6 +617,17 @@ dependencies = [ "hex-conservative 0.3.2", ] +[[package]] +name = "bitcoin_hashes" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8a45c2b41c457a9a9e4670422fcbdf109afb3b22bc920b4045e8bdfd788a3d" +dependencies = [ + "bitcoin-consensus-encoding 0.1.0", + "bitcoin-internals 0.5.0", + "hex-conservative 0.3.2", +] + [[package]] name = "bitcoin_uri" version = "0.1.0" @@ -716,9 +735,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "bzip2" @@ -877,12 +896,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "claims" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bba18ee93d577a8428902687bcc2b6b45a56b1981a1f6d779731c86cc4c5db18" - [[package]] name = "clap" version = "4.6.1" @@ -1040,9 +1053,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crunchy" @@ -1929,9 +1942,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccfe6121cbe750cf81efa362d85c0bde7ea298ec43092d3a193baca59cdbd634" +checksum = "961d16382652bfdd8c6f68b223b26a8c93e0d475c672f414411db31c6c5c900e" dependencies = [ "defmt", "jiff-static", @@ -1943,9 +1956,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e165e897f662d428f3cd3828a919dbe067c2d42bb1031eede74ef9d27ecdedd2" +checksum = "d0879bd39df99c4c5e2c6615ccc026391a423dde10532c573e6086eb94a802cc" dependencies = [ "proc-macro2", "quote", @@ -4093,18 +4106,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.53" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.53" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 1f91dd4f..3411ad43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ readme = "README.md" license = "MIT" [dependencies] -bdk_wallet = { version = "2.4.0", features = ["rusqlite", "keys-bip39", "compiler", "std"] } +bdk_wallet = { version = "3.1.0", features = ["rusqlite", "keys-bip39", "compiler", "std"] } clap = { version = "4.6", features = ["derive","env"] } clap_complete = "4.6" dirs = { version = "6.0.0" } @@ -32,14 +32,14 @@ tap = "1.0.1" bdk_bitcoind_rpc = { version = "0.22.0", features = ["std"], optional = true } bdk_electrum = { version = "0.24.0", optional = true } bdk_esplora = { version = "0.22.2", features = ["async-https", "tokio"], optional = true } -bdk_kyoto = { version = "0.15.4", optional = true } -bdk_redb = { version = "0.1.1", optional = true } +bdk_kyoto = { version = "0.17.0", optional = true } +bdk_redb = { version = "0.2.0", optional = true } bdk_sp = { version = "0.1.0", optional = true, git = "https://github.com/bitcoindevkit/bdk-sp", tag = "v0.1.0" } shlex = { version = "1.3.0", optional = true } payjoin = { version = "0.25.0", features = ["v1", "v2", "io", "_test-utils"], optional = true} reqwest = { version = "0.13.2", default-features = false, features = ["rustls"], optional = true } url = { version = "2.5.8", optional = true } -bdk_bip322 = { version = "0.1.0", optional = true } +# bdk_bip322 = { version = "0.1.0", optional = true } bitcoin-payment-instructions = { version = "0.7.0", optional = true} [features] @@ -61,7 +61,7 @@ dns_payment = ["bitcoin-payment-instructions"] # Internal features _payjoin-dependencies = ["payjoin", "reqwest", "url", "sqlite"] -bip322 = ["bdk_bip322"] +# bip322 = ["bdk_bip322"] # Use this to consensus verify transactions at sync time verify = [] @@ -74,7 +74,6 @@ compiler = [] silent-payments = ["dep:bdk_sp"] [dev-dependencies] -claims = "0.8.0" predicates = "3.0" tempfile = "3.8" assert_cmd = "2.2.2" diff --git a/src/client.rs b/src/client.rs index 483183c0..54c28d55 100644 --- a/src/client.rs +++ b/src/client.rs @@ -23,10 +23,7 @@ use { }; #[cfg(feature = "cbf")] -use { - crate::utils::trace_logger, - bdk_kyoto::{BuilderExt, LightClient}, -}; +use {crate::utils::trace_logger, bdk_kyoto::BuilderExt}; #[cfg(any( feature = "electrum", @@ -101,14 +98,10 @@ impl BlockchainClient { #[cfg(feature = "cbf")] Self::KyotoClient { client } => { let txid = tx.compute_txid(); - let wtxid = client - .requester - .broadcast_random(tx.clone()) - .await - .map_err(|_| { - tracing::warn!("Broadcast was unsuccessful"); - Error::Generic("Transaction broadcast timed out after 30 seconds".into()) - })?; + let wtxid = client.requester.submit_package(tx).await.map_err(|_| { + tracing::warn!("Broadcast was unsuccessful"); + Error::Generic("Transaction broadcast timed out after 30 seconds".into()) + })?; tracing::info!("Successfully broadcast WTXID: {wtxid}"); Ok(txid) } @@ -205,7 +198,8 @@ impl BlockchainClient { #[cfg(feature = "cbf")] pub struct KyotoClientHandle { pub requester: bdk_kyoto::Requester, - pub update_subscriber: tokio::sync::Mutex, + pub update_subscriber: + tokio::sync::Mutex>, } #[cfg(any( @@ -267,21 +261,16 @@ pub(crate) fn new_blockchain_client( .data_dir(&_datadir) .build_with_wallet(_wallet, scan_type)?; - let LightClient { - requester, - info_subscriber, - warning_subscriber, - update_subscriber, - node, - } = light_client; + let (client, logging, update_subscriber) = light_client.subscribe(); + // `start()` spawns the node's run loop on a tokio task internally. + let requester = client.start().requester(); let subscriber = tracing_subscriber::FmtSubscriber::new(); let _ = tracing::subscriber::set_global_default(subscriber); - tokio::task::spawn(async move { node.run().await }); - tokio::task::spawn( - async move { trace_logger(info_subscriber, warning_subscriber).await }, - ); + tokio::task::spawn(async move { + trace_logger(logging.info_subscriber, logging.warning_subscriber).await + }); BlockchainClient::KyotoClient { client: Box::new(KyotoClientHandle { diff --git a/src/commands.rs b/src/commands.rs index 1d920ad8..28911627 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -13,16 +13,17 @@ //! All subcommands are defined in the below enums. #![allow(clippy::large_enum_variant)] -#[cfg(feature = "bip322")] -use crate::handlers::offline::{SignMessageCommand, VerifyMessageCommand}; +// #[cfg(feature = "bip322")] +// use crate::handlers::offline::{SignMessageCommand, VerifyMessageCommand}; use crate::handlers::{ config::{ListWalletsCommand, SaveConfigCommand}, descriptor::DescriptorCommand, key::{DeriveKeyCommand, GenerateKeyCommand, RestoreKeyCommand}, offline::{ BalanceCommand, BumpFeeCommand, CombinePsbtCommand, CreateTxCommand, ExtractPsbtCommand, - FinalizePsbtCommand, NewAddressCommand, PoliciesCommand, PublicDescriptorCommand, - SignCommand, TransactionsCommand, UnspentCommand, UnusedAddressCommand, + FinalizePsbtCommand, LockUtxoCommand, LockedUtxosCommand, NewAddressCommand, + PoliciesCommand, PublicDescriptorCommand, SignCommand, TransactionsCommand, + UnlockUtxoCommand, UnspentCommand, UnusedAddressCommand, }, }; @@ -361,11 +362,17 @@ pub enum OfflineWalletSubCommand { /// Combines multiple PSBTs into one. CombinePsbt(CombinePsbtCommand), /// Sign a message using BIP322 - #[cfg(feature = "bip322")] - SignMessage(SignMessageCommand), + // #[cfg(feature = "bip322")] + // SignMessage(SignMessageCommand), /// Verify a BIP322 signature - #[cfg(feature = "bip322")] - VerifyMessage(VerifyMessageCommand), + // #[cfg(feature = "bip322")] + // VerifyMessage(VerifyMessageCommand), + /// Lock UTXO(s) so they're excluded from coin selection. + LockUtxo(LockUtxoCommand), + /// Unlock previously locked UTXO(s). + UnlockUtxo(UnlockUtxoCommand), + /// List currently locked UTXOs. + LockedUtxos(LockedUtxosCommand), /// Creates a new unsigned transaction from DNS payment instructions. #[cfg(feature = "dns_payment")] CreateDnsTx(CreateDnsTxCommand), diff --git a/src/error.rs b/src/error.rs index 7f7eab04..96af63ad 100644 --- a/src/error.rs +++ b/src/error.rs @@ -155,10 +155,9 @@ pub enum BDKCliError { #[cfg(feature = "payjoin")] #[error("Payjoin database error: {0}")] PayjoinDb(#[from] crate::handlers::payjoin::db::Error), - - #[cfg(feature = "bip322")] - #[error("BIP-322 error: {0}")] - Bip322Error(#[from] bdk_bip322::error::Error), + // #[cfg(feature = "bip322")] + // #[error("BIP-322 error: {0}")] + // Bip322Error(#[from] bdk_bip322::error::Error), } impl From for BDKCliError { diff --git a/src/handlers/descriptor.rs b/src/handlers/descriptor.rs index e52863a8..15d2d59a 100644 --- a/src/handlers/descriptor.rs +++ b/src/handlers/descriptor.rs @@ -53,12 +53,12 @@ impl AppCommand> for DescriptorCommand { match &self.key { Some(key) => { if is_mnemonic(key) { - generate_descriptor_from_mnemonic(key, ctx.network, &self.desc_type) + generate_descriptor_from_mnemonic(key, ctx.network.into(), &self.desc_type) } else { - generate_descriptors(&self.desc_type, key, ctx.network) + generate_descriptors(&self.desc_type, key, ctx.network.into()) } } - None => generate_descriptor_with_mnemonic(ctx.network, &self.desc_type), + None => generate_descriptor_with_mnemonic(ctx.network.into(), &self.desc_type), } } } diff --git a/src/handlers/dns/mod.rs b/src/handlers/dns/mod.rs index 6e682bc6..628bfbdb 100644 --- a/src/handlers/dns/mod.rs +++ b/src/handlers/dns/mod.rs @@ -127,7 +127,7 @@ impl AsyncAppCommand>> for CreateDnsTxCommand { tx_builder.set_exact_sequence(Sequence::MAX); } if self.offline_signer { - tx_builder.include_output_redeem_witness_script(); + tx_builder.add_global_xpubs(); } if let Some(fee_rate) = self.fee_rate && let Some(fee_rate) = FeeRate::from_sat_per_vb(fee_rate as u64) diff --git a/src/handlers/key.rs b/src/handlers/key.rs index c1a9e763..e7bb6457 100644 --- a/src/handlers/key.rs +++ b/src/handlers/key.rs @@ -56,7 +56,7 @@ impl AppCommand> for GenerateKeyCommand { .map_err(|_| Error::Generic("Mnemonic generation error".to_string()))?; let mnemonic = mnemonic.into_key(); let xkey: ExtendedKey = (mnemonic.clone(), self.password.clone()).into_extended_key()?; - let xprv = xkey.into_xprv(ctx.network).ok_or_else(|| { + let xprv = xkey.into_xprv(ctx.network.into()).ok_or_else(|| { Error::Generic("Privatekey info not found (should not happen)".to_string()) })?; let fingerprint = xprv.fingerprint(&secp); @@ -139,7 +139,7 @@ impl AppCommand> for RestoreKeyCommand { let mnemonic = Mnemonic::parse_in(Language::English, &self.mnemonic)?; let xkey: ExtendedKey = (mnemonic.clone(), &self.password).0.into_extended_key()?; - let xprv = xkey.into_xprv(ctx.network).ok_or_else(|| { + let xprv = xkey.into_xprv(ctx.network.into()).ok_or_else(|| { Error::Generic("Privatekey info not found (should not happen)".to_string()) })?; let fingerprint = xprv.fingerprint(&secp); diff --git a/src/handlers/offline.rs b/src/handlers/offline.rs index 6853194e..048dcfcb 100644 --- a/src/handlers/offline.rs +++ b/src/handlers/offline.rs @@ -28,12 +28,12 @@ use { bdk_wallet::keys::{DescriptorPublicKey, DescriptorSecretKey, SinglePubKey}, std::collections::HashMap, }; -#[cfg(feature = "bip322")] -use { - crate::utils::parse_signature_format, - crate::utils::types::MessageResult, - bdk_bip322::{BIP322, MessageProof}, -}; +// #[cfg(feature = "bip322")] +// use { +// crate::utils::parse_signature_format, +// crate::utils::types::MessageResult, +// bdk_bip322::{BIP322, MessageProof}, +// }; impl OfflineWalletSubCommand { pub fn execute(&self, ctx: &mut AppContext>) -> Result<(), Error> { @@ -73,14 +73,19 @@ impl OfflineWalletSubCommand { Self::CombinePsbt(combine_psbt_command) => combine_psbt_command .execute(ctx)? .write_out(std::io::stdout()), - #[cfg(feature = "bip322")] - Self::SignMessage(sign_message_command) => sign_message_command - .execute(ctx)? - .write_out(std::io::stdout()), - #[cfg(feature = "bip322")] - Self::VerifyMessage(verify_message_command) => verify_message_command - .execute(ctx)? - .write_out(std::io::stdout()), + // #[cfg(feature = "bip322")] + // Self::SignMessage(sign_message_command) => sign_message_command + // .execute(ctx)? + // .write_out(std::io::stdout()), + // #[cfg(feature = "bip322")] + // Self::VerifyMessage(verify_message_command) => verify_message_command + // .execute(ctx)? + // .write_out(std::io::stdout()), + Self::LockUtxo(lock_utxo) => lock_utxo.execute(ctx)?.write_out(std::io::stdout()), + Self::UnlockUtxo(unlock_utxo) => unlock_utxo.execute(ctx)?.write_out(std::io::stdout()), + Self::LockedUtxos(locked_utxos) => { + locked_utxos.execute(ctx)?.write_out(std::io::stdout()) + } #[cfg(feature = "dns_payment")] Self::CreateDnsTx(_) => Err(Error::Generic( "CreateDnsTx is dispatched asynchronously through main".to_string(), @@ -123,9 +128,13 @@ impl AppCommand>> for UnspentCommand { fn execute(&self, ctx: &mut AppContext>) -> Result { let wallet = &mut ctx.state.wallet; - let utxos = wallet - .list_unspent() - .map(|utxo| UnspentDetails::from_local_output(&utxo, ctx.network)) + let outputs: Vec<_> = wallet.list_unspent().collect(); + let utxos = outputs + .into_iter() + .map(|utxo| { + let is_locked = wallet.is_outpoint_locked(utxo.outpoint); + UnspentDetails::from_local_output(&utxo, ctx.network, is_locked) + }) .collect(); Ok(ListResult::new(utxos)) @@ -269,7 +278,7 @@ impl AppCommand>> for CreateTxCommand { } if self.offline_signer { - tx_builder.include_output_redeem_witness_script(); + tx_builder.add_global_xpubs(); } if let Some(fee_rate) = self.fee_rate @@ -424,7 +433,7 @@ impl AppCommand>> for CreateSpTxCommand { tx_builder.set_exact_sequence(Sequence::MAX); if self.offline_signer { - tx_builder.include_output_redeem_witness_script(); + tx_builder.add_global_xpubs(); } if let Some(fee_rate) = self.fee_rate @@ -586,7 +595,7 @@ impl AppCommand>> for BumpFeeCommand { } if self.offline_signer { - tx_builder.include_output_redeem_witness_script(); + tx_builder.add_global_xpubs(); } if let Some(utxos) = &self.utxos { @@ -764,6 +773,7 @@ impl AppCommand>> for CombinePsbtCommand { } } +/** #[cfg(feature = "bip322")] #[derive(Debug, Parser, Clone, PartialEq)] pub struct SignMessageCommand { @@ -851,3 +861,66 @@ impl AppCommand>> for VerifyMessageCommand { }) } } +**/ + +#[derive(Parser, Debug, Clone, PartialEq)] +pub struct LockUtxoCommand { + /// Outpoint(s) to lock, format TXID:VOUT. + #[arg(env = "TXID:VOUT", long = "utxo", required = true, value_parser = parse_outpoint)] + pub utxos: Vec, +} + +impl AppCommand>> for LockUtxoCommand { + type Output = ListResult; + fn execute(&self, ctx: &mut AppContext>) -> Result { + let wallet = &mut ctx.state.wallet; + for out_point in &self.utxos { + if wallet.get_utxo(*out_point).is_none() { + eprintln!("warning: {out_point} is not a known wallet UTXO; locking anyway"); + } + wallet.lock_outpoint(*out_point); + } + let locked = wallet + .list_locked_outpoints() + .map(|o| o.to_string()) + .collect(); + Ok(ListResult::new(locked)) + } +} + +#[derive(Parser, Debug, Clone, PartialEq)] +pub struct UnlockUtxoCommand { + /// Outpoint(s) to unlock, format TXID:VOUT. + #[arg(env = "TXID:VOUT", long = "utxo", required = true, value_parser = parse_outpoint)] + pub utxos: Vec, +} + +impl AppCommand>> for UnlockUtxoCommand { + type Output = ListResult; + fn execute(&self, ctx: &mut AppContext>) -> Result { + let wallet = &mut ctx.state.wallet; + for out_point in &self.utxos { + wallet.unlock_outpoint(*out_point); + } + let locked = wallet + .list_locked_outpoints() + .map(|o| o.to_string()) + .collect(); + Ok(ListResult::new(locked)) + } +} + +#[derive(Parser, Debug, Clone, PartialEq)] +pub struct LockedUtxosCommand {} + +impl AppCommand>> for LockedUtxosCommand { + type Output = ListResult; + fn execute(&self, ctx: &mut AppContext>) -> Result { + let wallet = &mut ctx.state.wallet; + let locked = wallet + .list_locked_outpoints() + .map(|o| o.to_string()) + .collect(); + Ok(ListResult::new(locked)) + } +} diff --git a/src/utils/common.rs b/src/utils/common.rs index e2866617..0fdc2aa2 100644 --- a/src/utils/common.rs +++ b/src/utils/common.rs @@ -1,6 +1,6 @@ use crate::{commands::WalletOpts, config::WalletConfig, error::BDKCliError as Error}; -#[cfg(feature = "bip322")] -use bdk_bip322::SignatureFormat; +// #[cfg(feature = "bip322")] +// use bdk_bip322::SignatureFormat; #[cfg(feature = "cbf")] use bdk_kyoto::{Info, Receiver, UnboundedReceiver, Warning}; #[cfg(feature = "silent-payments")] @@ -12,7 +12,7 @@ use bdk_wallet::bitcoin::{Address, Network, OutPoint, ScriptBuf}; feature = "cbf", feature = "rpc" ))] -use bdk_wallet::{bitcoin::Psbt, event::WalletEvent}; +use bdk_wallet::{WalletEvent, bitcoin::Psbt}; use crate::commands::OfflineWalletSubCommand; use std::{ @@ -197,20 +197,20 @@ pub(crate) fn parse_sp_code_value_pairs(s: &str) -> Result<(SilentPaymentCode, u Ok((key, value)) } -/// Function to parse the signature format from a string -#[cfg(feature = "bip322")] -pub(crate) fn parse_signature_format(format_str: &str) -> Result { - match format_str.to_lowercase().as_str() { - "legacy" => Ok(SignatureFormat::Legacy), - "simple" => Ok(SignatureFormat::Simple), - "full" => Ok(SignatureFormat::Full), - "fullproofoffunds" => Ok(SignatureFormat::FullProofOfFunds), - _ => Err(Error::Generic( - "Invalid signature format. Use 'legacy', 'simple', 'full', or 'fullproofoffunds'" - .to_string(), - )), - } -} +// /// Function to parse the signature format from a string +// #[cfg(feature = "bip322")] +// pub(crate) fn parse_signature_format(format_str: &str) -> Result { +// match format_str.to_lowercase().as_str() { +// "legacy" => Ok(SignatureFormat::Legacy), +// "simple" => Ok(SignatureFormat::Simple), +// "full" => Ok(SignatureFormat::Full), +// "fullproofoffunds" => Ok(SignatureFormat::FullProofOfFunds), +// _ => Err(Error::Generic( +// "Invalid signature format. Use 'legacy', 'simple', 'full', or 'fullproofoffunds'" +// .to_string(), +// )), +// } +// } pub fn command_requires_db(command: &OfflineWalletSubCommand) -> bool { match command { @@ -220,7 +220,10 @@ pub fn command_requires_db(command: &OfflineWalletSubCommand) -> bool { | OfflineWalletSubCommand::BumpFee(_) | OfflineWalletSubCommand::NewAddress(_) | OfflineWalletSubCommand::UnusedAddress(_) - | OfflineWalletSubCommand::CreateTx(_) => true, + | OfflineWalletSubCommand::CreateTx(_) + | OfflineWalletSubCommand::LockUtxo(_) + | OfflineWalletSubCommand::UnlockUtxo(_) + | OfflineWalletSubCommand::LockedUtxos(_) => true, OfflineWalletSubCommand::Policies(_) | OfflineWalletSubCommand::PublicDescriptor(_) @@ -229,11 +232,11 @@ pub fn command_requires_db(command: &OfflineWalletSubCommand) -> bool { | OfflineWalletSubCommand::FinalizePsbt(_) | OfflineWalletSubCommand::CombinePsbt(_) => false, - #[cfg(feature = "bip322")] - OfflineWalletSubCommand::SignMessage(_) => true, + // #[cfg(feature = "bip322")] + // OfflineWalletSubCommand::SignMessage(_) => true, - #[cfg(feature = "bip322")] - OfflineWalletSubCommand::VerifyMessage(_) => true, + // #[cfg(feature = "bip322")] + // OfflineWalletSubCommand::VerifyMessage(_) => true, #[cfg(feature = "silent-payments")] OfflineWalletSubCommand::CreateSpTx(_) => true, #[cfg(feature = "dns_payment")] diff --git a/src/utils/descriptors.rs b/src/utils/descriptors.rs index 44e795f1..9b0f5b7c 100644 --- a/src/utils/descriptors.rs +++ b/src/utils/descriptors.rs @@ -6,7 +6,7 @@ use bdk_wallet::{ KeychainKind, bip39::{Language, Mnemonic}, bitcoin::{ - Network, + NetworkKind, bip32::{DerivationPath, Xpriv, Xpub}, secp256k1::Secp256k1, }, @@ -25,7 +25,7 @@ use crate::utils::types::KeychainPair; pub fn generate_descriptors( desc_type: &str, key: &str, - network: Network, + network: NetworkKind, ) -> Result { let is_private = key.starts_with("xprv") || key.starts_with("tprv"); @@ -40,7 +40,7 @@ pub fn generate_descriptors( _ => 84u32, }; let coin_type = match network { - Network::Bitcoin => 0u32, + NetworkKind::Main => 0u32, _ => 1u32, }; let derivation_path = DerivationPath::from_str(&format!("m/{purpose}h/{coin_type}h/0h"))?; @@ -52,7 +52,7 @@ pub fn generate_descriptors( fn generate_private_descriptors( desc_type: &str, key: &str, - network: Network, + network: NetworkKind, ) -> Result { use bdk_wallet::template::{Bip44, Bip49, Bip84, Bip86}; @@ -168,7 +168,7 @@ pub fn build_public_descriptor( /// Generate new mnemonic and descriptors pub fn generate_descriptor_with_mnemonic( - network: Network, + network: NetworkKind, desc_type: &str, ) -> Result { let mnemonic: GeneratedKey = @@ -185,7 +185,7 @@ pub fn generate_descriptor_with_mnemonic( /// Generate descriptors from existing mnemonic pub fn generate_descriptor_from_mnemonic( mnemonic_str: &str, - network: Network, + network: NetworkKind, desc_type: &str, ) -> Result { let mnemonic = Mnemonic::parse_in(Language::English, mnemonic_str)?; diff --git a/src/utils/types.rs b/src/utils/types.rs index 176fd155..51855b0d 100644 --- a/src/utils/types.rs +++ b/src/utils/types.rs @@ -55,10 +55,11 @@ pub struct UnspentDetails { pub is_spent: bool, pub derivation_index: u32, pub chain_position: serde_json::Value, + pub is_locked: bool, } impl UnspentDetails { - pub fn from_local_output(utxo: &LocalOutput, _network: Network) -> Self { + pub fn from_local_output(utxo: &LocalOutput, _network: Network, is_locked: bool) -> Self { let outpoint_str = utxo.outpoint.to_string(); Self { @@ -68,6 +69,7 @@ impl UnspentDetails { is_spent: utxo.is_spent, derivation_index: utxo.derivation_index, chain_position: serde_json::to_value(utxo.chain_position).unwrap_or(json!({})), + is_locked, } } } @@ -108,18 +110,18 @@ pub struct KeychainPair { pub internal: T, } -#[cfg(feature = "bip322")] -#[derive(Serialize, Debug, Default)] -pub struct MessageResult { - #[serde(skip_serializing_if = "Option::is_none")] - pub proof: Option, +// #[cfg(feature = "bip322")] +// #[derive(Serialize, Debug, Default)] +// pub struct MessageResult { +// #[serde(skip_serializing_if = "Option::is_none")] +// pub proof: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub valid: Option, +// #[serde(skip_serializing_if = "Option::is_none")] +// pub valid: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub proven_amount: Option, -} +// #[serde(skip_serializing_if = "Option::is_none")] +// pub proven_amount: Option, +// } #[derive(Serialize, Debug)] pub struct StatusResult { diff --git a/tests/integration/offline.rs b/tests/integration/offline.rs index f19f623c..1c0fed28 100644 --- a/tests/integration/offline.rs +++ b/tests/integration/offline.rs @@ -150,117 +150,117 @@ mod test_offline { .failure() .stderr(predicate::str::contains("Invalid")); } - - #[cfg(feature = "bip322")] - #[test] - fn test_sign_message_and_verify_message() { - let (cli, mut cmd_init) = setup_wallet_config(); - cmd_init.assert().success(); - - let message = "bdk-cli integration test"; - - // Reveal exactly one address and reuse it for both sign and verify. - let addr_output = cli - .wallet_cmd(&["--wallet", WALLET_NAME, "new_address"]) - .output() - .expect("Failed to generate address"); - assert!( - addr_output.status.success(), - "new_address failed: {}", - String::from_utf8_lossy(&addr_output.stderr) - ); - let addr_json: Value = serde_json::from_slice(&addr_output.stdout).unwrap(); - let address = addr_json["address"] - .as_str() - .expect("missing address") - .to_string(); - println!("DEBUG signing address: {address}"); - - // Sign (default signature_type = "simple"). - let sign_output = cli - .wallet_cmd(&[ + /** + #[cfg(feature = "bip322")] + // #[test] + fn test_sign_message_and_verify_message() { + let (cli, mut cmd_init) = setup_wallet_config(); + cmd_init.assert().success(); + + let message = "bdk-cli integration test"; + + // Reveal exactly one address and reuse it for both sign and verify. + let addr_output = cli + .wallet_cmd(&["--wallet", WALLET_NAME, "new_address"]) + .output() + .expect("Failed to generate address"); + assert!( + addr_output.status.success(), + "new_address failed: {}", + String::from_utf8_lossy(&addr_output.stderr) + ); + let addr_json: Value = serde_json::from_slice(&addr_output.stdout).unwrap(); + let address = addr_json["address"] + .as_str() + .expect("missing address") + .to_string(); + println!("DEBUG signing address: {address}"); + + // Sign (default signature_type = "simple"). + let sign_output = cli + .wallet_cmd(&[ + "--wallet", + WALLET_NAME, + "sign_message", + "--message", + message, + "--address", + &address, + ]) + .output() + .expect("Failed to sign message"); + assert!( + sign_output.status.success(), + "sign_message failed: {}", + String::from_utf8_lossy(&sign_output.stderr) + ); + let sign_json: Value = serde_json::from_slice(&sign_output.stdout).unwrap(); + let proof = sign_json["proof"].as_str().expect("missing proof"); + println!("DEBUG proof: {proof}"); + println!("DEBUG verifying same address: {address}"); + + // Verify with the identical address + message. + cli.wallet_cmd(&[ "--wallet", WALLET_NAME, - "sign_message", - "--message", - message, + "verify_message", "--address", &address, + "--message", + message, + "--proof", + proof, ]) - .output() - .expect("Failed to sign message"); - assert!( - sign_output.status.success(), - "sign_message failed: {}", - String::from_utf8_lossy(&sign_output.stderr) - ); - let sign_json: Value = serde_json::from_slice(&sign_output.stdout).unwrap(); - let proof = sign_json["proof"].as_str().expect("missing proof"); - println!("DEBUG proof: {proof}"); - println!("DEBUG verifying same address: {address}"); - - // Verify with the identical address + message. - cli.wallet_cmd(&[ - "--wallet", - WALLET_NAME, - "verify_message", - "--address", - &address, - "--message", - message, - "--proof", - proof, - ]) - .assert() - .success() - .stdout(predicate::str::contains("\"valid\": true")); - } - - #[cfg(feature = "bip322")] - #[test] - fn test_verify_message_rejects_tampered_message() { - let (cli, mut cmd_init) = setup_wallet_config(); - cmd_init.assert().success(); - - let addr_output = cli - .wallet_cmd(&["--wallet", WALLET_NAME, "new_address"]) - .output() - .expect("Failed to generate address"); - let addr_json: Value = serde_json::from_slice(&addr_output.stdout).unwrap(); - let address = addr_json["address"].as_str().unwrap(); - - let sign_output = cli - .wallet_cmd(&[ + .assert() + .success() + .stdout(predicate::str::contains("\"valid\": true")); + } + + #[cfg(feature = "bip322")] + // #[test] + fn test_verify_message_rejects_tampered_message() { + let (cli, mut cmd_init) = setup_wallet_config(); + cmd_init.assert().success(); + + let addr_output = cli + .wallet_cmd(&["--wallet", WALLET_NAME, "new_address"]) + .output() + .expect("Failed to generate address"); + let addr_json: Value = serde_json::from_slice(&addr_output.stdout).unwrap(); + let address = addr_json["address"].as_str().unwrap(); + + let sign_output = cli + .wallet_cmd(&[ + "--wallet", + WALLET_NAME, + "sign_message", + "--message", + "original message", + "--address", + address, + ]) + .output() + .expect("Failed to sign message"); + let sign_json: Value = serde_json::from_slice(&sign_output.stdout).unwrap(); + let proof = sign_json["proof"].as_str().unwrap(); + + // A tampered message should fail + cli.wallet_cmd(&[ "--wallet", WALLET_NAME, - "sign_message", + "verify_message", + "--proof", + proof, "--message", - "original message", + "tampered message", "--address", address, ]) - .output() - .expect("Failed to sign message"); - let sign_json: Value = serde_json::from_slice(&sign_output.stdout).unwrap(); - let proof = sign_json["proof"].as_str().unwrap(); - - // A tampered message should fail - cli.wallet_cmd(&[ - "--wallet", - WALLET_NAME, - "verify_message", - "--proof", - proof, - "--message", - "tampered message", - "--address", - address, - ]) - .assert() - .success() - .stdout(predicate::str::contains("\"valid\": false")); - } - + .assert() + .success() + .stdout(predicate::str::contains("\"valid\": false")); + } + **/ #[test] fn test_create_tx_send_all_rejects_multiple_recipients() { let (cli, mut cmd_init) = setup_wallet_config(); diff --git a/tests/integration/online.rs b/tests/integration/online.rs index 0ba166d5..1a8f2eef 100644 --- a/tests/integration/online.rs +++ b/tests/integration/online.rs @@ -813,111 +813,111 @@ mod test_online { ); } } + /** + #[cfg(feature = "bip322")] + #[test] + fn test_verify_message_proof_of_funds_uses_persisted_utxos() { + let env = TestEnv::new().expect("Failed to start bdk_testenv"); + let server_url = env.electrsd.electrum_url.as_str(); + let temp_dir = TempDir::new().unwrap(); + let cli = BdkCli::new("regtest", Some(temp_dir.path().to_path_buf())); - #[cfg(feature = "bip322")] - #[test] - fn test_verify_message_proof_of_funds_uses_persisted_utxos() { - let env = TestEnv::new().expect("Failed to start bdk_testenv"); - let server_url = env.electrsd.electrum_url.as_str(); - let temp_dir = TempDir::new().unwrap(); - let cli = BdkCli::new("regtest", Some(temp_dir.path().to_path_buf())); - - // A wpkh wallet - let desc = cli.cmd("descriptor", &["--type", "wpkh"]).output().unwrap(); - let desc_val: Value = serde_json::from_slice(&desc.stdout).unwrap(); - let ext_desc = desc_val["private_descriptors"]["external"] - .as_str() - .unwrap(); - let int_desc = desc_val["private_descriptors"]["internal"] - .as_str() - .unwrap(); - cli.build_base_cmd() - .arg("wallet") - .arg("--wallet") - .arg(WALLET_NAME) - .arg("config") - .arg("--ext-descriptor") - .arg(ext_desc) - .arg("--int-descriptor") - .arg(int_desc) - .arg("--client-type") - .arg("electrum") - .arg("--database-type") - .arg("sqlite") - .arg("--url") - .arg(server_url) - .assert() - .success(); - - // Fund a single small UTXO - let address = cli_new_address(&cli); - let node_addr = env - .rpc_client() - .get_new_address(None, None) - .unwrap() - .assume_checked(); - env.mine_blocks(101, Some(node_addr)).unwrap(); - env.wait_until_electrum_sees_block(Duration::from_secs(10)) - .unwrap(); - let txid = env.send(&address, Amount::from_sat(5_000)).unwrap(); - env.wait_until_electrum_sees_txid(txid, Duration::from_secs(10)) - .unwrap(); - env.mine_blocks(3, None).unwrap(); - env.wait_until_electrum_sees_block(Duration::from_secs(10)) - .unwrap(); - cli_full_scan(&cli); - - // The funded outpoint to prove control of. - let unspent = run_wallet_json(&cli, &["unspent"]); - let outpoint = unspent["items"][0]["outpoint"] - .as_str() - .expect("funded wallet should have one UTXO") - .to_string(); - let addr = address.to_string(); + // A wpkh wallet + let desc = cli.cmd("descriptor", &["--type", "wpkh"]).output().unwrap(); + let desc_val: Value = serde_json::from_slice(&desc.stdout).unwrap(); + let ext_desc = desc_val["private_descriptors"]["external"] + .as_str() + .unwrap(); + let int_desc = desc_val["private_descriptors"]["internal"] + .as_str() + .unwrap(); + cli.build_base_cmd() + .arg("wallet") + .arg("--wallet") + .arg(WALLET_NAME) + .arg("config") + .arg("--ext-descriptor") + .arg(ext_desc) + .arg("--int-descriptor") + .arg(int_desc) + .arg("--client-type") + .arg("electrum") + .arg("--database-type") + .arg("sqlite") + .arg("--url") + .arg(server_url) + .assert() + .success(); - // Produce a proof-of-funds over that UTXO. - let proof = run_wallet_json( - &cli, - &[ - "sign_message", - "--message", - "proof-of-funds", - "--address", - &addr, - "--signature-type", - "fullproofoffunds", - "--utxos", - &outpoint, - ], - )["proof"] - .as_str() - .expect("sign_message should return a proof") - .to_string(); + // Fund a single small UTXO + let address = cli_new_address(&cli); + let node_addr = env + .rpc_client() + .get_new_address(None, None) + .unwrap() + .assume_checked(); + env.mine_blocks(101, Some(node_addr)).unwrap(); + env.wait_until_electrum_sees_block(Duration::from_secs(10)) + .unwrap(); + let txid = env.send(&address, Amount::from_sat(5_000)).unwrap(); + env.wait_until_electrum_sees_txid(txid, Duration::from_secs(10)) + .unwrap(); + env.mine_blocks(3, None).unwrap(); + env.wait_until_electrum_sees_block(Duration::from_secs(10)) + .unwrap(); + cli_full_scan(&cli); - let result = run_wallet_json( - &cli, - &[ - "verify_message", - "--proof", - &proof, - "--message", - "proof-of-funds", - "--address", - &addr, - ], - ); - assert_eq!( - result["valid"].as_bool(), - Some(true), - "proof-of-funds should verify against the persisted wallet: {result}" - ); - assert_eq!( - result["proven_amount"].as_u64(), - Some(5_000), - "proven_amount should equal the funded UTXO value: {result}" - ); - } + // The funded outpoint to prove control of. + let unspent = run_wallet_json(&cli, &["unspent"]); + let outpoint = unspent["items"][0]["outpoint"] + .as_str() + .expect("funded wallet should have one UTXO") + .to_string(); + let addr = address.to_string(); + + // Produce a proof-of-funds over that UTXO. + let proof = run_wallet_json( + &cli, + &[ + "sign_message", + "--message", + "proof-of-funds", + "--address", + &addr, + "--signature-type", + "fullproofoffunds", + "--utxos", + &outpoint, + ], + )["proof"] + .as_str() + .expect("sign_message should return a proof") + .to_string(); + let result = run_wallet_json( + &cli, + &[ + "verify_message", + "--proof", + &proof, + "--message", + "proof-of-funds", + "--address", + &addr, + ], + ); + assert_eq!( + result["valid"].as_bool(), + Some(true), + "proof-of-funds should verify against the persisted wallet: {result}" + ); + assert_eq!( + result["proven_amount"].as_u64(), + Some(5_000), + "proven_amount should equal the funded UTXO value: {result}" + ); + } + */ // `create_dns_tx` with a plain `--to` recipient #[cfg(feature = "dns_payment")] #[test] @@ -965,6 +965,92 @@ mod test_online { String::from_utf8_lossy(&out.stderr) ); } + + // lock → list → unlock cycle. + #[test] + fn test_lock_unlock_and_list_utxos() { + let (cli, mut cmd_init, env) = setup_online_wallet(); + cmd_init.assert().success(); + fund_and_sync_wallet(&cli, &env); + + let unspent = run_wallet_json(&cli, &["unspent"]); + let outpoint = unspent["items"][0]["outpoint"] + .as_str() + .expect("one utxo") + .to_string(); + + let locked = run_wallet_json(&cli, &["lock_utxo", "--utxo", &outpoint]); + assert_eq!(locked["count"].as_u64(), Some(1), "lock output: {locked}"); + + // separate process — must still be locked (persistence) + let listed = run_wallet_json(&cli, &["locked_utxos"]); + assert_eq!( + listed["count"].as_u64(), + Some(1), + "lock must persist across processes: {listed}" + ); + assert!( + listed["items"] + .as_array() + .unwrap() + .iter() + .any(|i| i.as_str() == Some(outpoint.as_str())), + "locked_utxos should list {outpoint}: {listed}" + ); + + let after = run_wallet_json(&cli, &["unlock_utxo", "--utxo", &outpoint]); + assert_eq!( + after["count"].as_u64(), + Some(0), + "unlock should clear it: {after}" + ); + let final_list = run_wallet_json(&cli, &["locked_utxos"]); + assert_eq!( + final_list["count"].as_u64(), + Some(0), + "empty after unlock: {final_list}" + ); + } + + // a locked UTXO is excluded from coin selection. + #[test] + fn test_locked_utxo_excluded_from_coin_selection() { + let (cli, mut cmd_init, env) = setup_online_wallet(); + cmd_init.assert().success(); + fund_and_sync_wallet(&cli, &env); + + let unspent = run_wallet_json(&cli, &["unspent"]); + let outpoint = unspent["items"][0]["outpoint"] + .as_str() + .unwrap() + .to_string(); + + run_wallet_json(&cli, &["lock_utxo", "--utxo", &outpoint]); + + // only UTXO is locked → create_tx must fail + let out = cli + .wallet_cmd(&[ + "--wallet", + WALLET_NAME, + "create_tx", + "--to", + &format!("{RECIPIENT}:20000"), + ]) + .output() + .unwrap(); + assert!( + !out.status.success(), + "create_tx must fail when the only UTXO is locked" + ); + + // unlock → create_tx succeeds + run_wallet_json(&cli, &["unlock_utxo", "--utxo", &outpoint]); + let psbt = run_wallet_json(&cli, &["create_tx", "--to", &format!("{RECIPIENT}:20000")]); + assert!( + psbt["psbt"].as_str().is_some(), + "create_tx should succeed after unlock: {psbt}" + ); + } } #[cfg(feature = "esplora")]