@exodus/ethereum-api 10.3.4 → 10.4.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/CHANGELOG.md +9 -0
- package/package.json +2 -2
- package/src/error-wrapper.js +9 -2
- package/src/errors.js +250 -0
- package/src/exodus-eth-server/api-coin-nodes.js +4 -0
- package/src/exodus-eth-server/clarity.js +4 -0
- package/src/exodus-eth-server/fetch-json.js +17 -3
- package/src/index.js +2 -0
- package/src/staking/ethereum/everstake.js +2 -2
- package/src/tx-create.js +10 -1
- package/src/tx-send/broadcast-error-handler.js +23 -4
- package/src/tx-send/tx-send.js +25 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [10.4.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@10.3.4...@exodus/ethereum-api@10.4.0) (2026-11-14)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **ethereum-api:** add EXOD send error codes and user hints ([#9205](https://github.com/ExodusMovement/assets/issues/9205)) ([ebdbd9e](https://github.com/ExodusMovement/assets/commit/ebdbd9e778107182c3e0b7d0fd12866f30d384e2))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
6
15
|
## [10.3.4](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@10.3.3...@exodus/ethereum-api@10.3.4) (2026-11-06)
|
|
7
16
|
|
|
8
17
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.4.0",
|
|
4
4
|
"description": "Transaction monitors, fee monitors, RPC with the blockchain node, and other networking code for Ethereum and EVM-based blockchains",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -65,5 +65,5 @@
|
|
|
65
65
|
"type": "git",
|
|
66
66
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
67
67
|
},
|
|
68
|
-
"gitHead": "
|
|
68
|
+
"gitHead": "e646d8655bc613c1f188e7d7187498d82700ae90"
|
|
69
69
|
}
|
package/src/error-wrapper.js
CHANGED
|
@@ -58,6 +58,10 @@ export const EVM_ERROR_REASONS = {
|
|
|
58
58
|
reason: safeString`broadcast tx failed`,
|
|
59
59
|
type: EVM_ERROR_TYPES.BROADCAST,
|
|
60
60
|
},
|
|
61
|
+
broadcastNodeUnavailable: {
|
|
62
|
+
reason: safeString`broadcast node unavailable`,
|
|
63
|
+
type: EVM_ERROR_TYPES.BROADCAST,
|
|
64
|
+
},
|
|
61
65
|
|
|
62
66
|
// Reversion reasons
|
|
63
67
|
executionReverted: {
|
|
@@ -303,9 +307,10 @@ export class EthLikeError extends Error {
|
|
|
303
307
|
* @param {string} hint - A hint to help the user understand the error. Must not contain any sensitive information such as private keys, transaction IDs, or wallet addresses.
|
|
304
308
|
* @param {TraceId} [traceId] - Optional TraceId object from HTTP response 'traceparent' header for debugging.
|
|
305
309
|
* @param {string} [baseAssetName] - Optional base asset name (e.g. 'ethereum', 'bnbsmartchain') for identifying the network.
|
|
310
|
+
* @param {Error} [cause] - Optional original error, kept so its name and code stay readable.
|
|
306
311
|
*/
|
|
307
|
-
constructor({ message, errorReasonInfo, hint, traceId, baseAssetName }) {
|
|
308
|
-
super(message)
|
|
312
|
+
constructor({ message, errorReasonInfo, hint, traceId, baseAssetName, cause }) {
|
|
313
|
+
super(message, cause === undefined ? undefined : { cause })
|
|
309
314
|
this.name = safeString`EthLikeError`
|
|
310
315
|
const filteredHint = this.#extractHint(hint)
|
|
311
316
|
// Only filtered hints are kept on the stack so that subsequent joins via
|
|
@@ -335,6 +340,7 @@ export class EthLikeError extends Error {
|
|
|
335
340
|
type: this.type,
|
|
336
341
|
traceId: this.traceId,
|
|
337
342
|
baseAssetName: this.baseAssetName,
|
|
343
|
+
sourceHint: this.sourceHint,
|
|
338
344
|
}
|
|
339
345
|
}
|
|
340
346
|
|
|
@@ -386,6 +392,7 @@ export const withErrorReason = async ({ promise, errorReasonInfo, hint, baseAsse
|
|
|
386
392
|
hint,
|
|
387
393
|
traceId: err.traceId,
|
|
388
394
|
baseAssetName,
|
|
395
|
+
cause: err,
|
|
389
396
|
})
|
|
390
397
|
}
|
|
391
398
|
}
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { safeString } from '@exodus/safe-string'
|
|
2
|
+
|
|
3
|
+
import { EVM_ERROR_REASONS, EVM_ERROR_TYPES, getEvmErrorReason } from './error-wrapper.js'
|
|
4
|
+
|
|
5
|
+
const freezeTable = (table) => {
|
|
6
|
+
for (const entry of Object.values(table)) {
|
|
7
|
+
for (const value of Object.values(entry)) {
|
|
8
|
+
if (Array.isArray(value)) Object.freeze(value)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
Object.freeze(entry)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return Object.freeze(table)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const EVM_ERRORS = freezeTable({
|
|
18
|
+
INSUFFICIENT_FUNDS_FOR_FEE: {
|
|
19
|
+
code: 'EXOD-3000',
|
|
20
|
+
hint: safeString`Not enough funds to pay the network fee`,
|
|
21
|
+
action: 'add-funds',
|
|
22
|
+
reasons: [EVM_ERROR_REASONS.insufficientFundsForGas.reason],
|
|
23
|
+
},
|
|
24
|
+
INSUFFICIENT_BALANCE: {
|
|
25
|
+
code: 'EXOD-3001',
|
|
26
|
+
hint: safeString`Not enough balance to send this amount`,
|
|
27
|
+
action: 'add-funds',
|
|
28
|
+
reasons: [EVM_ERROR_REASONS.insufficientBalance.reason],
|
|
29
|
+
},
|
|
30
|
+
FEE_EXCEEDS_AMOUNT: {
|
|
31
|
+
code: 'EXOD-3002',
|
|
32
|
+
hint: safeString`The network fee is higher than the amount to send`,
|
|
33
|
+
action: 'edit-transaction',
|
|
34
|
+
names: ['AmountIncludesFeeUnderflowError'],
|
|
35
|
+
},
|
|
36
|
+
FEE_TOO_LOW: {
|
|
37
|
+
code: 'EXOD-3003',
|
|
38
|
+
hint: safeString`The network fee is too low, try again with a higher fee`,
|
|
39
|
+
action: 'edit-transaction',
|
|
40
|
+
reasons: [
|
|
41
|
+
EVM_ERROR_REASONS.gasPriceBelowMin.reason,
|
|
42
|
+
EVM_ERROR_REASONS.transactionUnderpriced.reason,
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
REPLACEMENT_FEE_TOO_LOW: {
|
|
46
|
+
code: 'EXOD-3004',
|
|
47
|
+
hint: safeString`The fee is too low to replace the pending transaction`,
|
|
48
|
+
action: 'edit-transaction',
|
|
49
|
+
reasons: [EVM_ERROR_REASONS.replacementTransactionUnderpriced.reason],
|
|
50
|
+
},
|
|
51
|
+
GAS_LIMIT_TOO_LOW: {
|
|
52
|
+
code: 'EXOD-3005',
|
|
53
|
+
hint: safeString`The gas limit is too low for this transaction`,
|
|
54
|
+
action: 'edit-transaction',
|
|
55
|
+
reasons: [EVM_ERROR_REASONS.insufficientGas.reason],
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
INVALID_RECIPIENT: {
|
|
59
|
+
code: 'EXOD-3020',
|
|
60
|
+
hint: safeString`This recipient address is not allowed`,
|
|
61
|
+
action: 'edit-transaction',
|
|
62
|
+
patterns: [/^The receiving wallet address must not be\b/],
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
NONCE_TOO_LOW: {
|
|
66
|
+
code: 'EXOD-3040',
|
|
67
|
+
hint: safeString`This transaction conflicts with an earlier one, please try again`,
|
|
68
|
+
action: 'refresh-and-retry',
|
|
69
|
+
reasons: [EVM_ERROR_REASONS.nonceTooLow.reason],
|
|
70
|
+
},
|
|
71
|
+
NONCE_TOO_HIGH: {
|
|
72
|
+
code: 'EXOD-3041',
|
|
73
|
+
hint: safeString`An earlier transaction is still pending, try again once it confirms`,
|
|
74
|
+
action: 'retry-later',
|
|
75
|
+
reasons: [EVM_ERROR_REASONS.nonceTooHigh.reason],
|
|
76
|
+
},
|
|
77
|
+
NONCE_GAP: {
|
|
78
|
+
code: 'EXOD-3042',
|
|
79
|
+
hint: safeString`An earlier transaction was dropped, send a new transaction first`,
|
|
80
|
+
action: 'refresh-and-retry',
|
|
81
|
+
patterns: [/ERR_BUMP_TX_NONCE_GAP/],
|
|
82
|
+
reasons: [EVM_ERROR_REASONS.futureTransactionReplace.reason],
|
|
83
|
+
},
|
|
84
|
+
BUMP_TX_UNAVAILABLE: {
|
|
85
|
+
code: 'EXOD-3043',
|
|
86
|
+
hint: safeString`This transaction can no longer be sped up`,
|
|
87
|
+
action: 'edit-transaction',
|
|
88
|
+
patterns: [/ERR_BUMP_TX_DROPPED|^Cannot bump transaction\b/],
|
|
89
|
+
},
|
|
90
|
+
INVALID_SIGNATURE: {
|
|
91
|
+
code: 'EXOD-3044',
|
|
92
|
+
hint: safeString`The network rejected the transaction signature`,
|
|
93
|
+
action: 'contact-support',
|
|
94
|
+
reasons: [EVM_ERROR_REASONS.invalidRlpSerialization.reason],
|
|
95
|
+
},
|
|
96
|
+
REPLAY_PROTECTION_REQUIRED: {
|
|
97
|
+
code: 'EXOD-3045',
|
|
98
|
+
hint: safeString`This network requires replay-protected transactions`,
|
|
99
|
+
action: 'contact-support',
|
|
100
|
+
reasons: [EVM_ERROR_REASONS.eip155Required.reason],
|
|
101
|
+
},
|
|
102
|
+
TX_TYPE_NOT_SUPPORTED: {
|
|
103
|
+
code: 'EXOD-3046',
|
|
104
|
+
hint: safeString`This network does not support this transaction type`,
|
|
105
|
+
action: 'contact-support',
|
|
106
|
+
reasons: [EVM_ERROR_REASONS.transactionTypeNotSupported.reason],
|
|
107
|
+
},
|
|
108
|
+
CONTRACT_REVERTED: {
|
|
109
|
+
code: 'EXOD-3047',
|
|
110
|
+
hint: safeString`The contract rejected this transaction`,
|
|
111
|
+
action: 'edit-transaction',
|
|
112
|
+
reasons: [
|
|
113
|
+
EVM_ERROR_REASONS.executionReverted.reason,
|
|
114
|
+
EVM_ERROR_REASONS.invalidOpcode.reason,
|
|
115
|
+
EVM_ERROR_REASONS.invalidJumpDestination.reason,
|
|
116
|
+
EVM_ERROR_REASONS.mathError.reason,
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
ZERO_AMOUNT: {
|
|
120
|
+
code: 'EXOD-3048',
|
|
121
|
+
hint: safeString`The amount must be greater than zero`,
|
|
122
|
+
action: 'edit-transaction',
|
|
123
|
+
reasons: [EVM_ERROR_REASONS.zeroTransfer.reason],
|
|
124
|
+
},
|
|
125
|
+
GAS_ESTIMATION_FAILED: {
|
|
126
|
+
code: 'EXOD-3049',
|
|
127
|
+
hint: safeString`Could not estimate the network fee for this transaction`,
|
|
128
|
+
action: 'retry',
|
|
129
|
+
reasons: [EVM_ERROR_REASONS.fetchGasLimitFailed.reason],
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
RATE_LIMITED: {
|
|
133
|
+
code: 'EXOD-3061',
|
|
134
|
+
hint: safeString`The network is busy, try again later`,
|
|
135
|
+
action: 'retry-later',
|
|
136
|
+
patterns: [/\breturned 429\b|too many requests|rate.?limit/i],
|
|
137
|
+
},
|
|
138
|
+
NODE_REQUEST_FAILED: {
|
|
139
|
+
code: 'EXOD-3062',
|
|
140
|
+
hint: safeString`The network node returned an error, try again later`,
|
|
141
|
+
action: 'retry',
|
|
142
|
+
reasons: [EVM_ERROR_REASONS.broadcastNodeUnavailable.reason],
|
|
143
|
+
types: [EVM_ERROR_TYPES.NODE_STATE_READ, EVM_ERROR_TYPES.CONTRACT_CALL],
|
|
144
|
+
patterns: [/\breturned 50[03]\b/],
|
|
145
|
+
prefixes: ['Bad rpc response'],
|
|
146
|
+
},
|
|
147
|
+
NETWORK_UNREACHABLE: {
|
|
148
|
+
code: 'EXOD-3060',
|
|
149
|
+
hint: safeString`Could not reach the network, check your connection and try again`,
|
|
150
|
+
action: 'retry',
|
|
151
|
+
names: ['AbortError', 'TimeoutError'],
|
|
152
|
+
patterns: [
|
|
153
|
+
/failed to fetch|fetch failed|network ?request failed|networkerror|socket hang up|\be(?:conn(?:refused|reset|aborted)|timedout|notfound|ai_again|hostunreach|netunreach)\b|\btimeout\b|rpc_request_timeout|timed out|\breturned 50[24]\b/i,
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
BROADCAST_OUTCOME_UNKNOWN: {
|
|
157
|
+
code: 'EXOD-3063',
|
|
158
|
+
hint: safeString`The transaction may not have been sent, check your history before retrying`,
|
|
159
|
+
action: 'check-history',
|
|
160
|
+
reasons: [EVM_ERROR_REASONS.broadcastTxFailed.reason],
|
|
161
|
+
},
|
|
162
|
+
TX_RECORD_FAILED: {
|
|
163
|
+
code: 'EXOD-3064',
|
|
164
|
+
hint: safeString`The transaction was sent but could not be saved, check your history before sending again`,
|
|
165
|
+
action: 'check-history',
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
DELEGATED_ACCOUNT: {
|
|
169
|
+
code: 'EXOD-3080',
|
|
170
|
+
hint: safeString`This account's delegation does not allow this transaction`,
|
|
171
|
+
action: 'contact-support',
|
|
172
|
+
reasons: [EVM_ERROR_REASONS.delegatedAccountError.reason],
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
UNKNOWN_SEND_FAILURE: {
|
|
176
|
+
code: 'EXOD-3099',
|
|
177
|
+
hint: safeString`The transaction failed, please try again`,
|
|
178
|
+
action: 'refresh-and-retry',
|
|
179
|
+
},
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
const ENTRIES = Object.entries(EVM_ERRORS)
|
|
183
|
+
|
|
184
|
+
const EVM_CODES = new Set(ENTRIES.map(([, { code }]) => code))
|
|
185
|
+
|
|
186
|
+
const findKey = (field, matches) => ENTRIES.find(([, entry]) => entry[field]?.some(matches))?.[0]
|
|
187
|
+
|
|
188
|
+
const REASON_TO_ERROR_KEY = new Map(
|
|
189
|
+
ENTRIES.flatMap(([key, { reasons = [] }]) => reasons.map((reason) => [reason, key]))
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
const MAX_MESSAGE_LENGTH = 1000
|
|
193
|
+
|
|
194
|
+
const getErrorKey = (error) => {
|
|
195
|
+
const nameKey = findKey('names', (name) => name === error.name || name === error.cause?.name)
|
|
196
|
+
if (nameKey) return nameKey
|
|
197
|
+
if (error.reason === EVM_ERROR_REASONS.broadcastTxFailed.reason) {
|
|
198
|
+
return 'BROADCAST_OUTCOME_UNKNOWN'
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const message =
|
|
202
|
+
typeof error.message === 'string' ? error.message.slice(0, MAX_MESSAGE_LENGTH) : ''
|
|
203
|
+
|
|
204
|
+
const messageReasonKey = REASON_TO_ERROR_KEY.get(getEvmErrorReason(message)?.reason)
|
|
205
|
+
if (messageReasonKey) return messageReasonKey
|
|
206
|
+
|
|
207
|
+
const patternKey = findKey('patterns', (pattern) => pattern.test(message))
|
|
208
|
+
if (patternKey) return patternKey
|
|
209
|
+
|
|
210
|
+
const reasonKey = REASON_TO_ERROR_KEY.get(error.reason)
|
|
211
|
+
if (reasonKey) return reasonKey
|
|
212
|
+
|
|
213
|
+
const nodeKey =
|
|
214
|
+
findKey('types', (type) => type === error.type) ??
|
|
215
|
+
findKey('prefixes', (prefix) => message.startsWith(prefix))
|
|
216
|
+
if (nodeKey) return nodeKey
|
|
217
|
+
|
|
218
|
+
return 'UNKNOWN_SEND_FAILURE'
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const defineField = (error, field, value) =>
|
|
222
|
+
Object.defineProperty(error, field, {
|
|
223
|
+
value,
|
|
224
|
+
writable: true,
|
|
225
|
+
enumerable: true,
|
|
226
|
+
configurable: true,
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
const setErrorCode = (error, key) => {
|
|
230
|
+
const { code, hint, action } = EVM_ERRORS[key]
|
|
231
|
+
const sourceCode = error.code ?? error.cause?.code
|
|
232
|
+
if (sourceCode !== undefined) defineField(error, 'sourceCode', sourceCode)
|
|
233
|
+
if (error.hint !== undefined) defineField(error, 'sourceHint', error.hint)
|
|
234
|
+
defineField(error, 'code', code)
|
|
235
|
+
defineField(error, 'hint', hint)
|
|
236
|
+
return defineField(error, 'recommendedAction', action)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export const withTxRecordErrorCode = (error) => {
|
|
240
|
+
if (!error || typeof error !== 'object') return error
|
|
241
|
+
|
|
242
|
+
return setErrorCode(error, 'TX_RECORD_FAILED')
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export const withSendErrorCode = (error) => {
|
|
246
|
+
if (!error || typeof error !== 'object') return error
|
|
247
|
+
if (EVM_CODES.has(error.code)) return error
|
|
248
|
+
|
|
249
|
+
return setErrorCode(error, getErrorKey(error))
|
|
250
|
+
}
|
|
@@ -41,8 +41,22 @@ export const fetchJson = async (url, fetchOptions) => {
|
|
|
41
41
|
return json
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
export const fetchJsonRetry = (url, fetchOptions) => {
|
|
44
|
+
export const fetchJsonRetry = async (url, fetchOptions) => {
|
|
45
45
|
const waitTimes = ['3s']
|
|
46
|
-
const
|
|
47
|
-
|
|
46
|
+
const errorChain = []
|
|
47
|
+
const fetchAttempt = async () => {
|
|
48
|
+
try {
|
|
49
|
+
return await fetchJson(url, fetchOptions)
|
|
50
|
+
} catch (err) {
|
|
51
|
+
errorChain.push(err)
|
|
52
|
+
throw err
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const json = await retry(fetchAttempt, { delayTimesMs: waitTimes })()
|
|
57
|
+
if (errorChain.length > 0 && json?.error) {
|
|
58
|
+
json.__errorChain = errorChain
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return json
|
|
48
62
|
}
|
package/src/index.js
CHANGED
|
@@ -30,7 +30,7 @@ const fetch = async (path, config = Object.create(null)) => {
|
|
|
30
30
|
const isFiniteInteger = (e) => Number.isInteger(e) && Number.isFinite(e)
|
|
31
31
|
|
|
32
32
|
// Simulate an unstake to predict how much ETH the interchange pool returns instantly.
|
|
33
|
-
// https://wallet-sdk-api.everstake.
|
|
33
|
+
// https://wallet-sdk-api.everstake.com/swagger/#/Ethereum/post_ethereum_pool_simulate_unstake
|
|
34
34
|
// Routed through the Exodus Everstake proxy rather than calling Everstake directly from the client.
|
|
35
35
|
//
|
|
36
36
|
// Inputs:
|
|
@@ -87,7 +87,7 @@ export const getCachedEverstakeValidatorsQueue = createCachedStakingRead({
|
|
|
87
87
|
})
|
|
88
88
|
|
|
89
89
|
export const getEverstakeValidatorsQueue = async () => {
|
|
90
|
-
// https://swagger.eth-api-b2c.everstake.
|
|
90
|
+
// https://swagger.eth-api-b2c.everstake.com/#/Staking/validatorsQueue
|
|
91
91
|
const result = await fetch('v1/validators/queue')
|
|
92
92
|
|
|
93
93
|
const {
|
package/src/tx-create.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import createEthereumJsTx from '@exodus/ethereum-lib/src/unsigned-tx/create-ethereumjs-tx.js'
|
|
7
7
|
import assert from 'minimalistic-assert'
|
|
8
8
|
|
|
9
|
+
import { withSendErrorCode } from './errors.js'
|
|
9
10
|
import { authenticateReplacement } from './transaction-authentication.js'
|
|
10
11
|
import { ensureSaneEip1559GasPriceForTipGasPrice } from './fee-utils.js'
|
|
11
12
|
import { fetchGasLimit } from './gas-estimation.js'
|
|
@@ -432,7 +433,7 @@ const createBumpUnsignedTx = async ({
|
|
|
432
433
|
export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonce }) => {
|
|
433
434
|
assert(assetClientInterface, 'assetClientInterface is required')
|
|
434
435
|
assert(typeof chainId === 'number', 'chainId is required')
|
|
435
|
-
|
|
436
|
+
const createTx = async ({
|
|
436
437
|
asset,
|
|
437
438
|
walletAccount,
|
|
438
439
|
nft, // when sending nfts
|
|
@@ -652,4 +653,12 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
|
|
|
652
653
|
eip1559Enabled,
|
|
653
654
|
})
|
|
654
655
|
}
|
|
656
|
+
|
|
657
|
+
return async (params) => {
|
|
658
|
+
try {
|
|
659
|
+
return await createTx(params)
|
|
660
|
+
} catch (err) {
|
|
661
|
+
throw withSendErrorCode(err)
|
|
662
|
+
}
|
|
663
|
+
}
|
|
655
664
|
}
|
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
import { EthLikeError, EVM_ERROR_REASONS, getEvmErrorReason } from '../error-wrapper.js'
|
|
2
2
|
import { transactionExists } from '../eth-like-util.js'
|
|
3
3
|
|
|
4
|
+
const NODE_UNAVAILABLE_MESSAGE = /\breturned (?:429|50[03])\b/
|
|
5
|
+
|
|
6
|
+
const isNodeUnavailable = (attempt) => NODE_UNAVAILABLE_MESSAGE.test(attempt?.message ?? '')
|
|
7
|
+
|
|
8
|
+
const isOutcomeUnknownAttempt = (attempt) =>
|
|
9
|
+
!isNodeUnavailable(attempt) && !getEvmErrorReason(attempt?.message ?? '')
|
|
10
|
+
|
|
11
|
+
const getReplyReason = (err) =>
|
|
12
|
+
getEvmErrorReason(err.message) ??
|
|
13
|
+
(isNodeUnavailable(err)
|
|
14
|
+
? EVM_ERROR_REASONS.broadcastNodeUnavailable
|
|
15
|
+
: EVM_ERROR_REASONS.broadcastTxFailed)
|
|
16
|
+
|
|
17
|
+
const getAttemptReason = (err, replyInfo) => {
|
|
18
|
+
if (replyInfo.reason === EVM_ERROR_REASONS.alreadyKnown.reason) return replyInfo
|
|
19
|
+
if (!err.errorChain?.some(isOutcomeUnknownAttempt)) return replyInfo
|
|
20
|
+
return EVM_ERROR_REASONS.broadcastTxFailed
|
|
21
|
+
}
|
|
22
|
+
|
|
4
23
|
/**
|
|
5
24
|
* Handles broadcast errors by parsing the error message and either throwing
|
|
6
25
|
* an appropriate EthLikeError or returning info for retry logic.
|
|
@@ -20,13 +39,13 @@ export const handleBroadcastError = async (
|
|
|
20
39
|
err,
|
|
21
40
|
{ asset, txId, isHardware, hint, isBumpTx, isRetryNonceTx }
|
|
22
41
|
) => {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
const errorInfo = getEvmErrorReason(message) || EVM_ERROR_REASONS.broadcastTxFailed
|
|
42
|
+
const replyInfo = getReplyReason(err)
|
|
43
|
+
const errorInfo = getAttemptReason(err, replyInfo)
|
|
26
44
|
|
|
27
45
|
const isNonceTooLow = errorInfo.reason === EVM_ERROR_REASONS.nonceTooLow.reason
|
|
28
46
|
const isAmbiguousError =
|
|
29
|
-
|
|
47
|
+
replyInfo.reason === EVM_ERROR_REASONS.nonceTooLow.reason ||
|
|
48
|
+
replyInfo.reason === EVM_ERROR_REASONS.transactionUnderpriced.reason
|
|
30
49
|
|
|
31
50
|
if (errorInfo.reason === EVM_ERROR_REASONS.alreadyKnown.reason) {
|
|
32
51
|
console.debug('tx already broadcast')
|
package/src/tx-send/tx-send.js
CHANGED
|
@@ -5,6 +5,7 @@ import assert from 'minimalistic-assert'
|
|
|
5
5
|
import ms from 'ms'
|
|
6
6
|
|
|
7
7
|
import { EthLikeError, EVM_ERROR_REASONS } from '../error-wrapper.js'
|
|
8
|
+
import { withSendErrorCode, withTxRecordErrorCode } from '../errors.js'
|
|
8
9
|
import { getOptimisticTxLogEffects } from '../tx-log/index.js'
|
|
9
10
|
import { signTx } from '../tx-sign/index.js'
|
|
10
11
|
import { ARBITRARY_ADDRESS } from '../tx-type/index.js'
|
|
@@ -17,6 +18,10 @@ const isOutcomeUnknown = (error) =>
|
|
|
17
18
|
|
|
18
19
|
const isRejectedByNode = (error) => error instanceof EthLikeError && !isOutcomeUnknown(error)
|
|
19
20
|
|
|
21
|
+
const isNodeUnavailable = (error) =>
|
|
22
|
+
error instanceof EthLikeError &&
|
|
23
|
+
error.reason === EVM_ERROR_REASONS.broadcastNodeUnavailable.reason
|
|
24
|
+
|
|
20
25
|
const omitNonce = (optimisticTxLogEffects) =>
|
|
21
26
|
optimisticTxLogEffects.map(({ txs, ...optimisticTxLogEffect }) => ({
|
|
22
27
|
...optimisticTxLogEffect,
|
|
@@ -60,7 +65,12 @@ const txSendFactory = ({ assetClientInterface, createTx }) => {
|
|
|
60
65
|
}
|
|
61
66
|
}
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
const sendTx = async ({
|
|
69
|
+
asset,
|
|
70
|
+
walletAccount,
|
|
71
|
+
unsignedTx: providedUnsignedTx,
|
|
72
|
+
...legacyParams
|
|
73
|
+
}) => {
|
|
64
74
|
const baseAsset = asset.baseAsset
|
|
65
75
|
|
|
66
76
|
const { unsignedTx } = providedUnsignedTx
|
|
@@ -118,7 +128,7 @@ const txSendFactory = ({ assetClientInterface, createTx }) => {
|
|
|
118
128
|
})
|
|
119
129
|
return true
|
|
120
130
|
} catch (error) {
|
|
121
|
-
if (!isOutcomeUnknown(error)) return false
|
|
131
|
+
if (!isOutcomeUnknown(error) && !isNodeUnavailable(error)) return false
|
|
122
132
|
}
|
|
123
133
|
}
|
|
124
134
|
}
|
|
@@ -224,10 +234,22 @@ const txSendFactory = ({ assetClientInterface, createTx }) => {
|
|
|
224
234
|
})
|
|
225
235
|
}
|
|
226
236
|
|
|
227
|
-
|
|
237
|
+
try {
|
|
238
|
+
await writeTxLog(optimisticTxLogEffects)
|
|
239
|
+
} catch (err) {
|
|
240
|
+
throw withTxRecordErrorCode(err)
|
|
241
|
+
}
|
|
228
242
|
|
|
229
243
|
return { txId, nonce }
|
|
230
244
|
}
|
|
245
|
+
|
|
246
|
+
return async (params) => {
|
|
247
|
+
try {
|
|
248
|
+
return await sendTx(params)
|
|
249
|
+
} catch (err) {
|
|
250
|
+
throw withSendErrorCode(err)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
231
253
|
}
|
|
232
254
|
|
|
233
255
|
export default txSendFactory
|