Skip to content
Open
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
4 changes: 4 additions & 0 deletions payjoin-cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
- Fix panic when spending taproot UTXOs. They are now contributed as key-path
spends with an assumed default input weight.
- Any unsupported coin is simply skipped instead of crashing the cli.
- The v2 receiver no longer prints the fallback transaction hex when the
Original PSBT arrives. The message now points to
`payjoin-cli cancel <session-id>`, which broadcasts the fallback transaction
if the payjoin fails.

## 1.0.0

Expand Down
8 changes: 4 additions & 4 deletions payjoin-cli/src/app/v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1028,10 +1028,10 @@ impl App {
let proposal =
proposal.apply_broadcast_suitability(None, is_broadcast_suitable).save(persister)?;

persister.print(
"Fallback transaction received. Consider broadcasting this to get paid if the Payjoin fails:",
);
println!("{}", serialize_hex(&proposal.extract_tx_to_schedule_broadcast()));
let id = persister.session_id();
persister.print(format_args!(
"Fallback transaction received. Run `payjoin-cli cancel {id}` to cancel and broadcast it if the Payjoin fails."
));
Ok(ReceiveSession::MaybeInputsOwned(proposal))
}

Expand Down
4 changes: 4 additions & 0 deletions payjoin-ffi/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## Unreleased

- Deprecate `MaybeInputsOwned.extract_tx_to_schedule_broadcast`. Cancel the
session when the payjoin fails and broadcast the `fallback_tx` of the
resulting `ReceiverPendingFallback` instead.

## [0.25.0]

