fix(injected): reject with an Error, not a bare string - #133
Open
sktbrd wants to merge 1 commit into
Open
Conversation
Closes keepkey#132. The background sends failures across postMessage as a plain string (`sendResponse({ error: formatUserError(error) })`), and every injected provider rejected with that string untouched. EIP-1193 requires rejecting with an object carrying `code` and `message`, and dApp libraries rely on it: wagmi/viem/ethers inspect the rejection value, `in` throws on a primitive, and the user gets TypeError: Cannot use 'in' operator to search for 'data' in KeepKey Vault is not running. Open the KeepKey Vault desktop app... instead of the instruction we wrote for exactly that situation. The message we most want read is the one we made unreadable. Nothing about that message is special — every error from the extension rejected as a primitive. It is just long enough to make the TypeError absurd. Add toProviderError() and apply it at all five reject sites: injected.ts (EVM), solana-provider, tron-provider, solana-wallet-standard. It keeps an existing Error by reference so stacks are not discarded, preserves a `code` when one is already set, rebuilds structured errors that lost their prototype crossing postMessage, and never throws — a normalizer that can fail is worse than the bug it fixes. Follow-up (deliberately not in this change): formatUserError() flattens errors to a string, so the 4900 "provider disconnected" code from createVaultRequiredError() is dropped before it reaches the dApp. Sending `{ message, code }` would preserve it — toProviderError already handles that shape — but it changes what every consumer of `response.error` receives and wants a side-panel audit first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 #132.
The bug is bigger than the one error message
The reported symptom is that "Vault is not running" reaches the user as:
Tracing it end to end:
background/index.ts:1240—sendResponse({ error: formatUserError(error) })sends the failure as a plain stringdata.errorinjected.ts:283—callback.callback(data.error)passes the string throughinjected.ts:360—reject(error)rejects the EIP-1193 promise with a raw string'data' in err, andinthrows on a primitiveSo nothing about that message is special — every error from the extension rejects as a primitive. It's just long enough to make the TypeError read absurdly. And the message we most wanted the user to read is precisely the one we made unreadable.
EIP-1193 requires rejecting with an object carrying
codeandmessage. Rejecting with a primitive violates the spec, which is why consumer libraries choke.Not one site — five
Every provider had the identical
reject(error):injected.ts(EVM)solana-provider.tstron-provider.tssolana-wallet-standard.tsThe fix
New
injected/provider-error.tsexportingtoProviderError(), applied at all five. It:Errorby reference, so stacks and subclasses aren't discardedcodethat's already set (e.g. 4901/4001) rather than overwriting itpostMessage-32603(JSON-RPC internal error) when no code survivedFollow-up, deliberately not in this PR
formatUserError()flattens errors to a string, so the 4900 "provider disconnected" code thatcreateVaultRequiredError()sets is dropped before it reaches the dApp. That code is how a dApp shows a proper "wallet disconnected" state instead of a generic failure.Sending
{ message, code }from the background would preserve it — andtoProviderErroralready handles that shape, so the two are compatible — but it changes what every consumer ofresponse.errorreceives and deserves a side-panel audit first. Happy to do it as a second PR.Testing
provider-error.test.ts— 6 cases, including the exact regression (asserting'data' in errno longer throws), code preservation, prototype-loss recovery, and a fuzz-ish pass over0 / false / Symbol / bigint / [] / fnto prove it can't throwchrome-extensionsuite: 131 passed / 12 filestsc --noEmitcleanBranched from
origin/developin a scratch worktree; no local checkout was disturbed.