-
Notifications
You must be signed in to change notification settings - Fork 22
/
error.ts
288 lines (273 loc) · 11 KB
/
error.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
import * as Sentry from '@sentry/nextjs'
import {
INVALID_CONTRACT_ERROR_SUBSTRINGS,
NONEXISTENT_QUERY_ERROR_SUBSTRINGS,
} from './constants'
/**
* Whether or not an error contains a substring or any of a set of substrings.
*/
export const isErrorWithSubstring = (
error: unknown,
substringOrSubstrings: string | string[]
): boolean =>
error instanceof Error &&
[substringOrSubstrings]
.flat()
.some((substring) => (error as Error).message.includes(substring))
/**
* Whether or not an error is a non-existent query error.
*/
export const isNonexistentQueryError = (error: unknown): boolean =>
isErrorWithSubstring(error, NONEXISTENT_QUERY_ERROR_SUBSTRINGS)
/**
* Whether or not an error is an invalid contract error.
*/
export const isInvalidContractError = (error: unknown): boolean =>
isErrorWithSubstring(error, INVALID_CONTRACT_ERROR_SUBSTRINGS)
// Passing a map will allow common errors to be mapped to a custom error message
// for the given context.
export const processError = (
error: Error | any,
{
tags,
extra,
transform,
overrideCapture,
forceCapture,
}: {
tags?: Record<string, string | number | boolean | null | undefined>
extra?: Record<string, unknown>
transform?: Partial<Record<CommonError, string>>
overrideCapture?: Partial<Record<CommonError, boolean>>
/**
* If set to true, will sent error to Sentry. If set to false, will not send
* error to Sentry. If undefined, will use default behavior (reference the
* capture map).
*/
forceCapture?: boolean
} = {}
): string => {
// Convert to error type.
if (!(error instanceof Error)) {
error = new Error(
`${
// Some errors are not Error classes but have a message property.
typeof error === 'object' && 'message' in error ? error.message : error
}`
)
}
const { message } = error as Error
let recognizedError
// Attempt to recognize error.
for (const [commonError, patterns] of commonErrorPatternsEntries) {
// Match if any elements are matches.
const match = patterns.some((pattern) =>
Array.isArray(pattern)
? // If array of strings, every element must match.
pattern.every((p) => message.includes(p))
: message.includes(pattern)
)
// If recognized error, break.
if (match) {
recognizedError = commonError
break
}
}
// If recognized error, try to find it in the map, or else return the
// recognized error.
if (recognizedError) {
// Send to Sentry if we want to capture this recognized error.
if (
forceCapture !== false &&
((forceCapture === true ||
(overrideCapture && overrideCapture[recognizedError])) ??
captureCommonErrorMap[recognizedError])
) {
Sentry.captureException(error, { extra, tags })
}
return ((transform && transform[recognizedError]) ||
recognizedError) as string
}
// If we did not recognize the error and it's a Cosmos SDK error with a
// stacktrace, extract the error from the last line (since the first n-1 lines
// are golang stacktrace). This is a common string displayed in Cosmos SDK
// stacktraces.
if (
message.includes('github.com/cosmos/cosmos-sdk/baseapp.gRPCErrorToSDKError')
) {
error = new Error(message.split('\n').slice(-1)[0])
}
if (forceCapture !== false) {
// Send to Sentry since we were not expecting it.
Sentry.captureException(error, { extra, tags })
}
return error.message
}
// To add a new error:
// 1. Add a value to this enum.
// 2. Add matching parameters in commonErrorPatterns below.
// 3. If it should be sent to Sentry, add an entry to captureCommonErrorMap.
export enum CommonError {
RequestRejected = 'Wallet rejected transaction.',
InvalidAddress = 'Invalid address.',
InsufficientFees = "Insufficient fees. Reconnect your wallet, ensure you're on the right chain, and try again.",
InsufficientFunds = 'Insufficient funds.',
GetClientFailed = 'Failed to get client. Try refreshing the page or reconnecting your wallet.',
Network = 'Network error. Ensure you are connected to the internet, refresh the page, or try again later. If your network is working, the blockchain nodes may be having problems.',
Unauthorized = 'Unauthorized.',
InsufficientForProposalDeposit = 'Insufficient unstaked deposit tokens. Ensure you have enough unstaked deposit tokens to pay for the proposal deposit.',
PendingTransaction = 'You have another pending transaction. Please try again in 10 seconds.',
TextEncodingDecodingError = 'Text encoding/decoding error. Invalid character present in text.',
TxnSentTimeout = 'Transaction sent but has not yet been detected. Refresh this page to view its changes or check back later.',
InvalidJSONResponse = 'Invalid JSON response from server.',
BlockHeightTooLow = 'Block height is too low.',
TxPageOutOfRange = 'Transaction page is out of range.',
AuthorizationNotFound = 'Authorization does not exist.',
SignatureVerificationFailed = 'Signature verification failed. Try again in 10 seconds or reach out to us on Discord for help.',
IbcClientExpired = 'IBC client expired. Reach out to us on Discord for help.',
NoIndexerForChain = 'No indexer for chain.',
DaoInactive = 'This DAO is inactive, which means insufficient voting power has been staked. You cannot create a proposal at this time.',
ReconnectWallet = 'Please disconnect and reconnect your wallet.',
ProposalTooLarge = 'Proposal is too large. Please remove actions or shorten the description.',
NoSuchContract = 'Contract not found.',
ContractInstantiationFailed = 'Contract instantiation failed.',
ContractExecutionFailed = 'Contract execution failed.',
ContractMigrationFailed = 'Contract migration failed.',
InsufficientGas = 'Insufficient gas.',
ContractNameExists = 'Name already taken.',
OutOfGas = 'Out of gas.',
InvalidCoins = 'Invalid coins.',
}
// List of error substrings to match to determine the common error. Elements in
// value are OR'd together. Inner string arrays are AND'd together. For example:
// ["abc", "def"] matches "abc" or "def" or "abc def". ["abc", ["def", "ghi"]]
// matches "abc def ghi" or "def ghi" but NOT "abc def" or "abc ghi".
const commonErrorPatterns: Record<CommonError, (string | string[])[]> = {
[CommonError.RequestRejected]: ['Request rejected', 'Ledger init aborted'],
[CommonError.InvalidAddress]: [
'decoding bech32 failed: invalid checksum',
'contract: not found',
// Provided non-DAO address where a DAO address was expected.
'unknown variant `get_config`',
],
[CommonError.InsufficientFees]: [
'insufficient fees',
// sdk code format from polytone listener callback error
// https://github.com/cosmos/cosmos-sdk/blob/main/types/errors/errors.go
'codespace: sdk, code: 13',
],
[CommonError.InsufficientFunds]: [
'insufficient funds',
['fee payer address', 'does not exist'],
// sdk code format from polytone listener callback error
// https://github.com/cosmos/cosmos-sdk/blob/main/types/errors/errors.go
'codespace: sdk, code: 5',
],
[CommonError.GetClientFailed]: [
'Bad status on response: 403',
'Failed to retrieve account from signer',
],
[CommonError.Network]: [
'Failed to fetch',
'socket disconnected',
'socket hang up',
'Bad status on response: 5',
'ECONNREFUSED',
'ETIMEDOUT',
'tx already exists in cache',
'Load failed',
'fetch failed',
],
[CommonError.Unauthorized]: [
'Unauthorized',
// sdk code format from polytone listener callback error
// https://github.com/cosmos/cosmos-sdk/blob/main/types/errors/errors.go
'codespace: sdk, code: 4',
],
[CommonError.InsufficientForProposalDeposit]: ['Overflow: Cannot Sub with'],
[CommonError.PendingTransaction]: ['account sequence mismatch'],
[CommonError.TextEncodingDecodingError]: ['out of printable ASCII range'],
[CommonError.TxnSentTimeout]: [
'was submitted but was not yet found on the chain',
],
[CommonError.InvalidJSONResponse]: [
'invalid json response body',
'Unexpected token < in JSON',
],
[CommonError.BlockHeightTooLow]: [
['32603', 'not available', 'lowest height is'],
],
[CommonError.TxPageOutOfRange]: [
['32603', 'page should be within', 'range', 'given'],
],
[CommonError.AuthorizationNotFound]: ['authorization not found'],
[CommonError.SignatureVerificationFailed]: [
[
'Broadcasting transaction failed',
'signature verification failed; please verify account number',
'unauthorized',
],
],
[CommonError.IbcClientExpired]: [
[
'failed to send packet: cannot send packet using client',
'Expired: client is not active',
],
],
[CommonError.NoIndexerForChain]: ['No indexer for chain'],
[CommonError.DaoInactive]: [
'the DAO is currently inactive, you cannot create proposals',
],
[CommonError.ReconnectWallet]: [['Session', 'not established yet']],
[CommonError.ProposalTooLarge]: [['proposal is', 'bytes, must be <=']],
[CommonError.NoSuchContract]: [
'no such contract',
// wasm code format from polytone listener callback error
// https://github.com/CosmWasm/wasmd/blob/main/x/wasm/types/errors.go
'codespace: wasm, code: 22',
],
[CommonError.ContractInstantiationFailed]: [
// wasm code format from polytone listener callback error
// https://github.com/CosmWasm/wasmd/blob/main/x/wasm/types/errors.go
'codespace: wasm, code: 4',
],
[CommonError.ContractExecutionFailed]: [
// wasm code format from polytone listener callback error
// https://github.com/CosmWasm/wasmd/blob/main/x/wasm/types/errors.go
'codespace: wasm, code: 5',
],
[CommonError.ContractMigrationFailed]: [
// wasm code format from polytone listener callback error
// https://github.com/CosmWasm/wasmd/blob/main/x/wasm/types/errors.go
'codespace: wasm, code: 11',
],
[CommonError.InsufficientGas]: [
// wasm code format from polytone listener callback error
// https://github.com/CosmWasm/wasmd/blob/main/x/wasm/types/errors.go
'codespace: wasm, code: 6',
],
[CommonError.ContractNameExists]: ['contract account already exists'],
[CommonError.OutOfGas]: [
// sdk code format from polytone listener callback error
// https://github.com/cosmos/cosmos-sdk/blob/main/types/errors/errors.go
'codespace: sdk, code: 11',
],
[CommonError.InvalidCoins]: [
'invalid coins',
// sdk code format from polytone listener callback error
// https://github.com/cosmos/cosmos-sdk/blob/main/types/errors/errors.go
'codespace: sdk, code: 10',
],
}
const commonErrorPatternsEntries = Object.entries(commonErrorPatterns) as [
CommonError,
(string | string[])[]
][]
// Whether or not to send the error to Sentry. Some errors we want to clean up
// for the user but still investigate (e.g. InvalidJSONResponse), so let's send
// them to Sentry even if we recognize them.
const captureCommonErrorMap: Partial<Record<CommonError, boolean>> = {
[CommonError.InvalidJSONResponse]: true,
// This should be reported to us.
[CommonError.IbcClientExpired]: true,
}