You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
One place owns the canton-connect connection lifecycle: an XState machine where the invalid state combinations are unrepresentable by construction.
Rationale
CantonConnectProvider mirrors SDK state into ~10 useState fields written from six places. No single place owns the rules, so status: 'connected' with no party, isConnecting while disconnected, and an error beside a live session are all representable.
Five races were patched one at a time across #66, #67 and #63:
re-entrant connect;
disconnect racing a connect;
unmount racing a connect;
a late answer overwriting fresh state;
and an SDK swap leaving stale state.
All five are the same shape, an effect outliving the state that started it, so the fixes do not compose.
Scope
In scope:
The machine as internal model, grown test-first
Session states and wallet pushes
The machine replacing the provider's mirrored state
Picker UI (lives in canton-dappbooster + canton-theme)
Workarounds for SDK gaps (recorded in docs/dapp-sdk-issues.md instead)
New SDK capabilities
Architecture & technical considerations
There is no official lifecycle to transcribe: the OpenRPC dApp API spec carries no state model, the dApp API page defers to the SDK docs, and no SDK reference doc exists beyond release notes. The chart is our model of dapp-sdk 1.4.0's verified behavior and changes when the SDK does.
Decision (2026-08-11): the wallet picker is a black-box seam. The SDK popup path reports no intermediate steps, so the machine models no picker phases; a custom picker honors the same interface: final answer only. This supersedes the connecting inner states of the original sketch.
An xstate@5 machine owns the connection lifecycle, and the invalid combinations above are non representable by construction rather than guarded case by case
connect() publishes the connection state and also hands back an answer, and the wallet's pushes reach the dApp for exactly as long as a session exists
The five races above each have a regression test, and no test infers machine state from elapsed time
Objective
One place owns the canton-connect connection lifecycle: an XState machine where the invalid state combinations are unrepresentable by construction.
Rationale
CantonConnectProvidermirrors SDK state into ~10useStatefields written from six places. No single place owns the rules, sostatus: 'connected'with no party,isConnectingwhile disconnected, and an error beside a live session are all representable.Five races were patched one at a time across #66, #67 and #63:
All five are the same shape, an effect outliving the state that started it, so the fixes do not compose.
Scope
In scope:
Out of scope:
Architecture & technical considerations
There is no official lifecycle to transcribe: the OpenRPC dApp API spec carries no state model, the dApp API page defers to the SDK docs, and no SDK reference doc exists beyond release notes. The chart is our model of
dapp-sdk1.4.0's verified behavior and changes when the SDK does.Decision (2026-08-11): the wallet picker is a black-box seam. The SDK popup path reports no intermediate steps, so the machine models no picker phases; a custom picker honors the same interface: final answer only. This supersedes the
connectinginner states of the original sketch.stateDiagram-v2 state session { [*] --> unauthenticated unauthenticated --> connected: wallet.statusChanged [isAuthenticated] connected --> unauthenticated: wallet.statusChanged [isNotAuthenticated] connected --> connected: wallet.statusChanged / wallet.accountsChanged / party.selected } [*] --> idle idle --> restoring: session.restore idle --> connecting: connect.requested restoring --> connected: onDone [isRestoredAuthenticated] restoring --> unauthenticated: onDone [hasRestoredSession] restoring --> idle: onDone restoring --> idle: connect.cancelled restoring --> failed: onError connecting --> connected: wallet.answered [isAuthenticated] connecting --> unauthenticated: wallet.answered connecting --> failed: wallet.errored connecting --> idle: wallet.disconnected / connect.cancelled session --> disconnecting: wallet.disconnected session --> failed: readAccounts onError disconnecting --> idle: onDone / onError failed --> idle: connect.reset failed --> connecting: connect.requestedIssue breakdown
Acceptance criteria
xstate@5machine owns the connection lifecycle, and the invalid combinations above are non representable by construction rather than guarded case by caseconnect()publishes the connection state and also hands back an answer, and the wallet's pushes reach the dApp for exactly as long as a session exists