@exodus/ethereum-api 8.70.2 → 8.71.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 +28 -0
- package/package.json +2 -3
- package/src/create-asset-utils.js +1 -1
- package/src/create-asset.js +6 -1
- package/src/exodus-eth-server/clarity.js +2 -2
- package/src/gas-estimation.js +2 -0
- package/src/get-fee.js +8 -3
- package/src/staking/ethereum/everstake.js +53 -0
- package/src/staking/ethereum/service.js +6 -0
- package/src/tx-create.js +20 -2
- package/src/tx-log/clarity-monitor-v2.js +123 -39
- package/src/tx-log/clarity-monitor.js +100 -74
- package/src/tx-log/get-optimistic-txlog-effects.js +7 -3
- package/src/tx-type/index.js +17 -4
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,34 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [8.71.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.70.2...@exodus/ethereum-api@8.71.0) (2026-04-13)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* feat: evm duplex transactions (#7688)
|
|
13
|
+
|
|
14
|
+
* feat: import ethersproject-abi@5 to exodus/ethereumjs (#7626)
|
|
15
|
+
|
|
16
|
+
* feat: surface validator queue times to ethereum staking service (#7438)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
* fix: enable duplex transaction bumps and serialize tx.data.data to txLog (#7705)
|
|
23
|
+
|
|
24
|
+
* fix(ethereum-api): use the right ns key for sockets upon disconnection (#7716)
|
|
25
|
+
|
|
26
|
+
* fix: EVM balance updates when clarity history fetch fails (#7652)
|
|
27
|
+
|
|
28
|
+
* fix: remove unsignedTx from getFeeAsync return value (#7164)
|
|
29
|
+
|
|
30
|
+
* fix: route event-driven ticks through tickWalletAccounts in ClarityMonitor (#7711)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
6
34
|
## [8.70.2](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@8.70.1...@exodus/ethereum-api@8.70.2) (2026-03-27)
|
|
7
35
|
|
|
8
36
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.71.0",
|
|
4
4
|
"description": "Transaction monitors, fee monitors, RPC with the blockchain node, and other networking code for Ethereum and EVM-based blockchains",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
"@exodus/ethereum-meta": "^2.9.1",
|
|
34
34
|
"@exodus/ethereumholesky-meta": "^2.0.5",
|
|
35
35
|
"@exodus/ethereumjs": "^1.8.0",
|
|
36
|
-
"@exodus/ethersproject-abi": "^5.4.2-exodus.2",
|
|
37
36
|
"@exodus/fetch": "^1.3.0",
|
|
38
37
|
"@exodus/models": "^13.0.0",
|
|
39
38
|
"@exodus/safe-string": "^1.4.0",
|
|
@@ -69,5 +68,5 @@
|
|
|
69
68
|
"type": "git",
|
|
70
69
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
71
70
|
},
|
|
72
|
-
"gitHead": "
|
|
71
|
+
"gitHead": "0d1117298321691db0e13516ec5178fb2b8e1f73"
|
|
73
72
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BlacklistCheckTypes } from '@exodus/asset-lib'
|
|
2
|
-
import { defaultAbiCoder } from '@exodus/
|
|
2
|
+
import { defaultAbiCoder } from '@exodus/ethereumjs/ethers5-abi'
|
|
3
3
|
import { safeString } from '@exodus/safe-string'
|
|
4
4
|
import assert from 'minimalistic-assert'
|
|
5
5
|
import ms from 'ms'
|
package/src/create-asset.js
CHANGED
|
@@ -258,6 +258,11 @@ export const createAssetFactory = ({
|
|
|
258
258
|
createTx,
|
|
259
259
|
})
|
|
260
260
|
|
|
261
|
+
const getFeeAsync = async (...args) => {
|
|
262
|
+
const { unsignedTx, ...rest } = await createTx(...args)
|
|
263
|
+
return rest
|
|
264
|
+
}
|
|
265
|
+
|
|
261
266
|
const estimateL1DataFee = l1GasOracleAddress
|
|
262
267
|
? estimateL1DataFeeFactory({ l1GasOracleAddress, server })
|
|
263
268
|
: undefined
|
|
@@ -291,7 +296,7 @@ export const createAssetFactory = ({
|
|
|
291
296
|
...(getBlackListStatus && { getBlackListStatus }),
|
|
292
297
|
getConfirmationsNumber: () => confirmationsNumber,
|
|
293
298
|
getDefaultAddressPath: () => defaultAddressPath,
|
|
294
|
-
getFeeAsync
|
|
299
|
+
getFeeAsync, // createTx alias, remove me when possible
|
|
295
300
|
getFee,
|
|
296
301
|
getFeeData: () => feeData,
|
|
297
302
|
getKeyIdentifier: createGetKeyIdentifier({
|
|
@@ -79,11 +79,11 @@ export default class ClarityServer extends EventEmitter {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
disconnectRpc() {
|
|
82
|
-
this.disconnectSocket(this.
|
|
82
|
+
this.disconnectSocket(this.formatRpcNamespace())
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
disconnectFee() {
|
|
86
|
-
this.disconnectSocket(this.
|
|
86
|
+
this.disconnectSocket(this.formatFeeNamespace())
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
disconnectSocket(namespace) {
|
package/src/gas-estimation.js
CHANGED
|
@@ -113,6 +113,7 @@ export async function fetchGasLimit({
|
|
|
113
113
|
fromAddress: providedFromAddress,
|
|
114
114
|
toAddress: providedToAddress,
|
|
115
115
|
txInput: providedTxInput,
|
|
116
|
+
txValue: providedTxValue,
|
|
116
117
|
amount: providedAmount,
|
|
117
118
|
contractAddress: providedTxToAddress,
|
|
118
119
|
bip70,
|
|
@@ -133,6 +134,7 @@ export async function fetchGasLimit({
|
|
|
133
134
|
toAddress: providedToAddress,
|
|
134
135
|
txToAddress: providedTxToAddress,
|
|
135
136
|
txInput: providedTxInput,
|
|
137
|
+
txValue: providedTxValue,
|
|
136
138
|
txType,
|
|
137
139
|
})
|
|
138
140
|
|
package/src/get-fee.js
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
calculateBumpedGasPrice,
|
|
3
3
|
calculateBumpedGasPriceForFeeData,
|
|
4
4
|
calculateExtraEth,
|
|
5
|
+
isEthereumLikeToken,
|
|
5
6
|
} from '@exodus/ethereum-lib'
|
|
6
7
|
import assert from 'minimalistic-assert'
|
|
7
8
|
|
|
@@ -17,7 +18,11 @@ const taxes = {
|
|
|
17
18
|
paxgold: 0.0002,
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
export const getExtraFeeData = ({ asset, amount }) => {
|
|
21
|
+
export const getExtraFeeData = ({ asset, amount, txValue }) => {
|
|
22
|
+
if (isEthereumLikeToken(asset) && txValue?.gt(asset.baseAsset.currency.ZERO)) {
|
|
23
|
+
return { type: 'tax', extraFee: txValue }
|
|
24
|
+
}
|
|
25
|
+
|
|
21
26
|
const tax = taxes[asset.name]
|
|
22
27
|
if (!amount || !tax || amount.isZero) {
|
|
23
28
|
return {}
|
|
@@ -87,7 +92,7 @@ export const getAggregateTransactionPricing = ({ baseAsset, customFee, feeData,
|
|
|
87
92
|
|
|
88
93
|
export const getFeeFactory =
|
|
89
94
|
() =>
|
|
90
|
-
({ asset, feeData, customFee, txInput, gasLimit: providedGasLimit, amount }) => {
|
|
95
|
+
({ asset, feeData, customFee, txInput, gasLimit: providedGasLimit, amount, txValue }) => {
|
|
91
96
|
const {
|
|
92
97
|
feeData: { tipGasPrice, eip1559Enabled },
|
|
93
98
|
gasPrice,
|
|
@@ -103,7 +108,7 @@ export const getFeeFactory =
|
|
|
103
108
|
// lock in the `tipGasPrice` we used to compute the fees.
|
|
104
109
|
const maybeReturnTipGasPrice = eip1559Enabled ? { tipGasPrice } : null
|
|
105
110
|
|
|
106
|
-
const extraFeeData = getExtraFeeData({ asset, amount })
|
|
111
|
+
const extraFeeData = getExtraFeeData({ asset, amount, txValue })
|
|
107
112
|
|
|
108
113
|
const fee = gasPrice.mul(gasLimit)
|
|
109
114
|
return { ...maybeReturnTipGasPrice, fee, gasPrice, extraFeeData }
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { fetch as exodusFetch } from '@exodus/fetch'
|
|
2
|
+
import { TraceId } from '@exodus/traceparent'
|
|
3
|
+
import assert from 'minimalistic-assert'
|
|
4
|
+
|
|
5
|
+
const BASE_URL = 'https://eth-clarity.a.exodus.io/api/v2/ethereum/proxy/everstake/'
|
|
6
|
+
|
|
7
|
+
const fetch = async (path, config = Object.create(null)) => {
|
|
8
|
+
const url = new URL(path, BASE_URL).toString()
|
|
9
|
+
|
|
10
|
+
const response = await exodusFetch(url, config)
|
|
11
|
+
|
|
12
|
+
const newErrorWithTrace = (msg) => {
|
|
13
|
+
const error = new Error(msg)
|
|
14
|
+
error.traceId = TraceId.fromResponse(response)
|
|
15
|
+
return error
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!response.ok) throw newErrorWithTrace(`failed to fetch ${path}`)
|
|
19
|
+
|
|
20
|
+
const data = await response.json()
|
|
21
|
+
|
|
22
|
+
if (!data || typeof data !== 'object') throw newErrorWithTrace('malformed response')
|
|
23
|
+
|
|
24
|
+
return data
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const isFiniteInteger = (e) => Number.isInteger(e) && Number.isFinite(e)
|
|
28
|
+
|
|
29
|
+
export const getEverstakeValidatorsQueue = async () => {
|
|
30
|
+
// https://swagger.eth-api-b2c.everstake.one/#/Staking/validatorsQueue
|
|
31
|
+
const result = await fetch('v1/validators/queue')
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
// Estimated time in seconds of delay before validator become active
|
|
35
|
+
validator_activation_time: validatorActivationTime,
|
|
36
|
+
// Estimated time in seconds of delay before validator become exited
|
|
37
|
+
validator_exit_time: validatorExitTime,
|
|
38
|
+
// Withdraw period in seconds from Beacon chain
|
|
39
|
+
validator_withdraw_time: validatorWithdrawTime,
|
|
40
|
+
} = result
|
|
41
|
+
|
|
42
|
+
assert(isFiniteInteger(validatorActivationTime), 'expected integer validatorActivationTime')
|
|
43
|
+
assert(isFiniteInteger(validatorExitTime), 'expected integer validatorExitTime')
|
|
44
|
+
assert(isFiniteInteger(validatorWithdrawTime), 'expected integer validatorWithdrawTime')
|
|
45
|
+
|
|
46
|
+
// NOTE: We convert the values into milliseconds for
|
|
47
|
+
// normalized handling on clients.
|
|
48
|
+
return {
|
|
49
|
+
validatorActivationTime: validatorActivationTime * 1000,
|
|
50
|
+
validatorExitTime: validatorExitTime * 1000,
|
|
51
|
+
validatorWithdrawTime: validatorWithdrawTime * 1000,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -6,6 +6,7 @@ import { createWatchTx as defaultCreateWatch } from '../../watch-tx.js'
|
|
|
6
6
|
import { stakingProviderClientFactory } from '../staking-provider-client.js'
|
|
7
7
|
import { amountToCurrency, DISABLE_BALANCE_CHECKS, resolveFeeData } from '../utils/index.js'
|
|
8
8
|
import { EthereumStaking } from './api.js'
|
|
9
|
+
import { getEverstakeValidatorsQueue } from './everstake.js'
|
|
9
10
|
|
|
10
11
|
const WETH9_ADDRESS = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'
|
|
11
12
|
|
|
@@ -579,12 +580,14 @@ export async function getEthereumStakingInfo({ address, asset, server }) {
|
|
|
579
580
|
pendingDepositedBalance,
|
|
580
581
|
withdrawRequest,
|
|
581
582
|
rewardsBalance,
|
|
583
|
+
everstakeValidatorsQueue,
|
|
582
584
|
] = await Promise.all([
|
|
583
585
|
staking.autocompoundBalanceOf(delegator),
|
|
584
586
|
staking.pendingBalanceOf(delegator),
|
|
585
587
|
staking.pendingDepositedBalanceOf(delegator),
|
|
586
588
|
staking.withdrawRequest(delegator),
|
|
587
589
|
staking.getTotalRewards(delegator),
|
|
590
|
+
getEverstakeValidatorsQueue().catch(() => null),
|
|
588
591
|
])
|
|
589
592
|
|
|
590
593
|
const delegatedBalance = activeStakedBalance.add(pendingBalance).add(pendingDepositedBalance)
|
|
@@ -614,5 +617,8 @@ export async function getEthereumStakingInfo({ address, asset, server }) {
|
|
|
614
617
|
unclaimedUndelegatedBalance,
|
|
615
618
|
canClaimUndelegatedBalance,
|
|
616
619
|
isUndelegateInProgress,
|
|
620
|
+
validatorActivationTime: everstakeValidatorsQueue?.validatorActivationTime,
|
|
621
|
+
validatorExitTime: everstakeValidatorsQueue?.validatorExitTime,
|
|
622
|
+
validatorWithdrawTime: everstakeValidatorsQueue?.validatorWithdrawTime,
|
|
617
623
|
}
|
|
618
624
|
}
|
package/src/tx-create.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
assertValidTxValue,
|
|
2
3
|
currency2buffer,
|
|
3
4
|
getHighestIncentiveTxByNonce,
|
|
4
5
|
isEthereumLikeToken,
|
|
@@ -85,7 +86,7 @@ async function createUnsignedTxWithFees({
|
|
|
85
86
|
: asset.baseAsset.currency.ZERO
|
|
86
87
|
|
|
87
88
|
const fee = baseFee.add(l1DataFee)
|
|
88
|
-
const extraFeeData = getExtraFeeData({ asset, amount })
|
|
89
|
+
const extraFeeData = getExtraFeeData({ asset, amount, txValue })
|
|
89
90
|
const unsignedTx = {
|
|
90
91
|
txData: { transactionBuffer, chainId },
|
|
91
92
|
txMeta: {
|
|
@@ -262,8 +263,15 @@ const createBumpUnsignedTx = async ({
|
|
|
262
263
|
|
|
263
264
|
const amount = (replacedTokenTx || replacedTx).coinAmount.negate()
|
|
264
265
|
|
|
265
|
-
const
|
|
266
|
+
const txValue = assertValidTxValue({
|
|
267
|
+
asset,
|
|
268
|
+
amount,
|
|
269
|
+
txValue: replacedTx.coinAmount.negate(),
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
const isDuplex = Boolean(replacedTokenTx && txValue.gt(baseAsset.currency.ZERO))
|
|
266
273
|
|
|
274
|
+
const txInput = replacedTokenTx && !isDuplex ? null : replacedTx.data.data || '0x'
|
|
267
275
|
const replacedTxNonce = replacedTx.data.nonce
|
|
268
276
|
|
|
269
277
|
assert(
|
|
@@ -291,6 +299,7 @@ const createBumpUnsignedTx = async ({
|
|
|
291
299
|
txInput,
|
|
292
300
|
toAddress,
|
|
293
301
|
txType,
|
|
302
|
+
txValue,
|
|
294
303
|
walletAccount,
|
|
295
304
|
})
|
|
296
305
|
|
|
@@ -345,6 +354,8 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
|
|
|
345
354
|
address, // same as toAddress
|
|
346
355
|
txInput: providedTxInput, // Provided when swapping via a DEX contract
|
|
347
356
|
txType = TX_TYPE_TRANSFER, // Defines what kind of transaction is being performed.
|
|
357
|
+
txValue: providedTxValue, // Override the native ETH value (e.g. payable contract calls with txInput when specifying a token asset)
|
|
358
|
+
contractAddress: providedTxToAddress, // Controls the final target address of the transaction.
|
|
348
359
|
gasLimit: providedGasLimit, // Provided by exchange when known
|
|
349
360
|
amount: providedAmount, // The NU amount to be sent, to be included in the tx value or tx input
|
|
350
361
|
nonce: providedNonce,
|
|
@@ -377,6 +388,8 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
|
|
|
377
388
|
})
|
|
378
389
|
|
|
379
390
|
if (bumpTxId) {
|
|
391
|
+
assert(!providedTxValue && !providedTxToAddress)
|
|
392
|
+
|
|
380
393
|
return createBumpUnsignedTx({
|
|
381
394
|
chainId,
|
|
382
395
|
asset,
|
|
@@ -422,6 +435,8 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
|
|
|
422
435
|
})
|
|
423
436
|
|
|
424
437
|
if (nft) {
|
|
438
|
+
assert(!providedTxValue && !providedTxToAddress)
|
|
439
|
+
|
|
425
440
|
const {
|
|
426
441
|
contractAddress: txToAddress,
|
|
427
442
|
gasLimit,
|
|
@@ -464,7 +479,9 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
|
|
|
464
479
|
nonce,
|
|
465
480
|
txInput: providedTxInput,
|
|
466
481
|
toAddress: providedToAddress,
|
|
482
|
+
txToAddress: providedTxToAddress,
|
|
467
483
|
txType,
|
|
484
|
+
txValue: providedTxValue,
|
|
468
485
|
walletAccount,
|
|
469
486
|
})
|
|
470
487
|
|
|
@@ -478,6 +495,7 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
|
|
|
478
495
|
fromAddress,
|
|
479
496
|
toAddress: resolvedTxAttributes.toAddress,
|
|
480
497
|
txInput: resolvedTxAttributes.txInput,
|
|
498
|
+
txValue: resolvedTxAttributes.txValue,
|
|
481
499
|
contractAddress: resolvedTxAttributes.txToAddress,
|
|
482
500
|
bip70,
|
|
483
501
|
amount: resolvedTxAttributes.amount,
|
|
@@ -173,29 +173,103 @@ export class ClarityMonitorV2 extends BaseMonitor {
|
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
const { derivedData, tokensByAddress, assets, tokens, assetName } = walletAccountInfo
|
|
176
|
-
|
|
177
|
-
const response = await this.getHistoryFromServer({ walletAccount, derivedData, refresh })
|
|
178
|
-
|
|
179
|
-
const { allTxs } = await normalizeTransactionsResponse({
|
|
180
|
-
asset: this.asset,
|
|
181
|
-
fromAddress: derivedData.ourWalletAddress,
|
|
182
|
-
response,
|
|
183
|
-
walletAccount,
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
const cursor = response.cursor
|
|
187
|
-
|
|
188
|
-
await this.processAndFillTransactionsToState({
|
|
189
|
-
allTxs,
|
|
176
|
+
const { accountState, eip7702Delegation, isBlacklisted } = await this.getStateUpdate({
|
|
190
177
|
derivedData,
|
|
191
|
-
tokensByAddress,
|
|
192
|
-
assets,
|
|
193
178
|
tokens,
|
|
194
|
-
assetName,
|
|
195
179
|
walletAccount,
|
|
196
|
-
refresh,
|
|
197
|
-
cursor,
|
|
198
180
|
})
|
|
181
|
+
const batch = this.aci.createOperationsBatch()
|
|
182
|
+
const newData = {
|
|
183
|
+
...accountState,
|
|
184
|
+
...(isBlacklisted !== undefined && { isBlacklisted }),
|
|
185
|
+
...(eip7702Delegation !== undefined && { eip7702Delegation }),
|
|
186
|
+
}
|
|
187
|
+
let allTxs = []
|
|
188
|
+
let hasNewTxs = false
|
|
189
|
+
let historyError
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const response = await this.getHistoryFromServer({ walletAccount, derivedData, refresh })
|
|
193
|
+
|
|
194
|
+
;({ allTxs } = await normalizeTransactionsResponse({
|
|
195
|
+
asset: this.asset,
|
|
196
|
+
fromAddress: derivedData.ourWalletAddress,
|
|
197
|
+
response,
|
|
198
|
+
walletAccount,
|
|
199
|
+
}))
|
|
200
|
+
|
|
201
|
+
hasNewTxs = allTxs.length > 0
|
|
202
|
+
|
|
203
|
+
const logItemsByAsset = this.getAllLogItemsByAsset({
|
|
204
|
+
getLogItemsFromServerTx,
|
|
205
|
+
ourWalletAddress: derivedData.ourWalletAddress,
|
|
206
|
+
allTransactionsFromServer: allTxs,
|
|
207
|
+
asset: this.asset,
|
|
208
|
+
tokensByAddress,
|
|
209
|
+
assets,
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
const { txsToRemove } = await this.checkPendingTransactions({
|
|
213
|
+
txlist: allTxs,
|
|
214
|
+
walletAccount,
|
|
215
|
+
refresh,
|
|
216
|
+
logItemsByAsset,
|
|
217
|
+
asset: this.asset,
|
|
218
|
+
...derivedData,
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
this.aci.removeTxLogBatch({
|
|
222
|
+
assetName,
|
|
223
|
+
walletAccount,
|
|
224
|
+
txs: txsToRemove,
|
|
225
|
+
batch,
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
for (const [assetName, txs] of Object.entries(logItemsByAsset)) {
|
|
229
|
+
this.aci.updateTxLogAndNotifyBatch({
|
|
230
|
+
assetName,
|
|
231
|
+
walletAccount,
|
|
232
|
+
txs,
|
|
233
|
+
refresh,
|
|
234
|
+
batch,
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (response.cursor) {
|
|
239
|
+
newData.clarityCursor = response.cursor
|
|
240
|
+
}
|
|
241
|
+
} catch (error) {
|
|
242
|
+
historyError = error
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
this.aci.updateAccountStateBatch({
|
|
247
|
+
assetName,
|
|
248
|
+
walletAccount,
|
|
249
|
+
accountState,
|
|
250
|
+
newData,
|
|
251
|
+
batch,
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
await this.aci.executeOperationsBatch(batch)
|
|
255
|
+
} catch (batchError) {
|
|
256
|
+
if (!historyError) throw batchError
|
|
257
|
+
this.logger.warn('error persisting account state after history failure', batchError)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (historyError) {
|
|
261
|
+
throw historyError
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (refresh || hasNewTxs) {
|
|
265
|
+
const unknownTokenAddresses = this.getUnknownTokenAddresses({
|
|
266
|
+
transactions: allTxs,
|
|
267
|
+
tokensByAddress,
|
|
268
|
+
})
|
|
269
|
+
if (unknownTokenAddresses.length > 0) {
|
|
270
|
+
this.emit('unknown-tokens', unknownTokenAddresses)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
199
273
|
}
|
|
200
274
|
|
|
201
275
|
async processAndFillTransactionsToState({
|
|
@@ -209,7 +283,6 @@ export class ClarityMonitorV2 extends BaseMonitor {
|
|
|
209
283
|
refresh,
|
|
210
284
|
cursor,
|
|
211
285
|
}) {
|
|
212
|
-
const shouldCheckBlacklist = this.tickCount[walletAccount] === 0
|
|
213
286
|
const hasNewTxs = allTxs.length > 0
|
|
214
287
|
|
|
215
288
|
const logItemsByAsset = this.getAllLogItemsByAsset({
|
|
@@ -230,27 +303,11 @@ export class ClarityMonitorV2 extends BaseMonitor {
|
|
|
230
303
|
...derivedData,
|
|
231
304
|
})
|
|
232
305
|
|
|
233
|
-
const accountState = await this.
|
|
306
|
+
const { accountState, eip7702Delegation, isBlacklisted } = await this.getStateUpdate({
|
|
307
|
+
derivedData,
|
|
234
308
|
tokens,
|
|
235
|
-
|
|
236
|
-
ourWalletAddress: derivedData.ourWalletAddress,
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
const eip7702Delegation = await getCurrentEIP7702Delegation({
|
|
240
|
-
server: this.server,
|
|
241
|
-
address: derivedData.ourWalletAddress,
|
|
242
|
-
eip7702Supported: this.eip7702Supported,
|
|
243
|
-
currentDelegation: derivedData.currentAccountState?.eip7702Delegation,
|
|
244
|
-
logger: this.logger,
|
|
309
|
+
walletAccount,
|
|
245
310
|
})
|
|
246
|
-
const isBlacklisted = shouldCheckBlacklist
|
|
247
|
-
? await getCurrentBlackListStatus({
|
|
248
|
-
getBlackListStatus: this.getBlackListStatus,
|
|
249
|
-
address: derivedData.ourWalletAddress,
|
|
250
|
-
currentIsBlacklisted: derivedData.currentAccountState?.isBlacklisted,
|
|
251
|
-
logger: this.logger,
|
|
252
|
-
})
|
|
253
|
-
: undefined
|
|
254
311
|
|
|
255
312
|
const batch = this.aci.createOperationsBatch()
|
|
256
313
|
|
|
@@ -303,6 +360,33 @@ export class ClarityMonitorV2 extends BaseMonitor {
|
|
|
303
360
|
}
|
|
304
361
|
}
|
|
305
362
|
|
|
363
|
+
async getStateUpdate({ derivedData, tokens, walletAccount }) {
|
|
364
|
+
const shouldCheckBlacklist = this.tickCount[walletAccount] === 0
|
|
365
|
+
const accountState = await this.getNewAccountState({
|
|
366
|
+
tokens,
|
|
367
|
+
currentTokenBalances: derivedData.currentAccountState?.tokenBalances,
|
|
368
|
+
ourWalletAddress: derivedData.ourWalletAddress,
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
const eip7702Delegation = await getCurrentEIP7702Delegation({
|
|
372
|
+
server: this.server,
|
|
373
|
+
address: derivedData.ourWalletAddress,
|
|
374
|
+
eip7702Supported: this.eip7702Supported,
|
|
375
|
+
currentDelegation: derivedData.currentAccountState?.eip7702Delegation,
|
|
376
|
+
logger: this.logger,
|
|
377
|
+
})
|
|
378
|
+
const isBlacklisted = shouldCheckBlacklist
|
|
379
|
+
? await getCurrentBlackListStatus({
|
|
380
|
+
getBlackListStatus: this.getBlackListStatus,
|
|
381
|
+
address: derivedData.ourWalletAddress,
|
|
382
|
+
currentIsBlacklisted: derivedData.currentAccountState?.isBlacklisted,
|
|
383
|
+
logger: this.logger,
|
|
384
|
+
})
|
|
385
|
+
: undefined
|
|
386
|
+
|
|
387
|
+
return { accountState, eip7702Delegation, isBlacklisted }
|
|
388
|
+
}
|
|
389
|
+
|
|
306
390
|
async addSingleTx({ tx, address, cursor }) {
|
|
307
391
|
const walletAccounts = this.#walletAccountByAddress.get(address)
|
|
308
392
|
|
|
@@ -153,9 +153,7 @@ export class ClarityMonitor extends BaseMonitor {
|
|
|
153
153
|
|
|
154
154
|
async tick({ walletAccount, refresh }) {
|
|
155
155
|
await this.subscribeWalletAddresses()
|
|
156
|
-
|
|
157
|
-
// so tickCount is initialized and this fallback can be removed in a dedicated follow-up.
|
|
158
|
-
const tickCount = this.tickCount[walletAccount] ?? 0
|
|
156
|
+
const tickCount = this.tickCount[walletAccount]
|
|
159
157
|
const shouldCheckBlacklist = tickCount === 0
|
|
160
158
|
|
|
161
159
|
const assets = await this.aci.getAssetsForNetwork({ baseAssetName: this.asset.name })
|
|
@@ -167,35 +165,107 @@ export class ClarityMonitor extends BaseMonitor {
|
|
|
167
165
|
}, new Map())
|
|
168
166
|
const assetName = this.asset.name
|
|
169
167
|
const derivedData = await this.deriveData({ assetName, walletAccount, tokens })
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
fromAddress: derivedData.ourWalletAddress,
|
|
175
|
-
response,
|
|
176
|
-
walletAccount,
|
|
168
|
+
const { accountState, eip7702Delegation, isBlacklisted } = await this.getStateUpdate({
|
|
169
|
+
derivedData,
|
|
170
|
+
tokens,
|
|
171
|
+
shouldCheckBlacklist,
|
|
177
172
|
})
|
|
178
173
|
|
|
179
|
-
const
|
|
174
|
+
const batch = this.aci.createOperationsBatch()
|
|
175
|
+
const newData = {
|
|
176
|
+
...accountState,
|
|
177
|
+
...(isBlacklisted !== undefined && { isBlacklisted }),
|
|
178
|
+
...(eip7702Delegation !== undefined && { eip7702Delegation }),
|
|
179
|
+
}
|
|
180
|
+
let allTxs = []
|
|
181
|
+
let hasNewTxs = false
|
|
182
|
+
let historyError
|
|
180
183
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
ourWalletAddress: derivedData.ourWalletAddress,
|
|
184
|
-
allTransactionsFromServer: allTxs,
|
|
185
|
-
asset: this.asset,
|
|
186
|
-
tokensByAddress,
|
|
187
|
-
assets,
|
|
188
|
-
})
|
|
184
|
+
try {
|
|
185
|
+
const response = await this.getHistoryFromServer({ walletAccount, derivedData, refresh })
|
|
189
186
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
187
|
+
;({ allTxs } = await normalizeTransactionsResponse({
|
|
188
|
+
asset: this.asset,
|
|
189
|
+
fromAddress: derivedData.ourWalletAddress,
|
|
190
|
+
response,
|
|
191
|
+
walletAccount,
|
|
192
|
+
}))
|
|
193
|
+
|
|
194
|
+
hasNewTxs = allTxs.length > 0
|
|
195
|
+
|
|
196
|
+
const logItemsByAsset = this.getAllLogItemsByAsset({
|
|
197
|
+
getLogItemsFromServerTx,
|
|
198
|
+
ourWalletAddress: derivedData.ourWalletAddress,
|
|
199
|
+
allTransactionsFromServer: allTxs,
|
|
200
|
+
asset: this.asset,
|
|
201
|
+
tokensByAddress,
|
|
202
|
+
assets,
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
const { txsToRemove } = await this.checkPendingTransactions({
|
|
206
|
+
txlist: allTxs,
|
|
207
|
+
walletAccount,
|
|
208
|
+
refresh,
|
|
209
|
+
logItemsByAsset,
|
|
210
|
+
asset: this.asset,
|
|
211
|
+
...derivedData,
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
this.aci.removeTxLogBatch({
|
|
215
|
+
assetName,
|
|
216
|
+
walletAccount,
|
|
217
|
+
txs: txsToRemove,
|
|
218
|
+
batch,
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
for (const [assetName, txs] of Object.entries(logItemsByAsset)) {
|
|
222
|
+
this.aci.updateTxLogAndNotifyBatch({
|
|
223
|
+
assetName,
|
|
224
|
+
walletAccount,
|
|
225
|
+
txs,
|
|
226
|
+
refresh,
|
|
227
|
+
batch,
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
newData.clarityCursor = response.cursor
|
|
232
|
+
} catch (error) {
|
|
233
|
+
historyError = error
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
// Persist balance and account metadata even when the history path fails
|
|
238
|
+
// during startup bursts or other temporary backend pressure.
|
|
239
|
+
this.aci.updateAccountStateBatch({
|
|
240
|
+
assetName,
|
|
241
|
+
walletAccount,
|
|
242
|
+
accountState,
|
|
243
|
+
newData,
|
|
244
|
+
batch,
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
await this.aci.executeOperationsBatch(batch)
|
|
248
|
+
} catch (batchError) {
|
|
249
|
+
if (!historyError) throw batchError
|
|
250
|
+
this.logger.warn('error persisting account state after history failure', batchError)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (historyError) {
|
|
254
|
+
throw historyError
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (refresh || hasNewTxs) {
|
|
258
|
+
const unknownTokenAddresses = this.getUnknownTokenAddresses({
|
|
259
|
+
transactions: allTxs,
|
|
260
|
+
tokensByAddress,
|
|
261
|
+
})
|
|
262
|
+
if (unknownTokenAddresses.length > 0) {
|
|
263
|
+
this.emit('unknown-tokens', unknownTokenAddresses)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
198
267
|
|
|
268
|
+
async getStateUpdate({ derivedData, tokens, shouldCheckBlacklist }) {
|
|
199
269
|
const accountState = await this.getNewAccountState({
|
|
200
270
|
tokens,
|
|
201
271
|
currentTokenBalances: derivedData.currentAccountState?.tokenBalances,
|
|
@@ -209,6 +279,7 @@ export class ClarityMonitor extends BaseMonitor {
|
|
|
209
279
|
currentDelegation: derivedData.currentAccountState?.eip7702Delegation,
|
|
210
280
|
logger: this.logger,
|
|
211
281
|
})
|
|
282
|
+
|
|
212
283
|
const isBlacklisted = shouldCheckBlacklist
|
|
213
284
|
? await getCurrentBlackListStatus({
|
|
214
285
|
getBlackListStatus: this.getBlackListStatus,
|
|
@@ -218,52 +289,7 @@ export class ClarityMonitor extends BaseMonitor {
|
|
|
218
289
|
})
|
|
219
290
|
: undefined
|
|
220
291
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
this.aci.removeTxLogBatch({
|
|
224
|
-
assetName,
|
|
225
|
-
walletAccount,
|
|
226
|
-
txs: txsToRemove,
|
|
227
|
-
batch,
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
for (const [assetName, txs] of Object.entries(logItemsByAsset)) {
|
|
231
|
-
this.aci.updateTxLogAndNotifyBatch({
|
|
232
|
-
assetName,
|
|
233
|
-
walletAccount,
|
|
234
|
-
txs,
|
|
235
|
-
refresh,
|
|
236
|
-
batch,
|
|
237
|
-
})
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// All updates must go through newData (accountState param is only used for mem merging)
|
|
241
|
-
const newData = {
|
|
242
|
-
...accountState,
|
|
243
|
-
clarityCursor: response.cursor,
|
|
244
|
-
...(isBlacklisted !== undefined && { isBlacklisted }),
|
|
245
|
-
...(eip7702Delegation !== undefined && { eip7702Delegation }),
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
this.aci.updateAccountStateBatch({
|
|
249
|
-
assetName,
|
|
250
|
-
walletAccount,
|
|
251
|
-
accountState,
|
|
252
|
-
newData,
|
|
253
|
-
batch,
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
await this.aci.executeOperationsBatch(batch)
|
|
257
|
-
|
|
258
|
-
if (refresh || hasNewTxs) {
|
|
259
|
-
const unknownTokenAddresses = this.getUnknownTokenAddresses({
|
|
260
|
-
transactions: allTxs,
|
|
261
|
-
tokensByAddress,
|
|
262
|
-
})
|
|
263
|
-
if (unknownTokenAddresses.length > 0) {
|
|
264
|
-
this.emit('unknown-tokens', unknownTokenAddresses)
|
|
265
|
-
}
|
|
266
|
-
}
|
|
292
|
+
return { accountState, eip7702Delegation, isBlacklisted }
|
|
267
293
|
}
|
|
268
294
|
|
|
269
295
|
async getNewAccountState({ tokens, currentTokenBalances, ourWalletAddress }) {
|
|
@@ -376,7 +402,7 @@ export class ClarityMonitor extends BaseMonitor {
|
|
|
376
402
|
}
|
|
377
403
|
|
|
378
404
|
async onTransaction({ walletAccount }) {
|
|
379
|
-
return this.
|
|
405
|
+
return this.tickWalletAccounts({ walletAccount })
|
|
380
406
|
}
|
|
381
407
|
|
|
382
408
|
async onFeeUpdated(fee) {
|
|
@@ -36,7 +36,7 @@ export const getOptimisticTxLogEffects = async ({
|
|
|
36
36
|
// this converts an transactionBuffer to values we can use when creating the tx logs
|
|
37
37
|
const parsedTx = parseUnsignedTx({ asset, unsignedTx })
|
|
38
38
|
|
|
39
|
-
const { nonce, to } = parsedTx
|
|
39
|
+
const { nonce, to, txToAddress } = parsedTx
|
|
40
40
|
assert(Number.isInteger(nonce), 'expected integer nonce')
|
|
41
41
|
|
|
42
42
|
const amount = parsedTx.amount || asset.currency.ZERO
|
|
@@ -45,6 +45,9 @@ export const getOptimisticTxLogEffects = async ({
|
|
|
45
45
|
const feeAmount = parsedTx.fee
|
|
46
46
|
assert(feeAmount instanceof NumberUnit, 'expected feeAmount')
|
|
47
47
|
|
|
48
|
+
const txValue = parsedTx.value
|
|
49
|
+
assert(txValue instanceof NumberUnit, 'expected txValue')
|
|
50
|
+
|
|
48
51
|
const maybeTipGasPrice = parsedTx.tipGasPrice
|
|
49
52
|
if (maybeTipGasPrice) {
|
|
50
53
|
assert(maybeTipGasPrice instanceof NumberUnit, 'expected NumberUnit tipGasPrice')
|
|
@@ -78,7 +81,7 @@ export const getOptimisticTxLogEffects = async ({
|
|
|
78
81
|
|
|
79
82
|
// Contains effects for smart-contract initiated token movements.
|
|
80
83
|
const methodOptimisticSideEffects = await operationTxLogSideEffects({
|
|
81
|
-
txToAddress
|
|
84
|
+
txToAddress,
|
|
82
85
|
methodId,
|
|
83
86
|
asset,
|
|
84
87
|
walletAccount,
|
|
@@ -109,6 +112,7 @@ export const getOptimisticTxLogEffects = async ({
|
|
|
109
112
|
...(maybeTipGasPrice ? { tipGasPrice: maybeTipGasPrice.toBaseString() } : null),
|
|
110
113
|
...(methodId ? { methodId } : null),
|
|
111
114
|
...(bundleId ? { bundleId } : null),
|
|
115
|
+
...(data ? { data: bufferToHex(data) } : null),
|
|
112
116
|
},
|
|
113
117
|
}
|
|
114
118
|
|
|
@@ -134,7 +138,7 @@ export const getOptimisticTxLogEffects = async ({
|
|
|
134
138
|
txs: [
|
|
135
139
|
{
|
|
136
140
|
...sharedProps,
|
|
137
|
-
coinAmount: baseAsset.currency.ZERO,
|
|
141
|
+
coinAmount: selfSend ? baseAsset.currency.ZERO : txValue.abs().negate(),
|
|
138
142
|
coinName: baseAsset.name,
|
|
139
143
|
currencies: {
|
|
140
144
|
[baseAsset.name]: baseAsset.currency,
|
package/src/tx-type/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import NumberUnit from '@exodus/currency'
|
|
2
|
-
import { isEthereumLikeToken } from '@exodus/ethereum-lib'
|
|
2
|
+
import { assertValidTxValue, isEthereumLikeToken } from '@exodus/ethereum-lib'
|
|
3
3
|
import { bufferToHex } from '@exodus/ethereumjs/util'
|
|
4
4
|
import assert from 'minimalistic-assert'
|
|
5
5
|
|
|
@@ -154,6 +154,7 @@ export const resolveCriticalTxAttributes = ({
|
|
|
154
154
|
toAddress: providedToAddress,
|
|
155
155
|
txToAddress: providedTxToAddress,
|
|
156
156
|
txInput: providedTxInput,
|
|
157
|
+
txValue: providedTxValue,
|
|
157
158
|
txType,
|
|
158
159
|
}) => {
|
|
159
160
|
assert(asset, 'expected asset')
|
|
@@ -162,6 +163,8 @@ export const resolveCriticalTxAttributes = ({
|
|
|
162
163
|
const amount = providedAmount ?? asset.currency.ZERO
|
|
163
164
|
assert(amount instanceof NumberUnit, 'expected providedAmount')
|
|
164
165
|
|
|
166
|
+
const txValue = assertValidTxValue({ asset, amount, txValue: providedTxValue })
|
|
167
|
+
|
|
165
168
|
if (txType === TX_TYPE_CREATE_CONTRACT) {
|
|
166
169
|
assert(asset.name === asset.baseAsset.name, 'must use baseAsset for contract deployments')
|
|
167
170
|
assert(!providedToAddress, 'toAddress must be falsy when creating a contract')
|
|
@@ -173,20 +176,28 @@ export const resolveCriticalTxAttributes = ({
|
|
|
173
176
|
txInput: providedTxInput,
|
|
174
177
|
txToAddress: null,
|
|
175
178
|
txType,
|
|
176
|
-
txValue
|
|
179
|
+
txValue,
|
|
177
180
|
})
|
|
178
181
|
}
|
|
179
182
|
|
|
180
183
|
assert(txType === TX_TYPE_TRANSFER, 'expected TX_TYPE_TRANSFER')
|
|
181
184
|
|
|
185
|
+
// NOTE: As a helper, if the `toAddress` is omitted, we
|
|
186
|
+
// will automatically assume the recipient of the
|
|
187
|
+
// transaction value to be the `providedTxToAddress`.
|
|
188
|
+
//
|
|
189
|
+
// This means we'll assume the high-level recipient
|
|
190
|
+
// of the assets transferred will be the
|
|
191
|
+
// `providedTxToAddress`, as opposed to another account
|
|
192
|
+
// which would be credited as a side effect.
|
|
193
|
+
//
|
|
182
194
|
// HACK: If a `toAddress` hasn't been defined, then we
|
|
183
195
|
// fall back to the `ARBITRARY_ADDRESS`. Note that
|
|
184
196
|
// this should only be used to help determine the
|
|
185
197
|
// fee of a transaction where we don't yet know the
|
|
186
198
|
// intended recipient. Attempts to send to this
|
|
187
199
|
// sentinel address will ultimately `throw`.
|
|
188
|
-
const toAddress = providedToAddress || ARBITRARY_ADDRESS
|
|
189
|
-
const txValue = isEthereumLikeToken(asset) ? asset.baseAsset.currency.ZERO : amount
|
|
200
|
+
const toAddress = providedToAddress || providedTxToAddress || ARBITRARY_ADDRESS
|
|
190
201
|
const txInput = resolveTxInput({ amount, asset, txInput: providedTxInput, toAddress })
|
|
191
202
|
|
|
192
203
|
const baseProps = {
|
|
@@ -231,6 +242,7 @@ export const resolveTxAttributesByTxType = async ({
|
|
|
231
242
|
txInput: providedTxInput,
|
|
232
243
|
txToAddress: providedTxToAddress,
|
|
233
244
|
toAddress: providedToAddress,
|
|
245
|
+
txValue: providedTxValue,
|
|
234
246
|
txType,
|
|
235
247
|
walletAccount,
|
|
236
248
|
}) => {
|
|
@@ -260,6 +272,7 @@ export const resolveTxAttributesByTxType = async ({
|
|
|
260
272
|
toAddress: providedToAddress,
|
|
261
273
|
txInput: providedTxInput,
|
|
262
274
|
txToAddress: providedTxToAddress,
|
|
275
|
+
txValue: providedTxValue,
|
|
263
276
|
txType,
|
|
264
277
|
}),
|
|
265
278
|
})
|