@exodus/ethereum-api 10.3.0 → 10.3.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 +9 -0
- package/package.json +2 -2
- package/src/hooks/monitor.js +2 -1
- package/src/transaction-authentication.js +93 -0
- package/src/tx-create.js +80 -92
- package/src/tx-log/authenticated-monitor.js +85 -0
- package/src/tx-log/clarity-monitor.js +16 -12
- package/src/tx-log/ethereum-no-history-monitor.js +2 -2
- package/src/tx-log-staking-processor/index.js +14 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
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
|
+
## [10.3.1](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@10.3.0...@exodus/ethereum-api@10.3.1) (2026-11-03)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* authenticate EVM replacements and remaining outgoing history ([#9094](https://github.com/ExodusMovement/assets/issues/9094)) ([3cca521](https://github.com/ExodusMovement/assets/commit/3cca521f813fdb5e03c9d0dfaa4030b5c3b17bca))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
6
15
|
## [10.3.0](https://github.com/ExodusMovement/assets/compare/@exodus/ethereum-api@10.2.3...@exodus/ethereum-api@10.3.0) (2026-10-30)
|
|
7
16
|
|
|
8
17
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/ethereum-api",
|
|
3
|
-
"version": "10.3.
|
|
3
|
+
"version": "10.3.1",
|
|
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",
|
|
@@ -65,5 +65,5 @@
|
|
|
65
65
|
"type": "git",
|
|
66
66
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
67
67
|
},
|
|
68
|
-
"gitHead": "
|
|
68
|
+
"gitHead": "cc266df454f96bb8efd03909701476c54985e3d7"
|
|
69
69
|
}
|
package/src/hooks/monitor.js
CHANGED
|
@@ -54,7 +54,7 @@ export const createEthereumHooks = ({
|
|
|
54
54
|
const addresses = new Map()
|
|
55
55
|
const generations = new Map()
|
|
56
56
|
const commits = new Map()
|
|
57
|
-
const afterTickHook = async ({ walletAccount, refresh = false }) => {
|
|
57
|
+
const afterTickHook = async ({ walletAccount, refresh = false, monitor }) => {
|
|
58
58
|
// Order requests at entry, not after address discovery: discovery itself can
|
|
59
59
|
// be delayed while a newer explicit refresh is already running.
|
|
60
60
|
const accountKey = String(walletAccount)
|
|
@@ -126,6 +126,7 @@ export const createEthereumHooks = ({
|
|
|
126
126
|
processTxLog({
|
|
127
127
|
asset,
|
|
128
128
|
assetClientInterface,
|
|
129
|
+
monitor,
|
|
129
130
|
walletAccount,
|
|
130
131
|
batch,
|
|
131
132
|
})
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { verifyTransactionSignature } from '@exodus/ethereum-lib'
|
|
2
|
+
import assert from 'minimalistic-assert'
|
|
3
|
+
|
|
4
|
+
export async function fetchAuthenticatedTransaction({
|
|
5
|
+
baseAsset,
|
|
6
|
+
txId,
|
|
7
|
+
address,
|
|
8
|
+
server = baseAsset.server,
|
|
9
|
+
missingTransactionMessage = 'Signed transaction is required',
|
|
10
|
+
}) {
|
|
11
|
+
assert(typeof address === 'string', 'Wallet address is required for authentication')
|
|
12
|
+
const transaction = await server.getTransactionByHash(txId)
|
|
13
|
+
assert(transaction, missingTransactionMessage)
|
|
14
|
+
assert(
|
|
15
|
+
verifyTransactionSignature({
|
|
16
|
+
transaction,
|
|
17
|
+
txId,
|
|
18
|
+
address,
|
|
19
|
+
chainId: baseAsset.chainId,
|
|
20
|
+
}),
|
|
21
|
+
`Cannot authenticate transaction ${txId}`
|
|
22
|
+
)
|
|
23
|
+
return transaction
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function rpcInteger(value) {
|
|
27
|
+
const integer = BigInt(value)
|
|
28
|
+
const number = Number(integer)
|
|
29
|
+
assert(Number.isSafeInteger(number) && number >= 0, 'Transaction integer exceeds safe range')
|
|
30
|
+
return number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// A signed envelope authenticates these fields. Receipts, token events, internal
|
|
34
|
+
// transfers and inclusion status are separate execution claims, not signed data.
|
|
35
|
+
export async function authenticateHistoryTransactions({
|
|
36
|
+
transactions,
|
|
37
|
+
baseAsset,
|
|
38
|
+
address,
|
|
39
|
+
server = baseAsset.server,
|
|
40
|
+
}) {
|
|
41
|
+
const authenticated = []
|
|
42
|
+
const cache = new Map()
|
|
43
|
+
for (const item of transactions) {
|
|
44
|
+
if (item.from?.toLowerCase() !== address.toLowerCase()) {
|
|
45
|
+
authenticated.push(item)
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
let signed = cache.get(item.hash)
|
|
49
|
+
if (!signed) {
|
|
50
|
+
signed = await fetchAuthenticatedTransaction({ baseAsset, txId: item.hash, address, server })
|
|
51
|
+
cache.set(item.hash, signed)
|
|
52
|
+
}
|
|
53
|
+
authenticated.push({
|
|
54
|
+
...item,
|
|
55
|
+
hash: signed.hash,
|
|
56
|
+
from: signed.from.toLowerCase(),
|
|
57
|
+
to: signed.to?.toLowerCase() ?? null,
|
|
58
|
+
value: BigInt(signed.value).toString(),
|
|
59
|
+
nonce: rpcInteger(signed.nonce),
|
|
60
|
+
gas: rpcInteger(signed.gas),
|
|
61
|
+
input: signed.input,
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
return authenticated
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function authenticateReplacement({ baseAsset, tx, address }) {
|
|
68
|
+
const signed = await fetchAuthenticatedTransaction({
|
|
69
|
+
baseAsset,
|
|
70
|
+
txId: tx.txId,
|
|
71
|
+
address,
|
|
72
|
+
missingTransactionMessage: `ERR_BUMP_TX_DROPPED: transaction ${tx.txId} is no longer available`,
|
|
73
|
+
})
|
|
74
|
+
const type = rpcInteger(signed.type ?? '0x0')
|
|
75
|
+
// The current replacement serializer supports legacy and fee-market envelopes.
|
|
76
|
+
// Never silently discard access lists, blob commitments or 7702 authorizations.
|
|
77
|
+
assert([0, 2].includes(type), 'Unsupported replacement transaction envelope')
|
|
78
|
+
assert(!signed.accessList?.length, 'Cannot replace a transaction with an access list')
|
|
79
|
+
const nonce = rpcInteger(signed.nonce)
|
|
80
|
+
const gasLimit = rpcInteger(signed.gas)
|
|
81
|
+
const gasPrice = BigInt(type === 2 ? signed.maxFeePerGas : signed.gasPrice)
|
|
82
|
+
const pricingTx = tx.update({
|
|
83
|
+
feeAmount: baseAsset.currency.baseUnit(gasPrice * BigInt(gasLimit)),
|
|
84
|
+
data: {
|
|
85
|
+
...tx.data,
|
|
86
|
+
nonce,
|
|
87
|
+
gasLimit,
|
|
88
|
+
gasPrice: gasPrice.toString(),
|
|
89
|
+
tipGasPrice: type === 2 ? BigInt(signed.maxPriorityFeePerGas).toString() : undefined,
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
return { signed, nonce, gasLimit, pricingTx }
|
|
93
|
+
}
|
package/src/tx-create.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
assertValidTxValue,
|
|
3
2
|
currency2buffer,
|
|
4
3
|
getHighestIncentiveTxByNonce,
|
|
5
4
|
isEthereumLikeToken,
|
|
@@ -7,6 +6,7 @@ import {
|
|
|
7
6
|
import createEthereumJsTx from '@exodus/ethereum-lib/src/unsigned-tx/create-ethereumjs-tx.js'
|
|
8
7
|
import assert from 'minimalistic-assert'
|
|
9
8
|
|
|
9
|
+
import { authenticateReplacement } from './transaction-authentication.js'
|
|
10
10
|
import { ensureSaneEip1559GasPriceForTipGasPrice } from './fee-utils.js'
|
|
11
11
|
import { fetchGasLimit } from './gas-estimation.js'
|
|
12
12
|
import { getAggregateTransactionPricing, getExtraFeeData } from './get-fee.js'
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
resolveTxAttributesByTxType,
|
|
18
18
|
resolveTxFromAddress,
|
|
19
19
|
resolveTxNonce,
|
|
20
|
+
TX_TYPE_CREATE_CONTRACT,
|
|
20
21
|
TX_TYPE_TRANSFER,
|
|
21
22
|
} from './tx-type/index.js'
|
|
22
23
|
|
|
@@ -327,86 +328,45 @@ const createBumpUnsignedTx = async ({
|
|
|
327
328
|
|
|
328
329
|
const baseAsset = asset.baseAsset
|
|
329
330
|
const replacedTx = baseAssetTxLog.get(bumpTxId)
|
|
330
|
-
const assets = await assetClientInterface.getAssetsForNetwork({ baseAssetName: baseAsset.name })
|
|
331
331
|
if (!replacedTx || !replacedTx.pending) {
|
|
332
332
|
throw new Error(`Cannot bump transaction ${bumpTxId}: not found or confirmed`)
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const tokenTxSet = await assetClientInterface.getTxLog({
|
|
339
|
-
assetName: tokenAssetName,
|
|
340
|
-
walletAccount,
|
|
341
|
-
})
|
|
342
|
-
replacedTokenTx = tokenTxSet.get(bumpTxId)
|
|
343
|
-
|
|
344
|
-
if (replacedTokenTx) {
|
|
345
|
-
// Attempt to overwrite the asset to reflect the fact that
|
|
346
|
-
// we're performing a token transaction.
|
|
347
|
-
asset = assets[tokenAssetName]
|
|
348
|
-
if (!asset) {
|
|
349
|
-
throw new Error(
|
|
350
|
-
`unable to find ${tokenAssetName} during token bump transaction: asset was not available in assetsForNetwork`
|
|
351
|
-
)
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const sourceTx = replacedTokenTx || replacedTx
|
|
357
|
-
const toAddress = sourceTx.to
|
|
358
|
-
|
|
359
|
-
// For self-sends `coinAmount` is `0` (correct for balance accounting, the
|
|
360
|
-
// wallet's net delta really is zero), so we can't reconstruct the on-chain
|
|
361
|
-
// value from it. Recover from `data.rawCoinAmount`, which the tx-log
|
|
362
|
-
// writers persist alongside `coinAmount` for exactly this purpose.
|
|
363
|
-
if (sourceTx.selfSend && !sourceTx.data?.rawCoinAmount) {
|
|
364
|
-
throw new Error(
|
|
365
|
-
`Cannot bump self-send transaction ${bumpTxId}: original transfer amount is unrecoverable from the tx log (data.rawCoinAmount missing). Re-broadcast the send with a higher fee instead.`
|
|
366
|
-
)
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const amount = sourceTx.data?.rawCoinAmount
|
|
370
|
-
? asset.currency.baseUnit(sourceTx.data.rawCoinAmount)
|
|
371
|
-
: sourceTx.coinAmount.negate()
|
|
372
|
-
|
|
373
|
-
const txValue = assertValidTxValue({
|
|
374
|
-
asset,
|
|
375
|
-
amount,
|
|
376
|
-
txValue: replacedTx.data?.rawCoinAmount
|
|
377
|
-
? baseAsset.currency.baseUnit(replacedTx.data.rawCoinAmount)
|
|
378
|
-
: replacedTx.coinAmount.negate(),
|
|
335
|
+
const walletAddress = await assetClientInterface.getReceiveAddress({
|
|
336
|
+
assetName: baseAsset.name,
|
|
337
|
+
walletAccount,
|
|
379
338
|
})
|
|
380
|
-
|
|
381
|
-
const isDuplex = Boolean(replacedTokenTx && txValue.gt(baseAsset.currency.ZERO))
|
|
382
|
-
|
|
383
|
-
const txInput = replacedTx.data.data || '0x'
|
|
384
|
-
|
|
385
|
-
const isTokenTx =
|
|
386
|
-
!isDuplex /* nonpayable */ &&
|
|
387
|
-
replacedTokenTx &&
|
|
388
|
-
Object.keys(asset.contract.methodIds).includes(txInput.slice(0, 10))
|
|
389
|
-
|
|
390
|
-
const txToAddress = isTokenTx ? asset.contract.address : undefined
|
|
391
|
-
|
|
392
|
-
const replacedTxNonce = replacedTx.data.nonce
|
|
393
|
-
|
|
394
339
|
assert(
|
|
395
|
-
|
|
396
|
-
|
|
340
|
+
fromAddress.toLowerCase() === walletAddress.toLowerCase(),
|
|
341
|
+
'Replacement sender must be the wallet address'
|
|
397
342
|
)
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
343
|
+
const { signed, nonce, gasLimit, pricingTx } = await authenticateReplacement({
|
|
344
|
+
baseAsset,
|
|
345
|
+
tx: replacedTx,
|
|
346
|
+
address: fromAddress,
|
|
347
|
+
})
|
|
348
|
+
if (maybeProvidedNonce !== undefined && maybeProvidedNonce !== nonce) {
|
|
403
349
|
throw new Error('incorrect nonce for replacement transaction')
|
|
404
350
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
351
|
+
// Reuse the exact signed call. In particular, never reconstruct token calldata
|
|
352
|
+
// from indexer effects or use a tx-log recipient as the outer destination.
|
|
353
|
+
const txInput = signed.input
|
|
354
|
+
const txToAddress = signed.to ?? null
|
|
355
|
+
const txValue = baseAsset.currency.baseUnit(BigInt(signed.value))
|
|
356
|
+
let toAddress = txToAddress
|
|
357
|
+
let amount = asset === baseAsset ? txValue : asset.currency.ZERO
|
|
358
|
+
if (
|
|
359
|
+
asset !== baseAsset &&
|
|
360
|
+
asset.contract?.address.toLowerCase() === txToAddress?.toLowerCase() &&
|
|
361
|
+
Object.hasOwn(asset.contract.methodIds, txInput.slice(0, 10))
|
|
362
|
+
) {
|
|
363
|
+
const { method, values } = asset.contract.decodeInput(Buffer.from(txInput.slice(2), 'hex'))
|
|
364
|
+
if (method === 'transfer' || method === 'approve') {
|
|
365
|
+
toAddress = values[0]
|
|
366
|
+
amount = asset.currency.baseUnit(values[1])
|
|
367
|
+
}
|
|
368
|
+
}
|
|
408
369
|
await verifyBumpTxCanReplace({ baseAsset, bumpTxId, fromAddress, nonce })
|
|
409
|
-
|
|
410
370
|
const resolvedTxAttributes = await resolveTxAttributesByTxType({
|
|
411
371
|
asset,
|
|
412
372
|
assetClientInterface,
|
|
@@ -416,36 +376,46 @@ const createBumpUnsignedTx = async ({
|
|
|
416
376
|
txInput,
|
|
417
377
|
toAddress,
|
|
418
378
|
txToAddress,
|
|
419
|
-
txType,
|
|
379
|
+
txType: txToAddress === null ? TX_TYPE_CREATE_CONTRACT : TX_TYPE_TRANSFER,
|
|
420
380
|
txValue,
|
|
421
381
|
walletAccount,
|
|
422
382
|
})
|
|
423
383
|
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
const maybeTxToReplace = await getHighestIncentiveTxByNonce({
|
|
384
|
+
const prices = await resolveTxFactoryGasPrices({
|
|
427
385
|
assetClientInterface,
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
386
|
+
baseAsset,
|
|
387
|
+
feeData,
|
|
388
|
+
nonce,
|
|
389
|
+
txToReplace: pricingTx,
|
|
431
390
|
})
|
|
391
|
+
const { eip1559Enabled } = prices
|
|
392
|
+
let { resolvedGasPrice: gasPrice, resolvedTipGasPrice: tipGasPrice } = prices
|
|
432
393
|
|
|
433
|
-
|
|
434
|
-
maybeTxToReplace,
|
|
435
|
-
`unable to resolve pending transaction for nonce ${resolvedTxAttributes.nonce}`
|
|
436
|
-
)
|
|
437
|
-
|
|
438
|
-
const {
|
|
439
|
-
eip1559Enabled,
|
|
440
|
-
resolvedGasPrice: gasPrice,
|
|
441
|
-
resolvedTipGasPrice: tipGasPrice,
|
|
442
|
-
} = await resolveTxFactoryGasPrices({
|
|
394
|
+
const competingTx = await getHighestIncentiveTxByNonce({
|
|
443
395
|
assetClientInterface,
|
|
444
|
-
baseAsset,
|
|
445
|
-
feeData,
|
|
396
|
+
asset: baseAsset,
|
|
446
397
|
nonce,
|
|
447
|
-
|
|
398
|
+
walletAccount,
|
|
448
399
|
})
|
|
400
|
+
if (competingTx && competingTx.txId !== bumpTxId) {
|
|
401
|
+
const competing = await authenticateReplacement({
|
|
402
|
+
baseAsset,
|
|
403
|
+
tx: competingTx,
|
|
404
|
+
address: fromAddress,
|
|
405
|
+
})
|
|
406
|
+
assert(competing.nonce === nonce, 'Replacement nonce does not match signed transaction')
|
|
407
|
+
const competingPrices = await resolveTxFactoryGasPrices({
|
|
408
|
+
assetClientInterface,
|
|
409
|
+
baseAsset,
|
|
410
|
+
feeData,
|
|
411
|
+
nonce,
|
|
412
|
+
txToReplace: competing.pricingTx,
|
|
413
|
+
})
|
|
414
|
+
if (competingPrices.resolvedGasPrice.gt(gasPrice)) gasPrice = competingPrices.resolvedGasPrice
|
|
415
|
+
if (eip1559Enabled && competingPrices.resolvedTipGasPrice.gt(tipGasPrice)) {
|
|
416
|
+
tipGasPrice = competingPrices.resolvedTipGasPrice
|
|
417
|
+
}
|
|
418
|
+
}
|
|
449
419
|
|
|
450
420
|
return createUnsignedTxWithFees({
|
|
451
421
|
...resolvedTxAttributes,
|
|
@@ -540,6 +510,24 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
|
|
|
540
510
|
walletAccount,
|
|
541
511
|
})
|
|
542
512
|
|
|
513
|
+
if (maybeTxToReplace) {
|
|
514
|
+
const walletAddress = await assetClientInterface.getReceiveAddress({
|
|
515
|
+
assetName: baseAsset.name,
|
|
516
|
+
walletAccount,
|
|
517
|
+
})
|
|
518
|
+
assert(
|
|
519
|
+
fromAddress.toLowerCase() === walletAddress.toLowerCase(),
|
|
520
|
+
'Replacement sender must be the wallet address'
|
|
521
|
+
)
|
|
522
|
+
}
|
|
523
|
+
const pricingTxToReplace = maybeTxToReplace
|
|
524
|
+
? await authenticateReplacement({ baseAsset, tx: maybeTxToReplace, address: fromAddress })
|
|
525
|
+
: undefined
|
|
526
|
+
assert(
|
|
527
|
+
!pricingTxToReplace || pricingTxToReplace.nonce === nonce,
|
|
528
|
+
'Replacement nonce does not match signed transaction'
|
|
529
|
+
)
|
|
530
|
+
|
|
543
531
|
const {
|
|
544
532
|
eip1559Enabled,
|
|
545
533
|
resolvedGasPrice: gasPrice,
|
|
@@ -552,7 +540,7 @@ export const createTxFactory = ({ chainId, assetClientInterface, useAbsoluteNonc
|
|
|
552
540
|
gasPrice: providedGasPrice,
|
|
553
541
|
nonce,
|
|
554
542
|
tipGasPrice: providedTipGasPrice,
|
|
555
|
-
txToReplace:
|
|
543
|
+
txToReplace: pricingTxToReplace?.pricingTx,
|
|
556
544
|
})
|
|
557
545
|
|
|
558
546
|
if (nft) {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { BaseMonitor } from '@exodus/asset-lib'
|
|
2
|
+
import { Tx } from '@exodus/models'
|
|
3
|
+
import assert from 'minimalistic-assert'
|
|
4
|
+
|
|
5
|
+
import { fetchAuthenticatedTransaction, rpcInteger } from '../transaction-authentication.js'
|
|
6
|
+
|
|
7
|
+
export class AuthenticatedEthereumMonitor extends BaseMonitor {
|
|
8
|
+
async verifyNewSentTransactions({ assetName = this.asset.name, logItems, walletAccount }) {
|
|
9
|
+
if (logItems.length === 0) return
|
|
10
|
+
let address
|
|
11
|
+
let txLog
|
|
12
|
+
for (const item of logItems) {
|
|
13
|
+
const tx = Tx.fromJSON(item)
|
|
14
|
+
if (tx.dropped) {
|
|
15
|
+
txLog ??= await this.aci.getTxLog({ assetName, walletAccount })
|
|
16
|
+
const existing = txLog.get(tx.txId)
|
|
17
|
+
// A disappeared envelope must not prevent marking the stored transaction
|
|
18
|
+
// dropped. Only its status may change without reauthentication.
|
|
19
|
+
if (existing && tx.equals(existing.update({ dropped: true, error: tx.error }))) continue
|
|
20
|
+
}
|
|
21
|
+
if (!tx.sent) {
|
|
22
|
+
if (!tx.from?.length) continue
|
|
23
|
+
address ??= await this.aci.getReceiveAddress({
|
|
24
|
+
assetName: this.asset.name,
|
|
25
|
+
walletAccount,
|
|
26
|
+
useCache: true,
|
|
27
|
+
})
|
|
28
|
+
if (!tx.from.some((from) => from.toLowerCase() === address.toLowerCase())) continue
|
|
29
|
+
}
|
|
30
|
+
assert(
|
|
31
|
+
(await this.verifyTransactionAuthentication({ tx, assetName, walletAccount })) === true,
|
|
32
|
+
`Cannot authenticate outgoing transaction ${tx.txId}`
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async verifyTransactionAuthentication(params) {
|
|
38
|
+
if (typeof this.asset.verifyTransactionAuthentication === 'function') {
|
|
39
|
+
return super.verifyTransactionAuthentication(params)
|
|
40
|
+
}
|
|
41
|
+
const address = await this.aci.getReceiveAddress({
|
|
42
|
+
assetName: this.asset.name,
|
|
43
|
+
walletAccount: params.walletAccount,
|
|
44
|
+
useCache: true,
|
|
45
|
+
})
|
|
46
|
+
const transaction = await fetchAuthenticatedTransaction({
|
|
47
|
+
baseAsset: this.asset,
|
|
48
|
+
txId: params.tx.txId,
|
|
49
|
+
address,
|
|
50
|
+
server: this.server,
|
|
51
|
+
})
|
|
52
|
+
const tx = params.tx
|
|
53
|
+
// Bind the fields used by acceleration even for entries already in the log.
|
|
54
|
+
// Token destinations/amounts describe effects rather than the outer envelope;
|
|
55
|
+
// replacement creation always uses the verified envelope itself.
|
|
56
|
+
if (tx.data.nonce !== undefined && tx.data.nonce !== rpcInteger(transaction.nonce)) return false
|
|
57
|
+
if (tx.data.gasLimit !== undefined && tx.data.gasLimit !== rpcInteger(transaction.gas)) {
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
if (
|
|
61
|
+
tx.data.data !== undefined &&
|
|
62
|
+
tx.data.data.toLowerCase() !== transaction.input.toLowerCase()
|
|
63
|
+
) {
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
if (params.assetName === this.asset.name) {
|
|
67
|
+
// Base-asset fee entries for token operations can display the token
|
|
68
|
+
// recipient. That execution effect is not the envelope's destination.
|
|
69
|
+
if (
|
|
70
|
+
tx.tokens.length === 0 &&
|
|
71
|
+
tx.to &&
|
|
72
|
+
tx.to.toLowerCase() !== (transaction.to ?? address).toLowerCase()
|
|
73
|
+
) {
|
|
74
|
+
return false
|
|
75
|
+
}
|
|
76
|
+
if (
|
|
77
|
+
tx.data.rawCoinAmount !== undefined &&
|
|
78
|
+
BigInt(tx.data.rawCoinAmount) !== BigInt(transaction.value)
|
|
79
|
+
) {
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { BaseMonitor } from '@exodus/asset-lib'
|
|
2
1
|
import { getAssetAddresses } from '@exodus/ethereum-lib'
|
|
3
2
|
import lodash from 'lodash'
|
|
4
3
|
import assert from 'minimalistic-assert'
|
|
5
4
|
|
|
5
|
+
import { AuthenticatedEthereumMonitor } from './authenticated-monitor.js'
|
|
6
6
|
import { applyRemoteConfigEip7702Whitelist, createEip7702Whitelist } from '../eip7702-whitelist.js'
|
|
7
|
+
import { authenticateHistoryTransactions } from '../transaction-authentication.js'
|
|
7
8
|
import { executeEthLikeFeeMonitorUpdate } from '../fee-utils.js'
|
|
8
9
|
import {
|
|
9
10
|
filterEffects,
|
|
@@ -25,7 +26,7 @@ import {
|
|
|
25
26
|
|
|
26
27
|
const { isEmpty } = lodash
|
|
27
28
|
|
|
28
|
-
export class ClarityMonitor extends
|
|
29
|
+
export class ClarityMonitor extends AuthenticatedEthereumMonitor {
|
|
29
30
|
constructor({
|
|
30
31
|
server,
|
|
31
32
|
config,
|
|
@@ -255,6 +256,13 @@ export class ClarityMonitor extends BaseMonitor {
|
|
|
255
256
|
walletAccount,
|
|
256
257
|
}))
|
|
257
258
|
|
|
259
|
+
allTxs = await authenticateHistoryTransactions({
|
|
260
|
+
transactions: allTxs,
|
|
261
|
+
baseAsset: this.asset,
|
|
262
|
+
server: this.server,
|
|
263
|
+
address: derivedData.ourWalletAddress,
|
|
264
|
+
})
|
|
265
|
+
|
|
258
266
|
const logItemsByAsset = this.getAllLogItemsByAsset({
|
|
259
267
|
getLogItemsFromServerTx,
|
|
260
268
|
ourWalletAddress: derivedData.ourWalletAddress,
|
|
@@ -277,6 +285,12 @@ export class ClarityMonitor extends BaseMonitor {
|
|
|
277
285
|
...derivedData,
|
|
278
286
|
})
|
|
279
287
|
|
|
288
|
+
await this.updateTxLogByAssetBatch({
|
|
289
|
+
logItemsByAsset,
|
|
290
|
+
walletAccount,
|
|
291
|
+
refresh,
|
|
292
|
+
batch,
|
|
293
|
+
})
|
|
280
294
|
this.aci.removeTxLogBatch({
|
|
281
295
|
assetName,
|
|
282
296
|
walletAccount,
|
|
@@ -284,16 +298,6 @@ export class ClarityMonitor extends BaseMonitor {
|
|
|
284
298
|
batch,
|
|
285
299
|
})
|
|
286
300
|
|
|
287
|
-
for (const [assetName, txs] of Object.entries(logItemsByAsset)) {
|
|
288
|
-
this.aci.updateTxLogAndNotifyBatch({
|
|
289
|
-
assetName,
|
|
290
|
-
walletAccount,
|
|
291
|
-
txs,
|
|
292
|
-
refresh,
|
|
293
|
-
batch,
|
|
294
|
-
})
|
|
295
|
-
}
|
|
296
|
-
|
|
297
301
|
historyResponse = response
|
|
298
302
|
processedLogItemsByAsset = logItemsByAsset
|
|
299
303
|
} catch (error) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { BaseMonitor } from '@exodus/asset-lib'
|
|
2
1
|
import { SynchronizedTime } from '@exodus/basic-utils'
|
|
3
2
|
import { Tx } from '@exodus/models'
|
|
4
3
|
import lodash from 'lodash'
|
|
5
4
|
|
|
5
|
+
import { AuthenticatedEthereumMonitor } from './authenticated-monitor.js'
|
|
6
6
|
import { applyRemoteConfigEip7702Whitelist, createEip7702Whitelist } from '../eip7702-whitelist.js'
|
|
7
7
|
import { getBatchedRpcBalances } from './monitor-utils/get-batched-rpc-balances.js'
|
|
8
8
|
import { UNCONFIRMED_TX_LIMIT } from './monitor-utils/get-derive-transactions-to-check.js'
|
|
@@ -25,7 +25,7 @@ const getMissingNonce = (tx, txFromNode) => {
|
|
|
25
25
|
|
|
26
26
|
// The base ethereum monitor no history class handles listening for assets with no history
|
|
27
27
|
|
|
28
|
-
export class EthereumNoHistoryMonitor extends
|
|
28
|
+
export class EthereumNoHistoryMonitor extends AuthenticatedEthereumMonitor {
|
|
29
29
|
constructor({ server, config, eip7702Supported, eip7702Whitelist, getBlackListStatus, ...args }) {
|
|
30
30
|
super(args)
|
|
31
31
|
this.server = server
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import assert from 'minimalistic-assert'
|
|
2
|
+
|
|
1
3
|
import { getPolygonUndelegateTxInEthereumTxLog } from '../staking/matic/matic-staking-utils.js'
|
|
2
4
|
import { assetStakingTxProps } from './asset-staking-tx-data.js'
|
|
3
5
|
|
|
@@ -17,7 +19,13 @@ const getAssetExpandedTxLog = async ({ assetName, aci, txs, walletAccount }) =>
|
|
|
17
19
|
return additionalTxs
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
const processTxLog = async ({
|
|
22
|
+
const processTxLog = async ({
|
|
23
|
+
asset,
|
|
24
|
+
assetClientInterface: aci,
|
|
25
|
+
walletAccount,
|
|
26
|
+
batch,
|
|
27
|
+
monitor,
|
|
28
|
+
}) => {
|
|
21
29
|
const { name: assetName, currency } = asset
|
|
22
30
|
const txs = await aci.getTxLog({ assetName, walletAccount })
|
|
23
31
|
|
|
@@ -36,6 +44,11 @@ const processTxLog = async ({ asset, assetClientInterface: aci, walletAccount, b
|
|
|
36
44
|
const expandedTxs = await getAssetExpandedTxLog({ assetName, aci, txs, walletAccount })
|
|
37
45
|
newTxs.push(...expandedTxs)
|
|
38
46
|
|
|
47
|
+
if (expandedTxs.length > 0) {
|
|
48
|
+
assert(monitor, 'History monitor is required to authenticate new staking transactions')
|
|
49
|
+
await monitor.verifyNewSentTransactions({ assetName, logItems: expandedTxs, walletAccount })
|
|
50
|
+
}
|
|
51
|
+
|
|
39
52
|
if (newTxs.length > 0) {
|
|
40
53
|
aci.updateTxLogAndNotifyBatch({ assetName, walletAccount, txs: newTxs, batch })
|
|
41
54
|
}
|