Skip to content

Expose fail() and cancel() on Receiver session#1134

Closed
spacebear21 wants to merge 2 commits intopayjoin:masterfrom
spacebear21:fail-cancel-session
Closed

Expose fail() and cancel() on Receiver session#1134
spacebear21 wants to merge 2 commits intopayjoin:masterfrom
spacebear21:fail-cancel-session

Conversation

@spacebear21
Copy link
Copy Markdown
Collaborator

This gives implementers the ability to manually fail or cancel a receiver Payjoin session. A user might cancel a session for any reason, or the wallet might fail it if e.g. it detects a double-spend or cannot proceed past a transient error.

Note that the unit tests for closed sessions will need to be reworked once #1132 is merged, since the expected_receiver_state should be an error.

I used Claude sonnet 4.5 (it's quite good) to write the generic methods and unit tests.

Pull Request Checklist

Please confirm the following before requesting review:

@coveralls
Copy link
Copy Markdown
Collaborator

coveralls commented Oct 1, 2025

Pull Request Test Coverage Report for Build 18177469540

Details

  • 56 of 56 (100.0%) changed or added relevant lines in 2 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.08%) to 84.648%

Totals Coverage Status
Change from base Build 18176477486: 0.08%
Covered Lines: 8756
Relevant Lines: 10344

💛 - Coveralls

This gives implementers the ability to manually fail or cancel a
receiver Payjoin session. A user might cancel a session for any reason,
or the wallet might fail it if e.g. it detects a double-spend or cannot
proceed past a transient error.
Copy link
Copy Markdown
Collaborator

@arminsabouri arminsabouri left a comment

Choose a reason for hiding this comment

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

CodeAck

Comment thread payjoin/src/core/receive/v2/mod.rs Outdated
///
/// This method allows implementations to terminate the payjoin session when
/// the user decides to cancel the operation interactively.
pub fn cancel<P>(self, persister: &P) -> Result<(), P::InternalStorageError>
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.

Do you think cancel is simpler than just calling close() on the persister?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The difference is that cancel() (and fail()) ensures that a Closed event is added to the log before closing.

Comment thread payjoin/src/core/receive/v2/mod.rs Outdated
/// This method allows implementations to terminate the payjoin session when
/// they encounter errors that cannot be resolved, such as insufficient
/// funds or a double-spend detection.
pub fn fail<P>(self, persister: &P) -> Result<(), P::InternalStorageError>
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.

Should we push a generic failure error reply? Otherwise the sender will be hanging until expiration.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes good point, we do need a way to communicate this (and probably cancel() too) back to the sender.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I started down an approach where they transition to HasReplyableError to enforce responding to the sender. However there is an outstanding issue in that it needs to differentiate between Cancel and Failure outcomes when replaying the event log, so I think GotReplyableError would need to carry this information somehow.

I'm not sure if we actually need to reply in case of Cancel, assuming the receiver would broadcast the fallback tx?

Copy link
Copy Markdown
Contributor

@DanGould DanGould Oct 3, 2025

Choose a reason for hiding this comment

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

Cancel sounds like the unavailable wellKnownError to me, even if broadcast is sufficient

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The only reason I can think of for a receiver to do this would be if the receiver wanted to refuse a naive tx.

A better solution for this example would be having the receiver use req-pj

So for now letting it expire is fine.

@spacebear21 spacebear21 marked this pull request as draft October 2, 2025 20:51
@spacebear21 spacebear21 added this to the payjoin-1.0 milestone Oct 3, 2025
@benalleng
Copy link
Copy Markdown
Collaborator

benalleng commented Oct 3, 2025

I wanted to pose a question as to why we really need to have these 2 seperate instead of just having application devs use fail() OR cancel() and provide a reason instead of having both of these?

Comment on lines +1207 to +1222
/// Explicitly fail the session.
///
/// This method allows implementations to terminate the payjoin session when
/// they encounter errors that cannot be resolved, such as insufficient
/// funds or a double-spend detection.
pub fn fail(self) -> NextStateTransition<SessionEvent, Receiver<HasReplyableError>> {
let err = JsonReply::new(crate::error_codes::ErrorCode::Unavailable, "Receiver error");
NextStateTransition::success(
SessionEvent::GotReplyableError(err.clone()),
Receiver {
state: HasReplyableError { error_reply: err, outcome: SessionOutcome::Failure },
session_context: self.session_context,
},
)
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Since failure encapsulates "protocol violations" this method seems unnecessary, everything user-initiated would be a "cancel"

pub fn cancel(self) -> NextStateTransition<SessionEvent, Receiver<HasReplyableError>> {
let err = JsonReply::new(crate::error_codes::ErrorCode::Unavailable, "Receiver error");
NextStateTransition::success(
SessionEvent::GotReplyableError(err.clone()),
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This should transition to "Monitoring" for the fallback transaction

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think I've convinced myself that transitioning to Receiver<Monitor> on cancel() just for monitoring the fallback tx is overkill.

  1. It would require refactoring the Monitor typestate to make PsbtContext optional, since we may not have a payjoin psbt when calling cancel().
  2. It would also require a new SessionEvent type like CancelInitiated that transitions to Monitor "early" during replay.
  3. It introduced ambiguity around the role of Monitor typestate. Currently this state is where we monitor for a set of possible transactions that the sender may broadcast. Monitoring on cancel() would mean we're now also monitoring a transaction broadcast by ourselves. The current SessionOutcome(FallbackBroadcasted) implies the session was closed by the sender. Now it might actually be the receiver who did? And shouldn't we be closing with SessionOutcome(Cancel) anyway?

The wallet already knows to track a tx that it broadcasted itself, and going through the monitor typestate for confirmation doesn't add any useful additional information that I can think of, especially considering all this additional complexity.

So I think cancel() should simply a) immediately close the session with SessionOutcome(Cancel) and b) return the fallback transaction to broadcast.

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.

Ack

@spacebear21 spacebear21 mentioned this pull request Apr 8, 2026
2 tasks
@DanGould
Copy link
Copy Markdown
Contributor

superseded by #1470

@DanGould DanGould closed this Apr 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants