@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 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.0",
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": "99c7d9d4aae75358a0269479e056e5785fbad5ed"
68
+ "gitHead": "cc266df454f96bb8efd03909701476c54985e3d7"
69
69
  }
@@ -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
- let replacedTokenTx
336
- if (replacedTx.tokens.length > 0) {
337
- const [tokenAssetName] = replacedTx.tokens
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
- Number.isInteger(replacedTxNonce),
396
- `Cannot bump transaction ${bumpTxId}: data object seems to be corrupted`
340
+ fromAddress.toLowerCase() === walletAddress.toLowerCase(),
341
+ 'Replacement sender must be the wallet address'
397
342
  )
398
-
399
- // If we have evaluated a bump transaction and the `providedNonce` differs
400
- // from the `bumpNonce`, we've encountered a conflict and cannot respect
401
- // the caller's request.
402
- if (maybeProvidedNonce && maybeProvidedNonce !== replacedTxNonce) {
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
- const nonce = maybeProvidedNonce ?? replacedTxNonce
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 gasLimit = replacedTx.data.gasLimit
425
-
426
- const maybeTxToReplace = await getHighestIncentiveTxByNonce({
384
+ const prices = await resolveTxFactoryGasPrices({
427
385
  assetClientInterface,
428
- asset,
429
- nonce: resolvedTxAttributes.nonce,
430
- walletAccount,
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
- assert(
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
- txToReplace: maybeTxToReplace,
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: maybeTxToReplace,
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 BaseMonitor {
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 BaseMonitor {
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 ({ asset, assetClientInterface: aci, walletAccount, batch }) => {
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
  }