@exodus/ethereum-api 9.0.0-alpha.3 → 9.0.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 +2121 -0
- package/LICENSE +7 -0
- package/README.md +124 -0
- package/package.json +34 -33
- package/src/address-has-history.js +6 -2
- package/src/allowance/constants.js +4 -5
- package/src/allowance/index.js +159 -40
- package/src/check-tx/create-assess-transaction.js +23 -0
- package/src/check-tx/create-check-tx.js +104 -0
- package/src/check-tx/index.js +1 -0
- package/src/create-asset-plugin-factory.js +115 -0
- package/src/create-asset-utils.js +520 -0
- package/src/create-asset.js +272 -97
- package/src/create-token-factory.js +47 -17
- package/src/custom-fees.js +57 -0
- package/src/eip7702-whitelist.js +62 -0
- package/src/ens/index.js +12 -17
- package/src/error-wrapper.js +383 -0
- package/src/eth-like-util.js +172 -33
- package/src/exodus-eth-server/api-coin-nodes.js +27 -196
- package/src/exodus-eth-server/api.js +23 -14
- package/src/exodus-eth-server/clarity-v2.js +255 -0
- package/src/exodus-eth-server/clarity.js +67 -265
- package/src/exodus-eth-server/errors.js +40 -0
- package/src/exodus-eth-server/eth-like-server.js +385 -0
- package/src/exodus-eth-server/fetch-json.js +48 -0
- package/src/exodus-eth-server/index.js +77 -0
- package/src/exodus-eth-server/utils.js +31 -0
- package/src/exodus-eth-server/ws-gateway.js +306 -0
- package/src/exodus-eth-server/ws.js +1 -2
- package/src/fee-data/EthLikeFeeData.js +81 -0
- package/src/fee-data/index.js +54 -0
- package/src/fee-utils.js +229 -0
- package/src/gas-estimation.js +183 -66
- package/src/get-balance-for-address.js +1 -1
- package/src/get-balances.js +285 -27
- package/src/get-fee.js +92 -56
- package/src/get-historical-balance.js +58 -0
- package/src/hooks/index.js +1 -1
- package/src/hooks/monitor.js +182 -43
- package/src/hooks/staking-freshness.js +58 -0
- package/src/index.js +118 -12
- package/src/move-funds.js +153 -0
- package/src/multicall3/index.js +212 -0
- package/src/multisig-address.js +146 -0
- package/src/nft-utils.js +60 -0
- package/src/number-utils.js +34 -2
- package/src/optimism-gas/index.js +13 -5
- package/src/send-validations.js +58 -0
- package/src/server-based-fee-monitor.js +12 -19
- package/src/simulate-tx/fetch-tx-preview.js +0 -2
- package/src/simulate-tx/index.js +2 -2
- package/src/simulate-tx/simulate-eth-tx.js +24 -11
- package/src/simulation/common.js +34 -0
- package/src/simulation/create-simulate-message.js +49 -0
- package/src/simulation/create-simulate-transactions.js +108 -0
- package/src/simulation/estimate-fee.js +14 -0
- package/src/simulation/estimate-simple-transfer.js +15 -0
- package/src/simulation/get-message-type.js +18 -0
- package/src/simulation/simulate-message-api.js +68 -0
- package/src/simulation/simulate-transactions-api.js +266 -0
- package/src/simulation/simulate-transactions.js +16 -0
- package/src/simulation/transactions.js +52 -0
- package/src/simulation/try-estimating-changes-locally.js +26 -0
- package/src/staking/api/index.js +43 -0
- package/src/staking/cached-read.js +33 -0
- package/src/staking/ethereum/api.js +166 -34
- package/src/staking/ethereum/deps.js +37 -0
- package/src/staking/ethereum/everstake.js +113 -0
- package/src/staking/ethereum/index.js +4 -3
- package/src/staking/ethereum/service.js +682 -181
- package/src/staking/ethereum/staking-utils.js +275 -4
- package/src/staking/fantom-staking.js +9 -9
- package/src/staking/index.js +4 -4
- package/src/staking/matic/addresses.js +2 -0
- package/src/staking/matic/api.js +69 -49
- package/src/staking/matic/index.js +3 -3
- package/src/staking/matic/matic-staking-utils.js +243 -5
- package/src/staking/matic/matic-staking.md +53 -0
- package/src/staking/staking-provider-client.js +1 -1
- package/src/staking/utils/index.js +39 -0
- package/src/tx-create.js +666 -0
- package/src/tx-log/clarity-monitor.js +273 -103
- package/src/tx-log/clarity-truncated-history-monitor.js +109 -0
- package/src/tx-log/clarity-truncated-history-monitor.md +219 -0
- package/src/tx-log/clarity-utils/absolute.js +96 -0
- package/src/tx-log/clarity-utils/get-log-items-from-server-tx.js +94 -31
- package/src/tx-log/clarity-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
- package/src/tx-log/clarity-utils/index.js +9 -3
- package/src/tx-log/clarity-utils/is-spam-pending-tx.js +13 -0
- package/src/tx-log/clarity-utils/normalize-transactions-response.js +65 -0
- package/src/tx-log/ethereum-monitor.js +93 -47
- package/src/tx-log/ethereum-no-history-monitor.js +52 -41
- package/src/tx-log/get-optimistic-txlog-effects.js +379 -0
- package/src/tx-log/index.js +5 -3
- package/src/tx-log/monitor-utils/check-pending-transactions.js +14 -5
- package/src/tx-log/monitor-utils/exclude-unchanged-token-balances.js +1 -1
- package/src/tx-log/monitor-utils/extract-balance-from-clarity-account-info.js +108 -0
- package/src/tx-log/monitor-utils/get-balance-updates.js +44 -0
- package/src/tx-log/monitor-utils/get-batched-rpc-balances.js +47 -0
- package/src/tx-log/monitor-utils/get-current-blacklist-status.js +38 -0
- package/src/tx-log/monitor-utils/get-current-eip7702-delegation.js +79 -0
- package/src/tx-log/monitor-utils/get-derive-transactions-to-check.js +10 -5
- package/src/tx-log/monitor-utils/get-fee-amount.js +1 -1
- package/src/tx-log/monitor-utils/get-history-from-server.js +1 -1
- package/src/tx-log/monitor-utils/get-log-items-from-server-tx.js +75 -33
- package/src/tx-log/monitor-utils/get-names-of-tokens-transferred-by-server-tx.js +3 -2
- package/src/tx-log/monitor-utils/get-raw-transfer-amount.js +29 -0
- package/src/tx-log/monitor-utils/get-transfers-by-token-name.js +3 -2
- package/src/tx-log/monitor-utils/get-value-of-transfers.js +10 -12
- package/src/tx-log/monitor-utils/index.js +12 -7
- 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 +90 -0
- package/src/tx-log-staking-processor/asset-staking-tx-data.js +82 -24
- package/src/tx-log-staking-processor/index.js +28 -14
- package/src/tx-log-staking-processor/utils.js +21 -20
- package/src/tx-send/broadcast-error-handler.js +63 -0
- package/src/tx-send/index.js +1 -2
- package/src/tx-send/nonce-utils.js +97 -0
- package/src/tx-send/tx-send.js +94 -288
- package/src/tx-sign/index.js +17 -0
- package/src/tx-type/index.js +280 -0
- package/src/watch-tx.js +36 -0
- package/src/web3/createSimulateMessage.js +3 -2
- package/src/web3/createSimulateTransactions.js +11 -0
- package/src/web3/index.js +3 -1
- package/src/etherscan/account.js +0 -49
- package/src/etherscan/index.js +0 -28
- package/src/etherscan/logs.js +0 -16
- package/src/etherscan/proxy.js +0 -47
- package/src/etherscan/request.js +0 -26
- package/src/etherscan/ws.js +0 -88
- package/src/exodus-eth-server/server-factory.js +0 -28
- package/src/exodus-eth-server/server-map.js +0 -21
- package/src/get-fee-async.js +0 -97
- package/src/staking/matic/service.js +0 -437
- package/src/staking-api.js +0 -5
- package/src/tx-log/clarity-utils/get-derive-data-needed-for-tick.js +0 -12
- package/src/tx-log-staking-processor/get-asset-tx-amount.js +0 -26
- package/src/tx-send/get-fee-info.js +0 -41
- package/src/websocket/index.android.js +0 -2
- package/src/websocket/index.ios.js +0 -2
- package/src/websocket/index.js +0 -21
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { getEIP7702Delegation } from '../../eth-like-util.js'
|
|
2
|
+
|
|
3
|
+
const NOT_DELEGATED = {
|
|
4
|
+
isDelegated: false,
|
|
5
|
+
delegatedAddress: null,
|
|
6
|
+
delegatedName: null,
|
|
7
|
+
isWhitelisted: null,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function hasDelegationChanged(currentDelegation, newDelegation) {
|
|
11
|
+
return (
|
|
12
|
+
currentDelegation?.isDelegated !== newDelegation.isDelegated ||
|
|
13
|
+
currentDelegation?.delegatedAddress !== newDelegation.delegatedAddress ||
|
|
14
|
+
currentDelegation?.delegatedName !== newDelegation.delegatedName ||
|
|
15
|
+
currentDelegation?.isWhitelisted !== newDelegation.isWhitelisted
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Checks if the address has an EIP-7702 delegation and returns the delegation state.
|
|
21
|
+
* Returns the new state if changed, or undefined if unchanged (monitors treat undefined as "no write").
|
|
22
|
+
* On error, conservatively returns NOT_DELEGATED to avoid showing stale isDelegated: true,
|
|
23
|
+
* but only if the current state actually needs clearing.
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} params
|
|
26
|
+
* @param {Object} params.server - The server instance to use for getCode
|
|
27
|
+
* @param {string} params.address - The wallet address to check
|
|
28
|
+
* @param {Array<{address: string, name: string}>} [params.eip7702Supported] - Whitelist of trusted delegation targets.
|
|
29
|
+
* If not an array, the check is skipped entirely (chain does not support EIP-7702).
|
|
30
|
+
* An empty array means the check runs but every delegation will be isWhitelisted: false.
|
|
31
|
+
* @param {Object} [params.currentDelegation] - The current delegation state from accountState
|
|
32
|
+
* @param {Object} [params.logger] - Optional logger for warnings
|
|
33
|
+
* @returns {Promise<Object|undefined>} The new delegation state, or undefined if unchanged
|
|
34
|
+
*/
|
|
35
|
+
export async function getCurrentEIP7702Delegation({
|
|
36
|
+
server,
|
|
37
|
+
address,
|
|
38
|
+
eip7702Supported,
|
|
39
|
+
currentDelegation,
|
|
40
|
+
logger,
|
|
41
|
+
}) {
|
|
42
|
+
// Non-array (undefined, false, etc.) → chain doesn't support EIP-7702, skip entirely
|
|
43
|
+
if (!Array.isArray(eip7702Supported)) {
|
|
44
|
+
return hasDelegationChanged(currentDelegation, NOT_DELEGATED) ? NOT_DELEGATED : undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const result = await getEIP7702Delegation({ address, server })
|
|
49
|
+
|
|
50
|
+
if (!result.isDelegated) {
|
|
51
|
+
return hasDelegationChanged(currentDelegation, NOT_DELEGATED) ? NOT_DELEGATED : undefined
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// [] → check runs but nothing is trusted; populated array → whitelist check
|
|
55
|
+
const matchedDelegation = eip7702Supported.find(
|
|
56
|
+
({ address }) => address.toLowerCase() === result.delegatedAddress.toLowerCase()
|
|
57
|
+
)
|
|
58
|
+
const isWhitelisted = Boolean(matchedDelegation)
|
|
59
|
+
|
|
60
|
+
const newDelegation = {
|
|
61
|
+
isDelegated: true,
|
|
62
|
+
delegatedAddress: result.delegatedAddress,
|
|
63
|
+
delegatedName: matchedDelegation?.name ?? null,
|
|
64
|
+
isWhitelisted,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return hasDelegationChanged(currentDelegation, newDelegation) ? newDelegation : undefined
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (logger) {
|
|
70
|
+
logger.warn('Failed to check EIP-7702 delegation:', error)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// On error, conservatively clear delegation state — only hard RPC confirmation
|
|
74
|
+
// should result in isDelegated: true being shown to the user.
|
|
75
|
+
return hasDelegationChanged(currentDelegation, NOT_DELEGATED) ? NOT_DELEGATED : undefined
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default getCurrentEIP7702Delegation
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import ms from 'ms'
|
|
2
|
-
import getSenderNonceKey from './get-sender-nonce-key'
|
|
3
1
|
import { SynchronizedTime } from '@exodus/basic-utils'
|
|
2
|
+
import ms from 'ms'
|
|
3
|
+
|
|
4
|
+
import getSenderNonceKey from './get-sender-nonce-key.js'
|
|
4
5
|
|
|
5
6
|
export const UNCONFIRMED_TX_LIMIT = ms('5m')
|
|
6
7
|
|
|
@@ -9,6 +10,7 @@ const mapToObject = (map) => Object.fromEntries(map.entries()) // only for strin
|
|
|
9
10
|
export default function getDeriveTransactionsToCheck({ getTxLog }) {
|
|
10
11
|
return async ({ assetName: _assetName, walletAccount, tokens, ourWalletAddress }) => {
|
|
11
12
|
const pendingTransactionsToCheck = new Map()
|
|
13
|
+
const recentPendingTransactions = new Map()
|
|
12
14
|
const pendingTransactionsGroupedByAddressAndNonce = new Map()
|
|
13
15
|
const simulatedTransactions = new Map()
|
|
14
16
|
const now = SynchronizedTime.now()
|
|
@@ -18,11 +20,13 @@ export default function getDeriveTransactionsToCheck({ getTxLog }) {
|
|
|
18
20
|
for (const tx of txSet) {
|
|
19
21
|
// ERC20 sends have an entry in ETH and one in the ERC20 log so we just want the ETH one
|
|
20
22
|
if (
|
|
21
|
-
!pendingTransactionsToCheck.has(tx.txId) &&
|
|
22
23
|
tx.pending &&
|
|
23
|
-
|
|
24
|
+
!pendingTransactionsToCheck.has(tx.txId) &&
|
|
25
|
+
!recentPendingTransactions.has(tx.txId)
|
|
24
26
|
) {
|
|
25
|
-
|
|
27
|
+
const isRecent = now - tx.date.getTime() <= UNCONFIRMED_TX_LIMIT
|
|
28
|
+
const map = isRecent ? recentPendingTransactions : pendingTransactionsToCheck
|
|
29
|
+
map.set(tx.txId, { tx, assetName })
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
if (tx.meta.simulated) simulatedTransactions.set(tx.txId, tx)
|
|
@@ -44,6 +48,7 @@ export default function getDeriveTransactionsToCheck({ getTxLog }) {
|
|
|
44
48
|
|
|
45
49
|
return {
|
|
46
50
|
pendingTransactionsToCheck: mapToObject(pendingTransactionsToCheck),
|
|
51
|
+
recentPendingTransactions: mapToObject(recentPendingTransactions),
|
|
47
52
|
pendingTransactionsGroupedByAddressAndNonce: mapToObject(
|
|
48
53
|
pendingTransactionsGroupedByAddressAndNonce
|
|
49
54
|
),
|
|
@@ -1,14 +1,38 @@
|
|
|
1
|
-
import getValueOfTransfers from './get-value-of-transfers'
|
|
2
|
-
import getTransfersByTokenName from './get-transfers-by-token-name'
|
|
3
|
-
import getNamesOfTokensTransferredByServerTx from './get-names-of-tokens-transferred-by-server-tx'
|
|
4
|
-
import getFeeAmount from './get-fee-amount'
|
|
5
|
-
import isConsideredSentTokenTx from './is-considered-sent-token-tx'
|
|
6
|
-
import isConfirmedServerTx from './is-confirmed-server-tx'
|
|
7
1
|
import lodash from 'lodash'
|
|
8
2
|
|
|
3
|
+
import getFeeAmount from './get-fee-amount.js'
|
|
4
|
+
import getNamesOfTokensTransferredByServerTx from './get-names-of-tokens-transferred-by-server-tx.js'
|
|
5
|
+
import getRawTransferAmount from './get-raw-transfer-amount.js'
|
|
6
|
+
import getTransfersByTokenName from './get-transfers-by-token-name.js'
|
|
7
|
+
import getValueOfTransfers from './get-value-of-transfers.js'
|
|
8
|
+
import isConfirmedServerTx from './is-confirmed-server-tx.js'
|
|
9
|
+
import isConsideredSentTokenTx from './is-considered-sent-token-tx.js'
|
|
10
|
+
|
|
9
11
|
// This function takes a server transaction object fetched from magnifier,
|
|
10
12
|
// and transforms it into Tx models to update the exodus state.
|
|
11
13
|
|
|
14
|
+
const tryFindExternalRecipient = (transfers, ourWalletAddress) =>
|
|
15
|
+
transfers.find(({ to }) => to !== ourWalletAddress)?.to || ourWalletAddress
|
|
16
|
+
|
|
17
|
+
function isSelfSendTx({
|
|
18
|
+
coinAmount,
|
|
19
|
+
ourWalletWasSender,
|
|
20
|
+
sendingTransferPresent,
|
|
21
|
+
receivingTransferPresent,
|
|
22
|
+
}) {
|
|
23
|
+
return (
|
|
24
|
+
coinAmount.isZero && sendingTransferPresent && receivingTransferPresent && ourWalletWasSender
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isNftTransfer({ serverTx, ourWalletAddress }) {
|
|
29
|
+
if (!Array.isArray(serverTx.erc721) || serverTx.erc721.length === 0) return false
|
|
30
|
+
|
|
31
|
+
return serverTx.erc721.some(
|
|
32
|
+
(transfer) => transfer.to === ourWalletAddress || transfer.from === ourWalletAddress
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
12
36
|
export default function getLogItemsFromServerTx({
|
|
13
37
|
serverTx,
|
|
14
38
|
asset,
|
|
@@ -49,12 +73,36 @@ export default function getLogItemsFromServerTx({
|
|
|
49
73
|
|
|
50
74
|
if (sendingTransferPresent || receivingTransferPresent || nftTransferPresent) {
|
|
51
75
|
const coinAmount = getValueOfTransfers(ourWalletAddress, asset, ethereumTransfers)
|
|
76
|
+
// `rawCoinAmount` mirrors the field written by `getOptimisticTxLogEffects`
|
|
77
|
+
// and is consumed by the bump path (`tx-create.js`) to recover the
|
|
78
|
+
// user-signed on-chain `value` when accelerating a self-send (where
|
|
79
|
+
// `coinAmount` is zero). For the base-asset entry the raw amount is the
|
|
80
|
+
// top-level `value` field of the tx itself, not the sum of internal
|
|
81
|
+
// transfers — that's what the user signed and what we must restore on
|
|
82
|
+
// bump.
|
|
83
|
+
const rawCoinAmount = getRawTransferAmount({
|
|
84
|
+
ourWalletAddress,
|
|
85
|
+
asset,
|
|
86
|
+
transfers: [serverTx],
|
|
87
|
+
}).toBaseString()
|
|
52
88
|
const selfSend = isSelfSendTx({
|
|
53
89
|
coinAmount,
|
|
54
90
|
ourWalletWasSender,
|
|
55
91
|
sendingTransferPresent,
|
|
56
92
|
receivingTransferPresent,
|
|
57
93
|
})
|
|
94
|
+
let sent
|
|
95
|
+
if (sendingTransferPresent) {
|
|
96
|
+
sent = serverTx.internal
|
|
97
|
+
.map(({ from, to, value }) => {
|
|
98
|
+
if (from === ourWalletAddress) {
|
|
99
|
+
const amount = asset.currency.baseUnit(value).toDefaultString({ unit: true })
|
|
100
|
+
return { address: to, amount }
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
}
|
|
105
|
+
|
|
58
106
|
logItemsForServerTxEntries.push([
|
|
59
107
|
asset.name,
|
|
60
108
|
{
|
|
@@ -65,7 +113,9 @@ export default function getLogItemsFromServerTx({
|
|
|
65
113
|
data,
|
|
66
114
|
nonce,
|
|
67
115
|
gasLimit,
|
|
116
|
+
rawCoinAmount,
|
|
68
117
|
...methodId,
|
|
118
|
+
...(sent?.length > 0 ? { sent } : undefined),
|
|
69
119
|
},
|
|
70
120
|
...(ourWalletWasSender
|
|
71
121
|
? { from: [], to: toAddress, feeAmount, feeCoinName: asset.feeAsset.name }
|
|
@@ -94,11 +144,25 @@ export default function getLogItemsFromServerTx({
|
|
|
94
144
|
|
|
95
145
|
const token = assets[tokenName]
|
|
96
146
|
const tokenTransferToAddress = tryFindExternalRecipient(tokenTransfers, ourWalletAddress)
|
|
97
|
-
|
|
147
|
+
// Transfers from this reader's server source are tagged with
|
|
148
|
+
// `events: true` once the tx is included in a block (event-log derived)
|
|
149
|
+
// and `events: false` while still pending (tx-data derived). Only one
|
|
150
|
+
// flavor is real for a given lifecycle state; filtering avoids
|
|
151
|
+
// double-counting if both ever coexist for the same transfer. Both
|
|
152
|
+
// `coinAmount` and `rawCoinAmount` must see the same set so they stay
|
|
153
|
+
// consistent.
|
|
154
|
+
const filteredTokenTransfers = lodash.filter(tokenTransfers, {
|
|
155
|
+
events: confirmations > 0,
|
|
156
|
+
})
|
|
157
|
+
const coinAmount = getValueOfTransfers(ourWalletAddress, token, filteredTokenTransfers)
|
|
158
|
+
// See base-entry comment above; for token entries the raw amount is the
|
|
159
|
+
// absolute total tokens our wallet moved in this tx (sum of outgoing, or
|
|
160
|
+
// incoming for receive-only entries).
|
|
161
|
+
const rawCoinAmount = getRawTransferAmount({
|
|
98
162
|
ourWalletAddress,
|
|
99
|
-
token,
|
|
100
|
-
|
|
101
|
-
)
|
|
163
|
+
asset: token,
|
|
164
|
+
transfers: filteredTokenTransfers,
|
|
165
|
+
}).toBaseString()
|
|
102
166
|
const tokenFromAddresses = lodash.uniq(
|
|
103
167
|
tokenTransfers.filter(({ to }) => to === ourWalletAddress).map(({ from }) => from)
|
|
104
168
|
)
|
|
@@ -122,7 +186,7 @@ export default function getLogItemsFromServerTx({
|
|
|
122
186
|
...logItemCommonProperties,
|
|
123
187
|
coinAmount,
|
|
124
188
|
coinName: tokenName,
|
|
125
|
-
data: { data, nonce, gasLimit, ...methodId },
|
|
189
|
+
data: { data, nonce, gasLimit, rawCoinAmount, ...methodId },
|
|
126
190
|
...(isConsideredSent
|
|
127
191
|
? { from: [], to: tokenTransferToAddress, feeAmount, feeCoinName: asset.feeAsset.name }
|
|
128
192
|
: { from: tokenFromAddresses }),
|
|
@@ -137,25 +201,3 @@ export default function getLogItemsFromServerTx({
|
|
|
137
201
|
|
|
138
202
|
return Object.fromEntries(logItemsForServerTxEntries)
|
|
139
203
|
}
|
|
140
|
-
|
|
141
|
-
const tryFindExternalRecipient = (transfers, ourWalletAddress) =>
|
|
142
|
-
transfers.find(({ to }) => to !== ourWalletAddress)?.to || ourWalletAddress
|
|
143
|
-
|
|
144
|
-
function isSelfSendTx({
|
|
145
|
-
coinAmount,
|
|
146
|
-
ourWalletWasSender,
|
|
147
|
-
sendingTransferPresent,
|
|
148
|
-
receivingTransferPresent,
|
|
149
|
-
}) {
|
|
150
|
-
return (
|
|
151
|
-
coinAmount.isZero && sendingTransferPresent && receivingTransferPresent && ourWalletWasSender
|
|
152
|
-
)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function isNftTransfer({ serverTx, ourWalletAddress }) {
|
|
156
|
-
if (!Array.isArray(serverTx.erc721) || serverTx.erc721.length === 0) return false
|
|
157
|
-
|
|
158
|
-
return serverTx.erc721.some(
|
|
159
|
-
(transfer) => transfer.to === ourWalletAddress || transfer.from === ourWalletAddress
|
|
160
|
-
)
|
|
161
|
-
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import lodash from 'lodash'
|
|
2
|
-
import getValueOfTransfers from './get-value-of-transfers'
|
|
3
1
|
import { FEE_PAYMENT_PREFIX } from '@exodus/ethereum-lib'
|
|
2
|
+
import lodash from 'lodash'
|
|
3
|
+
|
|
4
|
+
import getValueOfTransfers from './get-value-of-transfers.js'
|
|
4
5
|
|
|
5
6
|
// For ETH transactions, we store an array of token names inside
|
|
6
7
|
// the TX model. This array is used to display text in the UI for
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Returns the absolute raw transferred amount for `ourWalletAddress`, in
|
|
2
|
+
// `asset` base units, as a NumberUnit. Unlike `getValueOfTransfers` (which
|
|
3
|
+
// returns the signed net delta), this preserves the raw transfer value even
|
|
4
|
+
// when sender and receiver net to zero — e.g. self-sends. Used by tx-log
|
|
5
|
+
// writers to persist `data.rawCoinAmount` so the bump path can reconstruct
|
|
6
|
+
// the original on-chain value when accelerating a self-send.
|
|
7
|
+
//
|
|
8
|
+
// Each transfer is expected to have the shape `{ from, to, value }`. For the
|
|
9
|
+
// base-asset entry, pass `[serverTx]` (the tx itself has the same shape at
|
|
10
|
+
// its top level); for token entries, pass the array of token transfers for
|
|
11
|
+
// that token.
|
|
12
|
+
//
|
|
13
|
+
// Outgoing transfers (from === ourWalletAddress) take precedence: when our
|
|
14
|
+
// wallet sent anything, the raw amount is the sum of what we sent. We fall
|
|
15
|
+
// back to incoming so receiver-only entries still report a non-zero raw
|
|
16
|
+
// amount for consistency, even though the bump path only consults it on the
|
|
17
|
+
// sender side.
|
|
18
|
+
|
|
19
|
+
export default function getRawTransferAmount({ ourWalletAddress, asset, transfers }) {
|
|
20
|
+
const outgoing = transfers
|
|
21
|
+
.filter(({ from }) => from === ourWalletAddress)
|
|
22
|
+
.reduce((sum, { value }) => sum.add(asset.currency.baseUnit(value)), asset.currency.ZERO)
|
|
23
|
+
|
|
24
|
+
if (!outgoing.isZero) return outgoing
|
|
25
|
+
|
|
26
|
+
return transfers
|
|
27
|
+
.filter(({ to }) => to === ourWalletAddress)
|
|
28
|
+
.reduce((sum, { value }) => sum.add(asset.currency.baseUnit(value)), asset.currency.ZERO)
|
|
29
|
+
}
|
|
@@ -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
|
}
|
|
@@ -4,19 +4,17 @@
|
|
|
4
4
|
// array is a transfer for that asset. If we sent more than we received, the value is negative.
|
|
5
5
|
|
|
6
6
|
export default function getValueOfTransfers(ourWalletAddress, asset, transfers) {
|
|
7
|
-
return transfers
|
|
8
|
-
|
|
9
|
-
const transferAmount = asset.currency.baseUnit(value)
|
|
7
|
+
return transfers.reduce((balanceDifference, { from, to, value }) => {
|
|
8
|
+
const transferAmount = asset.currency.baseUnit(value)
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
if (from === ourWalletAddress) {
|
|
11
|
+
balanceDifference = balanceDifference.sub(transferAmount)
|
|
12
|
+
}
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
if (to === ourWalletAddress) {
|
|
15
|
+
balanceDifference = balanceDifference.add(transferAmount)
|
|
16
|
+
}
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.to(asset.ticker)
|
|
18
|
+
return balanceDifference
|
|
19
|
+
}, asset.currency.ZERO)
|
|
22
20
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
export { default as getDeriveDataNeededForTick } from './get-derive-data-needed-for-tick'
|
|
1
|
+
export { default as getDeriveDataNeededForTick } from './get-derive-data-needed-for-tick.js'
|
|
2
2
|
|
|
3
|
-
export { default as getAllLogItemsByAsset } from './get-all-log-items-by-asset'
|
|
4
|
-
export { default as getLogItemsFromServerTx } from './get-log-items-from-server-tx'
|
|
5
|
-
export { default as getHistoryFromServer } from './get-history-from-server'
|
|
6
|
-
export { default as checkPendingTransactions } from './check-pending-transactions'
|
|
7
|
-
export { default as getDeriveTransactionsToCheck } from './get-derive-transactions-to-check'
|
|
8
|
-
export
|
|
3
|
+
export { default as getAllLogItemsByAsset } from './get-all-log-items-by-asset.js'
|
|
4
|
+
export { default as getLogItemsFromServerTx } from './get-log-items-from-server-tx.js'
|
|
5
|
+
export { default as getHistoryFromServer } from './get-history-from-server.js'
|
|
6
|
+
export { default as checkPendingTransactions } from './check-pending-transactions.js'
|
|
7
|
+
export { default as getDeriveTransactionsToCheck } from './get-derive-transactions-to-check.js'
|
|
8
|
+
export { default as getCurrentEIP7702Delegation } from './get-current-eip7702-delegation.js'
|
|
9
|
+
export { default as getCurrentBlackListStatus } from './get-current-blacklist-status.js'
|
|
10
|
+
export { default as verifyRpcPendingTxStatusBatch } from './verify-pending-tx-status-rpc.js'
|
|
11
|
+
export { default as isUnverifiablePendingTxExpired } from './is-unverifiable-pending-tx-expired.js'
|
|
12
|
+
export { extractBalanceFromClarityAccountInfo } from './extract-balance-from-clarity-account-info.js'
|
|
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
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Determines the status of an RPC transaction response.
|
|
3
|
+
*
|
|
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}}
|
|
10
|
+
*/
|
|
11
|
+
function getTxStatus(rpcTx) {
|
|
12
|
+
if (rpcTx === undefined) {
|
|
13
|
+
return { status: 'unknown' }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (rpcTx?.blockNumber) {
|
|
17
|
+
return { status: 'confirmed', blockNumber: parseInt(rpcTx.blockNumber, 16) }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (rpcTx) {
|
|
21
|
+
return { status: 'pending' }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return { status: 'dropped' }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Batch verify pending transactions against the RPC node before marking them as dropped.
|
|
29
|
+
*
|
|
30
|
+
* Any lag in the Clarity indexer can cause confirmed transactions to be incorrectly
|
|
31
|
+
* flagged as dropped if Clarity hasn't indexed their block yet.
|
|
32
|
+
* By checking the RPC directly, we avoid this race condition.
|
|
33
|
+
*
|
|
34
|
+
* Uses a single batch RPC call for efficiency and graceful error handling.
|
|
35
|
+
*
|
|
36
|
+
* @param {Object} params
|
|
37
|
+
* @param {Object} params.server - RPC server used to verify pending transactions
|
|
38
|
+
* @param {Object} params.logger - Logger instance for info/debug output
|
|
39
|
+
* @param {string[]} params.txIds - Array of transaction IDs/hashes to verify
|
|
40
|
+
* @returns {Promise<Object<string, {status: 'confirmed' | 'pending' | 'dropped' | 'unknown', blockNumber?: number}>>}
|
|
41
|
+
*/
|
|
42
|
+
export default async function verifyRpcPendingTxStatusBatch({ server, logger, txIds }) {
|
|
43
|
+
if (txIds.length === 0) return {}
|
|
44
|
+
|
|
45
|
+
// Build batch request
|
|
46
|
+
const requests = txIds.map((txId) => server.getTransactionByHashRequest(txId))
|
|
47
|
+
|
|
48
|
+
let responses
|
|
49
|
+
try {
|
|
50
|
+
// Tolerant parsing: one bad hash must not void the verdicts for every other tx
|
|
51
|
+
responses = await server.sendBatchRequest(requests, { tolerant: true })
|
|
52
|
+
} catch (error) {
|
|
53
|
+
// If batch request fails, skip all drop checks (safe default)
|
|
54
|
+
logger.warn('Batch RPC request failed, skipping drop checks', error)
|
|
55
|
+
return {}
|
|
56
|
+
}
|
|
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
|
+
|
|
64
|
+
// Map responses to statuses
|
|
65
|
+
const results = Object.create(null)
|
|
66
|
+
txIds.forEach((txId, idx) => {
|
|
67
|
+
const rpcTx = responses[idx]
|
|
68
|
+
const txStatus = getTxStatus(rpcTx)
|
|
69
|
+
results[txId] = txStatus
|
|
70
|
+
|
|
71
|
+
// Log each tx status
|
|
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`)
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
return results
|
|
90
|
+
}
|
|
@@ -3,57 +3,115 @@ import {
|
|
|
3
3
|
isEthereumDelegate,
|
|
4
4
|
isEthereumUndelegate,
|
|
5
5
|
isEthereumUndelegatePending,
|
|
6
|
-
} from '../staking/ethereum/staking-utils'
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
} from '../staking/ethereum/staking-utils.js'
|
|
7
|
+
import {
|
|
8
|
+
isPolygonClaimUndelegate,
|
|
9
|
+
isPolygonDelegate,
|
|
10
|
+
isPolygonUndelegate,
|
|
11
|
+
} from '../staking/matic/index.js'
|
|
12
|
+
import {
|
|
13
|
+
calculateRewardsFromStakeTx,
|
|
14
|
+
decodeEthLikeStakingTxInputAmount,
|
|
15
|
+
decodePolygonStakingTxInputAmount,
|
|
16
|
+
} from './utils.js'
|
|
11
17
|
|
|
12
|
-
const
|
|
18
|
+
const getEthereumStakingTxData = ({ tx, currency }) => {
|
|
13
19
|
if (
|
|
14
20
|
['delegate', 'undelegatePending', 'undelegate', 'claimUndelegate'].some(
|
|
15
21
|
(stakeTx) => tx.data?.[stakeTx]
|
|
16
|
-
)
|
|
17
|
-
|
|
22
|
+
) &&
|
|
23
|
+
tx.coinAmount.isZero
|
|
24
|
+
) {
|
|
18
25
|
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const txAmount = tx.coinAmount.toDefaultString()
|
|
19
29
|
|
|
20
30
|
if (isEthereumDelegate(tx)) {
|
|
21
|
-
return {
|
|
31
|
+
return {
|
|
32
|
+
coinAmount: currency.ZERO,
|
|
33
|
+
data: { delegate: txAmount, txAmount },
|
|
34
|
+
}
|
|
22
35
|
}
|
|
23
36
|
|
|
24
37
|
// undelegate must be taken in consideration, if unstaked ETH is still
|
|
25
38
|
// in the pool queue, undelgate transfers staked funds back inmediatly to the user
|
|
26
|
-
if (isEthereumUndelegatePending(tx))
|
|
39
|
+
if (isEthereumUndelegatePending(tx)) {
|
|
40
|
+
const undelegatePending = currency
|
|
41
|
+
.baseUnit(decodeEthLikeStakingTxInputAmount(tx))
|
|
42
|
+
.toDefaultString()
|
|
27
43
|
return {
|
|
28
|
-
|
|
44
|
+
coinAmount: currency.ZERO,
|
|
45
|
+
data: { undelegatePending, txAmount },
|
|
29
46
|
}
|
|
47
|
+
}
|
|
30
48
|
|
|
31
49
|
if (isEthereumUndelegate(tx)) {
|
|
32
|
-
|
|
50
|
+
const undelegate = currency.baseUnit(decodeEthLikeStakingTxInputAmount(tx)).toDefaultString()
|
|
51
|
+
return {
|
|
52
|
+
coinAmount: currency.ZERO,
|
|
53
|
+
data: { undelegate, txAmount },
|
|
54
|
+
}
|
|
33
55
|
}
|
|
34
56
|
|
|
35
57
|
// In the case of the ETH being actually staked and earning,
|
|
36
58
|
// unstake has a withdraw period, after that, unstaked can be claimed.
|
|
37
59
|
if (isEthereumClaimUndelegate(tx)) {
|
|
38
|
-
return {
|
|
60
|
+
return {
|
|
61
|
+
coinAmount: currency.ZERO,
|
|
62
|
+
data: { claimUndelegate: txAmount, txAmount },
|
|
63
|
+
}
|
|
39
64
|
}
|
|
40
65
|
}
|
|
41
66
|
|
|
42
|
-
const
|
|
43
|
-
if (tx.
|
|
67
|
+
const getPolygonStakingTxData = ({ tx, currency }) => {
|
|
68
|
+
if (tx.data?.claimUndelegate) return
|
|
69
|
+
|
|
70
|
+
if (['delegate', 'undelegate'].some((stakeTx) => tx.data?.[stakeTx]) && tx.coinAmount.isZero) {
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const txAmount = tx.coinAmount.toDefaultString()
|
|
44
75
|
|
|
45
76
|
if (isPolygonDelegate(tx)) {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return {
|
|
77
|
+
const delegate = currency.baseUnit(decodePolygonStakingTxInputAmount(tx)).toDefaultString()
|
|
78
|
+
// MATIC returned in unstake tx is always reward
|
|
79
|
+
const rewards = calculateRewardsFromStakeTx({ tx, currency })
|
|
80
|
+
return {
|
|
81
|
+
coinAmount: currency.ZERO,
|
|
82
|
+
data: {
|
|
83
|
+
delegate,
|
|
84
|
+
txAmount,
|
|
85
|
+
...(rewards ? { rewards } : {}),
|
|
86
|
+
},
|
|
87
|
+
}
|
|
50
88
|
}
|
|
51
89
|
|
|
52
|
-
if (
|
|
90
|
+
if (isPolygonUndelegate(tx)) {
|
|
91
|
+
const undelegate = currency.baseUnit(decodePolygonStakingTxInputAmount(tx)).toDefaultString()
|
|
92
|
+
// MATIC returned in unstake tx is always reward
|
|
93
|
+
const rewards = txAmount
|
|
94
|
+
return {
|
|
95
|
+
coinAmount: currency.ZERO,
|
|
96
|
+
data: { undelegate, txAmount, rewards },
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (isPolygonClaimUndelegate(tx)) {
|
|
101
|
+
return {
|
|
102
|
+
// NOTE: We intentionally omit for compatibility with `rx`:
|
|
103
|
+
// https://github.com/ExodusMovement/exodus-mobile/blob/229c9c0634af3e8e17eb1624019682c6fffe29bf/src/utils/getTxTag.js#L186
|
|
104
|
+
//
|
|
105
|
+
// coinAmount: currency.ZERO,
|
|
106
|
+
data: {
|
|
107
|
+
claimUndelegate: txAmount,
|
|
108
|
+
txAmount,
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
}
|
|
53
112
|
}
|
|
54
113
|
|
|
55
|
-
export const
|
|
56
|
-
polygon:
|
|
57
|
-
ethereum:
|
|
58
|
-
ethereumholesky: getEthereumStakeTxType,
|
|
114
|
+
export const assetStakingTxProps = {
|
|
115
|
+
polygon: getPolygonStakingTxData,
|
|
116
|
+
ethereum: getEthereumStakingTxData,
|
|
59
117
|
}
|