Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions payjoin/src/core/receive/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,17 @@ impl PsbtContext {
let payjoin_proposal = self.prepare_psbt(signed_psbt);
Ok(payjoin_proposal)
}

fn utxos_to_be_locked(&self) -> impl '_ + Iterator<Item = &bitcoin::OutPoint> {
let sender_input_indexes = self.sender_input_indexes();
self.payjoin_psbt
.unsigned_tx
.input
.iter()
.enumerate()
.filter(move |(i, _)| !sender_input_indexes.contains(i))
.map(|(_, input)| &input.previous_output)
}
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
Expand Down
28 changes: 24 additions & 4 deletions payjoin/src/core/receive/v1/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,11 +292,14 @@ impl ProvisionalProposal {
self,
wallet_process_psbt: impl Fn(&Psbt) -> Result<Psbt, ImplementationError>,
) -> Result<PayjoinProposal, Error> {
let original_psbt = self.psbt_context.original_psbt.clone();

@xstoicunicornx xstoicunicornx May 19, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I think you can avoid this clone() by:

  • updating PsbtContext::finalize_proposal() to not consume self
  • updating PsbtContext::prepare_psbt() to not consume self
  • move this assignment below let finalized_psbt = ... assignment

let finalized_psbt = self
.psbt_context
.finalize_proposal(wallet_process_psbt)
.map_err(|e| Error::Implementation(ImplementationError::new(e)))?;
Ok(PayjoinProposal { payjoin_psbt: finalized_psbt })
Ok(PayjoinProposal {
psbt_context: PsbtContext { payjoin_psbt: finalized_psbt, original_psbt },
})
}

/// The Payjoin proposal PSBT that the receiver needs to sign
Expand All @@ -311,17 +314,17 @@ impl ProvisionalProposal {
/// should find acceptable.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PayjoinProposal {
payjoin_psbt: Psbt,
psbt_context: PsbtContext,
}

impl PayjoinProposal {
/// The UTXOs that would be spent by this Payjoin transaction.
pub fn utxos_to_be_locked(&self) -> impl '_ + Iterator<Item = &bitcoin::OutPoint> {
self.payjoin_psbt.unsigned_tx.input.iter().map(|input| &input.previous_output)
self.psbt_context.utxos_to_be_locked()
}

/// The Payjoin Proposal PSBT.
pub fn psbt(&self) -> &Psbt { &self.payjoin_psbt }
pub fn psbt(&self) -> &Psbt { &self.psbt_context.payjoin_psbt }
}

#[cfg(test)]
Expand Down Expand Up @@ -549,4 +552,21 @@ mod tests {
let psbt = provisional_proposal.psbt_to_sign();
assert_eq!(psbt, PARSED_PAYJOIN_PROPOSAL.clone());
}

#[test]
fn test_utxos_to_be_locked() {
let provisional_proposal = ProvisionalProposal {
psbt_context: PsbtContext {
payjoin_psbt: PARSED_PAYJOIN_PROPOSAL.clone(),
original_psbt: PARSED_ORIGINAL_PSBT.clone(),
},
};
let finalized_proposal = provisional_proposal
.clone()
.finalize_proposal(|_| Ok(PARSED_PAYJOIN_PROPOSAL.clone()))
.unwrap();
let utxos = finalized_proposal.utxos_to_be_locked().collect::<Vec<_>>();
assert_eq!(utxos.len(), 1);
assert_eq!(*utxos[0], PARSED_PAYJOIN_PROPOSAL.unsigned_tx.input[1].previous_output);
}
}
5 changes: 2 additions & 3 deletions payjoin/src/core/receive/v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1149,9 +1149,8 @@ pub struct PayjoinProposal {
impl Receiver<PayjoinProposal> {
/// The UTXOs that would be spent by this Payjoin transaction.
pub fn utxos_to_be_locked(&self) -> impl '_ + Iterator<Item = &bitcoin::OutPoint> {
// TODO: de-duplicate this with the v1 implementation
// It would make more sense if the payjoin proposal was only available after utxos are locked via session persister
self.psbt_context.payjoin_psbt.unsigned_tx.input.iter().map(|input| &input.previous_output)
// TODO: It would make more sense if the payjoin proposal was only available after utxos are locked via session persister
self.psbt_context.utxos_to_be_locked()
}

/// The Payjoin Proposal PSBT.
Expand Down
Loading