@exodus/ethereum-api 8.64.6 → 8.64.7
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 +10 -0
- package/package.json +5 -4
- package/src/ens/index.js +1 -0
- package/src/error-wrapper.js +298 -16
- package/src/eth-like-util.js +74 -31
- package/src/exodus-eth-server/api-coin-nodes.js +2 -1
- package/src/exodus-eth-server/clarity-v2.js +25 -7
- package/src/exodus-eth-server/clarity.js +49 -2
- package/src/exodus-eth-server/index.js +1 -1
- package/src/index.js +7 -1
- package/src/nft-utils.js +6 -4
- package/src/optimism-gas/index.js +8 -1
- package/src/staking/ethereum/api.js +8 -1
- package/src/staking/matic/api.js +8 -1
- package/src/tx-send/broadcast-error-handler.js +58 -0
- package/src/tx-send/tx-send.js +22 -61
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
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
|
+
## [8.64.7](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.64.5...@exodus/ethereum-api@8.64.7) (2026-02-24)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* fix: harden EthlikeError and improve error handling in tx-send (#7308)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
6
16
|
## [8.64.6](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.64.5...@exodus/ethereum-api@8.64.6) (2026-02-23)
|
|
7
17
|
|
|
8
18
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "8.64.
|
|
3
|
+
"version": "8.64.7",
|
|
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",
|
|
@@ -35,9 +35,10 @@
|
|
|
35
35
|
"@exodus/ethereumjs": "^1.8.0",
|
|
36
36
|
"@exodus/fetch": "^1.3.0",
|
|
37
37
|
"@exodus/models": "^12.13.0",
|
|
38
|
-
"@exodus/safe-string": "^1.
|
|
38
|
+
"@exodus/safe-string": "^1.4.0",
|
|
39
39
|
"@exodus/simple-retry": "^0.0.6",
|
|
40
40
|
"@exodus/solidity-contract": "^1.3.0",
|
|
41
|
+
"@exodus/traceparent": "^3.0.1",
|
|
41
42
|
"@exodus/web3-ethereum-utils": "^4.6.0",
|
|
42
43
|
"bn.js": "^5.2.1",
|
|
43
44
|
"delay": "^4.0.1",
|
|
@@ -54,7 +55,7 @@
|
|
|
54
55
|
"devDependencies": {
|
|
55
56
|
"@exodus/assets-testing": "^1.0.0",
|
|
56
57
|
"@exodus/bsc-meta": "^2.5.1",
|
|
57
|
-
"@exodus/errors": "^3.
|
|
58
|
+
"@exodus/errors": "^3.8.0",
|
|
58
59
|
"@exodus/ethereumarbone-meta": "^2.1.2",
|
|
59
60
|
"@exodus/fantommainnet-meta": "^2.0.5",
|
|
60
61
|
"@exodus/matic-meta": "^2.2.7",
|
|
@@ -67,5 +68,5 @@
|
|
|
67
68
|
"type": "git",
|
|
68
69
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
69
70
|
},
|
|
70
|
-
"gitHead": "
|
|
71
|
+
"gitHead": "7d25477c075c7cef416ffe2c7e6e4c99e2928bdc"
|
|
71
72
|
}
|
package/src/ens/index.js
CHANGED
package/src/error-wrapper.js
CHANGED
|
@@ -1,13 +1,277 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import { safeString } from '@exodus/safe-string'
|
|
2
|
+
|
|
3
|
+
// Error types for categorizing EVM errors
|
|
4
|
+
export const EVM_ERROR_TYPES = {
|
|
5
|
+
NODE_STATE_READ: safeString`NODE_STATE_READ`, // RPC-level read operations
|
|
6
|
+
CONTRACT_CALL: safeString`CONTRACT_CALL`, // Smart contract calls (eth_call, estimateGas)
|
|
7
|
+
BROADCAST: safeString`BROADCAST`, // Transaction broadcast errors (includes reverts)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Operation-specific reasons
|
|
11
|
+
export const EVM_ERROR_REASONS = {
|
|
12
|
+
// RPC-level read operations
|
|
13
|
+
balanceFetchFailed: {
|
|
14
|
+
reason: safeString`balance fetch failed`,
|
|
15
|
+
type: EVM_ERROR_TYPES.NODE_STATE_READ,
|
|
16
|
+
},
|
|
17
|
+
nonceFetchFailed: {
|
|
18
|
+
reason: safeString`nonce fetch failed`,
|
|
19
|
+
type: EVM_ERROR_TYPES.NODE_STATE_READ,
|
|
20
|
+
},
|
|
21
|
+
getTransactionByHashFailed: {
|
|
22
|
+
reason: safeString`get transaction by hash failed`,
|
|
23
|
+
type: EVM_ERROR_TYPES.NODE_STATE_READ,
|
|
24
|
+
},
|
|
25
|
+
getCodeFailed: {
|
|
26
|
+
reason: safeString`get code failed`,
|
|
27
|
+
type: EVM_ERROR_TYPES.NODE_STATE_READ,
|
|
28
|
+
},
|
|
29
|
+
getGasPriceFailed: {
|
|
30
|
+
reason: safeString`get gas price failed`,
|
|
31
|
+
type: EVM_ERROR_TYPES.NODE_STATE_READ,
|
|
32
|
+
},
|
|
33
|
+
getBaseFeePerGasFailed: {
|
|
34
|
+
reason: safeString`get base fee per gas failed`,
|
|
35
|
+
type: EVM_ERROR_TYPES.NODE_STATE_READ,
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
// Generic smart contract call failure.
|
|
39
|
+
ethCallFailed: {
|
|
40
|
+
reason: safeString`eth call failed`,
|
|
41
|
+
type: EVM_ERROR_TYPES.CONTRACT_CALL,
|
|
42
|
+
},
|
|
43
|
+
ethCallErc20Failed: {
|
|
44
|
+
reason: safeString`eth call erc20 failed`,
|
|
45
|
+
type: EVM_ERROR_TYPES.CONTRACT_CALL,
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
// Gas Usage Simulation (checks contract reverts)
|
|
49
|
+
fetchGasLimitFailed: {
|
|
50
|
+
reason: safeString`fetch gas limit failed`,
|
|
51
|
+
type: EVM_ERROR_TYPES.CONTRACT_CALL,
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// Write operations (checks sender account state/ability to transact)
|
|
55
|
+
// Generic failure, no info about a particular smart contract revert.
|
|
56
|
+
broadcastTxFailed: {
|
|
57
|
+
reason: safeString`broadcast tx failed`,
|
|
58
|
+
type: EVM_ERROR_TYPES.BROADCAST,
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
// Reversion reasons
|
|
62
|
+
executionReverted: {
|
|
63
|
+
reason: safeString`execution reverted`,
|
|
64
|
+
type: EVM_ERROR_TYPES.BROADCAST,
|
|
65
|
+
},
|
|
66
|
+
invalidOpcode: {
|
|
67
|
+
reason: safeString`invalid opcode`,
|
|
68
|
+
type: EVM_ERROR_TYPES.BROADCAST,
|
|
69
|
+
},
|
|
70
|
+
insufficientFundsForGas: {
|
|
71
|
+
reason: safeString`insufficient funds for gas * price + value`,
|
|
72
|
+
type: EVM_ERROR_TYPES.BROADCAST,
|
|
73
|
+
},
|
|
74
|
+
alreadyKnown: {
|
|
75
|
+
reason: safeString`already known`,
|
|
76
|
+
type: EVM_ERROR_TYPES.BROADCAST,
|
|
77
|
+
},
|
|
78
|
+
nonceTooLow: {
|
|
79
|
+
reason: safeString`nonce too low`,
|
|
80
|
+
type: EVM_ERROR_TYPES.BROADCAST,
|
|
81
|
+
},
|
|
82
|
+
insufficientBalance: {
|
|
83
|
+
reason: safeString`execution reverted: insufficient balance / transfer exceeds balance`,
|
|
84
|
+
type: EVM_ERROR_TYPES.BROADCAST,
|
|
85
|
+
},
|
|
86
|
+
replacementTransactionUnderpriced: {
|
|
87
|
+
reason: safeString`replacement transaction underpriced`,
|
|
88
|
+
type: EVM_ERROR_TYPES.BROADCAST,
|
|
89
|
+
},
|
|
90
|
+
zeroTransfer: {
|
|
91
|
+
reason: safeString`execution reverted: transfer amount must be greater than zero`,
|
|
92
|
+
type: EVM_ERROR_TYPES.BROADCAST,
|
|
93
|
+
},
|
|
94
|
+
mathError: {
|
|
95
|
+
reason: safeString`execution reverted: arithmetic overflow/underflow`,
|
|
96
|
+
type: EVM_ERROR_TYPES.BROADCAST,
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
transactionUnderpriced: {
|
|
100
|
+
reason: safeString`transaction underpriced`,
|
|
101
|
+
type: EVM_ERROR_TYPES.BROADCAST,
|
|
102
|
+
},
|
|
103
|
+
nonceTooHigh: {
|
|
104
|
+
reason: safeString`nonce too high`,
|
|
105
|
+
type: EVM_ERROR_TYPES.BROADCAST,
|
|
106
|
+
},
|
|
107
|
+
gasPriceBelowMin: {
|
|
108
|
+
reason: safeString`transaction gas price below minimum`,
|
|
109
|
+
type: EVM_ERROR_TYPES.BROADCAST,
|
|
110
|
+
},
|
|
111
|
+
insufficientGas: {
|
|
112
|
+
reason: safeString`insufficient gas`,
|
|
113
|
+
type: EVM_ERROR_TYPES.BROADCAST,
|
|
114
|
+
},
|
|
115
|
+
futureTransactionReplace: {
|
|
116
|
+
reason: safeString`future transaction tries to replace pending`,
|
|
117
|
+
type: EVM_ERROR_TYPES.BROADCAST,
|
|
118
|
+
},
|
|
119
|
+
eip155Required: {
|
|
120
|
+
reason: safeString`eip-155 replay protection required`,
|
|
121
|
+
type: EVM_ERROR_TYPES.BROADCAST,
|
|
122
|
+
},
|
|
123
|
+
invalidRlpSerialization: {
|
|
124
|
+
reason: safeString`invalid rlp serialization`,
|
|
125
|
+
type: EVM_ERROR_TYPES.BROADCAST,
|
|
126
|
+
},
|
|
127
|
+
transactionTypeNotSupported: {
|
|
128
|
+
reason: safeString`transaction type not supported`,
|
|
129
|
+
type: EVM_ERROR_TYPES.BROADCAST,
|
|
130
|
+
},
|
|
131
|
+
delegatedAccountError: {
|
|
132
|
+
reason: safeString`delegated account error`,
|
|
133
|
+
type: EVM_ERROR_TYPES.BROADCAST,
|
|
134
|
+
},
|
|
135
|
+
invalidJumpDestination: {
|
|
136
|
+
reason: safeString`invalid jump destination`,
|
|
137
|
+
type: EVM_ERROR_TYPES.BROADCAST,
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Patterns to normalize (group similar errors together)
|
|
142
|
+
const EVM_NORMALIZATION_PATTERNS = [
|
|
143
|
+
// Insufficient funds for gas (native currency, not tokens)
|
|
144
|
+
{
|
|
145
|
+
pattern: /(insufficient funds for gas \* price \+ value|insufficient funds(?!.*balance))/i,
|
|
146
|
+
errorInfo: EVM_ERROR_REASONS.insufficientFundsForGas,
|
|
147
|
+
},
|
|
148
|
+
// "nonce too low" or "nonce is too low" - tx nonce already used
|
|
149
|
+
{
|
|
150
|
+
pattern: /\bnonce\b.*\btoo low\b/i,
|
|
151
|
+
errorInfo: EVM_ERROR_REASONS.nonceTooLow,
|
|
152
|
+
},
|
|
153
|
+
// Comprehensive insufficient balance / transfer exceeds balance pattern (TOKEN balance issues)
|
|
154
|
+
// Catches: ERC20/BEP20, 0xe450d38c custom error, insufficient balance, balanceNotEnough,
|
|
155
|
+
// and token-specific "transfer amount exceeds balance" (BaseToken, SaveYourAssets, XVS, Ondo, FLOKI, etc.)
|
|
156
|
+
{
|
|
157
|
+
pattern:
|
|
158
|
+
/(0xe450d38c|insufficient.?balance|balance.?not.?enough|not enough fund|(erc20|bep20):.*transfer amount exceeds balance|\w+:.*transfer amount exceeds balance)/i,
|
|
159
|
+
errorInfo: EVM_ERROR_REASONS.insufficientBalance,
|
|
160
|
+
},
|
|
161
|
+
// Arithmetic overflow/underflow errors (SafeMath, Solidity 0.8+, etc.)
|
|
162
|
+
{
|
|
163
|
+
pattern: /(safemath:.*overflow|arithmetic underflow or overflow)/i,
|
|
164
|
+
errorInfo: EVM_ERROR_REASONS.mathError,
|
|
165
|
+
},
|
|
166
|
+
// Transfer amount must be greater than zero (various phrasings)
|
|
167
|
+
{
|
|
168
|
+
pattern:
|
|
169
|
+
/(transfer amount must be greater than zero|amount must be greater than 0|amt must be over than 0|zero_amount|transfer amount must be positive|transfer amount zero)/i,
|
|
170
|
+
errorInfo: EVM_ERROR_REASONS.zeroTransfer,
|
|
171
|
+
},
|
|
172
|
+
// Gas price below network minimum (not mempool competition, but protocol minimum)
|
|
173
|
+
// "gas price below minimum", "gas tip cap...minimum", "max fee per gas less than block base fee"
|
|
174
|
+
{
|
|
175
|
+
pattern:
|
|
176
|
+
/gas price below minimum|gas tip cap.*minimum|max fee per gas less than block base fee/i,
|
|
177
|
+
errorInfo: EVM_ERROR_REASONS.gasPriceBelowMin,
|
|
178
|
+
},
|
|
179
|
+
// Gas limit errors
|
|
180
|
+
// "intrinsic gas too low", "insufficient gas", "floor data gas cost" - gas limit below minimum required
|
|
181
|
+
{
|
|
182
|
+
pattern: /intrinsic gas too low|insufficient gas|floor data gas cost/i,
|
|
183
|
+
errorInfo: EVM_ERROR_REASONS.insufficientGas,
|
|
184
|
+
},
|
|
185
|
+
// "nonce too high" or "nonce is too high" - tx nonce too far ahead
|
|
186
|
+
{
|
|
187
|
+
pattern: /\bnonce\b.*\btoo high\b/i,
|
|
188
|
+
errorInfo: EVM_ERROR_REASONS.nonceTooHigh,
|
|
189
|
+
},
|
|
190
|
+
// Transaction type not supported
|
|
191
|
+
// "transaction type not supported", "tx type not supported" - chain doesn't support this tx type (e.g., EIP-1559)
|
|
192
|
+
{
|
|
193
|
+
pattern: /transaction type not supported|tx type not supported/i,
|
|
194
|
+
errorInfo: EVM_ERROR_REASONS.transactionTypeNotSupported,
|
|
195
|
+
},
|
|
196
|
+
// Invalid jump destination
|
|
197
|
+
{
|
|
198
|
+
pattern: /invalid jump destination/i,
|
|
199
|
+
errorInfo: EVM_ERROR_REASONS.invalidJumpDestination,
|
|
200
|
+
},
|
|
201
|
+
// Transaction already in mempool/chain
|
|
202
|
+
// "already known", "known transaction", "already imported", "already in mempool"
|
|
203
|
+
{
|
|
204
|
+
pattern: /already known|known transaction|already imported|already in mempool/i,
|
|
205
|
+
errorInfo: EVM_ERROR_REASONS.alreadyKnown,
|
|
206
|
+
},
|
|
207
|
+
// Replacement transaction underpriced (must come BEFORE "transaction underpriced")
|
|
208
|
+
{
|
|
209
|
+
pattern: /replacement transaction underpriced/i,
|
|
210
|
+
errorInfo: EVM_ERROR_REASONS.replacementTransactionUnderpriced,
|
|
211
|
+
},
|
|
212
|
+
// Transaction underpriced (checked after replacement to avoid double match)
|
|
213
|
+
{
|
|
214
|
+
pattern: /transaction underpriced/i,
|
|
215
|
+
errorInfo: EVM_ERROR_REASONS.transactionUnderpriced,
|
|
216
|
+
},
|
|
217
|
+
// Replay-protected transactions required (EIP-155 chain ID signing)
|
|
218
|
+
// "only replay-protected", "replay-protected transaction/tx required", "not replay protected", "invalid chain id"
|
|
219
|
+
{
|
|
220
|
+
pattern:
|
|
221
|
+
/only replay-protected|replay-protected (transaction|tx) required|not replay protected|invalid chain id/i,
|
|
222
|
+
errorInfo: EVM_ERROR_REASONS.eip155Required,
|
|
223
|
+
},
|
|
224
|
+
// Future transaction tries to replace pending (nonce conflict)
|
|
225
|
+
{
|
|
226
|
+
pattern: /future transaction.*replace.*pending/i,
|
|
227
|
+
errorInfo: EVM_ERROR_REASONS.futureTransactionReplace,
|
|
228
|
+
},
|
|
229
|
+
// Serialization/signature errors
|
|
230
|
+
// "rlp:", "invalid rlp", "decode rlp", "invalid sender", "invalid signature", "bad signature"
|
|
231
|
+
{
|
|
232
|
+
pattern: /rlp:|invalid rlp|decode rlp|invalid sender|invalid signature|bad signature/i,
|
|
233
|
+
errorInfo: EVM_ERROR_REASONS.invalidRlpSerialization,
|
|
234
|
+
},
|
|
235
|
+
// EIP-7702 delegation errors
|
|
236
|
+
// "delegated" (but not "delegatecall"), "eip-7702", "eip7702" - account delegation related errors
|
|
237
|
+
{
|
|
238
|
+
pattern: /\bdelegated\b(?!\s*call)|eip-?7702/i,
|
|
239
|
+
errorInfo: EVM_ERROR_REASONS.delegatedAccountError,
|
|
240
|
+
},
|
|
241
|
+
// GENERIC CATCH-ALL PATTERNS (must be last)
|
|
242
|
+
{
|
|
243
|
+
pattern: /^execution reverted$/i,
|
|
244
|
+
errorInfo: EVM_ERROR_REASONS.executionReverted,
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
pattern: /\binvalid opcode\b/i,
|
|
248
|
+
errorInfo: EVM_ERROR_REASONS.invalidOpcode,
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
pattern: /^execution reverted: 0x$/i,
|
|
252
|
+
errorInfo: EVM_ERROR_REASONS.executionReverted,
|
|
253
|
+
},
|
|
254
|
+
// Final catch-all for any remaining "execution reverted:" messages
|
|
255
|
+
{
|
|
256
|
+
pattern: /execution reverted:/i,
|
|
257
|
+
errorInfo: EVM_ERROR_REASONS.executionReverted,
|
|
258
|
+
},
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
// This should be renamed something better
|
|
262
|
+
export function getEvmErrorReason(errorMessage) {
|
|
263
|
+
for (const { pattern, errorInfo } of EVM_NORMALIZATION_PATTERNS) {
|
|
264
|
+
if (pattern.test(errorMessage)) {
|
|
265
|
+
return {
|
|
266
|
+
reason: errorInfo.reason,
|
|
267
|
+
type: errorInfo.type,
|
|
268
|
+
wasNormalized: true,
|
|
269
|
+
patternUsed: pattern.toString(),
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return null
|
|
11
275
|
}
|
|
12
276
|
|
|
13
277
|
const MAX_HINT_LENGTH = 100
|
|
@@ -19,15 +283,22 @@ export class EthLikeError extends Error {
|
|
|
19
283
|
* Creates an instance of EthLikeError.
|
|
20
284
|
*
|
|
21
285
|
* @param {string} message - Standard error message.
|
|
22
|
-
* @param {
|
|
286
|
+
* @param {Object} errorReasonInfo - Object containing reason and type for the error.
|
|
287
|
+
* @param {string} errorReasonInfo.reason - A constant indicating the generic failure. Must not contain any sensitive information such as private keys, transaction IDs, or wallet addresses.
|
|
288
|
+
* @param {string} errorReasonInfo.type - A category for the error (e.g., 'NODE_STATE_READ', 'CONTRACT_CALL', 'BROADCAST').
|
|
23
289
|
* @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.
|
|
290
|
+
* @param {TraceId} [traceId] - Optional TraceId object from HTTP response 'traceparent' header for debugging.
|
|
291
|
+
* @param {string} [baseAssetName] - Optional base asset name (e.g. 'ethereum', 'bnbsmartchain') for identifying the network.
|
|
24
292
|
*/
|
|
25
|
-
constructor({ message,
|
|
293
|
+
constructor({ message, errorReasonInfo, hint, traceId, baseAssetName }) {
|
|
26
294
|
super(message)
|
|
27
|
-
this.name =
|
|
295
|
+
this.name = safeString`EthLikeError`
|
|
28
296
|
this.#hintStack = [hint] // NOTE: we can add more hints to the stack
|
|
29
|
-
this.reason = reason
|
|
297
|
+
this.reason = errorReasonInfo.reason
|
|
30
298
|
this.hint = this.#extractHint(hint)
|
|
299
|
+
this.type = errorReasonInfo.type
|
|
300
|
+
this.traceId = traceId
|
|
301
|
+
this.baseAssetName = baseAssetName
|
|
31
302
|
}
|
|
32
303
|
|
|
33
304
|
addHint = (hint) => {
|
|
@@ -41,6 +312,15 @@ export class EthLikeError extends Error {
|
|
|
41
312
|
return this
|
|
42
313
|
}
|
|
43
314
|
|
|
315
|
+
getContext = () => {
|
|
316
|
+
return {
|
|
317
|
+
reason: this.reason,
|
|
318
|
+
type: this.type,
|
|
319
|
+
traceId: this.traceId,
|
|
320
|
+
baseAssetName: this.baseAssetName,
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
44
324
|
// NOTE: a class method for now, if we move this to be a generic error for all assets, each asset will have its own implementation
|
|
45
325
|
#extractHint = (hint) => {
|
|
46
326
|
if (!hint) return ''
|
|
@@ -72,7 +352,7 @@ export class EthLikeError extends Error {
|
|
|
72
352
|
}
|
|
73
353
|
}
|
|
74
354
|
|
|
75
|
-
export const withErrorReason = async (promise,
|
|
355
|
+
export const withErrorReason = async ({ promise, errorReasonInfo, hint, baseAssetName }) => {
|
|
76
356
|
try {
|
|
77
357
|
return await promise
|
|
78
358
|
} catch (err) {
|
|
@@ -84,8 +364,10 @@ export const withErrorReason = async (promise, errorReason, hint) => {
|
|
|
84
364
|
|
|
85
365
|
throw new EthLikeError({
|
|
86
366
|
message: err.message,
|
|
87
|
-
|
|
367
|
+
errorReasonInfo,
|
|
88
368
|
hint,
|
|
369
|
+
traceId: err.traceId,
|
|
370
|
+
baseAssetName,
|
|
89
371
|
})
|
|
90
372
|
}
|
|
91
373
|
}
|
package/src/eth-like-util.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { memoizeLruCache } from '@exodus/asset-lib'
|
|
2
2
|
import { ABI, isEthereumLikeAsset, isEthereumLikeToken, normalizeTxId } from '@exodus/ethereum-lib'
|
|
3
|
+
import { safeString } from '@exodus/safe-string'
|
|
3
4
|
import SolidityContract from '@exodus/solidity-contract'
|
|
4
5
|
import assert from 'minimalistic-assert'
|
|
5
6
|
|
|
6
|
-
import
|
|
7
|
+
import { EthLikeError, EVM_ERROR_REASONS, withErrorReason } from './error-wrapper.js'
|
|
7
8
|
import { getServer, getServerByName } from './exodus-eth-server/index.js'
|
|
8
9
|
import { fromHexToString } from './number-utils.js'
|
|
9
10
|
|
|
@@ -18,7 +19,12 @@ export async function isContract(baseAssetName, address) {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export async function isContractAddress({ asset, address }) {
|
|
21
|
-
return
|
|
22
|
+
return withErrorReason({
|
|
23
|
+
promise: getServer(asset).isContract(address),
|
|
24
|
+
errorReasonInfo: EVM_ERROR_REASONS.getCodeFailed,
|
|
25
|
+
hint: safeString`isContractAddress`,
|
|
26
|
+
baseAssetName: asset.baseAsset.name,
|
|
27
|
+
})
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
export const isContractAddressCached = memoizeLruCache(
|
|
@@ -29,7 +35,12 @@ export const isContractAddressCached = memoizeLruCache(
|
|
|
29
35
|
|
|
30
36
|
export async function isForwarderContract({ asset, address }) {
|
|
31
37
|
const server = getServer(asset)
|
|
32
|
-
const contractCode = await
|
|
38
|
+
const contractCode = await withErrorReason({
|
|
39
|
+
promise: server.getCode(address),
|
|
40
|
+
errorReasonInfo: EVM_ERROR_REASONS.getCodeFailed,
|
|
41
|
+
hint: safeString`isForwarderContract`,
|
|
42
|
+
baseAssetName: asset.baseAsset.name,
|
|
43
|
+
})
|
|
33
44
|
return contractCode === FORWARDER_CONTRACT_CODE
|
|
34
45
|
}
|
|
35
46
|
|
|
@@ -41,21 +52,23 @@ export const isForwarderContractCached = memoizeLruCache(
|
|
|
41
52
|
|
|
42
53
|
export async function getNonce({ asset, address, tag = 'latest' }) {
|
|
43
54
|
const server = getServer(asset)
|
|
44
|
-
const nonce = await
|
|
45
|
-
server.getTransactionCount(address, tag),
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
const nonce = await withErrorReason({
|
|
56
|
+
promise: server.getTransactionCount(address, tag),
|
|
57
|
+
errorReasonInfo: EVM_ERROR_REASONS.nonceFetchFailed,
|
|
58
|
+
hint: safeString`getNonce`,
|
|
59
|
+
baseAssetName: asset.baseAsset.name,
|
|
60
|
+
})
|
|
49
61
|
|
|
50
62
|
return parseInt(nonce, 16)
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
export async function estimateGas({ asset, ...args }) {
|
|
54
|
-
return
|
|
55
|
-
getServer(asset).estimateGas(args),
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
return withErrorReason({
|
|
67
|
+
promise: getServer(asset).estimateGas(args),
|
|
68
|
+
errorReasonInfo: EVM_ERROR_REASONS.fetchGasLimitFailed,
|
|
69
|
+
hint: safeString`estimateGas`,
|
|
70
|
+
baseAssetName: asset.baseAsset.name,
|
|
71
|
+
})
|
|
59
72
|
}
|
|
60
73
|
|
|
61
74
|
// Only base assets, not tokens
|
|
@@ -63,7 +76,12 @@ export async function getBalance({ asset, address }) {
|
|
|
63
76
|
if (!isEthereumLikeAsset(asset)) throw new Error(`unsupported asset ${asset.name}`)
|
|
64
77
|
|
|
65
78
|
const server = getServer(asset)
|
|
66
|
-
const balances = await
|
|
79
|
+
const balances = await withErrorReason({
|
|
80
|
+
promise: server.getBalance(address),
|
|
81
|
+
errorReasonInfo: EVM_ERROR_REASONS.balanceFetchFailed,
|
|
82
|
+
hint: safeString`getBalance`,
|
|
83
|
+
baseAssetName: asset.baseAsset.name,
|
|
84
|
+
})
|
|
67
85
|
return balances?.confirmed?.value || '0'
|
|
68
86
|
}
|
|
69
87
|
|
|
@@ -71,16 +89,27 @@ export async function getBalance({ asset, address }) {
|
|
|
71
89
|
export async function getBalanceProxied({ asset, address, tag = 'latest' }) {
|
|
72
90
|
if (!isEthereumLikeAsset(asset)) throw new Error(`unsupported asset ${asset.name}`)
|
|
73
91
|
|
|
74
|
-
const result = await
|
|
92
|
+
const result = await withErrorReason({
|
|
93
|
+
promise: getServer(asset).getBalanceProxied(address),
|
|
94
|
+
errorReasonInfo: EVM_ERROR_REASONS.balanceFetchFailed,
|
|
95
|
+
hint: safeString`getBalanceProxied`,
|
|
96
|
+
baseAssetName: asset.baseAsset.name,
|
|
97
|
+
})
|
|
75
98
|
return fromHexToString(result)
|
|
76
99
|
}
|
|
77
100
|
|
|
78
101
|
// Only ETH-like assets with token support
|
|
102
|
+
// How would this return token balances? It does the same as getBalanceProxied... getting ETH balance
|
|
79
103
|
export async function getTokenBalance({ asset, address }) {
|
|
80
104
|
if (!isEthereumLikeToken(asset)) throw new Error(`unsupported ETH-like token ${asset.name}`)
|
|
81
105
|
|
|
82
106
|
const server = getServer(asset)
|
|
83
|
-
const balances = await
|
|
107
|
+
const balances = await withErrorReason({
|
|
108
|
+
promise: server.getBalance(address),
|
|
109
|
+
errorReasonInfo: EVM_ERROR_REASONS.balanceFetchFailed,
|
|
110
|
+
hint: safeString`getTokenBalance`,
|
|
111
|
+
baseAssetName: asset.baseAsset.name,
|
|
112
|
+
})
|
|
84
113
|
const contractAddress = asset.contract.address.toLowerCase()
|
|
85
114
|
return balances?.confirmed?.[contractAddress] || '0'
|
|
86
115
|
}
|
|
@@ -90,7 +119,12 @@ export async function getTokenBalanceFromNode({ asset, address }) {
|
|
|
90
119
|
|
|
91
120
|
const server = getServer(asset)
|
|
92
121
|
const contractAddress = asset.contract.address.toLowerCase()
|
|
93
|
-
const balances = await
|
|
122
|
+
const balances = await withErrorReason({
|
|
123
|
+
promise: server.balanceOf(address, contractAddress),
|
|
124
|
+
errorReasonInfo: EVM_ERROR_REASONS.ethCallErc20Failed,
|
|
125
|
+
hint: safeString`getTokenBalanceFromNode`,
|
|
126
|
+
baseAssetName: asset.baseAsset.name,
|
|
127
|
+
})
|
|
94
128
|
return balances?.confirmed?.[contractAddress] || '0'
|
|
95
129
|
}
|
|
96
130
|
|
|
@@ -102,14 +136,24 @@ export function sendRawTransaction(asset) {
|
|
|
102
136
|
export async function transactionExists({ asset, txId }) {
|
|
103
137
|
const server = getServer(asset)
|
|
104
138
|
txId = normalizeTxId(txId)
|
|
105
|
-
const txResult = await
|
|
139
|
+
const txResult = await withErrorReason({
|
|
140
|
+
promise: server.getTransactionByHash(txId),
|
|
141
|
+
errorReasonInfo: EVM_ERROR_REASONS.getTransactionByHashFailed,
|
|
142
|
+
hint: safeString`transactionExists`,
|
|
143
|
+
baseAssetName: asset.baseAsset.name,
|
|
144
|
+
})
|
|
106
145
|
return Boolean(txResult && txResult.hash === txId)
|
|
107
146
|
}
|
|
108
147
|
|
|
109
148
|
export async function getTransaction({ asset, txId }) {
|
|
110
149
|
const server = getServer(asset)
|
|
111
150
|
txId = normalizeTxId(txId)
|
|
112
|
-
return
|
|
151
|
+
return withErrorReason({
|
|
152
|
+
promise: server.getTransactionByHash(txId),
|
|
153
|
+
errorReasonInfo: EVM_ERROR_REASONS.getTransactionByHashFailed,
|
|
154
|
+
hint: safeString`getTransaction`,
|
|
155
|
+
baseAssetName: asset.baseAsset.name,
|
|
156
|
+
})
|
|
113
157
|
}
|
|
114
158
|
|
|
115
159
|
export const getIsForwarderContract = memoizeLruCache(
|
|
@@ -140,18 +184,12 @@ export const getERC20Params = async ({ asset, address, paramNames = DEFAULT_PARA
|
|
|
140
184
|
try {
|
|
141
185
|
callResponse = await server.ethCall({ to: address, data: ERC20[method].methodId })
|
|
142
186
|
} catch (err) {
|
|
143
|
-
|
|
144
|
-
throw new ErrorWrapper.EthLikeError({
|
|
145
|
-
message: `Can't find parameters for contract with address ${address}. Are you sure it is a valid ERC20 contract?`,
|
|
146
|
-
reason: ErrorWrapper.reasons.ethCallErc20Failed,
|
|
147
|
-
hint: 'ethCall:executionReverted',
|
|
148
|
-
})
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
throw new ErrorWrapper.EthLikeError({
|
|
187
|
+
throw new EthLikeError({
|
|
152
188
|
message: err.message,
|
|
153
|
-
|
|
154
|
-
hint:
|
|
189
|
+
errorReasonInfo: EVM_ERROR_REASONS.ethCallErc20Failed,
|
|
190
|
+
hint: safeString`getERC20Params`,
|
|
191
|
+
traceId: err.traceId,
|
|
192
|
+
baseAssetName: asset.baseAsset.name,
|
|
155
193
|
})
|
|
156
194
|
}
|
|
157
195
|
|
|
@@ -181,7 +219,12 @@ export const getERC20Params = async ({ asset, address, paramNames = DEFAULT_PARA
|
|
|
181
219
|
}
|
|
182
220
|
|
|
183
221
|
export async function getEIP7702Delegation({ address, server }) {
|
|
184
|
-
const code = await
|
|
222
|
+
const code = await withErrorReason({
|
|
223
|
+
promise: server.getCode(address),
|
|
224
|
+
errorReasonInfo: EVM_ERROR_REASONS.getCodeFailed,
|
|
225
|
+
hint: safeString`getEIP7702Delegation`,
|
|
226
|
+
baseAssetName: server.baseAssetName,
|
|
227
|
+
})
|
|
185
228
|
|
|
186
229
|
// No code at all
|
|
187
230
|
if (!code || code === '0x' || code === '0x0') {
|
|
@@ -11,8 +11,9 @@ import { getFallbackGasPriceEstimation } from './utils.js'
|
|
|
11
11
|
const { isEmpty } = lodash
|
|
12
12
|
|
|
13
13
|
export default class ApiCoinNodesServer extends EventEmitter {
|
|
14
|
-
constructor({ uri }) {
|
|
14
|
+
constructor({ baseAssetName, uri }) {
|
|
15
15
|
super()
|
|
16
|
+
this.baseAssetName = baseAssetName
|
|
16
17
|
this.uri = uri
|
|
17
18
|
this.defaultUri = uri
|
|
18
19
|
this.id = 0
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { retry } from '@exodus/simple-retry'
|
|
2
|
+
import { TraceId } from '@exodus/traceparent'
|
|
2
3
|
import assert from 'minimalistic-assert'
|
|
3
4
|
|
|
4
5
|
import ClarityServer, { RPC_REQUEST_TIMEOUT } from './clarity.js'
|
|
@@ -43,14 +44,31 @@ const fetchJson = async (url, fetchOptions) => {
|
|
|
43
44
|
const response = await fetch(url, fetchOptions)
|
|
44
45
|
|
|
45
46
|
if (!response.ok) {
|
|
46
|
-
|
|
47
|
+
const traceId = TraceId.fromResponse(response)
|
|
48
|
+
const error = new Error(
|
|
47
49
|
`${url} returned ${response.status}: ${
|
|
48
50
|
response.statusText || 'Unknown Status Text'
|
|
49
51
|
}. Body: ${await getTextFromResponse(response)}`
|
|
50
52
|
)
|
|
53
|
+
if (traceId) {
|
|
54
|
+
error.traceId = traceId
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
throw error
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const json = await response.json()
|
|
61
|
+
|
|
62
|
+
// Only capture trace ID if there's an RPC error in the response
|
|
63
|
+
// (handleJsonRPCResponse will extract it when throwing the error)
|
|
64
|
+
if (json.error) {
|
|
65
|
+
const traceId = TraceId.fromResponse(response)
|
|
66
|
+
if (traceId) {
|
|
67
|
+
json.__traceId = traceId
|
|
68
|
+
}
|
|
51
69
|
}
|
|
52
70
|
|
|
53
|
-
return
|
|
71
|
+
return json
|
|
54
72
|
}
|
|
55
73
|
|
|
56
74
|
async function fetchJsonRetry(url, fetchOptions) {
|
|
@@ -126,9 +144,6 @@ export default class ClarityServerV2 extends ClarityServer {
|
|
|
126
144
|
// See: https://github.com/ExodusMovement/clarity/blob/d3c2a7f501a4391da630592bca3bf57c3ddd5e89/src/modules/ethereum-like/gas-price/index.js#L192C5-L219C6
|
|
127
145
|
return await this.getGasPriceEstimation()
|
|
128
146
|
} catch {
|
|
129
|
-
console.log(
|
|
130
|
-
`failed to query ${this.baseAssetName} gas-price-estimation endpoint, falling back to websocket`
|
|
131
|
-
)
|
|
132
147
|
// HACK: The `getGasPriceEstimation` endpoint is not guaranteed
|
|
133
148
|
// to exist for all assets. In this case, we'll fallback
|
|
134
149
|
// to legacy behaviour, which is to query via the WebSocket.
|
|
@@ -185,8 +200,9 @@ export default class ClarityServerV2 extends ClarityServer {
|
|
|
185
200
|
}
|
|
186
201
|
}
|
|
187
202
|
|
|
188
|
-
fetchRpcHttpRequest = ({ baseApiPath, body }) =>
|
|
189
|
-
fetchHttpRequest({ baseApiPath, path: '/rpc', method: 'POST', body })
|
|
203
|
+
fetchRpcHttpRequest = ({ baseApiPath, body }) => {
|
|
204
|
+
return fetchHttpRequest({ baseApiPath, path: '/rpc', method: 'POST', body })
|
|
205
|
+
}
|
|
190
206
|
|
|
191
207
|
async sendRpcRequest(rpcRequest) {
|
|
192
208
|
try {
|
|
@@ -201,6 +217,8 @@ export default class ClarityServerV2 extends ClarityServer {
|
|
|
201
217
|
}
|
|
202
218
|
}
|
|
203
219
|
|
|
220
|
+
// Maybe some functions from clarity should be overriden to have handleJsonRPCResponse as well
|
|
221
|
+
|
|
204
222
|
async sendRawTransaction(...params) {
|
|
205
223
|
const { baseApiPath } = this
|
|
206
224
|
const request = this.sendRawTransactionRequest(...params)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { bufferToHex } from '@exodus/ethereumjs/util'
|
|
2
2
|
import { safeString } from '@exodus/safe-string'
|
|
3
3
|
import SolidityContract from '@exodus/solidity-contract'
|
|
4
|
+
import { TraceId } from '@exodus/traceparent'
|
|
4
5
|
import EventEmitter from 'events/events.js'
|
|
5
6
|
import io from 'socket.io-client'
|
|
6
7
|
|
|
@@ -104,12 +105,20 @@ export default class ClarityServer extends EventEmitter {
|
|
|
104
105
|
|
|
105
106
|
const revisedError = new Error(`Bad rpc response: ${message}`)
|
|
106
107
|
revisedError.hint = safeString`Bad rpc response: ${errorMessageToSafeHint(message)}`
|
|
108
|
+
|
|
109
|
+
// Preserve trace ID: from HTTP (__traceId) or WebSocket (traceparent in JSON body)
|
|
110
|
+
const traceId = response?.__traceId || response?.traceparent
|
|
111
|
+
if (traceId) {
|
|
112
|
+
revisedError.traceId = traceId
|
|
113
|
+
}
|
|
114
|
+
|
|
107
115
|
throw revisedError
|
|
108
116
|
}
|
|
109
117
|
|
|
110
118
|
return result
|
|
111
119
|
}
|
|
112
120
|
|
|
121
|
+
// Transport: WS only in ClarityServer, HTTP only in ClarityServerV2 (overridden)
|
|
113
122
|
async getAllTransactions(params) {
|
|
114
123
|
const transactions = { pending: [], confirmed: [] }
|
|
115
124
|
const cursor = await this.getTransactions({
|
|
@@ -129,6 +138,7 @@ export default class ClarityServer extends EventEmitter {
|
|
|
129
138
|
return { cursor, transactions }
|
|
130
139
|
}
|
|
131
140
|
|
|
141
|
+
// Transport: WS only (uses transactions socket, not RPC socket)
|
|
132
142
|
async getTransactions({ walletAccount, address, cursor, onChunk }) {
|
|
133
143
|
const socket = this.connectTransactions({ walletAccount, address })
|
|
134
144
|
const listener = (isPending, chunk, callback) => {
|
|
@@ -149,6 +159,7 @@ export default class ClarityServer extends EventEmitter {
|
|
|
149
159
|
.finally(() => socket.off('transactionsChunk', listener))
|
|
150
160
|
}
|
|
151
161
|
|
|
162
|
+
// Transport: WS only (uses fee socket)
|
|
152
163
|
getFeeFromWebSocket() {
|
|
153
164
|
const socket = this.connectFee()
|
|
154
165
|
return new Promise((resolve, reject) => {
|
|
@@ -165,10 +176,12 @@ export default class ClarityServer extends EventEmitter {
|
|
|
165
176
|
})
|
|
166
177
|
}
|
|
167
178
|
|
|
179
|
+
// Transport: WS only in ClarityServer, HTTP first → WS fallback in ClarityServerV2 (overridden)
|
|
168
180
|
async getFee() {
|
|
169
181
|
return this.getFeeFromWebSocket()
|
|
170
182
|
}
|
|
171
183
|
|
|
184
|
+
// Transport: Depends on getFee() - WS only in ClarityServer, HTTP first → WS fallback in ClarityServerV2
|
|
172
185
|
// for fee monitors
|
|
173
186
|
async getGasPrice() {
|
|
174
187
|
const fee = await this.getFee()
|
|
@@ -185,11 +198,18 @@ export default class ClarityServer extends EventEmitter {
|
|
|
185
198
|
const timeout = setTimeout(() => reject(new Error(RPC_REQUEST_TIMEOUT)), 3000)
|
|
186
199
|
rpcSocket.emit('request', rpcRequest, (response) => {
|
|
187
200
|
clearTimeout(timeout)
|
|
201
|
+
const rawTraceparent = response?.traceparent || response?.[0]?.traceparent
|
|
202
|
+
if (rawTraceparent) {
|
|
203
|
+
// eslint-disable-next-line @exodus/mutable/no-param-reassign-prop-only
|
|
204
|
+
response.traceparent = TraceId.fromRawTraceparent(rawTraceparent)
|
|
205
|
+
}
|
|
206
|
+
|
|
188
207
|
resolve(response)
|
|
189
208
|
})
|
|
190
209
|
})
|
|
191
210
|
}
|
|
192
211
|
|
|
212
|
+
// Transport: WS only in ClarityServer, WS first → HTTP fallback in ClarityServerV2
|
|
193
213
|
async sendBatchRequest(batch) {
|
|
194
214
|
const responses = await this.sendRpcRequest(batch)
|
|
195
215
|
// FIXME: this falls apart if responses is not an array
|
|
@@ -206,12 +226,14 @@ export default class ClarityServer extends EventEmitter {
|
|
|
206
226
|
return batch.map((request) => keyed[`${request.id}`])
|
|
207
227
|
}
|
|
208
228
|
|
|
229
|
+
// Transport: Uses sendRpcRequest - WS only in ClarityServer, WS first → HTTP fallback in ClarityServerV2
|
|
209
230
|
async sendRequest(request) {
|
|
210
231
|
const response = await this.sendRpcRequest(request)
|
|
211
232
|
|
|
212
233
|
return this.handleJsonRPCResponse(response)
|
|
213
234
|
}
|
|
214
235
|
|
|
236
|
+
// Transport: Via getCode → sendRequest → WS first → HTTP fallback in ClarityServerV2
|
|
215
237
|
async isContract(address) {
|
|
216
238
|
const code = await this.getCode(address)
|
|
217
239
|
return code.length > 2
|
|
@@ -327,11 +349,13 @@ export default class ClarityServer extends EventEmitter {
|
|
|
327
349
|
})
|
|
328
350
|
}
|
|
329
351
|
|
|
352
|
+
// Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
|
|
330
353
|
async proxyToCoinNode(params) {
|
|
331
354
|
const request = this.buildRequest(params)
|
|
332
355
|
return this.sendRequest(request)
|
|
333
356
|
}
|
|
334
357
|
|
|
358
|
+
// Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
|
|
335
359
|
async balanceOf(address, tokenAddress, tag = 'latest') {
|
|
336
360
|
const request = this.balanceOfRequest(address, tokenAddress, tag)
|
|
337
361
|
const result = await this.sendRequest(request)
|
|
@@ -343,69 +367,83 @@ export default class ClarityServer extends EventEmitter {
|
|
|
343
367
|
}
|
|
344
368
|
}
|
|
345
369
|
|
|
370
|
+
// Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
|
|
346
371
|
async getBalance(...params) {
|
|
347
|
-
const request = this.getBalanceRequest(...params)
|
|
372
|
+
const request = this.getBalanceRequest(...params) // eth_getBalance
|
|
348
373
|
return this.sendRequest(request)
|
|
349
374
|
}
|
|
350
375
|
|
|
376
|
+
// Transport: Via getBalance → sendRequest → WS first → HTTP fallback in ClarityServerV2
|
|
351
377
|
async getBalanceProxied(...params) {
|
|
352
|
-
return this.getBalance(...params)
|
|
378
|
+
return this.getBalance(...params) // eth_getBalance
|
|
353
379
|
}
|
|
354
380
|
|
|
381
|
+
// Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
|
|
355
382
|
async gasPrice(...params) {
|
|
356
383
|
const request = this.gasPriceRequest(...params)
|
|
357
384
|
return this.sendRequest(request)
|
|
358
385
|
}
|
|
359
386
|
|
|
387
|
+
// Transport: WS only in ClarityServer, HTTP only in ClarityServerV2 (overridden)
|
|
360
388
|
async estimateGas(...params) {
|
|
361
389
|
const request = this.estimateGasRequest(...params)
|
|
362
390
|
return this.sendRequest(request)
|
|
363
391
|
}
|
|
364
392
|
|
|
393
|
+
// Transport: WS only in ClarityServer, HTTP only in ClarityServerV2 (overridden)
|
|
365
394
|
async sendRawTransaction(...params) {
|
|
366
395
|
const request = this.sendRawTransactionRequest(...params)
|
|
367
396
|
return this.sendRequest(request)
|
|
368
397
|
}
|
|
369
398
|
|
|
399
|
+
// Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
|
|
370
400
|
async getCode(...params) {
|
|
371
401
|
const request = this.getCodeRequest(...params)
|
|
372
402
|
return this.sendRequest(request)
|
|
373
403
|
}
|
|
374
404
|
|
|
405
|
+
// Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
|
|
375
406
|
async getStorageAt(...params) {
|
|
376
407
|
const request = this.getStorageAtRequest(...params)
|
|
377
408
|
return this.sendRequest(request)
|
|
378
409
|
}
|
|
379
410
|
|
|
411
|
+
// Transport: WS only in ClarityServer, HTTP only in ClarityServerV2 (overridden)
|
|
380
412
|
async getTransactionCount(...params) {
|
|
381
413
|
const request = this.getTransactionCountRequest(...params)
|
|
382
414
|
return this.sendRequest(request)
|
|
383
415
|
}
|
|
384
416
|
|
|
417
|
+
// Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
|
|
385
418
|
async getTransactionByHash(...params) {
|
|
386
419
|
const request = this.getTransactionByHashRequest(...params)
|
|
387
420
|
return this.sendRequest(request)
|
|
388
421
|
}
|
|
389
422
|
|
|
423
|
+
// Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
|
|
390
424
|
async getTransactionReceipt(...params) {
|
|
391
425
|
const request = this.getTransactionReceiptRequest(...params)
|
|
392
426
|
return this.sendRequest(request)
|
|
393
427
|
}
|
|
394
428
|
|
|
429
|
+
// Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
|
|
395
430
|
async ethCall(...params) {
|
|
396
431
|
const request = this.ethCallRequest(...params)
|
|
397
432
|
return this.sendRequest(request)
|
|
398
433
|
}
|
|
399
434
|
|
|
435
|
+
// Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
|
|
400
436
|
async blockNumber(...params) {
|
|
401
437
|
const request = this.blockNumberRequest(...params)
|
|
402
438
|
return this.sendRequest(request)
|
|
403
439
|
}
|
|
404
440
|
|
|
441
|
+
// Transport: Via getBlockByNumber → sendRequest → WS first → HTTP fallback in ClarityServerV2
|
|
405
442
|
async getLatestBlock() {
|
|
406
443
|
return this.getBlockByNumber('latest')
|
|
407
444
|
}
|
|
408
445
|
|
|
446
|
+
// Transport: Via getLatestBlock → getBlockByNumber → sendRequest → WS first → HTTP fallback in ClarityServerV2
|
|
409
447
|
async getBaseFeePerGas() {
|
|
410
448
|
const response = await this.getLatestBlock()
|
|
411
449
|
if (response.baseFeePerGas) {
|
|
@@ -413,46 +451,55 @@ export default class ClarityServer extends EventEmitter {
|
|
|
413
451
|
}
|
|
414
452
|
}
|
|
415
453
|
|
|
454
|
+
// Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
|
|
416
455
|
async getBlockByHash(...params) {
|
|
417
456
|
const request = this.getBlockByHashRequest(...params)
|
|
418
457
|
return this.sendRequest(request)
|
|
419
458
|
}
|
|
420
459
|
|
|
460
|
+
// Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
|
|
421
461
|
async getBlockTransactionCountByNumber(...params) {
|
|
422
462
|
const request = this.getBlockTransactionCountByNumberRequest(...params)
|
|
423
463
|
return this.sendRequest(request)
|
|
424
464
|
}
|
|
425
465
|
|
|
466
|
+
// Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
|
|
426
467
|
async getBlockByNumber(...params) {
|
|
427
468
|
const request = this.getBlockByNumberRequest(...params)
|
|
428
469
|
return this.sendRequest(request)
|
|
429
470
|
}
|
|
430
471
|
|
|
472
|
+
// Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
|
|
431
473
|
async simulateV1(...params) {
|
|
432
474
|
const request = this.simulateV1Request(...params)
|
|
433
475
|
return this.sendRequest(request)
|
|
434
476
|
}
|
|
435
477
|
|
|
478
|
+
// Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
|
|
436
479
|
async simulateRawTransaction(...params) {
|
|
437
480
|
const request = this.simulateRawTransactionRequest(...params)
|
|
438
481
|
return this.sendRequest(request)
|
|
439
482
|
}
|
|
440
483
|
|
|
484
|
+
// Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
|
|
441
485
|
async getCoinbase() {
|
|
442
486
|
const request = this.coinbaseRequest()
|
|
443
487
|
return this.sendRequest(request)
|
|
444
488
|
}
|
|
445
489
|
|
|
490
|
+
// Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
|
|
446
491
|
async getCompilers() {
|
|
447
492
|
const request = this.getCompilersRequest()
|
|
448
493
|
return this.sendRequest(request)
|
|
449
494
|
}
|
|
450
495
|
|
|
496
|
+
// Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
|
|
451
497
|
async getNetVersion() {
|
|
452
498
|
const request = this.getNetVersion()
|
|
453
499
|
return this.sendRequest(request)
|
|
454
500
|
}
|
|
455
501
|
|
|
502
|
+
// Transport: Via sendRequest → WS first → HTTP fallback in ClarityServerV2
|
|
456
503
|
async getLogs(...params) {
|
|
457
504
|
const request = this.getLogsRequest(...params)
|
|
458
505
|
return this.sendRequest(request)
|
|
@@ -22,7 +22,7 @@ export function createEvmServer({ assetName, serverUrl, monitorType }) {
|
|
|
22
22
|
assert(monitorType, 'monitorType is required')
|
|
23
23
|
switch (monitorType) {
|
|
24
24
|
case 'no-history':
|
|
25
|
-
return new ApiCoinNodesServer({ uri: serverUrl })
|
|
25
|
+
return new ApiCoinNodesServer({ baseAssetName: assetName, uri: serverUrl })
|
|
26
26
|
case 'clarity':
|
|
27
27
|
return new ClarityServer({ baseAssetName: assetName, uri: serverUrl })
|
|
28
28
|
case 'clarity-v2':
|
package/src/index.js
CHANGED
|
@@ -88,7 +88,13 @@ export {
|
|
|
88
88
|
fromHexToBN,
|
|
89
89
|
} from './number-utils.js'
|
|
90
90
|
|
|
91
|
-
export {
|
|
91
|
+
export {
|
|
92
|
+
EVM_ERROR_REASONS,
|
|
93
|
+
EVM_ERROR_TYPES,
|
|
94
|
+
getEvmErrorReason,
|
|
95
|
+
withErrorReason,
|
|
96
|
+
EthLikeError,
|
|
97
|
+
} from './error-wrapper.js'
|
|
92
98
|
|
|
93
99
|
export { txSendFactory } from './tx-send/index.js'
|
|
94
100
|
|
package/src/nft-utils.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { safeString } from '@exodus/safe-string'
|
|
1
2
|
import SolidityContract from '@exodus/solidity-contract'
|
|
2
3
|
import assert from 'minimalistic-assert'
|
|
3
4
|
|
|
4
|
-
import
|
|
5
|
+
import { EthLikeError, EVM_ERROR_REASONS } from './error-wrapper.js'
|
|
5
6
|
import { fetchGasLimit } from './gas-estimation.js'
|
|
6
7
|
|
|
7
8
|
export const getNftArguments = async ({ asset, nft, fromAddress, toAddress }) => {
|
|
@@ -44,10 +45,11 @@ export const getNftArguments = async ({ asset, nft, fromAddress, toAddress }) =>
|
|
|
44
45
|
}
|
|
45
46
|
})
|
|
46
47
|
).catch((e) => {
|
|
47
|
-
throw new
|
|
48
|
+
throw new EthLikeError({
|
|
48
49
|
message: errors.join('\n'),
|
|
49
|
-
|
|
50
|
-
hint:
|
|
50
|
+
errorReasonInfo: EVM_ERROR_REASONS.fetchGasLimitFailed,
|
|
51
|
+
hint: safeString`getNftArguments`,
|
|
52
|
+
traceId: e.traceId,
|
|
51
53
|
})
|
|
52
54
|
})
|
|
53
55
|
return {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createContract, createEthereumJsTx } from '@exodus/ethereum-lib'
|
|
2
2
|
import { bufferToHex } from '@exodus/ethereumjs/util'
|
|
3
|
+
import { safeString } from '@exodus/safe-string'
|
|
3
4
|
import assert from 'minimalistic-assert'
|
|
4
5
|
|
|
6
|
+
import { EVM_ERROR_REASONS, withErrorReason } from '../error-wrapper.js'
|
|
5
7
|
import { fromHexToBigInt } from '../number-utils.js'
|
|
6
8
|
|
|
7
9
|
export const estimateL1DataFeeFactory = ({ l1GasOracleAddress, server }) => {
|
|
@@ -14,7 +16,12 @@ export const estimateL1DataFeeFactory = ({ l1GasOracleAddress, server }) => {
|
|
|
14
16
|
const callData = gasContract.getL1Fee.build(serialized)
|
|
15
17
|
const buffer = Buffer.from(callData)
|
|
16
18
|
const data = bufferToHex(buffer)
|
|
17
|
-
const hex = await
|
|
19
|
+
const hex = await withErrorReason({
|
|
20
|
+
promise: server.ethCall({ to: l1GasOracleAddress, data }, 'latest'),
|
|
21
|
+
errorReasonInfo: EVM_ERROR_REASONS.ethCallFailed,
|
|
22
|
+
hint: safeString`estimateL1DataFee`,
|
|
23
|
+
baseAssetName: server.baseAssetName,
|
|
24
|
+
})
|
|
18
25
|
const l1DataFee = fromHexToBigInt(hex)
|
|
19
26
|
const padFee = l1DataFee / BigInt(4)
|
|
20
27
|
const maxL1DataFee = l1DataFee + padFee
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createContract } from '@exodus/ethereum-lib'
|
|
2
2
|
import { bufferToHex } from '@exodus/ethereumjs/util'
|
|
3
|
+
import { safeString } from '@exodus/safe-string'
|
|
3
4
|
import { retry } from '@exodus/simple-retry'
|
|
4
5
|
|
|
6
|
+
import { EVM_ERROR_REASONS, withErrorReason } from '../../error-wrapper.js'
|
|
5
7
|
import { getServerByName } from '../../exodus-eth-server/index.js'
|
|
6
8
|
|
|
7
9
|
// TODO: Shouldn't this be a function of `ethereumStakingState.minDelegateAmount`?
|
|
@@ -78,7 +80,12 @@ export class EthereumStaking {
|
|
|
78
80
|
if (typeof from === 'string' && from.length > 0) data.from = from
|
|
79
81
|
|
|
80
82
|
const eth = this.server || getServerByName(this.asset.name)
|
|
81
|
-
return
|
|
83
|
+
return withErrorReason({
|
|
84
|
+
promise: retry((...args) => eth.ethCall(...args), { delayTimesMs: RETRY_DELAYS })(data),
|
|
85
|
+
errorReasonInfo: EVM_ERROR_REASONS.ethCallFailed,
|
|
86
|
+
hint: safeString`stakingEthReadFunctionContract`,
|
|
87
|
+
baseAssetName: eth.baseAssetName,
|
|
88
|
+
})
|
|
82
89
|
}
|
|
83
90
|
|
|
84
91
|
// === ACCOUNTING ===
|
package/src/staking/matic/api.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { createContract } from '@exodus/ethereum-lib'
|
|
2
2
|
import ethAssets from '@exodus/ethereum-meta'
|
|
3
3
|
import { bufferToHex } from '@exodus/ethereumjs/util'
|
|
4
|
+
import { safeString } from '@exodus/safe-string'
|
|
4
5
|
import { retry } from '@exodus/simple-retry'
|
|
5
6
|
import BN from 'bn.js'
|
|
6
7
|
|
|
8
|
+
import { EVM_ERROR_REASONS, withErrorReason } from '../../error-wrapper.js'
|
|
7
9
|
import { getServerByName } from '../../exodus-eth-server/index.js'
|
|
8
10
|
import { fromHexToBN, fromHexToString, splitIn32BytesArray } from '../../number-utils.js'
|
|
9
11
|
|
|
@@ -43,7 +45,12 @@ export class MaticStakingApi {
|
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
const eth = this.server || getServerByName('ethereum')
|
|
46
|
-
return
|
|
48
|
+
return withErrorReason({
|
|
49
|
+
promise: retry((...args) => eth.ethCall(...args), { delayTimesMs: RETRY_DELAYS })(data),
|
|
50
|
+
errorReasonInfo: EVM_ERROR_REASONS.ethCallFailed,
|
|
51
|
+
hint: safeString`stakingMaticReadFunctionContract`,
|
|
52
|
+
baseAssetName: eth.baseAssetName,
|
|
53
|
+
})
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
getWithdrawalDelay = async () => {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { EthLikeError, EVM_ERROR_REASONS, getEvmErrorReason } from '../error-wrapper.js'
|
|
2
|
+
import { transactionExists } from '../eth-like-util.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handles broadcast errors by parsing the error message and either throwing
|
|
6
|
+
* an appropriate EthLikeError or returning info for retry logic.
|
|
7
|
+
*
|
|
8
|
+
* @param {Error} err - The error from broadcastTx.
|
|
9
|
+
* @param {Object} options - Context for error handling.
|
|
10
|
+
* @param {Object} options.asset - The asset.
|
|
11
|
+
* @param {string} options.txId - The transaction ID.
|
|
12
|
+
* @param {boolean} options.isHardware - Whether this is a hardware wallet.
|
|
13
|
+
* @param {string} options.hint - Hint for the error.
|
|
14
|
+
* @returns {Promise<{ shouldRetry: boolean }>} - Returns if nonce too low and can retry.
|
|
15
|
+
* @throws {EthLikeError} - Throws for all other error cases.
|
|
16
|
+
*/
|
|
17
|
+
export const handleBroadcastError = async (err, { asset, txId, isHardware, hint, isBumpTx }) => {
|
|
18
|
+
const message = err.message
|
|
19
|
+
|
|
20
|
+
const errorInfo = getEvmErrorReason(message) || EVM_ERROR_REASONS.broadcastTxFailed
|
|
21
|
+
|
|
22
|
+
const isNonceTooLow = errorInfo.reason === EVM_ERROR_REASONS.nonceTooLow.reason
|
|
23
|
+
const isAmbiguousError =
|
|
24
|
+
isNonceTooLow || errorInfo.reason === EVM_ERROR_REASONS.transactionUnderpriced.reason
|
|
25
|
+
|
|
26
|
+
if (errorInfo.reason === EVM_ERROR_REASONS.alreadyKnown.reason) {
|
|
27
|
+
console.info('tx already broadcast')
|
|
28
|
+
return { shouldRetry: false }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let txAlreadyExists = false
|
|
32
|
+
if (isAmbiguousError) {
|
|
33
|
+
try {
|
|
34
|
+
txAlreadyExists = await transactionExists({ asset, txId })
|
|
35
|
+
} catch (verifyErr) {
|
|
36
|
+
// Can't verify - fall through to original error handling
|
|
37
|
+
console.warn('Could not verify tx existence:', verifyErr.message)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (txAlreadyExists) {
|
|
42
|
+
console.info('tx already broadcast')
|
|
43
|
+
return { shouldRetry: false }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// NOTE: Don't auto-retry nonce repair for bump/replacement txs.
|
|
47
|
+
// A replacement must keep the *same nonce* as the tx it's replacing.
|
|
48
|
+
// If we "fix" a bump tx by advancing the nonce, we create a brand-new tx instead of replacing the pending one.
|
|
49
|
+
if (isNonceTooLow && !isHardware && !isBumpTx) return { shouldRetry: true }
|
|
50
|
+
|
|
51
|
+
throw new EthLikeError({
|
|
52
|
+
message: err.message,
|
|
53
|
+
errorReasonInfo: errorInfo,
|
|
54
|
+
hint,
|
|
55
|
+
traceId: err.traceId,
|
|
56
|
+
baseAssetName: asset.baseAsset.name,
|
|
57
|
+
})
|
|
58
|
+
}
|
package/src/tx-send/tx-send.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { normalizeTxId, parseUnsignedTx, updateNonce } from '@exodus/ethereum-lib'
|
|
2
|
+
import { safeString } from '@exodus/safe-string'
|
|
2
3
|
import assert from 'minimalistic-assert'
|
|
3
4
|
|
|
4
|
-
import * as ErrorWrapper from '../error-wrapper.js'
|
|
5
|
-
import { transactionExists } from '../eth-like-util.js'
|
|
6
5
|
import { getOptimisticTxLogEffects } from '../tx-log/index.js'
|
|
7
6
|
import { ARBITRARY_ADDRESS } from '../tx-type/index.js'
|
|
7
|
+
import { handleBroadcastError } from './broadcast-error-handler.js'
|
|
8
8
|
|
|
9
9
|
const txSendFactory = ({ assetClientInterface, createTx }) => {
|
|
10
10
|
assert(assetClientInterface, 'assetClientInterface is required')
|
|
@@ -38,6 +38,7 @@ const txSendFactory = ({ assetClientInterface, createTx }) => {
|
|
|
38
38
|
`The receiving wallet address must not be ${ARBITRARY_ADDRESS}`
|
|
39
39
|
)
|
|
40
40
|
|
|
41
|
+
// Are there any signin-level errors that should be caught here?
|
|
41
42
|
let { txId, rawTx } = await signTx({
|
|
42
43
|
asset,
|
|
43
44
|
unsignedTx,
|
|
@@ -47,41 +48,16 @@ const txSendFactory = ({ assetClientInterface, createTx }) => {
|
|
|
47
48
|
try {
|
|
48
49
|
await baseAsset.api.broadcastTx(rawTx.toString('hex'))
|
|
49
50
|
} catch (err) {
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
? await transactionExists({ asset, txId })
|
|
61
|
-
: err.message.match(/already known/i) ||
|
|
62
|
-
err.message.match(/transaction already imported/i)
|
|
63
|
-
|
|
64
|
-
if (txAlreadyExists) {
|
|
65
|
-
console.info('tx already broadcast') // inject logger factory from platform
|
|
66
|
-
} else if (insufficientFundsErr) {
|
|
67
|
-
throw new ErrorWrapper.EthLikeError({
|
|
68
|
-
message: err.message,
|
|
69
|
-
reason: ErrorWrapper.reasons.insufficientFunds,
|
|
70
|
-
hint: 'broadcastTx',
|
|
71
|
-
})
|
|
72
|
-
} else if (unsignedTx.txMeta.bumpTxId) {
|
|
73
|
-
throw new ErrorWrapper.EthLikeError({
|
|
74
|
-
message: err.message,
|
|
75
|
-
reason: ErrorWrapper.reasons.bumpTxFailed,
|
|
76
|
-
hint: 'broadcastTx',
|
|
77
|
-
})
|
|
78
|
-
} else if (!nonceTooLowErr) {
|
|
79
|
-
throw new ErrorWrapper.EthLikeError({
|
|
80
|
-
message: err.message,
|
|
81
|
-
reason: ErrorWrapper.reasons.broadcastTxFailed,
|
|
82
|
-
hint: 'otherErr:broadcastTx',
|
|
83
|
-
})
|
|
84
|
-
} else if (nonceTooLowErr && !unsignedTx.txMeta.isHardware) {
|
|
51
|
+
const isBumpTx = !!unsignedTx.txMeta.bumpTxId
|
|
52
|
+
const { shouldRetry } = await handleBroadcastError(err, {
|
|
53
|
+
asset,
|
|
54
|
+
txId,
|
|
55
|
+
isHardware: !!unsignedTx.txMeta.isHardware,
|
|
56
|
+
hint: isBumpTx ? safeString`bumpTx` : safeString`broadcastTx`,
|
|
57
|
+
isBumpTx,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
if (shouldRetry) {
|
|
85
61
|
console.info('trying to send again...') // inject logger factory from platform
|
|
86
62
|
|
|
87
63
|
// let's try to fix the nonce issue
|
|
@@ -92,10 +68,9 @@ const txSendFactory = ({ assetClientInterface, createTx }) => {
|
|
|
92
68
|
forceFromNode: true,
|
|
93
69
|
})
|
|
94
70
|
|
|
95
|
-
// TODO: We should only do this for non-replacement transactions.
|
|
96
71
|
const triedNonce = parsedTx.nonce
|
|
97
|
-
|
|
98
|
-
if (typeof triedNonce === 'number') {
|
|
72
|
+
// Defensive: avoid retrying the same nonce if node nonce is stale (non-replacement sends only).
|
|
73
|
+
if (!isBumpTx && typeof triedNonce === 'number') {
|
|
99
74
|
newNonce = Math.max(newNonce, triedNonce + 1)
|
|
100
75
|
}
|
|
101
76
|
|
|
@@ -121,29 +96,15 @@ const txSendFactory = ({ assetClientInterface, createTx }) => {
|
|
|
121
96
|
|
|
122
97
|
try {
|
|
123
98
|
await baseAsset.api.broadcastTx(rawTx.toString('hex'))
|
|
124
|
-
} catch (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
})
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
throw new ErrorWrapper.EthLikeError({
|
|
135
|
-
message: err.message,
|
|
136
|
-
reason: ErrorWrapper.reasons.broadcastTxFailed,
|
|
137
|
-
hint: 'retry:broadcastTx',
|
|
99
|
+
} catch (retryErr) {
|
|
100
|
+
await handleBroadcastError(retryErr, {
|
|
101
|
+
asset,
|
|
102
|
+
txId,
|
|
103
|
+
isHardware: false,
|
|
104
|
+
hint: safeString`retry:broadcastTx`,
|
|
105
|
+
isBumpTx: false, // retry path is only for non-bump anyway
|
|
138
106
|
})
|
|
139
107
|
}
|
|
140
|
-
} else {
|
|
141
|
-
// If none of the above apply, terminate with a general error.
|
|
142
|
-
throw new ErrorWrapper.EthLikeError({
|
|
143
|
-
message: err.message,
|
|
144
|
-
reason: ErrorWrapper.reasons.broadcastTxFailed,
|
|
145
|
-
hint: 'broadcastTx',
|
|
146
|
-
})
|
|
147
108
|
}
|
|
148
109
|
}
|
|
149
110
|
|