Skip to content

Receiver fallback typestate#1558

Draft
spacebear21 wants to merge 8 commits into
payjoin:masterfrom
spacebear21:fallback-typestate
Draft

Receiver fallback typestate#1558
spacebear21 wants to merge 8 commits into
payjoin:masterfrom
spacebear21:fallback-typestate

Conversation

@spacebear21
Copy link
Copy Markdown
Collaborator

@spacebear21 spacebear21 commented May 14, 2026

This draft illustrates the idea described here for the receiver side. Supersedes #1542.

Planned with Claude Opus 4.7, implemented by Codex 5.5

Pull Request Checklist

Please confirm the following before requesting review:

Introduce HasFallback: State with a non-Option fallback_tx() and
implement it on the in-protocol receiver states whose contract
includes a confirmed broadcastable fallback.
- UncheckedOriginalPayload
is deliberately excluded; it holds the sender's Original PSBT but
has not yet run check_broadcast_suitability, so the PSBT is not yet
verified as broadcastable.
- HasReplyableError is also excluded; it
will gain an optional fallback field in a later commit and continues
to model the absent-fallback case at runtime.

The existing sealed::State::fallback_tx() returns Option<Transaction>
because it lives on the parent trait every state implements, including
ones that hold no fallback. It will be removed in a later commit in
favor of the HasFallback trait
Fix to pickup local changes in JS bindings.
PendingFallback represents a receiver session that was cancelled or
hit a fatal protocol error, and has a fallback transaction available
to broadcast.

While the session sits in PendingFallback the implementer holds an
obligation to broadcast, discard, or otherwise handle the fallback
transaction (e.g. save it to wallet DB for later broadcasting). This
state is preserved across restarts and session replays until the
implemeter calls `close()`, indicating that the handoff of the
fallback transaction is complete and no longer a payjoin concern.
HasReplyableError represents a receiver session that hit a replyable
error before reaching PendingFallback. The struct must model the
runtime fact that some sources can hand it a verified broadcastable
fallback and others cannot. Encoding the field as
Option<Transaction> keeps that distinction at the type level without
weakening the HasFallback trait contract.
Introduce MaybeTerminalTransition for the no-error fork (used by
cancel) and MaybeTerminalSuccessTransition for the error-bearing
fork (used by process_error_response). Both expose advance and
terminate constructors that map to Save and SaveAndClose actions
respectively. The success variant returns Option<NextState>; the
error variants preserve the caller's distinction between transient,
fatal-advance, and fatal-terminate.
The receiver side of v2 had a single blanket cancel implementation
that always terminated the session and handed the wallet an
Option<Transaction>. Fatal protocol errors emitted Closed(Failure)
directly. Both shapes lost the wallet's obligation to broadcast the
original transaction across a restart whenever a fallback existed.

Replace the blanket cancel with typestate-aware impls:
- impl<S: HasFallback> Receiver<S>::cancel advances to PendingFallback
- Receiver<Initialized>::cancel and Receiver<UncheckedOriginalPayload>
  ::cancel terminate with Closed(Cancel); neither holds a verified
  fallback
- Receiver<HasReplyableError>::cancel forks on the optional fallback:
  Some advances to PendingFallback, None terminates with Closed(Cancel)
The receiver side now lets a session pause in PendingFallback after a
cancel or a fatal protocol error. The cli needs a wallet-facing way to
enter that state, finish it (by broadcasting or discarding), and pick
it up on resume.
@spacebear21 spacebear21 changed the title Fallback typestate Receiver fallback typestate May 14, 2026
@arminsabouri arminsabouri mentioned this pull request May 15, 2026
2 tasks
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.

1 participant