Skip to content

feat(connect): the provider and the hooks run on the connection machine - #126

Open
fernandomg wants to merge 30 commits into
mainfrom
feat/76-provider
Open

feat(connect): the provider and the hooks run on the connection machine#126
fernandomg wants to merge 30 commits into
mainfrom
feat/76-provider

Conversation

@fernandomg

@fernandomg fernandomg commented Aug 29, 2026

Copy link
Copy Markdown
Member

Summary

Closes #76, Closes #85, closes #86, closes #57, closes #58

Third of three progressive PRs; merging this one closes the epic.

This swaps CantonConnectProvider, stops mirroring SDK state into multiple useState fields and runs the machine instead.

Changes

  1. CantonConnectProvider runs the connection machine and holds no React state for the session. It publishes:
    • config;
    • connection, the actor;
    • connect, disconnect and resetConnectError, the three actions.
  2. Every hook selects its own slice off connection, so a change in one slice re-renders only the hooks reading it
  3. useSignMessage, useExecute and useLedger also reject while the wallet is locked (wallet is locked - unlock it in the wallet), and useLedger().isReady is false then
  4. The package docs describe this provider
  5. connect() over a standing session runs a wallet change: the only recovery after a wallet-side disconnect, and cancelling it keeps the session

Deviations

#57 expects a failed init and a failed restore to surface an error. They are treated differently here:

Failure Surfaces Why Where it goes
init yes the dApp is misconfigured and no wallet can ever connect connectError is set; the cause logs at console.error
restore no the visitor has no session, which is not an error to them the app stays disconnected; the cause logs at console.debug

Acceptance criteria

  • An xstate@5 machine handles the connection lifecycle
  • dapp/frontend behaves as it did before this change

Test plan

Automated tests

  1. pnpm -C canton-connect test
  2. The full CI gate from the root, run on this branch on its own

Manual verification

Run the frontend app and connect an extension wallet.

Breaking changes

Nothing is published yet, and dapp/frontend and canton-dappbooster use only the hooks, so nothing in the repo needed a change.

Before After
useCantonConnectContext().party useParty().party
useCantonConnectContext().status useParty().status
useCantonConnectContext().isLocked useWalletStatus().isLocked
useCantonConnectContext().connectError useConnect().connectError
useCantonConnectContext().isConnecting useConnect().isConnecting
useCantonConnectContext().lastTx useExecute().lastTx
useCantonConnectContext().sdk useCantonConnectContext().connection.getSnapshot().context.sdk
Closing the picker set connectError connectError stays undefined
A request the wallet rejects arrived as its raw { code, message } object A real Error, in connectError, useSignMessage().error, useExecute().error and connect()'s rejection
error.code error.cause.code

Checklist

  • Self-reviewed my own diff
  • Tests added or updated
  • Docs updated (if applicable)
  • No unrelated changes bundled in

Screenshots

None.

Ten `useState` fields written from six places are gone. The provider creates the
actor, publishes it, and selects nothing: a provider that pre-selected the whole
session re-rendered every consumer on every tick of it.

The context value is the config, the actor as `ConnectionSubscription` and three
identity-stable actions. Machine input is read once, at actor creation, so a changed
`config` prop reaches the hooks and not the lifecycle.
`connect()` and `disconnect()` are a send plus a wait on a tag, so the promise over a
transition lives outside the machine and no caller names a state.

`connect()` resolves once the party lands, rejects with the wallet's own error when the
account read fails, and resolves partyless when the wallet connects locked. A connect
asked for mid-disconnect supersedes it instead of being dropped.
One `useSelector` per hook, so one slice changing no longer re-renders every consumer.

- `useWalletCall` is the one skeleton the three SDK-calling hooks share: it refuses
  with no session, refuses while the wallet is locked, and `useLedger().isReady`
  reports both
- `useTxFeed` keeps `lastTx` in React, the one piece of state the machine does not own
- `useConnect().reset()` forgets the last connect error, and a disconnect forgets it too
`FakeSessionProvider` resolves a real `connectionMachine` state, so a component test
selects exactly as it does in the app, and its `sdk` prop answers only the calls a hook
makes. The rest is the setup each suite was repeating: the discovery keys a restore
reads, a session render, a started connection, a picker that rejects.
Each bridge against a machine with stubbed actors: the state that answers it, the one
that supersedes it, and the unmount that stops the actor without answering.
Connect, restore, wallet pushes and the lock guards each get their own file, driving
the real facade through `createFakeWallet` rather than a stubbed sdk.
All of it rests on `dapp-sdk` internals no test can pin, so the chapter is the why:
the `beforeunload` the picker loses, the message the drain posts, and where it breaks.
…N-RPC objects

- the window transport rejects with the JSON-RPC error object itself (core-rpc-transport 1.10.0),
  so `connectError`, `useSignMessage().error` and `useExecute().error` carried a plain
  `{ code, message }` under an `Error` type, and `connect()` rejected with it
- `toError` wraps anything that is not an `Error`, keeping the wallet's object as `cause`;
  `toConnectError` and `useWalletCall` go through it
- the machine keeps what the wallet sent in `lastConnectError`; the classification is the hooks'
…ancels and config

- `CantonConnectProviderProps`: `config` is read once at actor creation; a later `walletPicker` or
  `additionalAdapters` never re-runs discovery, since `useActorRef` takes its options once
