@@ -75,15 +75,7 @@ impl<'a> SenderBuilder<'a> {
7575 self ,
7676 min_fee_rate : FeeRate ,
7777 ) -> MaybeBadInitInputsTransition < SessionEvent , Sender < WithReplyKey > , BuildSenderError > {
78- let v1 = match self . 0 . build_recommended ( min_fee_rate) {
79- Ok ( inner) => inner,
80- Err ( e) => return MaybeBadInitInputsTransition :: bad_init_inputs ( e) ,
81- } ;
82- let with_reply_key = WithReplyKey { v1, reply_key : HpkeKeyPair :: gen_keypair ( ) . 0 } ;
83- MaybeBadInitInputsTransition :: success (
84- SessionEvent :: CreatedReplyKey ( with_reply_key. clone ( ) ) ,
85- Sender { state : with_reply_key } ,
86- )
78+ self . v2_sender_from_v1 ( self . 0 . clone ( ) . build_recommended ( min_fee_rate) )
8779 }
8880
8981 /// Offer the receiver contribution to pay for his input.
@@ -106,21 +98,12 @@ impl<'a> SenderBuilder<'a> {
10698 min_fee_rate : FeeRate ,
10799 clamp_fee_contribution : bool ,
108100 ) -> MaybeBadInitInputsTransition < SessionEvent , Sender < WithReplyKey > , BuildSenderError > {
109- let v1 = match self . 0 . build_with_additional_fee (
101+ self . v2_sender_from_v1 ( self . 0 . clone ( ) . build_with_additional_fee (
110102 max_fee_contribution,
111103 change_index,
112104 min_fee_rate,
113105 clamp_fee_contribution,
114- ) {
115- Ok ( inner) => inner,
116- Err ( e) => return MaybeBadInitInputsTransition :: bad_init_inputs ( e) ,
117- } ;
118-
119- let with_reply_key = WithReplyKey { v1, reply_key : HpkeKeyPair :: gen_keypair ( ) . 0 } ;
120- MaybeBadInitInputsTransition :: success (
121- SessionEvent :: CreatedReplyKey ( with_reply_key. clone ( ) ) ,
122- Sender { state : with_reply_key } ,
123- )
106+ ) )
124107 }
125108
126109 /// Perform Payjoin without incentivizing the payee to cooperate.
@@ -131,11 +114,26 @@ impl<'a> SenderBuilder<'a> {
131114 self ,
132115 min_fee_rate : FeeRate ,
133116 ) -> MaybeBadInitInputsTransition < SessionEvent , Sender < WithReplyKey > , BuildSenderError > {
134- let v1 = match self . 0 . build_non_incentivizing ( min_fee_rate) {
117+ self . v2_sender_from_v1 ( self . 0 . clone ( ) . build_non_incentivizing ( min_fee_rate) )
118+ }
119+
120+ /// Helper function that takes a V1 sender build result and wraps it in a V2 Sender,
121+ /// returning the appropriate state transition.
122+ fn v2_sender_from_v1 (
123+ & self ,
124+ v1_result : Result < v1:: Sender , BuildSenderError > ,
125+ ) -> MaybeBadInitInputsTransition < SessionEvent , Sender < WithReplyKey > , BuildSenderError > {
126+ let mut v1 = match v1_result {
135127 Ok ( inner) => inner,
136128 Err ( e) => return MaybeBadInitInputsTransition :: bad_init_inputs ( e) ,
137129 } ;
138130
131+ // V2 senders may always ignore the receiver's `pjos` output substitution preference,
132+ // because all communications with the receiver are end-to-end authenticated.
133+ if self . 0 . output_substitution == OutputSubstitution :: Enabled {
134+ v1. output_substitution = OutputSubstitution :: Enabled ;
135+ }
136+
139137 let with_reply_key = WithReplyKey { v1, reply_key : HpkeKeyPair :: gen_keypair ( ) . 0 } ;
140138 MaybeBadInitInputsTransition :: success (
141139 SessionEvent :: CreatedReplyKey ( with_reply_key. clone ( ) ) ,
@@ -495,9 +493,12 @@ mod test {
495493 use std:: time:: { Duration , SystemTime } ;
496494
497495 use bitcoin:: hex:: FromHex ;
496+ use bitcoin:: Address ;
498497 use payjoin_test_utils:: { BoxError , EXAMPLE_URL , KEM , KEY_ID , PARSED_ORIGINAL_PSBT , SYMMETRIC } ;
499498
500499 use super :: * ;
500+ use crate :: persist:: NoopSessionPersister ;
501+ use crate :: receive:: v2:: Receiver ;
501502 use crate :: OhttpKeys ;
502503
503504 const SERIALIZED_BODY_V2 : & str = "63484e696450384241484d43414141414159386e757447674a647959475857694245623435486f65396c5747626b78682f36624e694f4a6443447544414141414141442b2f2f2f2f41747956754155414141414146366b554865684a38476e536442554f4f7636756a584c72576d734a5244434867495165414141414141415871525233514a62627a30686e513849765130667074476e2b766f746e656f66544141414141414542494b6762317755414141414146366b55336b34656b47484b57524e6241317256357452356b455644564e4348415163584667415578347046636c4e56676f31575741644e3153594e583874706854414243477343527a424541694238512b41366465702b527a393276687932366c5430416a5a6e3450524c6938426639716f422f434d6b30774967502f526a3250575a3367456a556b546c6844524e415130675877544f3774396e2b563134705a366f6c6a554249514d566d7341616f4e5748564d5330324c6654536530653338384c4e697450613155515a794f6968592b464667414241425941464562324769753663344b4f35595730706677336c4770396a4d55554141413d0a763d32" ;
@@ -614,4 +615,49 @@ mod test {
614615 }
615616 Ok ( ( ) )
616617 }
618+
619+ #[ test]
620+ fn test_v2_sender_builder ( ) {
621+ let address = Address :: from_str ( "2N47mmrWXsNBvQR6k78hWJoTji57zXwNcU7" )
622+ . expect ( "valid address" )
623+ . assume_checked ( ) ;
624+ let directory = EXAMPLE_URL . clone ( ) ;
625+ let ohttp_keys = OhttpKeys (
626+ ohttp:: KeyConfig :: new ( KEY_ID , KEM , Vec :: from ( SYMMETRIC ) ) . expect ( "valid key config" ) ,
627+ ) ;
628+ let pj_uri = Receiver :: create_session ( address. clone ( ) , directory, ohttp_keys, None )
629+ . save ( & NoopSessionPersister :: default ( ) )
630+ . expect ( "receiver should succeed" )
631+ . pj_uri ( ) ;
632+ let req_ctx = SenderBuilder :: new ( PARSED_ORIGINAL_PSBT . clone ( ) , pj_uri. clone ( ) )
633+ . build_recommended ( FeeRate :: BROADCAST_MIN )
634+ . save ( & NoopSessionPersister :: default ( ) )
635+ . expect ( "sender should succeed" ) ;
636+ // v2 senders may always override the receiver's `pjos` parameter to enable output
637+ // substitution
638+ assert_eq ! ( req_ctx. v1. output_substitution, OutputSubstitution :: Enabled ) ;
639+ assert_eq ! ( & req_ctx. v1. payee, & address. script_pubkey( ) ) ;
640+ let fee_contribution = req_ctx. v1 . fee_contribution . expect ( "sender should contribute fees" ) ;
641+ assert_eq ! ( fee_contribution. max_amount, Amount :: from_sat( 91 ) ) ;
642+ assert_eq ! ( fee_contribution. vout, 0 ) ;
643+ assert_eq ! ( req_ctx. v1. min_fee_rate, FeeRate :: from_sat_per_kwu( 250 ) ) ;
644+ // ensure that the other builder methods also enable output substitution
645+ let req_ctx = SenderBuilder :: new ( PARSED_ORIGINAL_PSBT . clone ( ) , pj_uri. clone ( ) )
646+ . build_non_incentivizing ( FeeRate :: BROADCAST_MIN )
647+ . save ( & NoopSessionPersister :: default ( ) )
648+ . expect ( "sender should succeed" ) ;
649+ assert_eq ! ( req_ctx. v1. output_substitution, OutputSubstitution :: Enabled ) ;
650+ let req_ctx = SenderBuilder :: new ( PARSED_ORIGINAL_PSBT . clone ( ) , pj_uri. clone ( ) )
651+ . build_with_additional_fee ( Amount :: ZERO , Some ( 0 ) , FeeRate :: BROADCAST_MIN , false )
652+ . save ( & NoopSessionPersister :: default ( ) )
653+ . expect ( "sender should succeed" ) ;
654+ assert_eq ! ( req_ctx. v1. output_substitution, OutputSubstitution :: Enabled ) ;
655+ // ensure that a v2 sender may still disable output substitution if they prefer.
656+ let req_ctx = SenderBuilder :: new ( PARSED_ORIGINAL_PSBT . clone ( ) , pj_uri)
657+ . always_disable_output_substitution ( )
658+ . build_recommended ( FeeRate :: BROADCAST_MIN )
659+ . save ( & NoopSessionPersister :: default ( ) )
660+ . expect ( "sender should succeed" ) ;
661+ assert_eq ! ( req_ctx. v1. output_substitution, OutputSubstitution :: Disabled ) ;
662+ }
617663}
0 commit comments