@valve-tech/viem-errors 0.3.1
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/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/dist/contract-error.d.ts +37 -0
- package/dist/contract-error.d.ts.map +1 -0
- package/dist/contract-error.js +65 -0
- package/dist/contract-error.js.map +1 -0
- package/dist/handle.d.ts +56 -0
- package/dist/handle.d.ts.map +1 -0
- package/dist/handle.js +49 -0
- package/dist/handle.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/messages.d.ts +55 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +114 -0
- package/dist/messages.js.map +1 -0
- package/dist/rejection.d.ts +35 -0
- package/dist/rejection.d.ts.map +1 -0
- package/dist/rejection.js +62 -0
- package/dist/rejection.js.map +1 -0
- package/dist/walk.d.ts +29 -0
- package/dist/walk.d.ts.map +1 -0
- package/dist/walk.js +38 -0
- package/dist/walk.js.map +1 -0
- package/package.json +52 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@valve-tech/viem-errors` are documented in
|
|
4
|
+
this file.
|
|
5
|
+
|
|
6
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
7
|
+
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
8
|
+
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Initial implementation. Functions extracted from a real-world dapp
|
|
14
|
+
(Provex) where they had been re-derived inline; this is the first
|
|
15
|
+
upstream packaging for general use.
|
|
16
|
+
- `walkErrorCause(error, options?)` — generator over the viem
|
|
17
|
+
cause chain. Default `maxDepth: 8`.
|
|
18
|
+
- `isUserRejectionError(error)` — three-signal check across the
|
|
19
|
+
cause chain (EIP-1193 `code === 4001`, class name
|
|
20
|
+
`UserRejectedRequestError`, message regex fallback).
|
|
21
|
+
- `USER_REJECTION_MESSAGE` — default rejection copy.
|
|
22
|
+
- `extractContractErrorName(error)` — finds decoded custom
|
|
23
|
+
Solidity error names on `data.errorName` anywhere in the cause
|
|
24
|
+
chain.
|
|
25
|
+
- `extractContractErrorNameFromMessage(raw)` — scrape fallback for
|
|
26
|
+
flattened messages.
|
|
27
|
+
- `getUserFriendlyErrorMessage(error, options?)` — pipeline:
|
|
28
|
+
rejection → decoded custom error → consumer patterns → default
|
|
29
|
+
patterns → fallback.
|
|
30
|
+
- `DEFAULT_ERROR_PATTERNS` — protocol-agnostic patterns
|
|
31
|
+
(wallet / gas / replacement / network / rate-limit / revert).
|
|
32
|
+
- `handleWalletError(error, options)` — wagmi `onError`-shape
|
|
33
|
+
sink with separate rejection vs real-error paths.
|
|
34
|
+
- 49 unit tests covering cause-chain traversal, rejection detection,
|
|
35
|
+
custom-error extraction, message mapping, override precedence, and
|
|
36
|
+
sink routing.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Valve Tech
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# `@valve-tech/viem-errors`
|
|
2
|
+
|
|
3
|
+
Cause-chain-aware error utilities for viem-based dapps. Detect EIP-1193
|
|
4
|
+
user rejections, extract decoded custom Solidity error names from
|
|
5
|
+
anywhere in viem's nested error chain, map raw RPC/wallet/contract
|
|
6
|
+
errors to short user-friendly messages with overridable patterns, and
|
|
7
|
+
route wagmi-style `onError` sinks through one helper.
|
|
8
|
+
|
|
9
|
+
Pure functions, no runtime dependencies. viem is a peer dependency
|
|
10
|
+
(used only for the `Hex` type — the runtime is plain JS).
|
|
11
|
+
|
|
12
|
+
## Why
|
|
13
|
+
|
|
14
|
+
Every dapp re-implements wallet error handling, and most get it
|
|
15
|
+
slightly wrong:
|
|
16
|
+
|
|
17
|
+
- **Top-level message matching misses real rejections.** viem nests
|
|
18
|
+
errors several layers deep (`ContractFunctionExecutionError` →
|
|
19
|
+
`RpcRequestError` → `UserRejectedRequestError`). The wrapper's
|
|
20
|
+
`.message` reads `"Failed to send transaction"` — looks like a
|
|
21
|
+
generic failure, but the cause is actually a user rejection that
|
|
22
|
+
should reset the UI to idle, not show a red error toast.
|
|
23
|
+
- **Decoded custom Solidity errors get hidden.** The wrapping message
|
|
24
|
+
contains `"execution reverted"` so dapps short-circuit to a generic
|
|
25
|
+
"transaction failed" message, ignoring the actual `data.errorName`
|
|
26
|
+
(`HashMismatch`, `InsufficientLiquidity`, etc.) that viem decoded
|
|
27
|
+
for them.
|
|
28
|
+
- **Generic copy is the same everywhere.** "Insufficient funds for
|
|
29
|
+
gas", "previous transaction is still pending", "rate limited" — all
|
|
30
|
+
re-derived per project from raw RPC strings.
|
|
31
|
+
|
|
32
|
+
This package solves all three at the primitive layer. Extend the
|
|
33
|
+
default error map with your protocol's custom errors; everything else
|
|
34
|
+
just works.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
npm install @valve-tech/viem-errors viem
|
|
40
|
+
# or
|
|
41
|
+
yarn add @valve-tech/viem-errors viem
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quick start
|
|
45
|
+
|
|
46
|
+
### Detect a wallet rejection
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { isUserRejectionError } from '@valve-tech/viem-errors'
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
await wallet.sendTransaction(tx)
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (isUserRejectionError(err)) {
|
|
55
|
+
// Reset to idle. Do NOT show a "transaction failed" error.
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
throw err
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Centralised error handling for wagmi `onError`
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { handleWalletError } from '@valve-tech/viem-errors'
|
|
66
|
+
|
|
67
|
+
const { writeAsync } = useContractWrite({
|
|
68
|
+
// ...
|
|
69
|
+
onError: (err) => handleWalletError(err, {
|
|
70
|
+
setStatus,
|
|
71
|
+
setErrorMessage: setError,
|
|
72
|
+
toast,
|
|
73
|
+
onError: (e) => analytics.track('write.error', { message: e.message }),
|
|
74
|
+
customErrors: {
|
|
75
|
+
HashMismatch: 'The proof did not match the deposit.',
|
|
76
|
+
InsufficientLiquidity: 'Not enough liquidity for this trade.',
|
|
77
|
+
},
|
|
78
|
+
}),
|
|
79
|
+
})
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Extract a decoded custom error
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { extractContractErrorName } from '@valve-tech/viem-errors'
|
|
86
|
+
|
|
87
|
+
catch (err) {
|
|
88
|
+
const name = extractContractErrorName(err)
|
|
89
|
+
if (name === 'IntentExpired') {
|
|
90
|
+
promptUserToRefresh()
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
// ...
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## API
|
|
98
|
+
|
|
99
|
+
| Export | Shape |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| `walkErrorCause(error, opts?)` | Generator yielding `error` then each link in its `.cause` chain (default `maxDepth: 8`). |
|
|
102
|
+
| `isUserRejectionError(error)` | `true` if any link has EIP-1193 `code === 4001`, name `UserRejectedRequestError`, or matches the rejection-message regex. |
|
|
103
|
+
| `USER_REJECTION_MESSAGE` | Toast-friendly rejection copy. |
|
|
104
|
+
| `extractContractErrorName(error)` | First valid Solidity error name from `data.errorName` in the cause chain, else `null`. |
|
|
105
|
+
| `extractContractErrorNameFromMessage(raw)` | Scrape viem's `"reverted with the following reason:\n<Name>"` format from a flattened message string. |
|
|
106
|
+
| `getUserFriendlyErrorMessage(error, opts?)` | Short user-facing message. Pipeline: rejection → decoded custom error → consumer patterns → default patterns → fallback. |
|
|
107
|
+
| `DEFAULT_ERROR_PATTERNS` | Protocol-agnostic patterns covering wallet/gas/network/RPC/rate-limit/revert. |
|
|
108
|
+
| `handleWalletError(error, opts)` | Apply `getUserFriendlyErrorMessage` + sinks (`setStatus`, `setErrorMessage`, `toast`, `onError`). |
|
|
109
|
+
|
|
110
|
+
## Design notes
|
|
111
|
+
|
|
112
|
+
- **`walkErrorCause` is a generator.** Consumers can break early without
|
|
113
|
+
scanning the whole chain. Default depth of 8 caps a circular cause
|
|
114
|
+
reference instead of looping forever.
|
|
115
|
+
- **`isUserRejectionError` checks three signals at every link.** Any one of
|
|
116
|
+
`code === 4001` / class name / message regex is sufficient. Three
|
|
117
|
+
signals exist because no single one is reliable across every wallet
|
|
118
|
+
+ version on the wire.
|
|
119
|
+
- **`getUserFriendlyErrorMessage` puts rejection detection FIRST.** A 4001
|
|
120
|
+
buried in a wrapper whose top-level message contains "execution
|
|
121
|
+
reverted" must still produce the cancelled-by-user copy.
|
|
122
|
+
- **Default patterns are deliberately protocol-agnostic.** Custom-error
|
|
123
|
+
copy (`HashMismatch` → "Proof did not match the deposit") belongs
|
|
124
|
+
in your dapp's `customErrors` map, not in this package.
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
MIT
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Extract the decoded custom Solidity error name from a viem
|
|
3
|
+
* revert error.
|
|
4
|
+
*
|
|
5
|
+
* viem's `ContractFunctionRevertedError` exposes
|
|
6
|
+
* `data: { errorName, args, ... }` for decoded custom errors when the ABI
|
|
7
|
+
* is supplied. The decoded name is what the UI actually wants to surface
|
|
8
|
+
* ("HashMismatch" or "InsufficientDepositLiquidity") — much more useful
|
|
9
|
+
* than the wrapper's generic "execution reverted" text.
|
|
10
|
+
*
|
|
11
|
+
* The hard part is that viem's `data` field can sit several layers deep
|
|
12
|
+
* in the cause chain — sometimes flattened across an RPC boundary. We
|
|
13
|
+
* provide both a structured walk (`extractContractErrorName`) and a
|
|
14
|
+
* message-scraping fallback (`extractContractErrorNameFromMessage`) for
|
|
15
|
+
* the case where the cause chain has been collapsed.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Walk the cause chain and return the first valid Solidity error name found
|
|
19
|
+
* on `data.errorName`. Returns null if no such name is present.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const name = extractContractErrorName(error)
|
|
24
|
+
* if (name) {
|
|
25
|
+
* showFriendlyMessage(name)
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare function extractContractErrorName(error: unknown): string | null;
|
|
30
|
+
/**
|
|
31
|
+
* Fall back to scraping viem's stringified message when the cause chain has
|
|
32
|
+
* been flattened (e.g. across an RPC boundary or after JSON round-tripping).
|
|
33
|
+
* Matches the literal "reverted with the following reason:\n<ErrorName>"
|
|
34
|
+
* shape viem produces.
|
|
35
|
+
*/
|
|
36
|
+
export declare function extractContractErrorNameFromMessage(raw: string): string | null;
|
|
37
|
+
//# sourceMappingURL=contract-error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract-error.d.ts","sourceRoot":"","sources":["../src/contract-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAYH;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAWtE;AAED;;;;;GAKG;AACH,wBAAgB,mCAAmC,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI9E"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Extract the decoded custom Solidity error name from a viem
|
|
3
|
+
* revert error.
|
|
4
|
+
*
|
|
5
|
+
* viem's `ContractFunctionRevertedError` exposes
|
|
6
|
+
* `data: { errorName, args, ... }` for decoded custom errors when the ABI
|
|
7
|
+
* is supplied. The decoded name is what the UI actually wants to surface
|
|
8
|
+
* ("HashMismatch" or "InsufficientDepositLiquidity") — much more useful
|
|
9
|
+
* than the wrapper's generic "execution reverted" text.
|
|
10
|
+
*
|
|
11
|
+
* The hard part is that viem's `data` field can sit several layers deep
|
|
12
|
+
* in the cause chain — sometimes flattened across an RPC boundary. We
|
|
13
|
+
* provide both a structured walk (`extractContractErrorName`) and a
|
|
14
|
+
* message-scraping fallback (`extractContractErrorNameFromMessage`) for
|
|
15
|
+
* the case where the cause chain has been collapsed.
|
|
16
|
+
*/
|
|
17
|
+
import { walkErrorCause } from './walk.js';
|
|
18
|
+
/**
|
|
19
|
+
* Solidity custom error names start with an uppercase ASCII letter and
|
|
20
|
+
* contain only ASCII letters, digits, and underscores. Anything outside
|
|
21
|
+
* that pattern is rejected to avoid false positives — `data` fields named
|
|
22
|
+
* `errorName` exist on a few non-revert error shapes too.
|
|
23
|
+
*/
|
|
24
|
+
const ERROR_NAME_PATTERN = /^[A-Z][A-Za-z0-9_]*$/;
|
|
25
|
+
/**
|
|
26
|
+
* Walk the cause chain and return the first valid Solidity error name found
|
|
27
|
+
* on `data.errorName`. Returns null if no such name is present.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* const name = extractContractErrorName(error)
|
|
32
|
+
* if (name) {
|
|
33
|
+
* showFriendlyMessage(name)
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function extractContractErrorName(error) {
|
|
38
|
+
for (const link of walkErrorCause(error)) {
|
|
39
|
+
if (link === null || link === undefined || typeof link !== 'object')
|
|
40
|
+
continue;
|
|
41
|
+
const data = link.data;
|
|
42
|
+
if (data === null || data === undefined || typeof data !== 'object')
|
|
43
|
+
continue;
|
|
44
|
+
const name = data.errorName;
|
|
45
|
+
if (typeof name !== 'string')
|
|
46
|
+
continue;
|
|
47
|
+
if (!ERROR_NAME_PATTERN.test(name))
|
|
48
|
+
continue;
|
|
49
|
+
return name;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Fall back to scraping viem's stringified message when the cause chain has
|
|
55
|
+
* been flattened (e.g. across an RPC boundary or after JSON round-tripping).
|
|
56
|
+
* Matches the literal "reverted with the following reason:\n<ErrorName>"
|
|
57
|
+
* shape viem produces.
|
|
58
|
+
*/
|
|
59
|
+
export function extractContractErrorNameFromMessage(raw) {
|
|
60
|
+
if (raw.length === 0)
|
|
61
|
+
return null;
|
|
62
|
+
const match = raw.match(/reverted with the following reason:\s*\n?\s*([A-Z][A-Za-z0-9_]*)\(?/);
|
|
63
|
+
return match?.[1] ?? null;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=contract-error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract-error.js","sourceRoot":"","sources":["../src/contract-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAE1C;;;;;GAKG;AACH,MAAM,kBAAkB,GAAG,sBAAsB,CAAA;AAEjD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,wBAAwB,CAAC,KAAc;IACrD,KAAK,MAAM,IAAI,IAAI,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;QACzC,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,SAAS,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,SAAQ;QAC7E,MAAM,IAAI,GAAI,IAA2B,CAAC,IAAI,CAAA;QAC9C,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,SAAS,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,SAAQ;QAC7E,MAAM,IAAI,GAAI,IAAgC,CAAC,SAAS,CAAA;QACxD,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,SAAQ;QACtC,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,SAAQ;QAC5C,OAAO,IAAI,CAAA;IACb,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mCAAmC,CAAC,GAAW;IAC7D,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACjC,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,qEAAqE,CAAC,CAAA;IAC9F,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAA;AAC3B,CAAC"}
|
package/dist/handle.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Centralised error handler for wagmi / viem write paths.
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates the rejection-detection + friendly-message + status-reset
|
|
5
|
+
* pattern so every catch block / `onError` callback is a single line. The
|
|
6
|
+
* downstream payoff: a dapp's wallet UX stays consistent across every
|
|
7
|
+
* write site instead of each one re-implementing rejection-vs-failure
|
|
8
|
+
* detection (and getting it slightly wrong).
|
|
9
|
+
*/
|
|
10
|
+
import { type ErrorPattern } from './messages.js';
|
|
11
|
+
/**
|
|
12
|
+
* Sinks the helper writes into when classifying an error.
|
|
13
|
+
*/
|
|
14
|
+
export interface HandleWalletErrorOptions {
|
|
15
|
+
/** Callback to set a UI status — `idle` on rejection, `error` on real failure. */
|
|
16
|
+
setStatus?: (status: 'idle' | 'error') => void;
|
|
17
|
+
/** Callback to store the user-facing message — `null` on rejection, friendly text on failure. */
|
|
18
|
+
setErrorMessage?: (message: string | null) => void;
|
|
19
|
+
/** Toast bridge (e.g. sonner). Rejection → `info`, real error → `error`. */
|
|
20
|
+
toast?: {
|
|
21
|
+
error: (message: string) => void;
|
|
22
|
+
info: (message: string) => void;
|
|
23
|
+
};
|
|
24
|
+
/** Always called with the underlying error (coerced to `Error`). */
|
|
25
|
+
onError?: (error: Error) => void;
|
|
26
|
+
/** Forwarded to `getUserFriendlyErrorMessage`. */
|
|
27
|
+
customErrors?: Record<string, string>;
|
|
28
|
+
/** Forwarded to `getUserFriendlyErrorMessage`. */
|
|
29
|
+
patterns?: readonly ErrorPattern[];
|
|
30
|
+
/** Forwarded to `getUserFriendlyErrorMessage`. */
|
|
31
|
+
fallback?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Classify a thrown value as either a user rejection or a real failure, then
|
|
35
|
+
* route the configured sinks accordingly.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* // wagmi onError shape
|
|
40
|
+
* onError: (err) => handleWalletError(err, {
|
|
41
|
+
* setStatus: setTxStatus,
|
|
42
|
+
* setErrorMessage: setError,
|
|
43
|
+
* toast,
|
|
44
|
+
* customErrors: { HashMismatch: 'Proof did not match.' },
|
|
45
|
+
* })
|
|
46
|
+
*
|
|
47
|
+
* // catch block shape
|
|
48
|
+
* try {
|
|
49
|
+
* await writeAsync(args)
|
|
50
|
+
* } catch (err) {
|
|
51
|
+
* handleWalletError(err, { setStatus, setErrorMessage, toast })
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare function handleWalletError(error: unknown, options?: HandleWalletErrorOptions): void;
|
|
56
|
+
//# sourceMappingURL=handle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handle.d.ts","sourceRoot":"","sources":["../src/handle.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,KAAK,YAAY,EAA+B,MAAM,eAAe,CAAA;AAE9E;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,kFAAkF;IAClF,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,KAAK,IAAI,CAAA;IAC9C,iGAAiG;IACjG,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAClD,4EAA4E;IAC5E,KAAK,CAAC,EAAE;QAAE,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAA;IAC7E,oEAAoE;IACpE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IAChC,kDAAkD;IAClD,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACrC,kDAAkD;IAClD,QAAQ,CAAC,EAAE,SAAS,YAAY,EAAE,CAAA;IAClC,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,OAAO,EACd,OAAO,GAAE,wBAA6B,GACrC,IAAI,CAeN"}
|
package/dist/handle.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Centralised error handler for wagmi / viem write paths.
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates the rejection-detection + friendly-message + status-reset
|
|
5
|
+
* pattern so every catch block / `onError` callback is a single line. The
|
|
6
|
+
* downstream payoff: a dapp's wallet UX stays consistent across every
|
|
7
|
+
* write site instead of each one re-implementing rejection-vs-failure
|
|
8
|
+
* detection (and getting it slightly wrong).
|
|
9
|
+
*/
|
|
10
|
+
import { USER_REJECTION_MESSAGE, isUserRejectionError } from './rejection.js';
|
|
11
|
+
import { getUserFriendlyErrorMessage } from './messages.js';
|
|
12
|
+
/**
|
|
13
|
+
* Classify a thrown value as either a user rejection or a real failure, then
|
|
14
|
+
* route the configured sinks accordingly.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* // wagmi onError shape
|
|
19
|
+
* onError: (err) => handleWalletError(err, {
|
|
20
|
+
* setStatus: setTxStatus,
|
|
21
|
+
* setErrorMessage: setError,
|
|
22
|
+
* toast,
|
|
23
|
+
* customErrors: { HashMismatch: 'Proof did not match.' },
|
|
24
|
+
* })
|
|
25
|
+
*
|
|
26
|
+
* // catch block shape
|
|
27
|
+
* try {
|
|
28
|
+
* await writeAsync(args)
|
|
29
|
+
* } catch (err) {
|
|
30
|
+
* handleWalletError(err, { setStatus, setErrorMessage, toast })
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function handleWalletError(error, options = {}) {
|
|
35
|
+
const { setStatus, setErrorMessage, toast, onError, customErrors, patterns, fallback } = options;
|
|
36
|
+
if (isUserRejectionError(error)) {
|
|
37
|
+
setStatus?.('idle');
|
|
38
|
+
setErrorMessage?.(null);
|
|
39
|
+
toast?.info(USER_REJECTION_MESSAGE);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
const message = getUserFriendlyErrorMessage(error, { customErrors, patterns, fallback });
|
|
43
|
+
setStatus?.('error');
|
|
44
|
+
setErrorMessage?.(message);
|
|
45
|
+
toast?.error(message);
|
|
46
|
+
}
|
|
47
|
+
onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=handle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handle.js","sourceRoot":"","sources":["../src/handle.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAC7E,OAAO,EAAqB,2BAA2B,EAAE,MAAM,eAAe,CAAA;AAsB9E;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,iBAAiB,CAC/B,KAAc,EACd,UAAoC,EAAE;IAEtC,MAAM,EAAE,SAAS,EAAE,eAAe,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAA;IAEhG,IAAI,oBAAoB,CAAC,KAAK,CAAC,EAAE,CAAC;QAChC,SAAS,EAAE,CAAC,MAAM,CAAC,CAAA;QACnB,eAAe,EAAE,CAAC,IAAI,CAAC,CAAA;QACvB,KAAK,EAAE,IAAI,CAAC,sBAAsB,CAAC,CAAA;IACrC,CAAC;SAAM,CAAC;QACN,MAAM,OAAO,GAAG,2BAA2B,CAAC,KAAK,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAA;QACxF,SAAS,EAAE,CAAC,OAAO,CAAC,CAAA;QACpB,eAAe,EAAE,CAAC,OAAO,CAAC,CAAA;QAC1B,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,CAAA;IACvB,CAAC;IAED,OAAO,EAAE,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;AACtE,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Public API of `@valve-tech/viem-errors`.
|
|
3
|
+
*/
|
|
4
|
+
export { walkErrorCause } from './walk.js';
|
|
5
|
+
export { isUserRejectionError, USER_REJECTION_MESSAGE } from './rejection.js';
|
|
6
|
+
export { extractContractErrorName, extractContractErrorNameFromMessage } from './contract-error.js';
|
|
7
|
+
export { getUserFriendlyErrorMessage, DEFAULT_ERROR_PATTERNS, type ErrorPattern, } from './messages.js';
|
|
8
|
+
export { handleWalletError, type HandleWalletErrorOptions } from './handle.js';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAC1C,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;AAC7E,OAAO,EAAE,wBAAwB,EAAE,mCAAmC,EAAE,MAAM,qBAAqB,CAAA;AACnG,OAAO,EACL,2BAA2B,EAC3B,sBAAsB,EACtB,KAAK,YAAY,GAClB,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,iBAAiB,EAAE,KAAK,wBAAwB,EAAE,MAAM,aAAa,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Public API of `@valve-tech/viem-errors`.
|
|
3
|
+
*/
|
|
4
|
+
export { walkErrorCause } from './walk.js';
|
|
5
|
+
export { isUserRejectionError, USER_REJECTION_MESSAGE } from './rejection.js';
|
|
6
|
+
export { extractContractErrorName, extractContractErrorNameFromMessage } from './contract-error.js';
|
|
7
|
+
export { getUserFriendlyErrorMessage, DEFAULT_ERROR_PATTERNS, } from './messages.js';
|
|
8
|
+
export { handleWalletError } from './handle.js';
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAC1C,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;AAC7E,OAAO,EAAE,wBAAwB,EAAE,mCAAmC,EAAE,MAAM,qBAAqB,CAAA;AACnG,OAAO,EACL,2BAA2B,EAC3B,sBAAsB,GAEvB,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,iBAAiB,EAAiC,MAAM,aAAa,CAAA"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Map raw viem / wagmi / wallet errors to short user-friendly
|
|
3
|
+
* messages.
|
|
4
|
+
*
|
|
5
|
+
* The default `DEFAULT_ERROR_PATTERNS` is deliberately protocol-agnostic —
|
|
6
|
+
* wallet rejection, gas/funds, replacement-tx, network/RPC, rate-limit,
|
|
7
|
+
* generic revert. Consumers add their protocol-specific custom-Solidity-error
|
|
8
|
+
* messages via the `customErrors` option on `getUserFriendlyErrorMessage`,
|
|
9
|
+
* and can prepend additional patterns via the `patterns` option without
|
|
10
|
+
* editing the default list.
|
|
11
|
+
*
|
|
12
|
+
* Match order at runtime:
|
|
13
|
+
* 1. Wallet rejection — surfaced first so a 4001 buried inside a wrapper
|
|
14
|
+
* whose top-level message contains "execution reverted" still produces
|
|
15
|
+
* the cancelled-by-user copy instead of the generic on-chain fallback.
|
|
16
|
+
* 2. Decoded custom-error name from `data.errorName` anywhere in the
|
|
17
|
+
* cause chain — best signal when the ABI is wired through viem.
|
|
18
|
+
* 3. Custom-error name extracted from the wrapper's stringified message —
|
|
19
|
+
* a flattened-cause-chain fallback.
|
|
20
|
+
* 4. Caller-supplied additional patterns (checked BEFORE defaults so a
|
|
21
|
+
* consumer can override generic copy).
|
|
22
|
+
* 5. Default protocol-agnostic patterns.
|
|
23
|
+
* 6. The configured `fallback`, or a generic message.
|
|
24
|
+
*/
|
|
25
|
+
/** A pattern → message pair used by `getUserFriendlyErrorMessage`. */
|
|
26
|
+
export interface ErrorPattern {
|
|
27
|
+
pattern: RegExp;
|
|
28
|
+
message: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Default protocol-agnostic patterns. Consumers can prepend their own
|
|
32
|
+
* via the `patterns` option; the consumer entries win.
|
|
33
|
+
*/
|
|
34
|
+
export declare const DEFAULT_ERROR_PATTERNS: readonly ErrorPattern[];
|
|
35
|
+
/**
|
|
36
|
+
* Convert a raw error into a short, user-friendly message safe to display.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* try {
|
|
41
|
+
* await client.signalIntent(params)
|
|
42
|
+
* } catch (error) {
|
|
43
|
+
* const message = getUserFriendlyErrorMessage(error, {
|
|
44
|
+
* customErrors: { HashMismatch: 'The proof did not match the deposit.' },
|
|
45
|
+
* })
|
|
46
|
+
* showToast(message)
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare function getUserFriendlyErrorMessage(error: unknown, options?: {
|
|
51
|
+
customErrors?: Record<string, string>;
|
|
52
|
+
patterns?: readonly ErrorPattern[];
|
|
53
|
+
fallback?: string;
|
|
54
|
+
}): string;
|
|
55
|
+
//# sourceMappingURL=messages.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAKH,sEAAsE;AACtE,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;;GAGG;AACH,eAAO,MAAM,sBAAsB,EAAE,SAAS,YAAY,EA4BzD,CAAA;AAKD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,OAAO,EACd,OAAO,GAAE;IACP,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACrC,QAAQ,CAAC,EAAE,SAAS,YAAY,EAAE,CAAA;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAA;CACb,GACL,MAAM,CAqBR"}
|
package/dist/messages.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Map raw viem / wagmi / wallet errors to short user-friendly
|
|
3
|
+
* messages.
|
|
4
|
+
*
|
|
5
|
+
* The default `DEFAULT_ERROR_PATTERNS` is deliberately protocol-agnostic —
|
|
6
|
+
* wallet rejection, gas/funds, replacement-tx, network/RPC, rate-limit,
|
|
7
|
+
* generic revert. Consumers add their protocol-specific custom-Solidity-error
|
|
8
|
+
* messages via the `customErrors` option on `getUserFriendlyErrorMessage`,
|
|
9
|
+
* and can prepend additional patterns via the `patterns` option without
|
|
10
|
+
* editing the default list.
|
|
11
|
+
*
|
|
12
|
+
* Match order at runtime:
|
|
13
|
+
* 1. Wallet rejection — surfaced first so a 4001 buried inside a wrapper
|
|
14
|
+
* whose top-level message contains "execution reverted" still produces
|
|
15
|
+
* the cancelled-by-user copy instead of the generic on-chain fallback.
|
|
16
|
+
* 2. Decoded custom-error name from `data.errorName` anywhere in the
|
|
17
|
+
* cause chain — best signal when the ABI is wired through viem.
|
|
18
|
+
* 3. Custom-error name extracted from the wrapper's stringified message —
|
|
19
|
+
* a flattened-cause-chain fallback.
|
|
20
|
+
* 4. Caller-supplied additional patterns (checked BEFORE defaults so a
|
|
21
|
+
* consumer can override generic copy).
|
|
22
|
+
* 5. Default protocol-agnostic patterns.
|
|
23
|
+
* 6. The configured `fallback`, or a generic message.
|
|
24
|
+
*/
|
|
25
|
+
import { extractContractErrorName, extractContractErrorNameFromMessage } from './contract-error.js';
|
|
26
|
+
import { isUserRejectionError } from './rejection.js';
|
|
27
|
+
/**
|
|
28
|
+
* Default protocol-agnostic patterns. Consumers can prepend their own
|
|
29
|
+
* via the `patterns` option; the consumer entries win.
|
|
30
|
+
*/
|
|
31
|
+
export const DEFAULT_ERROR_PATTERNS = [
|
|
32
|
+
// Gas / balance — should match before generic "execution reverted" because
|
|
33
|
+
// these are more specific.
|
|
34
|
+
{ pattern: /insufficient funds/i, message: 'Insufficient funds for gas fees.' },
|
|
35
|
+
{ pattern: /gas required exceeds/i, message: 'Transaction requires more gas than allowed.' },
|
|
36
|
+
// Replacement / nonce — single message because the user fix is identical
|
|
37
|
+
// (wait or speed up the prior pending tx).
|
|
38
|
+
{
|
|
39
|
+
pattern: /could not replace existing tx|replacement transaction underpriced|nonce too low/i,
|
|
40
|
+
message: 'A previous transaction is still pending. Please wait for it to confirm or speed it up in your wallet.',
|
|
41
|
+
},
|
|
42
|
+
// Network / RPC connectivity. "disconnected" before "could not detect" so
|
|
43
|
+
// the more user-actionable copy wins.
|
|
44
|
+
{ pattern: /disconnected|not connected/i, message: 'Wallet disconnected. Please reconnect and try again.' },
|
|
45
|
+
{ pattern: /could not detect network|network changed/i, message: 'Network connection issue. Please check your wallet network.' },
|
|
46
|
+
{ pattern: /timeout|etimedout/i, message: 'Request timed out. Please try again.' },
|
|
47
|
+
{ pattern: /fetch failed|econnrefused|enotfound/i, message: 'Unable to reach the server. Please check your connection.' },
|
|
48
|
+
// Rate limiting / service availability.
|
|
49
|
+
{ pattern: /\b429\b|too many requests/i, message: 'Too many requests. Please wait a moment and try again.' },
|
|
50
|
+
{ pattern: /\b503\b|service unavailable/i, message: 'Service temporarily unavailable. Please try again shortly.' },
|
|
51
|
+
// Generic on-chain revert — last resort. Specific custom-error extraction
|
|
52
|
+
// happens earlier in the pipeline, so reaching here means the revert
|
|
53
|
+
// wasn't decoded.
|
|
54
|
+
{ pattern: /execution reverted/i, message: 'Transaction failed on-chain.' },
|
|
55
|
+
];
|
|
56
|
+
const DEFAULT_FALLBACK = 'Something went wrong. Please try again.';
|
|
57
|
+
const DEFAULT_REJECTION_COPY = 'Transaction was cancelled.';
|
|
58
|
+
/**
|
|
59
|
+
* Convert a raw error into a short, user-friendly message safe to display.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* try {
|
|
64
|
+
* await client.signalIntent(params)
|
|
65
|
+
* } catch (error) {
|
|
66
|
+
* const message = getUserFriendlyErrorMessage(error, {
|
|
67
|
+
* customErrors: { HashMismatch: 'The proof did not match the deposit.' },
|
|
68
|
+
* })
|
|
69
|
+
* showToast(message)
|
|
70
|
+
* }
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export function getUserFriendlyErrorMessage(error, options = {}) {
|
|
74
|
+
if (isUserRejectionError(error))
|
|
75
|
+
return DEFAULT_REJECTION_COPY;
|
|
76
|
+
const raw = stringifyError(error);
|
|
77
|
+
const errorName = extractContractErrorName(error) ?? extractContractErrorNameFromMessage(raw);
|
|
78
|
+
if (errorName !== null) {
|
|
79
|
+
const friendly = options.customErrors?.[errorName];
|
|
80
|
+
return friendly !== undefined
|
|
81
|
+
? `${friendly} (${errorName})`
|
|
82
|
+
: `Transaction failed: ${humaniseErrorName(errorName)}.`;
|
|
83
|
+
}
|
|
84
|
+
const consumerPatterns = options.patterns ?? [];
|
|
85
|
+
for (const { pattern, message } of consumerPatterns) {
|
|
86
|
+
if (pattern.test(raw))
|
|
87
|
+
return message;
|
|
88
|
+
}
|
|
89
|
+
for (const { pattern, message } of DEFAULT_ERROR_PATTERNS) {
|
|
90
|
+
if (pattern.test(raw))
|
|
91
|
+
return message;
|
|
92
|
+
}
|
|
93
|
+
return options.fallback ?? DEFAULT_FALLBACK;
|
|
94
|
+
}
|
|
95
|
+
/** "PaymentVerificationFailed" → "Payment Verification Failed" */
|
|
96
|
+
function humaniseErrorName(name) {
|
|
97
|
+
return name.replace(/([A-Z])/g, ' $1').trim();
|
|
98
|
+
}
|
|
99
|
+
/** Normalise an unknown thrown value into a string for pattern matching. */
|
|
100
|
+
function stringifyError(error) {
|
|
101
|
+
if (error === null || error === undefined)
|
|
102
|
+
return '';
|
|
103
|
+
if (error instanceof Error)
|
|
104
|
+
return error.message;
|
|
105
|
+
if (typeof error === 'string')
|
|
106
|
+
return error;
|
|
107
|
+
try {
|
|
108
|
+
return JSON.stringify(error);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return String(error);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=messages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"messages.js","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Detect a user-initiated wallet rejection.
|
|
3
|
+
*
|
|
4
|
+
* Wallet UX needs to distinguish *I cancelled* from *something failed*.
|
|
5
|
+
* The two surfaces look superficially similar — both throw, both reach
|
|
6
|
+
* `catch` blocks — but they call for different UI: rejection → reset to
|
|
7
|
+
* idle, real failure → show error message and offer retry.
|
|
8
|
+
*
|
|
9
|
+
* The challenge: rejection text varies by wallet, locale, and error
|
|
10
|
+
* version (`User rejected the request`, `user cancelled`, `User denied
|
|
11
|
+
* transaction signature`, `User disapproved`, etc.). Top-level
|
|
12
|
+
* `.message` matching alone is fragile because viem nests the actual
|
|
13
|
+
* rejection several layers deep — the wrapper's message reads
|
|
14
|
+
* `Failed to send transaction`, which would otherwise look like a
|
|
15
|
+
* generic failure and get the wrong UI.
|
|
16
|
+
*/
|
|
17
|
+
/** Toast-friendly message shown when the user rejects a wallet prompt. */
|
|
18
|
+
export declare const USER_REJECTION_MESSAGE = "Transaction was rejected in wallet.";
|
|
19
|
+
/**
|
|
20
|
+
* Check whether an error represents a user-initiated wallet rejection.
|
|
21
|
+
*
|
|
22
|
+
* Walks the cause chain (viem nests rejections several layers deep —
|
|
23
|
+
* typically `ContractFunctionExecutionError` → `RpcRequestError` →
|
|
24
|
+
* `UserRejectedRequestError`) and checks three independent signals at
|
|
25
|
+
* each level:
|
|
26
|
+
* 1. **EIP-1193 `code === 4001`** — the spec-mandated rejection code,
|
|
27
|
+
* language-independent. The most reliable signal when present.
|
|
28
|
+
* 2. **Error name `UserRejectedRequestError`** — viem's class name.
|
|
29
|
+
* 3. **Message text regex** — fallback for wallets that bypass the
|
|
30
|
+
* standard error class.
|
|
31
|
+
*
|
|
32
|
+
* Any one of those three is sufficient at any level in the chain.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isUserRejectionError(error: unknown): boolean;
|
|
35
|
+
//# sourceMappingURL=rejection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rejection.d.ts","sourceRoot":"","sources":["../src/rejection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,0EAA0E;AAC1E,eAAO,MAAM,sBAAsB,wCAAwC,CAAA;AAS3E;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAe5D"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Detect a user-initiated wallet rejection.
|
|
3
|
+
*
|
|
4
|
+
* Wallet UX needs to distinguish *I cancelled* from *something failed*.
|
|
5
|
+
* The two surfaces look superficially similar — both throw, both reach
|
|
6
|
+
* `catch` blocks — but they call for different UI: rejection → reset to
|
|
7
|
+
* idle, real failure → show error message and offer retry.
|
|
8
|
+
*
|
|
9
|
+
* The challenge: rejection text varies by wallet, locale, and error
|
|
10
|
+
* version (`User rejected the request`, `user cancelled`, `User denied
|
|
11
|
+
* transaction signature`, `User disapproved`, etc.). Top-level
|
|
12
|
+
* `.message` matching alone is fragile because viem nests the actual
|
|
13
|
+
* rejection several layers deep — the wrapper's message reads
|
|
14
|
+
* `Failed to send transaction`, which would otherwise look like a
|
|
15
|
+
* generic failure and get the wrong UI.
|
|
16
|
+
*/
|
|
17
|
+
import { walkErrorCause } from './walk.js';
|
|
18
|
+
/** Toast-friendly message shown when the user rejects a wallet prompt. */
|
|
19
|
+
export const USER_REJECTION_MESSAGE = 'Transaction was rejected in wallet.';
|
|
20
|
+
/**
|
|
21
|
+
* Wallet-rejection text varies by wallet/locale ("User rejected the request",
|
|
22
|
+
* "user cancelled", "User denied transaction signature", …) so message
|
|
23
|
+
* matching alone is fragile. Order: most-common viem/MetaMask phrasing first.
|
|
24
|
+
*/
|
|
25
|
+
const REJECTION_MESSAGE_PATTERN = /user rejected|user denied|rejected the request|user cancelled|user canceled|user disapproved/i;
|
|
26
|
+
/**
|
|
27
|
+
* Check whether an error represents a user-initiated wallet rejection.
|
|
28
|
+
*
|
|
29
|
+
* Walks the cause chain (viem nests rejections several layers deep —
|
|
30
|
+
* typically `ContractFunctionExecutionError` → `RpcRequestError` →
|
|
31
|
+
* `UserRejectedRequestError`) and checks three independent signals at
|
|
32
|
+
* each level:
|
|
33
|
+
* 1. **EIP-1193 `code === 4001`** — the spec-mandated rejection code,
|
|
34
|
+
* language-independent. The most reliable signal when present.
|
|
35
|
+
* 2. **Error name `UserRejectedRequestError`** — viem's class name.
|
|
36
|
+
* 3. **Message text regex** — fallback for wallets that bypass the
|
|
37
|
+
* standard error class.
|
|
38
|
+
*
|
|
39
|
+
* Any one of those three is sufficient at any level in the chain.
|
|
40
|
+
*/
|
|
41
|
+
export function isUserRejectionError(error) {
|
|
42
|
+
for (const link of walkErrorCause(error)) {
|
|
43
|
+
if (link === null || link === undefined)
|
|
44
|
+
continue;
|
|
45
|
+
if (typeof link === 'string') {
|
|
46
|
+
if (REJECTION_MESSAGE_PATTERN.test(link))
|
|
47
|
+
return true;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (typeof link !== 'object')
|
|
51
|
+
continue;
|
|
52
|
+
const node = link;
|
|
53
|
+
if (node.code === 4001)
|
|
54
|
+
return true;
|
|
55
|
+
if (typeof node.name === 'string' && node.name === 'UserRejectedRequestError')
|
|
56
|
+
return true;
|
|
57
|
+
if (typeof node.message === 'string' && REJECTION_MESSAGE_PATTERN.test(node.message))
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=rejection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rejection.js","sourceRoot":"","sources":["../src/rejection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAE1C,0EAA0E;AAC1E,MAAM,CAAC,MAAM,sBAAsB,GAAG,qCAAqC,CAAA;AAE3E;;;;GAIG;AACH,MAAM,yBAAyB,GAAG,+FAA+F,CAAA;AAEjI;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAc;IACjD,KAAK,MAAM,IAAI,IAAI,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;QACzC,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,SAAS;YAAE,SAAQ;QACjD,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC7B,IAAI,yBAAyB,CAAC,IAAI,CAAC,IAAI,CAAC;gBAAE,OAAO,IAAI,CAAA;YACrD,SAAQ;QACV,CAAC;QACD,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,SAAQ;QAEtC,MAAM,IAAI,GAAG,IAA6D,CAAA;QAC1E,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI;YAAE,OAAO,IAAI,CAAA;QACnC,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,KAAK,0BAA0B;YAAE,OAAO,IAAI,CAAA;QAC1F,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,IAAI,yBAAyB,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC;YAAE,OAAO,IAAI,CAAA;IACnG,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC"}
|
package/dist/walk.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Walk an error's `cause` chain.
|
|
3
|
+
*
|
|
4
|
+
* viem nests errors several levels deep — `ContractFunctionExecutionError`
|
|
5
|
+
* wraps `RpcRequestError` wraps `UserRejectedRequestError`, etc. — so checks
|
|
6
|
+
* that only inspect the top-level error miss the actual underlying cause.
|
|
7
|
+
* This generator yields each link in order so callers can decide what to look
|
|
8
|
+
* for at each depth.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Walk an error and its `cause` chain, yielding each link in order, starting
|
|
12
|
+
* with the error itself. Stops at `null`/`undefined`, when the chain hits a
|
|
13
|
+
* primitive that has no `cause`, or when `maxDepth` is reached. Default depth
|
|
14
|
+
* is 8 — enough for every viem stack we've seen in the wild without risking
|
|
15
|
+
* a runaway loop on a circular chain.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* for (const link of walkErrorCause(error)) {
|
|
20
|
+
* if (typeof link === 'object' && link && 'code' in link && link.code === 4001) {
|
|
21
|
+
* return true
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function walkErrorCause(error: unknown, options?: {
|
|
27
|
+
maxDepth?: number;
|
|
28
|
+
}): Generator<unknown, void, unknown>;
|
|
29
|
+
//# sourceMappingURL=walk.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"walk.d.ts","sourceRoot":"","sources":["../src/walk.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;;;;;;;;GAeG;AACH,wBAAiB,cAAc,CAC7B,KAAK,EAAE,OAAO,EACd,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAClC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,CASnC"}
|
package/dist/walk.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Walk an error's `cause` chain.
|
|
3
|
+
*
|
|
4
|
+
* viem nests errors several levels deep — `ContractFunctionExecutionError`
|
|
5
|
+
* wraps `RpcRequestError` wraps `UserRejectedRequestError`, etc. — so checks
|
|
6
|
+
* that only inspect the top-level error miss the actual underlying cause.
|
|
7
|
+
* This generator yields each link in order so callers can decide what to look
|
|
8
|
+
* for at each depth.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Walk an error and its `cause` chain, yielding each link in order, starting
|
|
12
|
+
* with the error itself. Stops at `null`/`undefined`, when the chain hits a
|
|
13
|
+
* primitive that has no `cause`, or when `maxDepth` is reached. Default depth
|
|
14
|
+
* is 8 — enough for every viem stack we've seen in the wild without risking
|
|
15
|
+
* a runaway loop on a circular chain.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* for (const link of walkErrorCause(error)) {
|
|
20
|
+
* if (typeof link === 'object' && link && 'code' in link && link.code === 4001) {
|
|
21
|
+
* return true
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function* walkErrorCause(error, options = {}) {
|
|
27
|
+
const maxDepth = options.maxDepth ?? 8;
|
|
28
|
+
let current = error;
|
|
29
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
30
|
+
if (current === null || current === undefined)
|
|
31
|
+
return;
|
|
32
|
+
yield current;
|
|
33
|
+
if (typeof current !== 'object')
|
|
34
|
+
return;
|
|
35
|
+
current = current.cause;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=walk.js.map
|
package/dist/walk.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"walk.js","sourceRoot":"","sources":["../src/walk.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;;;;;;;;GAeG;AACH,MAAM,SAAS,CAAC,CAAC,cAAc,CAC7B,KAAc,EACd,UAAiC,EAAE;IAEnC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,CAAC,CAAA;IACtC,IAAI,OAAO,GAAY,KAAK,CAAA;IAC5B,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,QAAQ,EAAE,KAAK,EAAE,EAAE,CAAC;QAC9C,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,SAAS;YAAE,OAAM;QACrD,MAAM,OAAO,CAAA;QACb,IAAI,OAAO,OAAO,KAAK,QAAQ;YAAE,OAAM;QACvC,OAAO,GAAI,OAA+B,CAAC,KAAK,CAAA;IAClD,CAAC;AACH,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@valve-tech/viem-errors",
|
|
3
|
+
"version": "0.3.1",
|
|
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
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://github.com/valve-tech/evm-toolkit/tree/main/packages/viem-errors#readme",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/valve-tech/evm-toolkit.git",
|
|
10
|
+
"directory": "packages/viem-errors"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/valve-tech/evm-toolkit/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"ethereum",
|
|
17
|
+
"evm",
|
|
18
|
+
"viem",
|
|
19
|
+
"wagmi",
|
|
20
|
+
"errors",
|
|
21
|
+
"user-rejection",
|
|
22
|
+
"eip-1193",
|
|
23
|
+
"custom-error",
|
|
24
|
+
"wallet"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"main": "dist/index.js",
|
|
28
|
+
"types": "dist/index.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"import": "./dist/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md",
|
|
38
|
+
"CHANGELOG.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc -p .",
|
|
43
|
+
"typecheck": "tsc -p . --noEmit",
|
|
44
|
+
"lint": "eslint src",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"test:coverage": "vitest run --coverage",
|
|
47
|
+
"prepare": "yarn build"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"viem": "^2.0.0"
|
|
51
|
+
}
|
|
52
|
+
}
|