@exodus/ethereum-api 9.0.0-alpha.3 → 9.0.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 +2139 -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 +273 -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 +276 -103
- package/src/tx-log/clarity-truncated-history-monitor.js +130 -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,379 @@
|
|
|
1
|
+
import { SynchronizedTime } from '@exodus/basic-utils'
|
|
2
|
+
import NumberUnit from '@exodus/currency'
|
|
3
|
+
import {
|
|
4
|
+
getHighestIncentiveTxByNonce,
|
|
5
|
+
isEthereumLikeToken,
|
|
6
|
+
parseUnsignedTx,
|
|
7
|
+
} from '@exodus/ethereum-lib'
|
|
8
|
+
import { bufferToHex } from '@exodus/ethereumjs/util'
|
|
9
|
+
import assert from 'minimalistic-assert'
|
|
10
|
+
|
|
11
|
+
import { EthereumStaking } from '../staking/ethereum/api.js'
|
|
12
|
+
import {
|
|
13
|
+
ethereumUnstakeOptimisticSideEffectTxLogs,
|
|
14
|
+
ethereumUnstakePendingOptimisticSideEffectTxLogs,
|
|
15
|
+
isEthereumStakingPoolContract,
|
|
16
|
+
} from '../staking/ethereum/staking-utils.js'
|
|
17
|
+
import { MaticStakingApi } from '../staking/matic/api.js'
|
|
18
|
+
import {
|
|
19
|
+
DELEGATE,
|
|
20
|
+
maticDelegateOptimisticSideEffectTxLogs,
|
|
21
|
+
} from '../staking/matic/matic-staking-utils.js'
|
|
22
|
+
|
|
23
|
+
export const getOptimisticTxLogEffects = async ({
|
|
24
|
+
asset,
|
|
25
|
+
assetClientInterface,
|
|
26
|
+
bundleId,
|
|
27
|
+
confirmations = 0,
|
|
28
|
+
date = SynchronizedTime.now(),
|
|
29
|
+
fromAddress,
|
|
30
|
+
txId,
|
|
31
|
+
unsignedTx,
|
|
32
|
+
walletAccount,
|
|
33
|
+
}) => {
|
|
34
|
+
assert(asset, 'expected asset')
|
|
35
|
+
assert(assetClientInterface, 'expected assetClientInterface')
|
|
36
|
+
assert(Number.isInteger(confirmations), 'expected integer confirmations')
|
|
37
|
+
assert(typeof fromAddress === 'string', 'expected string fromAddress')
|
|
38
|
+
assert(txId, 'expected txId')
|
|
39
|
+
assert(unsignedTx, 'expected unsignedTx')
|
|
40
|
+
assert(walletAccount, 'expected walletAccount')
|
|
41
|
+
|
|
42
|
+
// this converts an transactionBuffer to values we can use when creating the tx logs
|
|
43
|
+
const parsedTx = parseUnsignedTx({ asset, unsignedTx })
|
|
44
|
+
|
|
45
|
+
const { nonce, to, txToAddress } = parsedTx
|
|
46
|
+
assert(Number.isInteger(nonce), 'expected integer nonce')
|
|
47
|
+
|
|
48
|
+
const amount = parsedTx.amount || asset.currency.ZERO
|
|
49
|
+
assert(amount instanceof NumberUnit, 'expected NumberUnit amount')
|
|
50
|
+
|
|
51
|
+
const feeAmount = parsedTx.fee
|
|
52
|
+
assert(feeAmount instanceof NumberUnit, 'expected feeAmount')
|
|
53
|
+
|
|
54
|
+
const txValue = parsedTx.value
|
|
55
|
+
assert(txValue instanceof NumberUnit, 'expected txValue')
|
|
56
|
+
|
|
57
|
+
const maybeTipGasPrice = parsedTx.tipGasPrice
|
|
58
|
+
if (maybeTipGasPrice) {
|
|
59
|
+
assert(maybeTipGasPrice instanceof NumberUnit, 'expected NumberUnit tipGasPrice')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const gasLimit = parsedTx.gasLimit
|
|
63
|
+
assert(Number.isInteger(gasLimit), 'expected integer gasLimit')
|
|
64
|
+
|
|
65
|
+
if (to) assert(typeof to === 'string', 'expected string toAddress')
|
|
66
|
+
|
|
67
|
+
const data = parsedTx.data
|
|
68
|
+
const methodId = data ? bufferToHex(data).slice(0, 10) : undefined
|
|
69
|
+
|
|
70
|
+
const selfSend = fromAddress.toLowerCase() === to?.toLowerCase()
|
|
71
|
+
|
|
72
|
+
const baseAsset = asset.baseAsset
|
|
73
|
+
|
|
74
|
+
const maybeTxToReplace = await getHighestIncentiveTxByNonce({
|
|
75
|
+
asset,
|
|
76
|
+
assetClientInterface,
|
|
77
|
+
nonce,
|
|
78
|
+
walletAccount,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const replacedTxId =
|
|
82
|
+
maybeTxToReplace && maybeTxToReplace.feeAmount.lt(feeAmount) ? maybeTxToReplace.txId : undefined
|
|
83
|
+
|
|
84
|
+
if (!replacedTxId && maybeTxToReplace) {
|
|
85
|
+
console.debug('Attempting to replace a transaction using an insufficient fee!')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Contains effects for smart-contract initiated token movements.
|
|
89
|
+
const methodOptimisticSideEffects = await operationTxLogSideEffects({
|
|
90
|
+
txToAddress,
|
|
91
|
+
methodId,
|
|
92
|
+
asset,
|
|
93
|
+
walletAccount,
|
|
94
|
+
feeAmount,
|
|
95
|
+
txId,
|
|
96
|
+
nonce,
|
|
97
|
+
gasLimit,
|
|
98
|
+
tipGasPrice: maybeTipGasPrice,
|
|
99
|
+
data,
|
|
100
|
+
date,
|
|
101
|
+
bundleId,
|
|
102
|
+
fromAddress,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// Fallback to basic logic that only handles transfers + fee decoding
|
|
106
|
+
const sharedProps = {
|
|
107
|
+
confirmations,
|
|
108
|
+
date,
|
|
109
|
+
feeAmount,
|
|
110
|
+
feeCoinName: asset.feeAsset.name,
|
|
111
|
+
selfSend,
|
|
112
|
+
from: [],
|
|
113
|
+
to,
|
|
114
|
+
txId,
|
|
115
|
+
data: {
|
|
116
|
+
gasLimit,
|
|
117
|
+
replacedTxId,
|
|
118
|
+
nonce,
|
|
119
|
+
...(maybeTipGasPrice ? { tipGasPrice: maybeTipGasPrice.toBaseString() } : null),
|
|
120
|
+
...(methodId ? { methodId } : null),
|
|
121
|
+
...(bundleId ? { bundleId } : null),
|
|
122
|
+
...(data ? { data: bufferToHex(data) } : null),
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const receiveWalletAccount = await findOtherPortfolioReceiveWalletAccount({
|
|
127
|
+
amount,
|
|
128
|
+
asset,
|
|
129
|
+
assetClientInterface,
|
|
130
|
+
methodId,
|
|
131
|
+
selfSend,
|
|
132
|
+
to,
|
|
133
|
+
txToAddress,
|
|
134
|
+
walletAccount,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const receiveSideEffects = receiveWalletAccount
|
|
138
|
+
? getOtherPortfolioReceiveEffects({
|
|
139
|
+
amount,
|
|
140
|
+
asset,
|
|
141
|
+
confirmations,
|
|
142
|
+
date,
|
|
143
|
+
fromAddress,
|
|
144
|
+
gasLimit,
|
|
145
|
+
methodId,
|
|
146
|
+
maybeTipGasPrice,
|
|
147
|
+
nonce,
|
|
148
|
+
receiveWalletAccount,
|
|
149
|
+
txId,
|
|
150
|
+
data,
|
|
151
|
+
})
|
|
152
|
+
: []
|
|
153
|
+
|
|
154
|
+
const optimisticTxLogEffects = [
|
|
155
|
+
{
|
|
156
|
+
assetName: asset.name,
|
|
157
|
+
walletAccount,
|
|
158
|
+
txs: [
|
|
159
|
+
{
|
|
160
|
+
...sharedProps,
|
|
161
|
+
data: { ...sharedProps.data, rawCoinAmount: amount.abs().toBaseString() },
|
|
162
|
+
coinAmount: selfSend ? asset.currency.ZERO : amount.abs().negate(),
|
|
163
|
+
coinName: asset.name,
|
|
164
|
+
currencies: {
|
|
165
|
+
[asset.name]: asset.currency,
|
|
166
|
+
[asset.feeAsset.name]: asset.feeAsset.currency,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
isEthereumLikeToken(asset) && {
|
|
172
|
+
assetName: baseAsset.name,
|
|
173
|
+
walletAccount,
|
|
174
|
+
txs: [
|
|
175
|
+
{
|
|
176
|
+
...sharedProps,
|
|
177
|
+
data: { ...sharedProps.data, rawCoinAmount: txValue.abs().toBaseString() },
|
|
178
|
+
coinAmount: selfSend ? baseAsset.currency.ZERO : txValue.abs().negate(),
|
|
179
|
+
coinName: baseAsset.name,
|
|
180
|
+
currencies: {
|
|
181
|
+
[baseAsset.name]: baseAsset.currency,
|
|
182
|
+
[asset.feeAsset.name]: asset.feeAsset.currency,
|
|
183
|
+
},
|
|
184
|
+
tokens: [asset.name],
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
...methodOptimisticSideEffects,
|
|
189
|
+
...receiveSideEffects,
|
|
190
|
+
].filter(Boolean)
|
|
191
|
+
|
|
192
|
+
return { optimisticTxLogEffects, nonce }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const findOtherPortfolioReceiveWalletAccount = async ({
|
|
196
|
+
amount,
|
|
197
|
+
asset,
|
|
198
|
+
assetClientInterface,
|
|
199
|
+
methodId,
|
|
200
|
+
selfSend,
|
|
201
|
+
to,
|
|
202
|
+
txToAddress,
|
|
203
|
+
walletAccount,
|
|
204
|
+
}) => {
|
|
205
|
+
// Exclude cases where another-portfolio receive logs would be misleading:
|
|
206
|
+
// history-backed assets, self-sends, incomplete/zero-value transfers, and
|
|
207
|
+
// token approvals/swaps/router calls that are not plain ERC-20 transfers.
|
|
208
|
+
if (!asset.baseAsset.api.features.noHistory || selfSend || !to || amount.isZero) return
|
|
209
|
+
|
|
210
|
+
if (isEthereumLikeToken(asset)) {
|
|
211
|
+
const isPlainTransfer =
|
|
212
|
+
txToAddress?.toLowerCase() === asset.contract.address.toLowerCase() &&
|
|
213
|
+
methodId === asset.contract.transfer.methodId
|
|
214
|
+
if (!isPlainTransfer) return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
return await getWalletAccountForReceiveAddress({
|
|
219
|
+
assetClientInterface,
|
|
220
|
+
baseAsset: asset.baseAsset,
|
|
221
|
+
sendingWalletAccount: walletAccount,
|
|
222
|
+
to,
|
|
223
|
+
})
|
|
224
|
+
} catch {}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const getOtherPortfolioReceiveEffects = ({
|
|
228
|
+
amount,
|
|
229
|
+
asset,
|
|
230
|
+
confirmations,
|
|
231
|
+
date,
|
|
232
|
+
fromAddress,
|
|
233
|
+
gasLimit,
|
|
234
|
+
methodId,
|
|
235
|
+
maybeTipGasPrice,
|
|
236
|
+
nonce,
|
|
237
|
+
receiveWalletAccount,
|
|
238
|
+
txId,
|
|
239
|
+
data,
|
|
240
|
+
}) => {
|
|
241
|
+
const receiveProps = {
|
|
242
|
+
confirmations,
|
|
243
|
+
date,
|
|
244
|
+
selfSend: false,
|
|
245
|
+
from: [fromAddress],
|
|
246
|
+
txId,
|
|
247
|
+
data: {
|
|
248
|
+
gasLimit,
|
|
249
|
+
nonce,
|
|
250
|
+
...(maybeTipGasPrice ? { tipGasPrice: maybeTipGasPrice.toBaseString() } : null),
|
|
251
|
+
...(methodId ? { methodId } : null),
|
|
252
|
+
...(data ? { data: bufferToHex(data) } : null),
|
|
253
|
+
},
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return [
|
|
257
|
+
{
|
|
258
|
+
assetName: asset.name,
|
|
259
|
+
walletAccount: receiveWalletAccount,
|
|
260
|
+
txs: [
|
|
261
|
+
{
|
|
262
|
+
...receiveProps,
|
|
263
|
+
coinAmount: amount.abs(),
|
|
264
|
+
coinName: asset.name,
|
|
265
|
+
currencies: {
|
|
266
|
+
[asset.name]: asset.currency,
|
|
267
|
+
[asset.feeAsset.name]: asset.feeAsset.currency,
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
},
|
|
272
|
+
]
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const getWalletAccountForReceiveAddress = async ({
|
|
276
|
+
assetClientInterface,
|
|
277
|
+
baseAsset,
|
|
278
|
+
sendingWalletAccount,
|
|
279
|
+
to,
|
|
280
|
+
}) => {
|
|
281
|
+
const normalizedTo = to.toLowerCase()
|
|
282
|
+
const walletAccounts = await assetClientInterface.getWalletAccounts({ assetName: baseAsset.name })
|
|
283
|
+
|
|
284
|
+
for (const walletAccount of walletAccounts) {
|
|
285
|
+
if (walletAccount === sendingWalletAccount) continue
|
|
286
|
+
|
|
287
|
+
const receiveAddresses = await assetClientInterface.getReceiveAddresses({
|
|
288
|
+
assetName: baseAsset.name,
|
|
289
|
+
walletAccount,
|
|
290
|
+
useCache: true,
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
if (receiveAddresses.some((address) => String(address).toLowerCase() === normalizedTo)) {
|
|
294
|
+
return walletAccount
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const operationTxLogSideEffects = async ({
|
|
300
|
+
txToAddress,
|
|
301
|
+
methodId,
|
|
302
|
+
asset,
|
|
303
|
+
walletAccount,
|
|
304
|
+
feeAmount,
|
|
305
|
+
txId,
|
|
306
|
+
nonce,
|
|
307
|
+
gasLimit,
|
|
308
|
+
tipGasPrice,
|
|
309
|
+
data,
|
|
310
|
+
date,
|
|
311
|
+
bundleId,
|
|
312
|
+
fromAddress,
|
|
313
|
+
}) => {
|
|
314
|
+
// Matic Delegate
|
|
315
|
+
if (
|
|
316
|
+
txToAddress?.toLowerCase() ===
|
|
317
|
+
MaticStakingApi.EVERSTAKE_VALIDATOR_CONTRACT_ADDR.toLowerCase() &&
|
|
318
|
+
methodId === DELEGATE
|
|
319
|
+
) {
|
|
320
|
+
return maticDelegateOptimisticSideEffectTxLogs({
|
|
321
|
+
asset,
|
|
322
|
+
walletAccount,
|
|
323
|
+
feeAmount,
|
|
324
|
+
delegateTxId: txId,
|
|
325
|
+
nonce,
|
|
326
|
+
estimatedDelegateGasLimit: gasLimit,
|
|
327
|
+
tipGasPrice,
|
|
328
|
+
transactionData: data,
|
|
329
|
+
date,
|
|
330
|
+
bundleId,
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Ethereum Undelegate
|
|
335
|
+
if (
|
|
336
|
+
isEthereumStakingPoolContract(txToAddress) &&
|
|
337
|
+
methodId === EthereumStaking.METHODS_IDS.UNSTAKE_PENDING
|
|
338
|
+
) {
|
|
339
|
+
return ethereumUnstakePendingOptimisticSideEffectTxLogs({
|
|
340
|
+
asset,
|
|
341
|
+
walletAccount,
|
|
342
|
+
feeAmount,
|
|
343
|
+
unstakePendingTxId: txId,
|
|
344
|
+
nonce,
|
|
345
|
+
estimatedUnstakePendingGasLimit: gasLimit,
|
|
346
|
+
tipGasPrice,
|
|
347
|
+
transactionData: data,
|
|
348
|
+
txToAddress,
|
|
349
|
+
date,
|
|
350
|
+
bundleId,
|
|
351
|
+
})
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Ethereum Undelegate (unstake from active validator)
|
|
355
|
+
if (
|
|
356
|
+
isEthereumStakingPoolContract(txToAddress) &&
|
|
357
|
+
methodId === EthereumStaking.METHODS_IDS.UNSTAKE
|
|
358
|
+
) {
|
|
359
|
+
return ethereumUnstakeOptimisticSideEffectTxLogs({
|
|
360
|
+
asset,
|
|
361
|
+
walletAccount,
|
|
362
|
+
feeAmount,
|
|
363
|
+
unstakeTxId: txId,
|
|
364
|
+
nonce,
|
|
365
|
+
estimatedUnstakeGasLimit: gasLimit,
|
|
366
|
+
tipGasPrice,
|
|
367
|
+
transactionData: data,
|
|
368
|
+
txToAddress,
|
|
369
|
+
fromAddress,
|
|
370
|
+
date,
|
|
371
|
+
bundleId,
|
|
372
|
+
})
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Add other officially supported operations here
|
|
376
|
+
// Then can fallback to simulation results (TODO)
|
|
377
|
+
// Then finally to the default basic logic
|
|
378
|
+
return []
|
|
379
|
+
}
|
package/src/tx-log/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export { ClarityMonitor } from './clarity-monitor'
|
|
1
|
+
export { EthereumMonitor } from './ethereum-monitor.js'
|
|
2
|
+
export { EthereumNoHistoryMonitor } from './ethereum-no-history-monitor.js'
|
|
3
|
+
export { ClarityMonitor } from './clarity-monitor.js'
|
|
4
|
+
export { ClarityTruncatedHistoryMonitor } from './clarity-truncated-history-monitor.js'
|
|
5
|
+
export { getOptimisticTxLogEffects } from './get-optimistic-txlog-effects.js'
|
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import getFeeAmount from './get-fee-amount.js'
|
|
2
|
+
import getSenderNonceKey from './get-sender-nonce-key.js'
|
|
3
3
|
|
|
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
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps Clarity `accountInfo` rows (GET .../transactions?truncated=true) into fields aligned
|
|
3
|
+
* with Ethereum-like account state: `balance`, `tokenBalances`, `nonce` (plus base units
|
|
4
|
+
* for amounts via `currency.baseUnit`).
|
|
5
|
+
* `account_balance` source rows are stored in `balance`/`tokenBalances`; truncated source rows
|
|
6
|
+
* are stored in `truncatedAccountState`.
|
|
7
|
+
*
|
|
8
|
+
* NOTE: All `row.value` payloads arrive over the wire as decimal strings (e.g. `"0"`,
|
|
9
|
+
* `"20191469428776270391810"`). They're parsed with `currency.baseUnit(...)` for amounts
|
|
10
|
+
* and `Number(...)` for nonce. `null`, `undefined`, and empty-string `""` all mean "row
|
|
11
|
+
* carries no value" and the row is skipped — `currency.baseUnit("")` throws and `Number("")`
|
|
12
|
+
* returns 0, both of which would silently corrupt accountState.
|
|
13
|
+
*/
|
|
14
|
+
const ACCOUNT_BALANCE = 'account_balance'
|
|
15
|
+
const TRUNCATED = 'truncated'
|
|
16
|
+
|
|
17
|
+
const hasValue = (value) => value !== null && value !== undefined && value !== ''
|
|
18
|
+
|
|
19
|
+
const warnUnknownSource = (row) => {
|
|
20
|
+
console.warn('Unknown accountInfo source', row)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const extractBalanceFromClarityAccountInfo = ({ accountInfo, asset, tokensByAddress }) => {
|
|
24
|
+
if (!accountInfo || accountInfo.length === 0) return {}
|
|
25
|
+
|
|
26
|
+
const sorted = [...accountInfo].sort((a, b) => (b.blockNumber || 0) - (a.blockNumber || 0))
|
|
27
|
+
|
|
28
|
+
const result = {}
|
|
29
|
+
const seenAssetIds = new Set()
|
|
30
|
+
const tokenBalances = Object.create(null)
|
|
31
|
+
const truncatedTokenBalances = Object.create(null)
|
|
32
|
+
let truncatedBalance
|
|
33
|
+
|
|
34
|
+
for (const row of sorted) {
|
|
35
|
+
switch (row.type) {
|
|
36
|
+
case 'balance':
|
|
37
|
+
if (hasValue(row.value)) {
|
|
38
|
+
const balance = asset.currency.baseUnit(row.value)
|
|
39
|
+
if (row.source === ACCOUNT_BALANCE) {
|
|
40
|
+
result.balance = balance
|
|
41
|
+
} else if (row.source === TRUNCATED) {
|
|
42
|
+
truncatedBalance = balance
|
|
43
|
+
} else {
|
|
44
|
+
warnUnknownSource(row)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
break
|
|
49
|
+
|
|
50
|
+
case 'nonce': {
|
|
51
|
+
// Nonce collapses regardless of source — both `account_balance` and
|
|
52
|
+
// `truncated` rows represent the latest chain-known nonce; max wins
|
|
53
|
+
// for safety against out-of-order rows.
|
|
54
|
+
if (!hasValue(row.value)) break
|
|
55
|
+
const nonce = Number(row.value)
|
|
56
|
+
if (!Number.isFinite(nonce) || nonce < 0) break
|
|
57
|
+
|
|
58
|
+
result.nonce = Math.max(result.nonce ?? 0, nonce)
|
|
59
|
+
|
|
60
|
+
break
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
case 'token': {
|
|
64
|
+
const assetId = row.assetId?.toLowerCase()
|
|
65
|
+
|
|
66
|
+
if (!assetId || seenAssetIds.has(assetId) || !hasValue(row.value)) break
|
|
67
|
+
const token = tokensByAddress.get(assetId)
|
|
68
|
+
if (!token) break
|
|
69
|
+
|
|
70
|
+
const balance = token.currency.baseUnit(row.value)
|
|
71
|
+
if (row.source === ACCOUNT_BALANCE) {
|
|
72
|
+
tokenBalances[token.name] = balance
|
|
73
|
+
} else if (row.source === TRUNCATED) {
|
|
74
|
+
truncatedTokenBalances[token.name] = balance
|
|
75
|
+
} else {
|
|
76
|
+
warnUnknownSource(row)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
seenAssetIds.add(assetId)
|
|
80
|
+
break
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
default:
|
|
84
|
+
break
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (Object.keys(tokenBalances).length > 0) {
|
|
89
|
+
result.tokenBalances = tokenBalances
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const hasTruncatedTokenBalances = Object.keys(truncatedTokenBalances).length > 0
|
|
93
|
+
if (truncatedBalance || hasTruncatedTokenBalances) {
|
|
94
|
+
const truncatedAccountState = Object.create(null)
|
|
95
|
+
|
|
96
|
+
if (truncatedBalance) {
|
|
97
|
+
truncatedAccountState.balance = truncatedBalance
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (hasTruncatedTokenBalances) {
|
|
101
|
+
truncatedAccountState.tokenBalances = truncatedTokenBalances
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
result.truncatedAccountState = truncatedAccountState
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return result
|
|
108
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export const getWalletUpdates = (ourWalletAddress, walletChanges) => {
|
|
2
|
+
return walletChanges.reduce(
|
|
3
|
+
(acc, walletChange) => {
|
|
4
|
+
const { wallet: walletAddress, type, from, to, contract: contractAddress } = walletChange
|
|
5
|
+
|
|
6
|
+
if (walletAddress !== ourWalletAddress) {
|
|
7
|
+
return acc
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
switch (type) {
|
|
11
|
+
case 'balance':
|
|
12
|
+
acc.baseBalanceUpdate = {
|
|
13
|
+
from,
|
|
14
|
+
to,
|
|
15
|
+
}
|
|
16
|
+
break
|
|
17
|
+
case 'nonce':
|
|
18
|
+
acc.nonceUpdate = {
|
|
19
|
+
from,
|
|
20
|
+
to,
|
|
21
|
+
}
|
|
22
|
+
break
|
|
23
|
+
case 'token':
|
|
24
|
+
acc.tokenBalancesUpdate = {
|
|
25
|
+
...acc.tokenBalancesUpdate,
|
|
26
|
+
[contractAddress]: {
|
|
27
|
+
from,
|
|
28
|
+
to,
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
break
|
|
32
|
+
default:
|
|
33
|
+
break
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return acc
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
baseBalanceUpdate: undefined,
|
|
40
|
+
nonceUpdate: undefined,
|
|
41
|
+
tokenBalancesUpdate: Object.create(null),
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import assert from 'minimalistic-assert'
|
|
2
|
+
|
|
3
|
+
import { fromHexToString } from '../../number-utils.js'
|
|
4
|
+
|
|
5
|
+
export const getBatchedRpcBalances = async ({
|
|
6
|
+
baseAsset,
|
|
7
|
+
tokens,
|
|
8
|
+
ourWalletAddress,
|
|
9
|
+
rpcBalanceAssetNames,
|
|
10
|
+
}) => {
|
|
11
|
+
assert(baseAsset, 'expected baseAsset')
|
|
12
|
+
assert(Array.isArray(tokens), 'expected array tokens')
|
|
13
|
+
assert(ourWalletAddress, 'expected ourWalletAddress')
|
|
14
|
+
|
|
15
|
+
const batch = (assetName) =>
|
|
16
|
+
!Array.isArray(rpcBalanceAssetNames) || rpcBalanceAssetNames.includes(assetName)
|
|
17
|
+
|
|
18
|
+
const isBaseAssetBatched = batch(baseAsset.name)
|
|
19
|
+
|
|
20
|
+
const rpcRequestAccumulator = tokens
|
|
21
|
+
.filter(({ name, contract }) => batch(name) && contract?.address)
|
|
22
|
+
.reduce(
|
|
23
|
+
(acc, { contract: { address: tokenAddress } }) =>
|
|
24
|
+
acc.balanceOfRequest(ourWalletAddress, tokenAddress),
|
|
25
|
+
isBaseAssetBatched
|
|
26
|
+
? baseAsset.createRpcRequestAccumulator().getBalanceRequest(ourWalletAddress)
|
|
27
|
+
: baseAsset.createRpcRequestAccumulator()
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if (rpcRequestAccumulator.length() === 0) return Object.create(null)
|
|
31
|
+
|
|
32
|
+
const results = await rpcRequestAccumulator.flush(baseAsset.server)
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
...(isBaseAssetBatched && { [baseAsset.name]: fromHexToString(results[0]) }),
|
|
36
|
+
...Object.fromEntries(
|
|
37
|
+
tokens
|
|
38
|
+
.filter(({ name, contract }) => batch(name) && contract?.address)
|
|
39
|
+
.map(({ name, contract: { address: tokenAddress } }, i) => {
|
|
40
|
+
const tokenBalance = results[i + (isBaseAssetBatched ? 1 : 0)]?.confirmed?.[tokenAddress]
|
|
41
|
+
|
|
42
|
+
assert(tokenBalance != null, `missing token balance for ${name} (${tokenAddress})`)
|
|
43
|
+
return [name, tokenBalance]
|
|
44
|
+
})
|
|
45
|
+
),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks the current blacklist status for an address and returns the state to use.
|
|
3
|
+
* Returns the new state if changed, undefined if unchanged.
|
|
4
|
+
* On error, resets to null (unknown) to avoid showing a stale blacklist warning —
|
|
5
|
+
* preferring a false negative over a false positive.
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} params
|
|
8
|
+
* @param {Function} [params.getBlackListStatus] - Asset blacklist status checker
|
|
9
|
+
* @param {string} params.address - Wallet address to check
|
|
10
|
+
* @param {boolean|null} [params.currentIsBlacklisted] - Current blacklist flag from accountState
|
|
11
|
+
* @param {Object} [params.logger] - Optional logger for warnings
|
|
12
|
+
* @returns {Promise<boolean|null|undefined>} The new blacklist flag, or undefined if unchanged
|
|
13
|
+
*/
|
|
14
|
+
export async function getCurrentBlackListStatus({
|
|
15
|
+
getBlackListStatus,
|
|
16
|
+
address,
|
|
17
|
+
currentIsBlacklisted,
|
|
18
|
+
logger,
|
|
19
|
+
}) {
|
|
20
|
+
if (typeof getBlackListStatus !== 'function') return
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const { isBlacklisted } = await getBlackListStatus({ address })
|
|
24
|
+
|
|
25
|
+
if (currentIsBlacklisted !== isBlacklisted) {
|
|
26
|
+
return isBlacklisted
|
|
27
|
+
}
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (logger) {
|
|
30
|
+
logger.warn('Failed to check blacklist status:', error)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Reset to null (unknown) on error — only a confirmed check should show the blacklist warning.
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default getCurrentBlackListStatus
|