- Publish the Python, JavaScript, C#, and Dart packages from one signed
Expand Down
5 changes: 5 additions & 0 deletions payjoin-ffi/src/receive/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -942,10 +942,15 @@ pub trait IsInputOwned: Send + Sync {

#[uniffi::export]
impl MaybeInputsOwned {
/// Deprecated since payjoin 0.0.0-unreleased: cancel the session when the payjoin
/// fails and broadcast the `fallback_tx` of the resulting `ReceiverPendingFallback`
/// instead.
///
/// Extract the transaction from the Original PSBT for scheduling broadcast as a
/// fallback in case the payjoin does not complete.
///
/// Returns the consensus-encoded raw transaction bytes.
#[allow(deprecated)]
pub fn extract_tx_to_schedule_broadcast(&self) -> Vec<u8> {
payjoin::bitcoin::consensus::encode::serialize(
&self.0.clone().extract_tx_to_schedule_broadcast(),
Expand Down
10 changes: 10 additions & 0 deletions payjoin/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

## Unreleased

- Close a leak in the `HasFallbackTx` seal. Generic code bounded on the trait
could call the sealed `fallback_tx` method, which was never part of the
documented API; such code no longer compiles. Read the fallback transaction
from `Receiver<PendingFallback>::fallback_tx` after `cancel`, or from
`SessionHistory::fallback_tx`.
- Deprecate `Receiver<MaybeInputsOwned>::extract_tx_to_schedule_broadcast`.
Schedule the fallback broadcast by cancelling the session when the payjoin
fails: `cancel` yields a `Receiver<PendingFallback>` whose `fallback_tx`
is the transaction to broadcast.

## 1.2.0

This release adds a non-blocking interface to the receiver state machine.
Expand Down
35 changes: 28 additions & 7 deletions payjoin/src/core/receive/v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ mod sealed {
impl State for super::Monitor {}
impl State for super::PendingFallback {}

pub trait FallbackTx: State {
pub(crate) trait FallbackTx: State {
fn fallback_tx(&self) -> bitcoin::Transaction;
}

Expand Down Expand Up @@ -407,9 +407,22 @@ impl<S: sealed::State> State for S {}
/// Marker trait for receiver protocol states that hold a verified broadcastable
/// fallback transaction.
///
/// This trait is sealed to prevent external implementations.
/// This trait is sealed to prevent external implementations. It does not expose the
/// fallback transaction itself: cancel the session with [`Receiver::cancel`] and read it
/// from [`Receiver<PendingFallback>::fallback_tx`].
///
/// ```compile_fail,E0624
/// use payjoin::bitcoin::Transaction;
/// use payjoin::receive::v2::{HasFallbackTx, Receiver};
///
/// fn fallback_tx<S: HasFallbackTx>(receiver: &Receiver<S>) -> Transaction {
/// receiver.fallback_tx()
/// }
/// ```
#[allow(private_bounds)]
pub trait HasFallbackTx: sealed::FallbackTx {}

#[allow(private_bounds)]
impl<T: sealed::FallbackTx> HasFallbackTx for T {}

/// A higher-level receiver construct which will be taken through different states through the
Expand Down Expand Up @@ -904,9 +917,9 @@ pub struct MaybeInputsOwned {

/// Typestate to check that the Original PSBT has no inputs owned by the receiver.
///
/// At this point, the Original PSBT has been verified as broadcastable; the receiver
/// can call [`Receiver<MaybeInputsOwned>::extract_tx_to_schedule_broadcast`] to
/// schedule a fallback broadcast in case the payjoin fails.
/// At this point, the Original PSBT has been verified as broadcastable. If the payjoin
/// fails from here on, call [`Receiver<MaybeInputsOwned>::cancel`] to obtain the
/// fallback transaction to broadcast.
///
/// Call [`Receiver<MaybeInputsOwned>::check_inputs_not_owned`] to advance to
/// [`Receiver<MaybeInputsSeen>`] to continue validation.
Expand All @@ -915,10 +928,18 @@ pub struct MaybeInputsOwned {
/// [`Receiver<MaybeInputsOwned>::inputs_owned_checklist`] and
/// [`Receiver<MaybeInputsOwned>::apply_inputs_owned_checklist`].
impl Receiver<MaybeInputsOwned> {
/// Extract the transaction from the Original PSBT for scheduling broadcast as a
/// fallback in case the payjoin does not complete.
/// Extract the transaction from the Original PSBT.
///
/// Broadcasting it from here bypasses the session: the event log never learns that
/// the fallback was sent. When the payjoin fails, call
/// [`Receiver<MaybeInputsOwned>::cancel`] instead and broadcast
/// [`Receiver<PendingFallback>::fallback_tx`].
///
/// Returns the extracted [`bitcoin::Transaction`].
#[deprecated(
since = "0.0.0-unreleased",
note = "cancel the session when the payjoin fails and broadcast `Receiver<PendingFallback>::fallback_tx` instead"
)]
pub fn extract_tx_to_schedule_broadcast(&self) -> bitcoin::Transaction {
self.state.fallback_tx()
}
Expand Down
11 changes: 3 additions & 8 deletions payjoin/src/core/receive/v2/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -840,15 +840,10 @@ mod tests {

#[tokio::test]
async fn getting_fallback_tx() {
let persister = InMemoryPersister::<SessionEvent>::default();
let session_context = SHARED_CONTEXT.clone();
let mut events = vec![];
let original = original_from_test_vector();
let maybe_inputs_owned = unchecked_receiver_from_test_vector()
.assume_interactive_receiver()
.save(&persister)
.unwrap();
let expected_fallback = maybe_inputs_owned.extract_tx_to_schedule_broadcast();
let expected_fallback = original.psbt.clone().extract_tx_unchecked_fee_rate();
let reply_key = Some(crate::HpkeKeyPair::gen_keypair().1);

events.push(SessionEvent::Created(session_context.clone()));
Expand Down Expand Up @@ -1006,7 +1001,7 @@ mod tests {
.apply_fee_range(None, None)
.save(&persister)
.expect("Contributed inputs should be valid");
let expected_fallback = maybe_inputs_owned.extract_tx_to_schedule_broadcast();
let expected_fallback = original.psbt.clone().extract_tx_unchecked_fee_rate();
let reply_key = Some(crate::HpkeKeyPair::gen_keypair().1);

events.push(SessionEvent::Created(session_context.clone()));
Expand Down Expand Up @@ -1087,7 +1082,7 @@ mod tests {
.finalize_proposal(|psbt| Ok(psbt.clone()))
.save(&persister)
.expect("Payjoin proposal should be finalized");
let expected_fallback = maybe_inputs_owned.extract_tx_to_schedule_broadcast();
let expected_fallback = original.psbt.clone().extract_tx_unchecked_fee_rate();
let reply_key = Some(crate::HpkeKeyPair::gen_keypair().1);

events.push(SessionEvent::Created(session_context.clone()));
Expand Down
3 changes: 0 additions & 3 deletions payjoin/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1165,9 +1165,6 @@ mod integration {
})
.save(recv_persister)?;

// in a payment processor where the sender could go offline, this is where you schedule to broadcast the original_tx
let _to_broadcast_in_failure_case = proposal.extract_tx_to_schedule_broadcast();

// Receive Check 2: receiver can't sign for proposal inputs
let proposal = proposal
.check_inputs_not_owned(&mut |outpoint| {
Expand Down
Loading