@exodus/ethereum-api 8.64.5 → 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 +20 -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 +7 -1
- package/src/exodus-eth-server/api.js +5 -0
- package/src/exodus-eth-server/clarity-v2.js +25 -7
- package/src/exodus-eth-server/clarity.js +54 -2
- package/src/exodus-eth-server/index.js +1 -1
- package/src/exodus-eth-server/utils.js +31 -0
- package/src/fee-utils.js +0 -9
- package/src/index.js +7 -1
- package/src/nft-utils.js +6 -4
- package/src/optimism-gas/index.js +8 -1
- package/src/server-based-fee-monitor.js +2 -10
- 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,26 @@
|
|
|
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
|
+
|
|
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)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
* fix: avoid race conditions resulting in a `tipGasPrice` of `0` where possible (#7458)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
6
26
|
## [8.64.5](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.64.4...@exodus/ethereum-api@8.64.5) (2026-02-11)
|
|
7
27
|
|
|
8
28
|
|
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') {
|
|
@@ -6,12 +6,14 @@ import lodash from 'lodash'
|
|
|
6
6
|
|
|
7
7
|
import { fromHexToString } from '../number-utils.js'
|
|
8
8
|
import { errorMessageToSafeHint } from './errors.js'
|
|
9
|
+
import { getFallbackGasPriceEstimation } from './utils.js'
|
|
9
10
|
|
|
10
11
|
const { isEmpty } = lodash
|
|
11
12
|
|
|
12
13
|
export default class ApiCoinNodesServer extends EventEmitter {
|
|
13
|
-
constructor({ uri }) {
|
|
14
|
+
constructor({ baseAssetName, uri }) {
|
|
14
15
|
super()
|
|
16
|
+
this.baseAssetName = baseAssetName
|
|
15
17
|
this.uri = uri
|
|
16
18
|
this.defaultUri = uri
|
|
17
19
|
this.id = 0
|
|
@@ -166,6 +168,10 @@ export default class ApiCoinNodesServer extends EventEmitter {
|
|
|
166
168
|
return this.sendRequest(request)
|
|
167
169
|
}
|
|
168
170
|
|
|
171
|
+
async getGasPriceEstimation() {
|
|
172
|
+
return getFallbackGasPriceEstimation({ server: this })
|
|
173
|
+
}
|
|
174
|
+
|
|
169
175
|
// for fee monitor
|
|
170
176
|
getGasPrice = this.gasPrice
|
|
171
177
|
|
|
@@ -6,6 +6,7 @@ import SolidityContract from '@exodus/solidity-contract'
|
|
|
6
6
|
import ms from 'ms'
|
|
7
7
|
|
|
8
8
|
import { fromHexToString } from '../number-utils.js'
|
|
9
|
+
import { getFallbackGasPriceEstimation } from './utils.js'
|
|
9
10
|
import createWebSocket from './ws.js'
|
|
10
11
|
|
|
11
12
|
const RETRY_DELAYS = ['10s']
|
|
@@ -120,6 +121,10 @@ export function create(defaultURL, ensAssetName) {
|
|
|
120
121
|
return requestWithRetry('proxy', { method: 'eth_gasPrice' })
|
|
121
122
|
},
|
|
122
123
|
|
|
124
|
+
async getGasPriceEstimation() {
|
|
125
|
+
return getFallbackGasPriceEstimation({ server: this })
|
|
126
|
+
},
|
|
127
|
+
|
|
123
128
|
// for fee monitor
|
|
124
129
|
async getGasPrice() {
|
|
125
130
|
return requestWithRetry('proxy', { method: 'eth_gasPrice' })
|