@@ -17,6 +17,18 @@ use clap::Parser;
1717use serde_json:: json;
1818use std:: collections:: BTreeMap ;
1919use std:: str:: FromStr ;
20+ #[ cfg( feature = "silent-payments" ) ]
21+ use {
22+ crate :: utils:: common:: parse_sp_code_value_pairs,
23+ bdk_sp:: {
24+ bitcoin:: { PrivateKey , PublicKey } ,
25+ encoding:: SilentPaymentCode ,
26+ send:: psbt:: derive_sp,
27+ } ,
28+ bdk_wallet:: bitcoin:: key:: Secp256k1 ,
29+ bdk_wallet:: keys:: { DescriptorPublicKey , DescriptorSecretKey , SinglePubKey } ,
30+ std:: collections:: HashMap ,
31+ } ;
2032#[ cfg( feature = "bip322" ) ]
2133use {
2234 crate :: utils:: parse_signature_format,
@@ -41,6 +53,8 @@ impl OfflineWalletSubCommand {
4153 Self :: CreateTx ( createtx_command) => {
4254 createtx_command. execute ( ctx) ?. write_out ( std:: io:: stdout ( ) )
4355 }
56+ #[ cfg( feature = "silent-payments" ) ]
57+ Self :: CreateSpTx ( cmd) => cmd. execute ( ctx) ?. write_out ( std:: io:: stdout ( ) ) ,
4458 Self :: BumpFee ( bumpfee_command) => {
4559 bumpfee_command. execute ( ctx) ?. write_out ( std:: io:: stdout ( ) )
4660 }
@@ -293,6 +307,227 @@ impl AppCommand<AppContext<OfflineOperations<'_>>> for CreateTxCommand {
293307 }
294308}
295309
310+ #[ cfg( feature = "silent-payments" ) ]
311+ #[ derive( Debug , Parser , Clone , PartialEq ) ]
312+ pub struct CreateSpTxCommand {
313+ /// Adds a recipient to the transaction.
314+ // Clap Doesn't support complex vector parsing https://github.com/clap-rs/clap/issues/1704.
315+ // Address and amount parsing is done at run time in handler function.
316+ #[ arg( env = "ADDRESS:SAT" , long = "to" , required = false , value_parser = parse_recipient) ]
317+ pub recipients : Option < Vec < ( ScriptBuf , u64 ) > > ,
318+ /// Parse silent payment recipients
319+ #[ arg( long = "to-sp" , required = true , value_parser = parse_sp_code_value_pairs) ]
320+ pub silent_payment_recipients : Vec < ( SilentPaymentCode , u64 ) > ,
321+ /// Sends all the funds (or all the selected utxos). Requires only one recipient with value 0.
322+ #[ arg( long = "send_all" , short = 'a' ) ]
323+ pub send_all : bool ,
324+ /// 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.
325+ #[ arg( long = "offline_signer" ) ]
326+ pub offline_signer : bool ,
327+ /// Selects which utxos *must* be spent.
328+ #[ arg( env = "MUST_SPEND_TXID:VOUT" , long = "utxos" , value_parser = parse_outpoint) ]
329+ pub utxos : Option < Vec < OutPoint > > ,
330+ /// Marks a utxo as unspendable.
331+ #[ arg( env = "CANT_SPEND_TXID:VOUT" , long = "unspendable" , value_parser = parse_outpoint) ]
332+ pub unspendable : Option < Vec < OutPoint > > ,
333+ /// Fee rate to use in sat/vbyte.
334+ #[ arg( env = "SATS_VBYTE" , short = 'f' , long = "fee_rate" ) ]
335+ pub fee_rate : Option < f32 > ,
336+ /// Selects which policy should be used to satisfy the external descriptor.
337+ #[ arg( env = "EXT_POLICY" , long = "external_policy" ) ]
338+ pub external_policy : Option < String > ,
339+ /// Selects which policy should be used to satisfy the internal descriptor.
340+ #[ arg( env = "INT_POLICY" , long = "internal_policy" ) ]
341+ pub internal_policy : Option < String > ,
342+ /// Optionally create an OP_RETURN output containing given String in utf8 encoding (max 80 bytes)
343+ #[ arg(
344+ env = "ADD_STRING" ,
345+ long = "add_string" ,
346+ short = 's' ,
347+ conflicts_with = "add_data"
348+ ) ]
349+ pub add_string : Option < String > ,
350+ /// Optionally create an OP_RETURN output containing given base64 encoded String. (max 80 bytes)
351+ #[ arg(
352+ env = "ADD_DATA" ,
353+ long = "add_data" ,
354+ short = 'o' ,
355+ conflicts_with = "add_string"
356+ ) ]
357+ pub add_data : Option < String > , //base 64 econding
358+ }
359+
360+ #[ cfg( feature = "silent-payments" ) ]
361+ impl AppCommand < AppContext < OfflineOperations < ' _ > > > for CreateSpTxCommand {
362+ type Output = RawPsbt ;
363+
364+ fn execute ( & self , ctx : & mut AppContext < OfflineOperations < ' _ > > ) -> Result < Self :: Output , Error > {
365+ let mut tx_builder = ctx. state . wallet . build_tx ( ) ;
366+
367+ let sp_recipients: Vec < SilentPaymentCode > = self
368+ . silent_payment_recipients
369+ . iter ( )
370+ . map ( |( sp_code, _) | sp_code. clone ( ) )
371+ . collect ( ) ;
372+
373+ if self . send_all {
374+ if sp_recipients. len ( ) == 1 && self . recipients . is_none ( ) {
375+ tx_builder
376+ . drain_wallet ( )
377+ . drain_to ( sp_recipients[ 0 ] . get_placeholder_p2tr_spk ( ) ) ;
378+ } else if let Some ( ref recipients) = self . recipients
379+ && sp_recipients. is_empty ( )
380+ {
381+ if recipients. len ( ) == 1 {
382+ tx_builder. drain_wallet ( ) . drain_to ( recipients[ 0 ] . 0 . clone ( ) ) ;
383+ } else {
384+ return Err ( Error :: Generic (
385+ "Wallet can only be drain to a single output" . to_string ( ) ,
386+ ) ) ;
387+ }
388+ } else {
389+ return Err ( Error :: Generic (
390+ "Wallet can only be drain to a single output" . to_string ( ) ,
391+ ) ) ;
392+ }
393+ } else {
394+ let mut outputs: Vec < ( ScriptBuf , Amount ) > = self
395+ . silent_payment_recipients
396+ . iter ( )
397+ . map ( |( sp_code, amount) | {
398+ let script = sp_code. get_placeholder_p2tr_spk ( ) ;
399+ ( script, Amount :: from_sat ( * amount) )
400+ } )
401+ . collect ( ) ;
402+
403+ if let Some ( recipients) = & self . recipients {
404+ let recipients = recipients
405+ . iter ( )
406+ . map ( |( script, amount) | ( script. clone ( ) , Amount :: from_sat ( * amount) ) ) ;
407+
408+ outputs. extend ( recipients) ;
409+ }
410+
411+ tx_builder. set_recipients ( outputs) ;
412+ }
413+
414+ // Do not enable RBF for this transaction
415+ tx_builder. set_exact_sequence ( Sequence :: MAX ) ;
416+
417+ if self . offline_signer {
418+ tx_builder. include_output_redeem_witness_script ( ) ;
419+ }
420+
421+ if let Some ( fee_rate) = self . fee_rate
422+ && let Some ( fee_rate) = FeeRate :: from_sat_per_vb ( fee_rate as u64 )
423+ {
424+ tx_builder. fee_rate ( fee_rate) ;
425+ }
426+
427+ if let Some ( utxos) = & self . utxos {
428+ tx_builder
429+ . add_utxos ( & utxos[ ..] )
430+ . map_err ( |_| bdk_wallet:: error:: CreateTxError :: UnknownUtxo ) ?;
431+ }
432+
433+ if let Some ( unspendable) = & self . unspendable {
434+ tx_builder. unspendable ( unspendable. to_vec ( ) ) ;
435+ }
436+
437+ if let Some ( base64_data) = & self . add_data {
438+ let op_return_data = BASE64_STANDARD
439+ . decode ( base64_data)
440+ . map_err ( |e| Error :: Generic ( e. to_string ( ) ) ) ?;
441+ tx_builder. add_data (
442+ & PushBytesBuf :: try_from ( op_return_data)
443+ . map_err ( |e| Error :: Generic ( e. to_string ( ) ) ) ?,
444+ ) ;
445+ } else if let Some ( string_data) = & self . add_string {
446+ let data = PushBytesBuf :: try_from ( string_data. as_bytes ( ) . to_vec ( ) )
447+ . map_err ( |e| Error :: Generic ( e. to_string ( ) ) ) ?;
448+ tx_builder. add_data ( & data) ;
449+ }
450+
451+ let policies = vec ! [
452+ self . external_policy
453+ . as_ref( )
454+ . map( |p| ( p, KeychainKind :: External ) ) ,
455+ self . internal_policy
456+ . as_ref( )
457+ . map( |p| ( p, KeychainKind :: Internal ) ) ,
458+ ] ;
459+
460+ for ( policy, keychain) in policies. into_iter ( ) . flatten ( ) {
461+ let policy = serde_json:: from_str :: < BTreeMap < String , Vec < usize > > > ( policy) ?;
462+ tx_builder. policy_path ( policy, keychain) ;
463+ }
464+
465+ let mut psbt = tx_builder. finish ( ) ?;
466+
467+ let unsigned_psbt = psbt. clone ( ) ;
468+
469+ let finalized = ctx. state . wallet . sign ( & mut psbt, SignOptions :: default ( ) ) ?;
470+
471+ if !finalized {
472+ return Err ( Error :: Generic (
473+ "Cannot produce silent payment outputs without intermediate signing phase."
474+ . to_string ( ) ,
475+ ) ) ;
476+ }
477+
478+ for ( full_input, psbt_input) in unsigned_psbt. inputs . iter ( ) . zip ( psbt. inputs . iter_mut ( ) ) {
479+ // repopulate key derivation data
480+ psbt_input. bip32_derivation = full_input. bip32_derivation . clone ( ) ;
481+ psbt_input. tap_key_origins = full_input. tap_key_origins . clone ( ) ;
482+ }
483+
484+ let secp = Secp256k1 :: new ( ) ;
485+ let mut external_signers = ctx
486+ . state
487+ . wallet
488+ . get_signers ( KeychainKind :: External )
489+ . as_key_map ( & secp) ;
490+ let internal_signers = ctx
491+ . state
492+ . wallet
493+ . get_signers ( KeychainKind :: Internal )
494+ . as_key_map ( & secp) ;
495+ external_signers. extend ( internal_signers) ;
496+
497+ match external_signers. iter ( ) . next ( ) . expect ( "not empty" ) {
498+ ( DescriptorPublicKey :: Single ( single_pub) , DescriptorSecretKey :: Single ( prv) ) => {
499+ match single_pub. key {
500+ SinglePubKey :: FullKey ( pk) => {
501+ let keys: HashMap < PublicKey , PrivateKey > = [ ( pk, prv. key ) ] . into ( ) ;
502+ derive_sp ( & mut psbt, & keys, & sp_recipients, & secp) . expect ( "will fix later" ) ;
503+ }
504+ SinglePubKey :: XOnly ( xonly) => {
505+ let keys: HashMap < bdk_sp:: bitcoin:: XOnlyPublicKey , PrivateKey > =
506+ [ ( xonly, prv. key ) ] . into ( ) ;
507+ derive_sp ( & mut psbt, & keys, & sp_recipients, & secp) . expect ( "will fix later" ) ;
508+ }
509+ } ;
510+ }
511+ ( _, DescriptorSecretKey :: XPrv ( k) ) => {
512+ derive_sp ( & mut psbt, & k. xkey , & sp_recipients, & secp) . expect ( "will fix later" ) ;
513+ }
514+ _ => unimplemented ! ( "multi xkey signer" ) ,
515+ } ;
516+
517+ // Unfinalize PSBT to resign
518+ for psbt_input in psbt. inputs . iter_mut ( ) {
519+ psbt_input. final_script_sig = None ;
520+ psbt_input. final_script_witness = None ;
521+ }
522+
523+ let _resigned = ctx. state . wallet . sign ( & mut psbt, SignOptions :: default ( ) ) ?;
524+
525+ let raw_tx = psbt. extract_tx ( ) ?;
526+
527+ Ok ( RawPsbt :: new ( & raw_tx) )
528+ }
529+ }
530+
296531#[ derive( Debug , Parser , Clone , PartialEq ) ]
297532pub struct BumpFeeCommand {
298533 /// TXID of the transaction to update.
0 commit comments