Skip to content

feat(wallet): shared wallet error taxonomy, normalised events and conformance tests - #218

Merged
truthixify merged 5 commits into
wraith-protocol:developfrom
aratass:feat/issue-214-wallet-errors
Sep 25, 2026
Merged

truthixify merged 5 commits into
wraith-protocol:developfrom
aratass:feat/issue-214-wallet-errors

Conversation

@aratass

@aratass aratass commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Closes #214.

Summary

The viem, Solana wallet-adapter and Freighter adapters surface disconnects, wrong networks, rejected signatures and missing wallets as unrelated provider errors. This PR adds:

  1. A shared wallet error taxonomy: WraithWalletError and five subclasses in src/errors.ts, following the existing WraithError hierarchy.
  2. A normaliser: normalizeWalletError() maps each provider's real error shapes onto that taxonomy, and withNormalizedWalletErrors() wraps an adapter so its methods reject with normalised errors.
  3. Normalised events: watchWalletEvents() reports account, network and disconnect changes from all three providers in one shape.
  4. Network checks: getNetwork() on the viem and Freighter adapters, and assertWalletNetwork().
  5. Conformance tests for connect, disconnect, wrong network, rejection, retry and unavailable wallets, run against all three adapters.
  6. Docs for app builders: docs/wallet-adapters.md, the wallet branch in docs/errors.md, and a CHANGELOG entry.

Everything is opt-in. The adapters' existing signMessage() and getAddress() behave exactly as before.

Error taxonomy

WraithError
└── WraithWalletError                 (new abstract family base)
    ├── WalletNotConnectedError       WRAITH/WALLET/NOT_CONNECTED
    ├── WalletUserRejectedError       WRAITH/WALLET/USER_REJECTED
    ├── WalletWrongNetworkError       WRAITH/WALLET/WRONG_NETWORK
    ├── WalletUnavailableError        WRAITH/WALLET/UNAVAILABLE
    └── WalletRequestFailedError      WRAITH/WALLET/REQUEST_FAILED  (anything unclassified)
  • Each class has a stable code, a docsLink, and a describe() hint, like the existing error classes.
  • context is { chain, reason, providerCode }, plus expectedNetwork and actualNetwork on WalletWrongNetworkError.
  • The original provider error is kept on a non-enumerable cause, so toJSON() output stays serialisable.
  • normalizeWalletError() always returns a WraithWalletError. It walks the cause chain and Solana's .error field, and returns any WraithWalletError it finds unchanged, so normalising twice is safe.
  • withNormalizedWalletErrors() only normalises errors thrown by the wallet. Key-derivation errors such as InvalidSignatureError still propagate unchanged from deriveStealthKeysFromWallet().

Normaliser mapping

viem and EIP-1193

