@@ -26,7 +26,7 @@ use url::Url;
2626
2727use crate :: output_substitution:: OutputSubstitution ;
2828use crate :: psbt:: PsbtExt ;
29- use crate :: Version ;
29+ use crate :: { Request , Version , MAX_CONTENT_LENGTH } ;
3030
3131// See usize casts
3232#[ cfg( not( any( target_pointer_width = "32" , target_pointer_width = "64" ) ) ) ]
@@ -37,20 +37,195 @@ mod error;
3737#[ cfg( feature = "v1" ) ]
3838#[ cfg_attr( docsrs, doc( cfg( feature = "v1" ) ) ) ]
3939pub mod v1;
40- #[ cfg( not( feature = "v1" ) ) ]
41- pub ( crate ) mod v1;
4240
4341#[ cfg( feature = "v2" ) ]
4442#[ cfg_attr( docsrs, doc( cfg( feature = "v2" ) ) ) ]
4543pub mod v2;
46- #[ cfg( all( feature = "v2" , not( feature = "v1" ) ) ) ]
47- pub use v1:: V1Context ;
4844
4945#[ cfg( feature = "_multiparty" ) ]
5046pub mod multiparty;
5147
5248type InternalResult < T > = Result < T , InternalProposalError > ;
5349
50+ /// A builder to construct the properties of a `PsbtContext`.
51+ #[ derive( Clone ) ]
52+ pub ( crate ) struct PsbtContextBuilder {
53+ pub ( crate ) psbt : Psbt ,
54+ pub ( crate ) payee : ScriptBuf ,
55+ pub ( crate ) amount : Option < bitcoin:: Amount > ,
56+ pub ( crate ) fee_contribution : Option < ( bitcoin:: Amount , Option < usize > ) > ,
57+ /// Decreases the fee contribution instead of erroring.
58+ ///
59+ /// If this option is true and a transaction with change amount lower than fee
60+ /// contribution is provided then instead of returning error the fee contribution will
61+ /// be just lowered in the request to match the change amount.
62+ pub ( crate ) clamp_fee_contribution : bool ,
63+ pub ( crate ) min_fee_rate : FeeRate ,
64+ }
65+
66+ /// We only need to add the weight of the txid: 32, index: 4 and sequence: 4 as rust_bitcoin
67+ /// already accounts for the scriptsig length when calculating InputWeightPrediction
68+ /// <https://docs.rs/bitcoin/latest/src/bitcoin/blockdata/transaction.rs.html#1621>
69+ const NON_WITNESS_INPUT_WEIGHT : bitcoin:: Weight = Weight :: from_non_witness_data_size ( 32 + 4 + 4 ) ;
70+
71+ impl PsbtContextBuilder {
72+ /// Prepare the context from which to make Sender requests
73+ ///
74+ /// Call [`PsbtContextBuilder::build_recommended()`] or other `build` methods
75+ /// to create a [`PsbtContext`]
76+ pub fn new ( psbt : Psbt , payee : ScriptBuf , amount : Option < bitcoin:: Amount > ) -> Self {
77+ Self {
78+ psbt,
79+ payee,
80+ amount,
81+ // Sender's optional parameters
82+ fee_contribution : None ,
83+ clamp_fee_contribution : false ,
84+ min_fee_rate : FeeRate :: ZERO ,
85+ }
86+ }
87+
88+ // Calculate the recommended fee contribution for an Original PSBT.
89+ //
90+ // BIP 78 recommends contributing `originalPSBTFeeRate * vsize(sender_input_type)`.
91+ // The minfeerate parameter is set if the contribution is available in change.
92+ //
93+ // This method fails if no recommendation can be made or if the PSBT is malformed.
94+ pub fn build_recommended (
95+ self ,
96+ min_fee_rate : FeeRate ,
97+ output_substitution : OutputSubstitution ,
98+ ) -> Result < PsbtContext , BuildSenderError > {
99+ // TODO support optional batched payout scripts. This would require a change to
100+ // build() which now checks for a single payee.
101+ let mut payout_scripts = std:: iter:: once ( self . payee . clone ( ) ) ;
102+
103+ // Check if the PSBT is a sweep transaction with only one output that's a payout script and no change
104+ if self . psbt . unsigned_tx . output . len ( ) == 1
105+ && payout_scripts. all ( |script| script == self . psbt . unsigned_tx . output [ 0 ] . script_pubkey )
106+ {
107+ return self . build_non_incentivizing ( min_fee_rate, output_substitution) ;
108+ }
109+
110+ if let Some ( ( additional_fee_index, fee_available) ) = self
111+ . psbt
112+ . unsigned_tx
113+ . output
114+ . clone ( )
115+ . into_iter ( )
116+ . enumerate ( )
117+ . find ( |( _, txo) | payout_scripts. all ( |script| script != txo. script_pubkey ) )
118+ . map ( |( i, txo) | ( i, txo. value ) )
119+ {
120+ let mut input_pairs = self . psbt . input_pairs ( ) ;
121+ let first_input_pair = input_pairs. next ( ) . ok_or ( InternalBuildSenderError :: NoInputs ) ?;
122+ let mut input_weight = first_input_pair
123+ . expected_input_weight ( )
124+ . map_err ( InternalBuildSenderError :: InputWeight ) ?;
125+ for input_pair in input_pairs {
126+ // use cheapest default if mixed input types
127+ if input_pair. address_type ( ) ? != first_input_pair. address_type ( ) ? {
128+ input_weight =
129+ bitcoin:: transaction:: InputWeightPrediction :: P2TR_KEY_NON_DEFAULT_SIGHASH
130+ . weight ( )
131+ + NON_WITNESS_INPUT_WEIGHT ;
132+ break ;
133+ }
134+ }
135+
136+ let recommended_additional_fee = min_fee_rate * input_weight;
137+ if fee_available < recommended_additional_fee {
138+ log:: warn!( "Insufficient funds to maintain specified minimum feerate." ) ;
139+ return self . build_with_additional_fee (
140+ fee_available,
141+ Some ( additional_fee_index) ,
142+ min_fee_rate,
143+ true ,
144+ output_substitution,
145+ ) ;
146+ }
147+ return self . build_with_additional_fee (
148+ recommended_additional_fee,
149+ Some ( additional_fee_index) ,
150+ min_fee_rate,
151+ false ,
152+ output_substitution,
153+ ) ;
154+ }
155+ self . build_non_incentivizing ( min_fee_rate, output_substitution)
156+ }
157+
158+ /// Offer the receiver contribution to pay for his input.
159+ ///
160+ /// These parameters will allow the receiver to take `max_fee_contribution` from given change
161+ /// output to pay for additional inputs. The recommended fee is `size_of_one_input * fee_rate`.
162+ ///
163+ /// `change_index` specifies which output can be used to pay fee. If `None` is provided, then
164+ /// the output is auto-detected unless the supplied transaction has more than two outputs.
165+ ///
166+ /// `clamp_fee_contribution` decreases fee contribution instead of erroring.
167+ ///
168+ /// If this option is true and a transaction with change amount lower than fee
169+ /// contribution is provided then instead of returning error the fee contribution will
170+ /// be just lowered in the request to match the change amount.
171+ pub fn build_with_additional_fee (
172+ mut self ,
173+ max_fee_contribution : bitcoin:: Amount ,
174+ change_index : Option < usize > ,
175+ min_fee_rate : FeeRate ,
176+ clamp_fee_contribution : bool ,
177+ output_substitution : OutputSubstitution ,
178+ ) -> Result < PsbtContext , BuildSenderError > {
179+ self . fee_contribution = Some ( ( max_fee_contribution, change_index) ) ;
180+ self . clamp_fee_contribution = clamp_fee_contribution;
181+ self . min_fee_rate = min_fee_rate;
182+ self . build ( output_substitution)
183+ }
184+
185+ /// Perform Payjoin without incentivizing the payee to cooperate.
186+ ///
187+ /// While it's generally better to offer some contribution some users may wish not to.
188+ /// This function disables contribution.
189+ pub fn build_non_incentivizing (
190+ mut self ,
191+ min_fee_rate : FeeRate ,
192+ output_substitution : OutputSubstitution ,
193+ ) -> Result < PsbtContext , BuildSenderError > {
194+ // since this is a builder, these should already be cleared
195+ // but we'll reset them to be sure
196+ self . fee_contribution = None ;
197+ self . clamp_fee_contribution = false ;
198+ self . min_fee_rate = min_fee_rate;
199+ self . build ( output_substitution)
200+ }
201+
202+ fn build (
203+ self ,
204+ output_substitution : OutputSubstitution ,
205+ ) -> Result < PsbtContext , BuildSenderError > {
206+ let mut psbt =
207+ self . psbt . validate ( ) . map_err ( InternalBuildSenderError :: InconsistentOriginalPsbt ) ?;
208+ psbt. validate_input_utxos ( ) . map_err ( InternalBuildSenderError :: InvalidOriginalInput ) ?;
209+
210+ check_single_payee ( & psbt, & self . payee , self . amount ) ?;
211+ let fee_contribution = determine_fee_contribution (
212+ & psbt,
213+ & self . payee ,
214+ self . fee_contribution ,
215+ self . clamp_fee_contribution ,
216+ ) ?;
217+ clear_unneeded_fields ( & mut psbt) ;
218+
219+ Ok ( PsbtContext {
220+ original_psbt : psbt,
221+ output_substitution,
222+ fee_contribution,
223+ min_fee_rate : self . min_fee_rate ,
224+ payee : self . payee ,
225+ } )
226+ }
227+ }
228+
54229#[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
55230#[ cfg_attr( feature = "v2" , derive( serde:: Serialize , serde:: Deserialize ) ) ]
56231pub ( crate ) struct AdditionalFeeContribution {
@@ -478,6 +653,56 @@ fn serialize_url(
478653 url
479654}
480655
656+ /// Construct serialized V1 Request and Context from a Payjoin Proposal
657+ pub ( crate ) fn create_v1_post_request ( endpoint : Url , psbt_ctx : PsbtContext ) -> ( Request , V1Context ) {
658+ let url = serialize_url (
659+ endpoint. clone ( ) ,
660+ psbt_ctx. output_substitution ,
661+ psbt_ctx. fee_contribution ,
662+ psbt_ctx. min_fee_rate ,
663+ Version :: One ,
664+ ) ;
665+ let body = psbt_ctx. original_psbt . to_string ( ) . as_bytes ( ) . to_vec ( ) ;
666+ (
667+ Request :: new_v1 ( & url, & body) ,
668+ V1Context {
669+ psbt_context : PsbtContext {
670+ original_psbt : psbt_ctx. original_psbt . clone ( ) ,
671+ output_substitution : psbt_ctx. output_substitution ,
672+ fee_contribution : psbt_ctx. fee_contribution ,
673+ payee : psbt_ctx. payee . clone ( ) ,
674+ min_fee_rate : psbt_ctx. min_fee_rate ,
675+ } ,
676+ } ,
677+ )
678+ }
679+
680+ /// Data required to validate the response.
681+ ///
682+ /// This type is used to process a BIP78 response.
683+ /// Call [`Self::process_response`] on it to continue the BIP78 flow.
684+ #[ derive( Debug , Clone ) ]
685+ pub struct V1Context {
686+ psbt_context : PsbtContext ,
687+ }
688+
689+ impl V1Context {
690+ /// Decodes and validates the response.
691+ ///
692+ /// Call this method with response from receiver to continue BIP78 flow. If the response is
693+ /// valid you will get appropriate PSBT that you should sign and broadcast.
694+ #[ inline]
695+ pub fn process_response ( self , response : & [ u8 ] ) -> Result < Psbt , ResponseError > {
696+ if response. len ( ) > MAX_CONTENT_LENGTH {
697+ return Err ( ResponseError :: from ( InternalValidationError :: ContentTooLarge ) ) ;
698+ }
699+
700+ let res_str = std:: str:: from_utf8 ( response) . map_err ( |_| InternalValidationError :: Parse ) ?;
701+ let proposal = Psbt :: from_str ( res_str) . map_err ( |_| ResponseError :: parse ( res_str) ) ?;
702+ self . psbt_context . process_proposal ( proposal) . map_err ( Into :: into)
703+ }
704+ }
705+
481706#[ cfg( test) ]
482707mod test {
483708 use std:: str:: FromStr ;
0 commit comments