diff --git a/payjoin-cli/CHANGELOG.md b/payjoin-cli/CHANGELOG.md index be97ca13f..6c41ca80d 100644 --- a/payjoin-cli/CHANGELOG.md +++ b/payjoin-cli/CHANGELOG.md @@ -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 `, which broadcasts the fallback transaction + if the payjoin fails. ## 1.0.0 diff --git a/payjoin-cli/src/app/v2/mod.rs b/payjoin-cli/src/app/v2/mod.rs index 924129b13..f1c174893 100644 --- a/payjoin-cli/src/app/v2/mod.rs +++ b/payjoin-cli/src/app/v2/mod.rs @@ -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)) } diff --git a/payjoin-ffi/CHANGELOG.md b/payjoin-ffi/CHANGELOG.md index 31f103cda..ac0a7d407 100644 --- a/payjoin-ffi/CHANGELOG.md +++ b/payjoin-ffi/CHANGELOG.md @@ -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 diff --git a/payjoin-ffi/src/receive/mod.rs b/payjoin-ffi/src/receive/mod.rs index b13dacb0d..fd9939614 100644 --- a/payjoin-ffi/src/receive/mod.rs +++ b/payjoin-ffi/src/receive/mod.rs @@ -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 { payjoin::bitcoin::consensus::encode::serialize( &self.0.clone().extract_tx_to_schedule_broadcast(), diff --git a/payjoin/CHANGELOG.md b/payjoin/CHANGELOG.md index 055cbfffe..a11dc6dd2 100644 --- a/payjoin/CHANGELOG.md +++ b/payjoin/CHANGELOG.md @@ -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::fallback_tx` after `cancel`, or from + `SessionHistory::fallback_tx`. +- Deprecate `Receiver::extract_tx_to_schedule_broadcast`. + Schedule the fallback broadcast by cancelling the session when the payjoin + fails: `cancel` yields a `Receiver` whose `fallback_tx` + is the transaction to broadcast. + ## 1.2.0 This release adds a non-blocking interface to the receiver state machine. diff --git a/payjoin/src/core/receive/v2/mod.rs b/payjoin/src/core/receive/v2/mod.rs index bbb0bbc3c..66c1fee0d 100644 --- a/payjoin/src/core/receive/v2/mod.rs +++ b/payjoin/src/core/receive/v2/mod.rs @@ -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; } @@ -407,9 +407,22 @@ impl 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::fallback_tx`]. +/// +/// ```compile_fail,E0624 +/// use payjoin::bitcoin::Transaction; +/// use payjoin::receive::v2::{HasFallbackTx, Receiver}; +/// +/// fn fallback_tx(receiver: &Receiver) -> Transaction { +/// receiver.fallback_tx() +/// } +/// ``` +#[allow(private_bounds)] pub trait HasFallbackTx: sealed::FallbackTx {} +#[allow(private_bounds)] impl HasFallbackTx for T {} /// A higher-level receiver construct which will be taken through different states through the @@ -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::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::cancel`] to obtain the +/// fallback transaction to broadcast. /// /// Call [`Receiver::check_inputs_not_owned`] to advance to /// [`Receiver`] to continue validation. @@ -915,10 +928,18 @@ pub struct MaybeInputsOwned { /// [`Receiver::inputs_owned_checklist`] and /// [`Receiver::apply_inputs_owned_checklist`]. impl Receiver { - /// 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::cancel`] instead and broadcast + /// [`Receiver::fallback_tx`]. /// /// Returns the extracted [`bitcoin::Transaction`]. + #[deprecated( + since = "0.0.0-unreleased", + note = "cancel the session when the payjoin fails and broadcast `Receiver::fallback_tx` instead" + )] pub fn extract_tx_to_schedule_broadcast(&self) -> bitcoin::Transaction { self.state.fallback_tx() } diff --git a/payjoin/src/core/receive/v2/session.rs b/payjoin/src/core/receive/v2/session.rs index 63fd3c3da..97887dafb 100644 --- a/payjoin/src/core/receive/v2/session.rs +++ b/payjoin/src/core/receive/v2/session.rs @@ -840,15 +840,10 @@ mod tests { #[tokio::test] async fn getting_fallback_tx() { - let persister = InMemoryPersister::::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())); @@ -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())); @@ -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())); diff --git a/payjoin/tests/integration.rs b/payjoin/tests/integration.rs index 2b67a4ce9..c62846142 100644 --- a/payjoin/tests/integration.rs +++ b/payjoin/tests/integration.rs @@ -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| {