@exodus/ethereum-api 8.64.6 → 8.65.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 +22 -0
- package/package.json +5 -4
- package/src/create-asset-utils.js +4 -0
- package/src/create-asset.js +3 -0
- 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/fee-utils.js +32 -30
- 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-log/clarity-monitor-v2.js +10 -1
- package/src/tx-log/clarity-monitor.js +3 -1
- package/src/tx-log/ethereum-no-history-monitor.js +3 -1
- package/src/tx-log/monitor-utils/get-current-eip7702-delegation.js +40 -10
- package/src/tx-send/broadcast-error-handler.js +58 -0
- package/src/tx-send/tx-send.js +22 -61
|
@@ -1,38 +1,68 @@
|
|
|
1
1
|
import { getEIP7702Delegation } from '../../eth-like-util.js'
|
|
2
2
|
|
|
3
|
+
const NOT_DELEGATED = { isDelegated: false, delegatedAddress: null, isWhitelisted: null }
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* Checks if the address has an EIP-7702 delegation and returns the delegation state.
|
|
5
7
|
* Returns the new state if changed, or the current state if unchanged.
|
|
6
|
-
* On error, returns
|
|
8
|
+
* On error, conservatively returns { isDelegated: false } to avoid showing stale delegation
|
|
9
|
+
* state that may no longer be accurate.
|
|
7
10
|
*
|
|
8
11
|
* @param {Object} params
|
|
9
12
|
* @param {Object} params.server - The server instance to use for getCode
|
|
10
13
|
* @param {string} params.address - The wallet address to check
|
|
14
|
+
* @param {Array<{address: string, name: string}>} [params.eip7702Supported] - Whitelist of trusted delegation targets.
|
|
15
|
+
* If not an array, the check is skipped entirely (chain does not support EIP-7702).
|
|
16
|
+
* An empty array means the check runs but every delegation will be isWhitelisted: false.
|
|
11
17
|
* @param {Object} [params.currentDelegation] - The current delegation state from accountState
|
|
12
18
|
* @param {Object} [params.logger] - Optional logger for warnings
|
|
13
19
|
* @returns {Promise<Object|undefined>} The delegation state to use
|
|
14
20
|
*/
|
|
15
|
-
export async function getCurrentEIP7702Delegation({
|
|
21
|
+
export async function getCurrentEIP7702Delegation({
|
|
22
|
+
server,
|
|
23
|
+
address,
|
|
24
|
+
eip7702Supported,
|
|
25
|
+
currentDelegation,
|
|
26
|
+
logger,
|
|
27
|
+
}) {
|
|
28
|
+
// Non-array (undefined, false, etc.) → chain doesn't support EIP-7702, skip entirely
|
|
29
|
+
if (!Array.isArray(eip7702Supported)) return NOT_DELEGATED
|
|
30
|
+
|
|
16
31
|
try {
|
|
17
32
|
const result = await getEIP7702Delegation({ address, server })
|
|
18
33
|
|
|
19
|
-
|
|
34
|
+
if (!result.isDelegated) return NOT_DELEGATED
|
|
35
|
+
|
|
36
|
+
// [] → check runs but nothing is trusted; populated array → whitelist check
|
|
37
|
+
const isWhitelisted = eip7702Supported.some(
|
|
38
|
+
({ address }) => address.toLowerCase() === result.delegatedAddress.toLowerCase()
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const newDelegation = {
|
|
42
|
+
isDelegated: true,
|
|
43
|
+
delegatedAddress: result.delegatedAddress,
|
|
44
|
+
isWhitelisted,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Return new state only if something changed
|
|
20
48
|
if (
|
|
21
|
-
currentDelegation?.isDelegated !==
|
|
22
|
-
currentDelegation?.delegatedAddress !==
|
|
49
|
+
currentDelegation?.isDelegated !== newDelegation.isDelegated ||
|
|
50
|
+
currentDelegation?.delegatedAddress !== newDelegation.delegatedAddress ||
|
|
51
|
+
currentDelegation?.isWhitelisted !== newDelegation.isWhitelisted
|
|
23
52
|
) {
|
|
24
|
-
return
|
|
25
|
-
isDelegated: result.isDelegated,
|
|
26
|
-
delegatedAddress: result.delegatedAddress,
|
|
27
|
-
}
|
|
53
|
+
return newDelegation
|
|
28
54
|
}
|
|
29
55
|
} catch (error) {
|
|
30
56
|
if (logger) {
|
|
31
57
|
logger.warn('Failed to check EIP-7702 delegation:', error)
|
|
32
58
|
}
|
|
59
|
+
|
|
60
|
+
// On error, conservatively clear delegation state — only hard RPC confirmation
|
|
61
|
+
// should result in isDelegated: true being shown to the user.
|
|
62
|
+
return NOT_DELEGATED
|
|
33
63
|
}
|
|
34
64
|
|
|
35
|
-
// Return current state if unchanged
|
|
65
|
+
// Return current state if unchanged
|
|
36
66
|
return currentDelegation
|
|
37
67
|
}
|
|
38
68
|
|
|
@@ -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
|
|