feat(wallet): shared wallet error taxonomy, normalised events and conformance tests - #218
Merged
truthixify merged 5 commits intoSep 25, 2026
Conversation
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
|
@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! 🚀 |
Contributor
|
Merged. The shared wallet errors and conformance tests are thorough, and the API stays additive. Thanks @aratass. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
WraithWalletErrorand five subclasses insrc/errors.ts, following the existingWraithErrorhierarchy.normalizeWalletError()maps each provider's real error shapes onto that taxonomy, andwithNormalizedWalletErrors()wraps an adapter so its methods reject with normalised errors.watchWalletEvents()reports account, network and disconnect changes from all three providers in one shape.getNetwork()on the viem and Freighter adapters, andassertWalletNetwork().docs/wallet-adapters.md, the wallet branch indocs/errors.md, and a CHANGELOG entry.Everything is opt-in. The adapters' existing
signMessage()andgetAddress()behave exactly as before.Error taxonomy
code, adocsLink, and adescribe()hint, like the existing error classes.contextis{ chain, reason, providerCode }, plusexpectedNetworkandactualNetworkonWalletWrongNetworkError.cause, sotoJSON()output stays serialisable.normalizeWalletError()always returns aWraithWalletError. It walks thecausechain and Solana's.errorfield, and returns anyWraithWalletErrorit finds unchanged, so normalising twice is safe.withNormalizedWalletErrors()only normalises errors thrown by the wallet. Key-derivation errors such asInvalidSignatureErrorstill propagate unchanged fromderiveStealthKeysFromWallet().Normaliser mapping
viem and EIP-1193
4001(viemUserRejectedRequestError),5000(CAIP-25 rejection, which viem'sbuildRequestalso maps toUserRejectedRequestError)WalletUserRejectedError4100(UnauthorizedProviderError),4900(ProviderDisconnectedError), viemAccountNotFoundErrorWalletNotConnectedError4901(ChainDisconnectedError),4902(SwitchChainError),5710(UnsupportedChainIdError), viemChainMismatchErrorWalletWrongNetworkError4200(UnsupportedProviderMethodError)WalletUnavailableError@solana/wallet-adapterWalletNotConnectedError,WalletDisconnectedError,WalletNotSelectedError(react)WalletNotConnectedErrorWalletNotReadyError(not installed or loadable)WalletUnavailableErrorWalletWindowClosedErrorWalletUserRejectedErrorWalletSignMessageError,WalletConnectionErrorand other wrappers.error(Phantom uses4001/4100/4900)Freighter (
@stellar/freighter-api3+){ 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"Freighter is not connected."WalletNotConnectedError{ code: -1 }internal errorWalletRequestFailedErrorThis SDK's own adapters
"The viem wallet client has no connected account.","The Solana wallet is not connected.","Freighter is not connected."WalletNotConnectedErrorTypeErrorfor a wallet withoutsignMessageWalletUnavailableErrorAnything else becomes
WalletRequestFailedError, withproviderCodeandreasonkept (for example-32002, "request already pending").Event normalisation
watchWalletEvents(source, listener)returns an unsubscribe function. Thesourceis 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'sWatchWalletChangesaccountsChanged([a, ...])/ Solanaconnect(publicKey)(adapters re-emit it on account switch) / Freighter poll with a new address{ type: 'accountChanged', chain, address }(EVM addresses EIP-55 checksummed, matchingViemWalletAdapter.getAddress())chainChangedorconnect({ chainId })/ Freighter poll with a new passphrase{ type: 'networkChanged', chain, network }:eip155:<chainId>for EVM, the network passphrase for StellardisconnectoraccountsChanged([])/ Solanadisconnect/ Freighter poll with an empty address{ type: 'disconnect', chain, error: WalletNotConnectedError }(provider error oncause, code onproviderCode)disconnect, the next account and network are reported again.disconnectcodes followCloseEvent(e.g.1013), so they are not matched against4900.Network checks
ViemWalletAdapter.getNetwork()reads the chain through the client'sgetChainId().FreighterWalletAdapter.getNetwork()reads it through Freighter'sgetNetwork().networkChanged.assertWalletNetwork(adapter, expected)throwsWalletWrongNetworkErrorwithexpectedNetworkandactualNetwork.@solana/wallet-adapterdoes not expose the wallet's cluster, so for Solana adapters it throwsWalletUnavailableError. This is documented.getNetwork()methods reject with normalised errors.Conformance tests
test/wallet/conformance.test.tsruns 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 viemWalletClientover a mock EIP-1193 transport.getAddress()givesWalletNotConnectedError. After connecting, it returns the account and a matchingaccountChangedfires. A declined connection request givesWalletUserRejectedError.disconnectevent fires andgetAddress()givesWalletNotConnectedError. Signing after a disconnect givesWalletNotConnectedError, except on Freighter, which re-prompts for access; declining givesWalletUserRejectedError. The unwrapped adapter still throws its old error.networkChangedfires,getNetwork()returns the new network, andassertWalletNetwork()givesWalletWrongNetworkError. Solana is covered by its documentedWalletUnavailableError.WalletUserRejectedError, both directly and throughderiveStealthKeysFromWallet().getAddress()and reports the accounts again. Switching back to the expected network passes the check.signMessage, giveWalletUnavailableError.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()andassertWalletNetwork().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 etccontains no removed or changed lines. It only adds code and API.ViemWalletAdapter,SolanaWalletAdapterandFreighterWalletAdapterkeep their constructors,signMessage(),getAddress()and thrown errors unchanged.getNetwork?()onBaseWalletAdaptergetChainId?onViemWalletClientgetNetwork?onFreighterWalletApietc/sdk.api.mdonly gains entries; it is regenerated in its own commit. The other five API reports did not change.Review notes / judgement calls
4901maps 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.TypeErrorfor a wallet withoutsignMessagemaps toWalletUnavailableError. This is the common "this Solana wallet can't sign messages" case.WalletConfigError,WalletLoadError,WalletTimeoutErrorandWalletWindowBlockedErrorare deliberately left asWalletRequestFailedError, with the name kept inproviderCode.FreighterWalletAdapterkeeps only Freighter'smessage, so Freighter's documented constants and this SDK's own adapter messages are matched exactly. Code-4counts as a decline only forstellaror an unspecified chain. The conformance tests pin these strings.getNetwork()methods throw typed errors, while the older sibling methods keep their historical errors.eip155:N). Stellar uses the network passphrase rather thanstellar:pubnet, because the passphrase also covers futurenet and custom networks.disconnect. It reports the current state, so a site without access yet gets adisconnectright after subscribing.WRAITH/WALLET/NOT_CONNECTEDrather than.../WALLET_NOT_CONNECTED.WalletNotConnectedErrorshares its name with the wallet-adapter class of the same meaning.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
tscagainst the real package types:WalletClientis not assignable toViemWalletClient, because ofsignMessage's parameter types. The same is true on develop.@stellar/freighter-api6.0.1 module is not assignable toFreighterWalletApi, becausesignMessagereturnssignedMessage: Buffer | null.The new structural types do accept the real types:
EIP1193ProviderPhantomWalletAdapterand wallet-adapter'sBaseWalletAdapterWatchWalletChangesandgetNetworkSources relied on
connect,disconnect,chainChangedandaccountsChangedevents;disconnectcodes followCloseEvent;on/removeListener.4902"Unrecognized chain ID" and-32002"Request already pending".errors/rpc.js: the RPC error classes and their codesutils/buildRequest.js: maps provider codes to those classes, including5000errors/chain.js:ChainMismatchErrorerrors/account.jsandactions/wallet/signMessage.js:AccountNotFoundErrorerrors/base.js:causeandshortMessage@solana/wallet-adapter-base0.9.28: error classes withnameand.error;connect/disconnectevents@solana/wallet-adapter-react0.15.40:WalletNotSelectedError;WalletNotConnectedErrorfromsignMessage@solana/wallet-adapter-phantom0.9.30: wraps wallet errors inWalletSignMessageError/WalletConnectionError; re-emitsconnecton account change@solana/wallet-standard-wallet-adapter-base1.1.6: account change becomesconnect; a cluster mismatch surfaces only as a bareWalletSendTransactionError@stellar/freighter-api6.0.1 source (also 3.0.0 and 1.7.1):FreighterApiNodeError/FreighterApiInternalError/FreighterApiDeclinedErrorsignMessagerequests access firstWatchWalletChangescallback shapegetAddressandsignMessage; 1.x threw stringsfreighterApiMessageListener.ts: declined requests and closed popups resolve withFreighterApiDeclinedError; a site without access getspublicKey: ""; 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.develop@92eb209(before)pnpm install --frozen-lockfilepnpm run format:checkpnpm buildpnpm api:checkpnpm testpnpm test:exports(entry point smoke tests from #217)The 123 extra tests are the three new files: conformance 48, errors 57, events 18.
Bundle size.
pnpm size(thebundle-sizejob, which runs forsrc/**changes) exits 0:🤖 Generated with Claude Code
https://claude.ai/code/session_01UhuzUyVKtcr7g1gbqgu3Mg