Provider signal Maps to
4001 (viem UserRejectedRequestError), 5000 (CAIP-25 rejection, which viem's buildRequest also maps to UserRejectedRequestError) WalletUserRejectedError
4100 (UnauthorizedProviderError), 4900 (ProviderDisconnectedError), viem AccountNotFoundError WalletNotConnectedError
4901 (ChainDisconnectedError), 4902 (SwitchChainError), 5710 (UnsupportedChainIdError), viem ChainMismatchError WalletWrongNetworkError
4200 (UnsupportedProviderMethodError) WalletUnavailableError

@solana/wallet-adapter

Provider signal Maps to
WalletNotConnectedError, WalletDisconnectedError, WalletNotSelectedError (react) WalletNotConnectedError
WalletNotReadyError (not installed or loadable) WalletUnavailableError
WalletWindowClosedError WalletUserRejectedError
WalletSignMessageError, WalletConnectionError and other wrappers classified by the wallet's own error in .error (Phantom uses 4001 / 4100 / 4900)

Freighter (@stellar/freighter-api 3+)

Provider signal Maps to
{ code: -4, message: "The user rejected this request." } (also returned when the popup is closed) WalletUserRejectedError
{ code: -1, message: "Node environment is not supported" } WalletUnavailableError
empty address, surfaced by the adapter as "Freighter is not connected." WalletNotConnectedError
{ code: -1 } internal error WalletRequestFailedError

This SDK's own adapters

Signal Maps to
"The viem wallet client has no connected account.", "The Solana wallet is not connected.", "Freighter is not connected." WalletNotConnectedError
Constructor TypeError for a wallet without signMessage WalletUnavailableError

Anything else becomes WalletRequestFailedError, with providerCode and reason kept (for example -32002, "request already pending").

Event normalisation

watchWalletEvents(source, listener) returns an unsubscribe function. The source is one of:

  • { chain: 'evm', provider }: the EIP-1193 provider (a viem WalletClient has no events)
  • { chain: 'solana', wallet }: the wallet-adapter adapter
  • { chain: 'stellar', watcher }: Freighter's WatchWalletChanges
Provider event Normalised event
EIP-1193 accountsChanged([a, ...]) / Solana connect(publicKey) (adapters re-emit it on account switch) / Freighter poll with a new address { type: 'accountChanged', chain, address } (EVM addresses EIP-55 checksummed, matching ViemWalletAdapter.getAddress())
EIP-1193 chainChanged or connect({ chainId }) / Freighter poll with a new passphrase { type: 'networkChanged', chain, network }: eip155:<chainId> for EVM, the network passphrase for Stellar
EIP-1193 disconnect or accountsChanged([]) / Solana disconnect / Freighter poll with an empty address { type: 'disconnect', chain, error: WalletNotConnectedError } (provider error on cause, code on providerCode)
  • Repeated values are dropped.
  • After a disconnect, the next account and network are reported again.
  • Failed Freighter polls are ignored.
  • EIP-1193 disconnect codes follow CloseEvent (e.g. 1013), so they are not matched against 4900.

Network checks

  • ViemWalletAdapter.getNetwork() reads the chain through the client's getChainId().
  • FreighterWalletAdapter.getNetwork() reads it through Freighter's getNetwork().
  • Both return the same format as networkChanged.
  • assertWalletNetwork(adapter, expected) throws WalletWrongNetworkError with expectedNetwork and actualNetwork.
  • @solana/wallet-adapter does not expose the wallet's cluster, so for Solana adapters it throws WalletUnavailableError. This is documented.
  • The new getNetwork() methods reject with normalised errors.

Conformance tests

test/wallet/conformance.test.ts runs 16 scenarios against each of the three adapters (48 tests). Each scenario uses the real SDK adapter class over a mock provider. The EVM harness is a real viem WalletClient over a mock EIP-1193 transport.

Scenario What is checked
Connect Before connecting, getAddress() gives WalletNotConnectedError. After connecting, it returns the account and a matching accountChanged fires. A declined connection request gives WalletUserRejectedError.
Disconnect A disconnect event fires and getAddress() gives WalletNotConnectedError. Signing after a disconnect gives WalletNotConnectedError, except on Freighter, which re-prompts for access; declining gives WalletUserRejectedError. The unwrapped adapter still throws its old error.
Wrong network networkChanged fires, getNetwork() returns the new network, and assertWalletNetwork() gives WalletWrongNetworkError. Solana is covered by its documented WalletUnavailableError.
Rejection A rejected signature gives WalletUserRejectedError, both directly and through deriveStealthKeysFromWallet().
Retry Retrying after two rejections derives keys identical to a first-time success. Reconnecting restores getAddress() and reports the accounts again. Switching back to the expected network passes the check.
Unavailable Each provider's own "unavailable" error, and a wallet without signMessage, give WalletUnavailableError.

Also added:

  • test/wallet/errors.test.ts (57 tests): taxonomy, the full mapping table using real viem error instances, cause walking, idempotency, the wrapper, getNetwork() and assertWalletNetwork().
  • test/wallet/events.test.ts (18 tests): event mapping, de-duplication, reconnect, malformed payloads and unsubscribe.
  • test/wallet/providers.fixture.ts: mocks copied from the published sources listed below.

No real wallet or network is used.

Backward compatibility

  • git diff origin/develop -- src etc contains no removed or changed lines. It only adds code and API.
  • No existing test file is modified.
  • No dependency or lockfile change.
  • ViemWalletAdapter, SolanaWalletAdapter and FreighterWalletAdapter keep their constructors, signMessage(), getAddress() and thrown errors unchanged.
  • Additive type changes:
    • optional getNetwork?() on BaseWalletAdapter
    • optional getChainId? on ViemWalletClient
    • optional getNetwork? on FreighterWalletApi
  • etc/sdk.api.md only gains entries; it is regenerated in its own commit. The other five API reports did not change.
  • Semver: additive exports, so minor per CONTRIBUTING.md. The same file asks for two maintainer reviews for package-export changes.

Review notes / judgement calls

  • 4901 maps to wrong network. EIP-1193 defines it as "not connected to the requested chain" while the provider is still connected to others. The fix is a network switch, not a reconnect.
  • Constructor TypeError for a wallet without signMessage maps to WalletUnavailableError. This is the common "this Solana wallet can't sign messages" case.
  • Unclassified Solana errors. WalletConfigError, WalletLoadError, WalletTimeoutError and WalletWindowBlockedError are deliberately left as WalletRequestFailedError, with the name kept in providerCode.
  • Exact-message matching. FreighterWalletAdapter keeps only Freighter's message, so Freighter's documented constants and this SDK's own adapter messages are matched exactly. Code -4 counts as a decline only for stellar or an unspecified chain. The conformance tests pin these strings.
  • New getNetwork() methods throw typed errors, while the older sibling methods keep their historical errors.
  • Network identifiers. EVM uses CAIP-2 (eip155:N). Stellar uses the network passphrase rather than stellar:pubnet, because the passphrase also covers futurenet and custom networks.
  • Freighter's first poll can emit disconnect. It reports the current state, so a site without access yet gets a disconnect right after subscribing.
  • Naming. Codes are WRAITH/WALLET/NOT_CONNECTED rather than .../WALLET_NOT_CONNECTED. WalletNotConnectedError shares its name with the wallet-adapter class of the same meaning.
  • Bundle size. The wallet errors live in the shared src/errors.ts, so CJS chain entries grow by 0.54 to 0.71 kB (see Verification). All entries stay within their limits. Moving the wallet errors to their own module would avoid this.

Pre-existing, not changed here (possible follow-ups). Both were found with tsc against the real package types:

  • A real viem WalletClient is not assignable to ViemWalletClient, because of signMessage's parameter types. The same is true on develop.
  • The @stellar/freighter-api 6.0.1 module is not assignable to FreighterWalletApi, because signMessage returns signedMessage: Buffer | null.

The new structural types do accept the real types:

  • viem EIP1193Provider
  • PhantomWalletAdapter and wallet-adapter's BaseWalletAdapter
  • Freighter's WatchWalletChanges and getNetwork

Sources relied on

  • EIP-1193: provider error codes 4001 / 4100 / 4200 / 4900 / 4901; the connect, disconnect, chainChanged and accountsChanged events; disconnect codes follow CloseEvent; on / removeListener.
  • EIP-3326: assigns no code for unknown chains. MetaMask docs: 4902 "Unrecognized chain ID" and -32002 "Request already pending".
  • viem 2.47.14 (the version installed here):
    • errors/rpc.js: the RPC error classes and their codes
    • utils/buildRequest.js: maps provider codes to those classes, including 5000
    • errors/chain.js: ChainMismatchError
    • errors/account.js and actions/wallet/signMessage.js: AccountNotFoundError
    • errors/base.js: cause and shortMessage
  • Solana packages, sources read from the npm tarballs:
    • @solana/wallet-adapter-base 0.9.28: error classes with name and .error; connect / disconnect events
    • @solana/wallet-adapter-react 0.15.40: WalletNotSelectedError; WalletNotConnectedError from signMessage
    • @solana/wallet-adapter-phantom 0.9.30: wraps wallet errors in WalletSignMessageError / WalletConnectionError; re-emits connect on account change
    • @solana/wallet-standard-wallet-adapter-base 1.1.6: account change becomes connect; a cluster mismatch surfaces only as a bare WalletSendTransactionError
  • Phantom Solana errors: 4001 / 4100 / 4900 / -32002 / -32603.
  • @stellar/freighter-api 6.0.1 source (also 3.0.0 and 1.7.1):
    • FreighterApiNodeError / FreighterApiInternalError / FreighterApiDeclinedError
    • the 2 s timeout, which covers only the connection-status and public-key requests
    • signMessage requests access first
    • WatchWalletChanges callback shape
    • 3.0.0 introduced object errors, getAddress and signMessage; 1.x threw strings
  • Freighter extension freighterApiMessageListener.ts: declined requests and closed popups resolve with FreighterApiDeclinedError; a site without access gets publicKey: ""; there is no network-mismatch error for message signing.

Verification

Rebased onto develop @ 92eb209 (after #215 and #217). The only conflict was the CHANGELOG, where both entries are kept. Run locally with pnpm 10 on Node 22, each command as CI runs it, in CI's order.

CI step develop @ 92eb209 (before) this branch (after)
pnpm install --frozen-lockfile exit 0 exit 0
pnpm run format:check exit 0, "All matched files use Prettier code style!" exit 0, same
pnpm build exit 0 exit 0
pnpm api:check exit 0, 6/6 "API Extractor completed successfully" exit 0, 6/6
pnpm test exit 0: 77 passed, 2 skipped files (79); 1232 passed, 5 skipped tests (1237); sdk-svelte 6 files / 10 tests passed exit 0: 80 passed, 2 skipped files (82); 1355 passed, 5 skipped tests (1360); sdk-svelte 6 files / 10 tests passed
pnpm test:exports (entry point smoke tests from #217) exit 0 exit 0, "entry point smoke tests passed"

The 123 extra tests are the three new files: conformance 48, errors 57, events 18.

Bundle size. pnpm size (the bundle-size job, which runs for src/** changes) exits 0:

Entry develop branch Δ limit
Root ESM 30.63 kB 32.87 kB +2.24 36.1
Root CJS 138.09 140.28 +2.19 159.7
EVM ESM / CJS 23.83 / 126.79 23.78 / 127.33 −0.05 / +0.54 27.5 / 145.9
Solana ESM / CJS 17.15 / 25.89 17.22 / 26.60 +0.07 / +0.71 19.8 / 29.8
CKB ESM / CJS 20.52 / 128.76 20.53 / 129.38 +0.01 / +0.62 23.6 / 148.1
Stellar ESM / CJS 26.48 / 34.29 26.52 / 34.94 +0.04 / +0.65 30.5 / 39.5
Vault ESM / CJS 1.93 / 2.07 1.93 / 2.07 0 / 0 2.3 / 2.4

🤖 Generated with Claude Code

https://claude.ai/code/session_01UhuzUyVKtcr7g1gbqgu3Mg

Viem, Solana wallet-adapter and Freighter report disconnects, wrong networks,
rejected signatures and missing wallets in different shapes.

- WraithWalletError with WalletNotConnectedError, WalletUserRejectedError,
  WalletWrongNetworkError, WalletUnavailableError and WalletRequestFailedError,
  following the existing WraithError hierarchy
- normalizeWalletError() maps EIP-1193 codes, viem error classes, Solana
  wallet-adapter errors and Freighter error results onto the taxonomy and keeps
  the original error on `cause`
- withNormalizedWalletErrors() wraps an adapter so its methods reject with
  normalised errors
- getNetwork() on the viem and Freighter adapters, and assertWalletNetwork(),
  report a wrong network the same way for both

The adapters' signMessage() and getAddress() behave exactly as before.

Refs wraith-protocol#214
EIP-1193 providers, Solana wallet-adapter adapters and Freighter's
WatchWalletChanges poller report account and network changes differently.
watchWalletEvents() subscribes to any of them and emits one event shape:

- accountChanged with the address (EIP-55 checksummed for EVM)
- networkChanged with the network (eip155:<chainId> for EVM, the network
  passphrase for Stellar)
- disconnect with a WalletNotConnectedError that keeps the provider's error

Repeated values are dropped, and after a disconnect the next account and
network are reported again. EIP-1193 disconnect codes follow CloseEvent, so
every disconnect event is treated as a disconnect whatever its code.

Refs wraith-protocol#214
Runs the same connect, disconnect, wrong network, rejection, retry and
unavailable scenarios against the viem, Solana wallet-adapter and Freighter
reference adapters, and checks that each ends in the same WraithWalletError
or normalised event.

Providers are mocks that reproduce each library's published error and event
shapes; the viem harness drives a real viem WalletClient over a mock EIP-1193
transport. No wallet or network is used.

Refs wraith-protocol#214
Adds docs/wallet-adapters.md for app builders: the WraithWalletError
taxonomy, how each provider's errors and events map onto it, network checks,
retry guidance, a per-adapter behaviour table and Freighter notes. Adds the
wallet errors to docs/errors.md and a CHANGELOG entry.

Refs wraith-protocol#214
…hecks

Regenerated with `api-extractor run --local` for all six configs; only
etc/sdk.api.md changed. The additions are the WraithWalletError classes,
normalizeWalletError(), withNormalizedWalletErrors(), watchWalletEvents(),
assertWalletNetwork() and their types, getNetwork() on the viem and Freighter
adapters, and optional getNetwork / getChainId members on existing
interfaces. Nothing was removed or changed.

Refs wraith-protocol#214
@drips-wave

drips-wave Bot commented Sep 25, 2026

Copy link
Copy Markdown

@aratass Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@truthixify
truthixify merged commit dfb1279 into wraith-protocol:develop Sep 25, 2026
16 checks passed
@truthixify

Copy link
Copy Markdown
Contributor

Merged. The shared wallet errors and conformance tests are thorough, and the API stays additive. Thanks @aratass.

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.

[Wave 9] Add wallet adapter failure and disconnect conformance tests

3 participants