From 88e02ddee432a57ad512c8a6b60c9e0e79a9f5de Mon Sep 17 00:00:00 2001 From: nymius <155548262+nymius@users.noreply.github.com> Date: Wed, 20 Aug 2025 13:42:12 -0300 Subject: [PATCH 01/10] feat(silentpayments): add CreateSpTx command for silent payment transactions Add experimental silent payment sending capabilities with new CreateSpTx command. This command creates signed transactions directly rather than PSBTs due to current limitations in secure shared derivation. - Add bdk_sp dependency with "sp" feature flag - Implement CreateSpTx subcommand for offline wallet operations - Add silent payment recipient parsing utility - Support mixed recipients (regular addresses + silent payments) - Generate signed transactions ready for broadcast - For the moment is not possible to enable RBF for the created transactions. Note: This is experimental functionality for testing only, not recommended for mainnet use. --- Cargo.lock | 9 +++ Cargo.toml | 4 ++ src/commands.rs | 59 ++++++++++++++++++ src/handlers.rs | 155 ++++++++++++++++++++++++++++++++++++++++++++++++ src/utils.rs | 21 +++++++ 5 files changed, 248 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 4ff8e29..1ab2363 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -186,6 +186,7 @@ dependencies = [ "bdk_esplora", "bdk_kyoto", "bdk_redb", + "bdk_sp", "bdk_wallet", "clap", "cli-table", @@ -281,6 +282,14 @@ dependencies = [ "thiserror", ] +[[package]] +name = "bdk_sp" +version = "0.1.0" +source = "git+https://github.com/bitcoindevkit/bdk-sp?tag=v0.1.0#79cfaf1e8829dd771c4461e6cd2a46c8abb00503" +dependencies = [ + "bitcoin", +] + [[package]] name = "bdk_wallet" version = "2.1.0" diff --git a/Cargo.toml b/Cargo.toml index d5767f3..89c3e18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ bdk_redb = { version = "0.1.0", optional = true } shlex = { version = "1.3.0", optional = true } tracing = "0.1.41" tracing-subscriber = "0.3.20" +bdk_sp = { version = "0.1.0", optional = true, git = "https://github.com/bitcoindevkit/bdk-sp", tag = "v0.1.0" } [features] default = ["repl", "sqlite"] @@ -54,3 +55,6 @@ verify = [] # Extra utility tools # Compile policies compiler = [] + +# Experimental silent payment sending capabilities +sp = ["dep:bdk_sp"] diff --git a/src/commands.rs b/src/commands.rs index 54cddf0..f96c74e 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -14,6 +14,9 @@ #![allow(clippy::large_enum_variant)] +#[cfg(feature = "sp")] +use {crate::utils::parse_sp_code_value_pairs, bdk_sp::encoding::SilentPaymentCode}; + use bdk_wallet::bitcoin::{ Address, Network, OutPoint, ScriptBuf, bip32::{DerivationPath, Xpriv}, @@ -311,6 +314,62 @@ pub enum OfflineWalletSubCommand { )] add_data: Option, //base 64 econding }, + /// Creates a silent payment transaction + /// + /// This sub-command is **EXPERIMENTAL** and should only be used for testing. Do not use this + /// feature to create transactions that spend actual funds on the Bitcoin mainnet. + + // This command DOES NOT return a PSBT. Instead, it directly returns a signed transaction + // ready for broadcast, as it is not yet possible to perform a shared derivation of a silent + // payment script pubkey in a secure and trustless manner. + #[cfg(feature = "sp")] + CreateSpTx { + /// Adds a recipient to the transaction. + // Clap Doesn't support complex vector parsing https://github.com/clap-rs/clap/issues/1704. + // Address and amount parsing is done at run time in handler function. + #[arg(env = "ADDRESS:SAT", long = "to", required = false, value_parser = parse_recipient)] + recipients: Option>, + /// Parse silent payment recipients + #[arg(long = "to-sp", required = true, value_parser = parse_sp_code_value_pairs)] + silent_payment_recipients: Vec<(SilentPaymentCode, u64)>, + /// Sends all the funds (or all the selected utxos). Requires only one recipient with value 0. + #[arg(long = "send_all", short = 'a')] + send_all: bool, + /// Make a PSBT that can be signed by offline signers and hardware wallets. Forces the addition of `non_witness_utxo` and more details to let the signer identify the change output. + #[arg(long = "offline_signer")] + offline_signer: bool, + /// Selects which utxos *must* be spent. + #[arg(env = "MUST_SPEND_TXID:VOUT", long = "utxos", value_parser = parse_outpoint)] + utxos: Option>, + /// Marks a utxo as unspendable. + #[arg(env = "CANT_SPEND_TXID:VOUT", long = "unspendable", value_parser = parse_outpoint)] + unspendable: Option>, + /// Fee rate to use in sat/vbyte. + #[arg(env = "SATS_VBYTE", short = 'f', long = "fee_rate")] + fee_rate: Option, + /// Selects which policy should be used to satisfy the external descriptor. + #[arg(env = "EXT_POLICY", long = "external_policy")] + external_policy: Option, + /// Selects which policy should be used to satisfy the internal descriptor. + #[arg(env = "INT_POLICY", long = "internal_policy")] + internal_policy: Option, + /// Optionally create an OP_RETURN output containing given String in utf8 encoding (max 80 bytes) + #[arg( + env = "ADD_STRING", + long = "add_string", + short = 's', + conflicts_with = "add_data" + )] + add_string: Option, + /// Optionally create an OP_RETURN output containing given base64 encoded String. (max 80 bytes) + #[arg( + env = "ADD_DATA", + long = "add_data", + short = 'o', + conflicts_with = "add_string" + )] + add_data: Option, //base 64 econding + }, /// Bumps the fees of an RBF transaction. BumpFee { /// TXID of the transaction to update. diff --git a/src/handlers.rs b/src/handlers.rs index d9d2cbe..028a1c8 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -54,6 +54,16 @@ use std::io::Write; use std::str::FromStr; #[cfg(any(feature = "redb", feature = "compiler"))] use std::sync::Arc; +#[cfg(feature = "sp")] +use { + bdk_sp::{ + bitcoin::{PrivateKey, PublicKey, ScriptBuf, XOnlyPublicKey}, + encoding::SilentPaymentCode, + send::psbt::derive_sp, + }, + bdk_wallet::keys::{DescriptorPublicKey, DescriptorSecretKey, SinglePubKey}, + std::collections::HashMap, +}; #[cfg(feature = "electrum")] use crate::utils::BlockchainClient::Electrum; @@ -323,7 +333,152 @@ pub fn handle_offline_wallet_subcommand( )?) } } + #[cfg(feature = "sp")] + CreateSpTx { + recipients: maybe_recipients, + silent_payment_recipients, + send_all, + offline_signer, + utxos, + unspendable, + fee_rate, + external_policy, + internal_policy, + add_data, + add_string, + } => { + let mut tx_builder = wallet.build_tx(); + + let sp_recipients: Vec = silent_payment_recipients + .iter() + .map(|(sp_code, _)| sp_code.clone()) + .collect(); + + let mut outputs: Vec<(ScriptBuf, Amount)> = silent_payment_recipients + .iter() + .map(|(sp_code, amount)| { + let script = sp_code.get_placeholder_p2tr_spk(); + (script, Amount::from_sat(*amount)) + }) + .collect(); + + if let Some(recipients) = maybe_recipients { + if send_all { + tx_builder.drain_wallet().drain_to(recipients[0].0.clone()); + } else { + let recipients = recipients + .into_iter() + .map(|(script, amount)| (script, Amount::from_sat(amount))); + + outputs.extend(recipients); + } + } + + tx_builder.set_recipients(outputs); + + // Do not enable RBF for this transaction + tx_builder.set_exact_sequence(Sequence::MAX); + + if offline_signer { + tx_builder.include_output_redeem_witness_script(); + } + + if let Some(fee_rate) = fee_rate { + if let Some(fee_rate) = FeeRate::from_sat_per_vb(fee_rate as u64) { + tx_builder.fee_rate(fee_rate); + } + } + + if let Some(utxos) = utxos { + tx_builder.add_utxos(&utxos[..]).unwrap(); + } + + if let Some(unspendable) = unspendable { + tx_builder.unspendable(unspendable); + } + + if let Some(base64_data) = add_data { + let op_return_data = BASE64_STANDARD.decode(base64_data).unwrap(); + tx_builder.add_data(&PushBytesBuf::try_from(op_return_data).unwrap()); + } else if let Some(string_data) = add_string { + let data = PushBytesBuf::try_from(string_data.as_bytes().to_vec()).unwrap(); + tx_builder.add_data(&data); + } + + let policies = vec![ + external_policy.map(|p| (p, KeychainKind::External)), + internal_policy.map(|p| (p, KeychainKind::Internal)), + ]; + + for (policy, keychain) in policies.into_iter().flatten() { + let policy = serde_json::from_str::>>(&policy)?; + tx_builder.policy_path(policy, keychain); + } + + let mut psbt = tx_builder.finish()?; + + let unsigned_psbt = psbt.clone(); + + let _signed = wallet.sign(&mut psbt, SignOptions::default())?; + + for (full_input, psbt_input) in unsigned_psbt.inputs.iter().zip(psbt.inputs.iter_mut()) + { + // repopulate key derivation data + psbt_input.bip32_derivation = full_input.bip32_derivation.clone(); + psbt_input.tap_key_origins = full_input.tap_key_origins.clone(); + } + + let secp = Secp256k1::new(); + let mut external_signers = wallet.get_signers(KeychainKind::External).as_key_map(&secp); + let internal_signers = wallet.get_signers(KeychainKind::Internal).as_key_map(&secp); + external_signers.extend(internal_signers); + + match external_signers.iter().next().expect("not empty") { + (DescriptorPublicKey::Single(single_pub), DescriptorSecretKey::Single(prv)) => { + match single_pub.key { + SinglePubKey::FullKey(pk) => { + let keys: HashMap = [(pk, prv.key)].into(); + derive_sp(&mut psbt, &keys, &sp_recipients, &secp) + .expect("will fix later"); + } + SinglePubKey::XOnly(xonly) => { + let keys: HashMap = + [(xonly, prv.key)].into(); + derive_sp(&mut psbt, &keys, &sp_recipients, &secp) + .expect("will fix later"); + } + }; + } + (_, DescriptorSecretKey::XPrv(k)) => { + derive_sp(&mut psbt, &k.xkey, &sp_recipients, &secp).expect("will fix later"); + } + _ => unimplemented!("multi xkey signer"), + }; + + // Unfinalize PSBT to resign + for psbt_input in psbt.inputs.iter_mut() { + psbt_input.final_script_sig = None; + psbt_input.final_script_witness = None; + } + let _resigned = wallet.sign(&mut psbt, SignOptions::default())?; + + let raw_tx = psbt.extract_tx()?; + if cli_opts.pretty { + let table = vec![vec![ + "Raw Transaction".cell().bold(true), + serialize_hex(&raw_tx).cell(), + ]] + .table() + .display() + .map_err(|e| Error::Generic(e.to_string()))?; + Ok(format!("{table}")) + } else { + Ok(serde_json::to_string_pretty( + &json!({"raw_tx": serialize_hex(&raw_tx)}), + )?) + } + } CreateTx { recipients, send_all, diff --git a/src/utils.rs b/src/utils.rs index cb81074..d9f53b8 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -21,6 +21,8 @@ use bdk_kyoto::{ BuilderExt, Info, LightClient, Receiver, ScanType::Sync, UnboundedReceiver, Warning, builder::Builder, }; +#[cfg(feature = "sp")] +use bdk_sp::encoding::SilentPaymentCode; use bdk_wallet::bitcoin::{Address, Network, OutPoint, ScriptBuf}; #[cfg(any( @@ -49,6 +51,25 @@ pub(crate) fn parse_recipient(s: &str) -> Result<(ScriptBuf, u64), String> { Ok((addr.script_pubkey(), val)) } +#[cfg(feature = "sp")] +pub(crate) fn parse_sp_code_value_pairs(s: &str) -> Result<(SilentPaymentCode, u64), String> { + let parts: Vec<&str> = s.split(':').collect(); + if parts.len() != 2 { + return Err(format!("Invalid format '{}'. Expected 'key:value'", s)); + } + + let value_0 = parts[0].trim(); + let key = SilentPaymentCode::try_from(value_0) + .map_err(|_| format!("Invalid silent payment address: {}", value_0))?; + + let value = parts[1] + .trim() + .parse::() + .map_err(|_| format!("Invalid number '{}' for key '{}'", parts[1], key))?; + + Ok((key, value)) +} + #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] /// Parse the proxy (Socket:Port) argument from the cli input. pub(crate) fn parse_proxy_auth(s: &str) -> Result<(String, String), Error> { From 6d2b5088277a15e3b50de96175fba27d28e7ec87 Mon Sep 17 00:00:00 2001 From: nymius <155548262+nymius@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:30:06 -0300 Subject: [PATCH 02/10] refactor(silentpayments): use full import path for XOnlyPublicKey in CreateSpTx --- src/handlers.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/handlers.rs b/src/handlers.rs index 028a1c8..20553b7 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -57,7 +57,7 @@ use std::sync::Arc; #[cfg(feature = "sp")] use { bdk_sp::{ - bitcoin::{PrivateKey, PublicKey, ScriptBuf, XOnlyPublicKey}, + bitcoin::{PrivateKey, PublicKey, ScriptBuf}, encoding::SilentPaymentCode, send::psbt::derive_sp, }, @@ -442,7 +442,7 @@ pub fn handle_offline_wallet_subcommand( .expect("will fix later"); } SinglePubKey::XOnly(xonly) => { - let keys: HashMap = + let keys: HashMap = [(xonly, prv.key)].into(); derive_sp(&mut psbt, &keys, &sp_recipients, &secp) .expect("will fix later"); From 157250a61a37543c233d07b21aab03e34dc4e539 Mon Sep 17 00:00:00 2001 From: nymius <155548262+nymius@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:31:00 -0300 Subject: [PATCH 03/10] feat(silentpayments): handle bdk_wallet::tx_builder::AddUtxoError::UnknownUtxo --- src/handlers.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/handlers.rs b/src/handlers.rs index 20553b7..e9460bc 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -390,7 +390,9 @@ pub fn handle_offline_wallet_subcommand( } if let Some(utxos) = utxos { - tx_builder.add_utxos(&utxos[..]).unwrap(); + tx_builder + .add_utxos(&utxos[..]) + .map_err(|_| bdk_wallet::error::CreateTxError::UnknownUtxo)?; } if let Some(unspendable) = unspendable { From 38abbdda8474c8861a4c8bd2f0fa9f0ebbe0d692 Mon Sep 17 00:00:00 2001 From: nymius <155548262+nymius@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:32:37 -0300 Subject: [PATCH 04/10] feat(silentpayments): add error handling in data carrier logic --- src/handlers.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/handlers.rs b/src/handlers.rs index e9460bc..06d1f4d 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -400,10 +400,16 @@ pub fn handle_offline_wallet_subcommand( } if let Some(base64_data) = add_data { - let op_return_data = BASE64_STANDARD.decode(base64_data).unwrap(); - tx_builder.add_data(&PushBytesBuf::try_from(op_return_data).unwrap()); + let op_return_data = BASE64_STANDARD + .decode(base64_data) + .map_err(|e| Error::Generic(e.to_string()))?; + tx_builder.add_data( + &PushBytesBuf::try_from(op_return_data) + .map_err(|e| Error::Generic(e.to_string()))? + ); } else if let Some(string_data) = add_string { - let data = PushBytesBuf::try_from(string_data.as_bytes().to_vec()).unwrap(); + let data = PushBytesBuf::try_from(string_data.as_bytes().to_vec()) + .map_err(|e| Error::Generic(e.to_string()))?; tx_builder.add_data(&data); } From 795e70703da0c2030fc47b79ccf764d71e265650 Mon Sep 17 00:00:00 2001 From: nymius <155548262+nymius@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:35:55 -0300 Subject: [PATCH 05/10] feat(create-tx): handle bdk_wallet::tx_builder::AddUtxoError::UnknownUtxo --- src/handlers.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/handlers.rs b/src/handlers.rs index 06d1f4d..58b6c6a 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -527,7 +527,9 @@ pub fn handle_offline_wallet_subcommand( } if let Some(utxos) = utxos { - tx_builder.add_utxos(&utxos[..]).unwrap(); + tx_builder + .add_utxos(&utxos[..]) + .map_err(|_| bdk_wallet::error::CreateTxError::UnknownUtxo)?; } if let Some(unspendable) = unspendable { From e8f2e48b3141876059170ca5a088cf2b694644db Mon Sep 17 00:00:00 2001 From: nymius <155548262+nymius@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:37:19 -0300 Subject: [PATCH 06/10] feat(create-tx): add error handling in data carrier logic --- src/handlers.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/handlers.rs b/src/handlers.rs index 58b6c6a..3271e8c 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -537,10 +537,16 @@ pub fn handle_offline_wallet_subcommand( } if let Some(base64_data) = add_data { - let op_return_data = BASE64_STANDARD.decode(base64_data).unwrap(); - tx_builder.add_data(&PushBytesBuf::try_from(op_return_data).unwrap()); + let op_return_data = BASE64_STANDARD + .decode(base64_data) + .map_err(|e| Error::Generic(e.to_string()))?; + tx_builder.add_data( + &PushBytesBuf::try_from(op_return_data) + .map_err(|e| Error::Generic(e.to_string()))?, + ); } else if let Some(string_data) = add_string { - let data = PushBytesBuf::try_from(string_data.as_bytes().to_vec()).unwrap(); + let data = PushBytesBuf::try_from(string_data.as_bytes().to_vec()) + .map_err(|e| Error::Generic(e.to_string()))?; tx_builder.add_data(&data); } From 4154a3335b25cb18a93b10e04a850b5b14745d24 Mon Sep 17 00:00:00 2001 From: nymius <155548262+nymius@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:44:29 -0300 Subject: [PATCH 07/10] feat(silentpayments): add BDKCliError::SilentPaymentParseError variant --- src/error.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/error.rs b/src/error.rs index 5f548d9..8c54363 100644 --- a/src/error.rs +++ b/src/error.rs @@ -21,6 +21,9 @@ pub enum BDKCliError { #[error("Create transaction error: {0}")] CreateTx(#[from] bdk_wallet::error::CreateTxError), + #[error("Silent payment address decoding error: {0}")] + SilentPaymentParseError(#[from] bdk_sp::encoding::ParseError), + #[error("Descriptor error: {0}")] DescriptorError(#[from] bdk_wallet::descriptor::error::Error), From d0873d15485556bfe748639bf9fac2dc7bc08253 Mon Sep 17 00:00:00 2001 From: nymius <155548262+nymius@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:45:12 -0300 Subject: [PATCH 08/10] refactor(silentpayments): use BDKCliError in parse_sp_code_value_pairs error return value --- src/utils.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index d9f53b8..c91707b 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -52,20 +52,22 @@ pub(crate) fn parse_recipient(s: &str) -> Result<(ScriptBuf, u64), String> { } #[cfg(feature = "sp")] -pub(crate) fn parse_sp_code_value_pairs(s: &str) -> Result<(SilentPaymentCode, u64), String> { +pub(crate) fn parse_sp_code_value_pairs(s: &str) -> Result<(SilentPaymentCode, u64), Error> { let parts: Vec<&str> = s.split(':').collect(); if parts.len() != 2 { - return Err(format!("Invalid format '{}'. Expected 'key:value'", s)); + return Err(Error::Generic(format!( + "Invalid format '{}'. Expected 'key:value'", + s + ))); } let value_0 = parts[0].trim(); - let key = SilentPaymentCode::try_from(value_0) - .map_err(|_| format!("Invalid silent payment address: {}", value_0))?; + let key = SilentPaymentCode::try_from(value_0)?; let value = parts[1] .trim() .parse::() - .map_err(|_| format!("Invalid number '{}' for key '{}'", parts[1], key))?; + .map_err(|_| Error::Generic(format!("Invalid number '{}' for key '{}'", parts[1], key)))?; Ok((key, value)) } From 84f8750e9e83cdc8300cf963ac3a5bb2dda7602e Mon Sep 17 00:00:00 2001 From: nymius <155548262+nymius@users.noreply.github.com> Date: Mon, 3 Nov 2025 18:06:15 -0300 Subject: [PATCH 09/10] feat(silentpayments): add SilentPaymentCode command The input for this command are two compressed public keys. The network is obtained from the wallet current network. The silent payment code generated is independent from any of the other stateful features of bdk-cli. This command is mainly intended for experimental use, do not lock any funds to the generated code if you don't know what you are doing and don't have the keys matchin the public keys used. --- src/commands.rs | 13 +++++++++++++ src/handlers.rs | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/commands.rs b/src/commands.rs index f96c74e..02add82 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -110,6 +110,19 @@ pub enum CliSubCommand { #[command(flatten)] wallet_opts: WalletOpts, }, + /// Silent payment code generation tool. + /// + /// Allows the encoding of two public keys into a silent payment code. + /// Useful to create silent payment transactions using fake silent payment codes. + #[cfg(feature = "sp")] + SilentPaymentCode { + /// The scan public key to use on the silent payment code. + #[arg(long = "scan_public_key")] + scan: bdk_sp::bitcoin::secp256k1::PublicKey, + /// The spend public key to use on the silent payment code. + #[arg(long = "spend_public_key")] + spend: bdk_sp::bitcoin::secp256k1::PublicKey, + } } /// Wallet operation subcommands. diff --git a/src/handlers.rs b/src/handlers.rs index 3271e8c..b559eff 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -1049,6 +1049,30 @@ pub(crate) fn is_final(psbt: &Psbt) -> Result<(), Error> { Ok(()) } +#[cfg(feature = "sp")] +pub(crate) fn handle_sp_subcommand( + scan_pubkey: bdk_sp::bitcoin::secp256k1::PublicKey, + spend_pubkey: bdk_sp::bitcoin::secp256k1::PublicKey, + network: Network, + pretty: bool, +) -> Result { + let sp_code = SilentPaymentCode::new_v0(scan_pubkey, spend_pubkey, network); + if pretty { + let table = vec![vec![ + "sp_code".cell().bold(true), + sp_code.to_string().cell(), + ]] + .table() + .display() + .map_err(|e| Error::Generic(e.to_string()))?; + Ok(format!("{table}")) + } else { + Ok(serde_json::to_string_pretty( + &json!({"sp_code": sp_code.to_string()}), + )?) + } +} + /// Handle a key sub-command /// /// Key sub-commands are described in [`KeySubCommand`]. @@ -1348,6 +1372,14 @@ pub(crate) async fn handle_command(cli_opts: CliOpts) -> Result { let result = handle_key_subcommand(network, key_subcommand, pretty)?; Ok(result) } + #[cfg(feature = "sp")] + CliSubCommand::SilentPaymentCode { + scan, + spend + } => { + let result = handle_sp_subcommand(scan, spend, network, pretty)?; + Ok(result) + } #[cfg(feature = "compiler")] CliSubCommand::Compile { policy, From 8d15df7cc98c06349f7da5889ee893a56049a038 Mon Sep 17 00:00:00 2001 From: nymius <155548262+nymius@users.noreply.github.com> Date: Tue, 4 Nov 2025 11:04:17 -0300 Subject: [PATCH 10/10] doc(silentpayments): add README section for new silent payment commands --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index bca4e00..bbbba34 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,27 @@ To generate a new extended master key, suitable for use in a descriptor: cargo run -- key generate ``` +#### Silent payments + +> [!WARNING] +> This tool does not support silent payment scanning, nor the `silent_payment_code` +> command has any control on the public keys provided. If you don't have access +> to a silent payment scanner with the keys you provided, you are not going to +> be able to discover any funds, and if you do not control the private keys, +> you are not going to be able to spend the funds. We do not recommend the use +> of any of the silent payment features with real funds. + +To experiment with silent payments, you can get two public keys in compressed format, `A1` and `A2`, and produce a silent payment code by calling: +```shell +cargo run --features sp -- --network signet silent_payment_code --scan_public_key '' --spend_public_key '' +``` + +Once you have a silent payment code, `SP_CODE_1` and an amount `AMOUNT_1` to send, you can create a valid transaction locking funds to a silent payment code derived address with the following command: + +```shell +cargo run --features electrum,sp -- --network testnet4 wallet --wallet sample_wallet --ext-descriptor "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)" --database-type sqlite --client-type electrum --url "ssl://mempool.space:40002" create_sp_tx --to-sp : +``` + ## Justfile We have added the `just` command runner to help you with common commands (during development) and running regtest `bitcoind` if you are using the `rpc` feature.