//! All subcommands are defined in the below enums.
#![allow(clippy::large_enum_variant)]
+#[cfg(feature = "silent-payments")]
+use {crate::utils::parse_sp_code_value_pairs, bdk_sp::encoding::SilentPaymentCode};
+
use bdk_wallet::bitcoin::{
Address, Network, OutPoint, ScriptBuf,
bip32::{DerivationPath, Xpriv},
#[arg(value_enum)]
shell: Shell,
},
+ /// 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 = "silent-payments")]
+ SilentPaymentCode {
+ /// The scan public key to use on the silent payment code.
+ #[arg(long = "scan_key")]
+ scan: bdk_sp::bitcoin::secp256k1::PublicKey,
+ /// The spend public key to use on the silent payment code.
+ #[arg(long = "spend_key")]
+ spend: bdk_sp::bitcoin::secp256k1::PublicKey,
+ },
}
/// Wallet operation subcommands.
)]
add_data: Option<String>, //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 = "silent-payments")]
+ 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<Vec<(ScriptBuf, u64)>>,
+ /// 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<Vec<OutPoint>>,
+ /// Marks a utxo as unspendable.
+ #[arg(env = "CANT_SPEND_TXID:VOUT", long = "unspendable", value_parser = parse_outpoint)]
+ unspendable: Option<Vec<OutPoint>>,
+ /// Fee rate to use in sat/vbyte.
+ #[arg(env = "SATS_VBYTE", short = 'f', long = "fee_rate")]
+ fee_rate: Option<f32>,
+ /// Selects which policy should be used to satisfy the external descriptor.
+ #[arg(env = "EXT_POLICY", long = "external_policy")]
+ external_policy: Option<String>,
+ /// Selects which policy should be used to satisfy the internal descriptor.
+ #[arg(env = "INT_POLICY", long = "internal_policy")]
+ internal_policy: Option<String>,
+ /// 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<String>,
+ /// 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<String>, //base 64 econding
+ },
/// Bumps the fees of an RBF transaction.
BumpFee {
/// TXID of the transaction to update.
use clap::CommandFactory;
use cli_table::{Cell, CellStruct, Style, Table, format::Justify};
use serde_json::json;
+#[cfg(feature = "silent-payments")]
+use {
+ bdk_sp::{
+ bitcoin::{PrivateKey, PublicKey, ScriptBuf},
+ encoding::SilentPaymentCode,
+ send::psbt::derive_sp,
+ },
+ bdk_wallet::keys::{DescriptorPublicKey, DescriptorSecretKey, SinglePubKey},
+};
#[cfg(feature = "electrum")]
use crate::utils::BlockchainClient::Electrum;
)?)
}
}
+ #[cfg(feature = "silent-payments")]
+ 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<SilentPaymentCode> = silent_payment_recipients
+ .iter()
+ .map(|(sp_code, _)| sp_code.clone())
+ .collect();
+
+ if send_all {
+ if sp_recipients.len() == 1 && maybe_recipients.is_none() {
+ tx_builder
+ .drain_wallet()
+ .drain_to(sp_recipients[0].get_placeholder_p2tr_spk());
+ } else if let Some(ref recipients) = maybe_recipients
+ && sp_recipients.is_empty()
+ {
+ if recipients.len() == 1 {
+ tx_builder.drain_wallet().drain_to(recipients[0].0.clone());
+ } else {
+ return Err(Error::Generic(
+ "Wallet can only be drain to a single output".to_string(),
+ ));
+ }
+ } else {
+ return Err(Error::Generic(
+ "Wallet can only be drain to a single output".to_string(),
+ ));
+ }
+ } else {
+ 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 {
+ 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
+ && 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[..])
+ .map_err(|_| bdk_wallet::error::CreateTxError::UnknownUtxo)?;
+ }
+
+ 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)
+ .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())
+ .map_err(|e| Error::Generic(e.to_string()))?;
+ 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::<BTreeMap<String, Vec<usize>>>(&policy)?;
+ tx_builder.policy_path(policy, keychain);
+ }
+
+ let mut psbt = tx_builder.finish()?;
+
+ let unsigned_psbt = psbt.clone();
+
+ let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
+
+ if !finalized {
+ return Err(Error::Generic(
+ "Cannot produce silent payment outputs without intermediate signing phase."
+ .to_string(),
+ ));
+ }
+
+ 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<PublicKey, PrivateKey> = [(pk, prv.key)].into();
+ derive_sp(&mut psbt, &keys, &sp_recipients, &secp)
+ .expect("will fix later");
+ }
+ SinglePubKey::XOnly(xonly) => {
+ let keys: HashMap<bdk_sp::bitcoin::XOnlyPublicKey, PrivateKey> =
+ [(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,
}
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 {
}
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);
}
Ok(())
}
+#[cfg(feature = "silent-payments")]
+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<String, Error> {
+ 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`].
let result = handle_key_subcommand(network, key_subcommand, pretty)?;
Ok(result)
}
+ #[cfg(feature = "silent-payments")]
+ CliSubCommand::SilentPaymentCode { scan, spend } => {
+ let network = cli_opts.network;
+ let result = handle_sp_subcommand(scan, spend, network, pretty)?;
+ Ok(result)
+ }
#[cfg(feature = "compiler")]
CliSubCommand::Compile {
policy,