@@ -82,15 +82,7 @@ impl<'a> SenderBuilder<'a> {
8282 self ,
8383 min_fee_rate : FeeRate ,
8484 ) -> MaybeBadInitInputsTransition < SessionEvent , Sender < WithReplyKey > , BuildSenderError > {
85- let v1 = match self . 0 . build_recommended ( min_fee_rate) {
86- Ok ( inner) => inner,
87- Err ( e) => return MaybeBadInitInputsTransition :: bad_init_inputs ( e) ,
88- } ;
89- let with_reply_key = WithReplyKey { v1, reply_key : HpkeKeyPair :: gen_keypair ( ) . 0 } ;
90- MaybeBadInitInputsTransition :: success (
91- SessionEvent :: CreatedReplyKey ( with_reply_key. clone ( ) ) ,
92- Sender { state : with_reply_key } ,
93- )
85+ self . v2_sender_from_v1 ( self . 0 . clone ( ) . build_recommended ( min_fee_rate) )
9486 }
9587
9688 /// Offer the receiver contribution to pay for his input.
@@ -113,21 +105,12 @@ impl<'a> SenderBuilder<'a> {
113105 min_fee_rate : FeeRate ,
114106 clamp_fee_contribution : bool ,
115107 ) -> MaybeBadInitInputsTransition < SessionEvent , Sender < WithReplyKey > , BuildSenderError > {
116- let v1 = match self . 0 . build_with_additional_fee (
108+ self . v2_sender_from_v1 ( self . 0 . clone ( ) . build_with_additional_fee (
117109 max_fee_contribution,
118110 change_index,
119111 min_fee_rate,
120112 clamp_fee_contribution,
121- ) {
122- Ok ( inner) => inner,
123- Err ( e) => return MaybeBadInitInputsTransition :: bad_init_inputs ( e) ,
124- } ;
125-
126- let with_reply_key = WithReplyKey { v1, reply_key : HpkeKeyPair :: gen_keypair ( ) . 0 } ;
127- MaybeBadInitInputsTransition :: success (
128- SessionEvent :: CreatedReplyKey ( with_reply_key. clone ( ) ) ,
129- Sender { state : with_reply_key } ,
130- )
113+ ) )
131114 }
132115
133116 /// Perform Payjoin without incentivizing the payee to cooperate.
@@ -138,11 +121,26 @@ impl<'a> SenderBuilder<'a> {
138121 self ,
139122 min_fee_rate : FeeRate ,
140123 ) -> MaybeBadInitInputsTransition < SessionEvent , Sender < WithReplyKey > , BuildSenderError > {
141- let v1 = match self . 0 . build_non_incentivizing ( min_fee_rate) {
124+ self . v2_sender_from_v1 ( self . 0 . clone ( ) . build_non_incentivizing ( min_fee_rate) )
125+ }
126+
127+ /// Helper function that takes a V1 sender build result and wraps it in a V2 Sender,
128+ /// returning the appropriate state transition.
129+ fn v2_sender_from_v1 (
130+ & self ,
131+ v1_result : Result < v1:: Sender , BuildSenderError > ,
132+ ) -> MaybeBadInitInputsTransition < SessionEvent , Sender < WithReplyKey > , BuildSenderError > {
133+ let mut v1 = match v1_result {
142134 Ok ( inner) => inner,
143135 Err ( e) => return MaybeBadInitInputsTransition :: bad_init_inputs ( e) ,
144136 } ;
145137
138+ // V2 senders may always ignore the receiver's `pjos` output substitution preference,
139+ // because all communications with the receiver are end-to-end authenticated.
140+ if self . 0 . output_substitution == OutputSubstitution :: Enabled {
141+ v1. output_substitution = OutputSubstitution :: Enabled ;
142+ }
143+
146144 let with_reply_key = WithReplyKey { v1, reply_key : HpkeKeyPair :: gen_keypair ( ) . 0 } ;
147145 MaybeBadInitInputsTransition :: success (
148146 SessionEvent :: CreatedReplyKey ( with_reply_key. clone ( ) ) ,
@@ -507,9 +505,12 @@ mod test {
507505 use std:: time:: { Duration , SystemTime } ;
508506
509507 use bitcoin:: hex:: FromHex ;
508+ use bitcoin:: Address ;
510509 use payjoin_test_utils:: { BoxError , EXAMPLE_URL , KEM , KEY_ID , PARSED_ORIGINAL_PSBT , SYMMETRIC } ;
511510
512511 use super :: * ;
512+ use crate :: persist:: NoopSessionPersister ;
513+ use crate :: receive:: v2:: Receiver ;
513514 use crate :: OhttpKeys ;
514515
515516 const SERIALIZED_BODY_V2 : & str = "63484e696450384241484d43414141414159386e757447674a647959475857694245623435486f65396c5747626b78682f36624e694f4a6443447544414141414141442b2f2f2f2f41747956754155414141414146366b554865684a38476e536442554f4f7636756a584c72576d734a5244434867495165414141414141415871525233514a62627a30686e513849765130667074476e2b766f746e656f66544141414141414542494b6762317755414141414146366b55336b34656b47484b57524e6241317256357452356b455644564e4348415163584667415578347046636c4e56676f31575741644e3153594e583874706854414243477343527a424541694238512b41366465702b527a393276687932366c5430416a5a6e3450524c6938426639716f422f434d6b30774967502f526a3250575a3367456a556b546c6844524e415130675877544f3774396e2b563134705a366f6c6a554249514d566d7341616f4e5748564d5330324c6654536530653338384c4e697450613155515a794f6968592b464667414241425941464562324769753663344b4f35595730706677336c4770396a4d55554141413d0a763d32" ;
@@ -626,4 +627,49 @@ mod test {
626627 }
627628 Ok ( ( ) )
628629 }
630+
631+ #[ test]
632+ fn test_v2_sender_builder ( ) {
633+ let address = Address :: from_str ( "2N47mmrWXsNBvQR6k78hWJoTji57zXwNcU7" )
634+ . expect ( "valid address" )
635+ . assume_checked ( ) ;
636+ let directory = EXAMPLE_URL . clone ( ) ;
637+ let ohttp_keys = OhttpKeys (
638+ ohttp:: KeyConfig :: new ( KEY_ID , KEM , Vec :: from ( SYMMETRIC ) ) . expect ( "valid key config" ) ,
639+ ) ;
640+ let pj_uri = Receiver :: create_session ( address. clone ( ) , directory, ohttp_keys, None )
641+ . save ( & NoopSessionPersister :: default ( ) )
642+ . expect ( "receiver should succeed" )
643+ . pj_uri ( ) ;
644+ let req_ctx = SenderBuilder :: new ( PARSED_ORIGINAL_PSBT . clone ( ) , pj_uri. clone ( ) )
645+ . build_recommended ( FeeRate :: BROADCAST_MIN )
646+ . save ( & NoopSessionPersister :: default ( ) )
647+ . expect ( "sender should succeed" ) ;
648+ // v2 senders may always override the receiver's `pjos` parameter to enable output
649+ // substitution
650+ assert_eq ! ( req_ctx. v1. output_substitution, OutputSubstitution :: Enabled ) ;
651+ assert_eq ! ( & req_ctx. v1. payee, & address. script_pubkey( ) ) ;
652+ let fee_contribution = req_ctx. v1 . fee_contribution . expect ( "sender should contribute fees" ) ;
653+ assert_eq ! ( fee_contribution. max_amount, Amount :: from_sat( 91 ) ) ;
654+ assert_eq ! ( fee_contribution. vout, 0 ) ;
655+ assert_eq ! ( req_ctx. v1. min_fee_rate, FeeRate :: from_sat_per_kwu( 250 ) ) ;
656+ // ensure that the other builder methods also enable output substitution
657+ let req_ctx = SenderBuilder :: new ( PARSED_ORIGINAL_PSBT . clone ( ) , pj_uri. clone ( ) )
658+ . build_non_incentivizing ( FeeRate :: BROADCAST_MIN )
659+ . save ( & NoopSessionPersister :: default ( ) )
660+ . expect ( "sender should succeed" ) ;
661+ assert_eq ! ( req_ctx. v1. output_substitution, OutputSubstitution :: Enabled ) ;
662+ let req_ctx = SenderBuilder :: new ( PARSED_ORIGINAL_PSBT . clone ( ) , pj_uri. clone ( ) )
663+ . build_with_additional_fee ( Amount :: ZERO , Some ( 0 ) , FeeRate :: BROADCAST_MIN , false )
664+ . save ( & NoopSessionPersister :: default ( ) )
665+ . expect ( "sender should succeed" ) ;
666+ assert_eq ! ( req_ctx. v1. output_substitution, OutputSubstitution :: Enabled ) ;
667+ // ensure that a v2 sender may still disable output substitution if they prefer.
668+ let req_ctx = SenderBuilder :: new ( PARSED_ORIGINAL_PSBT . clone ( ) , pj_uri)
669+ . always_disable_output_substitution ( )
670+ . build_recommended ( FeeRate :: BROADCAST_MIN )
671+ . save ( & NoopSessionPersister :: default ( ) )
672+ . expect ( "sender should succeed" ) ;
673+ assert_eq ! ( req_ctx. v1. output_substitution, OutputSubstitution :: Disabled ) ;
674+ }
629675}
0 commit comments