- `UseConnectResult`: `connectError` records a failure, and a picker the guard saw closed leaves it
  `undefined`; only a dismissal the SDK itself rejected is recorded as a cancel
- `UseWalletStatusResult.isLocked`: the party is dropped while locked, as the machine does; only
  `status` stays put
- `CantonConnectContextValue.disconnect`: settles on an unanswered wallet too, after the machine's
  10 s deadline
…s the provider does

- `FakeSessionProvider` published `resetConnectError: () => undefined`, so a consumer test that
  dismissed an error through it watched the error stay
- it now sends `connectError.reset` to the actor it rehydrated, the event the provider sends
- the pessimistic disconnect removed disconnect.superseded, so the bridge no
  longer waits on it; a disconnect always lands in disconnected now
- rewrite the interrupt test: a connect during a disconnect is ignored, the
  connect actor never runs, and the call rejects as cancelled once disconnected
Cut to what a reader cannot get from the tool's own docs: the actor-logic walkthrough,
both sequence diagrams, the wagmi essays and a duplicated provider section are gone.

The `dapp-sdk` mechanics stay in full. That SDK is under-documented and we depend on
it, so its quirks are the one thing a reader genuinely cannot look up.
Only beside an `architecture.md` that indexes it, when a section outgrows the seam it
describes. `canton-connect/` carries two today.
- the disconnect wait ends inside the machine after 10 s; the connect wait stays with #105
- b2db792 marked the bridges and the hook tags as not yet wired, true on feat/76-machine alone
- at this tip the provider wires both, so the chapter states the wired behavior
- the retiring failure edge and the wallet.statusChanged paragraph from that commit stay
@fernandomg fernandomg self-assigned this Aug 29, 2026
@vercel

vercel Bot commented Aug 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
demo.canton-dappbooster Ready Ready Preview Sep 1, 2026 2:15pm UTC
docs.canton-dappbooster Ready Ready Preview Sep 1, 2026 2:15pm UTC

Request Review

# Conflicts:
#	CLAUDE.md
#	canton-connect/CLAUDE.md
#	canton-connect/architecture.md
Base automatically changed from feat/76-doubles to feat/76-machine August 31, 2026 19:00
Base automatically changed from feat/76-machine to main August 31, 2026 19:02
- session dropped connect, so a wallet-side disconnect (same push as a lock) left no way back
- connecting/retiring/restoring split into new/changing, and the changing pair reports connecting
- a cancelled wallet change resumes the kept session without passing disconnected or idle

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This is the third and final PR of a progressive series that reworks canton-connect's connection lifecycle. It replaces CantonConnectProvider's mirroring of SDK state across ~10 useState fields with a single XState connection machine that owns the lifecycle. The provider now holds no session React state — it publishes config, the connection actor, and the three actions (connect, disconnect, resetConnectError) — and each hook subscribes to its own machine slice via @xstate/react's useSelector, so a change in one slice re-renders only the hooks that read it. This closes the epic (#76, #85, #86, #57, #58) by making invalid state combinations unrepresentable and folding the previously patched races into one owner.

Changes:

  • CantonConnectProvider runs the connection machine and holds no session state; hooks (useConnect, useParty, useWalletStatus, useExecute, useSignMessage, useLedger) each select their own machine slice.
  • Adds locked-wallet guards: useSignMessage/useExecute/useLedger reject while locked (wallet is locked - unlock it in the wallet) and useLedger().isReady is false; init failures surface via connectError while silent restore failures are logged (not surfaced).
  • Adds @xstate/react dependency, new public types (CantonConnectContextValue, TxStatusSnapshot), extensive machine/provider tests and new testing helpers, plus documentation updates.

Reviewed changes

Copilot reviewed 47 out of 48 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
pnpm-lock.yaml Adds @xstate/react@^6.1.0 and its transitive use-isomorphic-layout-effect.
CLAUDE.md Updates the canton-connect doc-distribution row to reflect the machine-owned lifecycle.
canton-connect/src/types.ts Adds TxStatusSnapshot and the public CantonConnectContextValue; refines ConnectionSubscription doc wording.
canton-connect/src/testing/walletLock.ts New helper pushing lock/unlock statusChanged events for wallet-a.
canton-connect/src/testing/walletA.ts New shared fake-wallet factory for provider tests.
canton-connect/src/testing/useSession.ts New test hook aggregating every published slice (plus sdk) into one object.
canton-connect/src/testing/throwingPicker.ts New picker that always throws, for never-succeed connect tests.
canton-connect/src/testing/startConnection.ts New helper booting a machine variant on the fake input.
canton-connect/src/testing/renderSession.tsx New helper rendering a hook inside a real provider with an auto-picker default.
canton-connect/src/testing/fakeSession.tsx Reworks the fake session provider to drive a real actor state via the machine.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread canton-connect/src/types.ts Outdated
- the merge committed a hybrid: an importer keyed on xstate@5.32.5, entries only for 5.32.6
- CI's frozen install rejects it; local installs passed on the up-to-date fast path
- main's lockfile plus the one added dependency keeps the PR merge textually trivial
- contract facts move to the leading blocks; the stale connects-locked claim goes
- dead {@link} targets get type-only imports

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 47 out of 48 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In review

2 participants