Skip to content

Commit 7a0c45f

Browse files
Create ProvisionalProposal non-blocking interface
1 parent a499811 commit 7a0c45f

4 files changed

Lines changed: 64 additions & 36 deletions

File tree

payjoin/src/core/receive/common/mod.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -862,8 +862,7 @@ mod tests {
862862
.commit_inputs()
863863
.calculate_psbt_context_with_fee_range(None, None)
864864
.expect("Contributed inputs should allow for valid fee contributions");
865-
let payjoin_proposal =
866-
psbt_context.finalize_proposal(|_| Ok(processed_psbt.clone())).expect("Valid psbt");
865+
let payjoin_proposal = psbt_context.finalize_proposal(&processed_psbt).expect("Valid psbt");
867866

868867
assert!(payjoin_proposal.xpub.is_empty());
869868

payjoin/src/core/receive/mod.rs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -255,12 +255,12 @@ pub struct PsbtContext {
255255

256256
impl PsbtContext {
257257
/// Prepare the PSBT by creating a new PSBT and copying only the fields allowed by the [spec](https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#senders-payjoin-proposal-checklist)
258-
fn prepare_psbt(self, processed_psbt: Psbt) -> Psbt {
258+
fn prepare_psbt(self, processed_psbt: &Psbt) -> Psbt {
259259
tracing::trace!("Original PSBT from callback: {processed_psbt:#?}");
260260

261261
// Create a new PSBT and copy only the allowed fields
262262
let mut filtered_psbt = Psbt {
263-
unsigned_tx: processed_psbt.unsigned_tx,
263+
unsigned_tx: processed_psbt.unsigned_tx.clone(),
264264
version: processed_psbt.version,
265265
xpub: BTreeMap::new(),
266266
proprietary: BTreeMap::new(),
@@ -313,33 +313,33 @@ impl PsbtContext {
313313
sender_input_indexes
314314
}
315315

316-
/// Finalizes the Payjoin proposal into a PSBT which the sender will find acceptable before
317-
/// they sign the transaction and broadcast it to the network.
318-
///
319-
/// Finalization consists of two steps:
320-
/// 1. Remove all sender signatures which were received with the original PSBT as these signatures are now invalid.
321-
/// 2. Sign and finalize the resulting PSBT using the passed `wallet_process_psbt` signing function.
322-
fn finalize_proposal(
323-
self,
324-
wallet_process_psbt: impl Fn(&Psbt) -> Result<Psbt, ImplementationError>,
325-
) -> Result<Psbt, ImplementationError> {
316+
/// Return the payjoin PSBT that is ready for signing
317+
fn payjoin_psbt_without_sender_signatures(&self) -> Psbt {
326318
let mut psbt = self.payjoin_psbt.clone();
327319
// Remove now-invalid sender signatures before applying the receiver signatures
328320
for i in self.sender_input_indexes() {
329-
tracing::trace!("Clearing sender input {i}");
330321
psbt.inputs[i].final_script_sig = None;
331322
psbt.inputs[i].final_script_witness = None;
332323
psbt.inputs[i].tap_key_sig = None;
333324
}
334-
let finalized_psbt = wallet_process_psbt(&psbt)?;
325+
psbt
326+
}
327+
328+
/// Finalizes the Payjoin proposal into a PSBT which the sender will find acceptable before
329+
/// they sign the transaction and broadcast it to the network.
330+
///
331+
/// Finalization consists of two steps:
332+
/// 1. Validate that signed psbt contains expected inputs and outputs
333+
/// 2. Prepare the psbt to be sent to sender
334+
fn finalize_proposal(self, signed_psbt: &Psbt) -> Result<Psbt, ImplementationError> {
335335
let expected_ntxid = self.payjoin_psbt.unsigned_tx.compute_ntxid();
336-
let actual_ntxid = finalized_psbt.unsigned_tx.compute_ntxid();
336+
let actual_ntxid = signed_psbt.unsigned_tx.compute_ntxid();
337337
if expected_ntxid != actual_ntxid {
338338
return Err(ImplementationError::from(
339339
format!("Ntxid mismatch: expected {expected_ntxid}, got {actual_ntxid}").as_str(),
340340
));
341341
}
342-
let payjoin_proposal = self.prepare_psbt(finalized_psbt);
342+
let payjoin_proposal = self.prepare_psbt(signed_psbt);
343343
Ok(payjoin_proposal)
344344
}
345345
}

payjoin/src/core/receive/v1/mod.rs

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,10 @@ impl crate::receive::common::WantsFeeRange {
365365
/// by the receiver. The receiver may sign and finalize the Payjoin proposal which will be sent to
366366
/// the sender for their signature.
367367
///
368-
/// Call [`Self::finalize_proposal`] to return a finalized [`PayjoinProposal`].
368+
/// Call [`Self::finalize_proposal`] to return a finalized [`PayjoinProposal`]. If caller needs to use
369+
/// non-blocking calls for signing the payjoin proposal psbt call [`Self::psbt_to_sign`] to
370+
/// extract the psbt to be signed and [`Self::finalize_signed_proposal`] to return the
371+
/// receiver signed proposal psbt and proceed to the next state.
369372
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
370373
pub struct ProvisionalProposal {
371374
psbt_context: PsbtContext,
@@ -382,19 +385,25 @@ impl ProvisionalProposal {
382385
self,
383386
wallet_process_psbt: impl Fn(&Psbt) -> Result<Psbt, ImplementationError>,
384387
) -> Result<PayjoinProposal, Error> {
385-
let finalized_psbt = self
386-
.psbt_context
387-
.finalize_proposal(wallet_process_psbt)
388-
.map_err(|e| Error::Implementation(ImplementationError::new(e)))?;
389-
Ok(PayjoinProposal { payjoin_psbt: finalized_psbt })
388+
let psbt = self.psbt_context.payjoin_psbt_without_sender_signatures();
389+
let signed_psbt = wallet_process_psbt(&psbt)?;
390+
self.finalize_signed_proposal(&signed_psbt)
390391
}
391392

392393
/// The Payjoin proposal PSBT that the receiver needs to sign
393394
///
394-
/// In some applications the entity that progresses the typestate
395-
/// is different from the entity that has access to the private keys,
396-
/// so the PSBT to sign must be accessible to such implementers.
397-
pub fn psbt_to_sign(&self) -> Psbt { self.psbt_context.payjoin_psbt.clone() }
395+
/// Use this for non-blocking signing of psbt
396+
pub fn psbt_to_sign(&self) -> Psbt {
397+
self.psbt_context.payjoin_psbt_without_sender_signatures()
398+
}
399+
400+
/// Process receiver signed payjoin proposal
401+
///
402+
/// Use this for non-blocking signing of psbt
403+
pub fn finalize_signed_proposal(self, signed_psbt: &Psbt) -> Result<PayjoinProposal, Error> {
404+
let finalized_psbt = self.psbt_context.finalize_proposal(signed_psbt)?;
405+
Ok(PayjoinProposal { payjoin_psbt: finalized_psbt })
406+
}
398407
}
399408

400409
/// A finalized Payjoin proposal, complete with fees and receiver signatures, that the sender

payjoin/src/core/receive/v2/mod.rs

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,6 +1158,10 @@ pub struct ProvisionalProposal {
11581158
/// the sender for their signature.
11591159
///
11601160
/// Call [`Receiver<ProvisionalProposal>::finalize_proposal`] to return a finalized [`PayjoinProposal`].
1161+
/// If caller needs to use non-blocking calls for signing the payjoin proposal psbt call
1162+
/// [`Receiver<ProvisionalProposal>::psbt_to_sign`] to extract the psbt to be signed and
1163+
/// [`Receiver<ProvisionalProposal>::finalize_signed_proposal`] to return the receiver signed proposal
1164+
/// psbt and proceed to the next state.
11611165
impl Receiver<ProvisionalProposal> {
11621166
/// Finalizes the Payjoin proposal into a PSBT which the sender will find acceptable before
11631167
/// they re-sign the transaction and broadcast it to the network.
@@ -1169,9 +1173,32 @@ impl Receiver<ProvisionalProposal> {
11691173
self,
11701174
wallet_process_psbt: impl Fn(&Psbt) -> Result<Psbt, ImplementationError>,
11711175
) -> MaybeTransientTransition<SessionEvent, Receiver<PayjoinProposal>, ImplementationError>
1176+
{
1177+
let psbt = self.state.psbt_context.payjoin_psbt_without_sender_signatures();
1178+
let signed_psbt = wallet_process_psbt(&psbt);
1179+
match signed_psbt {
1180+
Ok(signed_psbt) => self.finalize_signed_proposal(&signed_psbt),
1181+
Err(e) => MaybeTransientTransition::transient(e),
1182+
}
1183+
}
1184+
1185+
/// The Payjoin proposal PSBT that the receiver needs to sign
1186+
///
1187+
/// Use this for non-blocking signing of psbt
1188+
pub fn psbt_to_sign(&self) -> Psbt {
1189+
self.psbt_context.payjoin_psbt_without_sender_signatures()
1190+
}
1191+
1192+
/// Process receiver signed payjoin proposal
1193+
///
1194+
/// Use this for non-blocking signing of psbt
1195+
pub fn finalize_signed_proposal(
1196+
self,
1197+
signed_psbt: &Psbt,
1198+
) -> MaybeTransientTransition<SessionEvent, Receiver<PayjoinProposal>, ImplementationError>
11721199
{
11731200
let original_psbt = self.state.psbt_context.original_psbt.clone();
1174-
let inner = match self.state.psbt_context.finalize_proposal(wallet_process_psbt) {
1201+
let inner = match self.state.psbt_context.finalize_proposal(signed_psbt) {
11751202
Ok(inner) => inner,
11761203
Err(e) => {
11771204
return MaybeTransientTransition::transient(e);
@@ -1185,13 +1212,6 @@ impl Receiver<ProvisionalProposal> {
11851212
)
11861213
}
11871214

1188-
/// The Payjoin proposal PSBT that the receiver needs to sign
1189-
///
1190-
/// In some applications the entity that progresses the typestate
1191-
/// is different from the entity that has access to the private keys,
1192-
/// so the PSBT to sign must be accessible to such implementers.
1193-
pub fn psbt_to_sign(&self) -> Psbt { self.state.psbt_context.payjoin_psbt.clone() }
1194-
11951215
pub(crate) fn apply_payjoin_proposal(self, payjoin_psbt: Psbt) -> ReceiveSession {
11961216
let psbt_context = PsbtContext {
11971217
payjoin_psbt,

0 commit comments

Comments
 (0)