Skip to content

fix(wallets): load a wallet and list its signers with one request - #2289

Merged
albertoelias-crossmint merged 6 commits into
mainfrom
albertoe/wallets-init-fewer-requests
Nov 6, 2026
Merged

albertoelias-crossmint merged 6 commits into
mainfrom
albertoe/wallets-init-fewer-requests

Conversation

@albertoelias-crossmint

@albertoelias-crossmint albertoelias-crossmint commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

fix(wallets): load a wallet and list its signers with one request

Problem

getWallet in the browser resolves only after signer initialization. That initialization fetched data the wallet response already had. After GET wallet it loaded the device-key iframe, then sent GET signer to read the device signer's status. If the key was not yet linked to the wallet address, it also sent GET wallet again plus one GET signer per delegated signer. Each extra URL also costs a CORS preflight, so on staging a page reload waited 1.3 s for the wallet, and up to 3.4 s for a device with no key.

wallet.signers() had the same pattern: GET wallet, then GET signer per signer, only to read each signer's status.

What this PR does

  • Each config.delegatedSigners entry in the GET wallet response is the same object GET signer returns: chains on EVM, and the signer's transaction on Solana and Stellar. I compared the two responses on staging for EVM and Stellar wallets and they were byte-identical. For Solana, the backend repository fills in each signer's transaction when it builds the wallet response.
  • WalletFactory passes those signers to the Wallet. Signer initialization reads the device-key candidates and the signer status from them (SignerManager.assemble, DeviceRecoveryService.resolveAvailability). It keeps the same filter as signers(): only signers approved on this chain.
  • Wallets built without a fresh response (for example EVMWallet.from) receive no signers, so they still fetch the status. A status from an older response is never used.
  • getWallet calls a new optional DeviceSignerKeyStorage.preload() before its request. The iframe storage starts loading the iframe in it, so the iframe loads during the request instead of after it. Other storages inherit a no-op.
  • wallet.signers() maps each signer's status from its one GET wallet response.
flowchart LR
  subgraph Before
    A1[GET wallet] --> B1[load iframe] --> C1[GET signer]
    B1 -. key not linked .-> D1[GET wallet + GET signer x N]
  end
  subgraph After
    A2[GET wallet, iframe loading in parallel] --> C2[status from the response]
  end
Loading

Result

Browser getWallet on staging, median of 9 runs. The benchmark runs the real SDK against staging; it simulates the iframe at its measured 220 ms cold load and sends a real preflight before each new URL.

Device key on this device Before After
Linked to the wallet (usual reload) 1.33 s 0.53 s
Present, not yet linked 2.64 s* 0.86 s
None (needs recovery) 3.38 s* 0.72 s

*About 0.2 s high: the benchmark's second GET wallet uses a different URL than a browser would, which adds a preflight.

wallet.signers() goes from 1 + N requests to 1.

What to check when reviewing

  • Wallets created with apiSigners trust that response for the signer status during init. This holds as long as the GET wallet and GET signer serializers stay the same. If they diverge, the signer-init tests in wallet-factory.test.ts still pass, because they mock the API.
  • The signers() test for partial GET signer failures is gone, since those requests no longer happen.
  • fix(wallets): remove stale device signers on re-register [WAL-13217] #2205 (merged into this branch) adds a saveSignerLocator key-storage write at the end of init. That is an iframe RPC, not an API request.

Test plan

  • pnpm --filter @crossmint/wallets-sdk test:vitest: 780 passed. src/api/client.test.ts fails to load without PREVIEW_CK_KEY, same as on main.
  • New tests fail on the old code: the 5 signer-init tests in wallet-factory.test.ts and 3 of the signers() tests. They cover a mapped key, an unmapped key, a signer awaiting approval, a signer approved only on another chain, and preload running before the request.
  • biome check and tsc --noEmit on the wallets package are clean.
  • Not covered: the iframe preload body (the wallets package has no DOM test environment), and a logged-in run in a real browser.

🤖 Generated with Claude Code

AlbertoElias and others added 4 commits November 5, 2026 10:53
…ts key

getWallet awaits signer initialization. For a wallet with a device signer,
that ran after GET wallet, one step after another: load the key iframe, look
up the key, then GET the signer for its status. When the key was not yet
mapped to the wallet address it also re-fetched the wallet and every signer.

The wallet response already carries each delegated signer exactly as GET
signer returns it, so initialization now reads the status from there, and the
key storage starts loading alongside the wallet request. Wallets built without
a fresh response (EVMWallet.from and friends) still fetch.

Measured against staging with a browser's CORS preflights: 1.39 s -> 0.51 s
when the key is mapped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
signers() fetched the wallet, then GET signer once per delegated signer to read
its status. The wallet response already carries each signer exactly as GET
signer returns it (chains on EVM, the hydrated transaction on Solana and
Stellar), so map those directly. Recovery and signer lookups that call
signers() now cost one request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: efa7ce3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 10 packages
Name Type
@crossmint/wallets-sdk Patch
@crossmint/wallets-quickstart-devkit Patch
@crossmint/wallets-playground-react Patch
@crossmint/client-sdk-react-base Patch
@crossmint/client-sdk-react-native-ui Patch
@crossmint/client-sdk-react-ui Patch
@crossmint/integration-tests Patch
@crossmint/wallets-playground-expo Patch
@crossmint/auth-ssr-nextjs-demo Patch
@crossmint/client-sdk-nextjs-starter Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

devin-ai-integration[bot]

This comment was marked as resolved.

@albertoelias-crossmint
albertoelias-crossmint enabled auto-merge (squash) November 5, 2026 23:28
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no actionable issues were found.

Reviews (1) · Last reviewed commit: "test(wallets): stub saveSignerLocator in..." · Reviewed by Greptile

…type

The factory passes the wallet response's signers through an internal args type, so the
exported Wallet constructor signature is unchanged. The signer-init tests move into
wallet-factory.test.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@albertoelias-crossmint
albertoelias-crossmint merged commit 36903ca into main Nov 6, 2026
9 checks passed
@albertoelias-crossmint
albertoelias-crossmint deleted the albertoe/wallets-init-fewer-requests branch November 6, 2026 15:06
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.

3 participants