@valve-tech/viem-errors 0.10.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md ADDED
@@ -0,0 +1,162 @@
1
+ # AGENTS.md
2
+
3
+ Terse reference for AI agents (Claude Code, Cursor, Aider, etc.) integrating
4
+ `@valve-tech/viem-errors`. The full README is for humans; this file is for
5
+ agents that need to ground their work in the package's actual surface
6
+ quickly.
7
+
8
+ ## What this package does
9
+
10
+ Cause-chain-aware error utilities for viem-based dapps. Pure functions
11
+ over viem's nested `cause` chain that solve four problems every dapp
12
+ re-implements (and most get slightly wrong):
13
+
14
+ 1. **Detect EIP-1193 user rejections** even when buried under wrapper
15
+ errors whose top-level message looks like a generic failure.
16
+ 2. **Extract decoded custom Solidity error names** (`HashMismatch`,
17
+ `InsufficientLiquidity`) from viem's nested `data.errorName`
18
+ regardless of how the error is wrapped.
19
+ 3. **Map raw RPC/wallet/contract errors to short user-facing copy**
20
+ with overridable patterns.
21
+ 4. **Route wagmi-style `onError` sinks** through one helper so every
22
+ write site has consistent UX.
23
+
24
+ Pure functions, **no runtime dependencies**. `viem ^2.0.0` is a peer
25
+ dependency used only for the `Hex` type — the runtime is plain JS.
26
+
27
+ ## Public API
28
+
29
+ All exports live under `src/index.ts`. Single subpath; no sub-exports.
30
+
31
+ ```ts
32
+ import {
33
+ // chain walk (the foundation everything else is built on)
34
+ walkErrorCause,
35
+ // rejection detection
36
+ isUserRejectionError,
37
+ USER_REJECTION_MESSAGE,
38
+ // custom Solidity error extraction
39
+ extractContractErrorName,
40
+ extractContractErrorNameFromMessage,
41
+ // friendly-message mapping
42
+ getUserFriendlyErrorMessage,
43
+ DEFAULT_ERROR_PATTERNS,
44
+ type ErrorPattern,
45
+ // one-call handler for wagmi onError / catch blocks
46
+ handleWalletError,
47
+ type HandleWalletErrorOptions,
48
+ } from '@valve-tech/viem-errors'
49
+ ```
50
+
51
+ ## The five exports you'll actually call
52
+
53
+ | Export | Shape | Use when |
54
+ |---|---|---|
55
+ | `isUserRejectionError(error)` | `boolean` | You need to tell rejection from failure to decide between "reset to idle" vs "show error toast". |
56
+ | `extractContractErrorName(error)` | `string \| null` | You want to branch on the decoded Solidity error name (`'IntentExpired'`, `'HashMismatch'`, etc.) without parsing the raw revert message. |
57
+ | `getUserFriendlyErrorMessage(error, opts?)` | `string` | You need short user-facing copy and don't want to write the rejection-vs-decoded-vs-pattern pipeline yourself. |
58
+ | `handleWalletError(error, opts)` | `void` | One-liner for wagmi's `onError` callback or a catch block. Routes to `setStatus` / `setErrorMessage` / `toast.error|info` / `onError` sinks. |
59
+ | `walkErrorCause(error, opts?)` | `Iterable<unknown>` | You're doing custom inspection — generator yields the error then each link in `.cause` (default `maxDepth: 8`). |
60
+
61
+ ## Why three signals for rejection detection
62
+
63
+ `isUserRejectionError` walks the cause chain checking three signals at every link:
64
+
65
+ 1. EIP-1193 `code === 4001`
66
+ 2. Class name `UserRejectedRequestError` (viem's typed class)
67
+ 3. Message regex (`"User rejected"`, `"User denied"`, MetaMask's variants)
68
+
69
+ Any one is sufficient. The reason: no single signal is reliable across every wallet + version on the wire. WalletConnect drops the EIP-1193 code in some flows; injected providers wrap it in their own error class; mobile wallets sometimes only set the message. Checking all three at every link in the cause chain is the only check that works in production.
70
+
71
+ ## Why walk the cause chain at all
72
+
73
+ viem nests errors several layers deep:
74
+
75
+ ```
76
+ ContractFunctionExecutionError
77
+ └─ cause: RpcRequestError
78
+ └─ cause: UserRejectedRequestError ← the real signal lives here
79
+ ```
80
+
81
+ The wrapper's `.message` reads `"Failed to send transaction"`. Top-level message matching misses real rejections; top-level class checks miss them too. `walkErrorCause` is the foundation — every other detector in this package iterates with it.
82
+
83
+ ## The friendly-message pipeline
84
+
85
+ `getUserFriendlyErrorMessage(error, opts)` runs this order — **rejection check FIRST**:
86
+
87
+ ```
88
+ 1. isUserRejectionError(error) → USER_REJECTION_MESSAGE
89
+ 2. extractContractErrorName(error) → opts.customErrors[name] (if matched)
90
+ 3. opts.patterns + DEFAULT_ERROR_PATTERNS → first match's message
91
+ 4. opts.fallback ?? "Something went wrong. Please try again."
92
+ ```
93
+
94
+ Rejection-first matters because a `code === 4001` buried under a wrapper whose top-level message contains `"execution reverted"` must still produce the cancelled-by-user copy — not "transaction reverted on-chain".
95
+
96
+ `DEFAULT_ERROR_PATTERNS` covers protocol-agnostic cases (insufficient gas, replacement underpriced, rate-limited, network down, generic revert). Protocol-specific decoded errors (`HashMismatch` → "Proof didn't match the deposit") belong in `customErrors`, not in this package.
97
+
98
+ ## `handleWalletError` — the one-line shape
99
+
100
+ The canonical wagmi shape:
101
+
102
+ ```ts
103
+ useContractWrite({
104
+ // ...
105
+ onError: (err) => handleWalletError(err, {
106
+ setStatus, // 'idle' on rejection, 'error' on failure
107
+ setErrorMessage: setError, // null on rejection, friendly text on failure
108
+ toast, // toast.info on rejection, toast.error on failure
109
+ customErrors: {
110
+ HashMismatch: 'The proof did not match the deposit.',
111
+ InsufficientLiquidity: 'Not enough liquidity for this trade.',
112
+ },
113
+ onError: (e) => analytics.track('write.error', { message: e.message }),
114
+ }),
115
+ })
116
+ ```
117
+
118
+ `onError` is always called with the underlying error (coerced to `Error`) so analytics observers get the original. The other sinks branch by classification.
119
+
120
+ ## Pitfalls (read these)
121
+
122
+ 1. **Don't top-level-match for "user rejected".** Use `isUserRejectionError`. Top-level matches miss the buried-in-cause cases that production wallets actually emit.
123
+
124
+ 2. **Don't substring-match for "execution reverted" and bail with a generic message.** Call `extractContractErrorName` first — viem already decoded the error name into `data.errorName` somewhere in the cause chain. Falling back to "transaction failed" throws away the actual signal (`'IntentExpired'`, `'SlippageTooHigh'`).
125
+
126
+ 3. **Don't put protocol-specific copy in `DEFAULT_ERROR_PATTERNS`.** It's intentionally protocol-agnostic. Pass your protocol's custom-error map through `customErrors`.
127
+
128
+ 4. **Don't iterate `.cause` manually with a `while (e.cause) { e = e.cause }` loop.** It's an infinite loop on circular causes (rare, but real — some wallets emit them). `walkErrorCause`'s `maxDepth: 8` cap exists for this.
129
+
130
+ 5. **Don't catch and re-throw without preserving the original.** If you must wrap, set `cause: original` so the chain still walks. `handleWalletError`'s `onError` sink always receives the unwrapped original.
131
+
132
+ 6. **`getUserFriendlyErrorMessage` returns the FIRST pattern match.** If your custom pattern needs to win over a default, prepend it via `patterns: [...myPatterns, ...DEFAULT_ERROR_PATTERNS]` — order matters.
133
+
134
+ 7. **`handleWalletError` does NOT throw or re-throw.** It's a "side-effect-only" handler. If you want the catch block to bail after handling, throw yourself or split the catch:
135
+ ```ts
136
+ try { await writeAsync(...) }
137
+ catch (err) {
138
+ handleWalletError(err, { ... })
139
+ // pipeline-halt logic here
140
+ throw err
141
+ }
142
+ ```
143
+
144
+ ## Composition with sibling packages
145
+
146
+ `@valve-tech/wallet-adapter`'s `sendTransactionWithHooks` already throws a typed `WalletRejectedError` on rejection (using this package's three-signal detector internally). If you're using wallet-adapter's helpers, you don't need `isUserRejectionError` directly — `instanceof WalletRejectedError` is the canonical discriminator. Use viem-errors directly when you're NOT going through wallet-adapter (raw wagmi `useContractWrite`, raw viem `walletClient.sendTransaction`, etc.).
147
+
148
+ For decoded-error extraction, viem-errors stays valid even with wallet-adapter — `ContractRevertedError` carries the receipt, but `extractContractErrorName(err)` still gets you the decoded name from the cause chain regardless of which throw path you went through.
149
+
150
+ ## Skills (for AI agents)
151
+
152
+ `skills/` ships in the npm tarball. If you're an AI agent working in a
153
+ project that has installed this package, look in
154
+ `node_modules/@valve-tech/viem-errors/skills/viem-errors-integration/SKILL.md`
155
+ for trigger conditions, anti-pattern flags, and recipes.
156
+
157
+ ## Verifying provenance
158
+
159
+ ```bash
160
+ npm view @valve-tech/viem-errors@latest --json | jq .dist.attestations
161
+ npm audit signatures
162
+ ```
package/CHANGELOG.md CHANGED
@@ -6,6 +6,19 @@ this file.
6
6
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
  and this project adheres to [Semantic Versioning](https://semver.org/).
8
8
 
9
+ ## [0.11.0] — 2026-05-11
10
+
11
+ ### Notes
12
+
13
+ - Synchronized release — no changes to this package. Bumped in
14
+ lockstep with the rest of the toolkit alongside the v0.11.0
15
+ feature work in `@valve-tech/gas-oracle` (20-block ring lifecycle,
16
+ reorg detection, gap bridging), `@valve-tech/tx-tracker` (audit
17
+ fixes — durable rehydrate, retention enforcement, replaced-by
18
+ dedup, receipt-poll race, helper extraction), `@valve-tech/
19
+ wallet-adapter` (five wallet bridge examples), and
20
+ `@valve-tech/chain-source` (canonical-owner docs for wire types).
21
+
9
22
  ## [0.10.1] — 2026-05-08
10
23
 
11
24
  Synchronized release — no changes to this package. Republished at
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valve-tech/viem-errors",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "description": "Cause-chain-aware error utilities for viem-based dapps. Detect EIP-1193 user rejections (4001 / class name / message regex), extract decoded custom Solidity error names from anywhere in viem's nested cause chain, map raw RPC/wallet/contract errors to short user-friendly messages with overridable patterns, and route wagmi-style onError sinks through one helper. Pure functions, no runtime dependencies. Part of the valve-tech/evm-toolkit synchronized release line.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/valve-tech/evm-toolkit/tree/main/packages/viem-errors#readme",
@@ -34,7 +34,9 @@
34
34
  },
35
35
  "files": [
36
36
  "dist",
37
+ "skills",
37
38
  "README.md",
39
+ "AGENTS.md",
38
40
  "CHANGELOG.md",
39
41
  "LICENSE"
40
42
  ],
@@ -0,0 +1,163 @@
1
+ ---
2
+ name: viem-errors-integration
3
+ description: Integrate `@valve-tech/viem-errors` — cause-chain-aware error utilities for viem-based dapps — into wagmi `onError` callbacks, raw viem catch blocks, or anywhere a thrown error needs to be classified into "user rejected" vs "real failure" vs "decoded Solidity error". Use when the user is wiring `handleWalletError` into wagmi `useContractWrite` / `useSendTransaction`, calling `isUserRejectionError` to branch UI state to idle on rejection, calling `extractContractErrorName` to pull a decoded custom Solidity error name (`HashMismatch`, `InsufficientLiquidity`, `IntentExpired`) from viem's nested cause chain, mapping raw RPC errors to user-facing copy via `getUserFriendlyErrorMessage` + custom error patterns, or asks "why is my user-rejection check missing rejections" / "viem error is buried under a wrapper" / "how do I get the decoded error name from a `ContractFunctionExecutionError`". Also fires on imports of `@valve-tech/viem-errors` and questions about `walkErrorCause`, `DEFAULT_ERROR_PATTERNS`, `USER_REJECTION_MESSAGE`, the cause-chain `maxDepth` cap, or why three signals (EIP-1193 code, class name, message regex) are checked for rejection. Skip when the user is going through `@valve-tech/wallet-adapter`'s helpers (those throw typed `WalletRejectedError` / `ContractRevertedError` already — `instanceof` is the canonical discriminator there; delegate to wallet-adapter-integration), or when the user only wants generic JS error handling that has nothing to do with viem's cause-chain shape.
4
+ ---
5
+
6
+ # Integrating `@valve-tech/viem-errors`
7
+
8
+ Cause-chain-aware error utilities for viem-based dapps. Pure functions
9
+ over viem's nested error chain — no runtime dependencies. This skill is
10
+ for AI agents working in a project that imports the package, so they
11
+ recommend the right primitive for the user's situation rather than
12
+ re-implementing rejection/revert detection (which is what almost every
13
+ dapp does, and most get wrong).
14
+
15
+ ## Decision tree: which primitive to use
16
+
17
+ ```
18
+ Is the user inside wagmi's `onError` or a one-liner catch block where
19
+ they want classification + UI sinks (toast / setStatus / setError)?
20
+ ├── Yes — call `handleWalletError(err, { setStatus, setErrorMessage,
21
+ │ toast, customErrors })`. One line, all sinks routed.
22
+ └── No — do they need to BRANCH on the classification themselves?
23
+ ├── "Is this a user rejection?" → `isUserRejectionError(err)`
24
+ ├── "What custom Solidity error was thrown?"
25
+ │ → `extractContractErrorName(err)`
26
+ ├── "Give me a user-facing message string"
27
+ │ → `getUserFriendlyErrorMessage(err, { customErrors })`
28
+ └── "I'm doing custom inspection across the cause chain"
29
+ → iterate `walkErrorCause(err)` yourself
30
+ ```
31
+
32
+ ## How to recognize this package in the user's code
33
+
34
+ ```ts
35
+ import {
36
+ isUserRejectionError,
37
+ extractContractErrorName,
38
+ getUserFriendlyErrorMessage,
39
+ handleWalletError,
40
+ walkErrorCause,
41
+ } from '@valve-tech/viem-errors'
42
+ ```
43
+
44
+ `package.json` will show `"@valve-tech/viem-errors": "^0.10.x"`.
45
+
46
+ ## The canonical wagmi shape
47
+
48
+ ```ts
49
+ import { handleWalletError } from '@valve-tech/viem-errors'
50
+
51
+ const { writeAsync } = useContractWrite({
52
+ address, abi, functionName,
53
+ onError: (err) => handleWalletError(err, {
54
+ setStatus, // 'idle' on rejection, 'error' on failure
55
+ setErrorMessage: setError,
56
+ toast, // toast.info on rejection, toast.error on failure
57
+ customErrors: {
58
+ HashMismatch: 'The proof did not match the deposit.',
59
+ IntentExpired: 'This intent has expired — please refresh.',
60
+ InsufficientLiquidity: 'Not enough liquidity for this trade.',
61
+ },
62
+ onError: (e) => analytics.track('write.error', { message: e.message }),
63
+ }),
64
+ })
65
+ ```
66
+
67
+ The `customErrors` map is the per-protocol layer; `DEFAULT_ERROR_PATTERNS` covers the protocol-agnostic cases (insufficient gas, rate-limited, network down, generic revert) automatically.
68
+
69
+ ## The branching shape (when you need control)
70
+
71
+ ```ts
72
+ import { isUserRejectionError, extractContractErrorName } from '@valve-tech/viem-errors'
73
+
74
+ try {
75
+ await wallet.sendTransaction(tx)
76
+ } catch (err) {
77
+ if (isUserRejectionError(err)) {
78
+ setStatus('idle') // do NOT show error toast
79
+ return
80
+ }
81
+ const decoded = extractContractErrorName(err)
82
+ if (decoded === 'IntentExpired') {
83
+ promptUserToRefresh()
84
+ return
85
+ }
86
+ if (decoded === 'HashMismatch') {
87
+ showProofMismatchDialog()
88
+ return
89
+ }
90
+ // fall through to generic error UI
91
+ toast.error(getUserFriendlyErrorMessage(err))
92
+ throw err
93
+ }
94
+ ```
95
+
96
+ ## Anti-patterns to flag
97
+
98
+ When reviewing user code, watch for these and suggest fixes:
99
+
100
+ 1. **Top-level message matching for rejection.**
101
+ ```ts
102
+ // ❌ misses real rejections buried under wrappers
103
+ if (err.message.includes('User rejected')) ...
104
+
105
+ // ✅
106
+ if (isUserRejectionError(err)) ...
107
+ ```
108
+ The wrapper's `.message` typically reads `"Failed to send transaction"` even when the cause is a 4001. Walk the chain.
109
+
110
+ 2. **Top-level class checks for rejection.**
111
+ ```ts
112
+ // ❌ misses cases where viem wraps the rejection in a different class
113
+ if (err instanceof UserRejectedRequestError) ...
114
+ ```
115
+ Same problem — the typed class lives at some link in the cause chain, not necessarily at the top. `isUserRejectionError` walks for it.
116
+
117
+ 3. **Substring-matching `"execution reverted"` and bailing with a generic message.** viem already decoded the actual Solidity error name into `data.errorName` somewhere in the cause chain. Call `extractContractErrorName` first; only fall back to generic if it returns `null`.
118
+
119
+ 4. **Manual `while (e.cause) { e = e.cause }` loops.** Infinite loop on circular causes (rare but real — some wallet middleware emits them). `walkErrorCause`'s default `maxDepth: 8` cap is the safety net. If you must iterate manually, copy the cap.
120
+
121
+ 5. **Putting protocol-specific copy in `DEFAULT_ERROR_PATTERNS`** by extending the array. The defaults are intentionally protocol-agnostic. Pass your protocol's custom-error map through `customErrors` (works against `data.errorName` matches, not pattern regex).
122
+
123
+ 6. **Custom patterns ordered AFTER `DEFAULT_ERROR_PATTERNS`.** Pattern matching is first-match-wins. To override a default, prepend:
124
+ ```ts
125
+ getUserFriendlyErrorMessage(err, { patterns: [...myPatterns, ...DEFAULT_ERROR_PATTERNS] })
126
+ ```
127
+
128
+ 7. **Expecting `handleWalletError` to throw or re-throw.** It's a side-effect-only handler that routes to sinks. If your catch block needs to bail after handling, throw yourself:
129
+ ```ts
130
+ try { await writeAsync(args) }
131
+ catch (err) {
132
+ handleWalletError(err, { setStatus, setErrorMessage, toast })
133
+ throw err // explicit
134
+ }
135
+ ```
136
+
137
+ 8. **Re-implementing the three-signal rejection check.** Some dapps replicate the `code === 4001` + class name + regex logic inline. Just call `isUserRejectionError` — the three-signal check is the entire reason the package exists.
138
+
139
+ 9. **Wrapping errors without `cause`.** If you must throw a new error class, set `cause: original` so the chain still walks:
140
+ ```ts
141
+ // ❌ breaks the chain
142
+ throw new MyError(`Deposit failed: ${err.message}`)
143
+
144
+ // ✅ preserves it
145
+ throw new MyError('Deposit failed', { cause: err })
146
+ ```
147
+
148
+ ## When to skip this package
149
+
150
+ - **Going through `@valve-tech/wallet-adapter`'s helpers.** `sendTransactionWithHooks` already throws typed `WalletRejectedError` / `ContractRevertedError`. `instanceof` is the canonical discriminator there — no need to call `isUserRejectionError` after the fact. Internally those typed errors are detected via this package's three-signal check, so the discrimination is correct; you just don't need to redo it.
151
+ - **Non-viem error sources.** This package walks viem's cause chain shape. For raw HTTP errors, generic JS errors, or non-EVM SDKs, the helpers will return `null` / fall through to the fallback message — they won't crash, but they're not adding value either.
152
+
153
+ ## Composing with other packages
154
+
155
+ - `@valve-tech/wallet-adapter` uses this package internally for its `WalletRejectedError` discriminator. You don't need to call viem-errors directly when going through wallet-adapter's helpers.
156
+ - For decoded custom Solidity errors, `extractContractErrorName` works against `ContractRevertedError.cause` chains too — useful for branching after wallet-adapter's `instanceof ContractRevertedError` check.
157
+
158
+ ## Where to find more
159
+
160
+ - Full API + types: `node_modules/@valve-tech/viem-errors/AGENTS.md`
161
+ - Human-facing docs: `node_modules/@valve-tech/viem-errors/README.md`
162
+ - Compiled output: `node_modules/@valve-tech/viem-errors/dist/`
163
+ - Sibling skill: `wallet-adapter-integration` for the typed-error throw shape