@exodus/ethereum-api 8.78.2 → 8.79.1
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 +75 -0
- package/package.json +3 -3
- package/src/allowance/index.js +1 -3
- package/src/create-asset-utils.js +5 -5
- package/src/create-asset.js +8 -7
- package/src/create-token-factory.js +2 -0
- package/src/ens/index.js +0 -2
- package/src/exodus-eth-server/api-coin-nodes.js +7 -2
- package/src/exodus-eth-server/api.js +3 -3
- package/src/exodus-eth-server/clarity.js +3 -3
- package/src/exodus-eth-server/eth-like-server.js +17 -9
- package/src/exodus-eth-server/index.js +0 -6
- package/src/fee-data/EthLikeFeeData.js +0 -2
- package/src/hooks/monitor.js +161 -33
- package/src/hooks/staking-freshness.js +65 -0
- package/src/index.js +2 -0
- package/src/simulation/simulate-message-api.js +1 -1
- package/src/simulation/simulate-transactions-api.js +1 -1
- package/src/staking/api/index.js +9 -1
- package/src/staking/cached-read.js +33 -0
- package/src/staking/ethereum/api.js +52 -1
- package/src/staking/ethereum/deps.js +5 -1
- package/src/staking/ethereum/everstake.js +7 -0
- package/src/staking/ethereum/service.js +31 -23
- package/src/staking/utils/index.js +0 -2
- package/src/tx-log/clarity-monitor.js +59 -23
- package/src/tx-log/ethereum-monitor.js +54 -23
- package/src/tx-log/get-optimistic-txlog-effects.js +1 -1
- package/src/tx-log/monitor-utils/check-pending-transactions.js +12 -3
- package/src/tx-log/monitor-utils/get-derive-transactions-to-check.js +7 -3
- package/src/tx-log/monitor-utils/get-transfers-by-token-name.js +3 -2
- package/src/tx-log/monitor-utils/index.js +1 -0
- package/src/tx-log/monitor-utils/is-unverifiable-pending-tx-expired.js +19 -0
- package/src/tx-log/monitor-utils/verify-pending-tx-status-rpc.js +34 -11
- package/src/tx-log-staking-processor/utils.js +1 -1
- package/src/tx-send/broadcast-error-handler.js +2 -2
- package/src/tx-send/tx-send.js +3 -3
|
@@ -4,13 +4,18 @@ import getSenderNonceKey from './get-sender-nonce-key.js'
|
|
|
4
4
|
export default function checkPendingTransactions({
|
|
5
5
|
pendingTransactionsGroupedByAddressAndNonce = {},
|
|
6
6
|
pendingTransactionsToCheck = {},
|
|
7
|
+
recentPendingTransactions = {},
|
|
7
8
|
txlist,
|
|
8
9
|
ourWalletAddress,
|
|
9
10
|
asset,
|
|
10
11
|
}) {
|
|
11
12
|
for (const tx of txlist) {
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
const txId = tx.txId || tx.hash
|
|
14
|
+
// Found this transaction on the server, do not mark it as dropped or re-add it
|
|
15
|
+
// eslint-disable-next-line @exodus/hydra/no-param-reassign-prop-only
|
|
16
|
+
delete pendingTransactionsToCheck[txId]
|
|
17
|
+
// eslint-disable-next-line @exodus/hydra/no-param-reassign-prop-only
|
|
18
|
+
delete recentPendingTransactions[txId]
|
|
14
19
|
|
|
15
20
|
// check if this TX replaces another pending TX
|
|
16
21
|
const replacedTx =
|
|
@@ -29,5 +34,9 @@ export default function checkPendingTransactions({
|
|
|
29
34
|
}
|
|
30
35
|
}
|
|
31
36
|
|
|
32
|
-
return {
|
|
37
|
+
return {
|
|
38
|
+
pendingTransactionsGroupedByAddressAndNonce,
|
|
39
|
+
pendingTransactionsToCheck,
|
|
40
|
+
recentPendingTransactions,
|
|
41
|
+
}
|
|
33
42
|
}
|
|
@@ -10,6 +10,7 @@ const mapToObject = (map) => Object.fromEntries(map.entries()) // only for strin
|
|
|
10
10
|
export default function getDeriveTransactionsToCheck({ getTxLog }) {
|
|
11
11
|
return async ({ assetName: _assetName, walletAccount, tokens, ourWalletAddress }) => {
|
|
12
12
|
const pendingTransactionsToCheck = new Map()
|
|
13
|
+
const recentPendingTransactions = new Map()
|
|
13
14
|
const pendingTransactionsGroupedByAddressAndNonce = new Map()
|
|
14
15
|
const simulatedTransactions = new Map()
|
|
15
16
|
const now = SynchronizedTime.now()
|
|
@@ -19,11 +20,13 @@ export default function getDeriveTransactionsToCheck({ getTxLog }) {
|
|
|
19
20
|
for (const tx of txSet) {
|
|
20
21
|
// ERC20 sends have an entry in ETH and one in the ERC20 log so we just want the ETH one
|
|
21
22
|
if (
|
|
22
|
-
!pendingTransactionsToCheck.has(tx.txId) &&
|
|
23
23
|
tx.pending &&
|
|
24
|
-
|
|
24
|
+
!pendingTransactionsToCheck.has(tx.txId) &&
|
|
25
|
+
!recentPendingTransactions.has(tx.txId)
|
|
25
26
|
) {
|
|
26
|
-
|
|
27
|
+
const isRecent = now - tx.date.getTime() <= UNCONFIRMED_TX_LIMIT
|
|
28
|
+
const map = isRecent ? recentPendingTransactions : pendingTransactionsToCheck
|
|
29
|
+
map.set(tx.txId, { tx, assetName })
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
if (tx.meta.simulated) simulatedTransactions.set(tx.txId, tx)
|
|
@@ -45,6 +48,7 @@ export default function getDeriveTransactionsToCheck({ getTxLog }) {
|
|
|
45
48
|
|
|
46
49
|
return {
|
|
47
50
|
pendingTransactionsToCheck: mapToObject(pendingTransactionsToCheck),
|
|
51
|
+
recentPendingTransactions: mapToObject(recentPendingTransactions),
|
|
48
52
|
pendingTransactionsGroupedByAddressAndNonce: mapToObject(
|
|
49
53
|
pendingTransactionsGroupedByAddressAndNonce
|
|
50
54
|
),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { omit } from '@exodus/basic-utils'
|
|
2
|
+
import lodash from 'lodash' // oxlint-disable-line @exodus/oxlint-plugin-restricted-imports/prefer-basic-utils -- groupBy not in basic-utils
|
|
2
3
|
|
|
3
4
|
// Sorts an array of token transfer events into an object of transfer event arrays,
|
|
4
5
|
// whose keys are the names of the tokens which are being transferred in the corresponding array.
|
|
@@ -12,5 +13,5 @@ export default function getTransfersByTokenName(transfers, tokensByAddress) {
|
|
|
12
13
|
transfers,
|
|
13
14
|
(transfer) => tokensByAddress.get(transfer.address.toLowerCase())?.name || 'UNKNOWN'
|
|
14
15
|
)
|
|
15
|
-
return
|
|
16
|
+
return omit(transfersByTokenName, ['UNKNOWN'])
|
|
16
17
|
}
|
|
@@ -8,5 +8,6 @@ export { default as getDeriveTransactionsToCheck } from './get-derive-transactio
|
|
|
8
8
|
export { default as getCurrentEIP7702Delegation } from './get-current-eip7702-delegation.js'
|
|
9
9
|
export { default as getCurrentBlackListStatus } from './get-current-blacklist-status.js'
|
|
10
10
|
export { default as verifyRpcPendingTxStatusBatch } from './verify-pending-tx-status-rpc.js'
|
|
11
|
+
export { default as isUnverifiablePendingTxExpired } from './is-unverifiable-pending-tx-expired.js'
|
|
11
12
|
export { extractBalanceFromClarityAccountInfo } from './extract-balance-from-clarity-account-info.js'
|
|
12
13
|
export * from './exclude-unchanged-token-balances.js'
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { SynchronizedTime } from '@exodus/basic-utils'
|
|
2
|
+
import ms from 'ms'
|
|
3
|
+
|
|
4
|
+
// Upper bound on how long a pending tx with a per-entry 'unknown' verdict
|
|
5
|
+
// (a hash the node persistently errors on) keeps being preserved across
|
|
6
|
+
// refreshes. Without it, "cannot verify this hash" would mean "keep forever"
|
|
7
|
+
// and a phantom tx would pin nonce derivation indefinitely.
|
|
8
|
+
// Positively verified txs ('pending'/'confirmed') are exempt: their lifetime
|
|
9
|
+
// is bounded by the node itself (mempool eviction yields a 'dropped' verdict).
|
|
10
|
+
// A full batch failure is also exempt: it is evidence about nothing, so the
|
|
11
|
+
// monitors preserve unconditionally until the batch path recovers.
|
|
12
|
+
export const UNVERIFIABLE_PENDING_TX_MAX_AGE = ms('1h')
|
|
13
|
+
|
|
14
|
+
export default function isUnverifiablePendingTxExpired(tx) {
|
|
15
|
+
const txTime = tx.date?.getTime()
|
|
16
|
+
// Fail closed: a tx with a missing or invalid date cannot prove it is young
|
|
17
|
+
if (!Number.isFinite(txTime)) return true
|
|
18
|
+
return SynchronizedTime.now() - txTime > UNVERIFIABLE_PENDING_TX_MAX_AGE
|
|
19
|
+
}
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Determines the status of an RPC transaction response.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* `undefined` means the node returned a JSON-RPC error entry for this hash
|
|
5
|
+
* (tolerant batch parsing) - no verdict, as opposed to `null` which is a
|
|
6
|
+
* definitive "not found".
|
|
7
|
+
*
|
|
8
|
+
* @param {Object|null|undefined} rpcTx - The RPC transaction response
|
|
9
|
+
* @returns {{status: 'confirmed' | 'pending' | 'dropped' | 'unknown', blockNumber?: number}}
|
|
6
10
|
*/
|
|
7
11
|
function getTxStatus(rpcTx) {
|
|
12
|
+
if (rpcTx === undefined) {
|
|
13
|
+
return { status: 'unknown' }
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
if (rpcTx?.blockNumber) {
|
|
9
17
|
return { status: 'confirmed', blockNumber: parseInt(rpcTx.blockNumber, 16) }
|
|
10
18
|
}
|
|
@@ -29,7 +37,7 @@ function getTxStatus(rpcTx) {
|
|
|
29
37
|
* @param {Object} params.server - RPC server used to verify pending transactions
|
|
30
38
|
* @param {Object} params.logger - Logger instance for info/debug output
|
|
31
39
|
* @param {string[]} params.txIds - Array of transaction IDs/hashes to verify
|
|
32
|
-
* @returns {Promise<Object<string, {status: 'confirmed' | 'pending' | 'dropped', blockNumber?: number}>>}
|
|
40
|
+
* @returns {Promise<Object<string, {status: 'confirmed' | 'pending' | 'dropped' | 'unknown', blockNumber?: number}>>}
|
|
33
41
|
*/
|
|
34
42
|
export default async function verifyRpcPendingTxStatusBatch({ server, logger, txIds }) {
|
|
35
43
|
if (txIds.length === 0) return {}
|
|
@@ -39,27 +47,42 @@ export default async function verifyRpcPendingTxStatusBatch({ server, logger, tx
|
|
|
39
47
|
|
|
40
48
|
let responses
|
|
41
49
|
try {
|
|
42
|
-
|
|
50
|
+
// Tolerant parsing: one bad hash must not void the verdicts for every other tx
|
|
51
|
+
responses = await server.sendBatchRequest(requests, { tolerant: true })
|
|
43
52
|
} catch (error) {
|
|
44
53
|
// If batch request fails, skip all drop checks (safe default)
|
|
45
54
|
logger.warn('Batch RPC request failed, skipping drop checks', error)
|
|
46
55
|
return {}
|
|
47
56
|
}
|
|
48
57
|
|
|
58
|
+
if (!Array.isArray(responses) || responses.length !== txIds.length) {
|
|
59
|
+
// A malformed batch response must not produce 'dropped' verdicts (safe default)
|
|
60
|
+
logger.warn('Batch RPC response shape mismatch, skipping drop checks')
|
|
61
|
+
return {}
|
|
62
|
+
}
|
|
63
|
+
|
|
49
64
|
// Map responses to statuses
|
|
50
|
-
const results =
|
|
65
|
+
const results = Object.create(null)
|
|
51
66
|
txIds.forEach((txId, idx) => {
|
|
52
67
|
const rpcTx = responses[idx]
|
|
53
68
|
const txStatus = getTxStatus(rpcTx)
|
|
54
69
|
results[txId] = txStatus
|
|
55
70
|
|
|
56
71
|
// Log each tx status
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
72
|
+
switch (txStatus.status) {
|
|
73
|
+
case 'confirmed':
|
|
74
|
+
logger.info(
|
|
75
|
+
`tx ${txId} is confirmed on-chain (block ${txStatus.blockNumber}), skipping drop`
|
|
76
|
+
)
|
|
77
|
+
break
|
|
78
|
+
case 'pending':
|
|
79
|
+
logger.info(`tx ${txId} is still in mempool, skipping drop`)
|
|
80
|
+
break
|
|
81
|
+
case 'unknown':
|
|
82
|
+
logger.warn(`tx ${txId} could not be verified (RPC error entry), skipping drop`)
|
|
83
|
+
break
|
|
84
|
+
default:
|
|
85
|
+
logger.info(`tx ${txId} not found on-chain, marking as dropped`)
|
|
63
86
|
}
|
|
64
87
|
})
|
|
65
88
|
|
|
@@ -47,7 +47,7 @@ export const calculateRewardsFromStakeTx = ({ tx, currency }) => {
|
|
|
47
47
|
|
|
48
48
|
if (stakeTxContainsReward) {
|
|
49
49
|
const txAmount = stakedAmount.sub(tx.coinAmount.abs()).abs()
|
|
50
|
-
// eslint-disable-next-line @exodus/
|
|
50
|
+
// eslint-disable-next-line @exodus/hydra/no-param-reassign-prop-only -- TODO: Fix this the next time the file is edited.
|
|
51
51
|
return txAmount.toDefaultString()
|
|
52
52
|
}
|
|
53
53
|
}
|
|
@@ -29,7 +29,7 @@ export const handleBroadcastError = async (
|
|
|
29
29
|
isNonceTooLow || errorInfo.reason === EVM_ERROR_REASONS.transactionUnderpriced.reason
|
|
30
30
|
|
|
31
31
|
if (errorInfo.reason === EVM_ERROR_REASONS.alreadyKnown.reason) {
|
|
32
|
-
console.
|
|
32
|
+
console.debug('tx already broadcast')
|
|
33
33
|
return { shouldRetry: false }
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -44,7 +44,7 @@ export const handleBroadcastError = async (
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
if (txAlreadyExists) {
|
|
47
|
-
console.
|
|
47
|
+
console.debug('tx already broadcast')
|
|
48
48
|
return { shouldRetry: false }
|
|
49
49
|
}
|
|
50
50
|
|
package/src/tx-send/tx-send.js
CHANGED
|
@@ -50,7 +50,7 @@ const txSendFactory = ({ assetClientInterface, createTx }) => {
|
|
|
50
50
|
})
|
|
51
51
|
|
|
52
52
|
if (shouldRetry) {
|
|
53
|
-
console.
|
|
53
|
+
console.debug('trying to send again...') // inject logger factory from platform
|
|
54
54
|
|
|
55
55
|
// let's try to fix the nonce issue
|
|
56
56
|
let newNonce = await baseAsset.getNonce({
|
|
@@ -73,14 +73,14 @@ const txSendFactory = ({ assetClientInterface, createTx }) => {
|
|
|
73
73
|
//
|
|
74
74
|
// TODO: should return an unsignedTx -> i.e. completely replicate the transaction
|
|
75
75
|
if (unsignedTx.txData.transactionBuffer) {
|
|
76
|
-
// eslint-disable-next-line @exodus/
|
|
76
|
+
// eslint-disable-next-line @exodus/hydra/no-param-reassign-prop-only
|
|
77
77
|
unsignedTx.txData.transactionBuffer = updateNonce({
|
|
78
78
|
chainId: baseAsset.chainId,
|
|
79
79
|
newNonce,
|
|
80
80
|
unsignedTx,
|
|
81
81
|
})
|
|
82
82
|
} else {
|
|
83
|
-
// eslint-disable-next-line @exodus/
|
|
83
|
+
// eslint-disable-next-line @exodus/hydra/no-param-reassign-prop-only
|
|
84
84
|
unsignedTx.txData.nonce = newNonce
|
|
85
85
|
}
|
|
86
86
|
|