From 2282842374310ca1fb2a52e8e5d422d29eed8bd7 Mon Sep 17 00:00:00 2001 From: Shane Jonas Date: Fri, 8 Dec 2023 09:15:07 -0500 Subject: [PATCH 001/100] WIP TransactionController MultiChain --- .../src/TransactionController.ts | 104 ++++++++++++++++-- 1 file changed, 94 insertions(+), 10 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 23f2c77067..210ce575a2 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -24,6 +24,8 @@ import EthQuery from '@metamask/eth-query'; import type { GasFeeState } from '@metamask/gas-fee-controller'; import type { BlockTracker, + NetworkClientId, + NetworkController, NetworkState, Provider, } from '@metamask/network-controller'; @@ -288,6 +290,8 @@ export class TransactionController extends BaseControllerV1< transactionMeta: TransactionMeta, ) => (TransactionMeta | undefined)[]; + private readonly getNetworkClientById: NetworkController['getNetworkClientById']; + private failTransaction( transactionMeta: TransactionMeta, error: Error, @@ -327,6 +331,15 @@ export class TransactionController extends BaseControllerV1< */ override name = 'TransactionController'; + private readonly trackingMap: Map< + NetworkClientId, + Set<{ + nonceTracker: NonceTracker; + pendingTransactionTracker: PendingTransactionTracker; + incomingTransactionHelper: IncomingTransactionHelper; + }> + > = new Map(); + /** * Method used to sign transactions */ @@ -365,6 +378,7 @@ export class TransactionController extends BaseControllerV1< * @param options.provider - The provider used to create the underlying EthQuery instance. * @param options.securityProviderRequest - A function for verifying a transaction, whether it is malicious or not. * @param options.speedUpMultiplier - Multiplier used to determine a transaction's increased gas fee during speed up. + * @param options.getNetworkClientById - Gets the network client with the given id from the NetworkController. * @param options.hooks - The controller hooks. * @param options.hooks.afterSign - Additional logic to execute after signing a transaction. Return false to not change the status to signed. * @param options.hooks.beforeApproveOnInit - Additional logic to execute before starting an approval flow for a transaction during initialization. Return false to skip the transaction. @@ -396,6 +410,7 @@ export class TransactionController extends BaseControllerV1< provider, securityProviderRequest, speedUpMultiplier, + getNetworkClientById, hooks = {}, }: { blockTracker: BlockTracker; @@ -427,6 +442,7 @@ export class TransactionController extends BaseControllerV1< provider: Provider; securityProviderRequest?: SecurityProviderRequest; speedUpMultiplier?: number; + getNetworkClientById: NetworkController['getNetworkClientById']; hooks: { afterSign?: ( transactionMeta: TransactionMeta, @@ -458,7 +474,7 @@ export class TransactionController extends BaseControllerV1< }; this.initialize(); - + this.getNetworkClientById = getNetworkClientById; this.provider = provider; this.messagingSystem = messenger; this.getNetworkState = getNetworkState; @@ -1053,6 +1069,69 @@ export class TransactionController extends BaseControllerV1< this.hub.emit(`${transactionMeta.id}:speedup`, newTransactionMeta); } + startTrackingByNetworkClientId(networkClientId: NetworkClientId) { + const networkClient = this.getNetworkClientById(networkClientId); + // track using tracking map + this.trackingMap.set(networkClientId, new Set()); + const nonceTracker = new NonceTracker({ + provider: networkClient.provider as any, + blockTracker: networkClient.blockTracker, + getPendingTransactions: + this.getNonceTrackerPendingTransactions.bind(this), + getConfirmedTransactions: this.getNonceTrackerTransactions.bind( + this, + TransactionStatus.confirmed, + ), + }); + const incomingTransactionHelper = new IncomingTransactionHelper({ + blockTracker: networkClient.blockTracker, + getCurrentAccount: this.getSelectedAddress, + getLastFetchedBlockNumbers: () => this.state.lastFetchedBlockNumbers, + getNetworkState: this.getNetworkState, // TODO: fake this via networkClient + isEnabled: () => true, + queryEntireHistory: true, + remoteTransactionSource: new EtherscanRemoteTransactionSource({ + includeTokenTransfers: true, + }), + transactionLimit: this.config.txHistoryLimit, + updateTransactions: true, + }); + const pendingTransactionTracker = new PendingTransactionTracker({ + approveTransaction: this.approveTransaction.bind(this), + blockTracker: networkClient.provider as any, + getChainId: () => networkClient.configuration.chainId, + getEthQuery: () => this.ethQuery, // TODO: use networkClient to construct ethQuery + getTransactions: () => this.state.transactions, + isResubmitEnabled: true, // TODO: make this configurable + nonceTracker, + onStateChange: this.subscribe.bind(this), + publishTransaction: this.publishTransaction.bind(this), + hooks: { + beforeCheckPendingTransaction: + this.beforeCheckPendingTransaction.bind(this), + beforePublish: this.beforePublish.bind(this), + }, + }); + // subscribe to trackers + incomingTransactionHelper.hub.on( + 'transactions', + this.onIncomingTransactions.bind(this), + ); + + incomingTransactionHelper.hub.on( + 'updatedLastFetchedBlockNumbers', + this.onUpdatedLastFetchedBlockNumbers.bind(this), + ); + this.addPendingTransactionTrackerListeners(pendingTransactionTracker); + + // add to tracking map + this.trackingMap.get(networkClientId)?.add({ + nonceTracker, + incomingTransactionHelper, + pendingTransactionTracker, + }); + } + /** * Estimates required gas for a given transaction. * @@ -2528,23 +2607,25 @@ export class TransactionController extends BaseControllerV1< ); } - private addPendingTransactionTrackerListeners() { - this.pendingTransactionTracker.hub.on( + private addPendingTransactionTrackerListeners( + pendingTransactionTracker = this.pendingTransactionTracker, + ) { + pendingTransactionTracker.hub.on( 'transaction-confirmed', this.onConfirmedTransaction.bind(this), ); - this.pendingTransactionTracker.hub.on( + pendingTransactionTracker.hub.on( 'transaction-dropped', this.setTransactionStatusDropped.bind(this), ); - this.pendingTransactionTracker.hub.on( + pendingTransactionTracker.hub.on( 'transaction-failed', this.failTransaction.bind(this), ); - this.pendingTransactionTracker.hub.on( + pendingTransactionTracker.hub.on( 'transaction-updated', this.updateTransaction.bind(this), ); @@ -2609,10 +2690,14 @@ export class TransactionController extends BaseControllerV1< this.hub.emit('transaction-status-update', { transactionMeta }); } - private getNonceTrackerPendingTransactions(address: string) { + private getNonceTrackerPendingTransactions( + address: string, + chainId?: string, + ) { const standardPendingTransactions = this.getNonceTrackerTransactions( TransactionStatus.submitted, address, + chainId, ); const externalPendingTransactions = @@ -2624,11 +2709,10 @@ export class TransactionController extends BaseControllerV1< private getNonceTrackerTransactions( status: TransactionStatus, address: string, + chainId: string = this.getChainId(), ) { - const currentChainId = this.getChainId(); - return getAndFormatTransactionsForNonceTracker( - currentChainId, + chainId, address, status, this.state.transactions, From e12b8e31394df93ed729e5869cb871e95dad36b6 Mon Sep 17 00:00:00 2001 From: Shane Jonas Date: Mon, 11 Dec 2023 10:06:33 -0500 Subject: [PATCH 002/100] Fixed tx controller to only use one EtherscanRemoteTransactionSource --- .../src/TransactionController.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 210ce575a2..c45c18ee79 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -292,6 +292,8 @@ export class TransactionController extends BaseControllerV1< private readonly getNetworkClientById: NetworkController['getNetworkClientById']; + private readonly etherscanRemoteTransactionSource: EtherscanRemoteTransactionSource; + private failTransaction( transactionMeta: TransactionMeta, error: Error, @@ -520,6 +522,11 @@ export class TransactionController extends BaseControllerV1< ), }); + this.etherscanRemoteTransactionSource = + new EtherscanRemoteTransactionSource({ + includeTokenTransfers: incomingTransactions.includeTokenTransfers, + }); + this.incomingTransactionHelper = new IncomingTransactionHelper({ blockTracker, getCurrentAccount: getSelectedAddress, @@ -527,9 +534,7 @@ export class TransactionController extends BaseControllerV1< getNetworkState, isEnabled: incomingTransactions.isEnabled, queryEntireHistory: incomingTransactions.queryEntireHistory, - remoteTransactionSource: new EtherscanRemoteTransactionSource({ - includeTokenTransfers: incomingTransactions.includeTokenTransfers, - }), + remoteTransactionSource: this.etherscanRemoteTransactionSource, transactionLimit: this.config.txHistoryLimit, updateTransactions: incomingTransactions.updateTransactions, }); @@ -1090,9 +1095,7 @@ export class TransactionController extends BaseControllerV1< getNetworkState: this.getNetworkState, // TODO: fake this via networkClient isEnabled: () => true, queryEntireHistory: true, - remoteTransactionSource: new EtherscanRemoteTransactionSource({ - includeTokenTransfers: true, - }), + remoteTransactionSource: this.etherscanRemoteTransactionSource, transactionLimit: this.config.txHistoryLimit, updateTransactions: true, }); @@ -2672,7 +2675,7 @@ export class TransactionController extends BaseControllerV1< 'TransactionController#approveTransaction - Transaction signed', ); - this.onTransactionStatusChange(transactionMeta); + this.onTransactionStatusChange(transactionMeta); // TODO: fake this via networkClient const rawTx = bufferToHex(signedTx.serialize()); From a968ee7772c549c38712a2379410ba127d51360a Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Wed, 13 Dec 2023 14:06:29 -0600 Subject: [PATCH 003/100] TransactionController networkClientId updates (#3652) * Update `getCurrentNetworkEIP1559Compatibility` hook type to accept optional networkClientId param * It already does this in networkController, but perhaps it should be renamed getEIP1559Compatibility? * Update `getExternalPendingTransactions` hook type to accept optional chainId param * SmartTransactionController and extension need to be updated * Add `getNetworkClientIdForDomain` hook to constructor param * Add `getEthQuery` method that accepts optional networkClientId and returns the correct EthQuery instance * Update `addTransaction` * Add optional `networkClientId` to options object param * Use correct networkClientId/chainId * Update `stopTransaction` to use correct networkClientId/chainId * Update `estimateGas` * Add optional `networkClientId` param * Use correct networkClientId/chainId * Update `estimateGasBuffered` to accept optional networkClientId * SmartTransactionController and extension need to be updated * Update `updateGasProperties` to use correct networkClientId (from txMeta) * Update `approveTransaction` to use correct networkClientId (from txMeta) * Update `publishTransaction` to accept required ethQuery * Update `getEIP1559Compatibility` to accept optional networkClientId * GasFeeController has a similar method of it's own that we may need to update * Update `getNonceTrackerPendingTransactions` to pass `chainId` to `getExternalPendingTransactions` * Add optional `networkClientId` to `TransactionMeta` type * Update `UpdateGasRequest` to accept either providerConfig or NetworkClientConfiguration * Probably should rename the arg from `providerConfig` Questions: * should `wipeTransactions` be updated to filter by optional chainId? * should `getNonceLock` take in chainId (non-optional) to get correct nonceTracker nonceLock? * should `getTransactions` accept chainId and/or networkClientId param for filtering? * `createApprovalsForUnapprovedTransactions` isn't used anywhere in our repos? Remove it? * `approveTransaction` sets txMeta.txParams.chainId to the currentChainId. Shouldn't it read the value from txMeta instead? * shouldn't `addExternalTransaction` filter against the txMeta chainId, not the currentChainId? * shouldn't `markNonceDuplicatesDropped ` filter against the txMeta chainId, not the currentChainId? --------- Co-authored-by: Jiexi Luan --- .../src/TransactionController.ts | 121 +++++++++++++----- .../src/helpers/PendingTransactionTracker.ts | 13 +- packages/transaction-controller/src/types.ts | 6 + .../src/utils/gas-fees.ts | 20 ++- .../transaction-controller/src/utils/gas.ts | 5 +- 5 files changed, 120 insertions(+), 45 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index c45c18ee79..5caa38ef16 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -28,6 +28,7 @@ import type { NetworkController, NetworkState, Provider, + ProviderConfig, } from '@metamask/network-controller'; import { errorCodes, rpcErrors, providerErrors } from '@metamask/rpc-errors'; import type { Hex } from '@metamask/utils'; @@ -43,6 +44,7 @@ import type { } from 'nonce-tracker'; import { v1 as random } from 'uuid'; +import type { SelectedNetworkController } from '../../selected-network-controller/src/SelectedNetworkController'; import { EtherscanRemoteTransactionSource } from './helpers/EtherscanRemoteTransactionSource'; import { IncomingTransactionHelper } from './helpers/IncomingTransactionHelper'; import { PendingTransactionTracker } from './helpers/PendingTransactionTracker'; @@ -96,6 +98,7 @@ import { validateTransactionOrigin, validateTxParams, } from './utils/validation'; +import { NetworkClientConfiguration } from '@metamask/network-controller'; export const HARDFORK = Hardfork.London; @@ -247,7 +250,9 @@ export class TransactionController extends BaseControllerV1< private readonly getCurrentAccountEIP1559Compatibility: () => Promise; - private readonly getCurrentNetworkEIP1559Compatibility: () => Promise; + private readonly getCurrentNetworkEIP1559Compatibility: ( + networkClientId?: NetworkClientId, + ) => Promise; private readonly getGasFeeEstimates: () => Promise; @@ -257,6 +262,7 @@ export class TransactionController extends BaseControllerV1< private readonly getExternalPendingTransactions: ( address: string, + chainId?: string, ) => NonceTrackerTransaction[]; private readonly messagingSystem: TransactionControllerMessenger; @@ -292,6 +298,8 @@ export class TransactionController extends BaseControllerV1< private readonly getNetworkClientById: NetworkController['getNetworkClientById']; + private readonly getNetworkClientIdForDomain: SelectedNetworkController['getNetworkClientIdForDomain']; + private readonly etherscanRemoteTransactionSource: EtherscanRemoteTransactionSource; private failTransaction( @@ -389,6 +397,7 @@ export class TransactionController extends BaseControllerV1< * @param options.hooks.getAdditionalSignArguments - Returns additional arguments required to sign a transaction. * @param config - Initial options used to configure this controller. * @param state - Initial state to set on this controller. + * @param options.getNetworkClientIdForDomain */ constructor( { @@ -413,6 +422,7 @@ export class TransactionController extends BaseControllerV1< securityProviderRequest, speedUpMultiplier, getNetworkClientById, + getNetworkClientIdForDomain, hooks = {}, }: { blockTracker: BlockTracker; @@ -424,6 +434,7 @@ export class TransactionController extends BaseControllerV1< getCurrentNetworkEIP1559Compatibility: () => Promise; getExternalPendingTransactions?: ( address: string, + chainId?: string, ) => NonceTrackerTransaction[]; getGasFeeEstimates?: () => Promise; getNetworkState: () => NetworkState; @@ -445,6 +456,7 @@ export class TransactionController extends BaseControllerV1< securityProviderRequest?: SecurityProviderRequest; speedUpMultiplier?: number; getNetworkClientById: NetworkController['getNetworkClientById']; + getNetworkClientIdForDomain: SelectedNetworkController['getNetworkClientIdForDomain']; hooks: { afterSign?: ( transactionMeta: TransactionMeta, @@ -500,6 +512,8 @@ export class TransactionController extends BaseControllerV1< this.cancelMultiplier = cancelMultiplier ?? CANCEL_RATE; this.speedUpMultiplier = speedUpMultiplier ?? SPEED_UP_RATE; + this.getNetworkClientIdForDomain = getNetworkClientIdForDomain; + this.afterSign = hooks?.afterSign ?? (() => true); this.beforeApproveOnInit = hooks?.beforeApproveOnInit ?? (() => true); this.beforeCheckPendingTransaction = @@ -603,6 +617,13 @@ export class TransactionController extends BaseControllerV1< } } + getEthQuery(networkClientId?: NetworkClientId): EthQuery { + if (networkClientId) { + return new EthQuery(this.getNetworkClientById(networkClientId).provider); + } + return this.ethQuery; + } + /** * Add a new unapproved transaction to state. Parameters will be validated, a * unique transaction id will be generated, and gas and gasPrice will be calculated @@ -621,6 +642,7 @@ export class TransactionController extends BaseControllerV1< * @param opts.swaps - Options for swaps transactions. * @param opts.swaps.hasApproveTx - Whether the transaction has an approval transaction. * @param opts.swaps.meta - Metadata for swap transaction. + * @param opts.networkClientId * @returns Object containing a promise resolving to the transaction hash if approved. */ async addTransaction( @@ -635,6 +657,7 @@ export class TransactionController extends BaseControllerV1< sendFlowHistory, swaps = {}, type, + networkClientId, }: { actionId?: string; deviceConfirmedOn?: WalletDevice; @@ -648,13 +671,21 @@ export class TransactionController extends BaseControllerV1< meta?: Partial; }; type?: TransactionType; + networkClientId?: NetworkClientId; } = {}, ): Promise { log('Adding transaction', txParams); + // TODO(JL): Revisit this fallback during implementation + // networkClientId ??= this.getNetworkClientIdForDomain( + // origin ?? ORIGIN_METAMASK, + // ); + txParams = normalizeTxParams(txParams); - const isEIP1559Compatible = await this.getEIP1559Compatibility(); + const isEIP1559Compatible = await this.getEIP1559Compatibility( + networkClientId, + ); validateTxParams(txParams, isEIP1559Compatible); @@ -672,11 +703,13 @@ export class TransactionController extends BaseControllerV1< origin, ); + const ethQuery = this.getEthQuery(networkClientId); + const transactionType = - type ?? (await determineTransactionType(txParams, this.ethQuery)).type; + type ?? (await determineTransactionType(txParams, ethQuery)).type; const existingTransactionMeta = this.getTransactionWithActionId(actionId); - const chainId = this.getChainId(); + const chainId = this.getChainId(networkClientId); // If a request to add a transaction with the same actionId is submitted again, a new transaction will not be created for it. const transactionMeta: TransactionMeta = existingTransactionMeta || { @@ -694,6 +727,7 @@ export class TransactionController extends BaseControllerV1< userEditedGasLimit: false, verifiedOnBlockchain: false, type: transactionType, + networkClientId, }; await this.updateGasProperties(transactionMeta); @@ -874,11 +908,13 @@ export class TransactionController extends BaseControllerV1< txParams: newTxParams, }); - const hash = await this.publishTransaction(rawTx); + const ethQuery = this.getEthQuery(transactionMeta.networkClientId) + const hash = await this.publishTransaction(ethQuery, rawTx); const cancelTransactionMeta: TransactionMeta = { actionId, chainId: transactionMeta.chainId, + networkClientId: transactionMeta.networkClientId, estimatedBaseFee, hash, id: random(), @@ -1026,7 +1062,12 @@ export class TransactionController extends BaseControllerV1< log('Submitting speed up transaction', { oldFee, newFee, txParams }); - const hash = await query(this.ethQuery, 'sendRawTransaction', [rawTx]); + // TODO(JL): Usually we only want submit transactions on the specific network + // that the user approved them on, but it makes sense to allow cancelling + // from any network that's also on the same chain. We will need to add a fallback + // here to allow using networkClientIds other than the original + const ethQuery = this.getEthQuery(transactionMeta.networkClientId) + const hash = await this.publishTransaction(ethQuery, rawTx) const baseTransactionMeta: TransactionMeta = { ...transactionMeta, @@ -1141,10 +1182,11 @@ export class TransactionController extends BaseControllerV1< * @param transaction - The transaction to estimate gas for. * @returns The gas and gas price. */ - async estimateGas(transaction: TransactionParams) { + async estimateGas(transaction: TransactionParams, networkClientId?: NetworkClientId) { + const ethQuery = this.getEthQuery(networkClientId) const { estimatedGas, simulationFails } = await estimateGas( transaction, - this.ethQuery, + ethQuery, ); return { gas: estimatedGas, simulationFails }; @@ -1156,13 +1198,15 @@ export class TransactionController extends BaseControllerV1< * @param transaction - The transaction params to estimate gas for. * @param multiplier - The multiplier to use for the gas buffer. */ - async estimateGasBuffered( + async estimateGasBuffered( // NOTE(JL): Need to update SwapsController's usage of this method transaction: TransactionParams, multiplier: number, + networkClientId?: NetworkClientId ) { + const ethQuery = this.getEthQuery(networkClientId) const { blockGasLimit, estimatedGas, simulationFails } = await estimateGas( transaction, - this.ethQuery, + ethQuery, ); const gas = addGasBuffer(estimatedGas, blockGasLimit, multiplier); @@ -1511,7 +1555,7 @@ export class TransactionController extends BaseControllerV1< * @param address - The hex string address for the transaction. * @returns object with the `nextNonce` `nonceDetails`, and the releaseLock. */ - async getNonceLock(address: string): Promise { + async getNonceLock(address: string): Promise { // NOTE(JL): i think this should take in chainId, but not sure how to deal with networkClientId mapping return this.nonceTracker.getNonceLock(address); } @@ -1754,7 +1798,7 @@ export class TransactionController extends BaseControllerV1< filterToCurrentNetwork?: boolean; limit?: number; } = {}): TransactionMeta[] { - const chainId = this.getChainId(); + const chainId = this.getChainId(); // TODO(JL): This should be made into an optional param // searchCriteria is an object that might have values that aren't predicate // methods. When providing any other value type (string, number, etc), we // consider this shorthand for "check the value at key for strict equality @@ -1877,21 +1921,26 @@ export class TransactionController extends BaseControllerV1< private async updateGasProperties(transactionMeta: TransactionMeta) { const isEIP1559Compatible = - (await this.getEIP1559Compatibility()) && + (await this.getEIP1559Compatibility(transactionMeta.networkClientId)) && transactionMeta.txParams.type !== TransactionEnvelopeType.legacy; - const chainId = this.getChainId(); + const { networkClientId } = transactionMeta; + + let providerConfig: ProviderConfig | NetworkClientConfiguration = this.getNetworkState().providerConfig + if (networkClientId) { + providerConfig = this.getNetworkClientById(networkClientId).configuration + } await updateGas({ - ethQuery: this.ethQuery, - providerConfig: this.getNetworkState().providerConfig, + ethQuery: this.getEthQuery(networkClientId), + providerConfig, // should this be renamed? txMeta: transactionMeta, }); await updateGasFees({ eip1559: isEIP1559Compatible, ethQuery: this.ethQuery, - getSavedGasFees: this.getSavedGasFees.bind(this, chainId), + getSavedGasFees: this.getSavedGasFees.bind(this), getGasFeeEstimates: this.getGasFeeEstimates.bind(this), txMeta: transactionMeta, }); @@ -1912,7 +1961,7 @@ export class TransactionController extends BaseControllerV1< /** * Create approvals for all unapproved transactions on current chain. */ - private createApprovalsForUnapprovedTransactions() { + private createApprovalsForUnapprovedTransactions() { // NOTE(JL): this doesn't seem to be used anywhere. Can we remove it? const unapprovedTransactions = this.getCurrentChainTransactionsByStatus( TransactionStatus.unapproved, ); @@ -2109,7 +2158,7 @@ export class TransactionController extends BaseControllerV1< transactionMeta.status = TransactionStatus.approved; transactionMeta.txParams.nonce = nonce; - transactionMeta.txParams.chainId = chainId; + transactionMeta.txParams.chainId = transactionMeta.chainId; const baseTxParams = { ...transactionMeta.txParams, @@ -2145,10 +2194,12 @@ export class TransactionController extends BaseControllerV1< return; } + const ethQuery = this.getEthQuery(transactionMeta.networkClientId) + if (transactionMeta.type === TransactionType.swap) { log('Determining pre-transaction balance'); - const preTxBalance = await query(this.ethQuery, 'getBalance', [from]); + const preTxBalance = await query(ethQuery, 'getBalance', [from]); transactionMeta.preTxBalance = preTxBalance; @@ -2157,7 +2208,7 @@ export class TransactionController extends BaseControllerV1< log('Publishing transaction', txParams); - const hash = await this.publishTransaction(rawTx); + const hash = await this.publishTransaction(ethQuery, rawTx); log('Publish successful', hash); @@ -2187,8 +2238,8 @@ export class TransactionController extends BaseControllerV1< } } - private async publishTransaction(rawTransaction: string): Promise { - return await query(this.ethQuery, 'sendRawTransaction', [rawTransaction]); + private async publishTransaction(ethQuery: EthQuery, rawTransaction: string): Promise { + return await query(ethQuery, 'sendRawTransaction', [rawTransaction]); } /** @@ -2340,7 +2391,10 @@ export class TransactionController extends BaseControllerV1< return { meta: transaction, isCompleted }; } - private getChainId(): Hex { + private getChainId(networkClientId?: NetworkClientId): Hex { + if (networkClientId) { + return this.getNetworkClientById(networkClientId).configuration.chainId; + } const { providerConfig } = this.getNetworkState(); return providerConfig.chainId; } @@ -2465,7 +2519,7 @@ export class TransactionController extends BaseControllerV1< * @param transactionMeta - Nominated external transaction to be added to state. */ private addExternalTransaction(transactionMeta: TransactionMeta) { - const chainId = this.getChainId(); + const { chainId } = transactionMeta const { transactions } = this.state; const fromAddress = transactionMeta?.txParams?.from; const sameFromAndNetworkTransactions = transactions.filter( @@ -2506,10 +2560,11 @@ export class TransactionController extends BaseControllerV1< * @param transactionId - Used to identify original transaction. */ private markNonceDuplicatesDropped(transactionId: string) { - const chainId = this.getChainId(); const transactionMeta = this.getTransaction(transactionId); + // NOTE(JL): Should this method be exiting early if getTransaction returns no transaction object? const nonce = transactionMeta?.txParams?.nonce; const from = transactionMeta?.txParams?.from; + const chainId = transactionMeta?.chainId const sameNonceTxs = this.state.transactions.filter( (transaction) => transaction.txParams.from === from && @@ -2598,9 +2653,9 @@ export class TransactionController extends BaseControllerV1< } } - private async getEIP1559Compatibility() { + private async getEIP1559Compatibility(networkClientId?: NetworkClientId) { const currentNetworkIsEIP1559Compatible = - await this.getCurrentNetworkEIP1559Compatibility(); + await this.getCurrentNetworkEIP1559Compatibility(networkClientId); const currentAccountIsEIP1559Compatible = await this.getCurrentAccountEIP1559Compatibility(); @@ -2703,8 +2758,11 @@ export class TransactionController extends BaseControllerV1< chainId, ); - const externalPendingTransactions = - this.getExternalPendingTransactions(address); + // TODO(JL): modify getExternalPendingTransactions in extension to accept a chainId to filter for smartTransactions by chainId + const externalPendingTransactions = this.getExternalPendingTransactions( + address, + chainId, + ); return [...standardPendingTransactions, ...externalPendingTransactions]; } @@ -2743,9 +2801,10 @@ export class TransactionController extends BaseControllerV1< return; } + const ethQuery = this.getEthQuery(transactionMeta.networkClientId) const { updatedTransactionMeta, approvalTransactionMeta } = await updatePostTransactionBalance(transactionMeta, { - ethQuery: this.ethQuery, + ethQuery, getTransaction: this.getTransaction.bind(this), updateTransaction: this.updateTransaction.bind(this), }); diff --git a/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts b/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts index f90f413b6d..5da9a01421 100644 --- a/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts +++ b/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts @@ -1,6 +1,6 @@ import { query } from '@metamask/controller-utils'; import type EthQuery from '@metamask/eth-query'; -import type { BlockTracker } from '@metamask/network-controller'; +import type { BlockTracker, NetworkClientId } from '@metamask/network-controller'; import { createModuleLogger } from '@metamask/utils'; import EventEmitter from 'events'; import type { NonceTracker } from 'nonce-tracker'; @@ -64,7 +64,7 @@ export class PendingTransactionTracker { #getChainId: () => string; - #getEthQuery: () => EthQuery; + #getEthQuery: (networkClientId?: NetworkClientId) => EthQuery; #getTransactions: () => TransactionMeta[]; @@ -76,7 +76,7 @@ export class PendingTransactionTracker { #onStateChange: (listener: (state: TransactionState) => void) => void; - #publishTransaction: (rawTx: string) => Promise; + #publishTransaction: (ethQuery: EthQuery, rawTx: string) => Promise; #running: boolean; @@ -99,12 +99,12 @@ export class PendingTransactionTracker { approveTransaction: (transactionId: string) => Promise; blockTracker: BlockTracker; getChainId: () => string; - getEthQuery: () => EthQuery; + getEthQuery: (networkClientId?: NetworkClientId) => EthQuery; getTransactions: () => TransactionMeta[]; isResubmitEnabled?: boolean; nonceTracker: NonceTracker; onStateChange: (listener: (state: TransactionState) => void) => void; - publishTransaction: (rawTx: string) => Promise; + publishTransaction: (ethQuery: EthQuery, rawTx: string) => Promise; hooks?: { beforeCheckPendingTransaction?: ( transactionMeta: TransactionMeta, @@ -274,7 +274,8 @@ export class PendingTransactionTracker { return; } - await this.#publishTransaction(rawTx); + const ethQuery = this.#getEthQuery(txMeta.networkClientId) + await this.#publishTransaction(ethQuery, rawTx); txMeta.retryCount = (txMeta.retryCount ?? 0) + 1; diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index 78f66a0ed0..5324c2d874 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -1,4 +1,5 @@ import type { AccessList } from '@ethereumjs/tx'; +import type { NetworkClientId } from '@metamask/network-controller'; import type { Hex } from '@metamask/utils'; import type { Operation } from 'fast-json-patch'; @@ -195,6 +196,11 @@ type TransactionMetaBase = { */ isTransfer?: boolean; + /** + * The id for the NetworkClient for the transaction. + */ + networkClientId?: NetworkClientId; + /** * Network code as per EIP-155 for this transaction * diff --git a/packages/transaction-controller/src/utils/gas-fees.ts b/packages/transaction-controller/src/utils/gas-fees.ts index b550470488..2eb04c1c95 100644 --- a/packages/transaction-controller/src/utils/gas-fees.ts +++ b/packages/transaction-controller/src/utils/gas-fees.ts @@ -7,8 +7,12 @@ import { toHex, } from '@metamask/controller-utils'; import type EthQuery from '@metamask/eth-query'; -import type { GasFeeState } from '@metamask/gas-fee-controller'; +import type { + FetchGasFeeEstimateOptions, + GasFeeState, +} from '@metamask/gas-fee-controller'; import { GAS_ESTIMATE_TYPES } from '@metamask/gas-fee-controller'; +import type { Hex } from '@metamask/utils'; import { createModuleLogger } from '@metamask/utils'; import { addHexPrefix } from 'ethereumjs-util'; @@ -25,8 +29,10 @@ import { SWAP_TRANSACTION_TYPES } from './swaps'; export type UpdateGasFeesRequest = { eip1559: boolean; ethQuery: EthQuery; - getSavedGasFees: () => SavedGasFees | undefined; - getGasFeeEstimates: () => Promise; + getSavedGasFees: (chainId: Hex) => SavedGasFees | undefined; + getGasFeeEstimates: ( + options: FetchGasFeeEstimateOptions, + ) => Promise; txMeta: TransactionMeta; }; @@ -45,7 +51,9 @@ export async function updateGasFees(request: UpdateGasFeesRequest) { const isSwap = SWAP_TRANSACTION_TYPES.includes( txMeta.type as TransactionType, ); - const savedGasFees = isSwap ? undefined : request.getSavedGasFees(); + const savedGasFees = isSwap + ? undefined + : request.getSavedGasFees(txMeta.chainId); const suggestedGasFees = await getSuggestedGasFees(request); @@ -268,7 +276,9 @@ async function getSuggestedGasFees(request: UpdateGasFeesRequest) { } try { - const { gasFeeEstimates, gasEstimateType } = await getGasFeeEstimates(); + const { gasFeeEstimates, gasEstimateType } = await getGasFeeEstimates({ + networkClientId: txMeta.networkClientId, + }); if (eip1559 && gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) { const { diff --git a/packages/transaction-controller/src/utils/gas.ts b/packages/transaction-controller/src/utils/gas.ts index 47c1c2358a..68d3114f04 100644 --- a/packages/transaction-controller/src/utils/gas.ts +++ b/packages/transaction-controller/src/utils/gas.ts @@ -8,8 +8,7 @@ import { query, } from '@metamask/controller-utils'; import type EthQuery from '@metamask/eth-query'; -import type { ProviderConfig } from '@metamask/network-controller'; -import { createModuleLogger } from '@metamask/utils'; +import { Hex, createModuleLogger } from '@metamask/utils'; import { addHexPrefix } from 'ethereumjs-util'; import { GAS_BUFFER_CHAIN_OVERRIDES } from '../constants'; @@ -18,7 +17,7 @@ import type { TransactionMeta, TransactionParams } from '../types'; export type UpdateGasRequest = { ethQuery: EthQuery; - providerConfig: ProviderConfig; + providerConfig: {type: NetworkType, chainId: Hex} txMeta: TransactionMeta; }; From 558a2ef3ae0a71a7e569c506dbc6d6c1135557a1 Mon Sep 17 00:00:00 2001 From: jiexi Date: Fri, 15 Dec 2023 07:06:39 -0800 Subject: [PATCH 004/100] Jl/transaction multichain fix update gas request (#3665) ## Explanation Break `UpdateGasRequest.providerConfig `into `chainId` and `isCustomNetwork` ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- .../src/TransactionController.test.ts | 4 +++- .../src/TransactionController.ts | 12 +++++++++--- .../helpers/PendingTransactionTracker.test.ts | 5 ++++- .../src/helpers/PendingTransactionTracker.ts | 7 +++++-- .../src/utils/gas.test.ts | 15 ++++++++------- .../transaction-controller/src/utils/gas.ts | 17 ++++++++--------- 6 files changed, 37 insertions(+), 23 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index fb7588ff7e..aa808fd1c8 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -1355,7 +1355,9 @@ describe('TransactionController', () => { expect(updateGasMock).toHaveBeenCalledTimes(1); expect(updateGasMock).toHaveBeenCalledWith({ ethQuery: expect.any(Object), - providerConfig: MOCK_NETWORK.state.providerConfig, + chainId: MOCK_NETWORK.state.providerConfig.chainId, + isCustomNetwork: + MOCK_NETWORK.state.providerConfig.type === NetworkType.rpc, txMeta: expect.any(Object), }); }); diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 522ba918be..ca8a7a8863 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -99,6 +99,7 @@ import { validateTxParams, } from './utils/validation'; import { NetworkClientConfiguration } from '@metamask/network-controller'; +import { NetworkClientType } from '@metamask/network-controller'; export const HARDFORK = Hardfork.London; @@ -1944,14 +1945,19 @@ export class TransactionController extends BaseControllerV1< const { networkClientId } = transactionMeta; - let providerConfig: ProviderConfig | NetworkClientConfiguration = this.getNetworkState().providerConfig + const { providerConfig } = this.getNetworkState(); + let { chainId } = providerConfig; + let isCustomNetwork = providerConfig.type === NetworkType.rpc; if (networkClientId) { - providerConfig = this.getNetworkClientById(networkClientId).configuration + const { configuration } = this.getNetworkClientById(networkClientId); + chainId = configuration.chainId; + isCustomNetwork = configuration.type === NetworkClientType.Custom; } await updateGas({ ethQuery: this.getEthQuery(networkClientId), - providerConfig, // should this be renamed? + chainId, + isCustomNetwork, txMeta: transactionMeta, }); diff --git a/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts b/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts index 09a65cd905..e1a776d9a1 100644 --- a/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts +++ b/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts @@ -12,6 +12,8 @@ const CHAIN_ID_MOCK = '0x1'; const NONCE_MOCK = '0x2'; const BLOCK_NUMBER_MOCK = '0x123'; +const ETH_QUERY_MOCK = {} + const TRANSACTION_SUBMITTED_MOCK = { id: ID_MOCK, chainId: CHAIN_ID_MOCK, @@ -88,7 +90,7 @@ describe('PendingTransactionTracker', () => { blockTracker, failTransaction, getChainId: () => CHAIN_ID_MOCK, - getEthQuery: () => ({}), + getEthQuery: () => (ETH_QUERY_MOCK), getTransactions: () => [], nonceTracker: createNonceTrackerMock(), onStateChange, @@ -647,6 +649,7 @@ describe('PendingTransactionTracker', () => { expect(options.publishTransaction).toHaveBeenCalledTimes(1); expect(options.publishTransaction).toHaveBeenCalledWith( + ETH_QUERY_MOCK, TRANSACTION_SUBMITTED_MOCK.rawTx, ); }); diff --git a/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts b/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts index 25f500f924..91ea0ead63 100644 --- a/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts +++ b/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts @@ -1,6 +1,9 @@ import { query } from '@metamask/controller-utils'; import type EthQuery from '@metamask/eth-query'; -import type { BlockTracker, NetworkClientId } from '@metamask/network-controller'; +import type { + BlockTracker, + NetworkClientId, +} from '@metamask/network-controller'; import { createModuleLogger } from '@metamask/utils'; import EventEmitter from 'events'; import type { NonceTracker } from 'nonce-tracker'; @@ -278,7 +281,7 @@ export class PendingTransactionTracker { return; } - const ethQuery = this.#getEthQuery(txMeta.networkClientId) + const ethQuery = this.#getEthQuery(txMeta.networkClientId); await this.#publishTransaction(ethQuery, rawTx); txMeta.retryCount = (txMeta.retryCount ?? 0) + 1; diff --git a/packages/transaction-controller/src/utils/gas.test.ts b/packages/transaction-controller/src/utils/gas.test.ts index f1eae2c770..2e919bf885 100644 --- a/packages/transaction-controller/src/utils/gas.test.ts +++ b/packages/transaction-controller/src/utils/gas.test.ts @@ -1,6 +1,6 @@ /* eslint-disable jsdoc/require-jsdoc */ -import { NetworkType, query } from '@metamask/controller-utils'; +import { query } from '@metamask/controller-utils'; import type EthQuery from '@metamask/eth-query'; import { CHAIN_IDS } from '../constants'; @@ -32,7 +32,8 @@ const TRANSACTION_META_MOCK = { const UPDATE_GAS_REQUEST_MOCK = { txMeta: TRANSACTION_META_MOCK, - providerConfig: {}, + chainId: '0x0', + isCustomNetwork: false, ethQuery: ETH_QUERY_MOCK, } as UpdateGasRequest; @@ -112,7 +113,7 @@ describe('gas', () => { }); it('to estimate if custom network', async () => { - updateGasRequest.providerConfig.type = NetworkType.rpc; + updateGasRequest.isCustomNetwork = true; mockQuery({ getBlockByNumberResponse: { gasLimit: toHex(BLOCK_GAS_LIMIT_MOCK) }, @@ -170,7 +171,7 @@ describe('gas', () => { const estimatedGasPadded = Math.ceil(blockGasLimit90Percent - 10); const estimatedGas = estimatedGasPadded; // Optimism multiplier is 1 - updateGasRequest.providerConfig.chainId = CHAIN_IDS.OPTIMISM; + updateGasRequest.chainId = CHAIN_IDS.OPTIMISM; mockQuery({ getCodeResponse: CODE_MOCK, @@ -211,7 +212,7 @@ describe('gas', () => { describe('to fixed value', () => { it('if not custom network and no to parameter', async () => { - updateGasRequest.providerConfig.type = NetworkType.mainnet; + updateGasRequest.isCustomNetwork = false; delete updateGasRequest.txMeta.txParams.to; await updateGas(updateGasRequest); @@ -224,7 +225,7 @@ describe('gas', () => { }); it('if not custom network and to parameter and no data and no code', async () => { - updateGasRequest.providerConfig.type = NetworkType.mainnet; + updateGasRequest.isCustomNetwork = false; delete updateGasRequest.txMeta.txParams.data; mockQuery({ @@ -241,7 +242,7 @@ describe('gas', () => { }); it('if not custom network and to parameter and no data and empty code', async () => { - updateGasRequest.providerConfig.type = NetworkType.mainnet; + updateGasRequest.isCustomNetwork = false; delete updateGasRequest.txMeta.txParams.data; mockQuery({ diff --git a/packages/transaction-controller/src/utils/gas.ts b/packages/transaction-controller/src/utils/gas.ts index 57c7c204b8..396a912cae 100644 --- a/packages/transaction-controller/src/utils/gas.ts +++ b/packages/transaction-controller/src/utils/gas.ts @@ -2,13 +2,13 @@ import { BNToHex, - NetworkType, fractionBN, hexToBN, query, } from '@metamask/controller-utils'; import type EthQuery from '@metamask/eth-query'; -import { Hex, createModuleLogger } from '@metamask/utils'; +import type { Hex } from '@metamask/utils'; +import { createModuleLogger } from '@metamask/utils'; import { addHexPrefix } from 'ethereumjs-util'; import { GAS_BUFFER_CHAIN_OVERRIDES } from '../constants'; @@ -17,7 +17,8 @@ import type { TransactionMeta, TransactionParams } from '../types'; export type UpdateGasRequest = { ethQuery: EthQuery; - providerConfig: {type: NetworkType, chainId: Hex} + isCustomNetwork: boolean; + chainId: Hex; txMeta: TransactionMeta; }; @@ -119,7 +120,7 @@ export function addGasBuffer( async function getGas( request: UpdateGasRequest, ): Promise<[string, TransactionMeta['simulationFails']?]> { - const { providerConfig, txMeta } = request; + const { isCustomNetwork, chainId, txMeta } = request; if (txMeta.txParams.gas) { log('Using value from request', txMeta.txParams.gas); @@ -136,14 +137,14 @@ async function getGas( request.ethQuery, ); - if (providerConfig.type === NetworkType.rpc) { + if (isCustomNetwork) { log('Using original estimate as custom network'); return [estimatedGas, simulationFails]; } const bufferMultiplier = GAS_BUFFER_CHAIN_OVERRIDES[ - providerConfig.chainId as keyof typeof GAS_BUFFER_CHAIN_OVERRIDES + chainId as keyof typeof GAS_BUFFER_CHAIN_OVERRIDES ] ?? DEFAULT_GAS_MULTIPLIER; const bufferedGas = addGasBuffer( @@ -158,10 +159,8 @@ async function getGas( async function requiresFixedGas({ ethQuery, txMeta, - providerConfig, + isCustomNetwork, }: UpdateGasRequest): Promise { - const isCustomNetwork = providerConfig.type === NetworkType.rpc; - const { txParams: { to, data }, } = txMeta; From b9320d9739f253c58af9b34e2f085523b220a063 Mon Sep 17 00:00:00 2001 From: Shane Jonas Date: Fri, 8 Dec 2023 09:15:07 -0500 Subject: [PATCH 005/100] WIP TransactionController MultiChain --- .../src/TransactionController.ts | 104 ++++++++++++++++-- 1 file changed, 94 insertions(+), 10 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 497a5809e7..b38580c44a 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -24,6 +24,8 @@ import EthQuery from '@metamask/eth-query'; import type { GasFeeState } from '@metamask/gas-fee-controller'; import type { BlockTracker, + NetworkClientId, + NetworkController, NetworkState, Provider, } from '@metamask/network-controller'; @@ -292,6 +294,8 @@ export class TransactionController extends BaseControllerV1< transactionMeta: TransactionMeta, ) => (TransactionMeta | undefined)[]; + private readonly getNetworkClientById: NetworkController['getNetworkClientById']; + private failTransaction( transactionMeta: TransactionMeta, error: Error, @@ -331,6 +335,15 @@ export class TransactionController extends BaseControllerV1< */ override name = 'TransactionController'; + private readonly trackingMap: Map< + NetworkClientId, + Set<{ + nonceTracker: NonceTracker; + pendingTransactionTracker: PendingTransactionTracker; + incomingTransactionHelper: IncomingTransactionHelper; + }> + > = new Map(); + /** * Method used to sign transactions */ @@ -369,6 +382,7 @@ export class TransactionController extends BaseControllerV1< * @param options.provider - The provider used to create the underlying EthQuery instance. * @param options.securityProviderRequest - A function for verifying a transaction, whether it is malicious or not. * @param options.speedUpMultiplier - Multiplier used to determine a transaction's increased gas fee during speed up. + * @param options.getNetworkClientById - Gets the network client with the given id from the NetworkController. * @param options.hooks - The controller hooks. * @param options.hooks.afterSign - Additional logic to execute after signing a transaction. Return false to not change the status to signed. * @param options.hooks.beforeApproveOnInit - Additional logic to execute before starting an approval flow for a transaction during initialization. Return false to skip the transaction. @@ -400,6 +414,7 @@ export class TransactionController extends BaseControllerV1< provider, securityProviderRequest, speedUpMultiplier, + getNetworkClientById, hooks = {}, }: { blockTracker: BlockTracker; @@ -431,6 +446,7 @@ export class TransactionController extends BaseControllerV1< provider: Provider; securityProviderRequest?: SecurityProviderRequest; speedUpMultiplier?: number; + getNetworkClientById: NetworkController['getNetworkClientById']; hooks: { afterSign?: ( transactionMeta: TransactionMeta, @@ -462,7 +478,7 @@ export class TransactionController extends BaseControllerV1< }; this.initialize(); - + this.getNetworkClientById = getNetworkClientById; this.provider = provider; this.messagingSystem = messenger; this.getNetworkState = getNetworkState; @@ -1059,6 +1075,69 @@ export class TransactionController extends BaseControllerV1< this.hub.emit(`${transactionMeta.id}:speedup`, newTransactionMeta); } + startTrackingByNetworkClientId(networkClientId: NetworkClientId) { + const networkClient = this.getNetworkClientById(networkClientId); + // track using tracking map + this.trackingMap.set(networkClientId, new Set()); + const nonceTracker = new NonceTracker({ + provider: networkClient.provider as any, + blockTracker: networkClient.blockTracker, + getPendingTransactions: + this.getNonceTrackerPendingTransactions.bind(this), + getConfirmedTransactions: this.getNonceTrackerTransactions.bind( + this, + TransactionStatus.confirmed, + ), + }); + const incomingTransactionHelper = new IncomingTransactionHelper({ + blockTracker: networkClient.blockTracker, + getCurrentAccount: this.getSelectedAddress, + getLastFetchedBlockNumbers: () => this.state.lastFetchedBlockNumbers, + getNetworkState: this.getNetworkState, // TODO: fake this via networkClient + isEnabled: () => true, + queryEntireHistory: true, + remoteTransactionSource: new EtherscanRemoteTransactionSource({ + includeTokenTransfers: true, + }), + transactionLimit: this.config.txHistoryLimit, + updateTransactions: true, + }); + const pendingTransactionTracker = new PendingTransactionTracker({ + approveTransaction: this.approveTransaction.bind(this), + blockTracker: networkClient.provider as any, + getChainId: () => networkClient.configuration.chainId, + getEthQuery: () => this.ethQuery, // TODO: use networkClient to construct ethQuery + getTransactions: () => this.state.transactions, + isResubmitEnabled: true, // TODO: make this configurable + nonceTracker, + onStateChange: this.subscribe.bind(this), + publishTransaction: this.publishTransaction.bind(this), + hooks: { + beforeCheckPendingTransaction: + this.beforeCheckPendingTransaction.bind(this), + beforePublish: this.beforePublish.bind(this), + }, + }); + // subscribe to trackers + incomingTransactionHelper.hub.on( + 'transactions', + this.onIncomingTransactions.bind(this), + ); + + incomingTransactionHelper.hub.on( + 'updatedLastFetchedBlockNumbers', + this.onUpdatedLastFetchedBlockNumbers.bind(this), + ); + this.addPendingTransactionTrackerListeners(pendingTransactionTracker); + + // add to tracking map + this.trackingMap.get(networkClientId)?.add({ + nonceTracker, + incomingTransactionHelper, + pendingTransactionTracker, + }); + } + /** * Estimates required gas for a given transaction. * @@ -2550,23 +2629,25 @@ export class TransactionController extends BaseControllerV1< ); } - private addPendingTransactionTrackerListeners() { - this.pendingTransactionTracker.hub.on( + private addPendingTransactionTrackerListeners( + pendingTransactionTracker = this.pendingTransactionTracker, + ) { + pendingTransactionTracker.hub.on( 'transaction-confirmed', this.onConfirmedTransaction.bind(this), ); - this.pendingTransactionTracker.hub.on( + pendingTransactionTracker.hub.on( 'transaction-dropped', this.setTransactionStatusDropped.bind(this), ); - this.pendingTransactionTracker.hub.on( + pendingTransactionTracker.hub.on( 'transaction-failed', this.failTransaction.bind(this), ); - this.pendingTransactionTracker.hub.on( + pendingTransactionTracker.hub.on( 'transaction-updated', this.updateTransaction.bind(this), ); @@ -2631,10 +2712,14 @@ export class TransactionController extends BaseControllerV1< this.hub.emit('transaction-status-update', { transactionMeta }); } - private getNonceTrackerPendingTransactions(address: string) { + private getNonceTrackerPendingTransactions( + address: string, + chainId?: string, + ) { const standardPendingTransactions = this.getNonceTrackerTransactions( TransactionStatus.submitted, address, + chainId, ); const externalPendingTransactions = @@ -2646,11 +2731,10 @@ export class TransactionController extends BaseControllerV1< private getNonceTrackerTransactions( status: TransactionStatus, address: string, + chainId: string = this.getChainId(), ) { - const currentChainId = this.getChainId(); - return getAndFormatTransactionsForNonceTracker( - currentChainId, + chainId, address, status, this.state.transactions, From 50cd6ca1ce595ccd8e3b757b2da222f7556b20c1 Mon Sep 17 00:00:00 2001 From: Shane Jonas Date: Mon, 11 Dec 2023 10:06:33 -0500 Subject: [PATCH 006/100] Fixed tx controller to only use one EtherscanRemoteTransactionSource --- .../src/TransactionController.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index b38580c44a..aa61daab6c 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -296,6 +296,8 @@ export class TransactionController extends BaseControllerV1< private readonly getNetworkClientById: NetworkController['getNetworkClientById']; + private readonly etherscanRemoteTransactionSource: EtherscanRemoteTransactionSource; + private failTransaction( transactionMeta: TransactionMeta, error: Error, @@ -524,6 +526,11 @@ export class TransactionController extends BaseControllerV1< ), }); + this.etherscanRemoteTransactionSource = + new EtherscanRemoteTransactionSource({ + includeTokenTransfers: incomingTransactions.includeTokenTransfers, + }); + this.incomingTransactionHelper = new IncomingTransactionHelper({ blockTracker, getCurrentAccount: getSelectedAddress, @@ -531,9 +538,7 @@ export class TransactionController extends BaseControllerV1< getNetworkState, isEnabled: incomingTransactions.isEnabled, queryEntireHistory: incomingTransactions.queryEntireHistory, - remoteTransactionSource: new EtherscanRemoteTransactionSource({ - includeTokenTransfers: incomingTransactions.includeTokenTransfers, - }), + remoteTransactionSource: this.etherscanRemoteTransactionSource, transactionLimit: this.config.txHistoryLimit, updateTransactions: incomingTransactions.updateTransactions, }); @@ -1096,9 +1101,7 @@ export class TransactionController extends BaseControllerV1< getNetworkState: this.getNetworkState, // TODO: fake this via networkClient isEnabled: () => true, queryEntireHistory: true, - remoteTransactionSource: new EtherscanRemoteTransactionSource({ - includeTokenTransfers: true, - }), + remoteTransactionSource: this.etherscanRemoteTransactionSource, transactionLimit: this.config.txHistoryLimit, updateTransactions: true, }); @@ -2694,7 +2697,7 @@ export class TransactionController extends BaseControllerV1< 'TransactionController#approveTransaction - Transaction signed', ); - this.onTransactionStatusChange(transactionMeta); + this.onTransactionStatusChange(transactionMeta); // TODO: fake this via networkClient const rawTx = bufferToHex(signedTx.serialize()); From 76090162ca70682334ed0d583dc929e0d5cf5b5b Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Wed, 13 Dec 2023 14:06:29 -0600 Subject: [PATCH 007/100] TransactionController networkClientId updates (#3652) * Update `getCurrentNetworkEIP1559Compatibility` hook type to accept optional networkClientId param * It already does this in networkController, but perhaps it should be renamed getEIP1559Compatibility? * Update `getExternalPendingTransactions` hook type to accept optional chainId param * SmartTransactionController and extension need to be updated * Add `getNetworkClientIdForDomain` hook to constructor param * Add `getEthQuery` method that accepts optional networkClientId and returns the correct EthQuery instance * Update `addTransaction` * Add optional `networkClientId` to options object param * Use correct networkClientId/chainId * Update `stopTransaction` to use correct networkClientId/chainId * Update `estimateGas` * Add optional `networkClientId` param * Use correct networkClientId/chainId * Update `estimateGasBuffered` to accept optional networkClientId * SmartTransactionController and extension need to be updated * Update `updateGasProperties` to use correct networkClientId (from txMeta) * Update `approveTransaction` to use correct networkClientId (from txMeta) * Update `publishTransaction` to accept required ethQuery * Update `getEIP1559Compatibility` to accept optional networkClientId * GasFeeController has a similar method of it's own that we may need to update * Update `getNonceTrackerPendingTransactions` to pass `chainId` to `getExternalPendingTransactions` * Add optional `networkClientId` to `TransactionMeta` type * Update `UpdateGasRequest` to accept either providerConfig or NetworkClientConfiguration * Probably should rename the arg from `providerConfig` Questions: * should `wipeTransactions` be updated to filter by optional chainId? * should `getNonceLock` take in chainId (non-optional) to get correct nonceTracker nonceLock? * should `getTransactions` accept chainId and/or networkClientId param for filtering? * `createApprovalsForUnapprovedTransactions` isn't used anywhere in our repos? Remove it? * `approveTransaction` sets txMeta.txParams.chainId to the currentChainId. Shouldn't it read the value from txMeta instead? * shouldn't `addExternalTransaction` filter against the txMeta chainId, not the currentChainId? * shouldn't `markNonceDuplicatesDropped ` filter against the txMeta chainId, not the currentChainId? --------- Co-authored-by: Jiexi Luan --- .../src/TransactionController.ts | 121 +++++++++++++----- .../src/helpers/PendingTransactionTracker.ts | 13 +- packages/transaction-controller/src/types.ts | 6 + .../src/utils/gas-fees.ts | 20 ++- .../transaction-controller/src/utils/gas.ts | 5 +- 5 files changed, 120 insertions(+), 45 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index aa61daab6c..522ba918be 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -28,6 +28,7 @@ import type { NetworkController, NetworkState, Provider, + ProviderConfig, } from '@metamask/network-controller'; import { errorCodes, rpcErrors, providerErrors } from '@metamask/rpc-errors'; import type { Hex } from '@metamask/utils'; @@ -43,6 +44,7 @@ import type { } from 'nonce-tracker'; import { v1 as random } from 'uuid'; +import type { SelectedNetworkController } from '../../selected-network-controller/src/SelectedNetworkController'; import { EtherscanRemoteTransactionSource } from './helpers/EtherscanRemoteTransactionSource'; import { IncomingTransactionHelper } from './helpers/IncomingTransactionHelper'; import { PendingTransactionTracker } from './helpers/PendingTransactionTracker'; @@ -96,6 +98,7 @@ import { validateTransactionOrigin, validateTxParams, } from './utils/validation'; +import { NetworkClientConfiguration } from '@metamask/network-controller'; export const HARDFORK = Hardfork.London; @@ -251,7 +254,9 @@ export class TransactionController extends BaseControllerV1< private readonly getCurrentAccountEIP1559Compatibility: () => Promise; - private readonly getCurrentNetworkEIP1559Compatibility: () => Promise; + private readonly getCurrentNetworkEIP1559Compatibility: ( + networkClientId?: NetworkClientId, + ) => Promise; private readonly getGasFeeEstimates: () => Promise; @@ -261,6 +266,7 @@ export class TransactionController extends BaseControllerV1< private readonly getExternalPendingTransactions: ( address: string, + chainId?: string, ) => NonceTrackerTransaction[]; private readonly messagingSystem: TransactionControllerMessenger; @@ -296,6 +302,8 @@ export class TransactionController extends BaseControllerV1< private readonly getNetworkClientById: NetworkController['getNetworkClientById']; + private readonly getNetworkClientIdForDomain: SelectedNetworkController['getNetworkClientIdForDomain']; + private readonly etherscanRemoteTransactionSource: EtherscanRemoteTransactionSource; private failTransaction( @@ -393,6 +401,7 @@ export class TransactionController extends BaseControllerV1< * @param options.hooks.getAdditionalSignArguments - Returns additional arguments required to sign a transaction. * @param config - Initial options used to configure this controller. * @param state - Initial state to set on this controller. + * @param options.getNetworkClientIdForDomain */ constructor( { @@ -417,6 +426,7 @@ export class TransactionController extends BaseControllerV1< securityProviderRequest, speedUpMultiplier, getNetworkClientById, + getNetworkClientIdForDomain, hooks = {}, }: { blockTracker: BlockTracker; @@ -428,6 +438,7 @@ export class TransactionController extends BaseControllerV1< getCurrentNetworkEIP1559Compatibility: () => Promise; getExternalPendingTransactions?: ( address: string, + chainId?: string, ) => NonceTrackerTransaction[]; getGasFeeEstimates?: () => Promise; getNetworkState: () => NetworkState; @@ -449,6 +460,7 @@ export class TransactionController extends BaseControllerV1< securityProviderRequest?: SecurityProviderRequest; speedUpMultiplier?: number; getNetworkClientById: NetworkController['getNetworkClientById']; + getNetworkClientIdForDomain: SelectedNetworkController['getNetworkClientIdForDomain']; hooks: { afterSign?: ( transactionMeta: TransactionMeta, @@ -504,6 +516,8 @@ export class TransactionController extends BaseControllerV1< this.cancelMultiplier = cancelMultiplier ?? CANCEL_RATE; this.speedUpMultiplier = speedUpMultiplier ?? SPEED_UP_RATE; + this.getNetworkClientIdForDomain = getNetworkClientIdForDomain; + this.afterSign = hooks?.afterSign ?? (() => true); this.beforeApproveOnInit = hooks?.beforeApproveOnInit ?? (() => true); this.beforeCheckPendingTransaction = @@ -607,6 +621,13 @@ export class TransactionController extends BaseControllerV1< } } + getEthQuery(networkClientId?: NetworkClientId): EthQuery { + if (networkClientId) { + return new EthQuery(this.getNetworkClientById(networkClientId).provider); + } + return this.ethQuery; + } + /** * Add a new unapproved transaction to state. Parameters will be validated, a * unique transaction id will be generated, and gas and gasPrice will be calculated @@ -625,6 +646,7 @@ export class TransactionController extends BaseControllerV1< * @param opts.swaps - Options for swaps transactions. * @param opts.swaps.hasApproveTx - Whether the transaction has an approval transaction. * @param opts.swaps.meta - Metadata for swap transaction. + * @param opts.networkClientId * @returns Object containing a promise resolving to the transaction hash if approved. */ async addTransaction( @@ -639,6 +661,7 @@ export class TransactionController extends BaseControllerV1< sendFlowHistory, swaps = {}, type, + networkClientId, }: { actionId?: string; deviceConfirmedOn?: WalletDevice; @@ -652,13 +675,21 @@ export class TransactionController extends BaseControllerV1< meta?: Partial; }; type?: TransactionType; + networkClientId?: NetworkClientId; } = {}, ): Promise { log('Adding transaction', txParams); + // TODO(JL): Revisit this fallback during implementation + // networkClientId ??= this.getNetworkClientIdForDomain( + // origin ?? ORIGIN_METAMASK, + // ); + txParams = normalizeTxParams(txParams); - const isEIP1559Compatible = await this.getEIP1559Compatibility(); + const isEIP1559Compatible = await this.getEIP1559Compatibility( + networkClientId, + ); validateTxParams(txParams, isEIP1559Compatible); @@ -676,11 +707,13 @@ export class TransactionController extends BaseControllerV1< origin, ); + const ethQuery = this.getEthQuery(networkClientId); + const transactionType = - type ?? (await determineTransactionType(txParams, this.ethQuery)).type; + type ?? (await determineTransactionType(txParams, ethQuery)).type; const existingTransactionMeta = this.getTransactionWithActionId(actionId); - const chainId = this.getChainId(); + const chainId = this.getChainId(networkClientId); // If a request to add a transaction with the same actionId is submitted again, a new transaction will not be created for it. const transactionMeta: TransactionMeta = existingTransactionMeta || { @@ -698,6 +731,7 @@ export class TransactionController extends BaseControllerV1< userEditedGasLimit: false, verifiedOnBlockchain: false, type: transactionType, + networkClientId, }; await this.updateGasProperties(transactionMeta); @@ -880,11 +914,13 @@ export class TransactionController extends BaseControllerV1< txParams: newTxParams, }); - const hash = await this.publishTransaction(rawTx); + const ethQuery = this.getEthQuery(transactionMeta.networkClientId) + const hash = await this.publishTransaction(ethQuery, rawTx); const cancelTransactionMeta: TransactionMeta = { actionId, chainId: transactionMeta.chainId, + networkClientId: transactionMeta.networkClientId, estimatedBaseFee, hash, id: random(), @@ -1032,7 +1068,12 @@ export class TransactionController extends BaseControllerV1< log('Submitting speed up transaction', { oldFee, newFee, txParams }); - const hash = await query(this.ethQuery, 'sendRawTransaction', [rawTx]); + // TODO(JL): Usually we only want submit transactions on the specific network + // that the user approved them on, but it makes sense to allow cancelling + // from any network that's also on the same chain. We will need to add a fallback + // here to allow using networkClientIds other than the original + const ethQuery = this.getEthQuery(transactionMeta.networkClientId) + const hash = await this.publishTransaction(ethQuery, rawTx) const baseTransactionMeta: TransactionMeta = { ...transactionMeta, @@ -1147,10 +1188,11 @@ export class TransactionController extends BaseControllerV1< * @param transaction - The transaction to estimate gas for. * @returns The gas and gas price. */ - async estimateGas(transaction: TransactionParams) { + async estimateGas(transaction: TransactionParams, networkClientId?: NetworkClientId) { + const ethQuery = this.getEthQuery(networkClientId) const { estimatedGas, simulationFails } = await estimateGas( transaction, - this.ethQuery, + ethQuery, ); return { gas: estimatedGas, simulationFails }; @@ -1162,13 +1204,15 @@ export class TransactionController extends BaseControllerV1< * @param transaction - The transaction params to estimate gas for. * @param multiplier - The multiplier to use for the gas buffer. */ - async estimateGasBuffered( + async estimateGasBuffered( // NOTE(JL): Need to update SwapsController's usage of this method transaction: TransactionParams, multiplier: number, + networkClientId?: NetworkClientId ) { + const ethQuery = this.getEthQuery(networkClientId) const { blockGasLimit, estimatedGas, simulationFails } = await estimateGas( transaction, - this.ethQuery, + ethQuery, ); const gas = addGasBuffer(estimatedGas, blockGasLimit, multiplier); @@ -1521,7 +1565,7 @@ export class TransactionController extends BaseControllerV1< * @param address - The hex string address for the transaction. * @returns object with the `nextNonce` `nonceDetails`, and the releaseLock. */ - async getNonceLock(address: string): Promise { + async getNonceLock(address: string): Promise { // NOTE(JL): i think this should take in chainId, but not sure how to deal with networkClientId mapping return this.nonceTracker.getNonceLock(address); } @@ -1766,7 +1810,7 @@ export class TransactionController extends BaseControllerV1< filterToCurrentNetwork?: boolean; limit?: number; } = {}): TransactionMeta[] { - const chainId = this.getChainId(); + const chainId = this.getChainId(); // TODO(JL): This should be made into an optional param // searchCriteria is an object that might have values that aren't predicate // methods. When providing any other value type (string, number, etc), we // consider this shorthand for "check the value at key for strict equality @@ -1895,21 +1939,26 @@ export class TransactionController extends BaseControllerV1< private async updateGasProperties(transactionMeta: TransactionMeta) { const isEIP1559Compatible = - (await this.getEIP1559Compatibility()) && + (await this.getEIP1559Compatibility(transactionMeta.networkClientId)) && transactionMeta.txParams.type !== TransactionEnvelopeType.legacy; - const chainId = this.getChainId(); + const { networkClientId } = transactionMeta; + + let providerConfig: ProviderConfig | NetworkClientConfiguration = this.getNetworkState().providerConfig + if (networkClientId) { + providerConfig = this.getNetworkClientById(networkClientId).configuration + } await updateGas({ - ethQuery: this.ethQuery, - providerConfig: this.getNetworkState().providerConfig, + ethQuery: this.getEthQuery(networkClientId), + providerConfig, // should this be renamed? txMeta: transactionMeta, }); await updateGasFees({ eip1559: isEIP1559Compatible, ethQuery: this.ethQuery, - getSavedGasFees: this.getSavedGasFees.bind(this, chainId), + getSavedGasFees: this.getSavedGasFees.bind(this), getGasFeeEstimates: this.getGasFeeEstimates.bind(this), txMeta: transactionMeta, }); @@ -1930,7 +1979,7 @@ export class TransactionController extends BaseControllerV1< /** * Create approvals for all unapproved transactions on current chain. */ - private createApprovalsForUnapprovedTransactions() { + private createApprovalsForUnapprovedTransactions() { // NOTE(JL): this doesn't seem to be used anywhere. Can we remove it? const unapprovedTransactions = this.getCurrentChainTransactionsByStatus( TransactionStatus.unapproved, ); @@ -2129,7 +2178,7 @@ export class TransactionController extends BaseControllerV1< transactionMeta.status = TransactionStatus.approved; transactionMeta.txParams.nonce = nonce; - transactionMeta.txParams.chainId = chainId; + transactionMeta.txParams.chainId = transactionMeta.chainId; const baseTxParams = { ...transactionMeta.txParams, @@ -2165,10 +2214,12 @@ export class TransactionController extends BaseControllerV1< return; } + const ethQuery = this.getEthQuery(transactionMeta.networkClientId) + if (transactionMeta.type === TransactionType.swap) { log('Determining pre-transaction balance'); - const preTxBalance = await query(this.ethQuery, 'getBalance', [from]); + const preTxBalance = await query(ethQuery, 'getBalance', [from]); transactionMeta.preTxBalance = preTxBalance; @@ -2177,7 +2228,7 @@ export class TransactionController extends BaseControllerV1< log('Publishing transaction', txParams); - const hash = await this.publishTransaction(rawTx); + const hash = await this.publishTransaction(ethQuery, rawTx); log('Publish successful', hash); @@ -2209,8 +2260,8 @@ export class TransactionController extends BaseControllerV1< } } - private async publishTransaction(rawTransaction: string): Promise { - return await query(this.ethQuery, 'sendRawTransaction', [rawTransaction]); + private async publishTransaction(ethQuery: EthQuery, rawTransaction: string): Promise { + return await query(ethQuery, 'sendRawTransaction', [rawTransaction]); } /** @@ -2362,7 +2413,10 @@ export class TransactionController extends BaseControllerV1< return { meta: transaction, isCompleted }; } - private getChainId(): Hex { + private getChainId(networkClientId?: NetworkClientId): Hex { + if (networkClientId) { + return this.getNetworkClientById(networkClientId).configuration.chainId; + } const { providerConfig } = this.getNetworkState(); return providerConfig.chainId; } @@ -2487,7 +2541,7 @@ export class TransactionController extends BaseControllerV1< * @param transactionMeta - Nominated external transaction to be added to state. */ private addExternalTransaction(transactionMeta: TransactionMeta) { - const chainId = this.getChainId(); + const { chainId } = transactionMeta const { transactions } = this.state; const fromAddress = transactionMeta?.txParams?.from; const sameFromAndNetworkTransactions = transactions.filter( @@ -2528,10 +2582,11 @@ export class TransactionController extends BaseControllerV1< * @param transactionId - Used to identify original transaction. */ private markNonceDuplicatesDropped(transactionId: string) { - const chainId = this.getChainId(); const transactionMeta = this.getTransaction(transactionId); + // NOTE(JL): Should this method be exiting early if getTransaction returns no transaction object? const nonce = transactionMeta?.txParams?.nonce; const from = transactionMeta?.txParams?.from; + const chainId = transactionMeta?.chainId const sameNonceTxs = this.state.transactions.filter( (transaction) => transaction.txParams.from === from && @@ -2620,9 +2675,9 @@ export class TransactionController extends BaseControllerV1< } } - private async getEIP1559Compatibility() { + private async getEIP1559Compatibility(networkClientId?: NetworkClientId) { const currentNetworkIsEIP1559Compatible = - await this.getCurrentNetworkEIP1559Compatibility(); + await this.getCurrentNetworkEIP1559Compatibility(networkClientId); const currentAccountIsEIP1559Compatible = await this.getCurrentAccountEIP1559Compatibility(); @@ -2725,8 +2780,11 @@ export class TransactionController extends BaseControllerV1< chainId, ); - const externalPendingTransactions = - this.getExternalPendingTransactions(address); + // TODO(JL): modify getExternalPendingTransactions in extension to accept a chainId to filter for smartTransactions by chainId + const externalPendingTransactions = this.getExternalPendingTransactions( + address, + chainId, + ); return [...standardPendingTransactions, ...externalPendingTransactions]; } @@ -2765,9 +2823,10 @@ export class TransactionController extends BaseControllerV1< return; } + const ethQuery = this.getEthQuery(transactionMeta.networkClientId) const { updatedTransactionMeta, approvalTransactionMeta } = await updatePostTransactionBalance(transactionMeta, { - ethQuery: this.ethQuery, + ethQuery, getTransaction: this.getTransaction.bind(this), updateTransaction: this.updateTransaction.bind(this), }); diff --git a/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts b/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts index 9204c17405..25f500f924 100644 --- a/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts +++ b/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts @@ -1,6 +1,6 @@ import { query } from '@metamask/controller-utils'; import type EthQuery from '@metamask/eth-query'; -import type { BlockTracker } from '@metamask/network-controller'; +import type { BlockTracker, NetworkClientId } from '@metamask/network-controller'; import { createModuleLogger } from '@metamask/utils'; import EventEmitter from 'events'; import type { NonceTracker } from 'nonce-tracker'; @@ -64,7 +64,7 @@ export class PendingTransactionTracker { #getChainId: () => string; - #getEthQuery: () => EthQuery; + #getEthQuery: (networkClientId?: NetworkClientId) => EthQuery; #getTransactions: () => TransactionMeta[]; @@ -78,7 +78,7 @@ export class PendingTransactionTracker { #onStateChange: (listener: (state: TransactionState) => void) => void; - #publishTransaction: (rawTx: string) => Promise; + #publishTransaction: (ethQuery: EthQuery, rawTx: string) => Promise; #running: boolean; @@ -101,12 +101,12 @@ export class PendingTransactionTracker { approveTransaction: (transactionId: string) => Promise; blockTracker: BlockTracker; getChainId: () => string; - getEthQuery: () => EthQuery; + getEthQuery: (networkClientId?: NetworkClientId) => EthQuery; getTransactions: () => TransactionMeta[]; isResubmitEnabled?: boolean; nonceTracker: NonceTracker; onStateChange: (listener: (state: TransactionState) => void) => void; - publishTransaction: (rawTx: string) => Promise; + publishTransaction: (ethQuery: EthQuery, rawTx: string) => Promise; hooks?: { beforeCheckPendingTransaction?: ( transactionMeta: TransactionMeta, @@ -278,7 +278,8 @@ export class PendingTransactionTracker { return; } - await this.#publishTransaction(rawTx); + const ethQuery = this.#getEthQuery(txMeta.networkClientId) + await this.#publishTransaction(ethQuery, rawTx); txMeta.retryCount = (txMeta.retryCount ?? 0) + 1; diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index d159de0adb..1522a9b0b2 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -1,4 +1,5 @@ import type { AccessList } from '@ethereumjs/tx'; +import type { NetworkClientId } from '@metamask/network-controller'; import type { Hex } from '@metamask/utils'; import type { Operation } from 'fast-json-patch'; @@ -195,6 +196,11 @@ type TransactionMetaBase = { */ isTransfer?: boolean; + /** + * The id for the NetworkClient for the transaction. + */ + networkClientId?: NetworkClientId; + /** * Network code as per EIP-155 for this transaction * diff --git a/packages/transaction-controller/src/utils/gas-fees.ts b/packages/transaction-controller/src/utils/gas-fees.ts index b550470488..2eb04c1c95 100644 --- a/packages/transaction-controller/src/utils/gas-fees.ts +++ b/packages/transaction-controller/src/utils/gas-fees.ts @@ -7,8 +7,12 @@ import { toHex, } from '@metamask/controller-utils'; import type EthQuery from '@metamask/eth-query'; -import type { GasFeeState } from '@metamask/gas-fee-controller'; +import type { + FetchGasFeeEstimateOptions, + GasFeeState, +} from '@metamask/gas-fee-controller'; import { GAS_ESTIMATE_TYPES } from '@metamask/gas-fee-controller'; +import type { Hex } from '@metamask/utils'; import { createModuleLogger } from '@metamask/utils'; import { addHexPrefix } from 'ethereumjs-util'; @@ -25,8 +29,10 @@ import { SWAP_TRANSACTION_TYPES } from './swaps'; export type UpdateGasFeesRequest = { eip1559: boolean; ethQuery: EthQuery; - getSavedGasFees: () => SavedGasFees | undefined; - getGasFeeEstimates: () => Promise; + getSavedGasFees: (chainId: Hex) => SavedGasFees | undefined; + getGasFeeEstimates: ( + options: FetchGasFeeEstimateOptions, + ) => Promise; txMeta: TransactionMeta; }; @@ -45,7 +51,9 @@ export async function updateGasFees(request: UpdateGasFeesRequest) { const isSwap = SWAP_TRANSACTION_TYPES.includes( txMeta.type as TransactionType, ); - const savedGasFees = isSwap ? undefined : request.getSavedGasFees(); + const savedGasFees = isSwap + ? undefined + : request.getSavedGasFees(txMeta.chainId); const suggestedGasFees = await getSuggestedGasFees(request); @@ -268,7 +276,9 @@ async function getSuggestedGasFees(request: UpdateGasFeesRequest) { } try { - const { gasFeeEstimates, gasEstimateType } = await getGasFeeEstimates(); + const { gasFeeEstimates, gasEstimateType } = await getGasFeeEstimates({ + networkClientId: txMeta.networkClientId, + }); if (eip1559 && gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) { const { diff --git a/packages/transaction-controller/src/utils/gas.ts b/packages/transaction-controller/src/utils/gas.ts index a6044b1a45..57c7c204b8 100644 --- a/packages/transaction-controller/src/utils/gas.ts +++ b/packages/transaction-controller/src/utils/gas.ts @@ -8,8 +8,7 @@ import { query, } from '@metamask/controller-utils'; import type EthQuery from '@metamask/eth-query'; -import type { ProviderConfig } from '@metamask/network-controller'; -import { createModuleLogger } from '@metamask/utils'; +import { Hex, createModuleLogger } from '@metamask/utils'; import { addHexPrefix } from 'ethereumjs-util'; import { GAS_BUFFER_CHAIN_OVERRIDES } from '../constants'; @@ -18,7 +17,7 @@ import type { TransactionMeta, TransactionParams } from '../types'; export type UpdateGasRequest = { ethQuery: EthQuery; - providerConfig: ProviderConfig; + providerConfig: {type: NetworkType, chainId: Hex} txMeta: TransactionMeta; }; From 134cea690196bcd4a312163e0f2daf2518486bdd Mon Sep 17 00:00:00 2001 From: jiexi Date: Fri, 15 Dec 2023 07:06:39 -0800 Subject: [PATCH 008/100] Jl/transaction multichain fix update gas request (#3665) ## Explanation Break `UpdateGasRequest.providerConfig `into `chainId` and `isCustomNetwork` ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- .../src/TransactionController.test.ts | 4 +++- .../src/TransactionController.ts | 12 +++++++++--- .../helpers/PendingTransactionTracker.test.ts | 5 ++++- .../src/helpers/PendingTransactionTracker.ts | 7 +++++-- .../src/utils/gas.test.ts | 15 ++++++++------- .../transaction-controller/src/utils/gas.ts | 17 ++++++++--------- 6 files changed, 37 insertions(+), 23 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index fb7588ff7e..aa808fd1c8 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -1355,7 +1355,9 @@ describe('TransactionController', () => { expect(updateGasMock).toHaveBeenCalledTimes(1); expect(updateGasMock).toHaveBeenCalledWith({ ethQuery: expect.any(Object), - providerConfig: MOCK_NETWORK.state.providerConfig, + chainId: MOCK_NETWORK.state.providerConfig.chainId, + isCustomNetwork: + MOCK_NETWORK.state.providerConfig.type === NetworkType.rpc, txMeta: expect.any(Object), }); }); diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 522ba918be..ca8a7a8863 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -99,6 +99,7 @@ import { validateTxParams, } from './utils/validation'; import { NetworkClientConfiguration } from '@metamask/network-controller'; +import { NetworkClientType } from '@metamask/network-controller'; export const HARDFORK = Hardfork.London; @@ -1944,14 +1945,19 @@ export class TransactionController extends BaseControllerV1< const { networkClientId } = transactionMeta; - let providerConfig: ProviderConfig | NetworkClientConfiguration = this.getNetworkState().providerConfig + const { providerConfig } = this.getNetworkState(); + let { chainId } = providerConfig; + let isCustomNetwork = providerConfig.type === NetworkType.rpc; if (networkClientId) { - providerConfig = this.getNetworkClientById(networkClientId).configuration + const { configuration } = this.getNetworkClientById(networkClientId); + chainId = configuration.chainId; + isCustomNetwork = configuration.type === NetworkClientType.Custom; } await updateGas({ ethQuery: this.getEthQuery(networkClientId), - providerConfig, // should this be renamed? + chainId, + isCustomNetwork, txMeta: transactionMeta, }); diff --git a/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts b/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts index 09a65cd905..e1a776d9a1 100644 --- a/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts +++ b/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts @@ -12,6 +12,8 @@ const CHAIN_ID_MOCK = '0x1'; const NONCE_MOCK = '0x2'; const BLOCK_NUMBER_MOCK = '0x123'; +const ETH_QUERY_MOCK = {} + const TRANSACTION_SUBMITTED_MOCK = { id: ID_MOCK, chainId: CHAIN_ID_MOCK, @@ -88,7 +90,7 @@ describe('PendingTransactionTracker', () => { blockTracker, failTransaction, getChainId: () => CHAIN_ID_MOCK, - getEthQuery: () => ({}), + getEthQuery: () => (ETH_QUERY_MOCK), getTransactions: () => [], nonceTracker: createNonceTrackerMock(), onStateChange, @@ -647,6 +649,7 @@ describe('PendingTransactionTracker', () => { expect(options.publishTransaction).toHaveBeenCalledTimes(1); expect(options.publishTransaction).toHaveBeenCalledWith( + ETH_QUERY_MOCK, TRANSACTION_SUBMITTED_MOCK.rawTx, ); }); diff --git a/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts b/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts index 25f500f924..91ea0ead63 100644 --- a/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts +++ b/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts @@ -1,6 +1,9 @@ import { query } from '@metamask/controller-utils'; import type EthQuery from '@metamask/eth-query'; -import type { BlockTracker, NetworkClientId } from '@metamask/network-controller'; +import type { + BlockTracker, + NetworkClientId, +} from '@metamask/network-controller'; import { createModuleLogger } from '@metamask/utils'; import EventEmitter from 'events'; import type { NonceTracker } from 'nonce-tracker'; @@ -278,7 +281,7 @@ export class PendingTransactionTracker { return; } - const ethQuery = this.#getEthQuery(txMeta.networkClientId) + const ethQuery = this.#getEthQuery(txMeta.networkClientId); await this.#publishTransaction(ethQuery, rawTx); txMeta.retryCount = (txMeta.retryCount ?? 0) + 1; diff --git a/packages/transaction-controller/src/utils/gas.test.ts b/packages/transaction-controller/src/utils/gas.test.ts index f1eae2c770..2e919bf885 100644 --- a/packages/transaction-controller/src/utils/gas.test.ts +++ b/packages/transaction-controller/src/utils/gas.test.ts @@ -1,6 +1,6 @@ /* eslint-disable jsdoc/require-jsdoc */ -import { NetworkType, query } from '@metamask/controller-utils'; +import { query } from '@metamask/controller-utils'; import type EthQuery from '@metamask/eth-query'; import { CHAIN_IDS } from '../constants'; @@ -32,7 +32,8 @@ const TRANSACTION_META_MOCK = { const UPDATE_GAS_REQUEST_MOCK = { txMeta: TRANSACTION_META_MOCK, - providerConfig: {}, + chainId: '0x0', + isCustomNetwork: false, ethQuery: ETH_QUERY_MOCK, } as UpdateGasRequest; @@ -112,7 +113,7 @@ describe('gas', () => { }); it('to estimate if custom network', async () => { - updateGasRequest.providerConfig.type = NetworkType.rpc; + updateGasRequest.isCustomNetwork = true; mockQuery({ getBlockByNumberResponse: { gasLimit: toHex(BLOCK_GAS_LIMIT_MOCK) }, @@ -170,7 +171,7 @@ describe('gas', () => { const estimatedGasPadded = Math.ceil(blockGasLimit90Percent - 10); const estimatedGas = estimatedGasPadded; // Optimism multiplier is 1 - updateGasRequest.providerConfig.chainId = CHAIN_IDS.OPTIMISM; + updateGasRequest.chainId = CHAIN_IDS.OPTIMISM; mockQuery({ getCodeResponse: CODE_MOCK, @@ -211,7 +212,7 @@ describe('gas', () => { describe('to fixed value', () => { it('if not custom network and no to parameter', async () => { - updateGasRequest.providerConfig.type = NetworkType.mainnet; + updateGasRequest.isCustomNetwork = false; delete updateGasRequest.txMeta.txParams.to; await updateGas(updateGasRequest); @@ -224,7 +225,7 @@ describe('gas', () => { }); it('if not custom network and to parameter and no data and no code', async () => { - updateGasRequest.providerConfig.type = NetworkType.mainnet; + updateGasRequest.isCustomNetwork = false; delete updateGasRequest.txMeta.txParams.data; mockQuery({ @@ -241,7 +242,7 @@ describe('gas', () => { }); it('if not custom network and to parameter and no data and empty code', async () => { - updateGasRequest.providerConfig.type = NetworkType.mainnet; + updateGasRequest.isCustomNetwork = false; delete updateGasRequest.txMeta.txParams.data; mockQuery({ diff --git a/packages/transaction-controller/src/utils/gas.ts b/packages/transaction-controller/src/utils/gas.ts index 57c7c204b8..396a912cae 100644 --- a/packages/transaction-controller/src/utils/gas.ts +++ b/packages/transaction-controller/src/utils/gas.ts @@ -2,13 +2,13 @@ import { BNToHex, - NetworkType, fractionBN, hexToBN, query, } from '@metamask/controller-utils'; import type EthQuery from '@metamask/eth-query'; -import { Hex, createModuleLogger } from '@metamask/utils'; +import type { Hex } from '@metamask/utils'; +import { createModuleLogger } from '@metamask/utils'; import { addHexPrefix } from 'ethereumjs-util'; import { GAS_BUFFER_CHAIN_OVERRIDES } from '../constants'; @@ -17,7 +17,8 @@ import type { TransactionMeta, TransactionParams } from '../types'; export type UpdateGasRequest = { ethQuery: EthQuery; - providerConfig: {type: NetworkType, chainId: Hex} + isCustomNetwork: boolean; + chainId: Hex; txMeta: TransactionMeta; }; @@ -119,7 +120,7 @@ export function addGasBuffer( async function getGas( request: UpdateGasRequest, ): Promise<[string, TransactionMeta['simulationFails']?]> { - const { providerConfig, txMeta } = request; + const { isCustomNetwork, chainId, txMeta } = request; if (txMeta.txParams.gas) { log('Using value from request', txMeta.txParams.gas); @@ -136,14 +137,14 @@ async function getGas( request.ethQuery, ); - if (providerConfig.type === NetworkType.rpc) { + if (isCustomNetwork) { log('Using original estimate as custom network'); return [estimatedGas, simulationFails]; } const bufferMultiplier = GAS_BUFFER_CHAIN_OVERRIDES[ - providerConfig.chainId as keyof typeof GAS_BUFFER_CHAIN_OVERRIDES + chainId as keyof typeof GAS_BUFFER_CHAIN_OVERRIDES ] ?? DEFAULT_GAS_MULTIPLIER; const bufferedGas = addGasBuffer( @@ -158,10 +159,8 @@ async function getGas( async function requiresFixedGas({ ethQuery, txMeta, - providerConfig, + isCustomNetwork, }: UpdateGasRequest): Promise { - const isCustomNetwork = providerConfig.type === NetworkType.rpc; - const { txParams: { to, data }, } = txMeta; From c762696dab89f4f6573f8cb090069ec625862a2f Mon Sep 17 00:00:00 2001 From: Shane Date: Fri, 15 Dec 2023 12:31:38 -0800 Subject: [PATCH 009/100] Transaction multichain fix starttracking (#3673) ## Explanation This fixes `startTrackingByNetworkClientId`'s trackingMap as well as adds `stopTrackingByNetworkClientId` and some initial tests for them both --- .../src/TransactionController.test.ts | 25 ++++++ .../src/TransactionController.ts | 76 ++++++++++++++----- 2 files changed, 80 insertions(+), 21 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index aa808fd1c8..21b00ef44b 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -4399,4 +4399,29 @@ describe('TransactionController', () => { Current tx status: ${TransactionStatus.submitted}`); }); }); + describe('startTrackinbByNetworkClientId', () => { + it('should start tracking in a tracking map', () => { + const controller = newController(); + const trackingMap = controller.startTrackingByNetworkClientId('mainnet'); + expect(trackingMap.get('mainnet')?.nonceTracker).toBeDefined(); + expect( + trackingMap.get('mainnet')?.incomingTransactionHelper, + ).toBeDefined(); + expect( + trackingMap.get('mainnet')?.pendingTransactionTracker, + ).toBeDefined(); + }); + it('should stop tracking in a tracking map', () => { + const controller = newController(); + const trackingMap = controller.startTrackingByNetworkClientId('mainnet'); + const incomingTransactionHelper = + trackingMap.get('mainnet')?.incomingTransactionHelper; + if (!incomingTransactionHelper) { + throw new Error('incomingTransactionHelper is undefined'); + } + const stopSpy = jest.spyOn(incomingTransactionHelper, 'stop'); + controller.stopTrackingByNetworkClientId('mainnet'); + expect(stopSpy).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index ca8a7a8863..05a47912e5 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -348,11 +348,11 @@ export class TransactionController extends BaseControllerV1< private readonly trackingMap: Map< NetworkClientId, - Set<{ + { nonceTracker: NonceTracker; pendingTransactionTracker: PendingTransactionTracker; incomingTransactionHelper: IncomingTransactionHelper; - }> + } > = new Map(); /** @@ -400,9 +400,9 @@ export class TransactionController extends BaseControllerV1< * @param options.hooks.beforeCheckPendingTransaction - Additional logic to execute before checking pending transactions. Return false to prevent the broadcast of the transaction. * @param options.hooks.beforePublish - Additional logic to execute before publishing a transaction. Return false to prevent the broadcast of the transaction. * @param options.hooks.getAdditionalSignArguments - Returns additional arguments required to sign a transaction. + * @param options.getNetworkClientIdForDomain - Gets the network client id for the given domain. * @param config - Initial options used to configure this controller. * @param state - Initial state to set on this controller. - * @param options.getNetworkClientIdForDomain */ constructor( { @@ -915,7 +915,7 @@ export class TransactionController extends BaseControllerV1< txParams: newTxParams, }); - const ethQuery = this.getEthQuery(transactionMeta.networkClientId) + const ethQuery = this.getEthQuery(transactionMeta.networkClientId); const hash = await this.publishTransaction(ethQuery, rawTx); const cancelTransactionMeta: TransactionMeta = { @@ -1073,8 +1073,8 @@ export class TransactionController extends BaseControllerV1< // that the user approved them on, but it makes sense to allow cancelling // from any network that's also on the same chain. We will need to add a fallback // here to allow using networkClientIds other than the original - const ethQuery = this.getEthQuery(transactionMeta.networkClientId) - const hash = await this.publishTransaction(ethQuery, rawTx) + const ethQuery = this.getEthQuery(transactionMeta.networkClientId); + const hash = await this.publishTransaction(ethQuery, rawTx); const baseTransactionMeta: TransactionMeta = { ...transactionMeta, @@ -1122,10 +1122,25 @@ export class TransactionController extends BaseControllerV1< this.hub.emit(`${transactionMeta.id}:speedup`, newTransactionMeta); } + stopTrackingByNetworkClientId(networkClientId: NetworkClientId) { + const trackers = this.trackingMap.get(networkClientId); + if (trackers) { + this.removePendingTransactionTrackerListeners( + trackers.pendingTransactionTracker, + ); + trackers.incomingTransactionHelper.stop(); + + // doesn't seem like any cleanup is needed for nonceTracker + // trackers.nonceTracker + + // stop not exposed for pendingTransactionTracker + // trackers.pendingTransactionTracker.stop(); + } + this.trackingMap.delete(networkClientId); + } + startTrackingByNetworkClientId(networkClientId: NetworkClientId) { const networkClient = this.getNetworkClientById(networkClientId); - // track using tracking map - this.trackingMap.set(networkClientId, new Set()); const nonceTracker = new NonceTracker({ provider: networkClient.provider as any, blockTracker: networkClient.blockTracker, @@ -1176,11 +1191,12 @@ export class TransactionController extends BaseControllerV1< this.addPendingTransactionTrackerListeners(pendingTransactionTracker); // add to tracking map - this.trackingMap.get(networkClientId)?.add({ + this.trackingMap.set(networkClientId, { nonceTracker, incomingTransactionHelper, pendingTransactionTracker, }); + return this.trackingMap; } /** @@ -1189,8 +1205,11 @@ export class TransactionController extends BaseControllerV1< * @param transaction - The transaction to estimate gas for. * @returns The gas and gas price. */ - async estimateGas(transaction: TransactionParams, networkClientId?: NetworkClientId) { - const ethQuery = this.getEthQuery(networkClientId) + async estimateGas( + transaction: TransactionParams, + networkClientId?: NetworkClientId, + ) { + const ethQuery = this.getEthQuery(networkClientId); const { estimatedGas, simulationFails } = await estimateGas( transaction, ethQuery, @@ -1205,12 +1224,13 @@ export class TransactionController extends BaseControllerV1< * @param transaction - The transaction params to estimate gas for. * @param multiplier - The multiplier to use for the gas buffer. */ - async estimateGasBuffered( // NOTE(JL): Need to update SwapsController's usage of this method + async estimateGasBuffered( + // NOTE(JL): Need to update SwapsController's usage of this method transaction: TransactionParams, multiplier: number, - networkClientId?: NetworkClientId + networkClientId?: NetworkClientId, ) { - const ethQuery = this.getEthQuery(networkClientId) + const ethQuery = this.getEthQuery(networkClientId); const { blockGasLimit, estimatedGas, simulationFails } = await estimateGas( transaction, ethQuery, @@ -1566,7 +1586,8 @@ export class TransactionController extends BaseControllerV1< * @param address - The hex string address for the transaction. * @returns object with the `nextNonce` `nonceDetails`, and the releaseLock. */ - async getNonceLock(address: string): Promise { // NOTE(JL): i think this should take in chainId, but not sure how to deal with networkClientId mapping + async getNonceLock(address: string): Promise { + // NOTE(JL): i think this should take in chainId, but not sure how to deal with networkClientId mapping return this.nonceTracker.getNonceLock(address); } @@ -1985,7 +2006,8 @@ export class TransactionController extends BaseControllerV1< /** * Create approvals for all unapproved transactions on current chain. */ - private createApprovalsForUnapprovedTransactions() { // NOTE(JL): this doesn't seem to be used anywhere. Can we remove it? + private createApprovalsForUnapprovedTransactions() { + // NOTE(JL): this doesn't seem to be used anywhere. Can we remove it? const unapprovedTransactions = this.getCurrentChainTransactionsByStatus( TransactionStatus.unapproved, ); @@ -2220,7 +2242,7 @@ export class TransactionController extends BaseControllerV1< return; } - const ethQuery = this.getEthQuery(transactionMeta.networkClientId) + const ethQuery = this.getEthQuery(transactionMeta.networkClientId); if (transactionMeta.type === TransactionType.swap) { log('Determining pre-transaction balance'); @@ -2266,7 +2288,10 @@ export class TransactionController extends BaseControllerV1< } } - private async publishTransaction(ethQuery: EthQuery, rawTransaction: string): Promise { + private async publishTransaction( + ethQuery: EthQuery, + rawTransaction: string, + ): Promise { return await query(ethQuery, 'sendRawTransaction', [rawTransaction]); } @@ -2547,7 +2572,7 @@ export class TransactionController extends BaseControllerV1< * @param transactionMeta - Nominated external transaction to be added to state. */ private addExternalTransaction(transactionMeta: TransactionMeta) { - const { chainId } = transactionMeta + const { chainId } = transactionMeta; const { transactions } = this.state; const fromAddress = transactionMeta?.txParams?.from; const sameFromAndNetworkTransactions = transactions.filter( @@ -2592,7 +2617,7 @@ export class TransactionController extends BaseControllerV1< // NOTE(JL): Should this method be exiting early if getTransaction returns no transaction object? const nonce = transactionMeta?.txParams?.nonce; const from = transactionMeta?.txParams?.from; - const chainId = transactionMeta?.chainId + const chainId = transactionMeta?.chainId; const sameNonceTxs = this.state.transactions.filter( (transaction) => transaction.txParams.from === from && @@ -2693,6 +2718,15 @@ export class TransactionController extends BaseControllerV1< ); } + private removePendingTransactionTrackerListeners( + pendingTransactionTracker = this.pendingTransactionTracker, + ) { + pendingTransactionTracker.hub.removeAllListeners('transaction-confirmed'); + pendingTransactionTracker.hub.removeAllListeners('transaction-dropped'); + pendingTransactionTracker.hub.removeAllListeners('transaction-failed'); + pendingTransactionTracker.hub.removeAllListeners('transaction-updated'); + } + private addPendingTransactionTrackerListeners( pendingTransactionTracker = this.pendingTransactionTracker, ) { @@ -2829,7 +2863,7 @@ export class TransactionController extends BaseControllerV1< return; } - const ethQuery = this.getEthQuery(transactionMeta.networkClientId) + const ethQuery = this.getEthQuery(transactionMeta.networkClientId); const { updatedTransactionMeta, approvalTransactionMeta } = await updatePostTransactionBalance(transactionMeta, { ethQuery, From 65d13eb22c9c3f97268c592b06a4537d6071346f Mon Sep 17 00:00:00 2001 From: jiexi Date: Tue, 19 Dec 2023 11:59:18 -0800 Subject: [PATCH 010/100] PendingTransactionTracker stop and unsubscribe (#3685) ## Explanation * Makes `stop()` public on `PendingTransactionTracker` * Calls `stop()` on `PendingTransactionTracker` in the `trackingMap` * Moves subscription of transaction state changes (and now network state changes due to upstream controller changes) out of `PendingTransactionTracker` and into a loop in `TransactionController` ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- .../src/TransactionController.ts | 20 +++++++++++----- .../src/helpers/PendingTransactionTracker.ts | 23 ++++++++----------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 2aaaa1b8b6..e704d54845 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -577,11 +577,6 @@ export class TransactionController extends BaseControllerV1< getTransactions: () => this.state.transactions, isResubmitEnabled: pendingTransactions.isResubmitEnabled, nonceTracker: this.nonceTracker, - onStateChange: (listener) => { - this.subscribe(listener); - onNetworkStateChange(listener); - listener(); - }, publishTransaction: this.publishTransaction.bind(this), hooks: { beforeCheckPendingTransaction: @@ -592,14 +587,27 @@ export class TransactionController extends BaseControllerV1< this.addPendingTransactionTrackerListeners(); + this.subscribe(this.#onStateChange) + onNetworkStateChange(() => { log('Detected network change', this.getChainId()); + // TODO(JL): Network state changes also trigger PendingTransactionTracker's onStateChange. + // Verify if this is still necessary when the feature branch is being reviewed + this.#onStateChange() this.onBootCleanup(); }); this.onBootCleanup(); } + #onStateChange = () => { + // PendingTransactionTracker reads state through its getTransactions hook + this.pendingTransactionTracker.onStateChange() + for (const [_, trackingMap] of this.trackingMap) { + trackingMap.pendingTransactionTracker.onStateChange() + } + } + /** * Handle new method data request. * @@ -1129,6 +1137,7 @@ export class TransactionController extends BaseControllerV1< stopTrackingByNetworkClientId(networkClientId: NetworkClientId) { const trackers = this.trackingMap.get(networkClientId); if (trackers) { + trackers.pendingTransactionTracker.stop(); this.removePendingTransactionTrackerListeners( trackers.pendingTransactionTracker, ); @@ -1174,7 +1183,6 @@ export class TransactionController extends BaseControllerV1< getTransactions: () => this.state.transactions, isResubmitEnabled: true, // TODO: make this configurable nonceTracker, - onStateChange: this.subscribe.bind(this), publishTransaction: this.publishTransaction.bind(this), hooks: { beforeCheckPendingTransaction: diff --git a/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts b/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts index 01a8209259..94d43d0056 100644 --- a/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts +++ b/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts @@ -80,8 +80,6 @@ export class PendingTransactionTracker { #nonceTracker: NonceTracker; - #onStateChange: (listener: () => void) => void; - #publishTransaction: (ethQuery: EthQuery, rawTx: string) => Promise; #running: boolean; @@ -98,7 +96,6 @@ export class PendingTransactionTracker { getTransactions, isResubmitEnabled, nonceTracker, - onStateChange, publishTransaction, hooks, }: { @@ -109,7 +106,6 @@ export class PendingTransactionTracker { getTransactions: () => TransactionMeta[]; isResubmitEnabled?: boolean; nonceTracker: NonceTracker; - onStateChange: (listener: () => void) => void; publishTransaction: (ethQuery: EthQuery, rawTx: string) => Promise; hooks?: { beforeCheckPendingTransaction?: ( @@ -129,22 +125,21 @@ export class PendingTransactionTracker { this.#isResubmitEnabled = isResubmitEnabled ?? true; this.#listener = this.#onLatestBlock.bind(this); this.#nonceTracker = nonceTracker; - this.#onStateChange = onStateChange; this.#publishTransaction = publishTransaction; this.#running = false; this.#beforePublish = hooks?.beforePublish ?? (() => true); this.#beforeCheckPendingTransaction = hooks?.beforeCheckPendingTransaction ?? (() => true); + } - this.#onStateChange(() => { - const pendingTransactions = this.#getPendingTransactions(); + onStateChange = () => { + const pendingTransactions = this.#getPendingTransactions(); - if (pendingTransactions.length) { - this.#start(); - } else { - this.#stop(); - } - }); + if (pendingTransactions.length) { + this.#start(); + } else { + this.stop(); + } } #start() { @@ -158,7 +153,7 @@ export class PendingTransactionTracker { log('Started polling'); } - #stop() { + stop() { if (!this.#running) { return; } From a409fa4bac6fc3edfbce825549a7dd58177aba44 Mon Sep 17 00:00:00 2001 From: jiexi Date: Mon, 8 Jan 2024 14:00:32 -0800 Subject: [PATCH 011/100] Use chainId from tx object to created unsignedTx (#3671) ## Explanation * Update `getCommonConfiguration` with required `chainId` param * Use `chainId` param instead of current selected chainId to generate commonConfiguration object * Add `prepareUnsignedEthTx` required `chainId` param * Pass in chainId to `prepareUnsignedEthTx` from relevant tx object * Pass in chainId to `getCommonConfiguration` from relevant tx object * Update `approveTransactionsWithSameNonce` param type to include `{chainId: Hex}` to match what extension is actually calling it with ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --------- Co-authored-by: Alex Donesky --- .../src/TransactionController.test.ts | 6 +++ .../src/TransactionController.ts | 51 +++++++++---------- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 19f0a477d7..5a55761296 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -3465,6 +3465,7 @@ describe('TransactionController', () => { gas: '0x222', to: ACCOUNT_2_MOCK, value: '0x1', + chainId: MOCK_NETWORK.state.providerConfig.chainId }; await expect( @@ -3494,6 +3495,7 @@ describe('TransactionController', () => { gas: '0x5208', to: ACCOUNT_2_MOCK, value: '0x0', + chainId: MOCK_NETWORK.state.providerConfig.chainId }; // Send the transaction to put it in the process of being signed @@ -3524,6 +3526,7 @@ describe('TransactionController', () => { gas: '0x111', to: ACCOUNT_2_MOCK, value: '0x0', + chainId: MOCK_NETWORK.state.providerConfig.chainId }; const mockTransactionParam2 = { from: ACCOUNT_MOCK, @@ -3531,6 +3534,7 @@ describe('TransactionController', () => { gas: '0x222', to: ACCOUNT_2_MOCK, value: '0x1', + chainId: MOCK_NETWORK.state.providerConfig.chainId }; const result = await controller.approveTransactionsWithSameNonce([ @@ -3561,6 +3565,7 @@ describe('TransactionController', () => { gas: '0x111', to: ACCOUNT_2_MOCK, value: '0x0', + chainId: MOCK_NETWORK.state.providerConfig.chainId }; const mockTransactionParam2 = { from: ACCOUNT_MOCK, @@ -3568,6 +3573,7 @@ describe('TransactionController', () => { gas: '0x222', to: ACCOUNT_2_MOCK, value: '0x1', + chainId: MOCK_NETWORK.state.providerConfig.chainId }; await expect( diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index d9968c247c..883f165b07 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -30,6 +30,7 @@ import type { Provider, ProviderConfig, } from '@metamask/network-controller'; +import { NetworkClientConfiguration } from '@metamask/network-controller'; import { errorCodes, rpcErrors, providerErrors } from '@metamask/rpc-errors'; import type { Hex } from '@metamask/utils'; import { Mutex } from 'async-mutex'; @@ -98,7 +99,6 @@ import { validateTransactionOrigin, validateTxParams, } from './utils/validation'; -import { NetworkClientConfiguration } from '@metamask/network-controller'; import { NetworkClientType } from '@metamask/network-controller'; export const HARDFORK = Hardfork.London; @@ -907,7 +907,10 @@ export class TransactionController extends BaseControllerV1< value: '0x0', }; - const unsignedEthTx = this.prepareUnsignedEthTx(newTxParams); + const unsignedEthTx = this.prepareUnsignedEthTx( + transactionMeta.chainId, + newTxParams, + ); const signedTx = await this.sign( unsignedEthTx, @@ -1064,7 +1067,10 @@ export class TransactionController extends BaseControllerV1< gasPrice: newGasPrice, }; - const unsignedEthTx = this.prepareUnsignedEthTx(txParams); + const unsignedEthTx = this.prepareUnsignedEthTx( + transactionMeta.chainId, + txParams, + ); const signedTx = await this.sign( unsignedEthTx, @@ -1680,7 +1686,7 @@ export class TransactionController extends BaseControllerV1< * @returns The raw transactions. */ async approveTransactionsWithSameNonce( - listOfTxParams: TransactionParams[] = [], + listOfTxParams: (TransactionParams & { chainId: Hex })[] = [], ): Promise { log('Approving transactions with same nonce', { transactions: listOfTxParams, @@ -1691,12 +1697,11 @@ export class TransactionController extends BaseControllerV1< } const initialTx = listOfTxParams[0]; - const common = this.getCommonConfiguration(); + const common = this.getCommonConfiguration(initialTx.chainId); const initialTxAsEthTx = TransactionFactory.fromTxData(initialTx, { common, }); - const initialTxAsSerializedHex = bufferToHex(initialTxAsEthTx.serialize()); if (this.inProcessOfSigning.has(initialTxAsSerializedHex)) { @@ -1717,7 +1722,7 @@ export class TransactionController extends BaseControllerV1< rawTransactions = await Promise.all( listOfTxParams.map((txParams) => { txParams.nonce = addHexPrefix(nonce.toString(16)); - return this.signExternalTransaction(txParams); + return this.signExternalTransaction(txParams.chainId, txParams); }), ); } catch (err) { @@ -1927,6 +1932,7 @@ export class TransactionController extends BaseControllerV1< } private async signExternalTransaction( + chainId: Hex, transactionParams: TransactionParams, ): Promise { if (!this.sign) { @@ -1934,7 +1940,6 @@ export class TransactionController extends BaseControllerV1< } const normalizedTransactionParams = normalizeTxParams(transactionParams); - const chainId = this.getChainId(); const type = isEIP1559Transaction(normalizedTransactionParams) ? TransactionEnvelopeType.feeMarket : TransactionEnvelopeType.legacy; @@ -1946,7 +1951,7 @@ export class TransactionController extends BaseControllerV1< }; const { from } = updatedTransactionParams; - const common = this.getCommonConfiguration(); + const common = this.getCommonConfiguration(chainId); const unsignedTransaction = TransactionFactory.fromTxData( updatedTransactionParams, { common }, @@ -2444,10 +2449,13 @@ export class TransactionController extends BaseControllerV1< return providerConfig.chainId; } - private prepareUnsignedEthTx(txParams: TransactionParams): TypedTransaction { + private prepareUnsignedEthTx( + chainId: Hex, + txParams: TransactionParams, + ): TypedTransaction { return TransactionFactory.fromTxData(txParams, { - common: this.getCommonConfiguration(), freeze: false, + common: this.getCommonConfiguration(chainId), }); } @@ -2458,23 +2466,11 @@ export class TransactionController extends BaseControllerV1< * specified in txParams, @ethereumjs/tx is able to determine which EIP-2718 * transaction type to use. * + * @param chainId * @returns common configuration object */ - private getCommonConfiguration(): Common { - const { - providerConfig: { type: chain, chainId, nickname: name }, - } = this.getNetworkState(); - - if ( - chain !== RPC && - chain !== NetworkType['linea-goerli'] && - chain !== NetworkType['linea-mainnet'] - ) { - return new Common({ chain, hardfork: HARDFORK }); - } - + private getCommonConfiguration(chainId: Hex): Common { const customChainParams: Partial = { - name, chainId: parseInt(chainId, 16), defaultHardfork: HARDFORK, }; @@ -2750,7 +2746,10 @@ export class TransactionController extends BaseControllerV1< ): Promise { log('Signing transaction', txParams); - const unsignedEthTx = this.prepareUnsignedEthTx(txParams); + const unsignedEthTx = this.prepareUnsignedEthTx( + transactionMeta.chainId, + txParams, + ); this.inProcessOfSigning.add(transactionMeta.id); From 7733dbbb280d79c16306c3ea9373ba8f11b093b7 Mon Sep 17 00:00:00 2001 From: jiexi Date: Mon, 8 Jan 2024 14:21:57 -0800 Subject: [PATCH 012/100] Jl/transaction multichain nonce lock by chain (#3666) ## Explanation Naively couples existing mutex with nonceLock mutex. Doesn't seem to cause a deadlock ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- .../src/TransactionController.ts | 59 ++++++++++++++++--- .../helpers/PendingTransactionTracker.test.ts | 4 +- 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 883f165b07..e53cf59562 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -15,7 +15,6 @@ import { BaseControllerV1 } from '@metamask/base-controller'; import { query, NetworkType, - RPC, ApprovalType, ORIGIN_METAMASK, convertHexToDecimal, @@ -28,9 +27,10 @@ import type { NetworkController, NetworkState, Provider, - ProviderConfig, } from '@metamask/network-controller'; -import { NetworkClientConfiguration } from '@metamask/network-controller'; +import { + NetworkClientType, +} from '@metamask/network-controller'; import { errorCodes, rpcErrors, providerErrors } from '@metamask/rpc-errors'; import type { Hex } from '@metamask/utils'; import { Mutex } from 'async-mutex'; @@ -99,7 +99,6 @@ import { validateTransactionOrigin, validateTxParams, } from './utils/validation'; -import { NetworkClientType } from '@metamask/network-controller'; export const HARDFORK = Hardfork.London; @@ -249,6 +248,8 @@ export class TransactionController extends BaseControllerV1< private readonly mutex = new Mutex(); + private readonly nonceMutexByChainId = new Map(); + private readonly getSavedGasFees: (chainId: Hex) => SavedGasFees | undefined; private readonly getNetworkState: () => NetworkState; @@ -1161,6 +1162,11 @@ export class TransactionController extends BaseControllerV1< startTrackingByNetworkClientId(networkClientId: NetworkClientId) { const networkClient = this.getNetworkClientById(networkClientId); + const { chainId } = networkClient.configuration; + if (!this.nonceMutexByChainId.get(chainId)) { + this.nonceMutexByChainId.set(chainId, new Mutex()); + } + const nonceTracker = new NonceTracker({ provider: networkClient.provider as any, blockTracker: networkClient.blockTracker, @@ -1222,6 +1228,7 @@ export class TransactionController extends BaseControllerV1< * Estimates required gas for a given transaction. * * @param transaction - The transaction to estimate gas for. + * @param networkClientId * @returns The gas and gas price. */ async estimateGas( @@ -1242,6 +1249,7 @@ export class TransactionController extends BaseControllerV1< * * @param transaction - The transaction params to estimate gas for. * @param multiplier - The multiplier to use for the gas buffer. + * @param networkClientId */ async estimateGasBuffered( // NOTE(JL): Need to update SwapsController's usage of this method @@ -1603,11 +1611,46 @@ export class TransactionController extends BaseControllerV1< * Ensure `releaseLock` is called once processing of the `nonce` value is complete. * * @param address - The hex string address for the transaction. + * @param networkClientId * @returns object with the `nextNonce` `nonceDetails`, and the releaseLock. */ - async getNonceLock(address: string): Promise { - // NOTE(JL): i think this should take in chainId, but not sure how to deal with networkClientId mapping - return this.nonceTracker.getNonceLock(address); + async getNonceLock( + address: string, + networkClientId?: NetworkClientId, + ): Promise { + // TODO(JL): Revisit this method. It's a bit complicated and not obvious what it achieves. + let nonceMutexForChainId: Mutex | undefined; + let nonceTracker = this.nonceTracker + if (networkClientId) { + const networkClient = this.getNetworkClientById(networkClientId); + nonceMutexForChainId = this.nonceMutexByChainId.get( + networkClient.configuration.chainId, + ); + const trackers = this.trackingMap.get(networkClientId) + if (!trackers) { + throw new Error('missing nonceTracker for networkClientId') + } + nonceTracker = trackers?.nonceTracker + } + + // Acquires the lock for the chainId and the nonceLock from the nonceTracker and then + // couples them together by replacing the nonceLock's releaseLock method with + // an anonymous function that calls releases both the original nonceLock and the + // lock for the chainId. + const releaseLockForChainId = await nonceMutexForChainId?.acquire(); + try { + const nonceLock = await nonceTracker.getNonceLock(address); + return { + ...nonceLock, + releaseLock: () => { + nonceLock.releaseLock(); + releaseLockForChainId?.(); + }, + }; + } catch (err) { + releaseLockForChainId?.(); + throw err; + } } /** @@ -1714,7 +1757,7 @@ export class TransactionController extends BaseControllerV1< try { // TODO: we should add a check to verify that all transactions have the same from address const fromAddress = initialTx.from; - nonceLock = await this.nonceTracker.getNonceLock(fromAddress); + nonceLock = await this.getNonceLock(fromAddress); const nonce = nonceLock.nextNonce; log('Using nonce from nonce tracker', nonce, nonceLock.nonceDetails); diff --git a/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts b/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts index 80c879c67d..35ffe7e713 100644 --- a/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts +++ b/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts @@ -13,7 +13,7 @@ const CHAIN_ID_MOCK = '0x1'; const NONCE_MOCK = '0x2'; const BLOCK_NUMBER_MOCK = '0x123'; -const ETH_QUERY_MOCK = {} +const ETH_QUERY_MOCK = {}; const TRANSACTION_SUBMITTED_MOCK = { id: ID_MOCK, @@ -100,7 +100,7 @@ describe('PendingTransactionTracker', () => { blockTracker, failTransaction, getChainId: () => CHAIN_ID_MOCK, - getEthQuery: () => (ETH_QUERY_MOCK), + getEthQuery: () => ETH_QUERY_MOCK, getTransactions: jest.fn(), nonceTracker: createNonceTrackerMock(), onStateChange, From e04937117d085e1a03f46f0796c58b641c920b4a Mon Sep 17 00:00:00 2001 From: jiexi Date: Tue, 9 Jan 2024 08:29:40 -0800 Subject: [PATCH 013/100] Fix pendingTx spec (#3746) ## Explanation Fixes the PendingTransactionTracker specs. The specs should probably be cleaned up to use a helper rather than relying on mocking in beforeEach ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- .../helpers/PendingTransactionTracker.test.ts | 131 +++++++++--------- .../src/utils/gas.test.ts | 2 +- 2 files changed, 66 insertions(+), 67 deletions(-) diff --git a/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts b/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts index 35ffe7e713..7deba8c2ec 100644 --- a/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts +++ b/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts @@ -66,7 +66,7 @@ describe('PendingTransactionTracker', () => { const queryMock = jest.mocked(query); let blockTracker: jest.Mocked; let failTransaction: jest.Mock; - let onStateChange: jest.Mock; + let pendingTransactionTracker: PendingTransactionTracker // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any let options: any; @@ -79,7 +79,8 @@ describe('PendingTransactionTracker', () => { { ...TRANSACTION_SUBMITTED_MOCK }, ]); - onStateChange.mock.calls[0][0](); + + pendingTransactionTracker.onStateChange() if (transactionsOnCheck) { options.getTransactions.mockReturnValue(transactionsOnCheck); @@ -93,7 +94,6 @@ describe('PendingTransactionTracker', () => { blockTracker = createBlockTrackerMock(); failTransaction = jest.fn(); - onStateChange = jest.fn(); options = { approveTransaction: jest.fn(), @@ -103,18 +103,17 @@ describe('PendingTransactionTracker', () => { getEthQuery: () => ETH_QUERY_MOCK, getTransactions: jest.fn(), nonceTracker: createNonceTrackerMock(), - onStateChange, publishTransaction: jest.fn(), }; }); describe('on state change', () => { it('adds block tracker listener if pending transactions', () => { - new PendingTransactionTracker(options); + pendingTransactionTracker = new PendingTransactionTracker(options); options.getTransactions.mockReturnValue([TRANSACTION_SUBMITTED_MOCK]); - options.onStateChange.mock.calls[0][0](); + pendingTransactionTracker.onStateChange() expect(blockTracker.on).toHaveBeenCalledTimes(1); expect(blockTracker.on).toHaveBeenCalledWith( @@ -124,29 +123,29 @@ describe('PendingTransactionTracker', () => { }); it('does nothing if block tracker listener already added', () => { - new PendingTransactionTracker(options); + pendingTransactionTracker = new PendingTransactionTracker(options); options.getTransactions.mockReturnValue([TRANSACTION_SUBMITTED_MOCK]); - onStateChange.mock.calls[0][0](); - onStateChange.mock.calls[0][0](); + pendingTransactionTracker.onStateChange() + pendingTransactionTracker.onStateChange() expect(blockTracker.on).toHaveBeenCalledTimes(1); expect(blockTracker.removeListener).toHaveBeenCalledTimes(0); }); it('removes block tracker listener if no pending transactions and running', () => { - new PendingTransactionTracker(options); + pendingTransactionTracker = new PendingTransactionTracker(options); options.getTransactions.mockReturnValue([TRANSACTION_SUBMITTED_MOCK]); - onStateChange.mock.calls[0][0](); + pendingTransactionTracker.onStateChange() expect(blockTracker.removeListener).toHaveBeenCalledTimes(0); options.getTransactions.mockReturnValue([]); - onStateChange.mock.calls[0][0](); + pendingTransactionTracker.onStateChange() expect(blockTracker.removeListener).toHaveBeenCalledTimes(1); expect(blockTracker.removeListener).toHaveBeenCalledWith( @@ -156,21 +155,21 @@ describe('PendingTransactionTracker', () => { }); it('does nothing if block tracker listener already removed', () => { - new PendingTransactionTracker(options); + pendingTransactionTracker = new PendingTransactionTracker(options); options.getTransactions.mockReturnValue([TRANSACTION_SUBMITTED_MOCK]); - onStateChange.mock.calls[0][0](); + pendingTransactionTracker.onStateChange() expect(blockTracker.removeListener).toHaveBeenCalledTimes(0); options.getTransactions.mockReturnValue([]); - onStateChange.mock.calls[0][0](); + pendingTransactionTracker.onStateChange() expect(blockTracker.removeListener).toHaveBeenCalledTimes(1); - onStateChange.mock.calls[0][0](); + pendingTransactionTracker.onStateChange(); expect(blockTracker.removeListener).toHaveBeenCalledTimes(1); }); @@ -182,12 +181,12 @@ describe('PendingTransactionTracker', () => { it('if no pending transactions', async () => { const listener = jest.fn(); - const tracker = new PendingTransactionTracker(options); + pendingTransactionTracker = new PendingTransactionTracker(options); - tracker.hub.addListener('transaction-dropped', listener); - tracker.hub.addListener('transaction-failed', listener); - tracker.hub.addListener('transaction-confirmed', listener); - tracker.hub.addListener('transaction-updated', listener); + pendingTransactionTracker.hub.addListener('transaction-dropped', listener); + pendingTransactionTracker.hub.addListener('transaction-failed', listener); + pendingTransactionTracker.hub.addListener('transaction-confirmed', listener); + pendingTransactionTracker.hub.addListener('transaction-updated', listener); await onLatestBlock(undefined, [ { @@ -214,16 +213,16 @@ describe('PendingTransactionTracker', () => { it('if no receipt', async () => { const listener = jest.fn(); - const tracker = new PendingTransactionTracker({ + pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: () => [{ ...TRANSACTION_SUBMITTED_MOCK }], // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - tracker.hub.addListener('transaction-dropped', listener); - tracker.hub.addListener('transaction-failed', listener); - tracker.hub.addListener('transaction-confirmed', listener); + pendingTransactionTracker.hub.addListener('transaction-dropped', listener); + pendingTransactionTracker.hub.addListener('transaction-failed', listener); + pendingTransactionTracker.hub.addListener('transaction-confirmed', listener); queryMock.mockResolvedValueOnce(undefined); queryMock.mockResolvedValueOnce('0x1'); @@ -236,16 +235,16 @@ describe('PendingTransactionTracker', () => { it('if receipt has no status', async () => { const listener = jest.fn(); - const tracker = new PendingTransactionTracker({ + pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: () => [{ ...TRANSACTION_SUBMITTED_MOCK }], // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - tracker.hub.addListener('transaction-dropped', listener); - tracker.hub.addListener('transaction-failed', listener); - tracker.hub.addListener('transaction-confirmed', listener); + pendingTransactionTracker.hub.addListener('transaction-dropped', listener); + pendingTransactionTracker.hub.addListener('transaction-failed', listener); + pendingTransactionTracker.hub.addListener('transaction-confirmed', listener); queryMock.mockResolvedValueOnce({ ...RECEIPT_MOCK, status: null }); queryMock.mockResolvedValueOnce('0x1'); @@ -258,16 +257,16 @@ describe('PendingTransactionTracker', () => { it('if receipt has invalid status', async () => { const listener = jest.fn(); - const tracker = new PendingTransactionTracker({ + pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: () => [{ ...TRANSACTION_SUBMITTED_MOCK }], // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - tracker.hub.addListener('transaction-dropped', listener); - tracker.hub.addListener('transaction-failed', listener); - tracker.hub.addListener('transaction-confirmed', listener); + pendingTransactionTracker.hub.addListener('transaction-dropped', listener); + pendingTransactionTracker.hub.addListener('transaction-failed', listener); + pendingTransactionTracker.hub.addListener('transaction-confirmed', listener); queryMock.mockResolvedValueOnce({ ...RECEIPT_MOCK, status: '0x3' }); queryMock.mockResolvedValueOnce('0x1'); @@ -287,14 +286,14 @@ describe('PendingTransactionTracker', () => { hash: undefined, }; - const tracker = new PendingTransactionTracker({ + pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: () => [transactionMetaMock], // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - tracker.hub.addListener('transaction-failed', listener); + pendingTransactionTracker.hub.addListener('transaction-failed', listener); await onLatestBlock(); @@ -315,7 +314,7 @@ describe('PendingTransactionTracker', () => { hash: undefined, }; - const tracker = new PendingTransactionTracker({ + pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: () => [transactionMetaMock], hooks: { @@ -326,7 +325,7 @@ describe('PendingTransactionTracker', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - tracker.hub.addListener('transaction-failed', listener); + pendingTransactionTracker.hub.addListener('transaction-failed', listener); await onLatestBlock(); @@ -336,14 +335,14 @@ describe('PendingTransactionTracker', () => { it('if receipt has error status', async () => { const listener = jest.fn(); - const tracker = new PendingTransactionTracker({ + pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: () => [{ ...TRANSACTION_SUBMITTED_MOCK }], // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - tracker.hub.addListener('transaction-failed', listener); + pendingTransactionTracker.hub.addListener('transaction-failed', listener); queryMock.mockResolvedValueOnce({ ...RECEIPT_MOCK, status: '0x0' }); @@ -371,7 +370,7 @@ describe('PendingTransactionTracker', () => { ...TRANSACTION_SUBMITTED_MOCK, }; - const tracker = new PendingTransactionTracker({ + pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: () => [ confirmedTransactionMetaMock, @@ -381,7 +380,7 @@ describe('PendingTransactionTracker', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - tracker.hub.addListener('transaction-dropped', listener); + pendingTransactionTracker.hub.addListener('transaction-dropped', listener); await onLatestBlock(); @@ -392,14 +391,14 @@ describe('PendingTransactionTracker', () => { it('if nonce exceeded for 3 subsequent blocks', async () => { const listener = jest.fn(); - const tracker = new PendingTransactionTracker({ + pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: () => [{ ...TRANSACTION_SUBMITTED_MOCK }], // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - tracker.hub.addListener('transaction-dropped', listener); + pendingTransactionTracker.hub.addListener('transaction-dropped', listener); for (let i = 0; i < 4; i++) { expect(listener).toHaveBeenCalledTimes(0); @@ -428,7 +427,7 @@ describe('PendingTransactionTracker', () => { ...TRANSACTION_SUBMITTED_MOCK, }; - const tracker = new PendingTransactionTracker({ + pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: () => [ confirmedTransactionMetaMock, @@ -438,7 +437,7 @@ describe('PendingTransactionTracker', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - tracker.hub.addListener('transaction-dropped', listener); + pendingTransactionTracker.hub.addListener('transaction-dropped', listener); await onLatestBlock(); @@ -450,14 +449,14 @@ describe('PendingTransactionTracker', () => { it('if receipt has success status', async () => { const listener = jest.fn(); - const tracker = new PendingTransactionTracker({ + pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: () => [{ ...TRANSACTION_SUBMITTED_MOCK }], // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - tracker.hub.addListener('transaction-confirmed', listener); + pendingTransactionTracker.hub.addListener('transaction-confirmed', listener); queryMock.mockResolvedValueOnce(RECEIPT_MOCK); queryMock.mockResolvedValueOnce(BLOCK_MOCK); @@ -480,14 +479,14 @@ describe('PendingTransactionTracker', () => { it('if receipt has success status', async () => { const listener = jest.fn(); - const tracker = new PendingTransactionTracker({ + pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: () => [{ ...TRANSACTION_SUBMITTED_MOCK }], // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - tracker.hub.addListener('transaction-updated', listener); + pendingTransactionTracker.hub.addListener('transaction-updated', listener); queryMock.mockResolvedValueOnce(RECEIPT_MOCK); queryMock.mockResolvedValueOnce(BLOCK_MOCK); @@ -512,14 +511,14 @@ describe('PendingTransactionTracker', () => { const listener = jest.fn(); const transaction = { ...TRANSACTION_SUBMITTED_MOCK }; - const tracker = new PendingTransactionTracker({ + pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: () => [transaction], // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - tracker.hub.addListener('transaction-updated', listener); + pendingTransactionTracker.hub.addListener('transaction-updated', listener); queryMock.mockRejectedValueOnce(new Error('TestError')); queryMock.mockResolvedValueOnce(BLOCK_MOCK); @@ -545,7 +544,7 @@ describe('PendingTransactionTracker', () => { describe('resubmits', () => { describe('does nothing', () => { it('if no pending transactions', async () => { - new PendingTransactionTracker(options); + pendingTransactionTracker =new PendingTransactionTracker(options); await onLatestBlock(undefined, []); @@ -558,14 +557,14 @@ describe('PendingTransactionTracker', () => { it('if first retry check', async () => { const listener = jest.fn(); - const tracker = new PendingTransactionTracker({ + pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: () => [{ ...TRANSACTION_SUBMITTED_MOCK }], // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - tracker.hub.addListener('transaction-updated', listener); + pendingTransactionTracker.hub.addListener('transaction-updated', listener); queryMock.mockResolvedValueOnce(undefined); queryMock.mockResolvedValueOnce('0x1'); @@ -586,14 +585,14 @@ describe('PendingTransactionTracker', () => { const listener = jest.fn(); const transaction = { ...TRANSACTION_SUBMITTED_MOCK }; - const tracker = new PendingTransactionTracker({ + pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: () => [transaction], // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - tracker.hub.addListener('transaction-updated', listener); + pendingTransactionTracker.hub.addListener('transaction-updated', listener); queryMock.mockResolvedValueOnce(undefined); queryMock.mockResolvedValueOnce('0x1'); @@ -618,7 +617,7 @@ describe('PendingTransactionTracker', () => { ...TRANSACTION_SUBMITTED_MOCK, }; - const tracker = new PendingTransactionTracker({ + pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: () => [transaction], hooks: { @@ -629,7 +628,7 @@ describe('PendingTransactionTracker', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - tracker.hub.addListener('transaction-updated', listener); + pendingTransactionTracker.hub.addListener('transaction-updated', listener); queryMock.mockResolvedValueOnce(undefined); queryMock.mockResolvedValueOnce('0x1'); @@ -652,14 +651,14 @@ describe('PendingTransactionTracker', () => { const listener = jest.fn(); const transaction = { ...TRANSACTION_SUBMITTED_MOCK }; - const tracker = new PendingTransactionTracker({ + pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: () => [transaction], // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - tracker.hub.addListener('transaction-updated', listener); + pendingTransactionTracker.hub.addListener('transaction-updated', listener); queryMock.mockResolvedValueOnce(undefined); queryMock.mockResolvedValueOnce(BLOCK_MOCK); @@ -690,14 +689,14 @@ describe('PendingTransactionTracker', () => { const listener = jest.fn(); const transaction = { ...TRANSACTION_SUBMITTED_MOCK }; - const tracker = new PendingTransactionTracker({ + pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: () => [transaction], // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - tracker.hub.addListener('transaction-updated', listener); + pendingTransactionTracker.hub.addListener('transaction-updated', listener); queryMock.mockResolvedValueOnce(undefined); queryMock.mockResolvedValueOnce(BLOCK_MOCK); @@ -721,7 +720,7 @@ describe('PendingTransactionTracker', () => { it('if latest block number increased', async () => { const transaction = { ...TRANSACTION_SUBMITTED_MOCK }; - new PendingTransactionTracker({ + pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: () => [transaction], // TODO: Replace `any` with type @@ -744,7 +743,7 @@ describe('PendingTransactionTracker', () => { it('if latest block number matches retry count exponential delay', async () => { const transaction = { ...TRANSACTION_SUBMITTED_MOCK }; - new PendingTransactionTracker({ + pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: () => [transaction], // TODO: Replace `any` with type @@ -779,7 +778,7 @@ describe('PendingTransactionTracker', () => { it('unless resubmit disabled', async () => { const transaction = { ...TRANSACTION_SUBMITTED_MOCK }; - new PendingTransactionTracker({ + pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: () => [transaction], isResubmitEnabled: false, @@ -804,7 +803,7 @@ describe('PendingTransactionTracker', () => { rawTx: undefined, }; - new PendingTransactionTracker({ + pendingTransactionTracker = new PendingTransactionTracker({ ...options, getTransactions: () => [transaction], // TODO: Replace `any` with type diff --git a/packages/transaction-controller/src/utils/gas.test.ts b/packages/transaction-controller/src/utils/gas.test.ts index 46d8895304..53e66f73e8 100644 --- a/packages/transaction-controller/src/utils/gas.test.ts +++ b/packages/transaction-controller/src/utils/gas.test.ts @@ -134,7 +134,7 @@ describe('gas', () => { }); it('to estimate if not custom network and no to parameter', async () => { - updateGasRequest.providerConfig.type = NetworkType.mainnet; + updateGasRequest.isCustomNetwork = false; const gasEstimation = Math.ceil(GAS_MOCK * DEFAULT_GAS_MULTIPLIER); delete updateGasRequest.txMeta.txParams.to; mockQuery({ From bdc6c0e32eb28684d35305f16037dc49d5a6adc8 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Tue, 9 Jan 2024 12:38:13 -0600 Subject: [PATCH 014/100] lint (#3754) Fixes lint issues --- packages/transaction-controller/package.json | 2 + .../src/TransactionController.test.ts | 12 +- .../src/TransactionController.ts | 43 ++--- .../helpers/PendingTransactionTracker.test.ts | 156 +++++++++++++----- .../src/helpers/PendingTransactionTracker.ts | 2 +- yarn.lock | 2 + 6 files changed, 151 insertions(+), 66 deletions(-) diff --git a/packages/transaction-controller/package.json b/packages/transaction-controller/package.json index db249dc347..7f3764857e 100644 --- a/packages/transaction-controller/package.json +++ b/packages/transaction-controller/package.json @@ -42,6 +42,7 @@ "@metamask/metamask-eth-abis": "^3.0.0", "@metamask/network-controller": "^17.1.0", "@metamask/rpc-errors": "^6.1.0", + "@metamask/selected-network-controller": "^6.0.0", "@metamask/utils": "^8.2.0", "async-mutex": "^0.2.6", "eth-method-registry": "^3.0.0", @@ -69,6 +70,7 @@ "@metamask/approval-controller": "^5.1.1", "@metamask/gas-fee-controller": "^12.0.0", "@metamask/network-controller": "^17.1.0", + "@metamask/selected-network-controller": "^6.0.0", "babel-runtime": "^6.26.0" }, "engines": { diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 5a55761296..030a9ad2e2 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -3465,7 +3465,7 @@ describe('TransactionController', () => { gas: '0x222', to: ACCOUNT_2_MOCK, value: '0x1', - chainId: MOCK_NETWORK.state.providerConfig.chainId + chainId: MOCK_NETWORK.state.providerConfig.chainId, }; await expect( @@ -3495,7 +3495,7 @@ describe('TransactionController', () => { gas: '0x5208', to: ACCOUNT_2_MOCK, value: '0x0', - chainId: MOCK_NETWORK.state.providerConfig.chainId + chainId: MOCK_NETWORK.state.providerConfig.chainId, }; // Send the transaction to put it in the process of being signed @@ -3526,7 +3526,7 @@ describe('TransactionController', () => { gas: '0x111', to: ACCOUNT_2_MOCK, value: '0x0', - chainId: MOCK_NETWORK.state.providerConfig.chainId + chainId: MOCK_NETWORK.state.providerConfig.chainId, }; const mockTransactionParam2 = { from: ACCOUNT_MOCK, @@ -3534,7 +3534,7 @@ describe('TransactionController', () => { gas: '0x222', to: ACCOUNT_2_MOCK, value: '0x1', - chainId: MOCK_NETWORK.state.providerConfig.chainId + chainId: MOCK_NETWORK.state.providerConfig.chainId, }; const result = await controller.approveTransactionsWithSameNonce([ @@ -3565,7 +3565,7 @@ describe('TransactionController', () => { gas: '0x111', to: ACCOUNT_2_MOCK, value: '0x0', - chainId: MOCK_NETWORK.state.providerConfig.chainId + chainId: MOCK_NETWORK.state.providerConfig.chainId, }; const mockTransactionParam2 = { from: ACCOUNT_MOCK, @@ -3573,7 +3573,7 @@ describe('TransactionController', () => { gas: '0x222', to: ACCOUNT_2_MOCK, value: '0x1', - chainId: MOCK_NETWORK.state.providerConfig.chainId + chainId: MOCK_NETWORK.state.providerConfig.chainId, }; await expect( diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index e53cf59562..44862e9346 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -28,10 +28,9 @@ import type { NetworkState, Provider, } from '@metamask/network-controller'; -import { - NetworkClientType, -} from '@metamask/network-controller'; +import { NetworkClientType } from '@metamask/network-controller'; import { errorCodes, rpcErrors, providerErrors } from '@metamask/rpc-errors'; +import type { SelectedNetworkController } from '@metamask/selected-network-controller'; import type { Hex } from '@metamask/utils'; import { Mutex } from 'async-mutex'; import { MethodRegistry } from 'eth-method-registry'; @@ -45,7 +44,6 @@ import type { } from 'nonce-tracker'; import { v1 as random } from 'uuid'; -import type { SelectedNetworkController } from '../../selected-network-controller/src/SelectedNetworkController'; import { EtherscanRemoteTransactionSource } from './helpers/EtherscanRemoteTransactionSource'; import { IncomingTransactionHelper } from './helpers/IncomingTransactionHelper'; import { PendingTransactionTracker } from './helpers/PendingTransactionTracker'; @@ -404,7 +402,6 @@ export class TransactionController extends BaseControllerV1< * @param options.getNetworkClientIdForDomain - Gets the network client id for the given domain. * @param config - Initial options used to configure this controller. * @param state - Initial state to set on this controller. - * @param options.getNetworkClientIdForDomain */ constructor( { @@ -589,13 +586,13 @@ export class TransactionController extends BaseControllerV1< this.addPendingTransactionTrackerListeners(); - this.subscribe(this.#onStateChange) + this.subscribe(this.#onStateChange); onNetworkStateChange(() => { log('Detected network change', this.getChainId()); // TODO(JL): Network state changes also trigger PendingTransactionTracker's onStateChange. // Verify if this is still necessary when the feature branch is being reviewed - this.#onStateChange() + this.#onStateChange(); this.onBootCleanup(); }); @@ -604,11 +601,11 @@ export class TransactionController extends BaseControllerV1< #onStateChange = () => { // PendingTransactionTracker reads state through its getTransactions hook - this.pendingTransactionTracker.onStateChange() - for (const [_, trackingMap] of this.trackingMap) { - trackingMap.pendingTransactionTracker.onStateChange() + this.pendingTransactionTracker.onStateChange(); + for (const [, trackingMap] of this.trackingMap) { + trackingMap.pendingTransactionTracker.onStateChange(); } - } + }; /** * Handle new method data request. @@ -661,7 +658,7 @@ export class TransactionController extends BaseControllerV1< * @param opts.swaps - Options for swaps transactions. * @param opts.swaps.hasApproveTx - Whether the transaction has an approval transaction. * @param opts.swaps.meta - Metadata for swap transaction. - * @param opts.networkClientId + * @param opts.networkClientId - The id of the network client for this transaction. * @returns Object containing a promise resolving to the transaction hash if approved. */ async addTransaction( @@ -1168,6 +1165,8 @@ export class TransactionController extends BaseControllerV1< } const nonceTracker = new NonceTracker({ + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any provider: networkClient.provider as any, blockTracker: networkClient.blockTracker, getPendingTransactions: @@ -1190,6 +1189,8 @@ export class TransactionController extends BaseControllerV1< }); const pendingTransactionTracker = new PendingTransactionTracker({ approveTransaction: this.approveTransaction.bind(this), + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any blockTracker: networkClient.provider as any, getChainId: () => networkClient.configuration.chainId, getEthQuery: () => this.ethQuery, // TODO: use networkClient to construct ethQuery @@ -1228,7 +1229,7 @@ export class TransactionController extends BaseControllerV1< * Estimates required gas for a given transaction. * * @param transaction - The transaction to estimate gas for. - * @param networkClientId + * @param networkClientId - The network client id to use for the estimate. * @returns The gas and gas price. */ async estimateGas( @@ -1249,7 +1250,7 @@ export class TransactionController extends BaseControllerV1< * * @param transaction - The transaction params to estimate gas for. * @param multiplier - The multiplier to use for the gas buffer. - * @param networkClientId + * @param networkClientId - The network client id to use for the estimate. */ async estimateGasBuffered( // NOTE(JL): Need to update SwapsController's usage of this method @@ -1611,7 +1612,7 @@ export class TransactionController extends BaseControllerV1< * Ensure `releaseLock` is called once processing of the `nonce` value is complete. * * @param address - The hex string address for the transaction. - * @param networkClientId + * @param networkClientId - The network client ID for the transaction, used to fetch the correct nonce tracker. * @returns object with the `nextNonce` `nonceDetails`, and the releaseLock. */ async getNonceLock( @@ -1620,17 +1621,17 @@ export class TransactionController extends BaseControllerV1< ): Promise { // TODO(JL): Revisit this method. It's a bit complicated and not obvious what it achieves. let nonceMutexForChainId: Mutex | undefined; - let nonceTracker = this.nonceTracker + let { nonceTracker } = this; if (networkClientId) { const networkClient = this.getNetworkClientById(networkClientId); nonceMutexForChainId = this.nonceMutexByChainId.get( networkClient.configuration.chainId, ); - const trackers = this.trackingMap.get(networkClientId) + const trackers = this.trackingMap.get(networkClientId); if (!trackers) { - throw new Error('missing nonceTracker for networkClientId') + throw new Error('missing nonceTracker for networkClientId'); } - nonceTracker = trackers?.nonceTracker + nonceTracker = trackers?.nonceTracker; } // Acquires the lock for the chainId and the nonceLock from the nonceTracker and then @@ -2054,7 +2055,7 @@ export class TransactionController extends BaseControllerV1< } private getCurrentChainTransactionsByStatus(status: TransactionStatus) { - const chainId = this.getChainId(); + const chainId = this.getChainId(); // TODO remove this filter return this.state.transactions.filter( (transaction) => transaction.status === status && transaction.chainId === chainId, @@ -2509,7 +2510,7 @@ export class TransactionController extends BaseControllerV1< * specified in txParams, @ethereumjs/tx is able to determine which EIP-2718 * transaction type to use. * - * @param chainId + * @param chainId - The chainId to use for the configuration. * @returns common configuration object */ private getCommonConfiguration(chainId: Hex): Common { diff --git a/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts b/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts index 7deba8c2ec..4805ee89ff 100644 --- a/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts +++ b/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts @@ -66,7 +66,7 @@ describe('PendingTransactionTracker', () => { const queryMock = jest.mocked(query); let blockTracker: jest.Mocked; let failTransaction: jest.Mock; - let pendingTransactionTracker: PendingTransactionTracker + let pendingTransactionTracker: PendingTransactionTracker; // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any let options: any; @@ -79,8 +79,7 @@ describe('PendingTransactionTracker', () => { { ...TRANSACTION_SUBMITTED_MOCK }, ]); - - pendingTransactionTracker.onStateChange() + pendingTransactionTracker.onStateChange(); if (transactionsOnCheck) { options.getTransactions.mockReturnValue(transactionsOnCheck); @@ -113,7 +112,7 @@ describe('PendingTransactionTracker', () => { options.getTransactions.mockReturnValue([TRANSACTION_SUBMITTED_MOCK]); - pendingTransactionTracker.onStateChange() + pendingTransactionTracker.onStateChange(); expect(blockTracker.on).toHaveBeenCalledTimes(1); expect(blockTracker.on).toHaveBeenCalledWith( @@ -127,8 +126,8 @@ describe('PendingTransactionTracker', () => { options.getTransactions.mockReturnValue([TRANSACTION_SUBMITTED_MOCK]); - pendingTransactionTracker.onStateChange() - pendingTransactionTracker.onStateChange() + pendingTransactionTracker.onStateChange(); + pendingTransactionTracker.onStateChange(); expect(blockTracker.on).toHaveBeenCalledTimes(1); expect(blockTracker.removeListener).toHaveBeenCalledTimes(0); @@ -139,13 +138,13 @@ describe('PendingTransactionTracker', () => { options.getTransactions.mockReturnValue([TRANSACTION_SUBMITTED_MOCK]); - pendingTransactionTracker.onStateChange() + pendingTransactionTracker.onStateChange(); expect(blockTracker.removeListener).toHaveBeenCalledTimes(0); options.getTransactions.mockReturnValue([]); - pendingTransactionTracker.onStateChange() + pendingTransactionTracker.onStateChange(); expect(blockTracker.removeListener).toHaveBeenCalledTimes(1); expect(blockTracker.removeListener).toHaveBeenCalledWith( @@ -159,13 +158,13 @@ describe('PendingTransactionTracker', () => { options.getTransactions.mockReturnValue([TRANSACTION_SUBMITTED_MOCK]); - pendingTransactionTracker.onStateChange() + pendingTransactionTracker.onStateChange(); expect(blockTracker.removeListener).toHaveBeenCalledTimes(0); options.getTransactions.mockReturnValue([]); - pendingTransactionTracker.onStateChange() + pendingTransactionTracker.onStateChange(); expect(blockTracker.removeListener).toHaveBeenCalledTimes(1); @@ -183,10 +182,22 @@ describe('PendingTransactionTracker', () => { pendingTransactionTracker = new PendingTransactionTracker(options); - pendingTransactionTracker.hub.addListener('transaction-dropped', listener); - pendingTransactionTracker.hub.addListener('transaction-failed', listener); - pendingTransactionTracker.hub.addListener('transaction-confirmed', listener); - pendingTransactionTracker.hub.addListener('transaction-updated', listener); + pendingTransactionTracker.hub.addListener( + 'transaction-dropped', + listener, + ); + pendingTransactionTracker.hub.addListener( + 'transaction-failed', + listener, + ); + pendingTransactionTracker.hub.addListener( + 'transaction-confirmed', + listener, + ); + pendingTransactionTracker.hub.addListener( + 'transaction-updated', + listener, + ); await onLatestBlock(undefined, [ { @@ -220,9 +231,18 @@ describe('PendingTransactionTracker', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - pendingTransactionTracker.hub.addListener('transaction-dropped', listener); - pendingTransactionTracker.hub.addListener('transaction-failed', listener); - pendingTransactionTracker.hub.addListener('transaction-confirmed', listener); + pendingTransactionTracker.hub.addListener( + 'transaction-dropped', + listener, + ); + pendingTransactionTracker.hub.addListener( + 'transaction-failed', + listener, + ); + pendingTransactionTracker.hub.addListener( + 'transaction-confirmed', + listener, + ); queryMock.mockResolvedValueOnce(undefined); queryMock.mockResolvedValueOnce('0x1'); @@ -242,9 +262,18 @@ describe('PendingTransactionTracker', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - pendingTransactionTracker.hub.addListener('transaction-dropped', listener); - pendingTransactionTracker.hub.addListener('transaction-failed', listener); - pendingTransactionTracker.hub.addListener('transaction-confirmed', listener); + pendingTransactionTracker.hub.addListener( + 'transaction-dropped', + listener, + ); + pendingTransactionTracker.hub.addListener( + 'transaction-failed', + listener, + ); + pendingTransactionTracker.hub.addListener( + 'transaction-confirmed', + listener, + ); queryMock.mockResolvedValueOnce({ ...RECEIPT_MOCK, status: null }); queryMock.mockResolvedValueOnce('0x1'); @@ -264,9 +293,18 @@ describe('PendingTransactionTracker', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - pendingTransactionTracker.hub.addListener('transaction-dropped', listener); - pendingTransactionTracker.hub.addListener('transaction-failed', listener); - pendingTransactionTracker.hub.addListener('transaction-confirmed', listener); + pendingTransactionTracker.hub.addListener( + 'transaction-dropped', + listener, + ); + pendingTransactionTracker.hub.addListener( + 'transaction-failed', + listener, + ); + pendingTransactionTracker.hub.addListener( + 'transaction-confirmed', + listener, + ); queryMock.mockResolvedValueOnce({ ...RECEIPT_MOCK, status: '0x3' }); queryMock.mockResolvedValueOnce('0x1'); @@ -293,7 +331,10 @@ describe('PendingTransactionTracker', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - pendingTransactionTracker.hub.addListener('transaction-failed', listener); + pendingTransactionTracker.hub.addListener( + 'transaction-failed', + listener, + ); await onLatestBlock(); @@ -325,7 +366,10 @@ describe('PendingTransactionTracker', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - pendingTransactionTracker.hub.addListener('transaction-failed', listener); + pendingTransactionTracker.hub.addListener( + 'transaction-failed', + listener, + ); await onLatestBlock(); @@ -342,7 +386,10 @@ describe('PendingTransactionTracker', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - pendingTransactionTracker.hub.addListener('transaction-failed', listener); + pendingTransactionTracker.hub.addListener( + 'transaction-failed', + listener, + ); queryMock.mockResolvedValueOnce({ ...RECEIPT_MOCK, status: '0x0' }); @@ -380,7 +427,10 @@ describe('PendingTransactionTracker', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - pendingTransactionTracker.hub.addListener('transaction-dropped', listener); + pendingTransactionTracker.hub.addListener( + 'transaction-dropped', + listener, + ); await onLatestBlock(); @@ -398,7 +448,10 @@ describe('PendingTransactionTracker', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - pendingTransactionTracker.hub.addListener('transaction-dropped', listener); + pendingTransactionTracker.hub.addListener( + 'transaction-dropped', + listener, + ); for (let i = 0; i < 4; i++) { expect(listener).toHaveBeenCalledTimes(0); @@ -437,7 +490,10 @@ describe('PendingTransactionTracker', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - pendingTransactionTracker.hub.addListener('transaction-dropped', listener); + pendingTransactionTracker.hub.addListener( + 'transaction-dropped', + listener, + ); await onLatestBlock(); @@ -456,7 +512,10 @@ describe('PendingTransactionTracker', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - pendingTransactionTracker.hub.addListener('transaction-confirmed', listener); + pendingTransactionTracker.hub.addListener( + 'transaction-confirmed', + listener, + ); queryMock.mockResolvedValueOnce(RECEIPT_MOCK); queryMock.mockResolvedValueOnce(BLOCK_MOCK); @@ -486,7 +545,10 @@ describe('PendingTransactionTracker', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - pendingTransactionTracker.hub.addListener('transaction-updated', listener); + pendingTransactionTracker.hub.addListener( + 'transaction-updated', + listener, + ); queryMock.mockResolvedValueOnce(RECEIPT_MOCK); queryMock.mockResolvedValueOnce(BLOCK_MOCK); @@ -518,7 +580,10 @@ describe('PendingTransactionTracker', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - pendingTransactionTracker.hub.addListener('transaction-updated', listener); + pendingTransactionTracker.hub.addListener( + 'transaction-updated', + listener, + ); queryMock.mockRejectedValueOnce(new Error('TestError')); queryMock.mockResolvedValueOnce(BLOCK_MOCK); @@ -544,7 +609,7 @@ describe('PendingTransactionTracker', () => { describe('resubmits', () => { describe('does nothing', () => { it('if no pending transactions', async () => { - pendingTransactionTracker =new PendingTransactionTracker(options); + pendingTransactionTracker = new PendingTransactionTracker(options); await onLatestBlock(undefined, []); @@ -564,7 +629,10 @@ describe('PendingTransactionTracker', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - pendingTransactionTracker.hub.addListener('transaction-updated', listener); + pendingTransactionTracker.hub.addListener( + 'transaction-updated', + listener, + ); queryMock.mockResolvedValueOnce(undefined); queryMock.mockResolvedValueOnce('0x1'); @@ -592,7 +660,10 @@ describe('PendingTransactionTracker', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - pendingTransactionTracker.hub.addListener('transaction-updated', listener); + pendingTransactionTracker.hub.addListener( + 'transaction-updated', + listener, + ); queryMock.mockResolvedValueOnce(undefined); queryMock.mockResolvedValueOnce('0x1'); @@ -628,7 +699,10 @@ describe('PendingTransactionTracker', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - pendingTransactionTracker.hub.addListener('transaction-updated', listener); + pendingTransactionTracker.hub.addListener( + 'transaction-updated', + listener, + ); queryMock.mockResolvedValueOnce(undefined); queryMock.mockResolvedValueOnce('0x1'); @@ -658,7 +732,10 @@ describe('PendingTransactionTracker', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - pendingTransactionTracker.hub.addListener('transaction-updated', listener); + pendingTransactionTracker.hub.addListener( + 'transaction-updated', + listener, + ); queryMock.mockResolvedValueOnce(undefined); queryMock.mockResolvedValueOnce(BLOCK_MOCK); @@ -696,7 +773,10 @@ describe('PendingTransactionTracker', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - pendingTransactionTracker.hub.addListener('transaction-updated', listener); + pendingTransactionTracker.hub.addListener( + 'transaction-updated', + listener, + ); queryMock.mockResolvedValueOnce(undefined); queryMock.mockResolvedValueOnce(BLOCK_MOCK); diff --git a/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts b/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts index 94d43d0056..f816e4769c 100644 --- a/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts +++ b/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts @@ -140,7 +140,7 @@ export class PendingTransactionTracker { } else { this.stop(); } - } + }; #start() { if (this.#running) { diff --git a/yarn.lock b/yarn.lock index 109238b78a..39ed8d7881 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2983,6 +2983,7 @@ __metadata: "@metamask/metamask-eth-abis": ^3.0.0 "@metamask/network-controller": ^17.1.0 "@metamask/rpc-errors": ^6.1.0 + "@metamask/selected-network-controller": ^6.0.0 "@metamask/utils": ^8.2.0 "@types/jest": ^27.4.1 "@types/node": ^16.18.54 @@ -3005,6 +3006,7 @@ __metadata: "@metamask/approval-controller": ^5.1.1 "@metamask/gas-fee-controller": ^12.0.0 "@metamask/network-controller": ^17.1.0 + "@metamask/selected-network-controller": ^6.0.0 babel-runtime: ^6.26.0 languageName: unknown linkType: soft From a95800c6ec55a95656e92a3f57337650cb1bf309 Mon Sep 17 00:00:00 2001 From: jiexi Date: Tue, 9 Jan 2024 11:21:29 -0800 Subject: [PATCH 015/100] Fix existing TransactionController Multichain specs (#3752) ## Explanation ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- .../src/TransactionController.test.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 030a9ad2e2..291e045420 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -552,6 +552,24 @@ describe('TransactionController', () => { messenger = rejectMessengerMock; } + // TODO(JL): This needs to use different provider from globally selected + const mockGetNetworkClientById = jest + .fn() + .mockImplementation((networkClientId) => { + switch (networkClientId) { + case 'mainnet': + return { + configuration: { + chainId: toHex(1), + }, + blockTracker: finalNetwork.blockTracker, + provider: finalNetwork.provider, + }; + default: + throw new Error('Invalid network client id'); + } + }); + return new TransactionController( { blockTracker: finalNetwork.blockTracker, @@ -565,6 +583,7 @@ describe('TransactionController', () => { messenger, onNetworkStateChange: finalNetwork.subscribe, provider: finalNetwork.provider, + getNetworkClientById: mockGetNetworkClientById, ...options, }, { @@ -622,6 +641,7 @@ describe('TransactionController', () => { NonceTrackerPackage.NonceTracker.prototype.getNonceLock = getNonceLockSpy; incomingTransactionHelperMock = { + stop: jest.fn(), hub: { on: jest.fn(), }, @@ -629,9 +649,12 @@ describe('TransactionController', () => { pendingTransactionTrackerMock = { start: jest.fn(), + stop: jest.fn(), hub: { on: jest.fn(), + removeAllListeners: jest.fn(), }, + onStateChange: jest.fn(), } as unknown as jest.Mocked; incomingTransactionHelperClassMock.mockReturnValue( @@ -707,6 +730,9 @@ describe('TransactionController', () => { expect(getExternalPendingTransactions).toHaveBeenCalledTimes(1); expect(getExternalPendingTransactions).toHaveBeenCalledWith( ACCOUNT_MOCK, + // TODO(JL): This shouldn't be undefined. NonceTracker needs + // to be updated to call this method with the chainId. + undefined, ); }); }); @@ -1097,6 +1123,7 @@ describe('TransactionController', () => { const expectedInitialSnapshot = { actionId: undefined, chainId: expect.any(String), + networkClientId: undefined, dappSuggestedGasFees: undefined, deviceConfirmedOn: undefined, id: expect.any(String), From 377c30966f3a6c20aad4ac1227c3d7df6943a09f Mon Sep 17 00:00:00 2001 From: jiexi Date: Tue, 9 Jan 2024 11:23:03 -0800 Subject: [PATCH 016/100] TransactionController: use txMeta.chainId as source of truth rather than globally selected (#3664) ## Explanation In certain cases, TransactionController will get the current chainId from network state to populate/filter transaction data when it seems that simply pulling it off TransactionMetaBase would make more sense. This PR replaces that behavior by using chainId from txMeta object. ## References ## Changelog ### `@metamask/transaction-controller` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --------- Co-authored-by: Alex Co-authored-by: Brian Bergeron --- .../src/TokenRatesController.test.ts | 57 +++--- .../assets-controllers/src/assetsUtil.test.ts | 85 ++++++--- packages/assets-controllers/src/assetsUtil.ts | 4 +- .../src/TransactionController.test.ts | 162 +++++++++++++++++- .../src/TransactionController.ts | 3 +- 5 files changed, 255 insertions(+), 56 deletions(-) diff --git a/packages/assets-controllers/src/TokenRatesController.test.ts b/packages/assets-controllers/src/TokenRatesController.test.ts index f6bcf89b0f..c76812bc52 100644 --- a/packages/assets-controllers/src/TokenRatesController.test.ts +++ b/packages/assets-controllers/src/TokenRatesController.test.ts @@ -11,6 +11,7 @@ import nock from 'nock'; import { useFakeTimers } from 'sinon'; import { advanceTime } from '../../../tests/helpers'; +import { TOKEN_PRICES_BATCH_SIZE } from './assetsUtil'; import type { AbstractTokenPricesService, TokenPrice, @@ -1711,7 +1712,7 @@ describe('TokenRatesController', () => { ); }); - it('fetches rates for all tokens in batches of 100', async () => { + it('fetches rates for all tokens in batches', async () => { const chainId = toHex(1); const ticker = 'ETH'; const tokenAddresses = [...new Array(200).keys()] @@ -1748,17 +1749,21 @@ describe('TokenRatesController', () => { nativeCurrency: ticker, }); - expect(fetchTokenPricesSpy).toHaveBeenCalledTimes(2); - expect(fetchTokenPricesSpy).toHaveBeenNthCalledWith(1, { - chainId, - tokenAddresses: tokenAddresses.slice(0, 100), - currency: ticker, - }); - expect(fetchTokenPricesSpy).toHaveBeenNthCalledWith(2, { - chainId, - tokenAddresses: tokenAddresses.slice(100), - currency: ticker, - }); + const numBatches = Math.ceil( + tokenAddresses.length / TOKEN_PRICES_BATCH_SIZE, + ); + expect(fetchTokenPricesSpy).toHaveBeenCalledTimes(numBatches); + + for (let i = 1; i <= numBatches; i++) { + expect(fetchTokenPricesSpy).toHaveBeenNthCalledWith(i, { + chainId, + tokenAddresses: tokenAddresses.slice( + (i - 1) * TOKEN_PRICES_BATCH_SIZE, + i * TOKEN_PRICES_BATCH_SIZE, + ), + currency: ticker, + }); + } }, ); }); @@ -1981,7 +1986,7 @@ describe('TokenRatesController', () => { ); }); - it('fetches rates for all tokens in batches of 100 when native currency is not supported by the Price API', async () => { + it('fetches rates for all tokens in batches when native currency is not supported by the Price API', async () => { const chainId = toHex(1); const ticker = 'UNSUPPORTED'; const tokenAddresses = [...new Array(200).keys()] @@ -2028,17 +2033,21 @@ describe('TokenRatesController', () => { nativeCurrency: ticker, }); - expect(fetchTokenPricesSpy).toHaveBeenCalledTimes(2); - expect(fetchTokenPricesSpy).toHaveBeenNthCalledWith(1, { - chainId, - tokenAddresses: tokenAddresses.slice(0, 100), - currency: 'ETH', - }); - expect(fetchTokenPricesSpy).toHaveBeenNthCalledWith(2, { - chainId, - tokenAddresses: tokenAddresses.slice(100), - currency: 'ETH', - }); + const numBatches = Math.ceil( + tokenAddresses.length / TOKEN_PRICES_BATCH_SIZE, + ); + expect(fetchTokenPricesSpy).toHaveBeenCalledTimes(numBatches); + + for (let i = 1; i <= numBatches; i++) { + expect(fetchTokenPricesSpy).toHaveBeenNthCalledWith(i, { + chainId, + tokenAddresses: tokenAddresses.slice( + (i - 1) * TOKEN_PRICES_BATCH_SIZE, + i * TOKEN_PRICES_BATCH_SIZE, + ), + currency: 'ETH', + }); + } }, ); }); diff --git a/packages/assets-controllers/src/assetsUtil.test.ts b/packages/assets-controllers/src/assetsUtil.test.ts index 7ce788f870..4980d68593 100644 --- a/packages/assets-controllers/src/assetsUtil.test.ts +++ b/packages/assets-controllers/src/assetsUtil.test.ts @@ -8,6 +8,7 @@ import { import { add0x, type Hex } from '@metamask/utils'; import * as assetsUtil from './assetsUtil'; +import { TOKEN_PRICES_BATCH_SIZE } from './assetsUtil'; import type { Nft, NftMetadata } from './NftController'; import type { AbstractTokenPricesService } from './token-prices-service'; @@ -476,18 +477,11 @@ describe('assetsUtil', () => { expect(result).toStrictEqual({}); }); - it('should return successfully with a number of tokens less than 100', async () => { + it('should return successfully with a number of tokens less than the batch size', async () => { const testTokenAddress = '0x7BEF710a5759d197EC0Bf621c3Df802C2D60D848'; const testNativeCurrency = 'ETH'; const testChainId = '0x1'; const mockPriceService = createMockPriceService(); - jest - .spyOn(mockPriceService, 'validateCurrencySupported') - .mockReturnValue(true); - - jest - .spyOn(mockPriceService, 'validateChainIdSupported') - .mockReturnValue(true); jest.spyOn(mockPriceService, 'fetchTokenPrices').mockResolvedValue({ [testTokenAddress]: { @@ -509,7 +503,7 @@ describe('assetsUtil', () => { }); }); - it('should fetch successfully in batches of 100', async () => { + it('should fetch successfully in batches', async () => { const mockPriceService = createMockPriceService(); const tokenAddresses = [...new Array(200).keys()] .map(buildAddress) @@ -517,13 +511,6 @@ describe('assetsUtil', () => { const testNativeCurrency = 'ETH'; const testChainId = '0x1'; - jest - .spyOn(mockPriceService, 'validateCurrencySupported') - .mockReturnValue(true); - - jest - .spyOn(mockPriceService, 'validateChainIdSupported') - .mockReturnValue(true); const fetchTokenPricesSpy = jest.spyOn( mockPriceService, @@ -537,17 +524,65 @@ describe('assetsUtil', () => { chainId: testChainId, }); - expect(fetchTokenPricesSpy).toHaveBeenCalledTimes(2); - expect(fetchTokenPricesSpy).toHaveBeenNthCalledWith(1, { - chainId: testChainId, - tokenAddresses: tokenAddresses.slice(0, 100), - currency: testNativeCurrency, - }); - expect(fetchTokenPricesSpy).toHaveBeenNthCalledWith(2, { + const numBatches = Math.ceil( + tokenAddresses.length / TOKEN_PRICES_BATCH_SIZE, + ); + expect(fetchTokenPricesSpy).toHaveBeenCalledTimes(numBatches); + + for (let i = 1; i <= numBatches; i++) { + expect(fetchTokenPricesSpy).toHaveBeenNthCalledWith(i, { + chainId: testChainId, + tokenAddresses: tokenAddresses.slice( + (i - 1) * TOKEN_PRICES_BATCH_SIZE, + i * TOKEN_PRICES_BATCH_SIZE, + ), + currency: testNativeCurrency, + }); + } + }); + + it('should sort token addresses when batching', async () => { + const mockPriceService = createMockPriceService(); + + // Mock addresses in descending order + const tokenAddresses = [...new Array(200).keys()] + .map(buildAddress) + .sort() + .reverse(); + + const testNativeCurrency = 'ETH'; + const testChainId = '0x1'; + + const fetchTokenPricesSpy = jest.spyOn( + mockPriceService, + 'fetchTokenPrices', + ); + + await assetsUtil.fetchTokenContractExchangeRates({ + tokenPricesService: mockPriceService, + nativeCurrency: testNativeCurrency, + tokenAddresses: tokenAddresses as Hex[], chainId: testChainId, - tokenAddresses: tokenAddresses.slice(100), - currency: testNativeCurrency, }); + + // Expect batches in ascending order + tokenAddresses.sort(); + + const numBatches = Math.ceil( + tokenAddresses.length / TOKEN_PRICES_BATCH_SIZE, + ); + expect(fetchTokenPricesSpy).toHaveBeenCalledTimes(numBatches); + + for (let i = 1; i <= numBatches; i++) { + expect(fetchTokenPricesSpy).toHaveBeenNthCalledWith(i, { + chainId: testChainId, + tokenAddresses: tokenAddresses.slice( + (i - 1) * TOKEN_PRICES_BATCH_SIZE, + i * TOKEN_PRICES_BATCH_SIZE, + ), + currency: testNativeCurrency, + }); + } }); }); }); diff --git a/packages/assets-controllers/src/assetsUtil.ts b/packages/assets-controllers/src/assetsUtil.ts index 4416e03260..e1c8af400e 100644 --- a/packages/assets-controllers/src/assetsUtil.ts +++ b/packages/assets-controllers/src/assetsUtil.ts @@ -24,7 +24,7 @@ import { type ContractExchangeRates } from './TokenRatesController'; * The maximum number of token addresses that should be sent to the Price API in * a single request. */ -export const TOKEN_PRICES_BATCH_SIZE = 100; +export const TOKEN_PRICES_BATCH_SIZE = 30; /** * Compares nft metadata entries to any nft entry. @@ -432,7 +432,7 @@ export async function fetchTokenContractExchangeRates({ Hex, Awaited> >({ - values: tokenAddresses, + values: [...tokenAddresses].sort(), batchSize: TOKEN_PRICES_BATCH_SIZE, eachBatch: async (allTokenPricesByTokenAddress, batch) => { const tokenPricesByTokenAddressForBatch = diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 291e045420..4c1265e6a6 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -779,6 +779,46 @@ describe('TransactionController', () => { expect(transactions[0].status).toBe(TransactionStatus.submitted); }); + + it('only reads the current chain id once to filter for approved transactions', async () => { + const mockTransactionMeta = { + from: ACCOUNT_MOCK, + chainId: toHex(5), + status: TransactionStatus.approved, + txParams: { + from: ACCOUNT_MOCK, + to: ACCOUNT_2_MOCK, + }, + }; + const mockedTransactions = [ + { + id: '123', + ...mockTransactionMeta, + history: [{ ...mockTransactionMeta, id: '123' }], + }, + ]; + + const mockedControllerState = { + transactions: mockedTransactions, + methodData: {}, + lastFetchedBlockNumbers: {}, + }; + + const getNetworkStateMock = jest + .fn() + .mockReturnValue(MOCK_NETWORK.state); + + newController({ + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + state: mockedControllerState as any, + options: { getNetworkState: getNetworkStateMock }, + }); + + await flushPromises(); + + expect(getNetworkStateMock).toHaveBeenCalledTimes(1); + }); }); }); @@ -1144,6 +1184,20 @@ describe('TransactionController', () => { ]); }); + it('only reads the current chain id to filter to initially populate the metadata and for onBootCleanup', async () => { + const getNetworkStateMock = jest.fn().mockReturnValue(MOCK_NETWORK.state); + const controller = newController({ + options: { getNetworkState: getNetworkStateMock }, + }); + + await controller.addTransaction({ + from: ACCOUNT_MOCK, + to: ACCOUNT_MOCK, + }); + + expect(getNetworkStateMock).toHaveBeenCalledTimes(4); // not sure why this is 4. we shouldn't test like this + }); + describe('adds dappSuggestedGasFees to transaction', () => { it.each([ ['origin is MM', ORIGIN_METAMASK], @@ -2516,7 +2570,7 @@ describe('TransactionController', () => { const localTransactionIdWithSameNonce = '9'; controller.state.transactions.push({ id: localTransactionIdWithSameNonce, - chainId: toHex(5), + chainId: toHex(1), status: TransactionStatus.unapproved as const, time: 123456789, txParams: { @@ -2586,7 +2640,7 @@ describe('TransactionController', () => { const localTransactionIdWithSameNonce = '9'; controller.state.transactions.push({ id: localTransactionIdWithSameNonce, - chainId: toHex(5), + chainId: toHex(1), status: TransactionStatus.failed as const, error: new Error('mock error'), time: 123456789, @@ -2697,7 +2751,7 @@ describe('TransactionController', () => { from: ACCOUNT_MOCK, to: ACCOUNT_2_MOCK, id: '1', - chainId: toHex(1), + chainId: toHex(5), status: TransactionStatus.confirmed, txParams: { gasUsed: undefined, @@ -2725,6 +2779,55 @@ describe('TransactionController', () => { transactionMeta: externalTransaction, }); }); + + it('emits confirmed event with transaction chainId regardless of whether it matches globally selected chainId', async () => { + const mockGloballySelectedNetwork = { + ...MOCK_NETWORK, + state: { + ...MOCK_NETWORK.state, + providerConfig: { + type: NetworkType.sepolia, + chainId: ChainId.sepolia, + ticker: NetworksTicker.sepolia, + }, + }, + }; + const controller = newController({ + network: mockGloballySelectedNetwork, + }); + + const confirmedEventListener = jest.fn(); + + controller.hub.on('transaction-confirmed', confirmedEventListener); + + const externalTransactionToConfirm = { + from: ACCOUNT_MOCK, + to: ACCOUNT_2_MOCK, + id: '1', + chainId: ChainId.goerli, // doesn't match globally selected chainId (which is sepolia) + status: TransactionStatus.confirmed, + txParams: { + gasUsed: undefined, + from: ACCOUNT_MOCK, + to: ACCOUNT_2_MOCK, + }, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; + const externalTransactionReceipt = { + gasUsed: '0x5208', + }; + const externalBaseFeePerGas = '0x14'; + + await controller.confirmExternalTransaction( + externalTransactionToConfirm, + externalTransactionReceipt, + externalBaseFeePerGas, + ); + + const [[{ transactionMeta }]] = confirmedEventListener.mock.calls; + expect(transactionMeta.chainId).toBe(ChainId.goerli); + }); }); describe('updateTransactionSendFlowHistory', () => { @@ -4026,6 +4129,59 @@ describe('TransactionController', () => { ); }); + it('only reads the current chain id to filter for unapproved transactions and for onBootCleanup', async () => { + const mockTransactionMeta = { + from: ACCOUNT_MOCK, + chainId: toHex(5), + status: TransactionStatus.unapproved, + txParams: { + from: ACCOUNT_MOCK, + to: ACCOUNT_2_MOCK, + }, + }; + + const mockedTransactions = [ + { + id: '123', + ...mockTransactionMeta, + history: [{ ...mockTransactionMeta, id: '123' }], + }, + { + id: '1234', + ...mockTransactionMeta, + history: [{ ...mockTransactionMeta, id: '1234' }], + }, + { + id: '12345', + ...mockTransactionMeta, + history: [{ ...mockTransactionMeta, id: '12345' }], + isUserOperation: true, + }, + ]; + + const mockedControllerState = { + transactions: mockedTransactions, + methodData: {}, + lastFetchedBlockNumbers: {}, + }; + + const getNetworkStateMock = jest + .fn() + .mockReturnValue(MOCK_NETWORK.state); + + const controller = newController({ + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + state: mockedControllerState as any, + options: { getNetworkState: getNetworkStateMock }, + }); + + controller.initApprovals(); + await flushPromises(); + + expect(getNetworkStateMock).toHaveBeenCalledTimes(2); + }); + it('catches error without code property in error object while creating approval', async () => { const mockTransactionMeta = { from: ACCOUNT_MOCK, diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 44862e9346..3bbffffceb 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -2209,7 +2209,6 @@ export class TransactionController extends BaseControllerV1< private async approveTransaction(transactionId: string) { const { transactions } = this.state; const releaseLock = await this.mutex.acquire(); - const chainId = this.getChainId(); const index = transactions.findIndex(({ id }) => transactionId === id); const transactionMeta = transactions[index]; @@ -2227,7 +2226,7 @@ export class TransactionController extends BaseControllerV1< new Error('No sign method defined.'), ); return; - } else if (!chainId) { + } else if (!transactionMeta.chainId) { releaseLock(); this.failTransaction(transactionMeta, new Error('No chainId defined.')); return; From ae3859934e3d815468b329f9de0c0f49417128f3 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Tue, 9 Jan 2024 21:26:52 -0600 Subject: [PATCH 017/100] Bind `getNonceTrackerPendingTransactions` with chainId when passing to NonceTracker constructor (#3756) --- .../src/TransactionController.test.ts | 5 +++-- .../src/TransactionController.ts | 14 +++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 4c1265e6a6..421ce5c01d 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -717,6 +717,7 @@ describe('TransactionController', () => { }, ]; + // this feels like a weird thing to test here const pendingTransactions = nonceTrackerMock.mock.calls[0][0].getPendingTransactions( ACCOUNT_MOCK, @@ -730,8 +731,8 @@ describe('TransactionController', () => { expect(getExternalPendingTransactions).toHaveBeenCalledTimes(1); expect(getExternalPendingTransactions).toHaveBeenCalledWith( ACCOUNT_MOCK, - // TODO(JL): This shouldn't be undefined. NonceTracker needs - // to be updated to call this method with the chainId. + // This is undefined for the base nonceTracker + // TODO (AD) add tests for using external pending transactions with a networkClientId once we have trackingMaps instantiated undefined, ); }); diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 3bbffffceb..313f951f47 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -533,8 +533,10 @@ export class TransactionController extends BaseControllerV1< // @ts-expect-error provider types misaligned: SafeEventEmitterProvider vs Record provider, blockTracker, - getPendingTransactions: - this.getNonceTrackerPendingTransactions.bind(this), + getPendingTransactions: this.getNonceTrackerPendingTransactions.bind( + this, + undefined, + ), getConfirmedTransactions: this.getNonceTrackerTransactions.bind( this, TransactionStatus.confirmed, @@ -1169,8 +1171,10 @@ export class TransactionController extends BaseControllerV1< // eslint-disable-next-line @typescript-eslint/no-explicit-any provider: networkClient.provider as any, blockTracker: networkClient.blockTracker, - getPendingTransactions: - this.getNonceTrackerPendingTransactions.bind(this), + getPendingTransactions: this.getNonceTrackerPendingTransactions.bind( + this, + chainId, + ), getConfirmedTransactions: this.getNonceTrackerTransactions.bind( this, TransactionStatus.confirmed, @@ -2846,8 +2850,8 @@ export class TransactionController extends BaseControllerV1< } private getNonceTrackerPendingTransactions( + chainId: string | undefined, address: string, - chainId?: string, ) { const standardPendingTransactions = this.getNonceTrackerTransactions( TransactionStatus.submitted, From 41b8d1bb82142d796a88784332d5e963786b1e66 Mon Sep 17 00:00:00 2001 From: jiexi Date: Wed, 10 Jan 2024 09:04:51 -0800 Subject: [PATCH 018/100] Transaction multichain onBootCleanup all chains (#3759) ## Explanation * Change `onBootCleanup` to submit approved tx for all chains, not just currently selected * Add `chainId` fallback to `getEthQuery` ## References * Fixes https://github.com/MetaMask/MetaMask-planning/issues/1893 ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --------- Co-authored-by: Alex Donesky --- .../src/TransactionController.test.ts | 92 ++++++++-------- .../src/TransactionController.ts | 102 +++++++++++++----- 2 files changed, 115 insertions(+), 79 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 421ce5c01d..9d593f27d5 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -552,11 +552,11 @@ describe('TransactionController', () => { messenger = rejectMessengerMock; } - // TODO(JL): This needs to use different provider from globally selected const mockGetNetworkClientById = jest .fn() .mockImplementation((networkClientId) => { switch (networkClientId) { + // TODO(JL): This needs to use different provider from globally selected case 'mainnet': return { configuration: { @@ -565,11 +565,28 @@ describe('TransactionController', () => { blockTracker: finalNetwork.blockTracker, provider: finalNetwork.provider, }; + case 'global': + return { + configuration: { + chainId: finalNetwork.state.providerConfig.chainId, + }, + blockTracker: finalNetwork.blockTracker, + provider: finalNetwork.provider, + }; default: throw new Error('Invalid network client id'); } }); + const mockFindNetworkClientIdByChainId = jest + .fn() + .mockImplementation((chainId) => { + if (chainId !== finalNetwork.state.providerConfig.chainId) { + throw new Error("Couldn't find networkClientId for chainId"); + } + return 'global'; + }); + return new TransactionController( { blockTracker: finalNetwork.blockTracker, @@ -584,6 +601,7 @@ describe('TransactionController', () => { onNetworkStateChange: finalNetwork.subscribe, provider: finalNetwork.provider, getNetworkClientById: mockGetNetworkClientById, + findNetworkClientIdByChainId: mockFindNetworkClientIdByChainId, ...options, }, { @@ -744,10 +762,9 @@ describe('TransactionController', () => { updateGasFeesMock.mockReset(); }); - it('submits an approved transaction', async () => { + it('submits approved transactions for all chains', async () => { const mockTransactionMeta = { from: ACCOUNT_MOCK, - chainId: toHex(5), status: TransactionStatus.approved, txParams: { from: ACCOUNT_MOCK, @@ -757,45 +774,21 @@ describe('TransactionController', () => { const mockedTransactions = [ { id: '123', - ...mockTransactionMeta, history: [{ ...mockTransactionMeta, id: '123' }], + chainId: toHex(5), + ...mockTransactionMeta, }, - ]; - - const mockedControllerState = { - transactions: mockedTransactions, - methodData: {}, - lastFetchedBlockNumbers: {}, - }; - - const controller = newController({ - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - state: mockedControllerState as any, - }); - - await flushPromises(); - - const { transactions } = controller.state; - - expect(transactions[0].status).toBe(TransactionStatus.submitted); - }); - - it('only reads the current chain id once to filter for approved transactions', async () => { - const mockTransactionMeta = { - from: ACCOUNT_MOCK, - chainId: toHex(5), - status: TransactionStatus.approved, - txParams: { - from: ACCOUNT_MOCK, - to: ACCOUNT_2_MOCK, + { + id: '456', + history: [{ ...mockTransactionMeta, id: '456' }], + chainId: toHex(1), + ...mockTransactionMeta, }, - }; - const mockedTransactions = [ { - id: '123', + id: '789', + history: [{ ...mockTransactionMeta, id: '789' }], + chainId: toHex(16), ...mockTransactionMeta, - history: [{ ...mockTransactionMeta, id: '123' }], }, ]; @@ -805,20 +798,19 @@ describe('TransactionController', () => { lastFetchedBlockNumbers: {}, }; - const getNetworkStateMock = jest - .fn() - .mockReturnValue(MOCK_NETWORK.state); - - newController({ + const controller = newController({ // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any state: mockedControllerState as any, - options: { getNetworkState: getNetworkStateMock }, }); await flushPromises(); - expect(getNetworkStateMock).toHaveBeenCalledTimes(1); + const { transactions } = controller.state; + + expect(transactions[0].status).toBe(TransactionStatus.submitted); + expect(transactions[1].status).toBe(TransactionStatus.submitted); + expect(transactions[2].status).toBe(TransactionStatus.submitted); }); }); }); @@ -1185,7 +1177,7 @@ describe('TransactionController', () => { ]); }); - it('only reads the current chain id to filter to initially populate the metadata and for onBootCleanup', async () => { + it('only reads the current chain id to filter to initially populate the metadata', async () => { const getNetworkStateMock = jest.fn().mockReturnValue(MOCK_NETWORK.state); const controller = newController({ options: { getNetworkState: getNetworkStateMock }, @@ -1196,7 +1188,7 @@ describe('TransactionController', () => { to: ACCOUNT_MOCK, }); - expect(getNetworkStateMock).toHaveBeenCalledTimes(4); // not sure why this is 4. we shouldn't test like this + expect(getNetworkStateMock).toHaveBeenCalledTimes(2); // we shouldn't test like this }); describe('adds dappSuggestedGasFees to transaction', () => { @@ -2571,7 +2563,7 @@ describe('TransactionController', () => { const localTransactionIdWithSameNonce = '9'; controller.state.transactions.push({ id: localTransactionIdWithSameNonce, - chainId: toHex(1), + chainId: toHex(5), status: TransactionStatus.unapproved as const, time: 123456789, txParams: { @@ -2641,7 +2633,7 @@ describe('TransactionController', () => { const localTransactionIdWithSameNonce = '9'; controller.state.transactions.push({ id: localTransactionIdWithSameNonce, - chainId: toHex(1), + chainId: toHex(5), status: TransactionStatus.failed as const, error: new Error('mock error'), time: 123456789, @@ -4130,7 +4122,7 @@ describe('TransactionController', () => { ); }); - it('only reads the current chain id to filter for unapproved transactions and for onBootCleanup', async () => { + it('only reads the current chain id to filter for unapproved transactions', async () => { const mockTransactionMeta = { from: ACCOUNT_MOCK, chainId: toHex(5), @@ -4180,7 +4172,7 @@ describe('TransactionController', () => { controller.initApprovals(); await flushPromises(); - expect(getNetworkStateMock).toHaveBeenCalledTimes(2); + expect(getNetworkStateMock).toHaveBeenCalledTimes(1); }); it('catches error without code property in error object while creating approval', async () => { diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 313f951f47..58a0fb2829 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -27,8 +27,10 @@ import type { NetworkController, NetworkState, Provider, + NetworkClientConfiguration, } from '@metamask/network-controller'; import { NetworkClientType } from '@metamask/network-controller'; +import type { AutoManagedNetworkClient } from '@metamask/network-controller/src/create-auto-managed-network-client'; import { errorCodes, rpcErrors, providerErrors } from '@metamask/rpc-errors'; import type { SelectedNetworkController } from '@metamask/selected-network-controller'; import type { Hex } from '@metamask/utils'; @@ -300,6 +302,8 @@ export class TransactionController extends BaseControllerV1< transactionMeta: TransactionMeta, ) => (TransactionMeta | undefined)[]; + private readonly findNetworkClientIdByChainId: NetworkController['findNetworkClientIdByChainId']; + private readonly getNetworkClientById: NetworkController['getNetworkClientById']; private readonly getNetworkClientIdForDomain: SelectedNetworkController['getNetworkClientIdForDomain']; @@ -392,6 +396,7 @@ export class TransactionController extends BaseControllerV1< * @param options.provider - The provider used to create the underlying EthQuery instance. * @param options.securityProviderRequest - A function for verifying a transaction, whether it is malicious or not. * @param options.speedUpMultiplier - Multiplier used to determine a transaction's increased gas fee during speed up. + * @param options.findNetworkClientIdByChainId - Finds a networkClientId with the given chainId from the NetworkController. * @param options.getNetworkClientById - Gets the network client with the given id from the NetworkController. * @param options.hooks - The controller hooks. * @param options.hooks.afterSign - Additional logic to execute after signing a transaction. Return false to not change the status to signed. @@ -425,6 +430,7 @@ export class TransactionController extends BaseControllerV1< provider, securityProviderRequest, speedUpMultiplier, + findNetworkClientIdByChainId, getNetworkClientById, getNetworkClientIdForDomain, hooks = {}, @@ -459,6 +465,7 @@ export class TransactionController extends BaseControllerV1< provider: Provider; securityProviderRequest?: SecurityProviderRequest; speedUpMultiplier?: number; + findNetworkClientIdByChainId: NetworkController['findNetworkClientIdByChainId']; getNetworkClientById: NetworkController['getNetworkClientById']; getNetworkClientIdForDomain: SelectedNetworkController['getNetworkClientIdForDomain']; hooks: { @@ -492,6 +499,7 @@ export class TransactionController extends BaseControllerV1< }; this.initialize(); + this.findNetworkClientIdByChainId = findNetworkClientIdByChainId; this.getNetworkClientById = getNetworkClientById; this.provider = provider; this.messagingSystem = messenger; @@ -635,10 +643,41 @@ export class TransactionController extends BaseControllerV1< } } - getEthQuery(networkClientId?: NetworkClientId): EthQuery { + getEthQuery({ + networkClientId, + chainId, + }: { + networkClientId?: NetworkClientId; + chainId?: Hex; + }): EthQuery { + let networkClient: + | AutoManagedNetworkClient + | undefined; + if (networkClientId) { - return new EthQuery(this.getNetworkClientById(networkClientId).provider); + try { + networkClient = this.getNetworkClientById(networkClientId); + } catch (err) { + log('failed to get network client by networkClientId'); + } + } + + if (!networkClient && chainId) { + try { + networkClientId = this.findNetworkClientIdByChainId(chainId); + networkClient = this.getNetworkClientById(networkClientId); + } catch (err) { + log('failed to get network client by chainId'); + } + } + + if (networkClient) { + return new EthQuery(networkClient.provider); } + + // NOTE(JL): we're not ready to drop globally selected ethQuery yet. + // Some calls to getEthQuery only have access to optional networkClientId + // throw new Error('failed to get eth query instance'); return this.ethQuery; } @@ -721,13 +760,13 @@ export class TransactionController extends BaseControllerV1< origin, ); - const ethQuery = this.getEthQuery(networkClientId); + const chainId = this.getChainId(networkClientId); + const ethQuery = this.getEthQuery({ networkClientId, chainId }); const transactionType = type ?? (await determineTransactionType(txParams, ethQuery)).type; const existingTransactionMeta = this.getTransactionWithActionId(actionId); - const chainId = this.getChainId(networkClientId); // If a request to add a transaction with the same actionId is submitted again, a new transaction will not be created for it. const transactionMeta: TransactionMeta = existingTransactionMeta || { @@ -931,7 +970,10 @@ export class TransactionController extends BaseControllerV1< txParams: newTxParams, }); - const ethQuery = this.getEthQuery(transactionMeta.networkClientId); + const ethQuery = this.getEthQuery({ + networkClientId: transactionMeta.networkClientId, + chainId: transactionMeta.chainId, + }); const hash = await this.publishTransaction(ethQuery, rawTx); const cancelTransactionMeta: TransactionMeta = { @@ -1088,11 +1130,10 @@ export class TransactionController extends BaseControllerV1< log('Submitting speed up transaction', { oldFee, newFee, txParams }); - // TODO(JL): Usually we only want submit transactions on the specific network - // that the user approved them on, but it makes sense to allow cancelling - // from any network that's also on the same chain. We will need to add a fallback - // here to allow using networkClientIds other than the original - const ethQuery = this.getEthQuery(transactionMeta.networkClientId); + const ethQuery = this.getEthQuery({ + networkClientId: transactionMeta.networkClientId, + chainId: transactionMeta.chainId, + }); const hash = await this.publishTransaction(ethQuery, rawTx); const baseTransactionMeta: TransactionMeta = { @@ -1165,6 +1206,7 @@ export class TransactionController extends BaseControllerV1< if (!this.nonceMutexByChainId.get(chainId)) { this.nonceMutexByChainId.set(chainId, new Mutex()); } + const ethQuery = new EthQuery(networkClient.provider); const nonceTracker = new NonceTracker({ // TODO: Replace `any` with type @@ -1197,7 +1239,7 @@ export class TransactionController extends BaseControllerV1< // eslint-disable-next-line @typescript-eslint/no-explicit-any blockTracker: networkClient.provider as any, getChainId: () => networkClient.configuration.chainId, - getEthQuery: () => this.ethQuery, // TODO: use networkClient to construct ethQuery + getEthQuery: () => ethQuery, getTransactions: () => this.state.transactions, isResubmitEnabled: true, // TODO: make this configurable nonceTracker, @@ -1240,7 +1282,7 @@ export class TransactionController extends BaseControllerV1< transaction: TransactionParams, networkClientId?: NetworkClientId, ) { - const ethQuery = this.getEthQuery(networkClientId); + const ethQuery = this.getEthQuery({ networkClientId }); const { estimatedGas, simulationFails } = await estimateGas( transaction, ethQuery, @@ -1262,7 +1304,7 @@ export class TransactionController extends BaseControllerV1< multiplier: number, networkClientId?: NetworkClientId, ) { - const ethQuery = this.getEthQuery(networkClientId); + const ethQuery = this.getEthQuery({ networkClientId }); const { blockGasLimit, estimatedGas, simulationFails } = await estimateGas( transaction, ethQuery, @@ -1716,7 +1758,10 @@ export class TransactionController extends BaseControllerV1< const updatedTransaction = merge(transactionMeta, editableParams); const { type } = await determineTransactionType( updatedTransaction.txParams, - this.ethQuery, + this.getEthQuery({ + networkClientId: transactionMeta.networkClientId, + chainId: transactionMeta.chainId, + }), ); updatedTransaction.type = type; @@ -2043,7 +2088,7 @@ export class TransactionController extends BaseControllerV1< } await updateGas({ - ethQuery: this.getEthQuery(networkClientId), + ethQuery: this.getEthQuery({ networkClientId, chainId }), chainId, isCustomNetwork, txMeta: transactionMeta, @@ -2051,32 +2096,25 @@ export class TransactionController extends BaseControllerV1< await updateGasFees({ eip1559: isEIP1559Compatible, - ethQuery: this.ethQuery, + ethQuery: this.getEthQuery({ networkClientId, chainId }), getSavedGasFees: this.getSavedGasFees.bind(this), getGasFeeEstimates: this.getGasFeeEstimates.bind(this), txMeta: transactionMeta, }); } - private getCurrentChainTransactionsByStatus(status: TransactionStatus) { - const chainId = this.getChainId(); // TODO remove this filter - return this.state.transactions.filter( - (transaction) => - transaction.status === status && transaction.chainId === chainId, - ); - } - private onBootCleanup() { this.submitApprovedTransactions(); } /** - * Force to submit approved transactions on current chain. + * Force submit approved transactions for all chains. */ private submitApprovedTransactions() { - const approvedTransactions = this.getCurrentChainTransactionsByStatus( - TransactionStatus.approved, + const approvedTransactions = this.state.transactions.filter( + (transaction) => transaction.status === TransactionStatus.approved, ); + for (const transactionMeta of approvedTransactions) { if (this.beforeApproveOnInit(transactionMeta)) { this.approveTransaction(transactionMeta.id).catch((error) => { @@ -2286,7 +2324,10 @@ export class TransactionController extends BaseControllerV1< return; } - const ethQuery = this.getEthQuery(transactionMeta.networkClientId); + const ethQuery = this.getEthQuery({ + networkClientId: transactionMeta.networkClientId, + chainId: transactionMeta.chainId, + }); if (transactionMeta.type === TransactionType.swap) { log('Determining pre-transaction balance'); @@ -2902,7 +2943,10 @@ export class TransactionController extends BaseControllerV1< return; } - const ethQuery = this.getEthQuery(transactionMeta.networkClientId); + const ethQuery = this.getEthQuery({ + networkClientId: transactionMeta.networkClientId, + chainId: transactionMeta.chainId, + }); const { updatedTransactionMeta, approvalTransactionMeta } = await updatePostTransactionBalance(transactionMeta, { ethQuery, From d7939a82760991b6bb82fa4b619a181561b36b8a Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Wed, 10 Jan 2024 11:16:49 -0600 Subject: [PATCH 019/100] fix broken tests (#3761) Fixes 2 broken tests --- .../src/TransactionController.test.ts | 8 ++++---- packages/transaction-controller/tsconfig.build.json | 3 ++- packages/transaction-controller/tsconfig.json | 3 ++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 9d593f27d5..83cdb4e758 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -2524,7 +2524,7 @@ describe('TransactionController', () => { ]); }); - it('marks the same nonce local transactions statuses as dropped and defines replacedBy properties', async () => { + it('marks local transactions with the same nonce and chainId as status dropped and defines replacedBy properties', async () => { const droppedEventListener = jest.fn(); const changedStatusEventListener = jest.fn(); const controller = newController({ @@ -2559,7 +2559,7 @@ describe('TransactionController', () => { }; const externalBaseFeePerGas = '0x14'; - // Local unapproved transaction + // Local unapproved transaction with the same chainId and nonce const localTransactionIdWithSameNonce = '9'; controller.state.transactions.push({ id: localTransactionIdWithSameNonce, @@ -2606,7 +2606,7 @@ describe('TransactionController', () => { }); }); - it('doesnt mark transaction as dropped if same nonce local transaction status is failed', async () => { + it('doesnt mark transaction as dropped if local transaction with same nonce and chainId has status of failed', async () => { const controller = newController(); const externalTransactionId = '1'; const externalTransactionHash = '0x1'; @@ -2629,7 +2629,7 @@ describe('TransactionController', () => { }; const externalBaseFeePerGas = '0x14'; - // Off-chain failed local transaction + // Off-chain failed local transaction with the same chainId and nonce const localTransactionIdWithSameNonce = '9'; controller.state.transactions.push({ id: localTransactionIdWithSameNonce, diff --git a/packages/transaction-controller/tsconfig.build.json b/packages/transaction-controller/tsconfig.build.json index 504559792e..6c01c7a8cb 100644 --- a/packages/transaction-controller/tsconfig.build.json +++ b/packages/transaction-controller/tsconfig.build.json @@ -10,7 +10,8 @@ { "path": "../base-controller/tsconfig.build.json" }, { "path": "../controller-utils/tsconfig.build.json" }, { "path": "../gas-fee-controller/tsconfig.build.json" }, - { "path": "../network-controller/tsconfig.build.json" } + { "path": "../network-controller/tsconfig.build.json" }, + { "path": "../selected-network-controller/tsconfig.build.json" } ], "include": ["../../types", "./src"] } diff --git a/packages/transaction-controller/tsconfig.json b/packages/transaction-controller/tsconfig.json index 01c431b29e..fcc5337587 100644 --- a/packages/transaction-controller/tsconfig.json +++ b/packages/transaction-controller/tsconfig.json @@ -8,7 +8,8 @@ { "path": "../base-controller" }, { "path": "../controller-utils" }, { "path": "../gas-fee-controller" }, - { "path": "../network-controller" } + { "path": "../network-controller" }, + { "path": "../selected-network-controller" } ], "include": ["../../types", "./src"] } From 5b26dedc43aa31719575cc1475c53b0a633cda0e Mon Sep 17 00:00:00 2001 From: jiexi Date: Wed, 10 Jan 2024 11:34:38 -0800 Subject: [PATCH 020/100] Make getEthQuery private (#3762) ## Explanation * Makes `getEthQuery` #private * Leaves existing ts private keywords in place ## References Related https://github.com/MetaMask/MetaMask-planning/issues/1892 ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- .../src/TransactionController.ts | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 58a0fb2829..ea7b818a78 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -643,7 +643,7 @@ export class TransactionController extends BaseControllerV1< } } - getEthQuery({ + #getEthQuery({ networkClientId, chainId, }: { @@ -761,7 +761,7 @@ export class TransactionController extends BaseControllerV1< ); const chainId = this.getChainId(networkClientId); - const ethQuery = this.getEthQuery({ networkClientId, chainId }); + const ethQuery = this.#getEthQuery({ networkClientId, chainId }); const transactionType = type ?? (await determineTransactionType(txParams, ethQuery)).type; @@ -970,7 +970,7 @@ export class TransactionController extends BaseControllerV1< txParams: newTxParams, }); - const ethQuery = this.getEthQuery({ + const ethQuery = this.#getEthQuery({ networkClientId: transactionMeta.networkClientId, chainId: transactionMeta.chainId, }); @@ -1130,7 +1130,7 @@ export class TransactionController extends BaseControllerV1< log('Submitting speed up transaction', { oldFee, newFee, txParams }); - const ethQuery = this.getEthQuery({ + const ethQuery = this.#getEthQuery({ networkClientId: transactionMeta.networkClientId, chainId: transactionMeta.chainId, }); @@ -1182,6 +1182,7 @@ export class TransactionController extends BaseControllerV1< this.hub.emit(`${transactionMeta.id}:speedup`, newTransactionMeta); } + // NOTE(JL): Should this be private? stopTrackingByNetworkClientId(networkClientId: NetworkClientId) { const trackers = this.trackingMap.get(networkClientId); if (trackers) { @@ -1200,6 +1201,7 @@ export class TransactionController extends BaseControllerV1< this.trackingMap.delete(networkClientId); } + // NOTE(JL): Should this be private? startTrackingByNetworkClientId(networkClientId: NetworkClientId) { const networkClient = this.getNetworkClientById(networkClientId); const { chainId } = networkClient.configuration; @@ -1282,7 +1284,7 @@ export class TransactionController extends BaseControllerV1< transaction: TransactionParams, networkClientId?: NetworkClientId, ) { - const ethQuery = this.getEthQuery({ networkClientId }); + const ethQuery = this.#getEthQuery({ networkClientId }); const { estimatedGas, simulationFails } = await estimateGas( transaction, ethQuery, @@ -1304,7 +1306,7 @@ export class TransactionController extends BaseControllerV1< multiplier: number, networkClientId?: NetworkClientId, ) { - const ethQuery = this.getEthQuery({ networkClientId }); + const ethQuery = this.#getEthQuery({ networkClientId }); const { blockGasLimit, estimatedGas, simulationFails } = await estimateGas( transaction, ethQuery, @@ -1399,14 +1401,6 @@ export class TransactionController extends BaseControllerV1< }); } - startIncomingTransactionProcessing() { - this.incomingTransactionHelper.start(); - } - - stopIncomingTransactionProcessing() { - this.incomingTransactionHelper.stop(); - } - /** * Adds external provided transaction to state as confirmed transaction. * @@ -1665,6 +1659,7 @@ export class TransactionController extends BaseControllerV1< address: string, networkClientId?: NetworkClientId, ): Promise { + // TODO(JL): SmartTransactionController reaches into TransactionController.nonceTracker directly. Should probably change this. // TODO(JL): Revisit this method. It's a bit complicated and not obvious what it achieves. let nonceMutexForChainId: Mutex | undefined; let { nonceTracker } = this; @@ -1758,7 +1753,7 @@ export class TransactionController extends BaseControllerV1< const updatedTransaction = merge(transactionMeta, editableParams); const { type } = await determineTransactionType( updatedTransaction.txParams, - this.getEthQuery({ + this.#getEthQuery({ networkClientId: transactionMeta.networkClientId, chainId: transactionMeta.chainId, }), @@ -2088,7 +2083,7 @@ export class TransactionController extends BaseControllerV1< } await updateGas({ - ethQuery: this.getEthQuery({ networkClientId, chainId }), + ethQuery: this.#getEthQuery({ networkClientId, chainId }), chainId, isCustomNetwork, txMeta: transactionMeta, @@ -2096,7 +2091,7 @@ export class TransactionController extends BaseControllerV1< await updateGasFees({ eip1559: isEIP1559Compatible, - ethQuery: this.getEthQuery({ networkClientId, chainId }), + ethQuery: this.#getEthQuery({ networkClientId, chainId }), getSavedGasFees: this.getSavedGasFees.bind(this), getGasFeeEstimates: this.getGasFeeEstimates.bind(this), txMeta: transactionMeta, @@ -2324,7 +2319,7 @@ export class TransactionController extends BaseControllerV1< return; } - const ethQuery = this.getEthQuery({ + const ethQuery = this.#getEthQuery({ networkClientId: transactionMeta.networkClientId, chainId: transactionMeta.chainId, }); @@ -2943,7 +2938,7 @@ export class TransactionController extends BaseControllerV1< return; } - const ethQuery = this.getEthQuery({ + const ethQuery = this.#getEthQuery({ networkClientId: transactionMeta.networkClientId, chainId: transactionMeta.chainId, }); From 7a527b2ec8c5119c34755ec34977f8e350f3c18c Mon Sep 17 00:00:00 2001 From: jiexi Date: Wed, 10 Jan 2024 14:15:59 -0800 Subject: [PATCH 021/100] Jl/mmp 1894/transaction multichain incoming transaction network client (#3765) ## Explanation * Add optional networkClientId to start/stopIncomingTransactionPolling and updateIncomingTransactions * if not provided, action applies to _all_ incomingTransactionHelpers ## References See https://github.com/MetaMask/MetaMask-planning/issues/1894 ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --------- Co-authored-by: Alex --- .../src/TransactionController.test.ts | 1 + .../src/TransactionController.ts | 34 +++++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 83cdb4e758..b63c7c9356 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -4629,6 +4629,7 @@ describe('TransactionController', () => { Current tx status: ${TransactionStatus.submitted}`); }); }); + describe('startTrackinbByNetworkClientId', () => { it('should start tracking in a tracking map', () => { const controller = newController(); diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index ea7b818a78..a507434845 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -830,16 +830,44 @@ export class TransactionController extends BaseControllerV1< }; } - startIncomingTransactionPolling() { + startIncomingTransactionPolling(networkClientId?: NetworkClientId) { + if (networkClientId) { + this.trackingMap.get(networkClientId)?.incomingTransactionHelper.start(); + return; + } + this.incomingTransactionHelper.start(); + for (const [, trackingMap] of this.trackingMap) { + trackingMap.incomingTransactionHelper.start(); + } } - stopIncomingTransactionPolling() { + stopIncomingTransactionPolling(networkClientId?: NetworkClientId) { + if (networkClientId) { + this.trackingMap.get(networkClientId)?.incomingTransactionHelper.stop(); + return; + } + this.incomingTransactionHelper.stop(); + for (const [, trackingMap] of this.trackingMap) { + trackingMap.incomingTransactionHelper.stop(); + } } - async updateIncomingTransactions() { + async updateIncomingTransactions(networkClientId?: NetworkClientId) { + if (networkClientId) { + await this.trackingMap + .get(networkClientId) + ?.incomingTransactionHelper.update(); + return; + } + await this.incomingTransactionHelper.update(); + await Promise.allSettled( + Array.from(this.trackingMap).map(async ([_, trackingMap]) => { + return await trackingMap.incomingTransactionHelper.update(); + }), + ); } /** From 938b32f108f29f33823767c45aca37a36c7eb231 Mon Sep 17 00:00:00 2001 From: Shane Date: Thu, 11 Jan 2024 14:29:54 -0800 Subject: [PATCH 022/100] Transaction multichain init tracking map (#3770) ## Explanation ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --------- Co-authored-by: jiexi Co-authored-by: Alex Donesky --- .../src/TransactionController.test.ts | 198 +++++++++++++++++- .../src/TransactionController.ts | 78 ++++++- packages/transaction-controller/src/types.ts | 3 + 3 files changed, 272 insertions(+), 7 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 7e04b8e882..63edb84e74 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -20,6 +20,7 @@ import type { } from '@metamask/network-controller'; import { NetworkClientType, NetworkStatus } from '@metamask/network-controller'; import { errorCodes, providerErrors, rpcErrors } from '@metamask/rpc-errors'; +import { EventEmitter } from 'events'; import * as NonceTrackerPackage from 'nonce-tracker'; import { FakeBlockTracker } from '../../../tests/fake-block-tracker'; @@ -31,6 +32,7 @@ import type { TransactionControllerMessenger, TransactionConfig, TransactionState, + TransactionControllerEventEmitter, } from './TransactionController'; import { TransactionController } from './TransactionController'; import type { @@ -70,6 +72,17 @@ const mockFlags: { [key: string]: any } = { getBlockByNumberValue: null, }; +const SEPOLIA = { + chainId: toHex(11155111), + type: NetworkType.sepolia, + ticker: NetworksTicker.sepolia, +}; +const GOERLI = { + chainId: toHex(5), + type: NetworkType.goerli, + ticker: NetworksTicker.goerli, +}; + const ethQueryMockResults = { sendRawTransaction: 'mockSendRawTransactionResult', }; @@ -257,7 +270,21 @@ function buildMockMessenger({ }); } + const mockSubscribe = jest.fn(); + mockSubscribe.mockImplementation((_type, handler) => { + setTimeout(() => { + handler({}, [ + { + op: 'add', + path: ['networkConfigurations', 'foo', 'bar'], + value: 'foo', + }, + ]); + }, 0); + }); + const messenger = { + subscribe: mockSubscribe, call: jest.fn().mockImplementation(() => { if (approved) { return Promise.resolve({ resultCallbacks }); @@ -557,6 +584,30 @@ describe('TransactionController', () => { blockTracker: finalNetwork.blockTracker, provider: finalNetwork.provider, }; + case 'sepolia': + return { + configuration: { + chainId: SEPOLIA.chainId, + }, + blockTracker: buildMockBlockTracker('0x1'), + provider: MAINNET_PROVIDER, + }; + case 'goerli': + return { + configuration: { + chainId: GOERLI.chainId, + }, + blockTracker: buildMockBlockTracker('0x1'), + provider: MAINNET_PROVIDER, + }; + case 'customNetworkClientId-1': + return { + configuration: { + chainId: '0xa', + }, + blockTracker: buildMockBlockTracker('0x1'), + provider: MAINNET_PROVIDER, + }; case 'global': return { configuration: { @@ -566,7 +617,7 @@ describe('TransactionController', () => { provider: finalNetwork.provider, }; default: - throw new Error('Invalid network client id'); + throw new Error(`Invalid network client id ${networkClientId}`); } }); @@ -589,6 +640,23 @@ describe('TransactionController', () => { getGasFeeEstimates: () => Promise.resolve({}), getPermittedAccounts: () => [ACCOUNT_MOCK], getSelectedAddress: () => ACCOUNT_MOCK, + getNetworkClientRegistry: () => ({ + sepolia: { + configuration: { + chainId: SEPOLIA.chainId, + }, + }, + goerli: { + configuration: { + chainId: GOERLI.chainId, + }, + }, + 'customNetworkClientId-1': { + configuration: { + chainId: '0xa', + }, + }, + }), messenger, onNetworkStateChange: finalNetwork.subscribe, provider: finalNetwork.provider, @@ -733,7 +801,8 @@ describe('TransactionController', () => { ACCOUNT_MOCK, ); - expect(nonceTrackerMock).toHaveBeenCalledTimes(1); + // gets called in constructor now + expect(nonceTrackerMock).toHaveBeenCalledTimes(4); expect(pendingTransactions).toStrictEqual([ expect.any(Object), ...externalPendingTransactions, @@ -4621,8 +4690,7 @@ describe('TransactionController', () => { Current tx status: ${TransactionStatus.submitted}`); }); }); - - describe('startTrackinbByNetworkClientId', () => { + describe('startTrackingByNetworkClientId', () => { it('should start tracking in a tracking map', () => { const controller = newController(); const trackingMap = controller.startTrackingByNetworkClientId('mainnet'); @@ -4647,4 +4715,126 @@ describe('TransactionController', () => { expect(stopSpy).toHaveBeenCalledTimes(1); }); }); + describe('initTrackingMap', () => { + // eslint-disable-next-line jest/no-done-callback + it('should initialize the tracking map on construction', (done) => { + const hub = new EventEmitter() as TransactionControllerEventEmitter; + hub.on('tracking-map-init', (networkClientIds) => { + expect(networkClientIds).toStrictEqual([ + 'sepolia', + 'goerli', + 'customNetworkClientId-1', + ]); + done(); + }); + const controller = newController({ + options: { + hub, + }, + }); + expect(controller).toBeDefined(); + }); + // eslint-disable-next-line jest/no-done-callback + it('should handle removals in the networkController registry', (done) => { + const hub = new EventEmitter() as TransactionControllerEventEmitter; + const mockGetNetworkClientRegistry = jest.fn(); + mockGetNetworkClientRegistry.mockImplementationOnce(() => ({ + sepolia: { + configuration: { + chainId: SEPOLIA.chainId, + }, + }, + goerli: { + configuration: { + chainId: GOERLI.chainId, + }, + }, + 'customNetworkClientId-1': { + configuration: { + chainId: '0xa', + }, + }, + })); + hub.on('tracking-map-init', () => { + mockGetNetworkClientRegistry.mockClear(); + mockGetNetworkClientRegistry.mockImplementationOnce(() => ({ + sepolia: { + configuration: { + chainId: SEPOLIA.chainId, + }, + }, + goerli: { + configuration: { + chainId: GOERLI.chainId, + }, + }, + })); + }); + hub.on('tracking-map-remove', (networkClientIds) => { + expect(networkClientIds).toStrictEqual(['customNetworkClientId-1']); + done(); + }); + const controller = newController({ + options: { + getNetworkClientRegistry: mockGetNetworkClientRegistry, + hub, + }, + }); + expect(controller).toBeDefined(); + }); + }); + // eslint-disable-next-line jest/no-done-callback + it('should handle removals in the networkController registry', (done) => { + const hub = new EventEmitter() as TransactionControllerEventEmitter; + const mockGetNetworkClientRegistry = jest.fn(); + mockGetNetworkClientRegistry.mockImplementationOnce(() => ({ + sepolia: { + configuration: { + chainId: SEPOLIA.chainId, + }, + }, + })); + hub.on('tracking-map-init', () => { + mockGetNetworkClientRegistry.mockClear(); + mockGetNetworkClientRegistry.mockImplementationOnce(() => ({ + sepolia: { + configuration: { + chainId: SEPOLIA.chainId, + }, + }, + goerli: { + configuration: { + chainId: GOERLI.chainId, + }, + }, + })); + }); + hub.on('tracking-map-add', (networkClientIds) => { + expect(networkClientIds).toStrictEqual(['goerli']); + done(); + }); + const mockMessenger = buildMockMessenger({}); + (mockMessenger.messenger.subscribe as jest.Mock).mockImplementation( + (_type, handler) => { + setTimeout(() => { + handler({}, [ + { + op: 'remove', + path: ['networkConfigurations', 'foo', 'bar'], + value: 'foo', + }, + ]); + }, 0); + }, + ); + + const controller = newController({ + options: { + messenger: mockMessenger.messenger, + getNetworkClientRegistry: mockGetNetworkClientRegistry, + hub, + }, + }); + expect(controller).toBeDefined(); + }); }); diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index a507434845..f20f584219 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -25,9 +25,11 @@ import type { BlockTracker, NetworkClientId, NetworkController, + NetworkControllerStateChangeEvent, NetworkState, Provider, NetworkClientConfiguration, + ProviderConfig, } from '@metamask/network-controller'; import { NetworkClientType } from '@metamask/network-controller'; import type { AutoManagedNetworkClient } from '@metamask/network-controller/src/create-auto-managed-network-client'; @@ -198,15 +200,17 @@ const controllerName = 'TransactionController'; */ type AllowedActions = AddApprovalRequest; +type AllowedEvents = NetworkControllerStateChangeEvent; + /** * The messenger of the {@link TransactionController}. */ export type TransactionControllerMessenger = RestrictedControllerMessenger< typeof controllerName, AllowedActions, - never, + AllowedEvents, AllowedActions['type'], - never + AllowedEvents['type'] >; // This interface was created before this ESLint rule was added. @@ -306,6 +310,8 @@ export class TransactionController extends BaseControllerV1< private readonly getNetworkClientById: NetworkController['getNetworkClientById']; + private readonly getNetworkClientRegistry: NetworkController['getNetworkClientRegistry']; + private readonly getNetworkClientIdForDomain: SelectedNetworkController['getNetworkClientIdForDomain']; private readonly etherscanRemoteTransactionSource: EtherscanRemoteTransactionSource; @@ -405,6 +411,8 @@ export class TransactionController extends BaseControllerV1< * @param options.hooks.beforePublish - Additional logic to execute before publishing a transaction. Return false to prevent the broadcast of the transaction. * @param options.hooks.getAdditionalSignArguments - Returns additional arguments required to sign a transaction. * @param options.getNetworkClientIdForDomain - Gets the network client id for the given domain. + * @param options.hub - Use a different event emitter for the hub. + * @param options.getNetworkClientRegistry - Gets the network client registry. * @param config - Initial options used to configure this controller. * @param state - Initial state to set on this controller. */ @@ -433,6 +441,8 @@ export class TransactionController extends BaseControllerV1< findNetworkClientIdByChainId, getNetworkClientById, getNetworkClientIdForDomain, + getNetworkClientRegistry, + hub, hooks = {}, }: { blockTracker: BlockTracker; @@ -467,7 +477,9 @@ export class TransactionController extends BaseControllerV1< speedUpMultiplier?: number; findNetworkClientIdByChainId: NetworkController['findNetworkClientIdByChainId']; getNetworkClientById: NetworkController['getNetworkClientById']; + getNetworkClientRegistry: NetworkController['getNetworkClientRegistry']; getNetworkClientIdForDomain: SelectedNetworkController['getNetworkClientIdForDomain']; + hub: TransactionControllerEventEmitter; hooks: { afterSign?: ( transactionMeta: TransactionMeta, @@ -499,8 +511,10 @@ export class TransactionController extends BaseControllerV1< }; this.initialize(); + this.hub = hub ?? this.hub; this.findNetworkClientIdByChainId = findNetworkClientIdByChainId; this.getNetworkClientById = getNetworkClientById; + this.getNetworkClientRegistry = getNetworkClientRegistry; this.provider = provider; this.messagingSystem = messenger; this.getNetworkState = getNetworkState; @@ -598,6 +612,20 @@ export class TransactionController extends BaseControllerV1< this.subscribe(this.#onStateChange); + this.messagingSystem.subscribe( + 'NetworkController:stateChange', + (_, patches) => { + const shouldRefresh = patches.some((patch) => { + const correctOp = patch.op === 'add' || patch.op === 'remove'; + const correctPath = patch.path[0] === 'networkConfigurations'; + return correctOp && correctPath; + }); + if (shouldRefresh) { + this.#refreshTrackingMap(); + } + }, + ); + onNetworkStateChange(() => { log('Detected network change', this.getChainId()); // TODO(JL): Network state changes also trigger PendingTransactionTracker's onStateChange. @@ -607,8 +635,47 @@ export class TransactionController extends BaseControllerV1< }); this.onBootCleanup(); + this.#initTrackingMap(); } + #refreshTrackingMap = () => { + const networkClients = this.getNetworkClientRegistry(); + const networkClientIds = Object.keys(networkClients); + + const existingNetworkClientIds = Array.from(this.trackingMap.keys()); + + // Remove tracking for NetworkClientIds that no longer exist + const networkClientIdsToRemove = existingNetworkClientIds.filter( + (id) => !networkClientIds.includes(id), + ); + networkClientIdsToRemove.forEach((id) => { + this.stopTrackingByNetworkClientId(id); + }); + + if (networkClientIdsToRemove.length > 0) { + this.hub.emit('tracking-map-remove', networkClientIdsToRemove); + } + + // Start tracking new NetworkClientIds from the registry + const networkClientIdsToAdd = networkClientIds.filter( + (id) => !existingNetworkClientIds.includes(id), + ); + networkClientIdsToAdd.forEach((id) => { + this.startTrackingByNetworkClientId(id); + }); + + if (networkClientIdsToAdd.length > 0) { + this.hub.emit('tracking-map-add', networkClientIdsToAdd); + } + }; + + #initTrackingMap = () => { + const networkClients = this.getNetworkClientRegistry(); + const networkClientIds = Object.keys(networkClients); + networkClientIds.map((id) => this.startTrackingByNetworkClientId(id)); + this.hub.emit('tracking-map-init', networkClientIds); + }; + #onStateChange = () => { // PendingTransactionTracker reads state through its getTransactions hook this.pendingTransactionTracker.onStateChange(); @@ -1256,7 +1323,12 @@ export class TransactionController extends BaseControllerV1< blockTracker: networkClient.blockTracker, getCurrentAccount: this.getSelectedAddress, getLastFetchedBlockNumbers: () => this.state.lastFetchedBlockNumbers, - getNetworkState: this.getNetworkState, // TODO: fake this via networkClient + // TODO(JL): Fix this type + getNetworkState: () => { + return { + providerConfig: { chainId } as ProviderConfig, + } as NetworkState; + }, isEnabled: () => true, queryEntireHistory: true, remoteTransactionSource: this.etherscanRemoteTransactionSource, diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index 524f8828eb..0d00abf03a 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -4,6 +4,9 @@ import type { Hex } from '@metamask/utils'; import type { Operation } from 'fast-json-patch'; export type Events = { + ['tracking-map-init']: [networkClientIds: NetworkClientId[]]; + ['tracking-map-add']: [networkClientIds: NetworkClientId[]]; + ['tracking-map-remove']: [networkClientIds: NetworkClientId[]]; ['incomingTransactionBlock']: [blockNumber: number]; ['post-transaction-balance-updated']: [ { From e1651405a4fa72c62bde7d402259134b39e8c21f Mon Sep 17 00:00:00 2001 From: Shane Date: Mon, 15 Jan 2024 12:05:42 -0800 Subject: [PATCH 023/100] Added multichain addTransaction tests (#3780) ## Explanation ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- .../src/TransactionController.test.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 63edb84e74..e774e9f40d 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -1206,6 +1206,59 @@ describe('TransactionController', () => { ); }); + describe('multichain', () => { + it('adds unapproved transaction to state when using networkClientId', async () => { + const controller = newController(); + const sepoliaTxParams: TransactionParams = { + chainId: SEPOLIA.chainId, + from: ACCOUNT_MOCK, + to: ACCOUNT_2_MOCK, + }; + + await controller.addTransaction(sepoliaTxParams, { + origin: 'metamask', + actionId: ACTION_ID_MOCK, + networkClientId: 'sepolia', + }); + + const transactionMeta = controller.state.transactions[0]; + + expect(transactionMeta.txParams.from).toStrictEqual( + sepoliaTxParams.from, + ); + expect(transactionMeta.chainId).toStrictEqual(sepoliaTxParams.chainId); + expect(transactionMeta.networkClientId).toBe('sepolia'); + expect(transactionMeta.origin).toBe('metamask'); + }); + it('adds unapproved transaction with networkClientId and can be updated to submitted', async () => { + const controller = newController({ approve: true }); + const submittedEventListener = jest.fn(); + controller.hub.on('transaction-submitted', submittedEventListener); + + const sepoliaTxParams: TransactionParams = { + chainId: SEPOLIA.chainId, + from: ACCOUNT_MOCK, + to: ACCOUNT_MOCK, + }; + + const { result } = await controller.addTransaction(sepoliaTxParams, { + origin: 'metamask', + actionId: ACTION_ID_MOCK, + networkClientId: 'sepolia', + }); + + await result; + + const { txParams, status, networkClientId, chainId } = + controller.state.transactions[0]; + expect(submittedEventListener).toHaveBeenCalledTimes(1); + expect(txParams.from).toBe(ACCOUNT_MOCK); + expect(networkClientId).toBe('sepolia'); + expect(chainId).toBe(SEPOLIA.chainId); + expect(status).toBe(TransactionStatus.submitted); + }); + }); + it('generates initial history', async () => { const controller = newController(); From 58f8ea0eb07fe20e52b0a2c6c978b86780f17214 Mon Sep 17 00:00:00 2001 From: jiexi Date: Tue, 16 Jan 2024 09:39:53 -0800 Subject: [PATCH 024/100] Jl/mmp 1889/transaction multichain incoming transaction tests (#3782) ## Explanation * Update `startIncomingTransactionPolling`, `stopIncomingTransactionPolling`, `updateIncomingTransactions` * Accepts optional networkClientIds array * targets global IncomingTransactionHelper if no networkClientIds provided * Add `stopAllIncomingTransactionPolling` * Add specs for above methods * Update specs to return unique mocked helper instances rather than singleton ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --------- Co-authored-by: Alex Donesky --- .../src/TransactionController.test.ts | 156 ++++++++++++++---- .../src/TransactionController.ts | 40 ++--- 2 files changed, 147 insertions(+), 49 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index e774e9f40d..ebc7d63a9b 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -517,8 +517,8 @@ describe('TransactionController', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any let approveTransaction: (value?: any) => void; let getNonceLockSpy: jest.Mock; - let incomingTransactionHelperMock: jest.Mocked; - let pendingTransactionTrackerMock: jest.Mocked; + let incomingTransactionHelperMocks: jest.Mocked[]; + let pendingTransactionTrackerMocks: jest.Mocked[]; let timeCounter = 0; const incomingTransactionHelperClassMock = @@ -718,30 +718,35 @@ describe('TransactionController', () => { NonceTrackerPackage.NonceTracker.prototype.getNonceLock = getNonceLockSpy; - incomingTransactionHelperMock = { - stop: jest.fn(), - hub: { - on: jest.fn(), - }, - } as unknown as jest.Mocked; - - pendingTransactionTrackerMock = { - start: jest.fn(), - stop: jest.fn(), - hub: { - on: jest.fn(), - removeAllListeners: jest.fn(), - }, - onStateChange: jest.fn(), - } as unknown as jest.Mocked; + incomingTransactionHelperMocks = []; + incomingTransactionHelperClassMock.mockImplementation(() => { + const incomingTransactionHelperMock = { + start: jest.fn(), + stop: jest.fn(), + update: jest.fn(), + hub: { + on: jest.fn(), + }, + } as unknown as jest.Mocked; + incomingTransactionHelperMocks.push(incomingTransactionHelperMock); + return incomingTransactionHelperMock; + }); - incomingTransactionHelperClassMock.mockReturnValue( - incomingTransactionHelperMock, - ); + pendingTransactionTrackerMocks = []; + pendingTransactionTrackerClassMock.mockImplementation(() => { + const pendingTransactionTrackerMock = { + start: jest.fn(), + stop: jest.fn(), + hub: { + on: jest.fn(), + removeAllListeners: jest.fn(), + }, + onStateChange: jest.fn(), + } as unknown as jest.Mocked; - pendingTransactionTrackerClassMock.mockReturnValue( - pendingTransactionTrackerMock, - ); + pendingTransactionTrackerMocks.push(pendingTransactionTrackerMock); + return pendingTransactionTrackerMock; + }); }); afterEach(() => { @@ -3170,7 +3175,7 @@ describe('TransactionController', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (incomingTransactionHelperMock.hub.on as any).mock.calls[0][1]({ + await (incomingTransactionHelperMocks[0].hub.on as any).mock.calls[0][1]({ added: [TRANSACTION_META_MOCK, TRANSACTION_META_2_MOCK], updated: [], }); @@ -3196,7 +3201,7 @@ describe('TransactionController', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (incomingTransactionHelperMock.hub.on as any).mock.calls[0][1]({ + await (incomingTransactionHelperMocks[0].hub.on as any).mock.calls[0][1]({ added: [], updated: [updatedTransaction], }); @@ -3212,7 +3217,7 @@ describe('TransactionController', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (incomingTransactionHelperMock.hub.on as any).mock.calls[0][1]({ + await (incomingTransactionHelperMocks[0].hub.on as any).mock.calls[0][1]({ added: [TRANSACTION_META_MOCK, TRANSACTION_META_2_MOCK], updated: [], }); @@ -3233,7 +3238,7 @@ describe('TransactionController', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (incomingTransactionHelperMock.hub.on as any).mock.calls[1][1]({ + await (incomingTransactionHelperMocks[0].hub.on as any).mock.calls[1][1]({ lastFetchedBlockNumbers, blockNumber: 123, }); @@ -3252,7 +3257,7 @@ describe('TransactionController', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (incomingTransactionHelperMock.hub.on as any).mock.calls[1][1]({ + await (incomingTransactionHelperMocks[0].hub.on as any).mock.calls[1][1]({ lastFetchedBlockNumbers: { key: 234, }, @@ -3471,7 +3476,7 @@ describe('TransactionController', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any ...args: any ) { - (pendingTransactionTrackerMock.hub.on as jest.Mock).mock.calls.find( + (pendingTransactionTrackerMocks[0].hub.on as jest.Mock).mock.calls.find( (call) => call[0] === eventName, )[1](...args); } @@ -4890,4 +4895,95 @@ describe('TransactionController', () => { }); expect(controller).toBeDefined(); }); + + describe('startIncomingTransactionPolling', () => { + it('should start the incoming transaction helper for the specific networkClientIds provided', () => { + const controller = newController(); + const trackingMap = controller.startTrackingByNetworkClientId('mainnet'); + controller.startTrackingByNetworkClientId('sepolia'); + + controller.startIncomingTransactionPolling(['mainnet', 'sepolia']); + + expect( + trackingMap.get('mainnet')?.incomingTransactionHelper.start, + ).toHaveBeenCalledTimes(1); + expect( + trackingMap.get('sepolia')?.incomingTransactionHelper.start, + ).toHaveBeenCalledTimes(1); + }); + + it('should start the global incoming transaction helper when no networkClientIds provided', () => { + const controller = newController(); + + controller.startIncomingTransactionPolling([]); + + expect(incomingTransactionHelperMocks[0].start).toHaveBeenCalledTimes(1); + }); + }); + + describe('stopIncomingTransactionPolling', () => { + it('should stop the incoming transaction helper for the specific networkClientIds provided', () => { + const controller = newController(); + const trackingMap = controller.startTrackingByNetworkClientId('mainnet'); + controller.startTrackingByNetworkClientId('sepolia'); + + controller.stopIncomingTransactionPolling(['mainnet', 'sepolia']); + + expect( + trackingMap.get('mainnet')?.incomingTransactionHelper.stop, + ).toHaveBeenCalledTimes(1); + expect( + trackingMap.get('sepolia')?.incomingTransactionHelper.stop, + ).toHaveBeenCalledTimes(1); + }); + + it('should stop the global incoming transaction helper when no networkClientIds provided', () => { + const controller = newController(); + + controller.stopIncomingTransactionPolling([]); + + expect(incomingTransactionHelperMocks[0].stop).toHaveBeenCalledTimes(1); + }); + }); + + describe('stopAllIncomingTransactionPolling', () => { + it('should stop the global incoming transaction helper and each transaction helper in the tracking map', () => { + const controller = newController(); + const trackingMap = controller.startTrackingByNetworkClientId('mainnet'); + + controller.stopAllIncomingTransactionPolling(); + + expect(incomingTransactionHelperMocks[0].stop).toHaveBeenCalledTimes(1); + expect( + trackingMap.get('mainnet')?.incomingTransactionHelper.stop, + ).toHaveBeenCalledTimes(1); + expect( + trackingMap.get('sepolia')?.incomingTransactionHelper.stop, + ).toHaveBeenCalledTimes(1); + }); + }); + + describe('updateIncomingTransactions', () => { + it('should update the incoming transactions for the specific networkClientIds provided', async () => { + const controller = newController(); + const trackingMap = controller.startTrackingByNetworkClientId('mainnet'); + + await controller.updateIncomingTransactions(['mainnet', 'sepolia']); + + expect( + trackingMap.get('mainnet')?.incomingTransactionHelper.update, + ).toHaveBeenCalledTimes(1); + expect( + trackingMap.get('sepolia')?.incomingTransactionHelper.update, + ).toHaveBeenCalledTimes(1); + }); + + it('should update the global incoming transactions when no networkClientIds provided', async () => { + const controller = newController(); + + await controller.updateIncomingTransactions([]); + + expect(incomingTransactionHelperMocks[0].update).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index f20f584219..adce098c10 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -897,42 +897,44 @@ export class TransactionController extends BaseControllerV1< }; } - startIncomingTransactionPolling(networkClientId?: NetworkClientId) { - if (networkClientId) { - this.trackingMap.get(networkClientId)?.incomingTransactionHelper.start(); + startIncomingTransactionPolling(networkClientIds: NetworkClientId[] = []) { + if (networkClientIds.length === 0) { + this.incomingTransactionHelper.start(); return; } - - this.incomingTransactionHelper.start(); - for (const [, trackingMap] of this.trackingMap) { - trackingMap.incomingTransactionHelper.start(); - } + networkClientIds.forEach((networkClientId) => { + this.trackingMap.get(networkClientId)?.incomingTransactionHelper.start(); + }); } - stopIncomingTransactionPolling(networkClientId?: NetworkClientId) { - if (networkClientId) { - this.trackingMap.get(networkClientId)?.incomingTransactionHelper.stop(); + stopIncomingTransactionPolling(networkClientIds: NetworkClientId[] = []) { + if (networkClientIds.length === 0) { + this.incomingTransactionHelper.stop(); return; } + networkClientIds.forEach((networkClientId) => { + this.trackingMap.get(networkClientId)?.incomingTransactionHelper.stop(); + }); + } + stopAllIncomingTransactionPolling() { this.incomingTransactionHelper.stop(); for (const [, trackingMap] of this.trackingMap) { trackingMap.incomingTransactionHelper.stop(); } } - async updateIncomingTransactions(networkClientId?: NetworkClientId) { - if (networkClientId) { - await this.trackingMap - .get(networkClientId) - ?.incomingTransactionHelper.update(); + async updateIncomingTransactions(networkClientIds: NetworkClientId[] = []) { + if (networkClientIds.length === 0) { + await this.incomingTransactionHelper.update(); return; } - await this.incomingTransactionHelper.update(); await Promise.allSettled( - Array.from(this.trackingMap).map(async ([_, trackingMap]) => { - return await trackingMap.incomingTransactionHelper.update(); + networkClientIds.map(async (networkClientId) => { + return await this.trackingMap + .get(networkClientId) + ?.incomingTransactionHelper.update(); }), ); } From 97f4c3c6aa19996aa5759b499f78444faa2dc7a4 Mon Sep 17 00:00:00 2001 From: jiexi Date: Tue, 16 Jan 2024 11:45:19 -0800 Subject: [PATCH 025/100] Transaction multichain integration (#3789) ## Explanation ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --------- Co-authored-by: Shane Jonas --- .../src/TransactionController.ts | 14 +- .../TransactionControllerIntegration.test.ts | 205 ++++++++++++++++++ 2 files changed, 216 insertions(+), 3 deletions(-) create mode 100644 packages/transaction-controller/src/TransactionControllerIntegration.test.ts diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index adce098c10..4186386d9e 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -670,10 +670,15 @@ export class TransactionController extends BaseControllerV1< }; #initTrackingMap = () => { + console.log('init tracking map 1'); const networkClients = this.getNetworkClientRegistry(); + console.log('init tracking map 2'); const networkClientIds = Object.keys(networkClients); + console.log('init tracking map 3'); networkClientIds.map((id) => this.startTrackingByNetworkClientId(id)); + console.log('init tracking map 4'); this.hub.emit('tracking-map-init', networkClientIds); + console.log('init tracking map 5'); }; #onStateChange = () => { @@ -2174,14 +2179,17 @@ export class TransactionController extends BaseControllerV1< transactionMeta.txParams.type !== TransactionEnvelopeType.legacy; const { networkClientId } = transactionMeta; + let chainId; + let isCustomNetwork; - const { providerConfig } = this.getNetworkState(); - let { chainId } = providerConfig; - let isCustomNetwork = providerConfig.type === NetworkType.rpc; if (networkClientId) { const { configuration } = this.getNetworkClientById(networkClientId); chainId = configuration.chainId; isCustomNetwork = configuration.type === NetworkClientType.Custom; + } else { + const { providerConfig } = this.getNetworkState(); + chainId = providerConfig.chainId; + isCustomNetwork = providerConfig.type === NetworkType.rpc; } await updateGas({ diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts new file mode 100644 index 0000000000..6bada7c82b --- /dev/null +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -0,0 +1,205 @@ +import { ApprovalController } from '@metamask/approval-controller'; +import { ControllerMessenger } from '@metamask/base-controller'; +import { BUILT_IN_NETWORKS, NetworkType } from '@metamask/controller-utils'; +import { + NetworkController, + NetworkClientType, +} from '@metamask/network-controller'; + +import { mockNetwork } from '../../../tests/mock-network'; +import { TransactionController } from './TransactionController'; + +const ACCOUNT_MOCK = '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207'; +const ACCOUNT_2_MOCK = '0x08f137f335ea1b8f193b8f6ea92561a60d23a211'; + +const networkClientConfiguration = { + type: NetworkClientType.Infura, + network: NetworkType.mainnet, + chainId: BUILT_IN_NETWORKS[NetworkType.mainnet].chainId, + infuraProjectId: 'foo', + ticker: BUILT_IN_NETWORKS[NetworkType.mainnet].ticker, +} as const; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const newController = async (options: any) => { + const messenger = new ControllerMessenger(); + const networkController = new NetworkController({ + messenger: messenger.getRestricted({ name: 'NetworkController' }), + trackMetaMetricsEvent: () => { + // noop + }, + infuraProjectId: 'foo', + }); + await networkController.initializeProvider(); + const { provider } = networkController.getProviderAndBlockTracker(); + + const approvalController = new ApprovalController({ + messenger: messenger.getRestricted({ + name: 'ApprovalController', + }), + showApprovalRequest: jest.fn(), + }); + + const opts = { + provider, + messenger, + onNetworkStateChange: () => { + // noop + }, + getCurrentNetworkEIP1559Compatibility: + networkController.getEIP1559Compatibility.bind(networkController), + getNetworkClientRegistry: + networkController.getNetworkClientRegistry.bind(networkController), + findNetworkClientIdByChainId: + networkController.findNetworkClientIdByChainId.bind(networkController), + getNetworkClientById: + networkController.getNetworkClientById.bind(networkController), + getNetworkState: () => networkController.state, + ...options, + }; + const transactionController = new TransactionController(opts, { + // TODO(JL): fix this type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sign: async (transaction: any) => transaction, + }); + + return { + transactionController, + approvalController, + networkController, + }; +}; + +describe('TransactionController Integration', () => { + describe('constructor', () => { + // it('should create a new instance of TransactionController', async () => { + // mockNetwork({ + // networkClientConfiguration, + // mocks: [ + // { + // request: { + // method: 'eth_blockNumber', + // params: [], + // }, + // response: { + // result: '0x1', + // }, + // }, + // { + // request: { + // method: 'eth_blockNumber', + // params: [], + // }, + // response: { + // result: '0x2', + // }, + // }, + // { + // request: { + // method: 'eth_blockNumber', + // params: [], + // }, + // response: { + // result: '0x3', + // }, + // }, + // ], + // }); + // const transactionController = await newController({}); + // transactionController.stopTrackingByNetworkClientId('mainnet'); + // expect(transactionController).toBeDefined(); + // }); + describe('multichain transaction lifecycle', () => { + it('should add a new unapproved transaction', async () => { + mockNetwork({ + networkClientConfiguration, + mocks: [ + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x2', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3', + }, + }, + { + request: { + method: 'eth_getCode', + params: [ACCOUNT_2_MOCK, '0x1'], + }, + response: { + result: + // what should this be? + '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024657468546f546f6b656e53776170496e7075742875696e743235362c75696e743235362900000000000000000000000000000000000000000000000000000000', + }, + }, + { + request: { + method: 'eth_getBlockByNumber', + params: ['0x1', false], + }, + response: { + result: { + baseFeePerGas: '0x63c498a46', + number: '0x42', + }, + }, + }, + { + request: { + method: 'eth_gasPrice', + params: [], + }, + response: { + result: '0x1', + }, + }, + ], + }); + const { transactionController } = await newController({}); + await transactionController.addTransaction( + { + from: ACCOUNT_MOCK, + to: ACCOUNT_2_MOCK, + }, + { networkClientId: 'mainnet' }, + ); + expect(transactionController.state.transactions).toHaveLength(1); + expect(transactionController.state.transactions[0].status).toBe( + 'unapproved', + ); + }); + it('should be able to get to submitted state', async () => { + expect(true).toBe(true); + }); + it('should be able to get to completed state', async () => { + expect(true).toBe(true); + }); + it('should be able to get to cancelled state', async () => { + expect(true).toBe(true); + }); + it('should be able to get to speedup state', async () => { + expect(true).toBe(true); + }); + }); + }); +}); From c7c6cf454b5213c7438da63ff4f89d2c36a1b017 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Wed, 17 Jan 2024 15:03:17 -0600 Subject: [PATCH 026/100] use correct nonce tracker for approve transaction (#3792) Small gap I noticed --- .../src/TransactionController.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 4186386d9e..70910fe026 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -2361,6 +2361,7 @@ export class TransactionController extends BaseControllerV1< const { txParams: { from }, + networkClientId, } = transactionMeta; let releaseNonceLock: (() => void) | undefined; @@ -2384,9 +2385,18 @@ export class TransactionController extends BaseControllerV1< return; } + let { nonceTracker } = this; + if (networkClientId) { + const trackers = this.trackingMap.get(networkClientId); + if (!trackers) { + throw new Error('missing nonceTracker for networkClientId'); + } + nonceTracker = trackers?.nonceTracker; + } + const [nonce, releaseNonce] = await getNextNonce( transactionMeta, - this.nonceTracker, + nonceTracker, ); releaseNonceLock = releaseNonce; From 2987ea0f9a4190fcc30d88d0a7ccdeecaa36dd1f Mon Sep 17 00:00:00 2001 From: jiexi Date: Thu, 18 Jan 2024 14:26:17 -0800 Subject: [PATCH 027/100] Transaction multichain lifecycle (#3796) ## Explanation ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --------- Co-authored-by: Shane Jonas Co-authored-by: Alex --- .../src/TransactionController.ts | 12 +- .../TransactionControllerIntegration.test.ts | 1216 ++++++++++++++++- 2 files changed, 1167 insertions(+), 61 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 70910fe026..5a87ceb722 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -670,15 +670,10 @@ export class TransactionController extends BaseControllerV1< }; #initTrackingMap = () => { - console.log('init tracking map 1'); const networkClients = this.getNetworkClientRegistry(); - console.log('init tracking map 2'); const networkClientIds = Object.keys(networkClients); - console.log('init tracking map 3'); networkClientIds.map((id) => this.startTrackingByNetworkClientId(id)); - console.log('init tracking map 4'); this.hub.emit('tracking-map-init', networkClientIds); - console.log('init tracking map 5'); }; #onStateChange = () => { @@ -1297,8 +1292,7 @@ export class TransactionController extends BaseControllerV1< // doesn't seem like any cleanup is needed for nonceTracker // trackers.nonceTracker - // stop not exposed for pendingTransactionTracker - // trackers.pendingTransactionTracker.stop(); + trackers.pendingTransactionTracker.stop(); } this.trackingMap.delete(networkClientId); } @@ -1344,9 +1338,7 @@ export class TransactionController extends BaseControllerV1< }); const pendingTransactionTracker = new PendingTransactionTracker({ approveTransaction: this.approveTransaction.bind(this), - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - blockTracker: networkClient.provider as any, + blockTracker: networkClient.blockTracker, getChainId: () => networkClient.configuration.chainId, getEthQuery: () => ethQuery, getTransactions: () => this.state.transactions, diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index 6bada7c82b..2189991d28 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -5,18 +5,29 @@ import { NetworkController, NetworkClientType, } from '@metamask/network-controller'; +import { useFakeTimers } from 'sinon'; +import { advanceTime } from '../../../tests/helpers'; import { mockNetwork } from '../../../tests/mock-network'; import { TransactionController } from './TransactionController'; const ACCOUNT_MOCK = '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207'; const ACCOUNT_2_MOCK = '0x08f137f335ea1b8f193b8f6ea92561a60d23a211'; +const infuraProjectId = '341eacb578dd44a1a049cbc5f6fd4035'; const networkClientConfiguration = { + type: NetworkClientType.Infura, + network: NetworkType.goerli, + chainId: BUILT_IN_NETWORKS[NetworkType.goerli].chainId, + infuraProjectId, + ticker: BUILT_IN_NETWORKS[NetworkType.goerli].ticker, +} as const; + +const mainnetNetworkClientConfiguration = { type: NetworkClientType.Infura, network: NetworkType.mainnet, chainId: BUILT_IN_NETWORKS[NetworkType.mainnet].chainId, - infuraProjectId: 'foo', + infuraProjectId, ticker: BUILT_IN_NETWORKS[NetworkType.mainnet].ticker, } as const; @@ -28,10 +39,11 @@ const newController = async (options: any) => { trackMetaMetricsEvent: () => { // noop }, - infuraProjectId: 'foo', + infuraProjectId, }); await networkController.initializeProvider(); - const { provider } = networkController.getProviderAndBlockTracker(); + const { provider, blockTracker } = + networkController.getProviderAndBlockTracker(); const approvalController = new ApprovalController({ messenger: messenger.getRestricted({ @@ -43,6 +55,7 @@ const newController = async (options: any) => { const opts = { provider, messenger, + blockTracker, onNetworkStateChange: () => { // noop }, @@ -72,45 +85,117 @@ const newController = async (options: any) => { describe('TransactionController Integration', () => { describe('constructor', () => { - // it('should create a new instance of TransactionController', async () => { - // mockNetwork({ - // networkClientConfiguration, - // mocks: [ - // { - // request: { - // method: 'eth_blockNumber', - // params: [], - // }, - // response: { - // result: '0x1', - // }, - // }, - // { - // request: { - // method: 'eth_blockNumber', - // params: [], - // }, - // response: { - // result: '0x2', - // }, - // }, - // { - // request: { - // method: 'eth_blockNumber', - // params: [], - // }, - // response: { - // result: '0x3', - // }, - // }, - // ], - // }); - // const transactionController = await newController({}); - // transactionController.stopTrackingByNetworkClientId('mainnet'); - // expect(transactionController).toBeDefined(); - // }); - describe('multichain transaction lifecycle', () => { + it('should create a new instance of TransactionController', async () => { + mockNetwork({ + networkClientConfiguration: mainnetNetworkClientConfiguration, + mocks: [ + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x2', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3', + }, + }, + ], + }); + mockNetwork({ + networkClientConfiguration, + mocks: [ + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x2', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3', + }, + }, + ], + }); + const { transactionController } = await newController({}); + transactionController.stopTrackingByNetworkClientId('goerli'); + expect(transactionController).toBeDefined(); + }); + }); + describe('multichain transaction lifecycle', () => { + describe('when a transaction is added with a networkClientId that does not match the globally selected network', () => { it('should add a new unapproved transaction', async () => { + mockNetwork({ + networkClientConfiguration: mainnetNetworkClientConfiguration, + mocks: [ + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3b3301', + }, + }, + { + request: { + method: 'eth_getBlockByNumber', + params: ['0x3b3301'], + }, + response: { + result: { + baseFeePerGas: '0x63c498a46', + number: '0x3b3301', + }, + }, + }, + { + request: { + method: 'eth_getTransactionCount', + params: [ + '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + '0x3b3301', + ], + }, + response: { + result: '0x1', + }, + }, + ], + }); mockNetwork({ networkClientConfiguration, mocks: [ @@ -181,24 +266,1053 @@ describe('TransactionController Integration', () => { from: ACCOUNT_MOCK, to: ACCOUNT_2_MOCK, }, - { networkClientId: 'mainnet' }, + { networkClientId: 'goerli' }, ); + transactionController.stopTrackingByNetworkClientId('goerli'); expect(transactionController.state.transactions).toHaveLength(1); expect(transactionController.state.transactions[0].status).toBe( 'unapproved', ); }); it('should be able to get to submitted state', async () => { - expect(true).toBe(true); - }); - it('should be able to get to completed state', async () => { - expect(true).toBe(true); - }); - it('should be able to get to cancelled state', async () => { - expect(true).toBe(true); + mockNetwork({ + networkClientConfiguration: mainnetNetworkClientConfiguration, + mocks: [ + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3b3301', + }, + }, + { + request: { + method: 'eth_getBlockByNumber', + params: ['0x3b3301'], + }, + response: { + result: { + baseFeePerGas: '0x63c498a46', + number: '0x3b3301', + }, + }, + }, + { + request: { + method: 'eth_getTransactionCount', + params: [ + '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + '0x3b3301', + ], + }, + response: { + result: '0x1', + }, + }, + ], + }); + mockNetwork({ + networkClientConfiguration, + mocks: [ + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x2', + }, + }, + { + request: { + method: 'eth_getCode', + params: [ACCOUNT_2_MOCK, '0x1'], + }, + response: { + result: + // what should this be? + '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024657468546f546f6b656e53776170496e7075742875696e743235362c75696e743235362900000000000000000000000000000000000000000000000000000000', + }, + }, + { + request: { + method: 'eth_getBlockByNumber', + params: ['0x1', false], + }, + response: { + result: { + baseFeePerGas: '0x63c498a46', + number: '0x42', + }, + }, + }, + { + request: { + method: 'eth_gasPrice', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_estimateGas', + params: [ + { + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + to: '0x08f137f335ea1b8f193b8f6ea92561a60d23a211', + value: '0x0', + gas: '0x0', + }, + ], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_getTransactionCount', + params: ['0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', '0x1'], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_sendRawTransaction', + params: [ + '0x02e005010101019408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + ], + }, + response: { + result: '0x1', + }, + }, + ], + }); + const { transactionController, approvalController } = + await newController({}); + const { result, transactionMeta } = + await transactionController.addTransaction( + { + from: ACCOUNT_MOCK, + to: ACCOUNT_2_MOCK, + }, + { networkClientId: 'goerli' }, + ); + + await approvalController.accept(transactionMeta.id); + + await result; + + transactionController.stopTrackingByNetworkClientId('goerli'); + expect(transactionController.state.transactions).toHaveLength(1); + expect(transactionController.state.transactions[0].status).toBe( + 'submitted', + ); }); - it('should be able to get to speedup state', async () => { - expect(true).toBe(true); + it('should be able to get to confirmed state', async () => { + const clock = useFakeTimers(); + mockNetwork({ + networkClientConfiguration: mainnetNetworkClientConfiguration, + mocks: [ + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3b3301', + }, + }, + { + request: { + method: 'eth_getBlockByNumber', + params: ['0x3b3301'], + }, + response: { + result: { + baseFeePerGas: '0x63c498a46', + number: '0x3b3301', + }, + }, + }, + { + request: { + method: 'eth_getTransactionCount', + params: [ + '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + '0x3b3301', + ], + }, + response: { + result: '0x1', + }, + }, + ], + }); + mockNetwork({ + networkClientConfiguration, + mocks: [ + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3', + }, + }, + { + request: { + method: 'eth_getBlockByHash', + params: ['0x1', false], + }, + response: { + result: { + transactions: [], + }, + }, + }, + { + request: { + method: 'eth_getCode', + params: [ACCOUNT_2_MOCK, '0x1'], + }, + response: { + result: + // what should this be? + '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024657468546f546f6b656e53776170496e7075742875696e743235362c75696e743235362900000000000000000000000000000000000000000000000000000000', + }, + }, + { + request: { + method: 'eth_getBlockByNumber', + params: ['0x1', false], + }, + response: { + result: { + baseFeePerGas: '0x63c498a46', + number: '0x1', + }, + }, + }, + { + request: { + method: 'eth_gasPrice', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_estimateGas', + params: [ + { + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + to: '0x08f137f335ea1b8f193b8f6ea92561a60d23a211', + value: '0x0', + gas: '0x0', + }, + ], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_getTransactionCount', + params: ['0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', '0x1'], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_sendRawTransaction', + params: [ + '0x02e005010101019408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + ], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_getTransactionReceipt', + params: ['0x1'], + }, + response: { + result: { + blockHash: '0x1', + blockNumber: '0x3', // we need at least 2 blocks mocked since the first one is used for when the blockTracker is instantied before we have listeners + status: '0x1', // 0x1 = success + }, + }, + }, + ], + }); + const { transactionController, approvalController } = + await newController({}); + const { result, transactionMeta } = + await transactionController.addTransaction( + { + from: ACCOUNT_MOCK, + to: ACCOUNT_2_MOCK, + }, + { networkClientId: 'goerli' }, + ); + + await approvalController.accept(transactionMeta.id); + await advanceTime({ clock, duration: 1 }); + + await result; + // blocktracker polling is 20s + await advanceTime({ clock, duration: 20000 }); + await advanceTime({ clock, duration: 1 }); + await advanceTime({ clock, duration: 1 }); + + expect(transactionController.state.transactions).toHaveLength(1); + expect(transactionController.state.transactions[0].status).toBe( + 'confirmed', + ); + clock.restore(); + }); + it('should be able to cancel a transaction', async () => { + mockNetwork({ + networkClientConfiguration: mainnetNetworkClientConfiguration, + mocks: [ + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3b3301', + }, + }, + { + request: { + method: 'eth_getBlockByNumber', + params: ['0x3b3301'], + }, + response: { + result: { + baseFeePerGas: '0x63c498a46', + number: '0x3b3301', + }, + }, + }, + { + request: { + method: 'eth_getTransactionCount', + params: [ + '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + '0x3b3301', + ], + }, + response: { + result: '0x1', + }, + }, + ], + }); + mockNetwork({ + networkClientConfiguration, + mocks: [ + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3', + }, + }, + { + request: { + method: 'eth_getBlockByHash', + params: ['0x1', false], + }, + response: { + result: { + transactions: [], + }, + }, + }, + { + request: { + method: 'eth_getCode', + params: [ACCOUNT_2_MOCK, '0x1'], + }, + response: { + result: + // what should this be? + '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024657468546f546f6b656e53776170496e7075742875696e743235362c75696e743235362900000000000000000000000000000000000000000000000000000000', + }, + }, + { + request: { + method: 'eth_getBlockByNumber', + params: ['0x1', false], + }, + response: { + result: { + baseFeePerGas: '0x63c498a46', + number: '0x1', + }, + }, + }, + { + request: { + method: 'eth_gasPrice', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_estimateGas', + params: [ + { + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + to: '0x08f137f335ea1b8f193b8f6ea92561a60d23a211', + value: '0x0', + gas: '0x0', + }, + ], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_getTransactionCount', + params: ['0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', '0x1'], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_sendRawTransaction', + params: [ + '0x02e005010101019408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + ], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_getTransactionReceipt', + params: ['0x1'], + }, + response: { + result: { + blockHash: '0x1', + blockNumber: '0x3', // we need at least 2 blocks mocked since the first one is used for when the blockTracker is instantied before we have listeners + status: '0x1', // 0x1 = success + }, + }, + }, + { + request: { + method: 'eth_sendRawTransaction', + params: [ + '0x02e00501010101946bf137f335ea1b8f193b8f6ea92561a60d23a2078080c0808080', + ], + }, + response: { + result: '0x2', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x2', + }, + }, + { + request: { + method: 'eth_getTransactionReceipt', + params: ['0x2'], + }, + response: { + result: { + blockHash: '0x1', + blockNumber: '0x3', // we need at least 2 blocks mocked since the first one is used for when the blockTracker is instantied before we have listeners + status: '0x1', // 0x1 = success + }, + }, + }, + ], + }); + const { transactionController, approvalController } = + await newController({}); + const { result, transactionMeta } = + await transactionController.addTransaction( + { + from: ACCOUNT_MOCK, + to: ACCOUNT_2_MOCK, + }, + { networkClientId: 'goerli' }, + ); + + await approvalController.accept(transactionMeta.id); + + await result; + + await transactionController.stopTransaction(transactionMeta.id); + transactionController.stopTrackingByNetworkClientId('goerli'); + + expect(transactionController.state.transactions).toHaveLength(2); + expect(transactionController.state.transactions[1].status).toBe( + 'submitted', + ); + }); + it('should be able to confirm a cancelled transaction', async () => { + const clock = useFakeTimers(); + mockNetwork({ + networkClientConfiguration: mainnetNetworkClientConfiguration, + mocks: [ + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3b3301', + }, + }, + { + request: { + method: 'eth_getBlockByNumber', + params: ['0x3b3301'], + }, + response: { + result: { + baseFeePerGas: '0x63c498a46', + number: '0x3b3301', + }, + }, + }, + { + request: { + method: 'eth_getTransactionCount', + params: [ + '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + '0x3b3301', + ], + }, + response: { + result: '0x1', + }, + }, + ], + }); + mockNetwork({ + networkClientConfiguration, + mocks: [ + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3', + }, + }, + { + request: { + method: 'eth_getBlockByHash', + params: ['0x1', false], + }, + response: { + result: { + transactions: [], + }, + }, + }, + { + request: { + method: 'eth_getCode', + params: [ACCOUNT_2_MOCK, '0x1'], + }, + response: { + result: + // what should this be? + '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024657468546f546f6b656e53776170496e7075742875696e743235362c75696e743235362900000000000000000000000000000000000000000000000000000000', + }, + }, + { + request: { + method: 'eth_getBlockByNumber', + params: ['0x1', false], + }, + response: { + result: { + baseFeePerGas: '0x63c498a46', + number: '0x1', + }, + }, + }, + { + request: { + method: 'eth_gasPrice', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_estimateGas', + params: [ + { + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + to: '0x08f137f335ea1b8f193b8f6ea92561a60d23a211', + value: '0x0', + gas: '0x0', + }, + ], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_getTransactionCount', + params: ['0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', '0x1'], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_sendRawTransaction', + params: [ + '0x02e005010101019408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + ], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_getTransactionReceipt', + params: ['0x1'], + }, + response: { + result: null, + }, + }, + { + request: { + method: 'eth_getTransactionReceipt', + params: ['0x1'], + }, + response: { + result: null, + }, + }, + { + request: { + method: 'eth_sendRawTransaction', + params: [ + '0x02e00501010101946bf137f335ea1b8f193b8f6ea92561a60d23a2078080c0808080', + ], + }, + response: { + result: '0x2', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x4', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x4', + }, + }, + { + request: { + method: 'eth_getTransactionReceipt', + params: ['0x2'], + }, + response: { + result: { + blockHash: '0x2', + blockNumber: '0x4', // we need at least 2 blocks mocked since the first one is used for when the blockTracker is instantied before we have listeners + status: '0x1', // 0x1 = success + }, + }, + }, + { + request: { + method: 'eth_getBlockByHash', + params: ['0x2', false], + }, + response: { + result: { + transactions: [], + }, + }, + }, + ], + }); + const { transactionController, approvalController } = + await newController({}); + const { result, transactionMeta } = + await transactionController.addTransaction( + { + from: ACCOUNT_MOCK, + to: ACCOUNT_2_MOCK, + }, + { networkClientId: 'goerli' }, + ); + + await approvalController.accept(transactionMeta.id); + await advanceTime({ clock, duration: 1 }); + + await result; + + await transactionController.stopTransaction(transactionMeta.id); + + // blocktracker polling is 20s + await advanceTime({ clock, duration: 20000 }); + await advanceTime({ clock, duration: 1 }); + await advanceTime({ clock, duration: 1 }); + await advanceTime({ clock, duration: 20000 }); + await advanceTime({ clock, duration: 1 }); + await advanceTime({ clock, duration: 1 }); + + expect(transactionController.state.transactions).toHaveLength(2); + expect(transactionController.state.transactions[1].status).toBe( + 'confirmed', + ); + transactionController.stopTrackingByNetworkClientId('goerli'); + }); + it('should be able to get to speedup state', async () => { + const clock = useFakeTimers(); + mockNetwork({ + networkClientConfiguration: mainnetNetworkClientConfiguration, + mocks: [ + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3b3301', + }, + }, + { + request: { + method: 'eth_getBlockByNumber', + params: ['0x3b3301'], + }, + response: { + result: { + baseFeePerGas: '0x63c498a46', + number: '0x3b3301', + }, + }, + }, + { + request: { + method: 'eth_getTransactionCount', + params: [ + '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + '0x3b3301', + ], + }, + response: { + result: '0x1', + }, + }, + ], + }); + mockNetwork({ + networkClientConfiguration, + mocks: [ + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3', + }, + }, + { + request: { + method: 'eth_getBlockByHash', + params: ['0x1', false], + }, + response: { + result: { + transactions: [], + }, + }, + }, + { + request: { + method: 'eth_getCode', + params: [ACCOUNT_2_MOCK, '0x1'], + }, + response: { + result: + // what should this be? + '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024657468546f546f6b656e53776170496e7075742875696e743235362c75696e743235362900000000000000000000000000000000000000000000000000000000', + }, + }, + { + request: { + method: 'eth_getBlockByNumber', + params: ['0x1', false], + }, + response: { + result: { + baseFeePerGas: '0x63c498a46', + number: '0x1', + }, + }, + }, + { + request: { + method: 'eth_gasPrice', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_estimateGas', + params: [ + { + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + to: '0x08f137f335ea1b8f193b8f6ea92561a60d23a211', + value: '0x0', + gas: '0x0', + }, + ], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_getTransactionCount', + params: ['0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', '0x1'], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_sendRawTransaction', + params: [ + '0x02e405018203e88203e8809408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + ], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_getTransactionReceipt', + params: ['0x1'], + }, + response: { + result: null, + }, + }, + { + request: { + method: 'eth_getTransactionReceipt', + params: ['0x1'], + }, + response: { + result: null, + }, + }, + { + request: { + method: 'eth_sendRawTransaction', + params: [ + '0x02e4050182044c82044c809408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + ], + }, + response: { + result: '0x2', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x4', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x4', + }, + }, + { + request: { + method: 'eth_getTransactionReceipt', + params: ['0x2'], + }, + response: { + result: { + blockHash: '0x2', + blockNumber: '0x4', // we need at least 2 blocks mocked since the first one is used for when the blockTracker is instantied before we have listeners + status: '0x1', // 0x1 = success + }, + }, + }, + { + request: { + method: 'eth_getBlockByHash', + params: ['0x2', false], + }, + response: { + result: { + transactions: [], + }, + }, + }, + ], + }); + const { transactionController, approvalController } = + await newController({}); + const { result, transactionMeta } = + await transactionController.addTransaction( + { + from: ACCOUNT_MOCK, + to: ACCOUNT_2_MOCK, + maxFeePerGas: '0x3e8', + }, + { networkClientId: 'goerli' }, + ); + + await approvalController.accept(transactionMeta.id); + await advanceTime({ clock, duration: 1 }); + + await result; + + await transactionController.speedUpTransaction(transactionMeta.id); + + // blocktracker polling is 20s + await advanceTime({ clock, duration: 20000 }); + await advanceTime({ clock, duration: 1 }); + await advanceTime({ clock, duration: 1 }); + await advanceTime({ clock, duration: 20000 }); + await advanceTime({ clock, duration: 1 }); + await advanceTime({ clock, duration: 1 }); + + expect(transactionController.state.transactions).toHaveLength(2); + expect(transactionController.state.transactions[1].status).toBe( + 'confirmed', + ); + const baseFee = + transactionController.state.transactions[0].txParams.maxFeePerGas; + expect( + Number( + transactionController.state.transactions[1].txParams.maxFeePerGas, + ), + ).toBeGreaterThan(Number(baseFee)); + transactionController.stopTrackingByNetworkClientId('goerli'); }); }); }); From e7ee400ae2e4cee237bb01c25c88c5a220ab7de1 Mon Sep 17 00:00:00 2001 From: jiexi Date: Thu, 18 Jan 2024 15:10:27 -0800 Subject: [PATCH 028/100] Add initial IncomingTransactionPolling integration spec (#3791) ## Explanation * Add `startIncomingTransactionPolling` spec * Add `stopIncomingTransactionPolling` spec * Add `stopAllIncomingTransactionPolling` spec * Add `updateIncomingTransactions` spec * Instantiate `EtherscanRemoteTransactionSource` per `IncomingTransactionTracker` ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- packages/transaction-controller/package.json | 1 + .../src/TransactionController.ts | 40 +- .../TransactionControllerIntegration.test.ts | 407 +++++++++++++++++- .../src/helpers/EtherscanMocks.ts | 134 ++++++ .../EtherscanRemoteTransactionSource.test.ts | 146 +------ .../src/utils/etherscan.ts | 28 +- yarn.lock | 1 + 7 files changed, 599 insertions(+), 158 deletions(-) create mode 100644 packages/transaction-controller/src/helpers/EtherscanMocks.ts diff --git a/packages/transaction-controller/package.json b/packages/transaction-controller/package.json index da569710c7..b811024c4a 100644 --- a/packages/transaction-controller/package.json +++ b/packages/transaction-controller/package.json @@ -60,6 +60,7 @@ "babel-runtime": "^6.26.0", "deepmerge": "^4.2.2", "jest": "^27.5.1", + "nock": "^13.3.1", "sinon": "^9.2.4", "ts-jest": "^27.1.4", "typedoc": "^0.24.8", diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 5a87ceb722..a0d0284575 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -190,6 +190,13 @@ export const CANCEL_RATE = 1.5; */ export const SPEED_UP_RATE = 1.1; +type IncomingTransactionOptions = { + includeTokenTransfers?: boolean; + isEnabled?: () => boolean; + queryEntireHistory?: boolean; + updateTransactions?: boolean; +}; + /** * The name of the {@link TransactionController}. */ @@ -287,6 +294,8 @@ export class TransactionController extends BaseControllerV1< private readonly speedUpMultiplier: number; + private readonly incomingTransactionOptions: IncomingTransactionOptions; + private readonly afterSign: ( transactionMeta: TransactionMeta, signedTx: TypedTransaction, @@ -314,8 +323,6 @@ export class TransactionController extends BaseControllerV1< private readonly getNetworkClientIdForDomain: SelectedNetworkController['getNetworkClientIdForDomain']; - private readonly etherscanRemoteTransactionSource: EtherscanRemoteTransactionSource; - private failTransaction( transactionMeta: TransactionMeta, error: Error, @@ -461,12 +468,7 @@ export class TransactionController extends BaseControllerV1< getPermittedAccounts: (origin?: string) => Promise; getSavedGasFees?: (chainId: Hex) => SavedGasFees | undefined; getSelectedAddress: () => string; - incomingTransactions?: { - includeTokenTransfers?: boolean; - isEnabled?: () => boolean; - queryEntireHistory?: boolean; - updateTransactions?: boolean; - }; + incomingTransactions?: IncomingTransactionOptions; messenger: TransactionControllerMessenger; onNetworkStateChange: (listener: (state: NetworkState) => void) => void; pendingTransactions?: { @@ -538,6 +540,7 @@ export class TransactionController extends BaseControllerV1< this.securityProviderRequest = securityProviderRequest; this.cancelMultiplier = cancelMultiplier ?? CANCEL_RATE; this.speedUpMultiplier = speedUpMultiplier ?? SPEED_UP_RATE; + this.incomingTransactionOptions = incomingTransactions; this.getNetworkClientIdForDomain = getNetworkClientIdForDomain; @@ -565,7 +568,7 @@ export class TransactionController extends BaseControllerV1< ), }); - this.etherscanRemoteTransactionSource = + const etherscanRemoteTransactionSource = new EtherscanRemoteTransactionSource({ includeTokenTransfers: incomingTransactions.includeTokenTransfers, }); @@ -577,7 +580,7 @@ export class TransactionController extends BaseControllerV1< getNetworkState, isEnabled: incomingTransactions.isEnabled, queryEntireHistory: incomingTransactions.queryEntireHistory, - remoteTransactionSource: this.etherscanRemoteTransactionSource, + remoteTransactionSource: etherscanRemoteTransactionSource, transactionLimit: this.config.txHistoryLimit, updateTransactions: incomingTransactions.updateTransactions, }); @@ -1298,6 +1301,7 @@ export class TransactionController extends BaseControllerV1< } // NOTE(JL): Should this be private? + // TODO(JL): This should be idempotent startTrackingByNetworkClientId(networkClientId: NetworkClientId) { const networkClient = this.getNetworkClientById(networkClientId); const { chainId } = networkClient.configuration; @@ -1320,6 +1324,12 @@ export class TransactionController extends BaseControllerV1< TransactionStatus.confirmed, ), }); + + const etherscanRemoteTransactionSource = + new EtherscanRemoteTransactionSource({ + includeTokenTransfers: + this.incomingTransactionOptions.includeTokenTransfers, + }); const incomingTransactionHelper = new IncomingTransactionHelper({ blockTracker: networkClient.blockTracker, getCurrentAccount: this.getSelectedAddress, @@ -1330,11 +1340,11 @@ export class TransactionController extends BaseControllerV1< providerConfig: { chainId } as ProviderConfig, } as NetworkState; }, - isEnabled: () => true, - queryEntireHistory: true, - remoteTransactionSource: this.etherscanRemoteTransactionSource, + isEnabled: this.incomingTransactionOptions.isEnabled, + queryEntireHistory: this.incomingTransactionOptions.queryEntireHistory, + remoteTransactionSource: etherscanRemoteTransactionSource, transactionLimit: this.config.txHistoryLimit, - updateTransactions: true, + updateTransactions: this.incomingTransactionOptions.updateTransactions, }); const pendingTransactionTracker = new PendingTransactionTracker({ approveTransaction: this.approveTransaction.bind(this), @@ -2707,6 +2717,8 @@ export class TransactionController extends BaseControllerV1< }; blockNumber: number; }) { + // TODO(JL): the way this object is updated in place from IncomingTransactionHelper + // is incorrect. Additionally there may be a state clobbering issue we need to investigate still. this.update({ lastFetchedBlockNumbers }); this.hub.emit('incomingTransactionBlock', blockNumber); } diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index 2189991d28..d6de94ced6 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -5,11 +5,19 @@ import { NetworkController, NetworkClientType, } from '@metamask/network-controller'; +import nock from 'nock'; import { useFakeTimers } from 'sinon'; import { advanceTime } from '../../../tests/helpers'; import { mockNetwork } from '../../../tests/mock-network'; +import { + ETHERSCAN_TRANSACTION_BASE_MOCK, + ETHERSCAN_TRANSACTION_RESPONSE_MOCK, +} from './helpers/EtherscanMocks'; import { TransactionController } from './TransactionController'; +import type { TransactionMeta } from './types'; +import { TransactionStatus, TransactionType } from './types'; +import { getEtherscanApiHost } from './utils/etherscan'; const ACCOUNT_MOCK = '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207'; const ACCOUNT_2_MOCK = '0x08f137f335ea1b8f193b8f6ea92561a60d23a211'; @@ -54,8 +62,8 @@ const newController = async (options: any) => { const opts = { provider, - messenger, blockTracker, + messenger, onNetworkStateChange: () => { // noop }, @@ -68,6 +76,7 @@ const newController = async (options: any) => { getNetworkClientById: networkController.getNetworkClientById.bind(networkController), getNetworkState: () => networkController.state, + getSelectedAddress: () => '0xdeadbeef', ...options, }; const transactionController = new TransactionController(opts, { @@ -1060,6 +1069,7 @@ describe('TransactionController Integration', () => { 'confirmed', ); transactionController.stopTrackingByNetworkClientId('goerli'); + clock.restore(); }); it('should be able to get to speedup state', async () => { const clock = useFakeTimers(); @@ -1313,7 +1323,402 @@ describe('TransactionController Integration', () => { ), ).toBeGreaterThan(Number(baseFee)); transactionController.stopTrackingByNetworkClientId('goerli'); + clock.restore(); + }); + }); + }); + + describe('startIncomingTransactionPolling', () => { + // TODO(JL): IncomingTransactionHelper doesn't populate networkClientId on the generated tx object. Should it?.. + it('should add incoming transactions to state with the correct chainId for the given networkClientId on the next block', async () => { + const clock = useFakeTimers(); + // this is needed or the globally selected mainnet PollingBlockTracker makes this test fail + mockNetwork({ + networkClientConfiguration: mainnetNetworkClientConfiguration, + mocks: [ + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x2', + }, + }, + ], + }); + + const selectedAddress = ETHERSCAN_TRANSACTION_BASE_MOCK.to; + + const { networkController, transactionController } = await newController({ + getSelectedAddress: () => selectedAddress, + }); + + const expectedLastFetchedBlockNumbers: Record = {}; + const expectedTransactions: Partial[] = []; + + const networkClients = networkController.getNetworkClientRegistry(); + // NOTE(JL): This doesn't seem to work for the globally selected provider because of nock getting stacked on mainnet.infura.io twice + const networkClientIds = Object.keys(networkClients).filter( + (v) => v !== networkClientConfiguration.network, + ); + for (const networkClientId of networkClientIds) { + const config = networkClients[networkClientId].configuration; + mockNetwork({ + networkClientConfiguration: config, + mocks: [ + // blockTracker on instantiation + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + // blockTracker loop + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x2', + }, + }, + ], + }); + nock(getEtherscanApiHost(config.chainId)) + .get( + `/api?module=account&address=${selectedAddress}&offset=40&sort=desc&action=txlist&tag=latest&page=1`, + ) + .reply(200, ETHERSCAN_TRANSACTION_RESPONSE_MOCK); + + transactionController.startIncomingTransactionPolling([ + networkClientId, + ]); + + expectedLastFetchedBlockNumbers[ + `${config.chainId}#${selectedAddress}#normal` + ] = parseInt(ETHERSCAN_TRANSACTION_BASE_MOCK.blockNumber, 10); + expectedTransactions.push({ + blockNumber: ETHERSCAN_TRANSACTION_BASE_MOCK.blockNumber, + chainId: config.chainId, + type: TransactionType.incoming, + verifiedOnBlockchain: false, + status: TransactionStatus.confirmed, + }); + expectedTransactions.push({ + blockNumber: ETHERSCAN_TRANSACTION_BASE_MOCK.blockNumber, + chainId: config.chainId, + type: TransactionType.incoming, + verifiedOnBlockchain: false, + status: TransactionStatus.failed, + }); + } + await advanceTime({ clock, duration: 20000 }); + + expect(transactionController.state.transactions).toHaveLength( + 2 * networkClientIds.length, + ); + expect(transactionController.state.transactions).toStrictEqual( + expect.arrayContaining( + expectedTransactions.map(expect.objectContaining), + ), + ); + expect(transactionController.state.lastFetchedBlockNumbers).toStrictEqual( + expectedLastFetchedBlockNumbers, + ); + clock.restore(); + }); + }); + + describe('stopIncomingTransactionPolling', () => { + it('should not poll for new incoming transactions for the given networkClientId', async () => { + const clock = useFakeTimers(); + // this is needed or the globally selected mainnet PollingBlockTracker makes this test fail + mockNetwork({ + networkClientConfiguration: mainnetNetworkClientConfiguration, + mocks: [ + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + ], + }); + + const selectedAddress = ETHERSCAN_TRANSACTION_BASE_MOCK.to; + + const { networkController, transactionController } = await newController({ + getSelectedAddress: () => selectedAddress, + }); + + const networkClients = networkController.getNetworkClientRegistry(); + // NOTE(JL): This doesn't seem to work for the globally selected provider because of nock getting stacked on mainnet.infura.io twice + const networkClientIds = Object.keys(networkClients).filter( + (v) => v !== networkClientConfiguration.network, + ); + for (const networkClientId of networkClientIds) { + const config = networkClients[networkClientId].configuration; + mockNetwork({ + networkClientConfiguration: config, + mocks: [ + // blockTracker on instantiation + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + // blockTracker loop + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x2', + }, + }, + ], + }); + nock(getEtherscanApiHost(config.chainId)) + .get( + `/api?module=account&address=${selectedAddress}&offset=40&sort=desc&action=txlist&tag=latest&page=1`, + ) + .reply(200, ETHERSCAN_TRANSACTION_RESPONSE_MOCK); + + transactionController.startIncomingTransactionPolling([ + networkClientId, + ]); + + transactionController.stopIncomingTransactionPolling([networkClientId]); + } + await advanceTime({ clock, duration: 20000 }); + + expect(transactionController.state.transactions).toStrictEqual([]); + expect(transactionController.state.lastFetchedBlockNumbers).toStrictEqual( + {}, + ); + clock.restore(); + }); + }); + + describe('stopAllIncomingTransactionPolling', () => { + it('should not poll for incoming transactions on any network client', async () => { + const clock = useFakeTimers(); + // this is needed or the globally selected mainnet PollingBlockTracker makes this test fail + mockNetwork({ + networkClientConfiguration: mainnetNetworkClientConfiguration, + mocks: [ + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + ], + }); + + const selectedAddress = ETHERSCAN_TRANSACTION_BASE_MOCK.to; + + const { networkController, transactionController } = await newController({ + getSelectedAddress: () => selectedAddress, }); + + const networkClients = networkController.getNetworkClientRegistry(); + // NOTE(JL): This doesn't seem to work for the globally selected provider because of nock getting stacked on mainnet.infura.io twice + const networkClientIds = Object.keys(networkClients).filter( + (v) => v !== networkClientConfiguration.network, + ); + for (const networkClientId of networkClientIds) { + const config = networkClients[networkClientId].configuration; + mockNetwork({ + networkClientConfiguration: config, + mocks: [ + // blockTracker on instantiation + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + // blockTracker loop + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x2', + }, + }, + ], + }); + nock(getEtherscanApiHost(config.chainId)) + .get( + `/api?module=account&address=${selectedAddress}&offset=40&sort=desc&action=txlist&tag=latest&page=1`, + ) + .reply(200, ETHERSCAN_TRANSACTION_RESPONSE_MOCK); + + transactionController.startIncomingTransactionPolling([ + networkClientId, + ]); + } + + transactionController.stopAllIncomingTransactionPolling(); + await advanceTime({ clock, duration: 20000 }); + + expect(transactionController.state.transactions).toStrictEqual([]); + expect(transactionController.state.lastFetchedBlockNumbers).toStrictEqual( + {}, + ); + clock.restore(); + }); + }); + + describe('updateIncomingTransactions', () => { + it('should add incoming transactions to state with the correct chainId for the given networkClientId without waiting for the next block', async () => { + const clock = useFakeTimers(); + // this is needed or the globally selected mainnet PollingBlockTracker makes this test fail + mockNetwork({ + networkClientConfiguration: mainnetNetworkClientConfiguration, + mocks: [ + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + ], + }); + + const selectedAddress = ETHERSCAN_TRANSACTION_BASE_MOCK.to; + + const { networkController, transactionController } = await newController({ + getSelectedAddress: () => selectedAddress, + }); + + const expectedLastFetchedBlockNumbers: Record = {}; + const expectedTransactions: Partial[] = []; + + const networkClients = networkController.getNetworkClientRegistry(); + // NOTE(JL): This doesn't seem to work for the globally selected provider because of nock getting stacked on mainnet.infura.io twice + const networkClientIds = Object.keys(networkClients).filter( + (v) => v !== networkClientConfiguration.network, + ); + for (const networkClientId of networkClientIds) { + const config = networkClients[networkClientId].configuration; + mockNetwork({ + networkClientConfiguration: config, + mocks: [ + // blockTracker on instantiation + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + ], + }); + nock(getEtherscanApiHost(config.chainId)) + .get( + `/api?module=account&address=${selectedAddress}&offset=40&sort=desc&action=txlist&tag=latest&page=1`, + ) + .reply(200, ETHERSCAN_TRANSACTION_RESPONSE_MOCK); + + await transactionController.updateIncomingTransactions([ + networkClientId, + ]); + + expectedLastFetchedBlockNumbers[ + `${config.chainId}#${selectedAddress}#normal` + ] = parseInt(ETHERSCAN_TRANSACTION_BASE_MOCK.blockNumber, 10); + expectedTransactions.push({ + blockNumber: ETHERSCAN_TRANSACTION_BASE_MOCK.blockNumber, + chainId: config.chainId, + type: TransactionType.incoming, + verifiedOnBlockchain: false, + status: TransactionStatus.confirmed, + }); + expectedTransactions.push({ + blockNumber: ETHERSCAN_TRANSACTION_BASE_MOCK.blockNumber, + chainId: config.chainId, + type: TransactionType.incoming, + verifiedOnBlockchain: false, + status: TransactionStatus.failed, + }); + } + + expect(transactionController.state.transactions).toHaveLength( + 2 * networkClientIds.length, + ); + expect(transactionController.state.transactions).toStrictEqual( + expect.arrayContaining( + expectedTransactions.map(expect.objectContaining), + ), + ); + expect(transactionController.state.lastFetchedBlockNumbers).toStrictEqual( + expectedLastFetchedBlockNumbers, + ); + clock.restore(); }); }); }); diff --git a/packages/transaction-controller/src/helpers/EtherscanMocks.ts b/packages/transaction-controller/src/helpers/EtherscanMocks.ts new file mode 100644 index 0000000000..0cdeaa2541 --- /dev/null +++ b/packages/transaction-controller/src/helpers/EtherscanMocks.ts @@ -0,0 +1,134 @@ +import { TransactionStatus, TransactionType } from '../types'; +import type { + EtherscanTokenTransactionMeta, + EtherscanTransactionMeta, + EtherscanTransactionMetaBase, + EtherscanTransactionResponse, +} from '../utils/etherscan'; + +export const ID_MOCK = '6843ba00-f4bf-11e8-a715-5f2fff84549d'; + +export const ETHERSCAN_TRANSACTION_BASE_MOCK: EtherscanTransactionMetaBase = { + blockNumber: '4535105', + confirmations: '4', + contractAddress: '', + cumulativeGasUsed: '693910', + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + gas: '335208', + gasPrice: '20000000000', + gasUsed: '21000', + hash: '0x342e9d73e10004af41d04973339fc7219dbadcbb5629730cfe65e9f9cb15ff91', + nonce: '1', + timeStamp: '1543596356', + transactionIndex: '13', + value: '50000000000000000', + blockHash: '0x0000000001', + to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', +}; + +export const ETHERSCAN_TRANSACTION_SUCCESS_MOCK: EtherscanTransactionMeta = { + ...ETHERSCAN_TRANSACTION_BASE_MOCK, + functionName: 'testFunction', + input: '0x', + isError: '0', + methodId: 'testId', + txreceipt_status: '1', +}; + +const ETHERSCAN_TRANSACTION_ERROR_MOCK: EtherscanTransactionMeta = { + ...ETHERSCAN_TRANSACTION_SUCCESS_MOCK, + isError: '1', +}; + +const ETHERSCAN_TOKEN_TRANSACTION_MOCK: EtherscanTokenTransactionMeta = { + ...ETHERSCAN_TRANSACTION_BASE_MOCK, + tokenDecimal: '456', + tokenName: 'TestToken', + tokenSymbol: 'ABC', +}; + +export const ETHERSCAN_TRANSACTION_RESPONSE_MOCK: EtherscanTransactionResponse = + { + status: '1', + result: [ + ETHERSCAN_TRANSACTION_SUCCESS_MOCK, + ETHERSCAN_TRANSACTION_ERROR_MOCK, + ], + }; + +export const ETHERSCAN_TOKEN_TRANSACTION_RESPONSE_MOCK: EtherscanTransactionResponse = + { + status: '1', + result: [ + ETHERSCAN_TOKEN_TRANSACTION_MOCK, + ETHERSCAN_TOKEN_TRANSACTION_MOCK, + ], + }; + +export const ETHERSCAN_TRANSACTION_RESPONSE_EMPTY_MOCK: EtherscanTransactionResponse = + { + status: '0', + result: '', + }; + +export const ETHERSCAN_TOKEN_TRANSACTION_RESPONSE_EMPTY_MOCK: EtherscanTransactionResponse = + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ETHERSCAN_TRANSACTION_RESPONSE_EMPTY_MOCK as any; + +export const ETHERSCAN_TRANSACTION_RESPONSE_ERROR_MOCK: EtherscanTransactionResponse = + { + status: '0', + message: 'NOTOK', + result: 'Test Error', + }; + +export const ETHERSCAN_TOKEN_TRANSACTION_RESPONSE_ERROR_MOCK: EtherscanTransactionResponse = + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ETHERSCAN_TRANSACTION_RESPONSE_ERROR_MOCK as any; + +const EXPECTED_NORMALISED_TRANSACTION_BASE = { + blockNumber: ETHERSCAN_TRANSACTION_SUCCESS_MOCK.blockNumber, + chainId: undefined, + hash: ETHERSCAN_TRANSACTION_SUCCESS_MOCK.hash, + id: ID_MOCK, + status: TransactionStatus.confirmed, + time: 1543596356000, + txParams: { + chainId: undefined, + from: ETHERSCAN_TRANSACTION_SUCCESS_MOCK.from, + gas: '0x51d68', + gasPrice: '0x4a817c800', + gasUsed: '0x5208', + nonce: '0x1', + to: ETHERSCAN_TRANSACTION_SUCCESS_MOCK.to, + value: '0xb1a2bc2ec50000', + }, + type: TransactionType.incoming, + verifiedOnBlockchain: false, +}; + +export const EXPECTED_NORMALISED_TRANSACTION_SUCCESS = { + ...EXPECTED_NORMALISED_TRANSACTION_BASE, + txParams: { + ...EXPECTED_NORMALISED_TRANSACTION_BASE.txParams, + data: ETHERSCAN_TRANSACTION_SUCCESS_MOCK.input, + }, +}; + +export const EXPECTED_NORMALISED_TRANSACTION_ERROR = { + ...EXPECTED_NORMALISED_TRANSACTION_SUCCESS, + error: new Error('Transaction failed'), + status: TransactionStatus.failed, +}; + +export const EXPECTED_NORMALISED_TOKEN_TRANSACTION = { + ...EXPECTED_NORMALISED_TRANSACTION_BASE, + isTransfer: true, + transferInformation: { + contractAddress: '', + decimals: Number(ETHERSCAN_TOKEN_TRANSACTION_MOCK.tokenDecimal), + symbol: ETHERSCAN_TOKEN_TRANSACTION_MOCK.tokenSymbol, + }, +}; diff --git a/packages/transaction-controller/src/helpers/EtherscanRemoteTransactionSource.test.ts b/packages/transaction-controller/src/helpers/EtherscanRemoteTransactionSource.test.ts index 617d6765f9..946f2394c3 100644 --- a/packages/transaction-controller/src/helpers/EtherscanRemoteTransactionSource.test.ts +++ b/packages/transaction-controller/src/helpers/EtherscanRemoteTransactionSource.test.ts @@ -1,17 +1,22 @@ import { v1 as random } from 'uuid'; import { CHAIN_IDS } from '../constants'; -import { TransactionStatus, TransactionType } from '../types'; -import type { - EtherscanTokenTransactionMeta, - EtherscanTransactionMeta, - EtherscanTransactionMetaBase, - EtherscanTransactionResponse, -} from '../utils/etherscan'; import { fetchEtherscanTokenTransactions, fetchEtherscanTransactions, } from '../utils/etherscan'; +import { + ID_MOCK, + ETHERSCAN_TRANSACTION_RESPONSE_EMPTY_MOCK, + ETHERSCAN_TOKEN_TRANSACTION_RESPONSE_EMPTY_MOCK, + ETHERSCAN_TRANSACTION_RESPONSE_MOCK, + EXPECTED_NORMALISED_TRANSACTION_SUCCESS, + EXPECTED_NORMALISED_TRANSACTION_ERROR, + ETHERSCAN_TOKEN_TRANSACTION_RESPONSE_MOCK, + EXPECTED_NORMALISED_TOKEN_TRANSACTION, + ETHERSCAN_TOKEN_TRANSACTION_RESPONSE_ERROR_MOCK, + ETHERSCAN_TRANSACTION_RESPONSE_ERROR_MOCK, +} from './EtherscanMocks'; import { EtherscanRemoteTransactionSource } from './EtherscanRemoteTransactionSource'; jest.mock('../utils/etherscan', () => ({ @@ -21,133 +26,6 @@ jest.mock('../utils/etherscan', () => ({ jest.mock('uuid'); -const ID_MOCK = '6843ba00-f4bf-11e8-a715-5f2fff84549d'; - -const ETHERSCAN_TRANSACTION_BASE_MOCK: EtherscanTransactionMetaBase = { - blockNumber: '4535105', - confirmations: '4', - contractAddress: '', - cumulativeGasUsed: '693910', - from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - gas: '335208', - gasPrice: '20000000000', - gasUsed: '21000', - hash: '0x342e9d73e10004af41d04973339fc7219dbadcbb5629730cfe65e9f9cb15ff91', - nonce: '1', - timeStamp: '1543596356', - transactionIndex: '13', - value: '50000000000000000', - blockHash: '0x0000000001', - to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', -}; - -const ETHERSCAN_TRANSACTION_SUCCESS_MOCK: EtherscanTransactionMeta = { - ...ETHERSCAN_TRANSACTION_BASE_MOCK, - functionName: 'testFunction', - input: '0x', - isError: '0', - methodId: 'testId', - txreceipt_status: '1', -}; - -const ETHERSCAN_TRANSACTION_ERROR_MOCK: EtherscanTransactionMeta = { - ...ETHERSCAN_TRANSACTION_SUCCESS_MOCK, - isError: '1', -}; - -const ETHERSCAN_TOKEN_TRANSACTION_MOCK: EtherscanTokenTransactionMeta = { - ...ETHERSCAN_TRANSACTION_BASE_MOCK, - tokenDecimal: '456', - tokenName: 'TestToken', - tokenSymbol: 'ABC', -}; - -const ETHERSCAN_TRANSACTION_RESPONSE_MOCK: EtherscanTransactionResponse = - { - status: '1', - result: [ - ETHERSCAN_TRANSACTION_SUCCESS_MOCK, - ETHERSCAN_TRANSACTION_ERROR_MOCK, - ], - }; - -const ETHERSCAN_TOKEN_TRANSACTION_RESPONSE_MOCK: EtherscanTransactionResponse = - { - status: '1', - result: [ - ETHERSCAN_TOKEN_TRANSACTION_MOCK, - ETHERSCAN_TOKEN_TRANSACTION_MOCK, - ], - }; - -const ETHERSCAN_TRANSACTION_RESPONSE_EMPTY_MOCK: EtherscanTransactionResponse = - { - status: '0', - result: '', - }; - -const ETHERSCAN_TOKEN_TRANSACTION_RESPONSE_EMPTY_MOCK: EtherscanTransactionResponse = - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ETHERSCAN_TRANSACTION_RESPONSE_EMPTY_MOCK as any; - -const ETHERSCAN_TRANSACTION_RESPONSE_ERROR_MOCK: EtherscanTransactionResponse = - { - status: '0', - message: 'NOTOK', - result: 'Test Error', - }; - -const ETHERSCAN_TOKEN_TRANSACTION_RESPONSE_ERROR_MOCK: EtherscanTransactionResponse = - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ETHERSCAN_TRANSACTION_RESPONSE_ERROR_MOCK as any; - -const EXPECTED_NORMALISED_TRANSACTION_BASE = { - blockNumber: ETHERSCAN_TRANSACTION_SUCCESS_MOCK.blockNumber, - chainId: undefined, - hash: ETHERSCAN_TRANSACTION_SUCCESS_MOCK.hash, - id: ID_MOCK, - status: TransactionStatus.confirmed, - time: 1543596356000, - txParams: { - chainId: undefined, - from: ETHERSCAN_TRANSACTION_SUCCESS_MOCK.from, - gas: '0x51d68', - gasPrice: '0x4a817c800', - gasUsed: '0x5208', - nonce: '0x1', - to: ETHERSCAN_TRANSACTION_SUCCESS_MOCK.to, - value: '0xb1a2bc2ec50000', - }, - type: TransactionType.incoming, - verifiedOnBlockchain: false, -}; - -const EXPECTED_NORMALISED_TRANSACTION_SUCCESS = { - ...EXPECTED_NORMALISED_TRANSACTION_BASE, - txParams: { - ...EXPECTED_NORMALISED_TRANSACTION_BASE.txParams, - data: ETHERSCAN_TRANSACTION_SUCCESS_MOCK.input, - }, -}; - -const EXPECTED_NORMALISED_TRANSACTION_ERROR = { - ...EXPECTED_NORMALISED_TRANSACTION_SUCCESS, - error: new Error('Transaction failed'), - status: TransactionStatus.failed, -}; - -const EXPECTED_NORMALISED_TOKEN_TRANSACTION = { - ...EXPECTED_NORMALISED_TRANSACTION_BASE, - isTransfer: true, - transferInformation: { - contractAddress: '', - decimals: Number(ETHERSCAN_TOKEN_TRANSACTION_MOCK.tokenDecimal), - symbol: ETHERSCAN_TOKEN_TRANSACTION_MOCK.tokenSymbol, - }, -}; - describe('EtherscanRemoteTransactionSource', () => { const fetchEtherscanTransactionsMock = fetchEtherscanTransactions as jest.MockedFn< diff --git a/packages/transaction-controller/src/utils/etherscan.ts b/packages/transaction-controller/src/utils/etherscan.ts index ffcaec1dac..e4fed4a5c2 100644 --- a/packages/transaction-controller/src/utils/etherscan.ts +++ b/packages/transaction-controller/src/utils/etherscan.ts @@ -177,15 +177,7 @@ function getEtherscanApiUrl( chainId: Hex, urlParams: Record, ): string { - type SupportedChainId = keyof typeof ETHERSCAN_SUPPORTED_NETWORKS; - - const networkInfo = ETHERSCAN_SUPPORTED_NETWORKS[chainId as SupportedChainId]; - - if (!networkInfo) { - throw new Error(`Etherscan does not support chain with ID: ${chainId}`); - } - - const apiUrl = `https://${networkInfo.subdomain}.${networkInfo.domain}`; + const apiUrl = getEtherscanApiHost(chainId); let url = `${apiUrl}/api?`; for (const paramKey of Object.keys(urlParams)) { @@ -202,3 +194,21 @@ function getEtherscanApiUrl( return url; } + +/** + * Return the host url used to fetch data from Etherscan. + * + * @param chainId - Current chain ID used to determine subdomain and domain. + * @returns host URL to access Etherscan data. + */ +export function getEtherscanApiHost(chainId: Hex) { + type SupportedChainId = keyof typeof ETHERSCAN_SUPPORTED_NETWORKS; + + const networkInfo = ETHERSCAN_SUPPORTED_NETWORKS[chainId as SupportedChainId]; + + if (!networkInfo) { + throw new Error(`Etherscan does not support chain with ID: ${chainId}`); + } + + return `https://${networkInfo.subdomain}.${networkInfo.domain}`; +} diff --git a/yarn.lock b/yarn.lock index 70b3268404..81affd44dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2995,6 +2995,7 @@ __metadata: fast-json-patch: ^3.1.1 jest: ^27.5.1 lodash: ^4.17.21 + nock: ^13.3.1 nonce-tracker: ^3.0.0 sinon: ^9.2.4 ts-jest: ^27.1.4 From 41b19eea187cf57b4b5438f67bc50dddb6a834b4 Mon Sep 17 00:00:00 2001 From: jiexi Date: Fri, 19 Jan 2024 13:05:15 -0800 Subject: [PATCH 029/100] add getNonceLock tests (#3798) ## Explanation * Add scenario to `getNonceLock` to checks if the right nonceTracker is used when a networkClientId is provided * Add integration tests for `getNonceLock` that test result and that an acquired lock blocks subsequent attempts to acquire * Lint IncomingTxOptions jsdoc ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- .../src/TransactionController.test.ts | 27 +- .../src/TransactionController.ts | 13 +- .../TransactionControllerIntegration.test.ts | 255 ++++++++++++++++++ 3 files changed, 290 insertions(+), 5 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index ebc7d63a9b..7c9984b24e 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -2520,7 +2520,7 @@ describe('TransactionController', () => { }); describe('getNonceLock', () => { - it('gets the next nonce according to the nonce-tracker', async () => { + it('gets the next nonce from the globally selected nonceTracker when no networkClientId is provided', async () => { const controller = newController({ network: MOCK_LINEA_MAINNET_NETWORK, }); @@ -2531,6 +2531,31 @@ describe('TransactionController', () => { expect(getNonceLockSpy).toHaveBeenCalledWith(ACCOUNT_MOCK); expect(nextNonce).toBe(NONCE_MOCK); }); + + it('gets the next nonce from the nonceTracker for the provided networkClientId', async () => { + const controller = newController({ + network: MOCK_LINEA_MAINNET_NETWORK, + }); + + const trackingMap = controller.startTrackingByNetworkClientId( + 'customNetworkClientId-1', + ); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const { nonceTracker } = trackingMap.get('customNetworkClientId-1')!; + const nonceTrackerGetNonceLockSpy = jest.spyOn( + nonceTracker, + 'getNonceLock', + ); + + const { nextNonce } = await controller.getNonceLock( + ACCOUNT_MOCK, + 'customNetworkClientId-1', + ); + + expect(nonceTrackerGetNonceLockSpy).toHaveBeenCalledTimes(1); + expect(nonceTrackerGetNonceLockSpy).toHaveBeenCalledWith(ACCOUNT_MOCK); + expect(nextNonce).toBe(NONCE_MOCK); + }); }); describe('confirmExternalTransaction', () => { diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index a0d0284575..927684fb59 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -190,6 +190,15 @@ export const CANCEL_RATE = 1.5; */ export const SPEED_UP_RATE = 1.1; +/** + * @type IncomingTransactionOptions + * + * Configuration options for incoming transaction support + * @property includeTokenTransfers - Whether or not to include ERC20 token transfers. + * @property isEnabled - Whether or not incoming transaction retrieval is enabled. + * @property queryEntireHistory - Whether to initially query the entire transaction history or only recent blocks. + * @property updateTransactions - Whether to update local transactions using remote transaction data. + */ type IncomingTransactionOptions = { includeTokenTransfers?: boolean; isEnabled?: () => boolean; @@ -398,10 +407,6 @@ export class TransactionController extends BaseControllerV1< * @param options.getSavedGasFees - Gets the saved gas fee config. * @param options.getSelectedAddress - Gets the address of the currently selected account. * @param options.incomingTransactions - Configuration options for incoming transaction support. - * @param options.incomingTransactions.includeTokenTransfers - Whether or not to include ERC20 token transfers. - * @param options.incomingTransactions.isEnabled - Whether or not incoming transaction retrieval is enabled. - * @param options.incomingTransactions.queryEntireHistory - Whether to initially query the entire transaction history or only recent blocks. - * @param options.incomingTransactions.updateTransactions - Whether to update local transactions using remote transaction data. * @param options.messenger - The controller messenger. * @param options.onNetworkStateChange - Allows subscribing to network controller state changes. * @param options.pendingTransactions - Configuration options for pending transaction support. diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index d6de94ced6..038c513962 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -1721,4 +1721,259 @@ describe('TransactionController Integration', () => { clock.restore(); }); }); + + describe('getNonceLock', () => { + it('should get the nonce lock from the nonceTracker for the given networkClientId', async () => { + mockNetwork({ + networkClientConfiguration: mainnetNetworkClientConfiguration, + mocks: [ + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + ], + }); + + const { networkController, transactionController } = await newController( + {}, + ); + + const networkClients = networkController.getNetworkClientRegistry(); + // NOTE(JL): This doesn't seem to work for the globally selected provider because of nock getting stacked on mainnet.infura.io twice + const networkClientIds = Object.keys(networkClients).filter( + (v) => v !== networkClientConfiguration.network, + ); + for (const networkClientId of networkClientIds) { + const config = networkClients[networkClientId].configuration; + mockNetwork({ + networkClientConfiguration: config, + mocks: [ + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + // NonceTracker + { + request: { + method: 'eth_getTransactionCount', + params: [ACCOUNT_MOCK, '0x1'], + }, + response: { + result: '0xa', + }, + }, + ], + }); + + const nonceLock = await transactionController.getNonceLock( + ACCOUNT_MOCK, + networkClientId, + ); + expect(nonceLock.nextNonce).toBe(10); + } + }); + + it('should block other attempts to get the nonce lock from the nonceTracker until the first one is released for the given networkClientId', async () => { + mockNetwork({ + networkClientConfiguration: mainnetNetworkClientConfiguration, + mocks: [ + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + ], + }); + + const { networkController, transactionController } = await newController( + {}, + ); + + const networkClients = networkController.getNetworkClientRegistry(); + // NOTE(JL): This doesn't seem to work for the globally selected provider because of nock getting stacked on mainnet.infura.io twice + const networkClientIds = Object.keys(networkClients).filter( + (v) => v !== networkClientConfiguration.network, + ); + for (const networkClientId of networkClientIds) { + const config = networkClients[networkClientId].configuration; + mockNetwork({ + networkClientConfiguration: config, + mocks: [ + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + // NonceTracker + { + request: { + method: 'eth_getTransactionCount', + params: [ACCOUNT_MOCK, '0x1'], + }, + response: { + result: '0xa', + }, + }, + ], + }); + + const firstNonceLock = await transactionController.getNonceLock( + ACCOUNT_MOCK, + networkClientId, + ); + + expect(firstNonceLock.nextNonce).toBe(10); + + const secondNonceLock = transactionController.getNonceLock( + ACCOUNT_MOCK, + networkClientId, + ); + const delay = () => + new Promise((resolve) => { + setTimeout(resolve, 100, null); + }); + + let secondNonceLockIfAcquired = await Promise.race([ + secondNonceLock, + delay(), + ]); + expect(secondNonceLockIfAcquired).toBeNull(); + + await firstNonceLock.releaseLock(); + + secondNonceLockIfAcquired = await Promise.race([ + secondNonceLock, + delay(), + ]); + expect(secondNonceLockIfAcquired?.nextNonce).toBe(10); + } + }); + + it('should get the nonce lock from the globally selected nonceTracker if no networkClientId is provided', async () => { + mockNetwork({ + networkClientConfiguration: mainnetNetworkClientConfiguration, + mocks: [ + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + // NonceTracker + { + request: { + method: 'eth_getTransactionCount', + params: [ACCOUNT_MOCK, '0x1'], + }, + response: { + result: '0xa', + }, + }, + ], + }); + + const { transactionController } = await newController({}); + + const nonceLock = await transactionController.getNonceLock(ACCOUNT_MOCK); + expect(nonceLock.nextNonce).toBe(10); + }); + + it('should block other attempts to get the nonce lock from the globally selected nonceTracker until the first one is released if no networkClientId is provided', async () => { + mockNetwork({ + networkClientConfiguration: mainnetNetworkClientConfiguration, + mocks: [ + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + // NonceTracker + { + request: { + method: 'eth_getTransactionCount', + params: [ACCOUNT_MOCK, '0x1'], + }, + response: { + result: '0xa', + }, + }, + ], + }); + + const { transactionController } = await newController({}); + + const firstNonceLock = await transactionController.getNonceLock( + ACCOUNT_MOCK, + ); + + expect(firstNonceLock.nextNonce).toBe(10); + + const secondNonceLock = transactionController.getNonceLock(ACCOUNT_MOCK); + const delay = () => + new Promise((resolve) => { + setTimeout(resolve, 100, null); + }); + + let secondNonceLockIfAcquired = await Promise.race([ + secondNonceLock, + delay(), + ]); + expect(secondNonceLockIfAcquired).toBeNull(); + + await firstNonceLock.releaseLock(); + + secondNonceLockIfAcquired = await Promise.race([ + secondNonceLock, + delay(), + ]); + expect(secondNonceLockIfAcquired?.nextNonce).toBe(10); + }); + }); }); From 9b716b352f55253f3863a5c8dbd92cd1477fcff9 Mon Sep 17 00:00:00 2001 From: jiexi Date: Tue, 23 Jan 2024 07:37:46 -0800 Subject: [PATCH 030/100] Jl/transaction multichain nonce lock tx (#3814) ## Explanation * Add scenario to test nonce of two concurrent transactions being added on the same chainId from different networkClients * Add scenario to test nonce of two concurrent transactions from the same networkClient ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --------- Co-authored-by: Alex Donesky --- .../TransactionControllerIntegration.test.ts | 575 +++++++++++++++++- 1 file changed, 574 insertions(+), 1 deletion(-) diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index 038c513962..4f7eed7416 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -1,6 +1,6 @@ import { ApprovalController } from '@metamask/approval-controller'; import { ControllerMessenger } from '@metamask/base-controller'; -import { BUILT_IN_NETWORKS, NetworkType } from '@metamask/controller-utils'; +import { ApprovalType, BUILT_IN_NETWORKS, NetworkType } from '@metamask/controller-utils'; import { NetworkController, NetworkClientType, @@ -21,6 +21,7 @@ import { getEtherscanApiHost } from './utils/etherscan'; const ACCOUNT_MOCK = '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207'; const ACCOUNT_2_MOCK = '0x08f137f335ea1b8f193b8f6ea92561a60d23a211'; +const ACCOUNT_3_MOCK = '0xe688b84b23f322a994a53dbf8e15fa82cdb71127'; const infuraProjectId = '341eacb578dd44a1a049cbc5f6fd4035'; const networkClientConfiguration = { @@ -58,6 +59,9 @@ const newController = async (options: any) => { name: 'ApprovalController', }), showApprovalRequest: jest.fn(), + typesExcludedFromRateLimiting: [ + ApprovalType.Transaction, + ] }); const opts = { @@ -1326,6 +1330,575 @@ describe('TransactionController Integration', () => { clock.restore(); }); }); + + describe('when transactions are added concurrently with different networkClientIds but on the same chainId', () => { + it('should add each transaction with consecutive nonces', async () => { + const clock = useFakeTimers(); + mockNetwork({ + networkClientConfiguration: mainnetNetworkClientConfiguration, + mocks: [ + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3b3301', + }, + }, + { + request: { + method: 'eth_getBlockByNumber', + params: ['0x3b3301'], + }, + response: { + result: { + baseFeePerGas: '0x63c498a46', + number: '0x3b3301', + }, + }, + }, + { + request: { + method: 'eth_getTransactionCount', + params: [ + '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + '0x3b3301', + ], + }, + response: { + result: '0x1', + }, + }, + ], + }); + mockNetwork({ + networkClientConfiguration, + mocks: [ + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3', + }, + }, + { + request: { + method: 'eth_getBlockByHash', + params: ['0x1', false], + }, + response: { + result: { + transactions: [], + }, + }, + }, + { + request: { + method: 'eth_getCode', + params: [ACCOUNT_2_MOCK, '0x1'], + }, + response: { + result: + // what should this be? + '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024657468546f546f6b656e53776170496e7075742875696e743235362c75696e743235362900000000000000000000000000000000000000000000000000000000', + }, + }, + { + request: { + method: 'eth_getBlockByNumber', + params: ['0x1', false], + }, + response: { + result: { + baseFeePerGas: '0x63c498a46', + number: '0x1', + }, + }, + }, + { + request: { + method: 'eth_gasPrice', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_estimateGas', + params: [ + { + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + to: '0x08f137f335ea1b8f193b8f6ea92561a60d23a211', + value: '0x0', + gas: '0x0', + }, + ], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_getTransactionCount', + params: ['0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', '0x1'], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_sendRawTransaction', + params: [ + '0x02e005010101019408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + ], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_getTransactionReceipt', + params: ['0x1'], + }, + response: { + result: { + blockHash: '0x1', + blockNumber: '0x3', // we need at least 2 blocks mocked since the first one is used for when the blockTracker is instantied before we have listeners + status: '0x1', // 0x1 = success + }, + }, + }, + ], + }); + const { approvalController, networkController, transactionController } = + await newController({ + getPermittedAccounts: () => [ACCOUNT_MOCK], + getSelectedAddress: () => ACCOUNT_MOCK, + }); + const otherNetworkClientIdOnGoerli = + await networkController.upsertNetworkConfiguration( + { + rpcUrl: 'https://mock.rpc.url', + chainId: networkClientConfiguration.chainId, + ticker: networkClientConfiguration.ticker, + }, + { + referrer: 'https://mock.referrer', + source: 'dapp', + }, + ); + + mockNetwork({ + networkClientConfiguration: { + ...networkClientConfiguration, + type: NetworkClientType.Custom, + rpcUrl: 'https://mock.rpc.url', + }, + mocks: [ + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3', + }, + }, + { + request: { + method: 'eth_getBlockByHash', + params: ['0x1', false], + }, + response: { + result: { + transactions: [], + }, + }, + }, + { + request: { + method: 'eth_getCode', + params: [ACCOUNT_2_MOCK, '0x1'], + }, + response: { + result: + // what should this be? + '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024657468546f546f6b656e53776170496e7075742875696e743235362c75696e743235362900000000000000000000000000000000000000000000000000000000', + }, + }, + { + request: { + method: 'eth_getBlockByNumber', + params: ['0x1', false], + }, + response: { + result: { + baseFeePerGas: '0x63c498a46', + number: '0x1', + }, + }, + }, + { + request: { + method: 'eth_gasPrice', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_estimateGas', + params: [ + { + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + to: '0x08f137f335ea1b8f193b8f6ea92561a60d23a211', + value: '0x0', + gas: '0x0', + }, + ], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_getTransactionCount', + params: ['0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', '0x1'], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_sendRawTransaction', + params: [ + '0x02e0050201018094e688b84b23f322a994a53dbf8e15fa82cdb711278080c0808080', + ], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_getTransactionReceipt', + params: ['0x1'], + }, + response: { + result: { + blockHash: '0x1', + blockNumber: '0x3', // we need at least 2 blocks mocked since the first one is used for when the blockTracker is instantied before we have listeners + status: '0x1', // 0x1 = success + }, + }, + }, + ], + }); + + const addTx1 = await transactionController.addTransaction( + { + from: ACCOUNT_MOCK, + to: ACCOUNT_2_MOCK, + }, + { networkClientId: 'goerli' }, + ); + + const addTx2 = await transactionController.addTransaction( + { + from: ACCOUNT_MOCK, + to: ACCOUNT_3_MOCK, + }, + { + networkClientId: otherNetworkClientIdOnGoerli, + }, + ); + + await Promise.all([ + approvalController.accept(addTx1.transactionMeta.id), + approvalController.accept(addTx2.transactionMeta.id), + ]); + await advanceTime({ clock, duration: 1 }); + + await Promise.all([addTx1.result, addTx2.result]); + transactionController.stopTrackingByNetworkClientId('goerli'); + transactionController.stopTrackingByNetworkClientId( + otherNetworkClientIdOnGoerli, + ); + + const nonces = transactionController.state.transactions + .map((tx) => tx.txParams.nonce) + .sort(); + expect(nonces).toStrictEqual(['0x1', '0x2']); + clock.restore(); + }); + }); + + describe('when transactions are added concurrently with the same networkClientId', () => { + it('should add each transaction with consecutive nonces', async () => { + const clock = useFakeTimers(); + mockNetwork({ + networkClientConfiguration: mainnetNetworkClientConfiguration, + mocks: [ + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3b3301', + }, + }, + { + request: { + method: 'eth_getBlockByNumber', + params: ['0x3b3301'], + }, + response: { + result: { + baseFeePerGas: '0x63c498a46', + number: '0x3b3301', + }, + }, + }, + { + request: { + method: 'eth_getTransactionCount', + params: [ + '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + '0x3b3301', + ], + }, + response: { + result: '0x1', + }, + }, + ], + }); + mockNetwork({ + networkClientConfiguration, + mocks: [ + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3', + }, + }, + { + request: { + method: 'eth_getBlockByHash', + params: ['0x1', false], + }, + response: { + result: { + transactions: [], + }, + }, + }, + { + request: { + method: 'eth_getCode', + params: [ACCOUNT_2_MOCK, '0x1'], + }, + response: { + result: + // what should this be? + '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024657468546f546f6b656e53776170496e7075742875696e743235362c75696e743235362900000000000000000000000000000000000000000000000000000000', + }, + }, + { + request: { + method: 'eth_getCode', + params: [ACCOUNT_3_MOCK, '0x1'], + }, + response: { + result: + // what should this be? + '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024657468546f546f6b656e53776170496e7075742875696e743235362c75696e743235362900000000000000000000000000000000000000000000000000000000', + }, + }, + { + request: { + method: 'eth_getBlockByNumber', + params: ['0x1', false], + }, + response: { + result: { + baseFeePerGas: '0x63c498a46', + number: '0x1', + }, + }, + }, + { + request: { + method: 'eth_gasPrice', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_estimateGas', + params: [ + { + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + to: '0x08f137f335ea1b8f193b8f6ea92561a60d23a211', + value: '0x0', + gas: '0x0', + }, + ], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_getTransactionCount', + params: ['0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', '0x1'], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_sendRawTransaction', + params: [ + '0x02e005010101019408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + ], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_getTransactionReceipt', + params: ['0x1'], + }, + response: { + result: { + blockHash: '0x1', + blockNumber: '0x3', // we need at least 2 blocks mocked since the first one is used for when the blockTracker is instantied before we have listeners + status: '0x1', // 0x1 = success + }, + }, + }, + { + request: { + method: 'eth_sendRawTransaction', + params: [ + '0x02e0050201018094e688b84b23f322a994a53dbf8e15fa82cdb711278080c0808080', + ], + }, + response: { + result: '0x2', + }, + }, + { + request: { + method: 'eth_getTransactionReceipt', + params: ['0x2'], + }, + response: { + result: { + blockHash: '0x2', + blockNumber: '0x4', // we need at least 2 blocks mocked since the first one is used for when the blockTracker is instantied before we have listeners + status: '0x1', // 0x1 = success + }, + }, + }, + ], + }); + const { approvalController, transactionController } = + await newController({ + getPermittedAccounts: () => [ACCOUNT_MOCK], + getSelectedAddress: () => ACCOUNT_MOCK, + }); + + const addTx1 = await transactionController.addTransaction( + { + from: ACCOUNT_MOCK, + to: ACCOUNT_2_MOCK, + }, + { networkClientId: 'goerli' }, + ); + + await advanceTime({ clock, duration: 1 }); + + const addTx2 = await transactionController.addTransaction( + { + from: ACCOUNT_MOCK, + to: ACCOUNT_3_MOCK, + }, + { + networkClientId: 'goerli', + }, + ); + + await advanceTime({ clock, duration: 1 }); + + await Promise.all([ + approvalController.accept(addTx1.transactionMeta.id), + approvalController.accept(addTx2.transactionMeta.id), + ]); + + await advanceTime({ clock, duration: 1 }); + + await Promise.all([addTx1.result, addTx2.result]); + transactionController.stopTrackingByNetworkClientId('goerli'); + + const nonces = transactionController.state.transactions + .map((tx) => tx.txParams.nonce) + .sort(); + expect(nonces).toStrictEqual(['0x1', '0x2']); + clock.restore(); + }); + }); }); describe('startIncomingTransactionPolling', () => { From 317cc06d3a84bdcfd02c9bf726ab38667ec722ab Mon Sep 17 00:00:00 2001 From: jiexi Date: Tue, 23 Jan 2024 07:38:24 -0800 Subject: [PATCH 031/100] Jl/transaction multichain doc mock requests (#3816) ## Explanation * Reorder mocked requests based on when they occur * Add comments to what method/helper/util is making the request * Remove unnecessary mocked requests * Change our `getCode` calls to return a non contract result * Update sendRawTransaction calls based on this ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- .../TransactionControllerIntegration.test.ts | 692 +++++++----------- 1 file changed, 270 insertions(+), 422 deletions(-) diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index 4f7eed7416..ce34cf564d 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -102,6 +102,8 @@ describe('TransactionController Integration', () => { mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ + // NetworkController + // BlockTracker { request: { method: 'eth_blockNumber', @@ -111,56 +113,6 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x2', - }, - }, - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3', - }, - }, - ], - }); - mockNetwork({ - networkClientConfiguration, - mocks: [ - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x2', - }, - }, - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3', - }, - }, ], }); const { transactionController } = await newController({}); @@ -174,6 +126,8 @@ describe('TransactionController Integration', () => { mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ + // NetworkController + // BlockTracker { request: { method: 'eth_blockNumber', @@ -183,35 +137,13 @@ describe('TransactionController Integration', () => { result: '0x3b3301', }, }, - { - request: { - method: 'eth_getBlockByNumber', - params: ['0x3b3301'], - }, - response: { - result: { - baseFeePerGas: '0x63c498a46', - number: '0x3b3301', - }, - }, - }, - { - request: { - method: 'eth_getTransactionCount', - params: [ - '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - '0x3b3301', - ], - }, - response: { - result: '0x1', - }, - }, ], }); mockNetwork({ networkClientConfiguration, mocks: [ + // NetworkController + // BlockTracker { request: { method: 'eth_blockNumber', @@ -221,47 +153,18 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x2', - }, - }, - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3', - }, - }, + // readAddressAsContract + // requiresFixedGas (cached) { request: { method: 'eth_getCode', params: [ACCOUNT_2_MOCK, '0x1'], }, response: { - result: - // what should this be? - '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024657468546f546f6b656e53776170496e7075742875696e743235362c75696e743235362900000000000000000000000000000000000000000000000000000000', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - params: ['0x1', false], - }, - response: { - result: { - baseFeePerGas: '0x63c498a46', - number: '0x42', - }, + result: '0x', // non contract }, }, + // getSuggestedGasFees { request: { method: 'eth_gasPrice', @@ -291,6 +194,8 @@ describe('TransactionController Integration', () => { mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ + // NetworkController + // BlockTracker { request: { method: 'eth_blockNumber', @@ -300,35 +205,13 @@ describe('TransactionController Integration', () => { result: '0x3b3301', }, }, - { - request: { - method: 'eth_getBlockByNumber', - params: ['0x3b3301'], - }, - response: { - result: { - baseFeePerGas: '0x63c498a46', - number: '0x3b3301', - }, - }, - }, - { - request: { - method: 'eth_getTransactionCount', - params: [ - '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - '0x3b3301', - ], - }, - response: { - result: '0x1', - }, - }, ], }); mockNetwork({ networkClientConfiguration, mocks: [ + // NetworkController + // BlockTracker { request: { method: 'eth_blockNumber', @@ -338,26 +221,7 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x2', - }, - }, - { - request: { - method: 'eth_getCode', - params: [ACCOUNT_2_MOCK, '0x1'], - }, - response: { - result: - // what should this be? - '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024657468546f546f6b656e53776170496e7075742875696e743235362c75696e743235362900000000000000000000000000000000000000000000000000000000', - }, - }, + // NetworkController { request: { method: 'eth_getBlockByNumber', @@ -370,15 +234,28 @@ describe('TransactionController Integration', () => { }, }, }, + // BlockTracker { request: { - method: 'eth_gasPrice', + method: 'eth_blockNumber', params: [], }, response: { - result: '0x1', + result: '0x2', + }, + }, + // readAddressAsContract + // requiresFixedGas (cached) + { + request: { + method: 'eth_getCode', + params: [ACCOUNT_2_MOCK, '0x1'], + }, + response: { + result: '0x', // non contract }, }, + // estimateGas { request: { method: 'eth_estimateGas', @@ -395,6 +272,17 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + // getSuggestedGasFees + { + request: { + method: 'eth_gasPrice', + params: [], + }, + response: { + result: '0x1', + }, + }, + // NonceTracker { request: { method: 'eth_getTransactionCount', @@ -404,11 +292,12 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + // publishTransaction { request: { method: 'eth_sendRawTransaction', params: [ - '0x02e005010101019408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + '0x02e2050101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', ], }, response: { @@ -443,6 +332,8 @@ describe('TransactionController Integration', () => { mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ + // NetworkController + // BlockTracker { request: { method: 'eth_blockNumber', @@ -452,35 +343,13 @@ describe('TransactionController Integration', () => { result: '0x3b3301', }, }, - { - request: { - method: 'eth_getBlockByNumber', - params: ['0x3b3301'], - }, - response: { - result: { - baseFeePerGas: '0x63c498a46', - number: '0x3b3301', - }, - }, - }, - { - request: { - method: 'eth_getTransactionCount', - params: [ - '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - '0x3b3301', - ], - }, - response: { - result: '0x1', - }, - }, ], }); mockNetwork({ networkClientConfiguration, mocks: [ + // NetworkController + // BlockTracker { request: { method: 'eth_blockNumber', @@ -490,37 +359,7 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3', - }, - }, - { - request: { - method: 'eth_getBlockByHash', - params: ['0x1', false], - }, - response: { - result: { - transactions: [], - }, - }, - }, - { - request: { - method: 'eth_getCode', - params: [ACCOUNT_2_MOCK, '0x1'], - }, - response: { - result: - // what should this be? - '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024657468546f546f6b656e53776170496e7075742875696e743235362c75696e743235362900000000000000000000000000000000000000000000000000000000', - }, - }, + // NetworkController { request: { method: 'eth_getBlockByNumber', @@ -533,15 +372,18 @@ describe('TransactionController Integration', () => { }, }, }, + // readAddressAsContract + // requiresFixedGas (cached) { request: { - method: 'eth_gasPrice', - params: [], + method: 'eth_getCode', + params: [ACCOUNT_2_MOCK, '0x1'], }, response: { - result: '0x1', + result: '0x', // non contract }, }, + // estimateGas { request: { method: 'eth_estimateGas', @@ -558,6 +400,17 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + // getSuggestedGasFees + { + request: { + method: 'eth_gasPrice', + params: [], + }, + response: { + result: '0x1', + }, + }, + // NonceTracker { request: { method: 'eth_getTransactionCount', @@ -567,17 +420,29 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + // publishTransaction { request: { method: 'eth_sendRawTransaction', params: [ - '0x02e005010101019408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + '0x02e2050101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', ], }, response: { result: '0x1', }, }, + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3', + }, + }, + // PendingTransactionTracker.#checkTransaction { request: { method: 'eth_getTransactionReceipt', @@ -591,6 +456,18 @@ describe('TransactionController Integration', () => { }, }, }, + // PendingTransactionTracker.#onTransactionConfirmed + { + request: { + method: 'eth_getBlockByHash', + params: ['0x1', false], + }, + response: { + result: { + transactions: [], + }, + }, + }, ], }); const { transactionController, approvalController } = @@ -623,6 +500,8 @@ describe('TransactionController Integration', () => { mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ + // NetworkController + // BlockTracker { request: { method: 'eth_blockNumber', @@ -632,35 +511,13 @@ describe('TransactionController Integration', () => { result: '0x3b3301', }, }, - { - request: { - method: 'eth_getBlockByNumber', - params: ['0x3b3301'], - }, - response: { - result: { - baseFeePerGas: '0x63c498a46', - number: '0x3b3301', - }, - }, - }, - { - request: { - method: 'eth_getTransactionCount', - params: [ - '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - '0x3b3301', - ], - }, - response: { - result: '0x1', - }, - }, ], }); mockNetwork({ networkClientConfiguration, mocks: [ + // NetworkController + // BlockTracker { request: { method: 'eth_blockNumber', @@ -670,37 +527,7 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3', - }, - }, - { - request: { - method: 'eth_getBlockByHash', - params: ['0x1', false], - }, - response: { - result: { - transactions: [], - }, - }, - }, - { - request: { - method: 'eth_getCode', - params: [ACCOUNT_2_MOCK, '0x1'], - }, - response: { - result: - // what should this be? - '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024657468546f546f6b656e53776170496e7075742875696e743235362c75696e743235362900000000000000000000000000000000000000000000000000000000', - }, - }, + // NetworkController { request: { method: 'eth_getBlockByNumber', @@ -713,15 +540,18 @@ describe('TransactionController Integration', () => { }, }, }, + // readAddressAsContract + // requiresFixedGas (cached) { request: { - method: 'eth_gasPrice', - params: [], + method: 'eth_getCode', + params: [ACCOUNT_2_MOCK, '0x1'], }, response: { - result: '0x1', + result: '0x', // non contract }, }, + // estimateGas { request: { method: 'eth_estimateGas', @@ -738,6 +568,17 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + // getSuggestedGasFees + { + request: { + method: 'eth_gasPrice', + params: [], + }, + response: { + result: '0x1', + }, + }, + // NonceTracker { request: { method: 'eth_getTransactionCount', @@ -747,17 +588,29 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + // publishTransaction { request: { method: 'eth_sendRawTransaction', params: [ - '0x02e005010101019408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + '0x02e2050101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', ], }, response: { result: '0x1', }, }, + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3', + }, + }, + // PendingTransactionTracker.#checkTransaction { request: { method: 'eth_getTransactionReceipt', @@ -771,26 +624,41 @@ describe('TransactionController Integration', () => { }, }, }, + // PendingTransactionTracker.#onTransactionConfirmed { request: { - method: 'eth_sendRawTransaction', - params: [ - '0x02e00501010101946bf137f335ea1b8f193b8f6ea92561a60d23a2078080c0808080', - ], + method: 'eth_getBlockByHash', + params: ['0x1', false], }, response: { - result: '0x2', + result: { + transactions: [], + }, }, }, + // BlockTracker { request: { method: 'eth_blockNumber', params: [], }, + response: { + result: '0x3', + }, + }, + // publishTransaction + { + request: { + method: 'eth_sendRawTransaction', + params: [ + '0x02e205010101825208946bf137f335ea1b8f193b8f6ea92561a60d23a2078080c0808080', + ], + }, response: { result: '0x2', }, }, + // PendingTransactionTracker.#checkTransaction { request: { method: 'eth_getTransactionReceipt', @@ -834,6 +702,8 @@ describe('TransactionController Integration', () => { mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ + // NetworkController + // BlockTracker { request: { method: 'eth_blockNumber', @@ -843,35 +713,13 @@ describe('TransactionController Integration', () => { result: '0x3b3301', }, }, - { - request: { - method: 'eth_getBlockByNumber', - params: ['0x3b3301'], - }, - response: { - result: { - baseFeePerGas: '0x63c498a46', - number: '0x3b3301', - }, - }, - }, - { - request: { - method: 'eth_getTransactionCount', - params: [ - '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - '0x3b3301', - ], - }, - response: { - result: '0x1', - }, - }, ], }); mockNetwork({ networkClientConfiguration, mocks: [ + // NetworkController + // BlockTracker { request: { method: 'eth_blockNumber', @@ -881,37 +729,7 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3', - }, - }, - { - request: { - method: 'eth_getBlockByHash', - params: ['0x1', false], - }, - response: { - result: { - transactions: [], - }, - }, - }, - { - request: { - method: 'eth_getCode', - params: [ACCOUNT_2_MOCK, '0x1'], - }, - response: { - result: - // what should this be? - '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024657468546f546f6b656e53776170496e7075742875696e743235362c75696e743235362900000000000000000000000000000000000000000000000000000000', - }, - }, + // NetworkController { request: { method: 'eth_getBlockByNumber', @@ -924,15 +742,18 @@ describe('TransactionController Integration', () => { }, }, }, + // readAddressAsContract + // requiresFixedGas (cached) { request: { - method: 'eth_gasPrice', - params: [], + method: 'eth_getCode', + params: [ACCOUNT_2_MOCK, '0x1'], }, response: { - result: '0x1', + result: '0x', // non contract }, }, + // estimateGas { request: { method: 'eth_estimateGas', @@ -949,6 +770,17 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + // getSuggestedGasFees + { + request: { + method: 'eth_gasPrice', + params: [], + }, + response: { + result: '0x1', + }, + }, + // NonceTracker { request: { method: 'eth_getTransactionCount', @@ -958,46 +790,51 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + // publishTransaction { request: { method: 'eth_sendRawTransaction', params: [ - '0x02e005010101019408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + '0x02e2050101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', ], }, response: { result: '0x1', }, }, + // BlockTracker { request: { - method: 'eth_getTransactionReceipt', - params: ['0x1'], + method: 'eth_blockNumber', + params: [], }, response: { - result: null, + result: '0x3', }, }, + // publishTransaction { request: { - method: 'eth_getTransactionReceipt', - params: ['0x1'], + method: 'eth_sendRawTransaction', + params: [ + '0x02e205010101825208946bf137f335ea1b8f193b8f6ea92561a60d23a2078080c0808080', + ], }, response: { - result: null, + result: '0x2', }, }, + // PendingTransactionTracker.#checkTransaction { request: { - method: 'eth_sendRawTransaction', - params: [ - '0x02e00501010101946bf137f335ea1b8f193b8f6ea92561a60d23a2078080c0808080', - ], + method: 'eth_getTransactionReceipt', + params: ['0x1'], }, response: { - result: '0x2', + result: null, }, }, + // BlockTracker { request: { method: 'eth_blockNumber', @@ -1007,6 +844,7 @@ describe('TransactionController Integration', () => { result: '0x4', }, }, + // BlockTracker { request: { method: 'eth_blockNumber', @@ -1016,6 +854,17 @@ describe('TransactionController Integration', () => { result: '0x4', }, }, + // PendingTransactionTracker.#checkTransaction + { + request: { + method: 'eth_getTransactionReceipt', + params: ['0x1'], + }, + response: { + result: null, + }, + }, + // PendingTransactionTracker.#checkTransaction { request: { method: 'eth_getTransactionReceipt', @@ -1029,6 +878,7 @@ describe('TransactionController Integration', () => { }, }, }, + // PendingTransactionTracker.#onTransactionConfirmed { request: { method: 'eth_getBlockByHash', @@ -1080,6 +930,8 @@ describe('TransactionController Integration', () => { mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ + // NetworkController + // BlockTracker { request: { method: 'eth_blockNumber', @@ -1089,35 +941,13 @@ describe('TransactionController Integration', () => { result: '0x3b3301', }, }, - { - request: { - method: 'eth_getBlockByNumber', - params: ['0x3b3301'], - }, - response: { - result: { - baseFeePerGas: '0x63c498a46', - number: '0x3b3301', - }, - }, - }, - { - request: { - method: 'eth_getTransactionCount', - params: [ - '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - '0x3b3301', - ], - }, - response: { - result: '0x1', - }, - }, ], }); mockNetwork({ networkClientConfiguration, mocks: [ + // NetworkController + // BlockTracker { request: { method: 'eth_blockNumber', @@ -1127,37 +957,7 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3', - }, - }, - { - request: { - method: 'eth_getBlockByHash', - params: ['0x1', false], - }, - response: { - result: { - transactions: [], - }, - }, - }, - { - request: { - method: 'eth_getCode', - params: [ACCOUNT_2_MOCK, '0x1'], - }, - response: { - result: - // what should this be? - '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024657468546f546f6b656e53776170496e7075742875696e743235362c75696e743235362900000000000000000000000000000000000000000000000000000000', - }, - }, + // NetworkController { request: { method: 'eth_getBlockByNumber', @@ -1170,15 +970,18 @@ describe('TransactionController Integration', () => { }, }, }, + // readAddressAsContract + // requiresFixedGas (cached) { request: { - method: 'eth_gasPrice', - params: [], + method: 'eth_getCode', + params: [ACCOUNT_2_MOCK, '0x1'], }, response: { - result: '0x1', + result: '0x', // non contract }, }, + // estimateGas { request: { method: 'eth_estimateGas', @@ -1195,6 +998,17 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + // getSuggestedGasFees + { + request: { + method: 'eth_gasPrice', + params: [], + }, + response: { + result: '0x1', + }, + }, + // NonceTracker { request: { method: 'eth_getTransactionCount', @@ -1204,26 +1018,29 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + // publishTransaction { request: { method: 'eth_sendRawTransaction', params: [ - '0x02e405018203e88203e8809408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + '0x02e605018203e88203e88252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', ], }, response: { result: '0x1', }, }, + // BlockTracker { request: { - method: 'eth_getTransactionReceipt', - params: ['0x1'], + method: 'eth_blockNumber', + params: [], }, response: { - result: null, + result: '0x3', }, }, + // PendingTransactionTracker.#checkTransaction { request: { method: 'eth_getTransactionReceipt', @@ -1233,17 +1050,19 @@ describe('TransactionController Integration', () => { result: null, }, }, + // publishTransaction { request: { method: 'eth_sendRawTransaction', params: [ - '0x02e4050182044c82044c809408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + '0x02e6050182044c82044c8252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', ], }, response: { result: '0x2', }, }, + // BlockTracker { request: { method: 'eth_blockNumber', @@ -1253,6 +1072,7 @@ describe('TransactionController Integration', () => { result: '0x4', }, }, + // BlockTracker { request: { method: 'eth_blockNumber', @@ -1262,6 +1082,17 @@ describe('TransactionController Integration', () => { result: '0x4', }, }, + // PendingTransactionTracker.#checkTransaction + { + request: { + method: 'eth_getTransactionReceipt', + params: ['0x1'], + }, + response: { + result: null, + }, + }, + // PendingTransactionTracker.#checkTransaction { request: { method: 'eth_getTransactionReceipt', @@ -1275,6 +1106,7 @@ describe('TransactionController Integration', () => { }, }, }, + // PendingTransactionTracker.#onTransactionConfirmed { request: { method: 'eth_getBlockByHash', @@ -1905,10 +1737,11 @@ describe('TransactionController Integration', () => { // TODO(JL): IncomingTransactionHelper doesn't populate networkClientId on the generated tx object. Should it?.. it('should add incoming transactions to state with the correct chainId for the given networkClientId on the next block', async () => { const clock = useFakeTimers(); - // this is needed or the globally selected mainnet PollingBlockTracker makes this test fail mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ + // NetworkController + // BlockTracker { request: { method: 'eth_blockNumber', @@ -1918,6 +1751,7 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + // BlockTracker { request: { method: 'eth_blockNumber', @@ -1949,7 +1783,7 @@ describe('TransactionController Integration', () => { mockNetwork({ networkClientConfiguration: config, mocks: [ - // blockTracker on instantiation + // BlockTracker { request: { method: 'eth_blockNumber', @@ -1959,7 +1793,7 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, - // blockTracker loop + // BlockTracker { request: { method: 'eth_blockNumber', @@ -2019,10 +1853,11 @@ describe('TransactionController Integration', () => { describe('stopIncomingTransactionPolling', () => { it('should not poll for new incoming transactions for the given networkClientId', async () => { const clock = useFakeTimers(); - // this is needed or the globally selected mainnet PollingBlockTracker makes this test fail mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ + // NetworkController + // BlockTracker { request: { method: 'eth_blockNumber', @@ -2032,13 +1867,14 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + // BlockTracker { request: { method: 'eth_blockNumber', params: [], }, response: { - result: '0x1', + result: '0x2', }, }, ], @@ -2060,7 +1896,7 @@ describe('TransactionController Integration', () => { mockNetwork({ networkClientConfiguration: config, mocks: [ - // blockTracker on instantiation + // BlockTracker { request: { method: 'eth_blockNumber', @@ -2070,7 +1906,7 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, - // blockTracker loop + // BlockTracker { request: { method: 'eth_blockNumber', @@ -2107,10 +1943,11 @@ describe('TransactionController Integration', () => { describe('stopAllIncomingTransactionPolling', () => { it('should not poll for incoming transactions on any network client', async () => { const clock = useFakeTimers(); - // this is needed or the globally selected mainnet PollingBlockTracker makes this test fail mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ + // NetworkController + // BlockTracker { request: { method: 'eth_blockNumber', @@ -2120,6 +1957,7 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + // BlockTracker { request: { method: 'eth_blockNumber', @@ -2148,7 +1986,7 @@ describe('TransactionController Integration', () => { mockNetwork({ networkClientConfiguration: config, mocks: [ - // blockTracker on instantiation + // BlockTracker { request: { method: 'eth_blockNumber', @@ -2158,7 +1996,7 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, - // blockTracker loop + // BlockTracker { request: { method: 'eth_blockNumber', @@ -2195,10 +2033,11 @@ describe('TransactionController Integration', () => { describe('updateIncomingTransactions', () => { it('should add incoming transactions to state with the correct chainId for the given networkClientId without waiting for the next block', async () => { const clock = useFakeTimers(); - // this is needed or the globally selected mainnet PollingBlockTracker makes this test fail mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ + // NetworkController + // BlockTracker { request: { method: 'eth_blockNumber', @@ -2208,6 +2047,7 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + // BlockTracker { request: { method: 'eth_blockNumber', @@ -2239,7 +2079,7 @@ describe('TransactionController Integration', () => { mockNetwork({ networkClientConfiguration: config, mocks: [ - // blockTracker on instantiation + // BlockTracker { request: { method: 'eth_blockNumber', @@ -2300,6 +2140,8 @@ describe('TransactionController Integration', () => { mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ + // NetworkController + // BlockTracker { request: { method: 'eth_blockNumber', @@ -2309,6 +2151,7 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + // BlockTracker { request: { method: 'eth_blockNumber', @@ -2370,6 +2213,8 @@ describe('TransactionController Integration', () => { mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ + // NetworkController + // BlockTracker { request: { method: 'eth_blockNumber', @@ -2379,6 +2224,7 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + // BlockTracker { request: { method: 'eth_blockNumber', @@ -2464,6 +2310,7 @@ describe('TransactionController Integration', () => { mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ + // NetworkController // BlockTracker { request: { @@ -2497,6 +2344,7 @@ describe('TransactionController Integration', () => { mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ + // NetworkController // BlockTracker { request: { From 6ed26365cf8b5c1fd6e0e3cfca0b2970efd15268 Mon Sep 17 00:00:00 2001 From: Shane Date: Tue, 23 Jan 2024 13:53:03 -0800 Subject: [PATCH 032/100] Transaction multichain multiple networks (#3824) ## Explanation ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --------- Co-authored-by: Jiexi Luan Co-authored-by: Alex Donesky --- .../TransactionControllerIntegration.test.ts | 356 +++++++++++++++++- 1 file changed, 352 insertions(+), 4 deletions(-) diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index ce34cf564d..837723eccf 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -1,6 +1,10 @@ import { ApprovalController } from '@metamask/approval-controller'; import { ControllerMessenger } from '@metamask/base-controller'; -import { ApprovalType, BUILT_IN_NETWORKS, NetworkType } from '@metamask/controller-utils'; +import { + ApprovalType, + BUILT_IN_NETWORKS, + NetworkType, +} from '@metamask/controller-utils'; import { NetworkController, NetworkClientType, @@ -32,6 +36,14 @@ const networkClientConfiguration = { ticker: BUILT_IN_NETWORKS[NetworkType.goerli].ticker, } as const; +const sepoliaNetworkClientConfiguration = { + type: NetworkClientType.Infura, + network: NetworkType.sepolia, + chainId: BUILT_IN_NETWORKS[NetworkType.sepolia].chainId, + infuraProjectId, + ticker: BUILT_IN_NETWORKS[NetworkType.sepolia].ticker, +} as const; + const mainnetNetworkClientConfiguration = { type: NetworkClientType.Infura, network: NetworkType.mainnet, @@ -59,9 +71,7 @@ const newController = async (options: any) => { name: 'ApprovalController', }), showApprovalRequest: jest.fn(), - typesExcludedFromRateLimiting: [ - ApprovalType.Transaction, - ] + typesExcludedFromRateLimiting: [ApprovalType.Transaction], }); const opts = { @@ -81,6 +91,7 @@ const newController = async (options: any) => { networkController.getNetworkClientById.bind(networkController), getNetworkState: () => networkController.state, getSelectedAddress: () => '0xdeadbeef', + getPermittedAccounts: () => [ACCOUNT_MOCK], ...options, }; const transactionController = new TransactionController(opts, { @@ -494,6 +505,343 @@ describe('TransactionController Integration', () => { expect(transactionController.state.transactions[0].status).toBe( 'confirmed', ); + transactionController.stopTrackingByNetworkClientId('goerli'); + clock.restore(); + }); + it('should be able to send and confirm transactions on different chains', async () => { + const clock = useFakeTimers(); + mockNetwork({ + networkClientConfiguration: mainnetNetworkClientConfiguration, + mocks: [ + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3b3301', + }, + }, + { + request: { + method: 'eth_getBlockByNumber', + params: ['0x3b3301'], + }, + response: { + result: { + baseFeePerGas: '0x63c498a46', + number: '0x3b3301', + }, + }, + }, + { + request: { + method: 'eth_getTransactionCount', + params: [ + '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + '0x3b3301', + ], + }, + response: { + result: '0x1', + }, + }, + ], + }); + mockNetwork({ + networkClientConfiguration, + mocks: [ + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3', + }, + }, + { + request: { + method: 'eth_getBlockByHash', + params: ['0x1', false], + }, + response: { + result: { + transactions: [], + }, + }, + }, + { + request: { + method: 'eth_getCode', + params: [ACCOUNT_2_MOCK, '0x1'], + }, + response: { + result: + // what should this be? + '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024657468546f546f6b656e53776170496e7075742875696e743235362c75696e743235362900000000000000000000000000000000000000000000000000000000', + }, + }, + { + request: { + method: 'eth_getBlockByNumber', + params: ['0x1', false], + }, + response: { + result: { + baseFeePerGas: '0x63c498a46', + number: '0x1', + }, + }, + }, + { + request: { + method: 'eth_gasPrice', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_estimateGas', + params: [ + { + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + to: '0x08f137f335ea1b8f193b8f6ea92561a60d23a211', + value: '0x0', + gas: '0x0', + }, + ], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_getTransactionCount', + params: ['0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', '0x1'], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_sendRawTransaction', + params: [ + '0x02e005010101019408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + ], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_getTransactionReceipt', + params: ['0x1'], + }, + response: { + result: { + blockHash: '0x1', + blockNumber: '0x3', // we need at least 2 blocks mocked since the first one is used for when the blockTracker is instantied before we have listeners + status: '0x1', // 0x1 = success + }, + }, + }, + ], + }); + mockNetwork({ + networkClientConfiguration: sepoliaNetworkClientConfiguration, + mocks: [ + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x2', + }, + }, + { + request: { + method: 'eth_getBlockByHash', + params: ['0x1', false], + }, + response: { + result: { + transactions: [], + }, + }, + }, + { + request: { + method: 'eth_getCode', + params: [ACCOUNT_2_MOCK, '0x1'], + }, + response: { + result: + // what should this be? + '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024657468546f546f6b656e53776170496e7075742875696e743235362c75696e743235362900000000000000000000000000000000000000000000000000000000', + }, + }, + { + request: { + method: 'eth_getBlockByNumber', + params: ['0x1', false], + }, + response: { + result: { + baseFeePerGas: '0x63c498a46', + number: '0x1', + }, + }, + }, + { + request: { + method: 'eth_gasPrice', + params: [], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_estimateGas', + params: [ + { + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + to: '0x08f137f335ea1b8f193b8f6ea92561a60d23a211', + value: '0x0', + gas: '0x0', + }, + ], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_getTransactionCount', + params: ['0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', '0x1'], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_sendRawTransaction', + params: [ + '0x02e383aa36a7010101019408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + ], + }, + response: { + result: '0x1', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3', + }, + }, + { + request: { + method: 'eth_getTransactionReceipt', + params: ['0x1'], + }, + response: { + result: { + blockHash: '0x1', + blockNumber: '0x3', // we need at least 2 blocks mocked since the first one is used for when the blockTracker is instantied before we have listeners + status: '0x1', // 0x1 = success + }, + }, + }, + ], + }); + const { transactionController, approvalController } = + await newController({}); + const firstTransaction = await transactionController.addTransaction( + { + from: ACCOUNT_MOCK, + to: ACCOUNT_2_MOCK, + }, + { networkClientId: 'goerli' }, + ); + const secondTransaction = await transactionController.addTransaction( + { + from: ACCOUNT_MOCK, + to: ACCOUNT_2_MOCK, + }, + { networkClientId: 'sepolia', origin: 'test' }, + ); + + await Promise.all([ + approvalController.accept(firstTransaction.transactionMeta.id), + approvalController.accept(secondTransaction.transactionMeta.id), + ]); + await advanceTime({ clock, duration: 1 }); + await advanceTime({ clock, duration: 1 }); + + await Promise.all([firstTransaction.result, secondTransaction.result]); + + // blocktracker polling is 20s + await advanceTime({ clock, duration: 20000 }); + await advanceTime({ clock, duration: 1 }); + await advanceTime({ clock, duration: 1 }); + + expect(transactionController.state.transactions).toHaveLength(2); + expect(transactionController.state.transactions[0].status).toBe( + 'confirmed', + ); + expect( + transactionController.state.transactions[0].networkClientId, + ).toBe('sepolia'); + expect(transactionController.state.transactions[1].status).toBe( + 'confirmed', + ); + expect( + transactionController.state.transactions[1].networkClientId, + ).toBe('goerli'); + transactionController.stopTrackingByNetworkClientId('goerli'); + transactionController.stopTrackingByNetworkClientId('sepolia'); clock.restore(); }); it('should be able to cancel a transaction', async () => { From e916c67d3bc5f9e3ac76ac7f5f4a5d78f869e25f Mon Sep 17 00:00:00 2001 From: jiexi Date: Tue, 23 Jan 2024 15:38:15 -0800 Subject: [PATCH 033/100] Jl/transaction multichain resubmits approved tx on boot cleanup (#3825) ## Explanation * Passthrough state and config in `newController` param object * Add spec for resubmitting approved tx on instantiation * Fix `when transactions are added concurrently with different networkClientIds but on the same chainId` missing eth_blockNumber mocks ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --------- Co-authored-by: Alex Donesky --- packages/transaction-controller/package.json | 4 +- .../TransactionControllerIntegration.test.ts | 360 ++++++++++++++++-- yarn.lock | 23 +- 3 files changed, 324 insertions(+), 63 deletions(-) diff --git a/packages/transaction-controller/package.json b/packages/transaction-controller/package.json index 6f7b1fca37..122310b104 100644 --- a/packages/transaction-controller/package.json +++ b/packages/transaction-controller/package.json @@ -42,7 +42,7 @@ "@metamask/metamask-eth-abis": "^3.0.0", "@metamask/network-controller": "^17.2.0", "@metamask/rpc-errors": "^6.1.0", - "@metamask/selected-network-controller": "^6.0.0", + "@metamask/selected-network-controller": "^7.0.0", "@metamask/utils": "^8.3.0", "async-mutex": "^0.2.6", "eth-method-registry": "^3.0.0", @@ -71,7 +71,7 @@ "@metamask/approval-controller": "^5.1.2", "@metamask/gas-fee-controller": "^13.0.0", "@metamask/network-controller": "^17.2.0", - "@metamask/selected-network-controller": "^6.0.0", + "@metamask/selected-network-controller": "^7.0.0", "babel-runtime": "^6.26.0" }, "engines": { diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index 837723eccf..6405ca79ba 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -53,7 +53,7 @@ const mainnetNetworkClientConfiguration = { } as const; // eslint-disable-next-line @typescript-eslint/no-explicit-any -const newController = async (options: any) => { +const newController = async (options: any = {}) => { const messenger = new ControllerMessenger(); const networkController = new NetworkController({ messenger: messenger.getRestricted({ name: 'NetworkController' }), @@ -74,31 +74,37 @@ const newController = async (options: any) => { typesExcludedFromRateLimiting: [ApprovalType.Transaction], }); - const opts = { - provider, - blockTracker, - messenger, - onNetworkStateChange: () => { - // noop + const { state, config, ...opts } = options; + + const transactionController = new TransactionController( + { + provider, + blockTracker, + messenger, + onNetworkStateChange: () => { + // noop + }, + getCurrentNetworkEIP1559Compatibility: + networkController.getEIP1559Compatibility.bind(networkController), + getNetworkClientRegistry: + networkController.getNetworkClientRegistry.bind(networkController), + findNetworkClientIdByChainId: + networkController.findNetworkClientIdByChainId.bind(networkController), + getNetworkClientById: + networkController.getNetworkClientById.bind(networkController), + getNetworkState: () => networkController.state, + getSelectedAddress: () => '0xdeadbeef', + getPermittedAccounts: () => [ACCOUNT_MOCK], + ...opts, }, - getCurrentNetworkEIP1559Compatibility: - networkController.getEIP1559Compatibility.bind(networkController), - getNetworkClientRegistry: - networkController.getNetworkClientRegistry.bind(networkController), - findNetworkClientIdByChainId: - networkController.findNetworkClientIdByChainId.bind(networkController), - getNetworkClientById: - networkController.getNetworkClientById.bind(networkController), - getNetworkState: () => networkController.state, - getSelectedAddress: () => '0xdeadbeef', - getPermittedAccounts: () => [ACCOUNT_MOCK], - ...options, - }; - const transactionController = new TransactionController(opts, { - // TODO(JL): fix this type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - sign: async (transaction: any) => transaction, - }); + { + // TODO(JL): fix this type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sign: async (transaction: any) => transaction, + ...config, + }, + state, + ); return { transactionController, @@ -130,6 +136,257 @@ describe('TransactionController Integration', () => { transactionController.stopTrackingByNetworkClientId('goerli'); expect(transactionController).toBeDefined(); }); + + it('should submit all approved transactions in state when the controller is constructed', async () => { + const clock = useFakeTimers(); + mockNetwork({ + networkClientConfiguration: mainnetNetworkClientConfiguration, + mocks: [ + // NetworkController + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + ], + }); + + mockNetwork({ + networkClientConfiguration, + mocks: [ + // NetworkController + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + // publishTransaction + { + request: { + method: 'eth_sendRawTransaction', + params: [ + '0x02e2050101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + ], + }, + response: { + result: '0x1', + }, + }, + ], + }); + + mockNetwork({ + networkClientConfiguration: sepoliaNetworkClientConfiguration, + mocks: [ + // NetworkController + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + // publishTransaction + { + request: { + method: 'eth_sendRawTransaction', + params: [ + '0x02e583aa36a70101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + ], + }, + response: { + result: '0x1', + }, + }, + ], + }); + + const { transactionController } = await newController({ + state: { + transactions: [ + { + actionId: undefined, + chainId: '0x5', + dappSuggestedGasFees: undefined, + deviceConfirmedOn: undefined, + id: 'ecfe8c60-ba27-11ee-8643-dfd28279a442', + origin: undefined, + securityAlertResponse: undefined, + status: 'approved', + time: 1706039113766, + txParams: { + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + gas: '0x5208', + nonce: '0x1', + to: '0x08f137f335ea1b8f193b8f6ea92561a60d23a211', + value: '0x0', + maxFeePerGas: '0x1', + maxPriorityFeePerGas: '0x1', + }, + userEditedGasLimit: false, + verifiedOnBlockchain: false, + type: 'simpleSend', + networkClientId: 'goerli', + simulationFails: undefined, + originalGasEstimate: '0x5208', + defaultGasEstimates: { + gas: '0x5208', + maxFeePerGas: '0x1', + maxPriorityFeePerGas: '0x1', + gasPrice: undefined, + estimateType: 'dappSuggested', + }, + userFeeLevel: 'dappSuggested', + sendFlowHistory: [], + history: [ + { + chainId: '0x5', + id: 'ecfe8c60-ba27-11ee-8643-dfd28279a442', + status: 'unapproved', + time: 1706039113766, + txParams: { + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + to: '0x08f137f335ea1b8f193b8f6ea92561a60d23a211', + value: '0x0', + gas: '0x5208', + maxFeePerGas: '0x1', + maxPriorityFeePerGas: '0x1', + }, + userEditedGasLimit: false, + verifiedOnBlockchain: false, + type: 'simpleSend', + networkClientId: 'goerli', + originalGasEstimate: '0x5208', + defaultGasEstimates: { + gas: '0x5208', + maxFeePerGas: '0x1', + maxPriorityFeePerGas: '0x1', + estimateType: 'dappSuggested', + }, + userFeeLevel: 'dappSuggested', + sendFlowHistory: [], + }, + [ + { + op: 'add', + path: '/txParams/nonce', + value: '0x1', + note: 'TransactionController#approveTransaction - Transaction approved', + timestamp: 1706039113767, + }, + { + op: 'replace', + path: '/status', + value: 'approved', + }, + ], + ], + }, + { + actionId: undefined, + chainId: '0xaa36a7', + dappSuggestedGasFees: undefined, + deviceConfirmedOn: undefined, + id: 'c4cc0ff0-ba28-11ee-926f-55a7f9c2c2c6', + origin: undefined, + securityAlertResponse: undefined, + status: 'approved', + time: 1706039113766, + txParams: { + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + gas: '0x5208', + nonce: '0x1', + to: '0x08f137f335ea1b8f193b8f6ea92561a60d23a211', + value: '0x0', + maxFeePerGas: '0x1', + maxPriorityFeePerGas: '0x1', + }, + userEditedGasLimit: false, + verifiedOnBlockchain: false, + type: 'simpleSend', + networkClientId: 'sepolia', + simulationFails: undefined, + originalGasEstimate: '0x5208', + defaultGasEstimates: { + gas: '0x5208', + maxFeePerGas: '0x1', + maxPriorityFeePerGas: '0x1', + gasPrice: undefined, + estimateType: 'dappSuggested', + }, + userFeeLevel: 'dappSuggested', + sendFlowHistory: [], + history: [ + { + chainId: '0xaa36a7', + id: 'c4cc0ff0-ba28-11ee-926f-55a7f9c2c2c6', + status: 'unapproved', + time: 1706039113766, + txParams: { + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + to: '0x08f137f335ea1b8f193b8f6ea92561a60d23a211', + value: '0x0', + gas: '0x5208', + maxFeePerGas: '0x1', + maxPriorityFeePerGas: '0x1', + }, + userEditedGasLimit: false, + verifiedOnBlockchain: false, + type: 'simpleSend', + networkClientId: 'sepolia', + originalGasEstimate: '0x5208', + defaultGasEstimates: { + gas: '0x5208', + maxFeePerGas: '0x1', + maxPriorityFeePerGas: '0x1', + estimateType: 'dappSuggested', + }, + userFeeLevel: 'dappSuggested', + sendFlowHistory: [], + }, + [ + { + op: 'add', + path: '/txParams/nonce', + value: '0x1', + note: 'TransactionController#approveTransaction - Transaction approved', + timestamp: 1706039113767, + }, + { + op: 'replace', + path: '/status', + value: 'approved', + }, + ], + ], + }, + ], + }, + }); + await advanceTime({ clock, duration: 1 }); + + expect(transactionController.state.transactions).toHaveLength(2); + expect(transactionController.state.transactions[0].status).toBe( + 'submitted', + ); + expect(transactionController.state.transactions[1].status).toBe( + 'submitted', + ); + clock.restore(); + }); }); describe('multichain transaction lifecycle', () => { describe('when a transaction is added with a networkClientId that does not match the globally selected network', () => { @@ -1641,6 +1898,15 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3', + }, + }, { request: { method: 'eth_sendRawTransaction', @@ -1667,23 +1933,6 @@ describe('TransactionController Integration', () => { }, ], }); - const { approvalController, networkController, transactionController } = - await newController({ - getPermittedAccounts: () => [ACCOUNT_MOCK], - getSelectedAddress: () => ACCOUNT_MOCK, - }); - const otherNetworkClientIdOnGoerli = - await networkController.upsertNetworkConfiguration( - { - rpcUrl: 'https://mock.rpc.url', - chainId: networkClientConfiguration.chainId, - ticker: networkClientConfiguration.ticker, - }, - { - referrer: 'https://mock.referrer', - source: 'dapp', - }, - ); mockNetwork({ networkClientConfiguration: { @@ -1778,6 +2027,15 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3', + }, + }, { request: { method: 'eth_sendRawTransaction', @@ -1805,6 +2063,24 @@ describe('TransactionController Integration', () => { ], }); + const { approvalController, networkController, transactionController } = + await newController({ + getPermittedAccounts: () => [ACCOUNT_MOCK], + getSelectedAddress: () => ACCOUNT_MOCK, + }); + const otherNetworkClientIdOnGoerli = + await networkController.upsertNetworkConfiguration( + { + rpcUrl: 'https://mock.rpc.url', + chainId: networkClientConfiguration.chainId, + ticker: networkClientConfiguration.ticker, + }, + { + referrer: 'https://mock.referrer', + source: 'dapp', + }, + ); + const addTx1 = await transactionController.addTransaction( { from: ACCOUNT_MOCK, diff --git a/yarn.lock b/yarn.lock index 578bb0b997..4814978b31 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2332,7 +2332,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/network-controller@^17.1.0, @metamask/network-controller@^17.2.0, @metamask/network-controller@workspace:packages/network-controller": +"@metamask/network-controller@^17.2.0, @metamask/network-controller@workspace:packages/network-controller": version: 0.0.0-use.local resolution: "@metamask/network-controller@workspace:packages/network-controller" dependencies: @@ -2690,21 +2690,6 @@ __metadata: languageName: unknown linkType: soft -"@metamask/selected-network-controller@npm:^6.0.0": - version: 6.0.0 - resolution: "@metamask/selected-network-controller@npm:6.0.0" - dependencies: - "@metamask/base-controller": ^4.0.1 - "@metamask/json-rpc-engine": ^7.3.1 - "@metamask/network-controller": ^17.1.0 - "@metamask/swappable-obj-proxy": ^2.1.0 - "@metamask/utils": ^8.2.0 - peerDependencies: - "@metamask/network-controller": ^17.1.0 - checksum: c16124402f86495aac2a9d3078820b3bb1f71f972fbecb280a2d9f12f3e55caee867acb6ab0c9815ca76e31b1945f3c8a09acb8da77f5e1545163e48ce6e1880 - languageName: node - linkType: hard - "@metamask/signature-controller@workspace:packages/signature-controller": version: 0.0.0-use.local resolution: "@metamask/signature-controller@workspace:packages/signature-controller" @@ -2901,7 +2886,7 @@ __metadata: languageName: node linkType: hard -"@metamask/swappable-obj-proxy@npm:^2.1.0, @metamask/swappable-obj-proxy@npm:^2.2.0": +"@metamask/swappable-obj-proxy@npm:^2.2.0": version: 2.2.0 resolution: "@metamask/swappable-obj-proxy@npm:2.2.0" checksum: 343c95f72c96776980ef3e70600f7fa312be9a75683c132404a66ddd3c507abadee9c4deba1385246f73bded1938a7958e5a89fc407c19dfc352dd9b398e216f @@ -2925,7 +2910,7 @@ __metadata: "@metamask/metamask-eth-abis": ^3.0.0 "@metamask/network-controller": ^17.2.0 "@metamask/rpc-errors": ^6.1.0 - "@metamask/selected-network-controller": ^6.0.0 + "@metamask/selected-network-controller": ^7.0.0 "@metamask/utils": ^8.3.0 "@types/jest": ^27.4.1 "@types/node": ^16.18.54 @@ -2949,7 +2934,7 @@ __metadata: "@metamask/approval-controller": ^5.1.2 "@metamask/gas-fee-controller": ^13.0.0 "@metamask/network-controller": ^17.2.0 - "@metamask/selected-network-controller": ^6.0.0 + "@metamask/selected-network-controller": ^7.0.0 babel-runtime: ^6.26.0 languageName: unknown linkType: soft From d76f73dd05947a35caf2e3a55d07a695088d3f71 Mon Sep 17 00:00:00 2001 From: jiexi Date: Wed, 24 Jan 2024 10:30:39 -0800 Subject: [PATCH 034/100] Jl/mmp 1962/transaction multichain destroy (#3826) ## Explanation * add `#stopAllTracking()` that stops all trackers, incl global * add `addIncomingTransactionHelperListeners()` * add `removeIncomingTransactionHelperListeners()` * add `destroy()` * use destroy in specs * use beforeEach and afterEach to mock and restore timers for all scenarios * fix getNonceLock tests after this change ## References * Fixes https://github.com/MetaMask/MetaMask-planning/issues/1962 ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- .../src/TransactionController.test.ts | 5 +- .../src/TransactionController.ts | 68 +- .../TransactionControllerIntegration.test.ts | 619 +++++++++--------- 3 files changed, 371 insertions(+), 321 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index df6b9bb364..c787c11c86 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -726,6 +726,7 @@ describe('TransactionController', () => { update: jest.fn(), hub: { on: jest.fn(), + removeAllListeners: jest.fn(), }, } as unknown as jest.Mocked; incomingTransactionHelperMocks.push(incomingTransactionHelperMock); @@ -4817,7 +4818,7 @@ describe('TransactionController', () => { expect(controller).toBeDefined(); }); // eslint-disable-next-line jest/no-done-callback - it('should handle removals in the networkController registry', (done) => { + it('should handle removals from the networkController registry', (done) => { const hub = new EventEmitter() as TransactionControllerEventEmitter; const mockGetNetworkClientRegistry = jest.fn(); mockGetNetworkClientRegistry.mockImplementationOnce(() => ({ @@ -4866,7 +4867,7 @@ describe('TransactionController', () => { }); }); // eslint-disable-next-line jest/no-done-callback - it('should handle removals in the networkController registry', (done) => { + it('should handle additions to the networkController registry', (done) => { const hub = new EventEmitter() as TransactionControllerEventEmitter; const mockGetNetworkClientRegistry = jest.fn(); mockGetNetworkClientRegistry.mockImplementationOnce(() => ({ diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 927684fb59..9d7513832c 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -590,15 +590,7 @@ export class TransactionController extends BaseControllerV1< updateTransactions: incomingTransactions.updateTransactions, }); - this.incomingTransactionHelper.hub.on( - 'transactions', - this.onIncomingTransactions.bind(this), - ); - - this.incomingTransactionHelper.hub.on( - 'updatedLastFetchedBlockNumbers', - this.onUpdatedLastFetchedBlockNumbers.bind(this), - ); + this.addIncomingTransactionHelperListeners(); this.pendingTransactionTracker = new PendingTransactionTracker({ approveTransaction: this.approveTransaction.bind(this), @@ -646,6 +638,13 @@ export class TransactionController extends BaseControllerV1< this.#initTrackingMap(); } + /** + * Stops polling and removes listeners to prepare the controller for garbage collection. + */ + destroy() { + this.#stopAllTracking(); + } + #refreshTrackingMap = () => { const networkClients = this.getNetworkClientRegistry(); const networkClientIds = Object.keys(networkClients); @@ -1296,13 +1295,22 @@ export class TransactionController extends BaseControllerV1< trackers.pendingTransactionTracker, ); trackers.incomingTransactionHelper.stop(); + this.removeIncomingTransactionHelperListeners( + trackers.incomingTransactionHelper, + ); + } + this.trackingMap.delete(networkClientId); + } - // doesn't seem like any cleanup is needed for nonceTracker - // trackers.nonceTracker + #stopAllTracking() { + this.pendingTransactionTracker.stop(); + this.removePendingTransactionTrackerListeners(); + this.incomingTransactionHelper.stop(); + this.removeIncomingTransactionHelperListeners(); - trackers.pendingTransactionTracker.stop(); + for (const [networkClientId] of this.trackingMap) { + this.stopTrackingByNetworkClientId(networkClientId); } - this.trackingMap.delete(networkClientId); } // NOTE(JL): Should this be private? @@ -1351,6 +1359,9 @@ export class TransactionController extends BaseControllerV1< transactionLimit: this.config.txHistoryLimit, updateTransactions: this.incomingTransactionOptions.updateTransactions, }); + + this.addIncomingTransactionHelperListeners(incomingTransactionHelper); + const pendingTransactionTracker = new PendingTransactionTracker({ approveTransaction: this.approveTransaction.bind(this), blockTracker: networkClient.blockTracker, @@ -1366,16 +1377,7 @@ export class TransactionController extends BaseControllerV1< beforePublish: this.beforePublish.bind(this), }, }); - // subscribe to trackers - incomingTransactionHelper.hub.on( - 'transactions', - this.onIncomingTransactions.bind(this), - ); - incomingTransactionHelper.hub.on( - 'updatedLastFetchedBlockNumbers', - this.onUpdatedLastFetchedBlockNumbers.bind(this), - ); this.addPendingTransactionTrackerListeners(pendingTransactionTracker); // add to tracking map @@ -2919,6 +2921,28 @@ export class TransactionController extends BaseControllerV1< ); } + private removeIncomingTransactionHelperListeners( + incomingTransactionHelper = this.incomingTransactionHelper, + ) { + incomingTransactionHelper.hub.removeAllListeners('transactions'); + incomingTransactionHelper.hub.removeAllListeners( + 'updatedLastFetchedBlockNumbers', + ); + } + + private addIncomingTransactionHelperListeners( + incomingTransactionHelper = this.incomingTransactionHelper, + ) { + incomingTransactionHelper.hub.on( + 'transactions', + this.onIncomingTransactions.bind(this), + ); + incomingTransactionHelper.hub.on( + 'updatedLastFetchedBlockNumbers', + this.onUpdatedLastFetchedBlockNumbers.bind(this), + ); + } + private removePendingTransactionTrackerListeners( pendingTransactionTracker = this.pendingTransactionTracker, ) { diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index 6405ca79ba..3f2da0e7a0 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -10,6 +10,7 @@ import { NetworkClientType, } from '@metamask/network-controller'; import nock from 'nock'; +import type { SinonFakeTimers } from 'sinon'; import { useFakeTimers } from 'sinon'; import { advanceTime } from '../../../tests/helpers'; @@ -114,6 +115,15 @@ const newController = async (options: any = {}) => { }; describe('TransactionController Integration', () => { + let clock: SinonFakeTimers; + beforeEach(() => { + clock = useFakeTimers(); + }); + + afterEach(() => { + clock.restore(); + }); + describe('constructor', () => { it('should create a new instance of TransactionController', async () => { mockNetwork({ @@ -133,12 +143,11 @@ describe('TransactionController Integration', () => { ], }); const { transactionController } = await newController({}); - transactionController.stopTrackingByNetworkClientId('goerli'); expect(transactionController).toBeDefined(); + transactionController.destroy(); }); it('should submit all approved transactions in state when the controller is constructed', async () => { - const clock = useFakeTimers(); mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ @@ -385,7 +394,6 @@ describe('TransactionController Integration', () => { expect(transactionController.state.transactions[1].status).toBe( 'submitted', ); - clock.restore(); }); }); describe('multichain transaction lifecycle', () => { @@ -452,11 +460,11 @@ describe('TransactionController Integration', () => { }, { networkClientId: 'goerli' }, ); - transactionController.stopTrackingByNetworkClientId('goerli'); expect(transactionController.state.transactions).toHaveLength(1); expect(transactionController.state.transactions[0].status).toBe( 'unapproved', ); + transactionController.destroy(); }); it('should be able to get to submitted state', async () => { mockNetwork({ @@ -586,17 +594,17 @@ describe('TransactionController Integration', () => { ); await approvalController.accept(transactionMeta.id); + await advanceTime({ clock, duration: 1 }); await result; - transactionController.stopTrackingByNetworkClientId('goerli'); expect(transactionController.state.transactions).toHaveLength(1); expect(transactionController.state.transactions[0].status).toBe( 'submitted', ); + transactionController.destroy(); }); it('should be able to get to confirmed state', async () => { - const clock = useFakeTimers(); mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ @@ -762,11 +770,9 @@ describe('TransactionController Integration', () => { expect(transactionController.state.transactions[0].status).toBe( 'confirmed', ); - transactionController.stopTrackingByNetworkClientId('goerli'); - clock.restore(); + transactionController.destroy(); }); it('should be able to send and confirm transactions on different chains', async () => { - const clock = useFakeTimers(); mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ @@ -1097,9 +1103,7 @@ describe('TransactionController Integration', () => { expect( transactionController.state.transactions[1].networkClientId, ).toBe('goerli'); - transactionController.stopTrackingByNetworkClientId('goerli'); - transactionController.stopTrackingByNetworkClientId('sepolia'); - clock.restore(); + transactionController.destroy(); }); it('should be able to cancel a transaction', async () => { mockNetwork({ @@ -1291,19 +1295,19 @@ describe('TransactionController Integration', () => { ); await approvalController.accept(transactionMeta.id); + await advanceTime({ clock, duration: 1 }); await result; await transactionController.stopTransaction(transactionMeta.id); - transactionController.stopTrackingByNetworkClientId('goerli'); expect(transactionController.state.transactions).toHaveLength(2); expect(transactionController.state.transactions[1].status).toBe( 'submitted', ); + transactionController.destroy(); }); it('should be able to confirm a cancelled transaction', async () => { - const clock = useFakeTimers(); mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ @@ -1527,11 +1531,9 @@ describe('TransactionController Integration', () => { expect(transactionController.state.transactions[1].status).toBe( 'confirmed', ); - transactionController.stopTrackingByNetworkClientId('goerli'); - clock.restore(); + transactionController.destroy(); }); it('should be able to get to speedup state', async () => { - const clock = useFakeTimers(); mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ @@ -1763,14 +1765,12 @@ describe('TransactionController Integration', () => { transactionController.state.transactions[1].txParams.maxFeePerGas, ), ).toBeGreaterThan(Number(baseFee)); - transactionController.stopTrackingByNetworkClientId('goerli'); - clock.restore(); + transactionController.destroy(); }); }); describe('when transactions are added concurrently with different networkClientIds but on the same chainId', () => { it('should add each transaction with consecutive nonces', async () => { - const clock = useFakeTimers(); mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ @@ -2106,22 +2106,17 @@ describe('TransactionController Integration', () => { await advanceTime({ clock, duration: 1 }); await Promise.all([addTx1.result, addTx2.result]); - transactionController.stopTrackingByNetworkClientId('goerli'); - transactionController.stopTrackingByNetworkClientId( - otherNetworkClientIdOnGoerli, - ); const nonces = transactionController.state.transactions .map((tx) => tx.txParams.nonce) .sort(); expect(nonces).toStrictEqual(['0x1', '0x2']); - clock.restore(); + transactionController.destroy(); }); }); describe('when transactions are added concurrently with the same networkClientId', () => { it('should add each transaction with consecutive nonces', async () => { - const clock = useFakeTimers(); mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ @@ -2346,13 +2341,12 @@ describe('TransactionController Integration', () => { await advanceTime({ clock, duration: 1 }); await Promise.all([addTx1.result, addTx2.result]); - transactionController.stopTrackingByNetworkClientId('goerli'); const nonces = transactionController.state.transactions .map((tx) => tx.txParams.nonce) .sort(); expect(nonces).toStrictEqual(['0x1', '0x2']); - clock.restore(); + transactionController.destroy(); }); }); }); @@ -2360,7 +2354,6 @@ describe('TransactionController Integration', () => { describe('startIncomingTransactionPolling', () => { // TODO(JL): IncomingTransactionHelper doesn't populate networkClientId on the generated tx object. Should it?.. it('should add incoming transactions to state with the correct chainId for the given networkClientId on the next block', async () => { - const clock = useFakeTimers(); mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ @@ -2402,61 +2395,63 @@ describe('TransactionController Integration', () => { const networkClientIds = Object.keys(networkClients).filter( (v) => v !== networkClientConfiguration.network, ); - for (const networkClientId of networkClientIds) { - const config = networkClients[networkClientId].configuration; - mockNetwork({ - networkClientConfiguration: config, - mocks: [ - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], + await Promise.all( + networkClientIds.map(async (networkClientId) => { + const config = networkClients[networkClientId].configuration; + mockNetwork({ + networkClientConfiguration: config, + mocks: [ + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, }, - response: { - result: '0x2', + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x2', + }, }, - }, - ], - }); - nock(getEtherscanApiHost(config.chainId)) - .get( - `/api?module=account&address=${selectedAddress}&offset=40&sort=desc&action=txlist&tag=latest&page=1`, - ) - .reply(200, ETHERSCAN_TRANSACTION_RESPONSE_MOCK); - - transactionController.startIncomingTransactionPolling([ - networkClientId, - ]); - - expectedLastFetchedBlockNumbers[ - `${config.chainId}#${selectedAddress}#normal` - ] = parseInt(ETHERSCAN_TRANSACTION_BASE_MOCK.blockNumber, 10); - expectedTransactions.push({ - blockNumber: ETHERSCAN_TRANSACTION_BASE_MOCK.blockNumber, - chainId: config.chainId, - type: TransactionType.incoming, - verifiedOnBlockchain: false, - status: TransactionStatus.confirmed, - }); - expectedTransactions.push({ - blockNumber: ETHERSCAN_TRANSACTION_BASE_MOCK.blockNumber, - chainId: config.chainId, - type: TransactionType.incoming, - verifiedOnBlockchain: false, - status: TransactionStatus.failed, - }); - } + ], + }); + nock(getEtherscanApiHost(config.chainId)) + .get( + `/api?module=account&address=${selectedAddress}&offset=40&sort=desc&action=txlist&tag=latest&page=1`, + ) + .reply(200, ETHERSCAN_TRANSACTION_RESPONSE_MOCK); + + transactionController.startIncomingTransactionPolling([ + networkClientId, + ]); + + expectedLastFetchedBlockNumbers[ + `${config.chainId}#${selectedAddress}#normal` + ] = parseInt(ETHERSCAN_TRANSACTION_BASE_MOCK.blockNumber, 10); + expectedTransactions.push({ + blockNumber: ETHERSCAN_TRANSACTION_BASE_MOCK.blockNumber, + chainId: config.chainId, + type: TransactionType.incoming, + verifiedOnBlockchain: false, + status: TransactionStatus.confirmed, + }); + expectedTransactions.push({ + blockNumber: ETHERSCAN_TRANSACTION_BASE_MOCK.blockNumber, + chainId: config.chainId, + type: TransactionType.incoming, + verifiedOnBlockchain: false, + status: TransactionStatus.failed, + }); + }), + ); await advanceTime({ clock, duration: 20000 }); expect(transactionController.state.transactions).toHaveLength( @@ -2470,13 +2465,12 @@ describe('TransactionController Integration', () => { expect(transactionController.state.lastFetchedBlockNumbers).toStrictEqual( expectedLastFetchedBlockNumbers, ); - clock.restore(); + transactionController.destroy(); }); }); describe('stopIncomingTransactionPolling', () => { it('should not poll for new incoming transactions for the given networkClientId', async () => { - const clock = useFakeTimers(); mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ @@ -2515,58 +2509,61 @@ describe('TransactionController Integration', () => { const networkClientIds = Object.keys(networkClients).filter( (v) => v !== networkClientConfiguration.network, ); - for (const networkClientId of networkClientIds) { - const config = networkClients[networkClientId].configuration; - mockNetwork({ - networkClientConfiguration: config, - mocks: [ - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], + await Promise.all( + networkClientIds.map(async (networkClientId) => { + const config = networkClients[networkClientId].configuration; + mockNetwork({ + networkClientConfiguration: config, + mocks: [ + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, }, - response: { - result: '0x2', + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x2', + }, }, - }, - ], - }); - nock(getEtherscanApiHost(config.chainId)) - .get( - `/api?module=account&address=${selectedAddress}&offset=40&sort=desc&action=txlist&tag=latest&page=1`, - ) - .reply(200, ETHERSCAN_TRANSACTION_RESPONSE_MOCK); - - transactionController.startIncomingTransactionPolling([ - networkClientId, - ]); - - transactionController.stopIncomingTransactionPolling([networkClientId]); - } + ], + }); + nock(getEtherscanApiHost(config.chainId)) + .get( + `/api?module=account&address=${selectedAddress}&offset=40&sort=desc&action=txlist&tag=latest&page=1`, + ) + .reply(200, ETHERSCAN_TRANSACTION_RESPONSE_MOCK); + + transactionController.startIncomingTransactionPolling([ + networkClientId, + ]); + + transactionController.stopIncomingTransactionPolling([ + networkClientId, + ]); + }), + ); await advanceTime({ clock, duration: 20000 }); expect(transactionController.state.transactions).toStrictEqual([]); expect(transactionController.state.lastFetchedBlockNumbers).toStrictEqual( {}, ); - clock.restore(); + transactionController.destroy(); }); }); describe('stopAllIncomingTransactionPolling', () => { it('should not poll for incoming transactions on any network client', async () => { - const clock = useFakeTimers(); mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ @@ -2605,43 +2602,45 @@ describe('TransactionController Integration', () => { const networkClientIds = Object.keys(networkClients).filter( (v) => v !== networkClientConfiguration.network, ); - for (const networkClientId of networkClientIds) { - const config = networkClients[networkClientId].configuration; - mockNetwork({ - networkClientConfiguration: config, - mocks: [ - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], + await Promise.all( + networkClientIds.map(async (networkClientId) => { + const config = networkClients[networkClientId].configuration; + mockNetwork({ + networkClientConfiguration: config, + mocks: [ + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, }, - response: { - result: '0x2', + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x2', + }, }, - }, - ], - }); - nock(getEtherscanApiHost(config.chainId)) - .get( - `/api?module=account&address=${selectedAddress}&offset=40&sort=desc&action=txlist&tag=latest&page=1`, - ) - .reply(200, ETHERSCAN_TRANSACTION_RESPONSE_MOCK); - - transactionController.startIncomingTransactionPolling([ - networkClientId, - ]); - } + ], + }); + nock(getEtherscanApiHost(config.chainId)) + .get( + `/api?module=account&address=${selectedAddress}&offset=40&sort=desc&action=txlist&tag=latest&page=1`, + ) + .reply(200, ETHERSCAN_TRANSACTION_RESPONSE_MOCK); + + transactionController.startIncomingTransactionPolling([ + networkClientId, + ]); + }), + ); transactionController.stopAllIncomingTransactionPolling(); await advanceTime({ clock, duration: 20000 }); @@ -2650,13 +2649,12 @@ describe('TransactionController Integration', () => { expect(transactionController.state.lastFetchedBlockNumbers).toStrictEqual( {}, ); - clock.restore(); + transactionController.destroy(); }); }); describe('updateIncomingTransactions', () => { it('should add incoming transactions to state with the correct chainId for the given networkClientId without waiting for the next block', async () => { - const clock = useFakeTimers(); mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ @@ -2698,51 +2696,53 @@ describe('TransactionController Integration', () => { const networkClientIds = Object.keys(networkClients).filter( (v) => v !== networkClientConfiguration.network, ); - for (const networkClientId of networkClientIds) { - const config = networkClients[networkClientId].configuration; - mockNetwork({ - networkClientConfiguration: config, - mocks: [ - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', + await Promise.all( + networkClientIds.map(async (networkClientId) => { + const config = networkClients[networkClientId].configuration; + mockNetwork({ + networkClientConfiguration: config, + mocks: [ + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, }, - }, - ], - }); - nock(getEtherscanApiHost(config.chainId)) - .get( - `/api?module=account&address=${selectedAddress}&offset=40&sort=desc&action=txlist&tag=latest&page=1`, - ) - .reply(200, ETHERSCAN_TRANSACTION_RESPONSE_MOCK); - - await transactionController.updateIncomingTransactions([ - networkClientId, - ]); - - expectedLastFetchedBlockNumbers[ - `${config.chainId}#${selectedAddress}#normal` - ] = parseInt(ETHERSCAN_TRANSACTION_BASE_MOCK.blockNumber, 10); - expectedTransactions.push({ - blockNumber: ETHERSCAN_TRANSACTION_BASE_MOCK.blockNumber, - chainId: config.chainId, - type: TransactionType.incoming, - verifiedOnBlockchain: false, - status: TransactionStatus.confirmed, - }); - expectedTransactions.push({ - blockNumber: ETHERSCAN_TRANSACTION_BASE_MOCK.blockNumber, - chainId: config.chainId, - type: TransactionType.incoming, - verifiedOnBlockchain: false, - status: TransactionStatus.failed, - }); - } + ], + }); + nock(getEtherscanApiHost(config.chainId)) + .get( + `/api?module=account&address=${selectedAddress}&offset=40&sort=desc&action=txlist&tag=latest&page=1`, + ) + .reply(200, ETHERSCAN_TRANSACTION_RESPONSE_MOCK); + + await transactionController.updateIncomingTransactions([ + networkClientId, + ]); + + expectedLastFetchedBlockNumbers[ + `${config.chainId}#${selectedAddress}#normal` + ] = parseInt(ETHERSCAN_TRANSACTION_BASE_MOCK.blockNumber, 10); + expectedTransactions.push({ + blockNumber: ETHERSCAN_TRANSACTION_BASE_MOCK.blockNumber, + chainId: config.chainId, + type: TransactionType.incoming, + verifiedOnBlockchain: false, + status: TransactionStatus.confirmed, + }); + expectedTransactions.push({ + blockNumber: ETHERSCAN_TRANSACTION_BASE_MOCK.blockNumber, + chainId: config.chainId, + type: TransactionType.incoming, + verifiedOnBlockchain: false, + status: TransactionStatus.failed, + }); + }), + ); expect(transactionController.state.transactions).toHaveLength( 2 * networkClientIds.length, @@ -2755,7 +2755,7 @@ describe('TransactionController Integration', () => { expect(transactionController.state.lastFetchedBlockNumbers).toStrictEqual( expectedLastFetchedBlockNumbers, ); - clock.restore(); + transactionController.destroy(); }); }); @@ -2797,40 +2797,47 @@ describe('TransactionController Integration', () => { const networkClientIds = Object.keys(networkClients).filter( (v) => v !== networkClientConfiguration.network, ); - for (const networkClientId of networkClientIds) { - const config = networkClients[networkClientId].configuration; - mockNetwork({ - networkClientConfiguration: config, - mocks: [ - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NonceTracker - { - request: { - method: 'eth_getTransactionCount', - params: [ACCOUNT_MOCK, '0x1'], + await Promise.all( + networkClientIds.map(async (networkClientId) => { + const config = networkClients[networkClientId].configuration; + mockNetwork({ + networkClientConfiguration: config, + mocks: [ + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, }, - response: { - result: '0xa', + // NonceTracker + { + request: { + method: 'eth_getTransactionCount', + params: [ACCOUNT_MOCK, '0x1'], + }, + response: { + result: '0xa', + }, }, - }, - ], - }); + ], + }); - const nonceLock = await transactionController.getNonceLock( - ACCOUNT_MOCK, - networkClientId, - ); - expect(nonceLock.nextNonce).toBe(10); - } + const nonceLockPromise = transactionController.getNonceLock( + ACCOUNT_MOCK, + networkClientId, + ); + await advanceTime({ clock, duration: 1 }); + + const nonceLock = await nonceLockPromise; + + expect(nonceLock.nextNonce).toBe(10); + }), + ); + transactionController.destroy(); }); it('should block other attempts to get the nonce lock from the nonceTracker until the first one is released for the given networkClientId', async () => { @@ -2870,64 +2877,72 @@ describe('TransactionController Integration', () => { const networkClientIds = Object.keys(networkClients).filter( (v) => v !== networkClientConfiguration.network, ); - for (const networkClientId of networkClientIds) { - const config = networkClients[networkClientId].configuration; - mockNetwork({ - networkClientConfiguration: config, - mocks: [ - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NonceTracker - { - request: { - method: 'eth_getTransactionCount', - params: [ACCOUNT_MOCK, '0x1'], + await Promise.all( + networkClientIds.map(async (networkClientId) => { + const config = networkClients[networkClientId].configuration; + mockNetwork({ + networkClientConfiguration: config, + mocks: [ + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, }, - response: { - result: '0xa', + // NonceTracker + { + request: { + method: 'eth_getTransactionCount', + params: [ACCOUNT_MOCK, '0x1'], + }, + response: { + result: '0xa', + }, }, - }, - ], - }); - - const firstNonceLock = await transactionController.getNonceLock( - ACCOUNT_MOCK, - networkClientId, - ); - - expect(firstNonceLock.nextNonce).toBe(10); - - const secondNonceLock = transactionController.getNonceLock( - ACCOUNT_MOCK, - networkClientId, - ); - const delay = () => - new Promise((resolve) => { - setTimeout(resolve, 100, null); + ], }); - let secondNonceLockIfAcquired = await Promise.race([ - secondNonceLock, - delay(), - ]); - expect(secondNonceLockIfAcquired).toBeNull(); + const firstNonceLockPromise = transactionController.getNonceLock( + ACCOUNT_MOCK, + networkClientId, + ); + await advanceTime({ clock, duration: 1 }); - await firstNonceLock.releaseLock(); + const firstNonceLock = await firstNonceLockPromise; - secondNonceLockIfAcquired = await Promise.race([ - secondNonceLock, - delay(), - ]); - expect(secondNonceLockIfAcquired?.nextNonce).toBe(10); - } + expect(firstNonceLock.nextNonce).toBe(10); + + const secondNonceLockPromise = transactionController.getNonceLock( + ACCOUNT_MOCK, + networkClientId, + ); + const delay = () => + new Promise(async (resolve) => { + await advanceTime({ clock, duration: 100 }); + resolve(null); + }); + + let secondNonceLockIfAcquired = await Promise.race([ + secondNonceLockPromise, + delay(), + ]); + expect(secondNonceLockIfAcquired).toBeNull(); + + await firstNonceLock.releaseLock(); + await advanceTime({ clock, duration: 1 }); + + secondNonceLockIfAcquired = await Promise.race([ + secondNonceLockPromise, + delay(), + ]); + expect(secondNonceLockIfAcquired?.nextNonce).toBe(10); + }), + ); + transactionController.destroy(); }); it('should get the nonce lock from the globally selected nonceTracker if no networkClientId is provided', async () => { @@ -2960,8 +2975,13 @@ describe('TransactionController Integration', () => { const { transactionController } = await newController({}); - const nonceLock = await transactionController.getNonceLock(ACCOUNT_MOCK); + const nonceLockPromise = transactionController.getNonceLock(ACCOUNT_MOCK); + await advanceTime({ clock, duration: 1 }); + + const nonceLock = await nonceLockPromise; + expect(nonceLock.nextNonce).toBe(10); + transactionController.destroy(); }); it('should block other attempts to get the nonce lock from the globally selected nonceTracker until the first one is released if no networkClientId is provided', async () => { @@ -2994,20 +3014,24 @@ describe('TransactionController Integration', () => { const { transactionController } = await newController({}); - const firstNonceLock = await transactionController.getNonceLock( - ACCOUNT_MOCK, - ); + const firstNonceLockPromise = + transactionController.getNonceLock(ACCOUNT_MOCK); + await advanceTime({ clock, duration: 1 }); + + const firstNonceLock = await firstNonceLockPromise; expect(firstNonceLock.nextNonce).toBe(10); - const secondNonceLock = transactionController.getNonceLock(ACCOUNT_MOCK); + const secondNonceLockPromise = + transactionController.getNonceLock(ACCOUNT_MOCK); const delay = () => - new Promise((resolve) => { - setTimeout(resolve, 100, null); + new Promise(async (resolve) => { + await advanceTime({ clock, duration: 100 }); + resolve(null); }); let secondNonceLockIfAcquired = await Promise.race([ - secondNonceLock, + secondNonceLockPromise, delay(), ]); expect(secondNonceLockIfAcquired).toBeNull(); @@ -3015,10 +3039,11 @@ describe('TransactionController Integration', () => { await firstNonceLock.releaseLock(); secondNonceLockIfAcquired = await Promise.race([ - secondNonceLock, + secondNonceLockPromise, delay(), ]); expect(secondNonceLockIfAcquired?.nextNonce).toBe(10); + transactionController.destroy(); }); }); }); From 80527f6f89c088d9e9f7357355ad865a53058e08 Mon Sep 17 00:00:00 2001 From: jiexi Date: Wed, 24 Jan 2024 15:42:42 -0800 Subject: [PATCH 035/100] Cleanup updateGasProperties (#3841) ## Explanation * Cleanup updateGasProperties * Use chainId from txMeta instead of the providers ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- .../src/TransactionController.ts | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 9d7513832c..eab14ab266 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -2187,19 +2187,12 @@ export class TransactionController extends BaseControllerV1< (await this.getEIP1559Compatibility(transactionMeta.networkClientId)) && transactionMeta.txParams.type !== TransactionEnvelopeType.legacy; - const { networkClientId } = transactionMeta; - let chainId; - let isCustomNetwork; + const { networkClientId, chainId } = transactionMeta; - if (networkClientId) { - const { configuration } = this.getNetworkClientById(networkClientId); - chainId = configuration.chainId; - isCustomNetwork = configuration.type === NetworkClientType.Custom; - } else { - const { providerConfig } = this.getNetworkState(); - chainId = providerConfig.chainId; - isCustomNetwork = providerConfig.type === NetworkType.rpc; - } + const isCustomNetwork = networkClientId + ? this.getNetworkClientById(networkClientId).configuration.type === + NetworkClientType.Custom + : this.getNetworkState().providerConfig.type === NetworkType.rpc; await updateGas({ ethQuery: this.#getEthQuery({ networkClientId, chainId }), From 5d583f72099c2b36332a945bd4d42576b2b6ecc4 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Wed, 24 Jan 2024 17:50:38 -0600 Subject: [PATCH 036/100] Al/1948/refactor etherscan remote transaction source (#3822) Fixes: https://github.com/MetaMask/MetaMask-planning/issues/1948 - Extracts `etherscanRemoteTransactionSource` helper from the `trackingMap` and creates a parallel mapping of this helper keyed by chainId. `IncomingTransactionHelpers` (still keyed by networkClientId) are then pointed to the `etherscanRemoteTransactionSource` that matches their chainId. - Adds a mutex internal to the `etherscanRremoteTransactionSource` and some setTimeout logic so that `IncomingTransactionHelpers` with the same chainId can only hit the etherscan API for that chainId max every 5 seconds. --------- Co-authored-by: jiexi --- .../src/TransactionController.test.ts | 23 +- .../src/TransactionController.ts | 52 +++- .../TransactionControllerIntegration.test.ts | 223 +++++++++++++++++- .../src/helpers/EtherscanMocks.ts | 2 +- .../EtherscanRemoteTransactionSource.ts | 35 ++- .../src/helpers/IncomingTransactionHelper.ts | 25 +- 6 files changed, 323 insertions(+), 37 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index c787c11c86..fbfc378f56 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -4785,8 +4785,22 @@ describe('TransactionController', () => { trackingMap.get('mainnet')?.pendingTransactionTracker, ).toBeDefined(); }); + }); + describe('stopTrackingByNetworkClientId', () => { it('should stop tracking in a tracking map', () => { - const controller = newController(); + const mockGetNetworkClientRegistry = jest.fn(); + mockGetNetworkClientRegistry.mockImplementation(() => ({ + mainnet: { + configuration: { + chainId: '0x1', + }, + }, + })); + const controller = newController({ + options: { + getNetworkClientRegistry: mockGetNetworkClientRegistry, + }, + }); const trackingMap = controller.startTrackingByNetworkClientId('mainnet'); const incomingTransactionHelper = trackingMap.get('mainnet')?.incomingTransactionHelper; @@ -4798,6 +4812,7 @@ describe('TransactionController', () => { expect(stopSpy).toHaveBeenCalledTimes(1); }); }); + describe('initTrackingMap', () => { // eslint-disable-next-line jest/no-done-callback it('should initialize the tracking map on construction', (done) => { @@ -4821,7 +4836,7 @@ describe('TransactionController', () => { it('should handle removals from the networkController registry', (done) => { const hub = new EventEmitter() as TransactionControllerEventEmitter; const mockGetNetworkClientRegistry = jest.fn(); - mockGetNetworkClientRegistry.mockImplementationOnce(() => ({ + mockGetNetworkClientRegistry.mockImplementation(() => ({ sepolia: { configuration: { chainId: SEPOLIA.chainId, @@ -4840,7 +4855,7 @@ describe('TransactionController', () => { })); hub.on('tracking-map-init', () => { mockGetNetworkClientRegistry.mockClear(); - mockGetNetworkClientRegistry.mockImplementationOnce(() => ({ + mockGetNetworkClientRegistry.mockImplementation(() => ({ sepolia: { configuration: { chainId: SEPOLIA.chainId, @@ -4879,7 +4894,7 @@ describe('TransactionController', () => { })); hub.on('tracking-map-init', () => { mockGetNetworkClientRegistry.mockClear(); - mockGetNetworkClientRegistry.mockImplementationOnce(() => ({ + mockGetNetworkClientRegistry.mockImplementation(() => ({ sepolia: { configuration: { chainId: SEPOLIA.chainId, diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index eab14ab266..17794ab140 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -380,6 +380,11 @@ export class TransactionController extends BaseControllerV1< } > = new Map(); + readonly etherscanRemoteTransactionSourcesMap: Map< + Hex, + EtherscanRemoteTransactionSource + > = new Map(); + /** * Method used to sign transactions */ @@ -622,6 +627,7 @@ export class TransactionController extends BaseControllerV1< }); if (shouldRefresh) { this.#refreshTrackingMap(); + this.#refreshEtherscanRemoteTransactionSources(); } }, ); @@ -645,10 +651,30 @@ export class TransactionController extends BaseControllerV1< this.#stopAllTracking(); } + #refreshEtherscanRemoteTransactionSources = () => { + // this will be prettier when we have consolidated network clients with a single chainId: + // check if there are still other network clients using the same chainId + // if not remove the etherscanRemoteTransaction source from the map + const networkClients = this.getNetworkClientRegistry(); + const chainIdsInRegistry = new Set(); + Object.values(networkClients).forEach((networkClient) => + chainIdsInRegistry.add(networkClient.configuration.chainId), + ); + const existingChainIds = Array.from( + this.etherscanRemoteTransactionSourcesMap.keys(), + ); + const chainIdsToRemove = existingChainIds.filter( + (chainId) => !chainIdsInRegistry.has(chainId), + ); + + chainIdsToRemove.forEach((chainId) => { + this.etherscanRemoteTransactionSourcesMap.delete(chainId); + }); + }; + #refreshTrackingMap = () => { const networkClients = this.getNetworkClientRegistry(); const networkClientIds = Object.keys(networkClients); - const existingNetworkClientIds = Array.from(this.trackingMap.keys()); // Remove tracking for NetworkClientIds that no longer exist @@ -1298,8 +1324,8 @@ export class TransactionController extends BaseControllerV1< this.removeIncomingTransactionHelperListeners( trackers.incomingTransactionHelper, ); + this.trackingMap.delete(networkClientId); } - this.trackingMap.delete(networkClientId); } #stopAllTracking() { @@ -1318,6 +1344,20 @@ export class TransactionController extends BaseControllerV1< startTrackingByNetworkClientId(networkClientId: NetworkClientId) { const networkClient = this.getNetworkClientById(networkClientId); const { chainId } = networkClient.configuration; + + let etherscanRemoteTransactionSource = + this.etherscanRemoteTransactionSourcesMap.get(chainId); + if (!etherscanRemoteTransactionSource) { + etherscanRemoteTransactionSource = new EtherscanRemoteTransactionSource({ + includeTokenTransfers: + this.incomingTransactionOptions.includeTokenTransfers, + }); + this.etherscanRemoteTransactionSourcesMap.set( + chainId, + etherscanRemoteTransactionSource, + ); + } + if (!this.nonceMutexByChainId.get(chainId)) { this.nonceMutexByChainId.set(chainId, new Mutex()); } @@ -1338,16 +1378,14 @@ export class TransactionController extends BaseControllerV1< ), }); - const etherscanRemoteTransactionSource = - new EtherscanRemoteTransactionSource({ - includeTokenTransfers: - this.incomingTransactionOptions.includeTokenTransfers, - }); const incomingTransactionHelper = new IncomingTransactionHelper({ blockTracker: networkClient.blockTracker, getCurrentAccount: this.getSelectedAddress, getLastFetchedBlockNumbers: () => this.state.lastFetchedBlockNumbers, // TODO(JL): Fix this type + // TODO (AD): + // This is a hack until we remove the base IncomingTransactionHelper class + // and should be replaced with a plain chainId parameter getNetworkState: () => { return { providerConfig: { chainId } as ProviderConfig, diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index 3f2da0e7a0..9e65a510df 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -18,11 +18,14 @@ import { mockNetwork } from '../../../tests/mock-network'; import { ETHERSCAN_TRANSACTION_BASE_MOCK, ETHERSCAN_TRANSACTION_RESPONSE_MOCK, + ETHERSCAN_TOKEN_TRANSACTION_MOCK, + ETHERSCAN_TRANSACTION_SUCCESS_MOCK, } from './helpers/EtherscanMocks'; import { TransactionController } from './TransactionController'; import type { TransactionMeta } from './types'; import { TransactionStatus, TransactionType } from './types'; import { getEtherscanApiHost } from './utils/etherscan'; +import * as etherscanUtils from './utils/etherscan'; const ACCOUNT_MOCK = '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207'; const ACCOUNT_2_MOCK = '0x08f137f335ea1b8f193b8f6ea92561a60d23a211'; @@ -2467,6 +2470,219 @@ describe('TransactionController Integration', () => { ); transactionController.destroy(); }); + + describe('when called with multiple networkClients which share the same chainId', () => { + it('should only call the etherscan API max every 5 seconds, alternating between the token and txlist endpoints', async () => { + const fetchEtherscanNativeTxFetchSpy = jest.spyOn( + etherscanUtils, + 'fetchEtherscanTransactions', + ); + + const fetchEtherscanTokenTxFetchSpy = jest.spyOn( + etherscanUtils, + 'fetchEtherscanTokenTransactions', + ); + + // mocking infura mainnet + mockNetwork({ + networkClientConfiguration: mainnetNetworkClientConfiguration, + mocks: [ + // NetworkController + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x2', + }, + }, + ], + }); + + // mocking infura goerli + mockNetwork({ + networkClientConfiguration, + mocks: [ + // NetworkController + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x2', + }, + }, + ], + }); + + // mock the other goerli network client node requests + mockNetwork({ + networkClientConfiguration: { + ...networkClientConfiguration, + type: NetworkClientType.Custom, + rpcUrl: 'https://mock.rpc.url', + }, + mocks: [ + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x2', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3', + }, + }, + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x4', + }, + }, + ], + }); + + const selectedAddress = ETHERSCAN_TRANSACTION_BASE_MOCK.to; + + const { networkController, transactionController } = + await newController({ + getSelectedAddress: () => selectedAddress, + }); + + const otherGoerliClientNetworkClientId = + await networkController.upsertNetworkConfiguration( + { + rpcUrl: 'https://mock.rpc.url', + chainId: networkClientConfiguration.chainId, + ticker: networkClientConfiguration.ticker, + }, + { + referrer: 'https://mock.referrer', + source: 'dapp', + }, + ); + + // Etherscan API Mocks + + // Non-token transactions + nock(getEtherscanApiHost(networkClientConfiguration.chainId)) + .get( + `/api?module=account&address=${ETHERSCAN_TRANSACTION_BASE_MOCK.to}&offset=40&sort=desc&action=txlist&tag=latest&page=1`, + ) + .reply(200, { + status: '1', + result: [{ ...ETHERSCAN_TRANSACTION_SUCCESS_MOCK, blockNumber: 1 }], + }) + // block 2 + .get( + `/api?module=account&address=${ETHERSCAN_TRANSACTION_BASE_MOCK.to}&startBlock=2&offset=40&sort=desc&action=txlist&tag=latest&page=1`, + ) + .reply(200, { + status: '1', + result: [{ ...ETHERSCAN_TRANSACTION_SUCCESS_MOCK, blockNumber: 2 }], + }) + .persist(); + + // token transactions + nock(getEtherscanApiHost(networkClientConfiguration.chainId)) + .get( + `/api?module=account&address=${ETHERSCAN_TRANSACTION_BASE_MOCK.to}&offset=40&sort=desc&action=tokentx&tag=latest&page=1`, + ) + .reply(200, { + status: '1', + result: [{ ...ETHERSCAN_TOKEN_TRANSACTION_MOCK, blockNumber: 1 }], + }) + .get( + `/api?module=account&address=${ETHERSCAN_TRANSACTION_BASE_MOCK.to}&startBlock=2&offset=40&sort=desc&action=tokentx&tag=latest&page=1`, + ) + .reply(200, { + status: '1', + result: [{ ...ETHERSCAN_TOKEN_TRANSACTION_MOCK, blockNumber: 2 }], + }) + .persist(); + + // start polling with two clients which share the same chainId + transactionController.startIncomingTransactionPolling([ + networkClientConfiguration.network, // 'goerli' + otherGoerliClientNetworkClientId, + ]); + await advanceTime({ clock, duration: 1 }); + expect(fetchEtherscanNativeTxFetchSpy).toHaveBeenCalledTimes(1); + expect(fetchEtherscanTokenTxFetchSpy).toHaveBeenCalledTimes(0); + await advanceTime({ clock, duration: 4999 }); + // after 5 seconds we can call to the etherscan API again, this time to the token transactions endpoint + expect(fetchEtherscanNativeTxFetchSpy).toHaveBeenCalledTimes(1); + expect(fetchEtherscanTokenTxFetchSpy).toHaveBeenCalledTimes(1); + await advanceTime({ clock, duration: 5000 }); + // after another 5 seconds there should be no new calls to the etherscan API + // since no new blocks events have occurred + expect(fetchEtherscanNativeTxFetchSpy).toHaveBeenCalledTimes(1); + expect(fetchEtherscanTokenTxFetchSpy).toHaveBeenCalledTimes(1); + // next block arrives after 20 seconds elapsed from first call + await advanceTime({ clock, duration: 10000 }); + await advanceTime({ clock, duration: 1 }); // flushes extra promises/setTimeouts + // first the native transactions are fetched + expect(fetchEtherscanNativeTxFetchSpy).toHaveBeenCalledTimes(2); + expect(fetchEtherscanTokenTxFetchSpy).toHaveBeenCalledTimes(1); + await advanceTime({ clock, duration: 4000 }); + // no new calls to the etherscan API since 5 seconds have not passed + expect(fetchEtherscanNativeTxFetchSpy).toHaveBeenCalledTimes(2); + expect(fetchEtherscanTokenTxFetchSpy).toHaveBeenCalledTimes(1); + await advanceTime({ clock, duration: 1000 }); // flushes extra promises/setTimeouts + // then once 5 seconds have passed since the previous call to the etherscan API + // we call the token transactions endpoint + expect(fetchEtherscanNativeTxFetchSpy).toHaveBeenCalledTimes(2); + expect(fetchEtherscanTokenTxFetchSpy).toHaveBeenCalledTimes(2); + + transactionController.destroy(); + }); + }); }); describe('stopIncomingTransactionPolling', () => { @@ -2720,9 +2936,7 @@ describe('TransactionController Integration', () => { ) .reply(200, ETHERSCAN_TRANSACTION_RESPONSE_MOCK); - await transactionController.updateIncomingTransactions([ - networkClientId, - ]); + transactionController.updateIncomingTransactions([networkClientId]); expectedLastFetchedBlockNumbers[ `${config.chainId}#${selectedAddress}#normal` @@ -2744,6 +2958,9 @@ describe('TransactionController Integration', () => { }), ); + // we have to wait for the mutex is released after the 5 second gap between API calls finishes + await advanceTime({ clock, duration: 5000 }); + expect(transactionController.state.transactions).toHaveLength( 2 * networkClientIds.length, ); diff --git a/packages/transaction-controller/src/helpers/EtherscanMocks.ts b/packages/transaction-controller/src/helpers/EtherscanMocks.ts index 0cdeaa2541..4d2385c029 100644 --- a/packages/transaction-controller/src/helpers/EtherscanMocks.ts +++ b/packages/transaction-controller/src/helpers/EtherscanMocks.ts @@ -40,7 +40,7 @@ const ETHERSCAN_TRANSACTION_ERROR_MOCK: EtherscanTransactionMeta = { isError: '1', }; -const ETHERSCAN_TOKEN_TRANSACTION_MOCK: EtherscanTokenTransactionMeta = { +export const ETHERSCAN_TOKEN_TRANSACTION_MOCK: EtherscanTokenTransactionMeta = { ...ETHERSCAN_TRANSACTION_BASE_MOCK, tokenDecimal: '456', tokenName: 'TestToken', diff --git a/packages/transaction-controller/src/helpers/EtherscanRemoteTransactionSource.ts b/packages/transaction-controller/src/helpers/EtherscanRemoteTransactionSource.ts index bf320bc8ee..470aec5683 100644 --- a/packages/transaction-controller/src/helpers/EtherscanRemoteTransactionSource.ts +++ b/packages/transaction-controller/src/helpers/EtherscanRemoteTransactionSource.ts @@ -1,5 +1,6 @@ import { BNToHex } from '@metamask/controller-utils'; import type { Hex } from '@metamask/utils'; +import { Mutex } from 'async-mutex'; import { BN } from 'ethereumjs-util'; import { v1 as random } from 'uuid'; @@ -33,6 +34,10 @@ export class EtherscanRemoteTransactionSource #isTokenRequestPending: boolean; + #mutex = new Mutex(); + + ETHERSCAN_RATE_LIMIT_INTERVAL = 5000; + constructor({ includeTokenTransfers, }: { includeTokenTransfers?: boolean } = {}) { @@ -51,20 +56,36 @@ export class EtherscanRemoteTransactionSource async fetchTransactions( request: RemoteTransactionSourceRequest, ): Promise { + const releaseLock = await this.#mutex.acquire(); + const acquiredTime = Date.now(); + const etherscanRequest: EtherscanTransactionRequest = { ...request, chainId: request.currentChainId, }; - const transactions = this.#isTokenRequestPending - ? await this.#fetchTokenTransactions(request, etherscanRequest) - : await this.#fetchNormalTransactions(request, etherscanRequest); + try { + const transactions = this.#isTokenRequestPending + ? await this.#fetchTokenTransactions(request, etherscanRequest) + : await this.#fetchNormalTransactions(request, etherscanRequest); - if (this.#includeTokenTransfers) { - this.#isTokenRequestPending = !this.#isTokenRequestPending; - } + if (this.#includeTokenTransfers) { + this.#isTokenRequestPending = !this.#isTokenRequestPending; + } - return transactions; + return transactions; + } finally { + const elapsedTime = Date.now() - acquiredTime; + const remainingTime = Math.max( + 0, + this.ETHERSCAN_RATE_LIMIT_INTERVAL - elapsedTime, + ); + // Wait for the remaining time if it hasn't been 5 seconds yet + if (remainingTime > 0) { + await new Promise((resolve) => setTimeout(resolve, remainingTime)); + } + releaseLock(); + } } #fetchNormalTransactions = async ( diff --git a/packages/transaction-controller/src/helpers/IncomingTransactionHelper.ts b/packages/transaction-controller/src/helpers/IncomingTransactionHelper.ts index 331b686145..a30b6ec4ca 100644 --- a/packages/transaction-controller/src/helpers/IncomingTransactionHelper.ts +++ b/packages/transaction-controller/src/helpers/IncomingTransactionHelper.ts @@ -128,11 +128,7 @@ export class IncomingTransactionHelper { const additionalLastFetchedKeys = this.#remoteTransactionSource.getLastBlockVariations?.() ?? []; - const fromBlock = this.#getFromBlock( - latestBlockNumber, - additionalLastFetchedKeys, - ); - + const fromBlock = this.#getFromBlock(latestBlockNumber); const address = this.#getCurrentAccount(); const currentChainId = this.#getCurrentChainId(); @@ -152,7 +148,6 @@ export class IncomingTransactionHelper { log('Error while fetching remote transactions', error); return; } - if (!this.#updateTransactions) { remoteTransactions = remoteTransactions.filter( (tx) => tx.txParams.to?.toLowerCase() === address.toLowerCase(), @@ -187,7 +182,6 @@ export class IncomingTransactionHelper { updated: updatedTransactions, }); } - this.#updateLastFetchedBlockNumber( remoteTransactions, additionalLastFetchedKeys, @@ -232,14 +226,16 @@ export class IncomingTransactionHelper { ); } - #getFromBlock( - latestBlockNumber: number, - additionalKeys: string[], - ): number | undefined { - const lastFetchedKey = this.#getBlockNumberKey(additionalKeys); + #getLastFetchedBlockNumberDec(): number { + const additionalLastFetchedKeys = + this.#remoteTransactionSource.getLastBlockVariations?.() ?? []; + const lastFetchedKey = this.#getBlockNumberKey(additionalLastFetchedKeys); + const lastFetchedBlockNumbers = this.#getLastFetchedBlockNumbers(); + return lastFetchedBlockNumbers[lastFetchedKey]; + } - const lastFetchedBlockNumber = - this.#getLastFetchedBlockNumbers()[lastFetchedKey]; + #getFromBlock(latestBlockNumber: number): number | undefined { + const lastFetchedBlockNumber = this.#getLastFetchedBlockNumberDec(); if (lastFetchedBlockNumber) { return lastFetchedBlockNumber + 1; @@ -280,7 +276,6 @@ export class IncomingTransactionHelper { } lastFetchedBlockNumbers[lastFetchedKey] = lastFetchedBlockNumber; - this.hub.emit('updatedLastFetchedBlockNumbers', { lastFetchedBlockNumbers, blockNumber: lastFetchedBlockNumber, From b9b6522e5dfb6fc4d23925b16fbc9333547f316b Mon Sep 17 00:00:00 2001 From: jiexi Date: Thu, 25 Jan 2024 16:10:37 -0800 Subject: [PATCH 037/100] Fix trackingMap and EtherscanRemoteTxSource maps removals (#3852) ## Explanation * Fix refreshing trackMap and EtherscanRemoteTransactionSourceMap ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- .../src/TransactionController.ts | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 17794ab140..ccd6287262 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -620,15 +620,16 @@ export class TransactionController extends BaseControllerV1< this.messagingSystem.subscribe( 'NetworkController:stateChange', (_, patches) => { - const shouldRefresh = patches.some((patch) => { - const correctOp = patch.op === 'add' || patch.op === 'remove'; - const correctPath = patch.path[0] === 'networkConfigurations'; - return correctOp && correctPath; + const networkClients = this.getNetworkClientRegistry(); + patches.forEach(({ op, path }) => { + if (op === 'remove' && path[0] === 'networkConfigurations') { + const networkClientId = path[1] as NetworkClientId; + delete networkClients[networkClientId]; + } }); - if (shouldRefresh) { - this.#refreshTrackingMap(); - this.#refreshEtherscanRemoteTransactionSources(); - } + + this.#refreshTrackingMap(networkClients); + this.#refreshEtherscanRemoteTransactionSources(networkClients); }, ); @@ -651,11 +652,13 @@ export class TransactionController extends BaseControllerV1< this.#stopAllTracking(); } - #refreshEtherscanRemoteTransactionSources = () => { + // TODO(JL): I think NetworkController should expose a NetworkClientRegistry type + #refreshEtherscanRemoteTransactionSources = ( + networkClients: ReturnType, + ) => { // this will be prettier when we have consolidated network clients with a single chainId: // check if there are still other network clients using the same chainId // if not remove the etherscanRemoteTransaction source from the map - const networkClients = this.getNetworkClientRegistry(); const chainIdsInRegistry = new Set(); Object.values(networkClients).forEach((networkClient) => chainIdsInRegistry.add(networkClient.configuration.chainId), @@ -672,8 +675,10 @@ export class TransactionController extends BaseControllerV1< }); }; - #refreshTrackingMap = () => { - const networkClients = this.getNetworkClientRegistry(); + // TODO(JL): I think NetworkController should expose a NetworkClientRegistry type + #refreshTrackingMap = ( + networkClients: ReturnType, + ) => { const networkClientIds = Object.keys(networkClients); const existingNetworkClientIds = Array.from(this.trackingMap.keys()); From f9c556f6dbd12febe0682cf77391eeedcdb21ba8 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Fri, 26 Jan 2024 10:20:17 -0600 Subject: [PATCH 038/100] remove getNetworkClientIdForDomain (#3857) cleanup --- .../src/TransactionController.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index ccd6287262..8493e19123 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -330,8 +330,6 @@ export class TransactionController extends BaseControllerV1< private readonly getNetworkClientRegistry: NetworkController['getNetworkClientRegistry']; - private readonly getNetworkClientIdForDomain: SelectedNetworkController['getNetworkClientIdForDomain']; - private failTransaction( transactionMeta: TransactionMeta, error: Error, @@ -427,7 +425,6 @@ export class TransactionController extends BaseControllerV1< * @param options.hooks.beforeCheckPendingTransaction - Additional logic to execute before checking pending transactions. Return false to prevent the broadcast of the transaction. * @param options.hooks.beforePublish - Additional logic to execute before publishing a transaction. Return false to prevent the broadcast of the transaction. * @param options.hooks.getAdditionalSignArguments - Returns additional arguments required to sign a transaction. - * @param options.getNetworkClientIdForDomain - Gets the network client id for the given domain. * @param options.hub - Use a different event emitter for the hub. * @param options.getNetworkClientRegistry - Gets the network client registry. * @param config - Initial options used to configure this controller. @@ -457,7 +454,6 @@ export class TransactionController extends BaseControllerV1< speedUpMultiplier, findNetworkClientIdByChainId, getNetworkClientById, - getNetworkClientIdForDomain, getNetworkClientRegistry, hub, hooks = {}, @@ -490,7 +486,6 @@ export class TransactionController extends BaseControllerV1< findNetworkClientIdByChainId: NetworkController['findNetworkClientIdByChainId']; getNetworkClientById: NetworkController['getNetworkClientById']; getNetworkClientRegistry: NetworkController['getNetworkClientRegistry']; - getNetworkClientIdForDomain: SelectedNetworkController['getNetworkClientIdForDomain']; hub: TransactionControllerEventEmitter; hooks: { afterSign?: ( @@ -552,8 +547,6 @@ export class TransactionController extends BaseControllerV1< this.speedUpMultiplier = speedUpMultiplier ?? SPEED_UP_RATE; this.incomingTransactionOptions = incomingTransactions; - this.getNetworkClientIdForDomain = getNetworkClientIdForDomain; - this.afterSign = hooks?.afterSign ?? (() => true); this.beforeApproveOnInit = hooks?.beforeApproveOnInit ?? (() => true); this.beforeCheckPendingTransaction = @@ -838,11 +831,6 @@ export class TransactionController extends BaseControllerV1< ): Promise { log('Adding transaction', txParams); - // TODO(JL): Revisit this fallback during implementation - // networkClientId ??= this.getNetworkClientIdForDomain( - // origin ?? ORIGIN_METAMASK, - // ); - txParams = normalizeTxParams(txParams); const isEIP1559Compatible = await this.getEIP1559Compatibility( From 466eaf563d36427ed28a7840e5848431e9732798 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 26 Jan 2024 10:25:59 -0800 Subject: [PATCH 039/100] remove selected-network-controller dep --- packages/transaction-controller/package.json | 2 -- packages/transaction-controller/src/TransactionController.ts | 1 - yarn.lock | 2 -- 3 files changed, 5 deletions(-) diff --git a/packages/transaction-controller/package.json b/packages/transaction-controller/package.json index 122310b104..6cacf0f441 100644 --- a/packages/transaction-controller/package.json +++ b/packages/transaction-controller/package.json @@ -42,7 +42,6 @@ "@metamask/metamask-eth-abis": "^3.0.0", "@metamask/network-controller": "^17.2.0", "@metamask/rpc-errors": "^6.1.0", - "@metamask/selected-network-controller": "^7.0.0", "@metamask/utils": "^8.3.0", "async-mutex": "^0.2.6", "eth-method-registry": "^3.0.0", @@ -71,7 +70,6 @@ "@metamask/approval-controller": "^5.1.2", "@metamask/gas-fee-controller": "^13.0.0", "@metamask/network-controller": "^17.2.0", - "@metamask/selected-network-controller": "^7.0.0", "babel-runtime": "^6.26.0" }, "engines": { diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 8493e19123..e3486b19c7 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -34,7 +34,6 @@ import type { import { NetworkClientType } from '@metamask/network-controller'; import type { AutoManagedNetworkClient } from '@metamask/network-controller/src/create-auto-managed-network-client'; import { errorCodes, rpcErrors, providerErrors } from '@metamask/rpc-errors'; -import type { SelectedNetworkController } from '@metamask/selected-network-controller'; import type { Hex } from '@metamask/utils'; import { Mutex } from 'async-mutex'; import { MethodRegistry } from 'eth-method-registry'; diff --git a/yarn.lock b/yarn.lock index 8fd7257a32..52d3a437cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2910,7 +2910,6 @@ __metadata: "@metamask/metamask-eth-abis": ^3.0.0 "@metamask/network-controller": ^17.2.0 "@metamask/rpc-errors": ^6.1.0 - "@metamask/selected-network-controller": ^7.0.0 "@metamask/utils": ^8.3.0 "@types/jest": ^27.4.1 "@types/node": ^16.18.54 @@ -2934,7 +2933,6 @@ __metadata: "@metamask/approval-controller": ^5.1.2 "@metamask/gas-fee-controller": ^13.0.0 "@metamask/network-controller": ^17.2.0 - "@metamask/selected-network-controller": ^7.0.0 babel-runtime: ^6.26.0 languageName: unknown linkType: soft From a78ff3f73dbf5bba198551df96b6bb5a5dac5f9c Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 26 Jan 2024 10:27:46 -0800 Subject: [PATCH 040/100] remove selected-network-controller from tsconfig --- packages/transaction-controller/tsconfig.build.json | 3 +-- packages/transaction-controller/tsconfig.json | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/transaction-controller/tsconfig.build.json b/packages/transaction-controller/tsconfig.build.json index 6c01c7a8cb..504559792e 100644 --- a/packages/transaction-controller/tsconfig.build.json +++ b/packages/transaction-controller/tsconfig.build.json @@ -10,8 +10,7 @@ { "path": "../base-controller/tsconfig.build.json" }, { "path": "../controller-utils/tsconfig.build.json" }, { "path": "../gas-fee-controller/tsconfig.build.json" }, - { "path": "../network-controller/tsconfig.build.json" }, - { "path": "../selected-network-controller/tsconfig.build.json" } + { "path": "../network-controller/tsconfig.build.json" } ], "include": ["../../types", "./src"] } diff --git a/packages/transaction-controller/tsconfig.json b/packages/transaction-controller/tsconfig.json index fcc5337587..01c431b29e 100644 --- a/packages/transaction-controller/tsconfig.json +++ b/packages/transaction-controller/tsconfig.json @@ -8,8 +8,7 @@ { "path": "../base-controller" }, { "path": "../controller-utils" }, { "path": "../gas-fee-controller" }, - { "path": "../network-controller" }, - { "path": "../selected-network-controller" } + { "path": "../network-controller" } ], "include": ["../../types", "./src"] } From fa561f406c098d043ecc1fff194f5f340e78218c Mon Sep 17 00:00:00 2001 From: Shane Date: Fri, 26 Jan 2024 11:03:57 -0800 Subject: [PATCH 041/100] Transaction multichain change rpc url (#3846) ## Explanation ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- .../TransactionControllerIntegration.test.ts | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index 9e65a510df..a1288533d0 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -2354,6 +2354,109 @@ describe('TransactionController Integration', () => { }); }); + describe('when changing rpcUrl of networkClient', () => { + it('should start tracking when a new network is added', async () => { + mockNetwork({ + networkClientConfiguration: mainnetNetworkClientConfiguration, + mocks: [ + // NetworkController + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x2', + }, + }, + ], + }); + const { networkController, transactionController } = + await newController(); + const startTrackinSpy = jest.spyOn( + transactionController, + 'startTrackingByNetworkClientId', + ); + + await networkController.upsertNetworkConfiguration( + { + ...networkClientConfiguration, + rpcUrl: 'https://mock.rpc.url', + }, + { setActive: false, referrer: 'https://mock.referrer', source: 'dapp' }, + ); + + expect(startTrackinSpy).toHaveBeenCalledTimes(1); + expect(transactionController).toBeDefined(); + }); + it('should stop tracking when a network is removed', async () => { + mockNetwork({ + networkClientConfiguration: mainnetNetworkClientConfiguration, + mocks: [ + // NetworkController + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x2', + }, + }, + ], + }); + const { networkController, transactionController } = + await newController(); + const stopTrackinSpy = jest.spyOn( + transactionController, + 'stopTrackingByNetworkClientId', + ); + + const configurationId = + await networkController.upsertNetworkConfiguration( + { + ...networkClientConfiguration, + rpcUrl: 'https://mock.rpc.url', + }, + { + setActive: false, + referrer: 'https://mock.referrer', + source: 'dapp', + }, + ); + + networkController.removeNetworkConfiguration(configurationId); + + // advance time to trigger events + await advanceTime({ clock, duration: 1000 }); + + expect(stopTrackinSpy).toHaveBeenCalledTimes(1); + expect(transactionController).toBeDefined(); + }); + }); + describe('startIncomingTransactionPolling', () => { // TODO(JL): IncomingTransactionHelper doesn't populate networkClientId on the generated tx object. Should it?.. it('should add incoming transactions to state with the correct chainId for the given networkClientId on the next block', async () => { From ea2fbe1266bbeb01f6299fa8273f44f870c51c89 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 26 Jan 2024 12:02:13 -0800 Subject: [PATCH 042/100] delete comments --- packages/transaction-controller/src/TransactionController.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 18cd2cec7f..ba4c6a802c 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -1447,7 +1447,6 @@ export class TransactionController extends BaseControllerV1< * @param networkClientId - The network client id to use for the estimate. */ async estimateGasBuffered( - // NOTE(JL): Need to update SwapsController's usage of this method transaction: TransactionParams, multiplier: number, networkClientId?: NetworkClientId, @@ -1805,8 +1804,6 @@ export class TransactionController extends BaseControllerV1< address: string, networkClientId?: NetworkClientId, ): Promise { - // TODO(JL): SmartTransactionController reaches into TransactionController.nonceTracker directly. Should probably change this. - // TODO(JL): Revisit this method. It's a bit complicated and not obvious what it achieves. let nonceMutexForChainId: Mutex | undefined; let { nonceTracker } = this; if (networkClientId) { From b6b4e3769a9a3dfbd7a5c6ae68c0e4c3b7334fd5 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 26 Jan 2024 12:04:03 -0800 Subject: [PATCH 043/100] delete comment --- packages/transaction-controller/src/TransactionController.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index ba4c6a802c..03c7316c8b 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -3076,7 +3076,6 @@ export class TransactionController extends BaseControllerV1< chainId, ); - // TODO(JL): modify getExternalPendingTransactions in extension to accept a chainId to filter for smartTransactions by chainId const externalPendingTransactions = this.getExternalPendingTransactions( address, chainId, From c8bb6bfdf0a0deb8ada8d4878fcf3855bf90f18d Mon Sep 17 00:00:00 2001 From: jiexi Date: Fri, 26 Jan 2024 12:13:55 -0800 Subject: [PATCH 044/100] Fix pending tx options mutlichain instantiation (#3858) ## Explanation ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- .../src/TransactionController.ts | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 03c7316c8b..8ee18aa247 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -192,7 +192,7 @@ export const SPEED_UP_RATE = 1.1; /** * @type IncomingTransactionOptions * - * Configuration options for incoming transaction support + * Configuration options for the IncomingTransactionHelper * @property includeTokenTransfers - Whether or not to include ERC20 token transfers. * @property isEnabled - Whether or not incoming transaction retrieval is enabled. * @property queryEntireHistory - Whether to initially query the entire transaction history or only recent blocks. @@ -205,6 +205,16 @@ type IncomingTransactionOptions = { updateTransactions?: boolean; }; +/** + * @type PendingTransactionOptions + * + * Configuration options for the PendingTransactionTracker + * @property isResubmitEnabled - Whether transaction publishing is automatically retried. + */ +type PendingTransactionOptions = { + isResubmitEnabled?: boolean; +}; + /** * The name of the {@link TransactionController}. */ @@ -304,6 +314,8 @@ export class TransactionController extends BaseControllerV1< private readonly incomingTransactionOptions: IncomingTransactionOptions; + private readonly pendingTransactionOptions: PendingTransactionOptions; + private readonly afterSign: ( transactionMeta: TransactionMeta, signedTx: TypedTransaction, @@ -412,7 +424,6 @@ export class TransactionController extends BaseControllerV1< * @param options.messenger - The controller messenger. * @param options.onNetworkStateChange - Allows subscribing to network controller state changes. * @param options.pendingTransactions - Configuration options for pending transaction support. - * @param options.pendingTransactions.isResubmitEnabled - Whether transaction publishing is automatically retried. * @param options.provider - The provider used to create the underlying EthQuery instance. * @param options.securityProviderRequest - A function for verifying a transaction, whether it is malicious or not. * @param options.speedUpMultiplier - Multiplier used to determine a transaction's increased gas fee during speed up. @@ -476,9 +487,7 @@ export class TransactionController extends BaseControllerV1< incomingTransactions?: IncomingTransactionOptions; messenger: TransactionControllerMessenger; onNetworkStateChange: (listener: (state: NetworkState) => void) => void; - pendingTransactions?: { - isResubmitEnabled?: boolean; - }; + pendingTransactions?: PendingTransactionOptions; provider: Provider; securityProviderRequest?: SecurityProviderRequest; speedUpMultiplier?: number; @@ -545,6 +554,7 @@ export class TransactionController extends BaseControllerV1< this.cancelMultiplier = cancelMultiplier ?? CANCEL_RATE; this.speedUpMultiplier = speedUpMultiplier ?? SPEED_UP_RATE; this.incomingTransactionOptions = incomingTransactions; + this.pendingTransactionOptions = pendingTransactions; this.afterSign = hooks?.afterSign ?? (() => true); this.beforeApproveOnInit = hooks?.beforeApproveOnInit ?? (() => true); @@ -1398,7 +1408,7 @@ export class TransactionController extends BaseControllerV1< getChainId: () => networkClient.configuration.chainId, getEthQuery: () => ethQuery, getTransactions: () => this.state.transactions, - isResubmitEnabled: true, // TODO: make this configurable + isResubmitEnabled: this.pendingTransactionOptions.isResubmitEnabled, nonceTracker, publishTransaction: this.publishTransaction.bind(this), hooks: { From 07fb74ca115bbf7835c4c894a964eae45cb87c0f Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 26 Jan 2024 13:07:16 -0800 Subject: [PATCH 045/100] lol --- .../src/TransactionControllerIntegration.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index a1288533d0..1505ec7957 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -102,9 +102,7 @@ const newController = async (options: any = {}) => { ...opts, }, { - // TODO(JL): fix this type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - sign: async (transaction: any) => transaction, + sign: (transaction) => Promise.resolve(transaction), ...config, }, state, From 689fcacd0fac258632c2899d8c40fe2dd14a4fdf Mon Sep 17 00:00:00 2001 From: jiexi Date: Fri, 26 Jan 2024 13:08:26 -0800 Subject: [PATCH 046/100] Update packages/transaction-controller/src/TransactionControllerIntegration.test.ts --- .../src/TransactionControllerIntegration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index 1505ec7957..83f9c64d16 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -148,7 +148,7 @@ describe('TransactionController Integration', () => { transactionController.destroy(); }); - it('should submit all approved transactions in state when the controller is constructed', async () => { + it('should submit all approved transactions in state', async () => { mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ From 4e9d346cbbb05e49e4248c47a5efcebc8253c2ad Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 26 Jan 2024 13:12:43 -0800 Subject: [PATCH 047/100] remove mock tx history in "should submit all approved transactions in state" --- .../TransactionControllerIntegration.test.ts | 88 +------------------ 1 file changed, 2 insertions(+), 86 deletions(-) diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index 1505ec7957..4a36a1a6d3 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -261,49 +261,7 @@ describe('TransactionController Integration', () => { }, userFeeLevel: 'dappSuggested', sendFlowHistory: [], - history: [ - { - chainId: '0x5', - id: 'ecfe8c60-ba27-11ee-8643-dfd28279a442', - status: 'unapproved', - time: 1706039113766, - txParams: { - from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - to: '0x08f137f335ea1b8f193b8f6ea92561a60d23a211', - value: '0x0', - gas: '0x5208', - maxFeePerGas: '0x1', - maxPriorityFeePerGas: '0x1', - }, - userEditedGasLimit: false, - verifiedOnBlockchain: false, - type: 'simpleSend', - networkClientId: 'goerli', - originalGasEstimate: '0x5208', - defaultGasEstimates: { - gas: '0x5208', - maxFeePerGas: '0x1', - maxPriorityFeePerGas: '0x1', - estimateType: 'dappSuggested', - }, - userFeeLevel: 'dappSuggested', - sendFlowHistory: [], - }, - [ - { - op: 'add', - path: '/txParams/nonce', - value: '0x1', - note: 'TransactionController#approveTransaction - Transaction approved', - timestamp: 1706039113767, - }, - { - op: 'replace', - path: '/status', - value: 'approved', - }, - ], - ], + history: [{}, []], }, { actionId: undefined, @@ -339,49 +297,7 @@ describe('TransactionController Integration', () => { }, userFeeLevel: 'dappSuggested', sendFlowHistory: [], - history: [ - { - chainId: '0xaa36a7', - id: 'c4cc0ff0-ba28-11ee-926f-55a7f9c2c2c6', - status: 'unapproved', - time: 1706039113766, - txParams: { - from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - to: '0x08f137f335ea1b8f193b8f6ea92561a60d23a211', - value: '0x0', - gas: '0x5208', - maxFeePerGas: '0x1', - maxPriorityFeePerGas: '0x1', - }, - userEditedGasLimit: false, - verifiedOnBlockchain: false, - type: 'simpleSend', - networkClientId: 'sepolia', - originalGasEstimate: '0x5208', - defaultGasEstimates: { - gas: '0x5208', - maxFeePerGas: '0x1', - maxPriorityFeePerGas: '0x1', - estimateType: 'dappSuggested', - }, - userFeeLevel: 'dappSuggested', - sendFlowHistory: [], - }, - [ - { - op: 'add', - path: '/txParams/nonce', - value: '0x1', - note: 'TransactionController#approveTransaction - Transaction approved', - timestamp: 1706039113767, - }, - { - op: 'replace', - path: '/status', - value: 'approved', - }, - ], - ], + history: [{}, []], }, ], }, From 45e47447b1aeb95e89f532de6e9606cfe95f833b Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 26 Jan 2024 13:23:18 -0800 Subject: [PATCH 048/100] switch eth_getCode to non contract. Fix sendRaw --- .../TransactionControllerIntegration.test.ts | 34 ++++++------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index 0380b6938b..c68e704331 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -766,9 +766,7 @@ describe('TransactionController Integration', () => { params: [ACCOUNT_2_MOCK, '0x1'], }, response: { - result: - // what should this be? - '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024657468546f546f6b656e53776170496e7075742875696e743235362c75696e743235362900000000000000000000000000000000000000000000000000000000', + result: '0x', // non contract }, }, { @@ -821,7 +819,7 @@ describe('TransactionController Integration', () => { request: { method: 'eth_sendRawTransaction', params: [ - '0x02e005010101019408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + '0x02e2050101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', ], }, response: { @@ -881,9 +879,7 @@ describe('TransactionController Integration', () => { params: [ACCOUNT_2_MOCK, '0x1'], }, response: { - result: - // what should this be? - '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024657468546f546f6b656e53776170496e7075742875696e743235362c75696e743235362900000000000000000000000000000000000000000000000000000000', + result: '0x', // non contract }, }, { @@ -936,7 +932,7 @@ describe('TransactionController Integration', () => { request: { method: 'eth_sendRawTransaction', params: [ - '0x02e383aa36a7010101019408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + '0x02e583aa36a70101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', ], }, response: { @@ -1764,9 +1760,7 @@ describe('TransactionController Integration', () => { params: [ACCOUNT_2_MOCK, '0x1'], }, response: { - result: - // what should this be? - '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024657468546f546f6b656e53776170496e7075742875696e743235362c75696e743235362900000000000000000000000000000000000000000000000000000000', + result: '0x', // non contract }, }, { @@ -1828,7 +1822,7 @@ describe('TransactionController Integration', () => { request: { method: 'eth_sendRawTransaction', params: [ - '0x02e005010101019408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + '0x02e2050101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', ], }, response: { @@ -1893,9 +1887,7 @@ describe('TransactionController Integration', () => { params: [ACCOUNT_2_MOCK, '0x1'], }, response: { - result: - // what should this be? - '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024657468546f546f6b656e53776170496e7075742875696e743235362c75696e743235362900000000000000000000000000000000000000000000000000000000', + result: '0x', // non contract }, }, { @@ -2110,9 +2102,7 @@ describe('TransactionController Integration', () => { params: [ACCOUNT_2_MOCK, '0x1'], }, response: { - result: - // what should this be? - '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024657468546f546f6b656e53776170496e7075742875696e743235362c75696e743235362900000000000000000000000000000000000000000000000000000000', + result: '0x', // non contract }, }, { @@ -2121,9 +2111,7 @@ describe('TransactionController Integration', () => { params: [ACCOUNT_3_MOCK, '0x1'], }, response: { - result: - // what should this be? - '0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000024657468546f546f6b656e53776170496e7075742875696e743235362c75696e743235362900000000000000000000000000000000000000000000000000000000', + result: '0x', // non contract }, }, { @@ -2176,7 +2164,7 @@ describe('TransactionController Integration', () => { request: { method: 'eth_sendRawTransaction', params: [ - '0x02e005010101019408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + '0x02e2050101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', ], }, response: { @@ -2200,7 +2188,7 @@ describe('TransactionController Integration', () => { request: { method: 'eth_sendRawTransaction', params: [ - '0x02e0050201018094e688b84b23f322a994a53dbf8e15fa82cdb711278080c0808080', + '0x02e20502010182520894e688b84b23f322a994a53dbf8e15fa82cdb711278080c0808080', ], }, response: { From 13ed1e8bce76debf9ef9a90f5b29f8cfc13b25f2 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 26 Jan 2024 13:25:47 -0800 Subject: [PATCH 049/100] update comments about stacking nock --- .../src/TransactionControllerIntegration.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index c68e704331..3acfdcbb4c 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -2399,7 +2399,7 @@ describe('TransactionController Integration', () => { const expectedTransactions: Partial[] = []; const networkClients = networkController.getNetworkClientRegistry(); - // NOTE(JL): This doesn't seem to work for the globally selected provider because of nock getting stacked on mainnet.infura.io twice + // Skip the globally selected provider because we can't use nock to mock it twice const networkClientIds = Object.keys(networkClients).filter( (v) => v !== networkClientConfiguration.network, ); @@ -2726,7 +2726,7 @@ describe('TransactionController Integration', () => { }); const networkClients = networkController.getNetworkClientRegistry(); - // NOTE(JL): This doesn't seem to work for the globally selected provider because of nock getting stacked on mainnet.infura.io twice + // Skip the globally selected provider because we can't use nock to mock it twice const networkClientIds = Object.keys(networkClients).filter( (v) => v !== networkClientConfiguration.network, ); @@ -2819,7 +2819,7 @@ describe('TransactionController Integration', () => { }); const networkClients = networkController.getNetworkClientRegistry(); - // NOTE(JL): This doesn't seem to work for the globally selected provider because of nock getting stacked on mainnet.infura.io twice + // Skip the globally selected provider because we can't use nock to mock it twice const networkClientIds = Object.keys(networkClients).filter( (v) => v !== networkClientConfiguration.network, ); @@ -2913,7 +2913,7 @@ describe('TransactionController Integration', () => { const expectedTransactions: Partial[] = []; const networkClients = networkController.getNetworkClientRegistry(); - // NOTE(JL): This doesn't seem to work for the globally selected provider because of nock getting stacked on mainnet.infura.io twice + // Skip the globally selected provider because we can't use nock to mock it twice const networkClientIds = Object.keys(networkClients).filter( (v) => v !== networkClientConfiguration.network, ); @@ -3015,7 +3015,7 @@ describe('TransactionController Integration', () => { ); const networkClients = networkController.getNetworkClientRegistry(); - // NOTE(JL): This doesn't seem to work for the globally selected provider because of nock getting stacked on mainnet.infura.io twice + // Skip the globally selected provider because we can't use nock to mock it twice const networkClientIds = Object.keys(networkClients).filter( (v) => v !== networkClientConfiguration.network, ); @@ -3095,7 +3095,7 @@ describe('TransactionController Integration', () => { ); const networkClients = networkController.getNetworkClientRegistry(); - // NOTE(JL): This doesn't seem to work for the globally selected provider because of nock getting stacked on mainnet.infura.io twice + // Skip the globally selected provider because we can't use nock to mock it twice const networkClientIds = Object.keys(networkClients).filter( (v) => v !== networkClientConfiguration.network, ); From ecea7a9de36352bc3293b0c710b31ec3668a2606 Mon Sep 17 00:00:00 2001 From: jiexi Date: Fri, 26 Jan 2024 13:26:53 -0800 Subject: [PATCH 050/100] Update packages/transaction-controller/src/TransactionController.test.ts --- .../transaction-controller/src/TransactionController.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 95c4c5f573..658300aec8 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -276,7 +276,7 @@ function buildMockMessenger({ handler({}, [ { op: 'add', - path: ['networkConfigurations', 'foo', 'bar'], + path: ['networkConfigurations', 'foo'], value: 'foo', }, ]); From 352575ac7cd3362b7bb97a9817395ce13ace90d3 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 26 Jan 2024 13:29:45 -0800 Subject: [PATCH 051/100] remove network type comment --- packages/transaction-controller/src/TransactionController.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 8ee18aa247..e09c99b511 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -654,7 +654,6 @@ export class TransactionController extends BaseControllerV1< this.#stopAllTracking(); } - // TODO(JL): I think NetworkController should expose a NetworkClientRegistry type #refreshEtherscanRemoteTransactionSources = ( networkClients: ReturnType, ) => { @@ -677,7 +676,6 @@ export class TransactionController extends BaseControllerV1< }); }; - // TODO(JL): I think NetworkController should expose a NetworkClientRegistry type #refreshTrackingMap = ( networkClients: ReturnType, ) => { From a4e6e80a9cdb37a8d2bee9ba9c62b257639ce914 Mon Sep 17 00:00:00 2001 From: Shane Date: Fri, 26 Jan 2024 13:48:37 -0800 Subject: [PATCH 052/100] Added feature flag for multichain (#3851) ## Explanation ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --------- Co-authored-by: jiexi Co-authored-by: Alex Donesky --- .../src/TransactionController.test.ts | 57 +++++++++++-- .../src/TransactionController.ts | 82 ++++++++++++------- .../TransactionControllerIntegration.test.ts | 64 +++++++++++++++ 3 files changed, 166 insertions(+), 37 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 658300aec8..748c82ed15 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -807,8 +807,7 @@ describe('TransactionController', () => { ACCOUNT_MOCK, ); - // gets called in constructor now - expect(nonceTrackerMock).toHaveBeenCalledTimes(4); + expect(nonceTrackerMock).toHaveBeenCalledTimes(1); expect(pendingTransactions).toStrictEqual([ expect.any(Object), ...externalPendingTransactions, @@ -1214,7 +1213,9 @@ describe('TransactionController', () => { describe('multichain', () => { it('adds unapproved transaction to state when using networkClientId', async () => { - const controller = newController(); + const controller = newController({ + options: { enableMultiChain: true }, + }); const sepoliaTxParams: TransactionParams = { chainId: SEPOLIA.chainId, from: ACCOUNT_MOCK, @@ -1237,7 +1238,10 @@ describe('TransactionController', () => { expect(transactionMeta.origin).toBe('metamask'); }); it('adds unapproved transaction with networkClientId and can be updated to submitted', async () => { - const controller = newController({ approve: true }); + const controller = newController({ + approve: true, + options: { enableMultichain: true }, + }); const submittedEventListener = jest.fn(); controller.hub.on('transaction-submitted', submittedEventListener); @@ -4863,6 +4867,18 @@ describe('TransactionController', () => { }); describe('initTrackingMap', () => { + it('doesnt get called if the feature flag is disabled', () => { + const hub = new EventEmitter() as TransactionControllerEventEmitter; + const spy = jest.fn(); + hub.on('tracking-map-init', spy); + newController({ + options: { + enableMultichain: false, + hub, + }, + }); + expect(spy).not.toHaveBeenCalled(); + }); // eslint-disable-next-line jest/no-done-callback it('should initialize the tracking map on construction', (done) => { const hub = new EventEmitter() as TransactionControllerEventEmitter; @@ -4876,6 +4892,7 @@ describe('TransactionController', () => { }); const controller = newController({ options: { + enableMultichain: true, hub, }, }); @@ -4924,6 +4941,7 @@ describe('TransactionController', () => { const controller = newController({ options: { getNetworkClientRegistry: mockGetNetworkClientRegistry, + enableMultichain: true, hub, }, }); @@ -4979,6 +4997,7 @@ describe('TransactionController', () => { options: { messenger: mockMessenger.messenger, getNetworkClientRegistry: mockGetNetworkClientRegistry, + enableMultichain: true, hub, }, }); @@ -4987,7 +5006,11 @@ describe('TransactionController', () => { describe('startIncomingTransactionPolling', () => { it('should start the incoming transaction helper for the specific networkClientIds provided', () => { - const controller = newController(); + const controller = newController({ + options: { + enableMultichain: true, + }, + }); const trackingMap = controller.startTrackingByNetworkClientId('mainnet'); controller.startTrackingByNetworkClientId('sepolia'); @@ -5002,7 +5025,11 @@ describe('TransactionController', () => { }); it('should start the global incoming transaction helper when no networkClientIds provided', () => { - const controller = newController(); + const controller = newController({ + options: { + enableMultichain: true, + }, + }); controller.startIncomingTransactionPolling([]); @@ -5012,7 +5039,11 @@ describe('TransactionController', () => { describe('stopIncomingTransactionPolling', () => { it('should stop the incoming transaction helper for the specific networkClientIds provided', () => { - const controller = newController(); + const controller = newController({ + options: { + enableMultichain: true, + }, + }); const trackingMap = controller.startTrackingByNetworkClientId('mainnet'); controller.startTrackingByNetworkClientId('sepolia'); @@ -5037,7 +5068,11 @@ describe('TransactionController', () => { describe('stopAllIncomingTransactionPolling', () => { it('should stop the global incoming transaction helper and each transaction helper in the tracking map', () => { - const controller = newController(); + const controller = newController({ + options: { + enableMultichain: true, + }, + }); const trackingMap = controller.startTrackingByNetworkClientId('mainnet'); controller.stopAllIncomingTransactionPolling(); @@ -5054,7 +5089,11 @@ describe('TransactionController', () => { describe('updateIncomingTransactions', () => { it('should update the incoming transactions for the specific networkClientIds provided', async () => { - const controller = newController(); + const controller = newController({ + options: { + enableMultichain: true, + }, + }); const trackingMap = controller.startTrackingByNetworkClientId('mainnet'); await controller.updateIncomingTransactions(['mainnet', 'sepolia']); diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index e09c99b511..7a45f6442a 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -300,6 +300,8 @@ export class TransactionController extends BaseControllerV1< chainId?: string, ) => NonceTrackerTransaction[]; + private enableMultichain: boolean; + private readonly messagingSystem: TransactionControllerMessenger; private readonly incomingTransactionHelper: IncomingTransactionHelper; @@ -437,6 +439,7 @@ export class TransactionController extends BaseControllerV1< * @param options.hooks.getAdditionalSignArguments - Returns additional arguments required to sign a transaction. * @param options.hub - Use a different event emitter for the hub. * @param options.getNetworkClientRegistry - Gets the network client registry. + * @param options.enableMultichain - Enable multichain support. * @param config - Initial options used to configure this controller. * @param state - Initial state to set on this controller. */ @@ -466,6 +469,7 @@ export class TransactionController extends BaseControllerV1< getNetworkClientById, getNetworkClientRegistry, hub, + enableMultichain = false, hooks = {}, }: { blockTracker: BlockTracker; @@ -495,6 +499,7 @@ export class TransactionController extends BaseControllerV1< getNetworkClientById: NetworkController['getNetworkClientById']; getNetworkClientRegistry: NetworkController['getNetworkClientRegistry']; hub: TransactionControllerEventEmitter; + enableMultichain: boolean; hooks: { afterSign?: ( transactionMeta: TransactionMeta, @@ -527,6 +532,7 @@ export class TransactionController extends BaseControllerV1< this.initialize(); this.hub = hub ?? this.hub; + this.enableMultichain = enableMultichain; this.findNetworkClientIdByChainId = findNetworkClientIdByChainId; this.getNetworkClientById = getNetworkClientById; this.getNetworkClientRegistry = getNetworkClientRegistry; @@ -619,22 +625,6 @@ export class TransactionController extends BaseControllerV1< this.subscribe(this.#onStateChange); - this.messagingSystem.subscribe( - 'NetworkController:stateChange', - (_, patches) => { - const networkClients = this.getNetworkClientRegistry(); - patches.forEach(({ op, path }) => { - if (op === 'remove' && path[0] === 'networkConfigurations') { - const networkClientId = path[1] as NetworkClientId; - delete networkClients[networkClientId]; - } - }); - - this.#refreshTrackingMap(networkClients); - this.#refreshEtherscanRemoteTransactionSources(networkClients); - }, - ); - onNetworkStateChange(() => { log('Detected network change', this.getChainId()); // TODO(JL): Network state changes also trigger PendingTransactionTracker's onStateChange. @@ -644,7 +634,33 @@ export class TransactionController extends BaseControllerV1< }); this.onBootCleanup(); - this.#initTrackingMap(); + this.messagingSystem.subscribe( + 'NetworkController:stateChange', + (_, patches) => { + if (this.enableMultichain) { + const networkClients = this.getNetworkClientRegistry(); + patches.forEach(({ op, path }) => { + if (op === 'remove' && path[0] === 'networkConfigurations') { + const networkClientId = path[1] as NetworkClientId; + delete networkClients[networkClientId]; + } + }); + + this.#refreshTrackingMap(networkClients); + this.#refreshEtherscanRemoteTransactionSources(networkClients); + } + }, + ); + if (this.enableMultichain) { + this.#initTrackingMap(); + } + } + + setEnableMultichain(enableMultichain: boolean) { + this.enableMultichain = enableMultichain; + if (enableMultichain) { + this.#initTrackingMap(); + } } /** @@ -717,8 +733,10 @@ export class TransactionController extends BaseControllerV1< #onStateChange = () => { // PendingTransactionTracker reads state through its getTransactions hook this.pendingTransactionTracker.onStateChange(); - for (const [, trackingMap] of this.trackingMap) { - trackingMap.pendingTransactionTracker.onStateChange(); + if (this.enableMultichain) { + for (const [, trackingMap] of this.trackingMap) { + trackingMap.pendingTransactionTracker.onStateChange(); + } } }; @@ -755,6 +773,10 @@ export class TransactionController extends BaseControllerV1< networkClientId?: NetworkClientId; chainId?: Hex; }): EthQuery { + // if multichain is disabled, use the global ethQuery + if (!this.enableMultichain) { + return this.ethQuery; + } let networkClient: | AutoManagedNetworkClient | undefined; @@ -931,7 +953,7 @@ export class TransactionController extends BaseControllerV1< } startIncomingTransactionPolling(networkClientIds: NetworkClientId[] = []) { - if (networkClientIds.length === 0) { + if (networkClientIds.length === 0 || !this.enableMultichain) { this.incomingTransactionHelper.start(); return; } @@ -941,7 +963,7 @@ export class TransactionController extends BaseControllerV1< } stopIncomingTransactionPolling(networkClientIds: NetworkClientId[] = []) { - if (networkClientIds.length === 0) { + if (networkClientIds.length === 0 || !this.enableMultichain) { this.incomingTransactionHelper.stop(); return; } @@ -952,13 +974,15 @@ export class TransactionController extends BaseControllerV1< stopAllIncomingTransactionPolling() { this.incomingTransactionHelper.stop(); - for (const [, trackingMap] of this.trackingMap) { - trackingMap.incomingTransactionHelper.stop(); + if (this.enableMultichain) { + for (const [, trackingMap] of this.trackingMap) { + trackingMap.incomingTransactionHelper.stop(); + } } } async updateIncomingTransactions(networkClientIds: NetworkClientId[] = []) { - if (networkClientIds.length === 0) { + if (networkClientIds.length === 0 || !this.enableMultichain) { await this.incomingTransactionHelper.update(); return; } @@ -1334,8 +1358,10 @@ export class TransactionController extends BaseControllerV1< this.incomingTransactionHelper.stop(); this.removeIncomingTransactionHelperListeners(); - for (const [networkClientId] of this.trackingMap) { - this.stopTrackingByNetworkClientId(networkClientId); + if (this.enableMultichain) { + for (const [networkClientId] of this.trackingMap) { + this.stopTrackingByNetworkClientId(networkClientId); + } } } @@ -1814,7 +1840,7 @@ export class TransactionController extends BaseControllerV1< ): Promise { let nonceMutexForChainId: Mutex | undefined; let { nonceTracker } = this; - if (networkClientId) { + if (networkClientId && this.enableMultichain) { const networkClient = this.getNetworkClientById(networkClientId); nonceMutexForChainId = this.nonceMutexByChainId.get( networkClient.configuration.chainId, @@ -2431,7 +2457,7 @@ export class TransactionController extends BaseControllerV1< } let { nonceTracker } = this; - if (networkClientId) { + if (networkClientId && this.enableMultichain) { const trackers = this.trackingMap.get(networkClientId); if (!trackers) { throw new Error('missing nonceTracker for networkClientId'); diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index 3acfdcbb4c..ab3cb547d0 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -99,6 +99,7 @@ const newController = async (options: any = {}) => { getNetworkState: () => networkController.state, getSelectedAddress: () => '0xdeadbeef', getPermittedAccounts: () => [ACCOUNT_MOCK], + enableMultichain: true, ...opts, }, { @@ -2359,6 +2360,69 @@ describe('TransactionController Integration', () => { }); }); + describe('feature flag', () => { + it('should not track multichain transactions on network stateChange when feature flag is disabled', async () => { + mockNetwork({ + networkClientConfiguration: mainnetNetworkClientConfiguration, + mocks: [ + // NetworkController + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x2', + }, + }, + ], + }); + const { networkController, transactionController } = await newController({ + enableMultichain: false, + }); + const startTrackinSpy = jest.spyOn( + transactionController, + 'startTrackingByNetworkClientId', + ); + const stopTrackinSpy = jest.spyOn( + transactionController, + 'stopTrackingByNetworkClientId', + ); + + const configurationId = + await networkController.upsertNetworkConfiguration( + { + ...networkClientConfiguration, + rpcUrl: 'https://mock.rpc.url', + }, + { + setActive: false, + referrer: 'https://mock.referrer', + source: 'dapp', + }, + ); + + networkController.removeNetworkConfiguration(configurationId); + + // advance time to trigger events + await advanceTime({ clock, duration: 1000 }); + + expect(startTrackinSpy).toHaveBeenCalledTimes(0); + expect(stopTrackinSpy).toHaveBeenCalledTimes(0); + }); + }); + describe('startIncomingTransactionPolling', () => { // TODO(JL): IncomingTransactionHelper doesn't populate networkClientId on the generated tx object. Should it?.. it('should add incoming transactions to state with the correct chainId for the given networkClientId on the next block', async () => { From 5c60bd352887833ae95bd0ba217dbafeb92ba808 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 26 Jan 2024 13:54:13 -0800 Subject: [PATCH 053/100] Remove SEPOLIA GOERLIA constants. Add mainnet to mock. Update specs --- .../src/TransactionController.test.ts | 50 ++++++++----------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 658300aec8..e93bcd75a3 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -72,17 +72,6 @@ const mockFlags: { [key: string]: any } = { getBlockByNumberValue: null, }; -const SEPOLIA = { - chainId: toHex(11155111), - type: NetworkType.sepolia, - ticker: NetworksTicker.sepolia, -}; -const GOERLI = { - chainId: toHex(5), - type: NetworkType.goerli, - ticker: NetworksTicker.goerli, -}; - const ethQueryMockResults = { sendRawTransaction: 'mockSendRawTransactionResult', }; @@ -587,7 +576,7 @@ describe('TransactionController', () => { case 'sepolia': return { configuration: { - chainId: SEPOLIA.chainId, + chainId: ChainId.sepolia, }, blockTracker: buildMockBlockTracker('0x1'), provider: MAINNET_PROVIDER, @@ -595,7 +584,7 @@ describe('TransactionController', () => { case 'goerli': return { configuration: { - chainId: GOERLI.chainId, + chainId: ChainId.goerli, }, blockTracker: buildMockBlockTracker('0x1'), provider: MAINNET_PROVIDER, @@ -634,21 +623,25 @@ describe('TransactionController', () => { { blockTracker: finalNetwork.blockTracker, getNetworkState: () => finalNetwork.state, - getCurrentAccountEIP1559Compatibility: () => true, getCurrentNetworkEIP1559Compatibility: () => true, getSavedGasFees: () => undefined, getGasFeeEstimates: () => Promise.resolve({}), getPermittedAccounts: () => [ACCOUNT_MOCK], getSelectedAddress: () => ACCOUNT_MOCK, getNetworkClientRegistry: () => ({ + mainnet: { + configuration: { + chainId: toHex(1), + }, + }, sepolia: { configuration: { - chainId: SEPOLIA.chainId, + chainId: ChainId.sepolia, }, }, goerli: { configuration: { - chainId: GOERLI.chainId, + chainId: ChainId.goerli, }, }, 'customNetworkClientId-1': { @@ -807,8 +800,8 @@ describe('TransactionController', () => { ACCOUNT_MOCK, ); - // gets called in constructor now - expect(nonceTrackerMock).toHaveBeenCalledTimes(4); + // One NonceTracker instance per networkClient in the registry and one more for the global instance + expect(nonceTrackerMock).toHaveBeenCalledTimes(5); expect(pendingTransactions).toStrictEqual([ expect.any(Object), ...externalPendingTransactions, @@ -1216,7 +1209,7 @@ describe('TransactionController', () => { it('adds unapproved transaction to state when using networkClientId', async () => { const controller = newController(); const sepoliaTxParams: TransactionParams = { - chainId: SEPOLIA.chainId, + chainId: ChainId.sepolia, from: ACCOUNT_MOCK, to: ACCOUNT_2_MOCK, }; @@ -1242,7 +1235,7 @@ describe('TransactionController', () => { controller.hub.on('transaction-submitted', submittedEventListener); const sepoliaTxParams: TransactionParams = { - chainId: SEPOLIA.chainId, + chainId: ChainId.sepolia, from: ACCOUNT_MOCK, to: ACCOUNT_MOCK, }; @@ -1260,7 +1253,7 @@ describe('TransactionController', () => { expect(submittedEventListener).toHaveBeenCalledTimes(1); expect(txParams.from).toBe(ACCOUNT_MOCK); expect(networkClientId).toBe('sepolia'); - expect(chainId).toBe(SEPOLIA.chainId); + expect(chainId).toBe(ChainId.sepolia); expect(status).toBe(TransactionStatus.submitted); }); }); @@ -4868,6 +4861,7 @@ describe('TransactionController', () => { const hub = new EventEmitter() as TransactionControllerEventEmitter; hub.on('tracking-map-init', (networkClientIds) => { expect(networkClientIds).toStrictEqual([ + 'mainnet', 'sepolia', 'goerli', 'customNetworkClientId-1', @@ -4888,12 +4882,12 @@ describe('TransactionController', () => { mockGetNetworkClientRegistry.mockImplementation(() => ({ sepolia: { configuration: { - chainId: SEPOLIA.chainId, + chainId: ChainId.sepolia, }, }, goerli: { configuration: { - chainId: GOERLI.chainId, + chainId: ChainId.goerli, }, }, 'customNetworkClientId-1': { @@ -4907,12 +4901,12 @@ describe('TransactionController', () => { mockGetNetworkClientRegistry.mockImplementation(() => ({ sepolia: { configuration: { - chainId: SEPOLIA.chainId, + chainId: ChainId.sepolia, }, }, goerli: { configuration: { - chainId: GOERLI.chainId, + chainId: ChainId.goerli, }, }, })); @@ -4937,7 +4931,7 @@ describe('TransactionController', () => { mockGetNetworkClientRegistry.mockImplementationOnce(() => ({ sepolia: { configuration: { - chainId: SEPOLIA.chainId, + chainId: ChainId.sepolia, }, }, })); @@ -4946,12 +4940,12 @@ describe('TransactionController', () => { mockGetNetworkClientRegistry.mockImplementation(() => ({ sepolia: { configuration: { - chainId: SEPOLIA.chainId, + chainId: ChainId.sepolia, }, }, goerli: { configuration: { - chainId: GOERLI.chainId, + chainId: ChainId.goerli, }, }, })); From 81fcd605873eb74e6f05d47cb9f85a46a4d2d216 Mon Sep 17 00:00:00 2001 From: jiexi Date: Fri, 26 Jan 2024 13:54:23 -0800 Subject: [PATCH 054/100] change getNetworkState to getChainId for IncomingTxHelper (#3859) ## Explanation ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- .../src/TransactionController.ts | 13 ++------- .../helpers/IncomingTransactionHelper.test.ts | 27 ++++++++----------- .../src/helpers/IncomingTransactionHelper.ts | 20 ++++++-------- 3 files changed, 21 insertions(+), 39 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 7a45f6442a..8ba23029b9 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -29,7 +29,6 @@ import type { NetworkState, Provider, NetworkClientConfiguration, - ProviderConfig, } from '@metamask/network-controller'; import { NetworkClientType } from '@metamask/network-controller'; import type { AutoManagedNetworkClient } from '@metamask/network-controller/src/create-auto-managed-network-client'; @@ -595,7 +594,7 @@ export class TransactionController extends BaseControllerV1< blockTracker, getCurrentAccount: getSelectedAddress, getLastFetchedBlockNumbers: () => this.state.lastFetchedBlockNumbers, - getNetworkState, + getChainId: this.getChainId.bind(this), isEnabled: incomingTransactions.isEnabled, queryEntireHistory: incomingTransactions.queryEntireHistory, remoteTransactionSource: etherscanRemoteTransactionSource, @@ -1408,15 +1407,7 @@ export class TransactionController extends BaseControllerV1< blockTracker: networkClient.blockTracker, getCurrentAccount: this.getSelectedAddress, getLastFetchedBlockNumbers: () => this.state.lastFetchedBlockNumbers, - // TODO(JL): Fix this type - // TODO (AD): - // This is a hack until we remove the base IncomingTransactionHelper class - // and should be replaced with a plain chainId parameter - getNetworkState: () => { - return { - providerConfig: { chainId } as ProviderConfig, - } as NetworkState; - }, + getChainId: () => chainId, isEnabled: this.incomingTransactionOptions.isEnabled, queryEntireHistory: this.incomingTransactionOptions.queryEntireHistory, remoteTransactionSource: etherscanRemoteTransactionSource, diff --git a/packages/transaction-controller/src/helpers/IncomingTransactionHelper.test.ts b/packages/transaction-controller/src/helpers/IncomingTransactionHelper.test.ts index 274f1128ee..49b39c4eff 100644 --- a/packages/transaction-controller/src/helpers/IncomingTransactionHelper.test.ts +++ b/packages/transaction-controller/src/helpers/IncomingTransactionHelper.test.ts @@ -1,8 +1,7 @@ /* eslint-disable jest/prefer-spy-on */ /* eslint-disable jsdoc/require-jsdoc */ -import { NetworkType } from '@metamask/controller-utils'; -import type { BlockTracker, NetworkState } from '@metamask/network-controller'; +import type { BlockTracker } from '@metamask/network-controller'; import { TransactionStatus, @@ -19,13 +18,7 @@ jest.mock('@metamask/controller-utils', () => ({ console.error = jest.fn(); -const NETWORK_STATE_MOCK: NetworkState = { - providerConfig: { - chainId: '0x1', - type: NetworkType.mainnet, - }, -} as unknown as NetworkState; - +const CHAIN_ID_MOCK = '0x1' as const; const ADDRESS_MOCK = '0x1'; const FROM_BLOCK_HEX_MOCK = '0x20'; const FROM_BLOCK_DECIMAL_MOCK = 32; @@ -41,7 +34,7 @@ const CONTROLLER_ARGS_MOCK = { blockTracker: BLOCK_TRACKER_MOCK, getCurrentAccount: () => ADDRESS_MOCK, getLastFetchedBlockNumbers: () => ({}), - getNetworkState: () => NETWORK_STATE_MOCK, + getChainId: () => CHAIN_ID_MOCK, remoteTransactionSource: {} as RemoteTransactionSource, transactionLimit: 1, }; @@ -154,7 +147,7 @@ describe('IncomingTransactionHelper', () => { expect(remoteTransactionSource.fetchTransactions).toHaveBeenCalledWith({ address: ADDRESS_MOCK, - currentChainId: NETWORK_STATE_MOCK.providerConfig.chainId, + currentChainId: CHAIN_ID_MOCK, fromBlock: undefined, limit: CONTROLLER_ARGS_MOCK.transactionLimit, }); @@ -210,7 +203,7 @@ describe('IncomingTransactionHelper', () => { ...CONTROLLER_ARGS_MOCK, remoteTransactionSource, getLastFetchedBlockNumbers: () => ({ - [`${NETWORK_STATE_MOCK.providerConfig.chainId}#${ADDRESS_MOCK}#${LAST_BLOCK_VARIATION_MOCK}`]: + [`${CHAIN_ID_MOCK}#${ADDRESS_MOCK}#${LAST_BLOCK_VARIATION_MOCK}`]: FROM_BLOCK_DECIMAL_MOCK, }), }); @@ -477,7 +470,7 @@ describe('IncomingTransactionHelper', () => { ); expect(lastFetchedBlockNumbers).toStrictEqual({ - [`${NETWORK_STATE_MOCK.providerConfig.chainId}#${ADDRESS_MOCK}#${LAST_BLOCK_VARIATION_MOCK}`]: + [`${CHAIN_ID_MOCK}#${ADDRESS_MOCK}#${LAST_BLOCK_VARIATION_MOCK}`]: parseInt(TRANSACTION_MOCK_2.blockNumber as string, 10), }); }); @@ -535,7 +528,7 @@ describe('IncomingTransactionHelper', () => { TRANSACTION_MOCK_2, ]), getLastFetchedBlockNumbers: () => ({ - [`${NETWORK_STATE_MOCK.providerConfig.chainId}#${ADDRESS_MOCK}#${LAST_BLOCK_VARIATION_MOCK}`]: + [`${CHAIN_ID_MOCK}#${ADDRESS_MOCK}#${LAST_BLOCK_VARIATION_MOCK}`]: parseInt(TRANSACTION_MOCK_2.blockNumber as string, 10), }), }); @@ -577,8 +570,10 @@ describe('IncomingTransactionHelper', () => { ); expect(lastFetchedBlockNumbers).toStrictEqual({ - [`${NETWORK_STATE_MOCK.providerConfig.chainId}#${ADDRESS_MOCK}`]: - parseInt(TRANSACTION_MOCK_2.blockNumber as string, 10), + [`${CHAIN_ID_MOCK}#${ADDRESS_MOCK}`]: parseInt( + TRANSACTION_MOCK_2.blockNumber as string, + 10, + ), }); }); }); diff --git a/packages/transaction-controller/src/helpers/IncomingTransactionHelper.ts b/packages/transaction-controller/src/helpers/IncomingTransactionHelper.ts index a30b6ec4ca..dd4c790f29 100644 --- a/packages/transaction-controller/src/helpers/IncomingTransactionHelper.ts +++ b/packages/transaction-controller/src/helpers/IncomingTransactionHelper.ts @@ -1,4 +1,4 @@ -import type { BlockTracker, NetworkState } from '@metamask/network-controller'; +import type { BlockTracker } from '@metamask/network-controller'; import type { Hex } from '@metamask/utils'; import { Mutex } from 'async-mutex'; import EventEmitter from 'events'; @@ -26,7 +26,7 @@ export class IncomingTransactionHelper { #getLocalTransactions: () => TransactionMeta[]; - #getNetworkState: () => NetworkState; + #getChainId: () => Hex; #isEnabled: () => boolean; @@ -49,7 +49,7 @@ export class IncomingTransactionHelper { getCurrentAccount, getLastFetchedBlockNumbers, getLocalTransactions, - getNetworkState, + getChainId, isEnabled, queryEntireHistory, remoteTransactionSource, @@ -60,7 +60,7 @@ export class IncomingTransactionHelper { getCurrentAccount: () => string; getLastFetchedBlockNumbers: () => Record; getLocalTransactions?: () => TransactionMeta[]; - getNetworkState: () => NetworkState; + getChainId: () => Hex; isEnabled?: () => boolean; queryEntireHistory?: boolean; remoteTransactionSource: RemoteTransactionSource; @@ -73,7 +73,7 @@ export class IncomingTransactionHelper { this.#getCurrentAccount = getCurrentAccount; this.#getLastFetchedBlockNumbers = getLastFetchedBlockNumbers; this.#getLocalTransactions = getLocalTransactions || (() => []); - this.#getNetworkState = getNetworkState; + this.#getChainId = getChainId; this.#isEnabled = isEnabled ?? (() => true); this.#isRunning = false; this.#queryEntireHistory = queryEntireHistory ?? true; @@ -130,7 +130,7 @@ export class IncomingTransactionHelper { const fromBlock = this.#getFromBlock(latestBlockNumber); const address = this.#getCurrentAccount(); - const currentChainId = this.#getCurrentChainId(); + const currentChainId = this.#getChainId(); let remoteTransactions = []; @@ -283,7 +283,7 @@ export class IncomingTransactionHelper { } #getBlockNumberKey(additionalKeys: string[]): string { - const currentChainId = this.#getCurrentChainId(); + const currentChainId = this.#getChainId(); const currentAccount = this.#getCurrentAccount()?.toLowerCase(); return [currentChainId, currentAccount, ...additionalKeys].join('#'); @@ -291,15 +291,11 @@ export class IncomingTransactionHelper { #canStart(): boolean { const isEnabled = this.#isEnabled(); - const currentChainId = this.#getCurrentChainId(); + const currentChainId = this.#getChainId(); const isSupportedNetwork = this.#remoteTransactionSource.isSupportedNetwork(currentChainId); return isEnabled && isSupportedNetwork; } - - #getCurrentChainId(): Hex { - return this.#getNetworkState().providerConfig.chainId; - } } From 36bea91b528349477f9b8e3306326a512b50f032 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 26 Jan 2024 14:01:01 -0800 Subject: [PATCH 055/100] remove comment --- packages/transaction-controller/src/TransactionController.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 8ba23029b9..6e6e0b8485 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -2777,8 +2777,6 @@ export class TransactionController extends BaseControllerV1< }; blockNumber: number; }) { - // TODO(JL): the way this object is updated in place from IncomingTransactionHelper - // is incorrect. Additionally there may be a state clobbering issue we need to investigate still. this.update({ lastFetchedBlockNumbers }); this.hub.emit('incomingTransactionBlock', blockNumber); } From 443b2f87e8b2dbb4a60319283860ecd819befd6b Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 26 Jan 2024 14:08:08 -0800 Subject: [PATCH 056/100] Remove global mock networkClient --- .../src/TransactionController.test.ts | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 892ab228ec..e4e6510a19 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -564,7 +564,6 @@ describe('TransactionController', () => { .fn() .mockImplementation((networkClientId) => { switch (networkClientId) { - // TODO(JL): This needs to use different provider from globally selected case 'mainnet': return { configuration: { @@ -597,27 +596,12 @@ describe('TransactionController', () => { blockTracker: buildMockBlockTracker('0x1'), provider: MAINNET_PROVIDER, }; - case 'global': - return { - configuration: { - chainId: finalNetwork.state.providerConfig.chainId, - }, - blockTracker: finalNetwork.blockTracker, - provider: finalNetwork.provider, - }; default: throw new Error(`Invalid network client id ${networkClientId}`); } }); - const mockFindNetworkClientIdByChainId = jest - .fn() - .mockImplementation((chainId) => { - if (chainId !== finalNetwork.state.providerConfig.chainId) { - throw new Error("Couldn't find networkClientId for chainId"); - } - return 'global'; - }); + const mockFindNetworkClientIdByChainId = jest.fn(); return new TransactionController( { From dc148dc0e15004948b45760f355ec7ce6fcb4d68 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 26 Jan 2024 14:15:24 -0800 Subject: [PATCH 057/100] update "only reads the current chain id to filter to initially populate the metadata" comment --- .../transaction-controller/src/TransactionController.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index e4e6510a19..42d40c98f1 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -1288,7 +1288,9 @@ describe('TransactionController', () => { to: ACCOUNT_MOCK, }); - expect(getNetworkStateMock).toHaveBeenCalledTimes(2); // we shouldn't test like this + // First call comes from getting the chainId to populate the initial unapproved transaction + // Second call comes from getting the network type to populate the initial gas estimates + expect(getNetworkStateMock).toHaveBeenCalledTimes(2); }); describe('adds dappSuggestedGasFees to transaction', () => { From eb302780690043b0e7cbe46219d23770db600856 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Fri, 26 Jan 2024 16:22:13 -0600 Subject: [PATCH 058/100] fix comment (#3861) FIx comment --- .../src/TransactionControllerIntegration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index ab3cb547d0..f779893b17 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -3027,7 +3027,7 @@ describe('TransactionController Integration', () => { }), ); - // we have to wait for the mutex is released after the 5 second gap between API calls finishes + // we have to wait for the mutex to be released after the 5 second API rate limit timer await advanceTime({ clock, duration: 5000 }); expect(transactionController.state.transactions).toHaveLength( From 90643291820e8eb499439bed6875efdab5df0e6e Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 26 Jan 2024 14:22:58 -0800 Subject: [PATCH 059/100] exit early markNonceDuplicatesDropped if no txMeta found --- .../src/TransactionController.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 6e6e0b8485..127523872f 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -2867,13 +2867,15 @@ export class TransactionController extends BaseControllerV1< */ private markNonceDuplicatesDropped(transactionId: string) { const transactionMeta = this.getTransaction(transactionId); - // NOTE(JL): Should this method be exiting early if getTransaction returns no transaction object? - const nonce = transactionMeta?.txParams?.nonce; - const from = transactionMeta?.txParams?.from; + if (!transactionMeta) { + return; + } + const nonce = transactionMeta.txParams?.nonce; + const from = transactionMeta.txParams?.from; // NOTE(JL): the line below was removed upstream in favor of this.getChainId() // not sure specifically why that was the case // https://github.com/MetaMask/core/commit/89654542c9c61308cfad6a310f7fe2b4b669117b - const chainId = transactionMeta?.chainId; + const { chainId } = transactionMeta; const sameNonceTxs = this.state.transactions.filter( (transaction) => @@ -2890,8 +2892,8 @@ export class TransactionController extends BaseControllerV1< // Mark all same nonce transactions as dropped and give it a replacedBy hash for (const transaction of sameNonceTxs) { - transaction.replacedBy = transactionMeta?.hash; - transaction.replacedById = transactionMeta?.id; + transaction.replacedBy = transactionMeta.hash; + transaction.replacedById = transactionMeta.id; // Drop any transaction that wasn't previously failed (off chain failure) if (transaction.status !== TransactionStatus.failed) { this.setTransactionStatusDropped(transaction); From 54905db8083fc2ff97b3859352945732991df2ef Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 26 Jan 2024 16:12:27 -0800 Subject: [PATCH 060/100] Add comments to mock requests and reorder --- .../TransactionControllerIntegration.test.ts | 456 ++++++++---------- 1 file changed, 209 insertions(+), 247 deletions(-) diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index f779893b17..618c4a06ca 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -694,35 +694,13 @@ describe('TransactionController Integration', () => { mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ + // NetworkController + // BlockTracker { request: { method: 'eth_blockNumber', params: [], }, - response: { - result: '0x3b3301', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - params: ['0x3b3301'], - }, - response: { - result: { - baseFeePerGas: '0x63c498a46', - number: '0x3b3301', - }, - }, - }, - { - request: { - method: 'eth_getTransactionCount', - params: [ - '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - '0x3b3301', - ], - }, response: { result: '0x1', }, @@ -732,6 +710,8 @@ describe('TransactionController Integration', () => { mockNetwork({ networkClientConfiguration, mocks: [ + // NetworkController + // BlockTracker { request: { method: 'eth_blockNumber', @@ -741,35 +721,7 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3', - }, - }, - { - request: { - method: 'eth_getBlockByHash', - params: ['0x1', false], - }, - response: { - result: { - transactions: [], - }, - }, - }, - { - request: { - method: 'eth_getCode', - params: [ACCOUNT_2_MOCK, '0x1'], - }, - response: { - result: '0x', // non contract - }, - }, + // NetworkController { request: { method: 'eth_getBlockByNumber', @@ -782,15 +734,18 @@ describe('TransactionController Integration', () => { }, }, }, + // readAddressAsContract + // requiresFixedGas (cached) { request: { - method: 'eth_gasPrice', - params: [], + method: 'eth_getCode', + params: [ACCOUNT_2_MOCK, '0x1'], }, response: { - result: '0x1', + result: '0x', // non contract }, }, + // estimateGas { request: { method: 'eth_estimateGas', @@ -807,62 +762,63 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + // getSuggestedGasFees { request: { - method: 'eth_getTransactionCount', - params: ['0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', '0x1'], + method: 'eth_gasPrice', + params: [], }, response: { result: '0x1', }, }, + // NonceTracker { request: { - method: 'eth_sendRawTransaction', - params: [ - '0x02e2050101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', - ], + method: 'eth_getTransactionCount', + params: ['0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', '0x1'], }, response: { result: '0x1', }, }, + // publishTransaction { request: { - method: 'eth_getTransactionReceipt', - params: ['0x1'], + method: 'eth_sendRawTransaction', + params: [ + '0x02e2050101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + ], }, response: { - result: { - blockHash: '0x1', - blockNumber: '0x3', // we need at least 2 blocks mocked since the first one is used for when the blockTracker is instantied before we have listeners - status: '0x1', // 0x1 = success - }, + result: '0x1', }, }, - ], - }); - mockNetwork({ - networkClientConfiguration: sepoliaNetworkClientConfiguration, - mocks: [ + // BlockTracker { request: { method: 'eth_blockNumber', params: [], }, response: { - result: '0x1', + result: '0x3', }, }, + // PendingTransactionTracker.#checkTransaction { request: { - method: 'eth_blockNumber', - params: [], + method: 'eth_getTransactionReceipt', + params: ['0x1'], }, response: { - result: '0x2', + result: { + blockHash: '0x1', + blockNumber: '0x3', // we need at least 2 blocks mocked since the first one is used for when the blockTracker is instantied before we have listeners + status: '0x1', // 0x1 = success + }, }, }, + // PendingTransactionTracker.#onTransactionConfirmed { request: { method: 'eth_getBlockByHash', @@ -874,15 +830,23 @@ describe('TransactionController Integration', () => { }, }, }, + ], + }); + mockNetwork({ + networkClientConfiguration: sepoliaNetworkClientConfiguration, + mocks: [ + // NetworkController + // BlockTracker { request: { - method: 'eth_getCode', - params: [ACCOUNT_2_MOCK, '0x1'], + method: 'eth_blockNumber', + params: [], }, response: { - result: '0x', // non contract + result: '0x1', }, }, + // NetworkController { request: { method: 'eth_getBlockByNumber', @@ -895,15 +859,18 @@ describe('TransactionController Integration', () => { }, }, }, + // readAddressAsContract + // requiresFixedGas (cached) { request: { - method: 'eth_gasPrice', - params: [], + method: 'eth_getCode', + params: [ACCOUNT_2_MOCK, '0x1'], }, response: { - result: '0x1', + result: '0x', // non contract }, }, + // estimateGas { request: { method: 'eth_estimateGas', @@ -920,6 +887,17 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + // getSuggestedGasFees + { + request: { + method: 'eth_gasPrice', + params: [], + }, + response: { + result: '0x1', + }, + }, + // NonceTracker { request: { method: 'eth_getTransactionCount', @@ -929,6 +907,7 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + // publishTransaction { request: { method: 'eth_sendRawTransaction', @@ -940,6 +919,7 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + // BlockTracker { request: { method: 'eth_blockNumber', @@ -949,25 +929,29 @@ describe('TransactionController Integration', () => { result: '0x3', }, }, + // PendingTransactionTracker.#checkTransaction { request: { - method: 'eth_blockNumber', - params: [], + method: 'eth_getTransactionReceipt', + params: ['0x1'], }, response: { - result: '0x3', + result: { + blockHash: '0x1', + blockNumber: '0x3', // we need at least 2 blocks mocked since the first one is used for when the blockTracker is instantied before we have listeners + status: '0x1', // 0x1 = success + }, }, }, + // PendingTransactionTracker.#onTransactionConfirmed { request: { - method: 'eth_getTransactionReceipt', - params: ['0x1'], + method: 'eth_getBlockByHash', + params: ['0x1', false], }, response: { result: { - blockHash: '0x1', - blockNumber: '0x3', // we need at least 2 blocks mocked since the first one is used for when the blockTracker is instantied before we have listeners - status: '0x1', // 0x1 = success + transactions: [], }, }, }, @@ -1688,37 +1672,15 @@ describe('TransactionController Integration', () => { mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ + // NetworkController + // BlockTracker { request: { method: 'eth_blockNumber', params: [], }, response: { - result: '0x3b3301', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - params: ['0x3b3301'], - }, - response: { - result: { - baseFeePerGas: '0x63c498a46', - number: '0x3b3301', - }, - }, - }, - { - request: { - method: 'eth_getTransactionCount', - params: [ - '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - '0x3b3301', - ], - }, - response: { - result: '0x1', + result: '1', }, }, ], @@ -1726,6 +1688,8 @@ describe('TransactionController Integration', () => { mockNetwork({ networkClientConfiguration, mocks: [ + // NetworkController + // BlockTracker { request: { method: 'eth_blockNumber', @@ -1735,35 +1699,7 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3', - }, - }, - { - request: { - method: 'eth_getBlockByHash', - params: ['0x1', false], - }, - response: { - result: { - transactions: [], - }, - }, - }, - { - request: { - method: 'eth_getCode', - params: [ACCOUNT_2_MOCK, '0x1'], - }, - response: { - result: '0x', // non contract - }, - }, + // NetworkController { request: { method: 'eth_getBlockByNumber', @@ -1776,15 +1712,18 @@ describe('TransactionController Integration', () => { }, }, }, + // readAddressAsContract + // requiresFixedGas (cached) { request: { - method: 'eth_gasPrice', - params: [], + method: 'eth_getCode', + params: [ACCOUNT_2_MOCK, '0x1'], }, response: { - result: '0x1', + result: '0x', // non contract }, }, + // estimateGas { request: { method: 'eth_estimateGas', @@ -1801,24 +1740,27 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + // getSuggestedGasFees { request: { - method: 'eth_getTransactionCount', - params: ['0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', '0x1'], + method: 'eth_gasPrice', + params: [], }, response: { result: '0x1', }, }, + // NonceTracker { request: { - method: 'eth_blockNumber', - params: [], + method: 'eth_getTransactionCount', + params: ['0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', '0x1'], }, response: { - result: '0x3', + result: '0x1', }, }, + // publishTransaction { request: { method: 'eth_sendRawTransaction', @@ -1830,6 +1772,17 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3', + }, + }, + // PendingTransactionTracker.#checkTransaction { request: { method: 'eth_getTransactionReceipt', @@ -1843,6 +1796,18 @@ describe('TransactionController Integration', () => { }, }, }, + // PendingTransactionTracker.#onTransactionConfirmed + { + request: { + method: 'eth_getBlockByHash', + params: ['0x1', false], + }, + response: { + result: { + transactions: [], + }, + }, + }, ], }); @@ -1853,6 +1818,8 @@ describe('TransactionController Integration', () => { rpcUrl: 'https://mock.rpc.url', }, mocks: [ + // NetworkController + // BlockTracker { request: { method: 'eth_blockNumber', @@ -1862,35 +1829,7 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3', - }, - }, - { - request: { - method: 'eth_getBlockByHash', - params: ['0x1', false], - }, - response: { - result: { - transactions: [], - }, - }, - }, - { - request: { - method: 'eth_getCode', - params: [ACCOUNT_2_MOCK, '0x1'], - }, - response: { - result: '0x', // non contract - }, - }, + // NetworkController { request: { method: 'eth_getBlockByNumber', @@ -1903,15 +1842,18 @@ describe('TransactionController Integration', () => { }, }, }, + // readAddressAsContract + // requiresFixedGas (cached) { request: { - method: 'eth_gasPrice', - params: [], + method: 'eth_getCode', + params: [ACCOUNT_2_MOCK, '0x1'], }, response: { - result: '0x1', + result: '0x', // non contract }, }, + // estimateGas { request: { method: 'eth_estimateGas', @@ -1928,24 +1870,27 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + // getSuggestedGasFees { request: { - method: 'eth_getTransactionCount', - params: ['0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', '0x1'], + method: 'eth_gasPrice', + params: [], }, response: { result: '0x1', }, }, + // NonceTracker { request: { - method: 'eth_blockNumber', - params: [], + method: 'eth_getTransactionCount', + params: ['0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', '0x1'], }, response: { - result: '0x3', + result: '0x1', }, }, + // publishTransaction { request: { method: 'eth_sendRawTransaction', @@ -1957,6 +1902,17 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3', + }, + }, + // PendingTransactionTracker.#checkTransaction { request: { method: 'eth_getTransactionReceipt', @@ -1970,6 +1926,18 @@ describe('TransactionController Integration', () => { }, }, }, + // PendingTransactionTracker.#onTransactionConfirmed + { + request: { + method: 'eth_getBlockByHash', + params: ['0x1', false], + }, + response: { + result: { + transactions: [], + }, + }, + }, ], }); @@ -2030,35 +1998,13 @@ describe('TransactionController Integration', () => { mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ + // NetworkController + // BlockTracker { request: { method: 'eth_blockNumber', params: [], }, - response: { - result: '0x3b3301', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - params: ['0x3b3301'], - }, - response: { - result: { - baseFeePerGas: '0x63c498a46', - number: '0x3b3301', - }, - }, - }, - { - request: { - method: 'eth_getTransactionCount', - params: [ - '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - '0x3b3301', - ], - }, response: { result: '0x1', }, @@ -2068,6 +2014,8 @@ describe('TransactionController Integration', () => { mockNetwork({ networkClientConfiguration, mocks: [ + // NetworkController + // BlockTracker { request: { method: 'eth_blockNumber', @@ -2077,26 +2025,21 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + // NetworkController { request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3', - }, - }, - { - request: { - method: 'eth_getBlockByHash', + method: 'eth_getBlockByNumber', params: ['0x1', false], }, response: { result: { - transactions: [], + baseFeePerGas: '0x63c498a46', + number: '0x1', }, }, }, + // readAddressAsContract + // requiresFixedGas (cached) { request: { method: 'eth_getCode', @@ -2106,6 +2049,8 @@ describe('TransactionController Integration', () => { result: '0x', // non contract }, }, + // readAddressAsContract + // requiresFixedGas (cached) { request: { method: 'eth_getCode', @@ -2115,27 +2060,7 @@ describe('TransactionController Integration', () => { result: '0x', // non contract }, }, - { - request: { - method: 'eth_getBlockByNumber', - params: ['0x1', false], - }, - response: { - result: { - baseFeePerGas: '0x63c498a46', - number: '0x1', - }, - }, - }, - { - request: { - method: 'eth_gasPrice', - params: [], - }, - response: { - result: '0x1', - }, - }, + // estimateGas { request: { method: 'eth_estimateGas', @@ -2152,6 +2077,17 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + // getSuggestedGasFees + { + request: { + method: 'eth_gasPrice', + params: [], + }, + response: { + result: '0x1', + }, + }, + // NonceTracker { request: { method: 'eth_getTransactionCount', @@ -2161,6 +2097,7 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + // publishTransaction { request: { method: 'eth_sendRawTransaction', @@ -2172,6 +2109,17 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x3', + }, + }, + // PendingTransactionTracker.#checkTransaction { request: { method: 'eth_getTransactionReceipt', @@ -2185,6 +2133,19 @@ describe('TransactionController Integration', () => { }, }, }, + // PendingTransactionTracker.#onTransactionConfirmed + { + request: { + method: 'eth_getBlockByHash', + params: ['0x1', false], + }, + response: { + result: { + transactions: [], + }, + }, + }, + // publishTransaction { request: { method: 'eth_sendRawTransaction', @@ -2196,6 +2157,7 @@ describe('TransactionController Integration', () => { result: '0x2', }, }, + // PendingTransactionTracker.#checkTransaction { request: { method: 'eth_getTransactionReceipt', From f3d331194141676bb012d692afe81852aded6abe Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Fri, 26 Jan 2024 16:16:49 -0800 Subject: [PATCH 061/100] Add missing destroy() in spec --- .../src/TransactionControllerIntegration.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index 618c4a06ca..aca50fbc81 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -312,6 +312,7 @@ describe('TransactionController Integration', () => { expect(transactionController.state.transactions[1].status).toBe( 'submitted', ); + transactionController.destroy(); }); }); describe('multichain transaction lifecycle', () => { @@ -2319,6 +2320,7 @@ describe('TransactionController Integration', () => { expect(stopTrackinSpy).toHaveBeenCalledTimes(1); expect(transactionController).toBeDefined(); + transactionController.destroy(); }); }); From c1b6466a5a863fce996bf4195b9fdc77caa40713 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 29 Jan 2024 11:49:38 -0600 Subject: [PATCH 062/100] remove comment --- .../transaction-controller/src/TransactionController.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 42d40c98f1..696daa510d 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -792,7 +792,6 @@ describe('TransactionController', () => { expect(getExternalPendingTransactions).toHaveBeenCalledWith( ACCOUNT_MOCK, // This is undefined for the base nonceTracker - // TODO (AD) add tests for using external pending transactions with a networkClientId once we have trackingMaps instantiated undefined, ); }); From f485576dc843c205081322661dd45ab07d35de47 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 29 Jan 2024 12:00:05 -0600 Subject: [PATCH 063/100] remove another comment --- packages/transaction-controller/src/TransactionController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 127523872f..33ef3639dd 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -2115,7 +2115,7 @@ export class TransactionController extends BaseControllerV1< filterToCurrentNetwork?: boolean; limit?: number; } = {}): TransactionMeta[] { - const chainId = this.getChainId(); // TODO(JL): This should be made into an optional param + const chainId = this.getChainId(); // searchCriteria is an object that might have values that aren't predicate // methods. When providing any other value type (string, number, etc), we // consider this shorthand for "check the value at key for strict equality From 7e26558021e6c992421879c61d03d49067ff8d3b Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Mon, 29 Jan 2024 12:35:42 -0600 Subject: [PATCH 064/100] Fix pendingTransaction polling listener (#3865) only poll for pending transactions on base pendingTransactionTracker when networkController state changes occur --- .../src/TransactionController.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 33ef3639dd..8b5b583704 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -622,13 +622,15 @@ export class TransactionController extends BaseControllerV1< this.addPendingTransactionTrackerListeners(); - this.subscribe(this.#onStateChange); + // when transactionsController state changes + // check for pending transactions and start polling if there are any + this.subscribe(this.#checkForPendingTransactionAndStartPolling); + // TODO once v2 is merged make sure this only runs when + // selectedNetworkClientId changes onNetworkStateChange(() => { log('Detected network change', this.getChainId()); - // TODO(JL): Network state changes also trigger PendingTransactionTracker's onStateChange. - // Verify if this is still necessary when the feature branch is being reviewed - this.#onStateChange(); + this.pendingTransactionTracker.onStateChange(); this.onBootCleanup(); }); @@ -729,7 +731,7 @@ export class TransactionController extends BaseControllerV1< this.hub.emit('tracking-map-init', networkClientIds); }; - #onStateChange = () => { + #checkForPendingTransactionAndStartPolling = () => { // PendingTransactionTracker reads state through its getTransactions hook this.pendingTransactionTracker.onStateChange(); if (this.enableMultichain) { From 259e3e46d5b7e775754e9f78a527c10c685266bf Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Mon, 29 Jan 2024 12:37:19 -0600 Subject: [PATCH 065/100] Ad/make tracking methods private (#3860) Makes new `startTrackingByNetworkClientId` and `stopTrackingByNetworkClientId` methods private and adjusts tests accordingly --------- Co-authored-by: Jiexi Luan --- .../src/TransactionController.test.ts | 256 +++++------------- .../src/TransactionController.ts | 42 ++- .../TransactionControllerIntegration.test.ts | 164 ++++++++--- packages/transaction-controller/src/types.ts | 4 +- 4 files changed, 222 insertions(+), 244 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 696daa510d..7ea7377567 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -602,7 +602,6 @@ describe('TransactionController', () => { }); const mockFindNetworkClientIdByChainId = jest.fn(); - return new TransactionController( { blockTracker: finalNetwork.blockTracker, @@ -1189,7 +1188,7 @@ describe('TransactionController', () => { describe('multichain', () => { it('adds unapproved transaction to state when using networkClientId', async () => { const controller = newController({ - options: { enableMultiChain: true }, + options: { enableMultichain: true }, }); const sepoliaTxParams: TransactionParams = { chainId: ChainId.sepolia, @@ -2512,31 +2511,6 @@ describe('TransactionController', () => { expect(getNonceLockSpy).toHaveBeenCalledWith(ACCOUNT_MOCK); expect(nextNonce).toBe(NONCE_MOCK); }); - - it('gets the next nonce from the nonceTracker for the provided networkClientId', async () => { - const controller = newController({ - network: MOCK_LINEA_MAINNET_NETWORK, - }); - - const trackingMap = controller.startTrackingByNetworkClientId( - 'customNetworkClientId-1', - ); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const { nonceTracker } = trackingMap.get('customNetworkClientId-1')!; - const nonceTrackerGetNonceLockSpy = jest.spyOn( - nonceTracker, - 'getNonceLock', - ); - - const { nextNonce } = await controller.getNonceLock( - ACCOUNT_MOCK, - 'customNetworkClientId-1', - ); - - expect(nonceTrackerGetNonceLockSpy).toHaveBeenCalledTimes(1); - expect(nonceTrackerGetNonceLockSpy).toHaveBeenCalledWith(ACCOUNT_MOCK); - expect(nextNonce).toBe(NONCE_MOCK); - }); }); describe('confirmExternalTransaction', () => { @@ -4803,45 +4777,6 @@ describe('TransactionController', () => { Current tx status: ${TransactionStatus.submitted}`); }); }); - describe('startTrackingByNetworkClientId', () => { - it('should start tracking in a tracking map', () => { - const controller = newController(); - const trackingMap = controller.startTrackingByNetworkClientId('mainnet'); - expect(trackingMap.get('mainnet')?.nonceTracker).toBeDefined(); - expect( - trackingMap.get('mainnet')?.incomingTransactionHelper, - ).toBeDefined(); - expect( - trackingMap.get('mainnet')?.pendingTransactionTracker, - ).toBeDefined(); - }); - }); - describe('stopTrackingByNetworkClientId', () => { - it('should stop tracking in a tracking map', () => { - const mockGetNetworkClientRegistry = jest.fn(); - mockGetNetworkClientRegistry.mockImplementation(() => ({ - mainnet: { - configuration: { - chainId: '0x1', - }, - }, - })); - const controller = newController({ - options: { - getNetworkClientRegistry: mockGetNetworkClientRegistry, - }, - }); - const trackingMap = controller.startTrackingByNetworkClientId('mainnet'); - const incomingTransactionHelper = - trackingMap.get('mainnet')?.incomingTransactionHelper; - if (!incomingTransactionHelper) { - throw new Error('incomingTransactionHelper is undefined'); - } - const stopSpy = jest.spyOn(incomingTransactionHelper, 'stop'); - controller.stopTrackingByNetworkClientId('mainnet'); - expect(stopSpy).toHaveBeenCalledTimes(1); - }); - }); describe('initTrackingMap', () => { it('doesnt get called if the feature flag is disabled', () => { @@ -4856,28 +4791,29 @@ describe('TransactionController', () => { }); expect(spy).not.toHaveBeenCalled(); }); - // eslint-disable-next-line jest/no-done-callback - it('should initialize the tracking map on construction', (done) => { + it('should initialize the tracking map on construction', async () => { const hub = new EventEmitter() as TransactionControllerEventEmitter; - hub.on('tracking-map-init', (networkClientIds) => { - expect(networkClientIds).toStrictEqual([ - 'mainnet', - 'sepolia', - 'goerli', - 'customNetworkClientId-1', - ]); - done(); + const receivedEvents = new Promise((resolve) => { + hub.on('tracking-map-init', (networkClientIds) => { + expect(networkClientIds).toStrictEqual([ + 'mainnet', + 'sepolia', + 'goerli', + 'customNetworkClientId-1', + ]); + resolve(undefined); + }); }); - const controller = newController({ + + newController({ options: { enableMultichain: true, hub, }, }); - expect(controller).toBeDefined(); + await receivedEvents; }); - // eslint-disable-next-line jest/no-done-callback - it('should handle removals from the networkController registry', (done) => { + it('should handle removals from the networkController registry', async () => { const hub = new EventEmitter() as TransactionControllerEventEmitter; const mockGetNetworkClientRegistry = jest.fn(); mockGetNetworkClientRegistry.mockImplementation(() => ({ @@ -4897,37 +4833,51 @@ describe('TransactionController', () => { }, }, })); - hub.on('tracking-map-init', () => { - mockGetNetworkClientRegistry.mockClear(); - mockGetNetworkClientRegistry.mockImplementation(() => ({ - sepolia: { - configuration: { - chainId: ChainId.sepolia, - }, - }, - goerli: { - configuration: { - chainId: ChainId.goerli, - }, - }, - })); - }); - hub.on('tracking-map-remove', (networkClientIds) => { - expect(networkClientIds).toStrictEqual(['customNetworkClientId-1']); - done(); + const receivedEvents = new Promise((resolve) => { + let expectedNetworkClientIds = ['customNetworkClientId-1', 'goerli']; + hub.on('tracking-map-remove', (networkClientId) => { + expect(expectedNetworkClientIds).toContain(networkClientId); + expectedNetworkClientIds = expectedNetworkClientIds.filter( + (v) => v !== networkClientId, + ); + if (expectedNetworkClientIds.length === 0) { + resolve(undefined); + } + }); }); - const controller = newController({ + + const mockMessenger = buildMockMessenger({}); + (mockMessenger.messenger.subscribe as jest.Mock).mockImplementation( + (_type, handler) => { + setTimeout(() => { + handler({}, [ + { + op: 'remove', + path: ['networkConfigurations', 'goerli'], + value: 'foo', + }, + { + op: 'remove', + path: ['networkConfigurations', 'customNetworkClientId-1'], + value: 'foo', + }, + ]); + }, 0); + }, + ); + + newController({ options: { + messenger: mockMessenger.messenger, getNetworkClientRegistry: mockGetNetworkClientRegistry, enableMultichain: true, hub, }, }); - expect(controller).toBeDefined(); + await receivedEvents; }); }); - // eslint-disable-next-line jest/no-done-callback - it('should handle additions to the networkController registry', (done) => { + it('should handle additions to the networkController registry', async () => { const hub = new EventEmitter() as TransactionControllerEventEmitter; const mockGetNetworkClientRegistry = jest.fn(); mockGetNetworkClientRegistry.mockImplementationOnce(() => ({ @@ -4937,6 +4887,7 @@ describe('TransactionController', () => { }, }, })); + hub.on('tracking-map-init', () => { mockGetNetworkClientRegistry.mockClear(); mockGetNetworkClientRegistry.mockImplementation(() => ({ @@ -4952,9 +4903,18 @@ describe('TransactionController', () => { }, })); }); - hub.on('tracking-map-add', (networkClientIds) => { - expect(networkClientIds).toStrictEqual(['goerli']); - done(); + + const receivedEvents = new Promise((resolve) => { + let expectedNetworkClientIds = ['goerli', 'sepolia']; + hub.on('tracking-map-add', (networkClientId) => { + expect(expectedNetworkClientIds).toContain(networkClientId); + expectedNetworkClientIds = expectedNetworkClientIds.filter( + (v) => v !== networkClientId, + ); + if (expectedNetworkClientIds.length === 0) { + resolve(undefined); + } + }); }); const mockMessenger = buildMockMessenger({}); (mockMessenger.messenger.subscribe as jest.Mock).mockImplementation( @@ -4962,8 +4922,8 @@ describe('TransactionController', () => { setTimeout(() => { handler({}, [ { - op: 'remove', - path: ['networkConfigurations', 'foo', 'bar'], + op: 'add', + path: ['networkConfigurations', 'goerli'], value: 'foo', }, ]); @@ -4971,7 +4931,7 @@ describe('TransactionController', () => { }, ); - const controller = newController({ + newController({ options: { messenger: mockMessenger.messenger, getNetworkClientRegistry: mockGetNetworkClientRegistry, @@ -4979,29 +4939,10 @@ describe('TransactionController', () => { hub, }, }); - expect(controller).toBeDefined(); + await receivedEvents; }); describe('startIncomingTransactionPolling', () => { - it('should start the incoming transaction helper for the specific networkClientIds provided', () => { - const controller = newController({ - options: { - enableMultichain: true, - }, - }); - const trackingMap = controller.startTrackingByNetworkClientId('mainnet'); - controller.startTrackingByNetworkClientId('sepolia'); - - controller.startIncomingTransactionPolling(['mainnet', 'sepolia']); - - expect( - trackingMap.get('mainnet')?.incomingTransactionHelper.start, - ).toHaveBeenCalledTimes(1); - expect( - trackingMap.get('sepolia')?.incomingTransactionHelper.start, - ).toHaveBeenCalledTimes(1); - }); - it('should start the global incoming transaction helper when no networkClientIds provided', () => { const controller = newController({ options: { @@ -5016,25 +4957,6 @@ describe('TransactionController', () => { }); describe('stopIncomingTransactionPolling', () => { - it('should stop the incoming transaction helper for the specific networkClientIds provided', () => { - const controller = newController({ - options: { - enableMultichain: true, - }, - }); - const trackingMap = controller.startTrackingByNetworkClientId('mainnet'); - controller.startTrackingByNetworkClientId('sepolia'); - - controller.stopIncomingTransactionPolling(['mainnet', 'sepolia']); - - expect( - trackingMap.get('mainnet')?.incomingTransactionHelper.stop, - ).toHaveBeenCalledTimes(1); - expect( - trackingMap.get('sepolia')?.incomingTransactionHelper.stop, - ).toHaveBeenCalledTimes(1); - }); - it('should stop the global incoming transaction helper when no networkClientIds provided', () => { const controller = newController(); @@ -5044,49 +4966,9 @@ describe('TransactionController', () => { }); }); - describe('stopAllIncomingTransactionPolling', () => { - it('should stop the global incoming transaction helper and each transaction helper in the tracking map', () => { - const controller = newController({ - options: { - enableMultichain: true, - }, - }); - const trackingMap = controller.startTrackingByNetworkClientId('mainnet'); - - controller.stopAllIncomingTransactionPolling(); - - expect(incomingTransactionHelperMocks[0].stop).toHaveBeenCalledTimes(1); - expect( - trackingMap.get('mainnet')?.incomingTransactionHelper.stop, - ).toHaveBeenCalledTimes(1); - expect( - trackingMap.get('sepolia')?.incomingTransactionHelper.stop, - ).toHaveBeenCalledTimes(1); - }); - }); - describe('updateIncomingTransactions', () => { - it('should update the incoming transactions for the specific networkClientIds provided', async () => { - const controller = newController({ - options: { - enableMultichain: true, - }, - }); - const trackingMap = controller.startTrackingByNetworkClientId('mainnet'); - - await controller.updateIncomingTransactions(['mainnet', 'sepolia']); - - expect( - trackingMap.get('mainnet')?.incomingTransactionHelper.update, - ).toHaveBeenCalledTimes(1); - expect( - trackingMap.get('sepolia')?.incomingTransactionHelper.update, - ).toHaveBeenCalledTimes(1); - }); - it('should update the global incoming transactions when no networkClientIds provided', async () => { const controller = newController(); - await controller.updateIncomingTransactions([]); expect(incomingTransactionHelperMocks[0].update).toHaveBeenCalledTimes(1); diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 8b5b583704..c4298ef2f9 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -528,7 +528,6 @@ export class TransactionController extends BaseControllerV1< transactions: [], lastFetchedBlockNumbers: {}, }; - this.initialize(); this.hub = hub ?? this.hub; this.enableMultichain = enableMultichain; @@ -704,30 +703,22 @@ export class TransactionController extends BaseControllerV1< (id) => !networkClientIds.includes(id), ); networkClientIdsToRemove.forEach((id) => { - this.stopTrackingByNetworkClientId(id); + this.#stopTrackingByNetworkClientId(id); }); - if (networkClientIdsToRemove.length > 0) { - this.hub.emit('tracking-map-remove', networkClientIdsToRemove); - } - // Start tracking new NetworkClientIds from the registry const networkClientIdsToAdd = networkClientIds.filter( (id) => !existingNetworkClientIds.includes(id), ); networkClientIdsToAdd.forEach((id) => { - this.startTrackingByNetworkClientId(id); + this.#startTrackingByNetworkClientId(id); }); - - if (networkClientIdsToAdd.length > 0) { - this.hub.emit('tracking-map-add', networkClientIdsToAdd); - } }; #initTrackingMap = () => { const networkClients = this.getNetworkClientRegistry(); const networkClientIds = Object.keys(networkClients); - networkClientIds.map((id) => this.startTrackingByNetworkClientId(id)); + networkClientIds.map((id) => this.#startTrackingByNetworkClientId(id)); this.hub.emit('tracking-map-init', networkClientIds); }; @@ -862,6 +853,11 @@ export class TransactionController extends BaseControllerV1< log('Adding transaction', txParams); txParams = normalizeTxParams(txParams); + if (networkClientId && !this.trackingMap.has(networkClientId)) { + throw new Error( + 'The networkClientId for this transaction could not be found', + ); + } const isEIP1559Compatible = await this.getEIP1559Compatibility( networkClientId, @@ -1337,8 +1333,7 @@ export class TransactionController extends BaseControllerV1< this.hub.emit(`${transactionMeta.id}:speedup`, newTransactionMeta); } - // NOTE(JL): Should this be private? - stopTrackingByNetworkClientId(networkClientId: NetworkClientId) { + #stopTrackingByNetworkClientId(networkClientId: NetworkClientId) { const trackers = this.trackingMap.get(networkClientId); if (trackers) { trackers.pendingTransactionTracker.stop(); @@ -1350,6 +1345,7 @@ export class TransactionController extends BaseControllerV1< trackers.incomingTransactionHelper, ); this.trackingMap.delete(networkClientId); + this.hub.emit('tracking-map-remove', networkClientId); } } @@ -1361,14 +1357,16 @@ export class TransactionController extends BaseControllerV1< if (this.enableMultichain) { for (const [networkClientId] of this.trackingMap) { - this.stopTrackingByNetworkClientId(networkClientId); + this.#stopTrackingByNetworkClientId(networkClientId); } } } - // NOTE(JL): Should this be private? - // TODO(JL): This should be idempotent - startTrackingByNetworkClientId(networkClientId: NetworkClientId) { + #startTrackingByNetworkClientId(networkClientId: NetworkClientId) { + const trackers = this.trackingMap.get(networkClientId); + if (trackers) { + return; + } const networkClient = this.getNetworkClientById(networkClientId); const { chainId } = networkClient.configuration; @@ -1437,13 +1435,13 @@ export class TransactionController extends BaseControllerV1< this.addPendingTransactionTrackerListeners(pendingTransactionTracker); - // add to tracking map this.trackingMap.set(networkClientId, { nonceTracker, incomingTransactionHelper, pendingTransactionTracker, }); - return this.trackingMap; + + this.hub.emit('tracking-map-add', networkClientId); } /** @@ -2422,7 +2420,6 @@ export class TransactionController extends BaseControllerV1< const releaseLock = await this.mutex.acquire(); const index = transactions.findIndex(({ id }) => transactionId === id); const transactionMeta = transactions[index]; - const { txParams: { from }, networkClientId, @@ -2450,14 +2447,13 @@ export class TransactionController extends BaseControllerV1< } let { nonceTracker } = this; - if (networkClientId && this.enableMultichain) { + if (networkClientId) { const trackers = this.trackingMap.get(networkClientId); if (!trackers) { throw new Error('missing nonceTracker for networkClientId'); } nonceTracker = trackers?.nonceTracker; } - const [nonce, releaseNonce] = await getNextNonce( transactionMeta, nonceTracker, diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index aca50fbc81..5ac0f91d87 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -2236,6 +2236,16 @@ describe('TransactionController Integration', () => { result: '0x1', }, }, + ], + }); + mockNetwork({ + networkClientConfiguration: { + ...networkClientConfiguration, + type: NetworkClientType.Custom, + rpcUrl: 'https://mock.rpc.url', + }, + mocks: [ + // NetworkController // BlockTracker { request: { @@ -2243,28 +2253,76 @@ describe('TransactionController Integration', () => { params: [], }, response: { - result: '0x2', + result: '0x1', + }, + }, + // NetworkController + { + request: { + method: 'eth_getBlockByNumber', + params: ['0x1', false], + }, + response: { + result: { + baseFeePerGas: '0x63c498a46', + number: '0x42', + }, + }, + }, + // readAddressAsContract + // requiresFixedGas (cached) + { + request: { + method: 'eth_getCode', + params: [ACCOUNT_2_MOCK, '0x1'], + }, + response: { + result: '0x', // non contract + }, + }, + // getSuggestedGasFees + { + request: { + method: 'eth_gasPrice', + params: [], + }, + response: { + result: '0x1', }, }, ], }); const { networkController, transactionController } = await newController(); - const startTrackinSpy = jest.spyOn( - transactionController, - 'startTrackingByNetworkClientId', - ); - await networkController.upsertNetworkConfiguration( + const otherNetworkClientIdOnGoerli = + await networkController.upsertNetworkConfiguration( + { + ...networkClientConfiguration, + rpcUrl: 'https://mock.rpc.url', + }, + { + setActive: false, + referrer: 'https://mock.referrer', + source: 'dapp', + }, + ); + + await transactionController.addTransaction( { - ...networkClientConfiguration, - rpcUrl: 'https://mock.rpc.url', + from: ACCOUNT_MOCK, + to: ACCOUNT_3_MOCK, + }, + { + networkClientId: otherNetworkClientIdOnGoerli, }, - { setActive: false, referrer: 'https://mock.referrer', source: 'dapp' }, ); - expect(startTrackinSpy).toHaveBeenCalledTimes(1); - expect(transactionController).toBeDefined(); + expect(transactionController.state.transactions[0]).toStrictEqual( + expect.objectContaining({ + networkClientId: otherNetworkClientIdOnGoerli, + }), + ); }); it('should stop tracking when a network is removed', async () => { mockNetwork({ @@ -2295,10 +2353,6 @@ describe('TransactionController Integration', () => { }); const { networkController, transactionController } = await newController(); - const stopTrackinSpy = jest.spyOn( - transactionController, - 'stopTrackingByNetworkClientId', - ); const configurationId = await networkController.upsertNetworkConfiguration( @@ -2315,10 +2369,18 @@ describe('TransactionController Integration', () => { networkController.removeNetworkConfiguration(configurationId); - // advance time to trigger events - await advanceTime({ clock, duration: 1000 }); + await expect( + transactionController.addTransaction( + { + from: ACCOUNT_MOCK, + to: ACCOUNT_2_MOCK, + }, + { networkClientId: configurationId }, + ), + ).rejects.toThrow( + 'The networkClientId for this transaction could not be found', + ); - expect(stopTrackinSpy).toHaveBeenCalledTimes(1); expect(transactionController).toBeDefined(); transactionController.destroy(); }); @@ -2350,19 +2412,43 @@ describe('TransactionController Integration', () => { result: '0x2', }, }, + { + request: { + method: 'eth_getBlockByNumber', + params: ['0x1', false], + }, + response: { + result: { + baseFeePerGas: '0x63c498a46', + number: '0x42', + }, + }, + }, + { + request: { + method: 'eth_gasPrice', + params: [], + }, + response: { + result: '0x1', + }, + }, + // eth_getCode + { + request: { + method: 'eth_getCode', + params: [ACCOUNT_2_MOCK, '0x1'], + }, + response: { + result: '0x', // non contract + }, + }, ], }); + const { networkController, transactionController } = await newController({ enableMultichain: false, }); - const startTrackinSpy = jest.spyOn( - transactionController, - 'startTrackingByNetworkClientId', - ); - const stopTrackinSpy = jest.spyOn( - transactionController, - 'stopTrackingByNetworkClientId', - ); const configurationId = await networkController.upsertNetworkConfiguration( @@ -2377,13 +2463,27 @@ describe('TransactionController Integration', () => { }, ); - networkController.removeNetworkConfiguration(configurationId); - - // advance time to trigger events - await advanceTime({ clock, duration: 1000 }); + // add a transaction with the networkClientId of the newly added network + // and expect it to throw since the networkClientId won't be found in the trackingMap + await expect( + transactionController.addTransaction( + { + from: ACCOUNT_MOCK, + to: ACCOUNT_2_MOCK, + }, + { networkClientId: configurationId }, + ), + ).rejects.toThrow( + 'The networkClientId for this transaction could not be found', + ); - expect(startTrackinSpy).toHaveBeenCalledTimes(0); - expect(stopTrackinSpy).toHaveBeenCalledTimes(0); + // adding a transaction without a networkClientId should work + expect( + await transactionController.addTransaction({ + from: ACCOUNT_MOCK, + to: ACCOUNT_2_MOCK, + }), + ).toBeDefined(); }); }); diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index be3a6213ce..192b9720c7 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -5,8 +5,8 @@ import type { Operation } from 'fast-json-patch'; export type Events = { ['tracking-map-init']: [networkClientIds: NetworkClientId[]]; - ['tracking-map-add']: [networkClientIds: NetworkClientId[]]; - ['tracking-map-remove']: [networkClientIds: NetworkClientId[]]; + ['tracking-map-add']: [networkClientId: NetworkClientId]; + ['tracking-map-remove']: [networkClientId: NetworkClientId]; ['incomingTransactionBlock']: [blockNumber: number]; ['post-transaction-balance-updated']: [ { From 019f09e7fc6da45cdd7782fb114c02c45ae81d6c Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 29 Jan 2024 12:40:45 -0600 Subject: [PATCH 066/100] remove comment --- packages/transaction-controller/src/TransactionController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index c4298ef2f9..92c317a517 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -3071,7 +3071,7 @@ export class TransactionController extends BaseControllerV1< 'TransactionController#approveTransaction - Transaction signed', ); - this.onTransactionStatusChange(transactionMeta); // TODO: fake this via networkClient + this.onTransactionStatusChange(transactionMeta); const rawTx = bufferToHex(signedTx.serialize()); From 4aa57dc603975b45c17076e4cc608d98398a627e Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 29 Jan 2024 12:48:40 -0600 Subject: [PATCH 067/100] markNonceDuplicatesDropped test todo --- packages/transaction-controller/src/TransactionController.ts | 3 --- .../src/TransactionControllerIntegration.test.ts | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 92c317a517..f278fc1b3a 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -2870,9 +2870,6 @@ export class TransactionController extends BaseControllerV1< } const nonce = transactionMeta.txParams?.nonce; const from = transactionMeta.txParams?.from; - // NOTE(JL): the line below was removed upstream in favor of this.getChainId() - // not sure specifically why that was the case - // https://github.com/MetaMask/core/commit/89654542c9c61308cfad6a310f7fe2b4b669117b const { chainId } = transactionMeta; const sameNonceTxs = this.state.transactions.filter( diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index 5ac0f91d87..62626b939f 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -2218,6 +2218,9 @@ describe('TransactionController Integration', () => { transactionController.destroy(); }); }); + it.todo('markNonceDuplicatesDropped', () => { + // todo: test that the nonce duplicates are dropped + }); }); describe('when changing rpcUrl of networkClient', () => { From fbb2ada9deb25410ad746cde1e18b5eb9c7a95f8 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 29 Jan 2024 12:23:14 -0800 Subject: [PATCH 068/100] Add missing destroy() in spec 2 --- .../src/TransactionControllerIntegration.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index 62626b939f..f8ca19f0d9 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -2326,6 +2326,7 @@ describe('TransactionController Integration', () => { networkClientId: otherNetworkClientIdOnGoerli, }), ); + transactionController.destroy(); }); it('should stop tracking when a network is removed', async () => { mockNetwork({ @@ -2487,6 +2488,7 @@ describe('TransactionController Integration', () => { to: ACCOUNT_2_MOCK, }), ).toBeDefined(); + transactionController.destroy(); }); }); From 6b5c983e8c6b25c8820d2178442c3b262eb43153 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 29 Jan 2024 14:34:19 -0600 Subject: [PATCH 069/100] fix todo test --- .../src/TransactionControllerIntegration.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index f8ca19f0d9..86de769eb0 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -2218,9 +2218,7 @@ describe('TransactionController Integration', () => { transactionController.destroy(); }); }); - it.todo('markNonceDuplicatesDropped', () => { - // todo: test that the nonce duplicates are dropped - }); + it.todo('markNonceDuplicatesDropped'); }); describe('when changing rpcUrl of networkClient', () => { From 79bfec5d6c79ad95e8d4e303836622916d665f0a Mon Sep 17 00:00:00 2001 From: Shane Date: Tue, 30 Jan 2024 06:26:07 -0800 Subject: [PATCH 070/100] Update packages/transaction-controller/src/types.ts Co-authored-by: Matthew Walsh --- packages/transaction-controller/src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index 192b9720c7..1c75e3fc53 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -205,7 +205,7 @@ type TransactionMetaBase = { isUserOperation?: boolean; /** - * The id for the NetworkClient for the transaction. + * The ID of the network client used by the transaction. */ networkClientId?: NetworkClientId; From fa89ff49cfc986e89bc741af43121a3785ca5473 Mon Sep 17 00:00:00 2001 From: jiexi Date: Tue, 30 Jan 2024 11:07:22 -0800 Subject: [PATCH 071/100] Jl/transaction multichain fix nonce tracker lock (#3869) ## Explanation * Change `nonceMutexByChainId` from map of mutex by chainId to map of map of mutex by chainId and key * PendingTransactionTracker now accepts `getGlobalLock` fn in param instead of nonceTracker directly * Add `#acquireNonceLockForChainIdKey` that helps get the lock for the respective chainId and key * Update utils.getNextNonce to take anon fn to acquire nonceLock instead of nonceTracker directly ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- .../src/TransactionController.test.ts | 2 +- .../src/TransactionController.ts | 71 +-- .../TransactionControllerIntegration.test.ts | 406 +++++++++++++++++- .../helpers/PendingTransactionTracker.test.ts | 11 +- .../src/helpers/PendingTransactionTracker.ts | 13 +- .../src/utils/nonce.test.ts | 34 +- .../transaction-controller/src/utils/nonce.ts | 8 +- 7 files changed, 471 insertions(+), 74 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index bc3a7d2e82..ce32642e46 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -20,8 +20,8 @@ import type { } from '@metamask/network-controller'; import { NetworkClientType, NetworkStatus } from '@metamask/network-controller'; import { errorCodes, providerErrors, rpcErrors } from '@metamask/rpc-errors'; -import { EventEmitter } from 'events'; import { createDeferredPromise } from '@metamask/utils'; +import { EventEmitter } from 'events'; import * as NonceTrackerPackage from 'nonce-tracker'; import { FakeBlockTracker } from '../../../tests/fake-block-tracker'; diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 5240d90e37..a37a136e79 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -276,7 +276,7 @@ export class TransactionController extends BaseControllerV1< private readonly mutex = new Mutex(); - private readonly nonceMutexByChainId = new Map(); + private readonly nonceMutexesByChainId = new Map>(); private readonly getSavedGasFees: (chainId: Hex) => SavedGasFees | undefined; @@ -612,7 +612,10 @@ export class TransactionController extends BaseControllerV1< getEthQuery: () => this.ethQuery, getTransactions: () => this.state.transactions, isResubmitEnabled: pendingTransactions.isResubmitEnabled, - nonceTracker: this.nonceTracker, + getGlobalLock: async () => + this.#acquireNonceLockForChainIdKey({ + chainId: this.getChainId(), + }), publishTransaction: this.publishTransaction.bind(this), hooks: { beforeCheckPendingTransaction: @@ -1385,9 +1388,6 @@ export class TransactionController extends BaseControllerV1< ); } - if (!this.nonceMutexByChainId.get(chainId)) { - this.nonceMutexByChainId.set(chainId, new Mutex()); - } const ethQuery = new EthQuery(networkClient.provider); const nonceTracker = new NonceTracker({ @@ -1422,11 +1422,13 @@ export class TransactionController extends BaseControllerV1< const pendingTransactionTracker = new PendingTransactionTracker({ approveTransaction: this.approveTransaction.bind(this), blockTracker: networkClient.blockTracker, - getChainId: () => networkClient.configuration.chainId, + getChainId: () => chainId, getEthQuery: () => ethQuery, getTransactions: () => this.state.transactions, isResubmitEnabled: this.pendingTransactionOptions.isResubmitEnabled, - nonceTracker, + getGlobalLock: this.#acquireNonceLockForChainIdKey.bind(this, { + chainId, + }), publishTransaction: this.publishTransaction.bind(this), hooks: { beforeCheckPendingTransaction: @@ -1819,6 +1821,35 @@ export class TransactionController extends BaseControllerV1< return this.getTransaction(transactionId) as TransactionMeta; } + /** + * Gets the mutex intended to guard the nonceTracker for a particular chainId and key . + * + * @param opts - The options object. + * @param opts.chainId - The hex chainId. + * @param opts.key - The hex address (or constant) pertaining to the chainId + * @returns Mutex instance for the given chainId and key pair + */ + async #acquireNonceLockForChainIdKey({ + chainId, + key = 'global', + }: { + chainId: Hex; + key?: string; + }): Promise<() => void> { + let nonceMutexesForChainId = this.nonceMutexesByChainId.get(chainId); + if (!nonceMutexesForChainId) { + nonceMutexesForChainId = new Map(); + this.nonceMutexesByChainId.set(chainId, nonceMutexesForChainId); + } + let nonceMutexForKey = nonceMutexesForChainId.get(key); + if (!nonceMutexForKey) { + nonceMutexForKey = new Mutex(); + nonceMutexesForChainId.set(key, nonceMutexForKey); + } + + return await nonceMutexForKey.acquire(); + } + /** * Gets the next nonce according to the nonce-tracker. * Ensure `releaseLock` is called once processing of the `nonce` value is complete. @@ -1831,13 +1862,14 @@ export class TransactionController extends BaseControllerV1< address: string, networkClientId?: NetworkClientId, ): Promise { - let nonceMutexForChainId: Mutex | undefined; + let releaseLockForChainIdKey: (() => void) | undefined; let { nonceTracker } = this; if (networkClientId && this.enableMultichain) { const networkClient = this.getNetworkClientById(networkClientId); - nonceMutexForChainId = this.nonceMutexByChainId.get( - networkClient.configuration.chainId, - ); + releaseLockForChainIdKey = await this.#acquireNonceLockForChainIdKey({ + chainId: networkClient.configuration.chainId, + key: address, + }); const trackers = this.trackingMap.get(networkClientId); if (!trackers) { throw new Error('missing nonceTracker for networkClientId'); @@ -1845,22 +1877,21 @@ export class TransactionController extends BaseControllerV1< nonceTracker = trackers?.nonceTracker; } - // Acquires the lock for the chainId and the nonceLock from the nonceTracker and then + // Acquires the lock for the chainId + address and the nonceLock from the nonceTracker, then // couples them together by replacing the nonceLock's releaseLock method with // an anonymous function that calls releases both the original nonceLock and the // lock for the chainId. - const releaseLockForChainId = await nonceMutexForChainId?.acquire(); try { const nonceLock = await nonceTracker.getNonceLock(address); return { ...nonceLock, releaseLock: () => { nonceLock.releaseLock(); - releaseLockForChainId?.(); + releaseLockForChainIdKey?.(); }, }; } catch (err) { - releaseLockForChainId?.(); + releaseLockForChainIdKey?.(); throw err; } } @@ -2473,17 +2504,9 @@ export class TransactionController extends BaseControllerV1< return; } - let { nonceTracker } = this; - if (networkClientId) { - const trackers = this.trackingMap.get(networkClientId); - if (!trackers) { - throw new Error('missing nonceTracker for networkClientId'); - } - nonceTracker = trackers?.nonceTracker; - } const [nonce, releaseNonce] = await getNextNonce( transactionMeta, - nonceTracker, + (address: string) => this.getNonceLock(address, networkClientId), ); releaseNonceLock = releaseNonce; diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index 86de769eb0..9414ec7012 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -3193,7 +3193,7 @@ describe('TransactionController Integration', () => { transactionController.destroy(); }); - it('should block other attempts to get the nonce lock from the nonceTracker until the first one is released for the given networkClientId', async () => { + it('should block attempts to get the nonce lock for the same address from the nonceTracker for the networkClientId until the previous lock is released', async () => { mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ @@ -3298,6 +3298,349 @@ describe('TransactionController Integration', () => { transactionController.destroy(); }); + it('should block attempts to get the nonce lock for the same address from the nonceTracker for the different networkClientIds on the same chainId until the previous lock is released', async () => { + mockNetwork({ + networkClientConfiguration: mainnetNetworkClientConfiguration, + mocks: [ + // NetworkController + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + ], + }); + + const { networkController, transactionController } = await newController( + {}, + ); + mockNetwork({ + networkClientConfiguration, + mocks: [ + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + // NonceTracker + { + request: { + method: 'eth_getTransactionCount', + params: [ACCOUNT_MOCK, '0x1'], + }, + response: { + result: '0xa', + }, + }, + ], + }); + + mockNetwork({ + networkClientConfiguration: { + ...networkClientConfiguration, + type: NetworkClientType.Custom, + rpcUrl: 'https://mock.rpc.url', + }, + mocks: [ + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + // NonceTracker + { + request: { + method: 'eth_getTransactionCount', + params: [ACCOUNT_MOCK, '0x1'], + }, + response: { + result: '0xa', + }, + }, + ], + }); + + const otherNetworkClientIdOnGoerli = + await networkController.upsertNetworkConfiguration( + { + rpcUrl: 'https://mock.rpc.url', + chainId: networkClientConfiguration.chainId, + ticker: networkClientConfiguration.ticker, + }, + { + referrer: 'https://mock.referrer', + source: 'dapp', + }, + ); + + const firstNonceLockPromise = transactionController.getNonceLock( + ACCOUNT_MOCK, + 'goerli', + ); + await advanceTime({ clock, duration: 1 }); + + const firstNonceLock = await firstNonceLockPromise; + + expect(firstNonceLock.nextNonce).toBe(10); + + const secondNonceLockPromise = transactionController.getNonceLock( + ACCOUNT_MOCK, + otherNetworkClientIdOnGoerli, + ); + const delay = () => + new Promise(async (resolve) => { + await advanceTime({ clock, duration: 100 }); + resolve(null); + }); + + let secondNonceLockIfAcquired = await Promise.race([ + secondNonceLockPromise, + delay(), + ]); + expect(secondNonceLockIfAcquired).toBeNull(); + + await firstNonceLock.releaseLock(); + await advanceTime({ clock, duration: 1 }); + + secondNonceLockIfAcquired = await Promise.race([ + secondNonceLockPromise, + delay(), + ]); + expect(secondNonceLockIfAcquired?.nextNonce).toBe(10); + + transactionController.destroy(); + }); + + it('should not block attempts to get the nonce lock for the same addresses from the nonceTracker for different networkClientIds', async () => { + mockNetwork({ + networkClientConfiguration: mainnetNetworkClientConfiguration, + mocks: [ + // NetworkController + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + ], + }); + + const { transactionController } = await newController({}); + + mockNetwork({ + networkClientConfiguration, + mocks: [ + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + // NonceTracker + { + request: { + method: 'eth_getTransactionCount', + params: [ACCOUNT_MOCK, '0x1'], + }, + response: { + result: '0xa', + }, + }, + ], + }); + + mockNetwork({ + networkClientConfiguration: sepoliaNetworkClientConfiguration, + mocks: [ + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + // NonceTracker + { + request: { + method: 'eth_getTransactionCount', + params: [ACCOUNT_MOCK, '0x1'], + }, + response: { + result: '0xf', + }, + }, + ], + }); + + const firstNonceLockPromise = transactionController.getNonceLock( + ACCOUNT_MOCK, + 'goerli', + ); + await advanceTime({ clock, duration: 1 }); + + const firstNonceLock = await firstNonceLockPromise; + + expect(firstNonceLock.nextNonce).toBe(10); + + const secondNonceLockPromise = transactionController.getNonceLock( + ACCOUNT_MOCK, + 'sepolia', + ); + await advanceTime({ clock, duration: 1 }); + + const secondNonceLock = await secondNonceLockPromise; + + expect(secondNonceLock.nextNonce).toBe(15); + + transactionController.destroy(); + }); + + it('should not block attempts to get the nonce lock for different addresses from the nonceTracker for the networkClientId', async () => { + mockNetwork({ + networkClientConfiguration: mainnetNetworkClientConfiguration, + mocks: [ + // NetworkController + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + ], + }); + + const { networkController, transactionController } = await newController( + {}, + ); + + const networkClients = networkController.getNetworkClientRegistry(); + // Skip the globally selected provider because we can't use nock to mock it twice + const networkClientIds = Object.keys(networkClients).filter( + (v) => v !== networkClientConfiguration.network, + ); + await Promise.all( + networkClientIds.map(async (networkClientId) => { + const config = networkClients[networkClientId].configuration; + mockNetwork({ + networkClientConfiguration: config, + mocks: [ + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + // NonceTracker + { + request: { + method: 'eth_getTransactionCount', + params: [ACCOUNT_MOCK, '0x1'], + }, + response: { + result: '0xa', + }, + }, + // NonceTracker + { + request: { + method: 'eth_getTransactionCount', + params: [ACCOUNT_2_MOCK, '0x1'], + }, + response: { + result: '0xf', + }, + }, + ], + }); + + const firstNonceLockPromise = transactionController.getNonceLock( + ACCOUNT_MOCK, + networkClientId, + ); + await advanceTime({ clock, duration: 1 }); + + const firstNonceLock = await firstNonceLockPromise; + + expect(firstNonceLock.nextNonce).toBe(10); + + const secondNonceLockPromise = transactionController.getNonceLock( + ACCOUNT_2_MOCK, + networkClientId, + ); + await advanceTime({ clock, duration: 1 }); + + const secondNonceLock = await secondNonceLockPromise; + + expect(secondNonceLock.nextNonce).toBe(15); + }), + ); + transactionController.destroy(); + }); + it('should get the nonce lock from the globally selected nonceTracker if no networkClientId is provided', async () => { mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, @@ -3337,7 +3680,7 @@ describe('TransactionController Integration', () => { transactionController.destroy(); }); - it('should block other attempts to get the nonce lock from the globally selected nonceTracker until the first one is released if no networkClientId is provided', async () => { + it('should block attempts to get the nonce lock from the globally selected NonceTracker for the same address until the previous lock is released', async () => { mockNetwork({ networkClientConfiguration: mainnetNetworkClientConfiguration, mocks: [ @@ -3398,5 +3741,64 @@ describe('TransactionController Integration', () => { expect(secondNonceLockIfAcquired?.nextNonce).toBe(10); transactionController.destroy(); }); + + it('should not block attempts to get the nonce lock from the globally selected nonceTracker for different addresses', async () => { + mockNetwork({ + networkClientConfiguration: mainnetNetworkClientConfiguration, + mocks: [ + // NetworkController + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + // NonceTracker + { + request: { + method: 'eth_getTransactionCount', + params: [ACCOUNT_MOCK, '0x1'], + }, + response: { + result: '0xa', + }, + }, + // NonceTracker + { + request: { + method: 'eth_getTransactionCount', + params: [ACCOUNT_2_MOCK, '0x1'], + }, + response: { + result: '0xf', + }, + }, + ], + }); + + const { transactionController } = await newController({}); + + const firstNonceLockPromise = + transactionController.getNonceLock(ACCOUNT_MOCK); + await advanceTime({ clock, duration: 1 }); + + const firstNonceLock = await firstNonceLockPromise; + + expect(firstNonceLock.nextNonce).toBe(10); + + const secondNonceLockPromise = + transactionController.getNonceLock(ACCOUNT_2_MOCK); + await advanceTime({ clock, duration: 1 }); + + const secondNonceLock = await secondNonceLockPromise; + + expect(secondNonceLock.nextNonce).toBe(15); + + transactionController.destroy(); + }); }); }); diff --git a/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts b/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts index 4805ee89ff..34706ccd92 100644 --- a/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts +++ b/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts @@ -2,7 +2,6 @@ import { query } from '@metamask/controller-utils'; import type { BlockTracker } from '@metamask/network-controller'; -import type { NonceTracker } from 'nonce-tracker'; import type { TransactionMeta } from '../types'; import { TransactionStatus } from '../types'; @@ -54,14 +53,6 @@ function createBlockTrackerMock(): jest.Mocked { } as any; } -function createNonceTrackerMock(): jest.Mocked { - return { - getGlobalLock: () => Promise.resolve({ releaseLock: jest.fn() }), - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; -} - describe('PendingTransactionTracker', () => { const queryMock = jest.mocked(query); let blockTracker: jest.Mocked; @@ -101,7 +92,7 @@ describe('PendingTransactionTracker', () => { getChainId: () => CHAIN_ID_MOCK, getEthQuery: () => ETH_QUERY_MOCK, getTransactions: jest.fn(), - nonceTracker: createNonceTrackerMock(), + getGlobalLock: () => Promise.resolve(jest.fn()), publishTransaction: jest.fn(), }; }); diff --git a/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts b/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts index f816e4769c..ca89627772 100644 --- a/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts +++ b/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts @@ -6,7 +6,6 @@ import type { } from '@metamask/network-controller'; import { createModuleLogger } from '@metamask/utils'; import EventEmitter from 'events'; -import type { NonceTracker } from 'nonce-tracker'; import { projectLogger } from '../logger'; import type { TransactionMeta, TransactionReceipt } from '../types'; @@ -78,7 +77,7 @@ export class PendingTransactionTracker { // eslint-disable-next-line @typescript-eslint/no-explicit-any #listener: any; - #nonceTracker: NonceTracker; + #getGlobalLock: () => Promise<() => void>; #publishTransaction: (ethQuery: EthQuery, rawTx: string) => Promise; @@ -95,7 +94,7 @@ export class PendingTransactionTracker { getEthQuery, getTransactions, isResubmitEnabled, - nonceTracker, + getGlobalLock, publishTransaction, hooks, }: { @@ -105,7 +104,7 @@ export class PendingTransactionTracker { getEthQuery: (networkClientId?: NetworkClientId) => EthQuery; getTransactions: () => TransactionMeta[]; isResubmitEnabled?: boolean; - nonceTracker: NonceTracker; + getGlobalLock: () => Promise<() => void>; publishTransaction: (ethQuery: EthQuery, rawTx: string) => Promise; hooks?: { beforeCheckPendingTransaction?: ( @@ -124,7 +123,7 @@ export class PendingTransactionTracker { this.#getTransactions = getTransactions; this.#isResubmitEnabled = isResubmitEnabled ?? true; this.#listener = this.#onLatestBlock.bind(this); - this.#nonceTracker = nonceTracker; + this.#getGlobalLock = getGlobalLock; this.#publishTransaction = publishTransaction; this.#running = false; this.#beforePublish = hooks?.beforePublish ?? (() => true); @@ -165,7 +164,7 @@ export class PendingTransactionTracker { } async #onLatestBlock(latestBlockNumber: string) { - const nonceGlobalLock = await this.#nonceTracker.getGlobalLock(); + const releaseLock = await this.#getGlobalLock(); try { await this.#checkTransactions(); @@ -173,7 +172,7 @@ export class PendingTransactionTracker { /* istanbul ignore next */ log('Failed to check transactions', error); } finally { - nonceGlobalLock.releaseLock(); + releaseLock(); } try { diff --git a/packages/transaction-controller/src/utils/nonce.test.ts b/packages/transaction-controller/src/utils/nonce.test.ts index e48cdd46c5..87238f3a69 100644 --- a/packages/transaction-controller/src/utils/nonce.test.ts +++ b/packages/transaction-controller/src/utils/nonce.test.ts @@ -1,5 +1,5 @@ import type { - NonceTracker, + NonceLock, Transaction as NonceTrackerTransaction, } from 'nonce-tracker'; @@ -17,16 +17,6 @@ const TRANSACTION_META_MOCK: TransactionMeta = { }, }; -/** - * Creates a mock instance of a nonce tracker. - * @returns The mock instance. - */ -function createNonceTrackerMock(): jest.Mocked { - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return { getNonceLock: jest.fn() } as any; -} - describe('nonce', () => { describe('getNextNonce', () => { it('returns custom nonce if provided', async () => { @@ -35,11 +25,9 @@ describe('nonce', () => { customNonceValue: '123', }; - const nonceTracker = createNonceTrackerMock(); - const [nonce, releaseLock] = await getNextNonce( transactionMeta, - nonceTracker, + jest.fn(), ); expect(nonce).toBe('0x7b'); @@ -55,11 +43,9 @@ describe('nonce', () => { }, }; - const nonceTracker = createNonceTrackerMock(); - const [nonce, releaseLock] = await getNextNonce( transactionMeta, - nonceTracker, + jest.fn(), ); expect(nonce).toBe('0x123'); @@ -74,19 +60,15 @@ describe('nonce', () => { }, }; - const nonceTracker = createNonceTrackerMock(); const releaseLock = jest.fn(); - nonceTracker.getNonceLock.mockResolvedValueOnce({ - nextNonce: 456, - releaseLock, - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any); - const [nonce, resultReleaseLock] = await getNextNonce( transactionMeta, - nonceTracker, + () => + Promise.resolve({ + nextNonce: 456, + releaseLock, + } as unknown as NonceLock), ); expect(nonce).toBe('0x1c8'); diff --git a/packages/transaction-controller/src/utils/nonce.ts b/packages/transaction-controller/src/utils/nonce.ts index 346a5b400a..545f3a8156 100644 --- a/packages/transaction-controller/src/utils/nonce.ts +++ b/packages/transaction-controller/src/utils/nonce.ts @@ -1,6 +1,6 @@ import { toHex } from '@metamask/controller-utils'; import type { - NonceTracker, + NonceLock, Transaction as NonceTrackerTransaction, } from 'nonce-tracker'; @@ -13,12 +13,12 @@ const log = createModuleLogger(projectLogger, 'nonce'); * Determine the next nonce to be used for a transaction. * * @param txMeta - The transaction metadata. - * @param nonceTracker - An instance of a nonce tracker. + * @param getNonceLock - An anonymous function that acquires the nonce lock for an address * @returns The next hexadecimal nonce to be used for the given transaction, and optionally a function to release the nonce lock. */ export async function getNextNonce( txMeta: TransactionMeta, - nonceTracker: NonceTracker, + getNonceLock: (address: string) => Promise, ): Promise<[string, (() => void) | undefined]> { const { customNonceValue, @@ -37,7 +37,7 @@ export async function getNextNonce( return [existingNonce, undefined]; } - const nonceLock = await nonceTracker.getNonceLock(from); + const nonceLock = await getNonceLock(from); const nonce = toHex(nonceLock.nextNonce); const releaseLock = nonceLock.releaseLock.bind(nonceLock); From b1a025d8a31a76e02a514cacf9a384e095025111 Mon Sep 17 00:00:00 2001 From: jiexi Date: Tue, 30 Jan 2024 13:07:51 -0800 Subject: [PATCH 072/100] Move etherscan mocks (#3873) ## Explanation * Moves etherscan mocks into `test` folder outside of the `src` folder which also means it won't be included in builds ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- .../src/TransactionControllerIntegration.test.ts | 2 +- .../helpers/EtherscanRemoteTransactionSource.test.ts | 12 ++++++------ .../{src/helpers => test}/EtherscanMocks.ts | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) rename packages/transaction-controller/{src/helpers => test}/EtherscanMocks.ts (97%) diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index 9414ec7012..ab12180433 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -20,7 +20,7 @@ import { ETHERSCAN_TRANSACTION_RESPONSE_MOCK, ETHERSCAN_TOKEN_TRANSACTION_MOCK, ETHERSCAN_TRANSACTION_SUCCESS_MOCK, -} from './helpers/EtherscanMocks'; +} from '../test/EtherscanMocks'; import { TransactionController } from './TransactionController'; import type { TransactionMeta } from './types'; import { TransactionStatus, TransactionType } from './types'; diff --git a/packages/transaction-controller/src/helpers/EtherscanRemoteTransactionSource.test.ts b/packages/transaction-controller/src/helpers/EtherscanRemoteTransactionSource.test.ts index 946f2394c3..d70c394bd8 100644 --- a/packages/transaction-controller/src/helpers/EtherscanRemoteTransactionSource.test.ts +++ b/packages/transaction-controller/src/helpers/EtherscanRemoteTransactionSource.test.ts @@ -1,10 +1,5 @@ import { v1 as random } from 'uuid'; -import { CHAIN_IDS } from '../constants'; -import { - fetchEtherscanTokenTransactions, - fetchEtherscanTransactions, -} from '../utils/etherscan'; import { ID_MOCK, ETHERSCAN_TRANSACTION_RESPONSE_EMPTY_MOCK, @@ -16,7 +11,12 @@ import { EXPECTED_NORMALISED_TOKEN_TRANSACTION, ETHERSCAN_TOKEN_TRANSACTION_RESPONSE_ERROR_MOCK, ETHERSCAN_TRANSACTION_RESPONSE_ERROR_MOCK, -} from './EtherscanMocks'; +} from '../../test/EtherscanMocks'; +import { CHAIN_IDS } from '../constants'; +import { + fetchEtherscanTokenTransactions, + fetchEtherscanTransactions, +} from '../utils/etherscan'; import { EtherscanRemoteTransactionSource } from './EtherscanRemoteTransactionSource'; jest.mock('../utils/etherscan', () => ({ diff --git a/packages/transaction-controller/src/helpers/EtherscanMocks.ts b/packages/transaction-controller/test/EtherscanMocks.ts similarity index 97% rename from packages/transaction-controller/src/helpers/EtherscanMocks.ts rename to packages/transaction-controller/test/EtherscanMocks.ts index 4d2385c029..6598f9b9bc 100644 --- a/packages/transaction-controller/src/helpers/EtherscanMocks.ts +++ b/packages/transaction-controller/test/EtherscanMocks.ts @@ -1,10 +1,10 @@ -import { TransactionStatus, TransactionType } from '../types'; +import { TransactionStatus, TransactionType } from '../src/types'; import type { EtherscanTokenTransactionMeta, EtherscanTransactionMeta, EtherscanTransactionMetaBase, EtherscanTransactionResponse, -} from '../utils/etherscan'; +} from '../src/utils/etherscan'; export const ID_MOCK = '6843ba00-f4bf-11e8-a715-5f2fff84549d'; From 986492860c31da91ab1593d24ad99c4d8b8d02b0 Mon Sep 17 00:00:00 2001 From: jiexi Date: Tue, 30 Jan 2024 13:42:40 -0800 Subject: [PATCH 073/100] move # private to bottom of TxController (#3874) ## Explanation * Move all # private methods below the public ones in TransactionController ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- .../src/TransactionController.ts | 428 +++++++++--------- 1 file changed, 214 insertions(+), 214 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index a37a136e79..bc7a432f1f 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -675,68 +675,6 @@ export class TransactionController extends BaseControllerV1< this.#stopAllTracking(); } - #refreshEtherscanRemoteTransactionSources = ( - networkClients: ReturnType, - ) => { - // this will be prettier when we have consolidated network clients with a single chainId: - // check if there are still other network clients using the same chainId - // if not remove the etherscanRemoteTransaction source from the map - const chainIdsInRegistry = new Set(); - Object.values(networkClients).forEach((networkClient) => - chainIdsInRegistry.add(networkClient.configuration.chainId), - ); - const existingChainIds = Array.from( - this.etherscanRemoteTransactionSourcesMap.keys(), - ); - const chainIdsToRemove = existingChainIds.filter( - (chainId) => !chainIdsInRegistry.has(chainId), - ); - - chainIdsToRemove.forEach((chainId) => { - this.etherscanRemoteTransactionSourcesMap.delete(chainId); - }); - }; - - #refreshTrackingMap = ( - networkClients: ReturnType, - ) => { - const networkClientIds = Object.keys(networkClients); - const existingNetworkClientIds = Array.from(this.trackingMap.keys()); - - // Remove tracking for NetworkClientIds that no longer exist - const networkClientIdsToRemove = existingNetworkClientIds.filter( - (id) => !networkClientIds.includes(id), - ); - networkClientIdsToRemove.forEach((id) => { - this.#stopTrackingByNetworkClientId(id); - }); - - // Start tracking new NetworkClientIds from the registry - const networkClientIdsToAdd = networkClientIds.filter( - (id) => !existingNetworkClientIds.includes(id), - ); - networkClientIdsToAdd.forEach((id) => { - this.#startTrackingByNetworkClientId(id); - }); - }; - - #initTrackingMap = () => { - const networkClients = this.getNetworkClientRegistry(); - const networkClientIds = Object.keys(networkClients); - networkClientIds.map((id) => this.#startTrackingByNetworkClientId(id)); - this.hub.emit('tracking-map-init', networkClientIds); - }; - - #checkForPendingTransactionAndStartPolling = () => { - // PendingTransactionTracker reads state through its getTransactions hook - this.pendingTransactionTracker.onStateChange(); - if (this.enableMultichain) { - for (const [, trackingMap] of this.trackingMap) { - trackingMap.pendingTransactionTracker.onStateChange(); - } - } - }; - /** * Handle new method data request. * @@ -763,48 +701,6 @@ export class TransactionController extends BaseControllerV1< } } - #getEthQuery({ - networkClientId, - chainId, - }: { - networkClientId?: NetworkClientId; - chainId?: Hex; - }): EthQuery { - // if multichain is disabled, use the global ethQuery - if (!this.enableMultichain) { - return this.ethQuery; - } - let networkClient: - | AutoManagedNetworkClient - | undefined; - - if (networkClientId) { - try { - networkClient = this.getNetworkClientById(networkClientId); - } catch (err) { - log('failed to get network client by networkClientId'); - } - } - - if (!networkClient && chainId) { - try { - networkClientId = this.findNetworkClientIdByChainId(chainId); - networkClient = this.getNetworkClientById(networkClientId); - } catch (err) { - log('failed to get network client by chainId'); - } - } - - if (networkClient) { - return new EthQuery(networkClient.provider); - } - - // NOTE(JL): we're not ready to drop globally selected ethQuery yet. - // Some calls to getEthQuery only have access to optional networkClientId - // throw new Error('failed to get eth query instance'); - return this.ethQuery; - } - /** * Add a new unapproved transaction to state. Parameters will be validated, a * unique transaction id will be generated, and gas and gasPrice will be calculated @@ -1338,116 +1234,6 @@ export class TransactionController extends BaseControllerV1< this.hub.emit(`${transactionMeta.id}:speedup`, newTransactionMeta); } - #stopTrackingByNetworkClientId(networkClientId: NetworkClientId) { - const trackers = this.trackingMap.get(networkClientId); - if (trackers) { - trackers.pendingTransactionTracker.stop(); - this.removePendingTransactionTrackerListeners( - trackers.pendingTransactionTracker, - ); - trackers.incomingTransactionHelper.stop(); - this.removeIncomingTransactionHelperListeners( - trackers.incomingTransactionHelper, - ); - this.trackingMap.delete(networkClientId); - this.hub.emit('tracking-map-remove', networkClientId); - } - } - - #stopAllTracking() { - this.pendingTransactionTracker.stop(); - this.removePendingTransactionTrackerListeners(); - this.incomingTransactionHelper.stop(); - this.removeIncomingTransactionHelperListeners(); - - if (this.enableMultichain) { - for (const [networkClientId] of this.trackingMap) { - this.#stopTrackingByNetworkClientId(networkClientId); - } - } - } - - #startTrackingByNetworkClientId(networkClientId: NetworkClientId) { - const trackers = this.trackingMap.get(networkClientId); - if (trackers) { - return; - } - const networkClient = this.getNetworkClientById(networkClientId); - const { chainId } = networkClient.configuration; - - let etherscanRemoteTransactionSource = - this.etherscanRemoteTransactionSourcesMap.get(chainId); - if (!etherscanRemoteTransactionSource) { - etherscanRemoteTransactionSource = new EtherscanRemoteTransactionSource({ - includeTokenTransfers: - this.incomingTransactionOptions.includeTokenTransfers, - }); - this.etherscanRemoteTransactionSourcesMap.set( - chainId, - etherscanRemoteTransactionSource, - ); - } - - const ethQuery = new EthQuery(networkClient.provider); - - const nonceTracker = new NonceTracker({ - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - provider: networkClient.provider as any, - blockTracker: networkClient.blockTracker, - getPendingTransactions: this.getNonceTrackerPendingTransactions.bind( - this, - chainId, - ), - getConfirmedTransactions: this.getNonceTrackerTransactions.bind( - this, - TransactionStatus.confirmed, - ), - }); - - const incomingTransactionHelper = new IncomingTransactionHelper({ - blockTracker: networkClient.blockTracker, - getCurrentAccount: this.getSelectedAddress, - getLastFetchedBlockNumbers: () => this.state.lastFetchedBlockNumbers, - getChainId: () => chainId, - isEnabled: this.incomingTransactionOptions.isEnabled, - queryEntireHistory: this.incomingTransactionOptions.queryEntireHistory, - remoteTransactionSource: etherscanRemoteTransactionSource, - transactionLimit: this.config.txHistoryLimit, - updateTransactions: this.incomingTransactionOptions.updateTransactions, - }); - - this.addIncomingTransactionHelperListeners(incomingTransactionHelper); - - const pendingTransactionTracker = new PendingTransactionTracker({ - approveTransaction: this.approveTransaction.bind(this), - blockTracker: networkClient.blockTracker, - getChainId: () => chainId, - getEthQuery: () => ethQuery, - getTransactions: () => this.state.transactions, - isResubmitEnabled: this.pendingTransactionOptions.isResubmitEnabled, - getGlobalLock: this.#acquireNonceLockForChainIdKey.bind(this, { - chainId, - }), - publishTransaction: this.publishTransaction.bind(this), - hooks: { - beforeCheckPendingTransaction: - this.beforeCheckPendingTransaction.bind(this), - beforePublish: this.beforePublish.bind(this), - }, - }); - - this.addPendingTransactionTrackerListeners(pendingTransactionTracker); - - this.trackingMap.set(networkClientId, { - nonceTracker, - incomingTransactionHelper, - pendingTransactionTracker, - }); - - this.hub.emit('tracking-map-add', networkClientId); - } - /** * Estimates required gas for a given transaction. * @@ -3216,4 +3002,218 @@ export class TransactionController extends BaseControllerV1< log('Error while updating post transaction balance', error); } } + + #getEthQuery({ + networkClientId, + chainId, + }: { + networkClientId?: NetworkClientId; + chainId?: Hex; + }): EthQuery { + // if multichain is disabled, use the global ethQuery + if (!this.enableMultichain) { + return this.ethQuery; + } + let networkClient: + | AutoManagedNetworkClient + | undefined; + + if (networkClientId) { + try { + networkClient = this.getNetworkClientById(networkClientId); + } catch (err) { + log('failed to get network client by networkClientId'); + } + } + + if (!networkClient && chainId) { + try { + networkClientId = this.findNetworkClientIdByChainId(chainId); + networkClient = this.getNetworkClientById(networkClientId); + } catch (err) { + log('failed to get network client by chainId'); + } + } + + if (networkClient) { + return new EthQuery(networkClient.provider); + } + + // NOTE(JL): we're not ready to drop globally selected ethQuery yet. + // Some calls to getEthQuery only have access to optional networkClientId + // throw new Error('failed to get eth query instance'); + return this.ethQuery; + } + + #refreshEtherscanRemoteTransactionSources = ( + networkClients: ReturnType, + ) => { + // this will be prettier when we have consolidated network clients with a single chainId: + // check if there are still other network clients using the same chainId + // if not remove the etherscanRemoteTransaction source from the map + const chainIdsInRegistry = new Set(); + Object.values(networkClients).forEach((networkClient) => + chainIdsInRegistry.add(networkClient.configuration.chainId), + ); + const existingChainIds = Array.from( + this.etherscanRemoteTransactionSourcesMap.keys(), + ); + const chainIdsToRemove = existingChainIds.filter( + (chainId) => !chainIdsInRegistry.has(chainId), + ); + + chainIdsToRemove.forEach((chainId) => { + this.etherscanRemoteTransactionSourcesMap.delete(chainId); + }); + }; + + #refreshTrackingMap = ( + networkClients: ReturnType, + ) => { + const networkClientIds = Object.keys(networkClients); + const existingNetworkClientIds = Array.from(this.trackingMap.keys()); + + // Remove tracking for NetworkClientIds that no longer exist + const networkClientIdsToRemove = existingNetworkClientIds.filter( + (id) => !networkClientIds.includes(id), + ); + networkClientIdsToRemove.forEach((id) => { + this.#stopTrackingByNetworkClientId(id); + }); + + // Start tracking new NetworkClientIds from the registry + const networkClientIdsToAdd = networkClientIds.filter( + (id) => !existingNetworkClientIds.includes(id), + ); + networkClientIdsToAdd.forEach((id) => { + this.#startTrackingByNetworkClientId(id); + }); + }; + + #initTrackingMap = () => { + const networkClients = this.getNetworkClientRegistry(); + const networkClientIds = Object.keys(networkClients); + networkClientIds.map((id) => this.#startTrackingByNetworkClientId(id)); + this.hub.emit('tracking-map-init', networkClientIds); + }; + + #checkForPendingTransactionAndStartPolling = () => { + // PendingTransactionTracker reads state through its getTransactions hook + this.pendingTransactionTracker.onStateChange(); + if (this.enableMultichain) { + for (const [, trackingMap] of this.trackingMap) { + trackingMap.pendingTransactionTracker.onStateChange(); + } + } + }; + + #stopTrackingByNetworkClientId(networkClientId: NetworkClientId) { + const trackers = this.trackingMap.get(networkClientId); + if (trackers) { + trackers.pendingTransactionTracker.stop(); + this.removePendingTransactionTrackerListeners( + trackers.pendingTransactionTracker, + ); + trackers.incomingTransactionHelper.stop(); + this.removeIncomingTransactionHelperListeners( + trackers.incomingTransactionHelper, + ); + this.trackingMap.delete(networkClientId); + this.hub.emit('tracking-map-remove', networkClientId); + } + } + + #stopAllTracking() { + this.pendingTransactionTracker.stop(); + this.removePendingTransactionTrackerListeners(); + this.incomingTransactionHelper.stop(); + this.removeIncomingTransactionHelperListeners(); + + if (this.enableMultichain) { + for (const [networkClientId] of this.trackingMap) { + this.#stopTrackingByNetworkClientId(networkClientId); + } + } + } + + #startTrackingByNetworkClientId(networkClientId: NetworkClientId) { + const trackers = this.trackingMap.get(networkClientId); + if (trackers) { + return; + } + const networkClient = this.getNetworkClientById(networkClientId); + const { chainId } = networkClient.configuration; + + let etherscanRemoteTransactionSource = + this.etherscanRemoteTransactionSourcesMap.get(chainId); + if (!etherscanRemoteTransactionSource) { + etherscanRemoteTransactionSource = new EtherscanRemoteTransactionSource({ + includeTokenTransfers: + this.incomingTransactionOptions.includeTokenTransfers, + }); + this.etherscanRemoteTransactionSourcesMap.set( + chainId, + etherscanRemoteTransactionSource, + ); + } + + const ethQuery = new EthQuery(networkClient.provider); + + const nonceTracker = new NonceTracker({ + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + provider: networkClient.provider as any, + blockTracker: networkClient.blockTracker, + getPendingTransactions: this.getNonceTrackerPendingTransactions.bind( + this, + chainId, + ), + getConfirmedTransactions: this.getNonceTrackerTransactions.bind( + this, + TransactionStatus.confirmed, + ), + }); + + const incomingTransactionHelper = new IncomingTransactionHelper({ + blockTracker: networkClient.blockTracker, + getCurrentAccount: this.getSelectedAddress, + getLastFetchedBlockNumbers: () => this.state.lastFetchedBlockNumbers, + getChainId: () => chainId, + isEnabled: this.incomingTransactionOptions.isEnabled, + queryEntireHistory: this.incomingTransactionOptions.queryEntireHistory, + remoteTransactionSource: etherscanRemoteTransactionSource, + transactionLimit: this.config.txHistoryLimit, + updateTransactions: this.incomingTransactionOptions.updateTransactions, + }); + + this.addIncomingTransactionHelperListeners(incomingTransactionHelper); + + const pendingTransactionTracker = new PendingTransactionTracker({ + approveTransaction: this.approveTransaction.bind(this), + blockTracker: networkClient.blockTracker, + getChainId: () => chainId, + getEthQuery: () => ethQuery, + getTransactions: () => this.state.transactions, + isResubmitEnabled: this.pendingTransactionOptions.isResubmitEnabled, + getGlobalLock: this.#acquireNonceLockForChainIdKey.bind(this, { + chainId, + }), + publishTransaction: this.publishTransaction.bind(this), + hooks: { + beforeCheckPendingTransaction: + this.beforeCheckPendingTransaction.bind(this), + beforePublish: this.beforePublish.bind(this), + }, + }); + + this.addPendingTransactionTrackerListeners(pendingTransactionTracker); + + this.trackingMap.set(networkClientId, { + nonceTracker, + incomingTransactionHelper, + pendingTransactionTracker, + }); + + this.hub.emit('tracking-map-add', networkClientId); + } } From 8fb21c7dcfae94c8497ef962bf8671fb837af5c1 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Tue, 30 Jan 2024 16:14:21 -0600 Subject: [PATCH 074/100] Improve rate limiting logic in `etherscanRemoteTransactionSource` (#3875) Addresses [feedback](https://github.com/MetaMask/core/pull/3643#discussion_r1470225885) given on the feature branch. We no longer wait til the rate limit time is up to return the fetchedTransaction. We only wait the full time to unlock the mutex to prevent another request until 5 seconds have elapsed. --- .../TransactionControllerIntegration.test.ts | 2 +- .../EtherscanRemoteTransactionSource.ts | 26 +++++++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index ab12180433..7137565f74 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -3095,7 +3095,7 @@ describe('TransactionController Integration', () => { ); // we have to wait for the mutex to be released after the 5 second API rate limit timer - await advanceTime({ clock, duration: 5000 }); + await advanceTime({ clock, duration: 1 }); expect(transactionController.state.transactions).toHaveLength( 2 * networkClientIds.length, diff --git a/packages/transaction-controller/src/helpers/EtherscanRemoteTransactionSource.ts b/packages/transaction-controller/src/helpers/EtherscanRemoteTransactionSource.ts index 470aec5683..5274d6c9b4 100644 --- a/packages/transaction-controller/src/helpers/EtherscanRemoteTransactionSource.ts +++ b/packages/transaction-controller/src/helpers/EtherscanRemoteTransactionSource.ts @@ -24,6 +24,7 @@ import type { EtherscanTransactionResponse, } from '../utils/etherscan'; +const ETHERSCAN_RATE_LIMIT_INTERVAL = 5000; /** * A RemoteTransactionSource that fetches transaction data from Etherscan. */ @@ -36,8 +37,6 @@ export class EtherscanRemoteTransactionSource #mutex = new Mutex(); - ETHERSCAN_RATE_LIMIT_INTERVAL = 5000; - constructor({ includeTokenTransfers, }: { includeTokenTransfers?: boolean } = {}) { @@ -75,15 +74,20 @@ export class EtherscanRemoteTransactionSource return transactions; } finally { - const elapsedTime = Date.now() - acquiredTime; - const remainingTime = Math.max( - 0, - this.ETHERSCAN_RATE_LIMIT_INTERVAL - elapsedTime, - ); - // Wait for the remaining time if it hasn't been 5 seconds yet - if (remainingTime > 0) { - await new Promise((resolve) => setTimeout(resolve, remainingTime)); - } + this.#releaseLockAfterInterval(acquiredTime, releaseLock); + } + } + + #releaseLockAfterInterval(acquireTime: number, releaseLock: () => void) { + const elapsedTime = Date.now() - acquireTime; + const remainingTime = Math.max( + 0, + ETHERSCAN_RATE_LIMIT_INTERVAL - elapsedTime, + ); + // Wait for the remaining time if it hasn't been 5 seconds yet + if (remainingTime > 0) { + setTimeout(releaseLock, remainingTime); + } else { releaseLock(); } } From 3c5d1e967581aeb865780b750039abdd2c9921e0 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 30 Jan 2024 14:19:51 -0800 Subject: [PATCH 075/100] Fix type import issue --- .../transaction-controller/src/TransactionController.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index a37a136e79..dbb3647c5c 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -28,10 +28,9 @@ import type { NetworkControllerStateChangeEvent, NetworkState, Provider, - NetworkClientConfiguration, + NetworkClient, } from '@metamask/network-controller'; import { NetworkClientType } from '@metamask/network-controller'; -import type { AutoManagedNetworkClient } from '@metamask/network-controller/src/create-auto-managed-network-client'; import { errorCodes, rpcErrors, providerErrors } from '@metamask/rpc-errors'; import type { Hex } from '@metamask/utils'; import { Mutex } from 'async-mutex'; @@ -774,9 +773,7 @@ export class TransactionController extends BaseControllerV1< if (!this.enableMultichain) { return this.ethQuery; } - let networkClient: - | AutoManagedNetworkClient - | undefined; + let networkClient: NetworkClient | undefined; if (networkClientId) { try { From d182fd150929b25a7f45422bce3076665f03643c Mon Sep 17 00:00:00 2001 From: Shane Jonas Date: Wed, 31 Jan 2024 15:31:43 -0500 Subject: [PATCH 076/100] Changed onStateChange public function name to startIfPendingTransactions --- .../src/TransactionController.test.ts | 1 + .../src/TransactionController.ts | 6 +++--- .../helpers/PendingTransactionTracker.test.ts | 18 +++++++++--------- .../src/helpers/PendingTransactionTracker.ts | 2 +- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index ce32642e46..eeaf310b1d 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -715,6 +715,7 @@ describe('TransactionController', () => { const pendingTransactionTrackerMock = { start: jest.fn(), stop: jest.fn(), + startIfPendingTransactions: jest.fn(), hub: { on: jest.fn(), removeAllListeners: jest.fn(), diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 2c95a0228c..60d9cf0bb8 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -633,7 +633,7 @@ export class TransactionController extends BaseControllerV1< // selectedNetworkClientId changes onNetworkStateChange(() => { log('Detected network change', this.getChainId()); - this.pendingTransactionTracker.onStateChange(); + this.pendingTransactionTracker.startIfPendingTransactions(); this.onBootCleanup(); }); @@ -3096,10 +3096,10 @@ export class TransactionController extends BaseControllerV1< #checkForPendingTransactionAndStartPolling = () => { // PendingTransactionTracker reads state through its getTransactions hook - this.pendingTransactionTracker.onStateChange(); + this.pendingTransactionTracker.startIfPendingTransactions(); if (this.enableMultichain) { for (const [, trackingMap] of this.trackingMap) { - trackingMap.pendingTransactionTracker.onStateChange(); + trackingMap.pendingTransactionTracker.startIfPendingTransactions(); } } }; diff --git a/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts b/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts index 34706ccd92..736f54cc56 100644 --- a/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts +++ b/packages/transaction-controller/src/helpers/PendingTransactionTracker.test.ts @@ -70,7 +70,7 @@ describe('PendingTransactionTracker', () => { { ...TRANSACTION_SUBMITTED_MOCK }, ]); - pendingTransactionTracker.onStateChange(); + pendingTransactionTracker.startIfPendingTransactions(); if (transactionsOnCheck) { options.getTransactions.mockReturnValue(transactionsOnCheck); @@ -103,7 +103,7 @@ describe('PendingTransactionTracker', () => { options.getTransactions.mockReturnValue([TRANSACTION_SUBMITTED_MOCK]); - pendingTransactionTracker.onStateChange(); + pendingTransactionTracker.startIfPendingTransactions(); expect(blockTracker.on).toHaveBeenCalledTimes(1); expect(blockTracker.on).toHaveBeenCalledWith( @@ -117,8 +117,8 @@ describe('PendingTransactionTracker', () => { options.getTransactions.mockReturnValue([TRANSACTION_SUBMITTED_MOCK]); - pendingTransactionTracker.onStateChange(); - pendingTransactionTracker.onStateChange(); + pendingTransactionTracker.startIfPendingTransactions(); + pendingTransactionTracker.startIfPendingTransactions(); expect(blockTracker.on).toHaveBeenCalledTimes(1); expect(blockTracker.removeListener).toHaveBeenCalledTimes(0); @@ -129,13 +129,13 @@ describe('PendingTransactionTracker', () => { options.getTransactions.mockReturnValue([TRANSACTION_SUBMITTED_MOCK]); - pendingTransactionTracker.onStateChange(); + pendingTransactionTracker.startIfPendingTransactions(); expect(blockTracker.removeListener).toHaveBeenCalledTimes(0); options.getTransactions.mockReturnValue([]); - pendingTransactionTracker.onStateChange(); + pendingTransactionTracker.startIfPendingTransactions(); expect(blockTracker.removeListener).toHaveBeenCalledTimes(1); expect(blockTracker.removeListener).toHaveBeenCalledWith( @@ -149,17 +149,17 @@ describe('PendingTransactionTracker', () => { options.getTransactions.mockReturnValue([TRANSACTION_SUBMITTED_MOCK]); - pendingTransactionTracker.onStateChange(); + pendingTransactionTracker.startIfPendingTransactions(); expect(blockTracker.removeListener).toHaveBeenCalledTimes(0); options.getTransactions.mockReturnValue([]); - pendingTransactionTracker.onStateChange(); + pendingTransactionTracker.startIfPendingTransactions(); expect(blockTracker.removeListener).toHaveBeenCalledTimes(1); - pendingTransactionTracker.onStateChange(); + pendingTransactionTracker.startIfPendingTransactions(); expect(blockTracker.removeListener).toHaveBeenCalledTimes(1); }); diff --git a/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts b/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts index ca89627772..66deeb2f9b 100644 --- a/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts +++ b/packages/transaction-controller/src/helpers/PendingTransactionTracker.ts @@ -131,7 +131,7 @@ export class PendingTransactionTracker { hooks?.beforeCheckPendingTransaction ?? (() => true); } - onStateChange = () => { + startIfPendingTransactions = () => { const pendingTransactions = this.#getPendingTransactions(); if (pendingTransactions.length) { From 0ee4fb899b41fe66ac3073d35fc1f078737b105b Mon Sep 17 00:00:00 2001 From: Shane Jonas Date: Wed, 31 Jan 2024 15:43:17 -0500 Subject: [PATCH 077/100] Added tests for gethEtherscanApiHost --- .../src/utils/etherscan.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/transaction-controller/src/utils/etherscan.test.ts b/packages/transaction-controller/src/utils/etherscan.test.ts index 3087c729f6..bcafa69b5d 100644 --- a/packages/transaction-controller/src/utils/etherscan.test.ts +++ b/packages/transaction-controller/src/utils/etherscan.test.ts @@ -7,6 +7,7 @@ import type { EtherscanTransactionResponse, } from './etherscan'; import * as Etherscan from './etherscan'; +import {getEtherscanApiHost}from './etherscan'; jest.mock('@metamask/controller-utils', () => ({ ...jest.requireActual('@metamask/controller-utils'), @@ -37,6 +38,21 @@ describe('Etherscan', () => { jest.resetAllMocks(); }); + describe('getEtherscanApiHost', () => { + it('returns Etherscan API host for supported network', () => { + expect(getEtherscanApiHost(CHAIN_IDS.GOERLI)).toBe( + `https://${ETHERSCAN_SUPPORTED_NETWORKS[CHAIN_IDS.GOERLI].subdomain}.${ + ETHERSCAN_SUPPORTED_NETWORKS[CHAIN_IDS.GOERLI].domain + }`, + ); + }); + it('returns an error for unsupported network', () => { + expect(() => getEtherscanApiHost('0x11111111111111111111')).toThrow( + 'Etherscan does not support chain with ID: 0x11111111111111111111', + ); + }); + }); + describe.each([ ['fetchEtherscanTransactions', 'txlist'], ['fetchEtherscanTokenTransactions', 'tokentx'], From 909eed4347a6d2cc761119058be2e326352f4d40 Mon Sep 17 00:00:00 2001 From: Shane Jonas Date: Wed, 31 Jan 2024 15:46:52 -0500 Subject: [PATCH 078/100] Added type alias for NetworkClientRegistry --- .../src/TransactionController.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 60d9cf0bb8..a65aa1d0d6 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -213,6 +213,16 @@ type PendingTransactionOptions = { isResubmitEnabled?: boolean; }; +/** + * @type NetworkClientRegistry + * + * Registry of network clients provided by the NetworkController + */ + +type NetworkClientRegistry = ReturnType< + NetworkController['getNetworkClientRegistry'] +>; + /** * The name of the {@link TransactionController}. */ @@ -3043,7 +3053,7 @@ export class TransactionController extends BaseControllerV1< } #refreshEtherscanRemoteTransactionSources = ( - networkClients: ReturnType, + networkClients: NetworkClientRegistry, ) => { // this will be prettier when we have consolidated network clients with a single chainId: // check if there are still other network clients using the same chainId From bcc00b3baf1f35418ffa4381a6852afd3f690786 Mon Sep 17 00:00:00 2001 From: Shane Jonas Date: Wed, 31 Jan 2024 15:50:55 -0500 Subject: [PATCH 079/100] Changed #refreshEtherscanRemoteTransactionSources to be called in #refreshTrackingMap --- packages/transaction-controller/src/TransactionController.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index a65aa1d0d6..4aa68c94fc 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -661,7 +661,6 @@ export class TransactionController extends BaseControllerV1< }); this.#refreshTrackingMap(networkClients); - this.#refreshEtherscanRemoteTransactionSources(networkClients); } }, ); @@ -3077,6 +3076,8 @@ export class TransactionController extends BaseControllerV1< #refreshTrackingMap = ( networkClients: ReturnType, ) => { + this.#refreshEtherscanRemoteTransactionSources(networkClients); + const networkClientIds = Object.keys(networkClients); const existingNetworkClientIds = Array.from(this.trackingMap.keys()); From cbe2fbb7bd8dd2aed90f8cf04c0c93e6d85e0f7c Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Wed, 31 Jan 2024 13:13:37 -0800 Subject: [PATCH 080/100] lint --- packages/transaction-controller/src/utils/etherscan.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transaction-controller/src/utils/etherscan.test.ts b/packages/transaction-controller/src/utils/etherscan.test.ts index bcafa69b5d..222dbc1240 100644 --- a/packages/transaction-controller/src/utils/etherscan.test.ts +++ b/packages/transaction-controller/src/utils/etherscan.test.ts @@ -7,7 +7,7 @@ import type { EtherscanTransactionResponse, } from './etherscan'; import * as Etherscan from './etherscan'; -import {getEtherscanApiHost}from './etherscan'; +import { getEtherscanApiHost } from './etherscan'; jest.mock('@metamask/controller-utils', () => ({ ...jest.requireActual('@metamask/controller-utils'), From 3d4d733eea18a0199952e0b02b19601823722798 Mon Sep 17 00:00:00 2001 From: Shane Jonas Date: Fri, 2 Feb 2024 10:35:46 -0500 Subject: [PATCH 081/100] Fixed alias return type for NetworkClientRegistry --- packages/transaction-controller/src/TransactionController.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 4aa68c94fc..04ba81da78 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -3073,9 +3073,7 @@ export class TransactionController extends BaseControllerV1< }); }; - #refreshTrackingMap = ( - networkClients: ReturnType, - ) => { + #refreshTrackingMap = (networkClients: NetworkClientRegistry) => { this.#refreshEtherscanRemoteTransactionSources(networkClients); const networkClientIds = Object.keys(networkClients); From 00d5f3cfd8754ea11b683bd1b7cc42a60fab89d8 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Fri, 2 Feb 2024 10:41:12 -0600 Subject: [PATCH 082/100] Ad/transaction multichain feedback (#3885) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses feedback from @mcmire on https://github.com/MetaMask/core/pull/3643 ### Tests deleted in `TransactionController.test.ts` -> what tests handle the same coverage: ‘doesnt get called if the feature flag is disabled' -> `should not call getNetworkClientRegistry on networkController:stateChange when feature flag is disabled` 'should initialize the tracking map on construction' -> `should call getNetworkClientRegistry on construction when feature flag is enabled` 'should handle removals from the networkController registry' -> `should stop tracking when a network is removed` 'should handle additions to the networkController registry' -> `should start tracking when a new network is added` --- .../src/TransactionController.test.ts | 193 +------- .../src/TransactionController.ts | 4 - .../TransactionControllerIntegration.test.ts | 460 +++++++++++++----- 3 files changed, 338 insertions(+), 319 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index eeaf310b1d..f0a280bccc 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -21,7 +21,6 @@ import type { import { NetworkClientType, NetworkStatus } from '@metamask/network-controller'; import { errorCodes, providerErrors, rpcErrors } from '@metamask/rpc-errors'; import { createDeferredPromise } from '@metamask/utils'; -import { EventEmitter } from 'events'; import * as NonceTrackerPackage from 'nonce-tracker'; import { FakeBlockTracker } from '../../../tests/fake-block-tracker'; @@ -33,7 +32,6 @@ import type { TransactionControllerMessenger, TransactionConfig, TransactionState, - TransactionControllerEventEmitter, } from './TransactionController'; import { TransactionController } from './TransactionController'; import type { @@ -1187,7 +1185,7 @@ describe('TransactionController', () => { ); }); - describe('multichain', () => { + describe('when enableMultichain: true is specified', () => { it('adds unapproved transaction to state when using networkClientId', async () => { const controller = newController({ options: { enableMultichain: true }, @@ -1213,6 +1211,7 @@ describe('TransactionController', () => { expect(transactionMeta.networkClientId).toBe('sepolia'); expect(transactionMeta.origin).toBe('metamask'); }); + it('adds unapproved transaction with networkClientId and can be updated to submitted', async () => { const controller = newController({ approve: true, @@ -4780,194 +4779,6 @@ describe('TransactionController', () => { }); }); - describe('initTrackingMap', () => { - it('doesnt get called if the feature flag is disabled', () => { - const hub = new EventEmitter() as TransactionControllerEventEmitter; - const spy = jest.fn(); - hub.on('tracking-map-init', spy); - newController({ - options: { - enableMultichain: false, - hub, - }, - }); - expect(spy).not.toHaveBeenCalled(); - }); - it('should initialize the tracking map on construction', async () => { - const hub = new EventEmitter() as TransactionControllerEventEmitter; - const receivedEvents = new Promise((resolve) => { - hub.on('tracking-map-init', (networkClientIds) => { - expect(networkClientIds).toStrictEqual([ - 'mainnet', - 'sepolia', - 'goerli', - 'customNetworkClientId-1', - ]); - resolve(undefined); - }); - }); - - newController({ - options: { - enableMultichain: true, - hub, - }, - }); - await receivedEvents; - }); - it('should handle removals from the networkController registry', async () => { - const hub = new EventEmitter() as TransactionControllerEventEmitter; - const mockGetNetworkClientRegistry = jest.fn(); - mockGetNetworkClientRegistry.mockImplementation(() => ({ - sepolia: { - configuration: { - chainId: ChainId.sepolia, - }, - }, - goerli: { - configuration: { - chainId: ChainId.goerli, - }, - }, - 'customNetworkClientId-1': { - configuration: { - chainId: '0xa', - }, - }, - })); - const receivedEvents = new Promise((resolve) => { - let expectedNetworkClientIds = ['customNetworkClientId-1', 'goerli']; - hub.on('tracking-map-remove', (networkClientId) => { - expect(expectedNetworkClientIds).toContain(networkClientId); - expectedNetworkClientIds = expectedNetworkClientIds.filter( - (v) => v !== networkClientId, - ); - if (expectedNetworkClientIds.length === 0) { - resolve(undefined); - } - }); - }); - - const mockMessenger = buildMockMessenger({}); - (mockMessenger.messenger.subscribe as jest.Mock).mockImplementation( - (_type, handler) => { - setTimeout(() => { - handler({}, [ - { - op: 'remove', - path: ['networkConfigurations', 'goerli'], - value: 'foo', - }, - { - op: 'remove', - path: ['networkConfigurations', 'customNetworkClientId-1'], - value: 'foo', - }, - ]); - }, 0); - }, - ); - - newController({ - options: { - messenger: mockMessenger.messenger, - getNetworkClientRegistry: mockGetNetworkClientRegistry, - enableMultichain: true, - hub, - }, - }); - await receivedEvents; - }); - }); - it('should handle additions to the networkController registry', async () => { - const hub = new EventEmitter() as TransactionControllerEventEmitter; - const mockGetNetworkClientRegistry = jest.fn(); - mockGetNetworkClientRegistry.mockImplementationOnce(() => ({ - sepolia: { - configuration: { - chainId: ChainId.sepolia, - }, - }, - })); - - hub.on('tracking-map-init', () => { - mockGetNetworkClientRegistry.mockClear(); - mockGetNetworkClientRegistry.mockImplementation(() => ({ - sepolia: { - configuration: { - chainId: ChainId.sepolia, - }, - }, - goerli: { - configuration: { - chainId: ChainId.goerli, - }, - }, - })); - }); - - const receivedEvents = new Promise((resolve) => { - let expectedNetworkClientIds = ['goerli', 'sepolia']; - hub.on('tracking-map-add', (networkClientId) => { - expect(expectedNetworkClientIds).toContain(networkClientId); - expectedNetworkClientIds = expectedNetworkClientIds.filter( - (v) => v !== networkClientId, - ); - if (expectedNetworkClientIds.length === 0) { - resolve(undefined); - } - }); - }); - const mockMessenger = buildMockMessenger({}); - (mockMessenger.messenger.subscribe as jest.Mock).mockImplementation( - (_type, handler) => { - setTimeout(() => { - handler({}, [ - { - op: 'add', - path: ['networkConfigurations', 'goerli'], - value: 'foo', - }, - ]); - }, 0); - }, - ); - - newController({ - options: { - messenger: mockMessenger.messenger, - getNetworkClientRegistry: mockGetNetworkClientRegistry, - enableMultichain: true, - hub, - }, - }); - await receivedEvents; - }); - - describe('startIncomingTransactionPolling', () => { - it('should start the global incoming transaction helper when no networkClientIds provided', () => { - const controller = newController({ - options: { - enableMultichain: true, - }, - }); - - controller.startIncomingTransactionPolling([]); - - expect(incomingTransactionHelperMocks[0].start).toHaveBeenCalledTimes(1); - }); - }); - - describe('stopIncomingTransactionPolling', () => { - it('should stop the global incoming transaction helper when no networkClientIds provided', () => { - const controller = newController(); - - controller.stopIncomingTransactionPolling([]); - - expect(incomingTransactionHelperMocks[0].stop).toHaveBeenCalledTimes(1); - }); - }); - describe('updateIncomingTransactions', () => { it('should update the global incoming transactions when no networkClientIds provided', async () => { const controller = newController(); diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 04ba81da78..5a236b4e10 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -447,7 +447,6 @@ export class TransactionController extends BaseControllerV1< * @param options.hooks.beforeCheckPendingTransaction - Additional logic to execute before checking pending transactions. Return false to prevent the broadcast of the transaction. * @param options.hooks.beforePublish - Additional logic to execute before publishing a transaction. Return false to prevent the broadcast of the transaction. * @param options.hooks.getAdditionalSignArguments - Returns additional arguments required to sign a transaction. - * @param options.hub - Use a different event emitter for the hub. * @param options.getNetworkClientRegistry - Gets the network client registry. * @param options.enableMultichain - Enable multichain support. * @param config - Initial options used to configure this controller. @@ -478,7 +477,6 @@ export class TransactionController extends BaseControllerV1< findNetworkClientIdByChainId, getNetworkClientById, getNetworkClientRegistry, - hub, enableMultichain = false, hooks = {}, }: { @@ -508,7 +506,6 @@ export class TransactionController extends BaseControllerV1< findNetworkClientIdByChainId: NetworkController['findNetworkClientIdByChainId']; getNetworkClientById: NetworkController['getNetworkClientById']; getNetworkClientRegistry: NetworkController['getNetworkClientRegistry']; - hub: TransactionControllerEventEmitter; enableMultichain: boolean; hooks: { afterSign?: ( @@ -540,7 +537,6 @@ export class TransactionController extends BaseControllerV1< lastFetchedBlockNumbers: {}, }; this.initialize(); - this.hub = hub ?? this.hub; this.enableMultichain = enableMultichain; this.findNetworkClientIdByChainId = findNetworkClientIdByChainId; this.getNetworkClientById = getNetworkClientById; diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index 7137565f74..9e8a30e618 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -3,12 +3,14 @@ import { ControllerMessenger } from '@metamask/base-controller'; import { ApprovalType, BUILT_IN_NETWORKS, + InfuraNetworkType, NetworkType, } from '@metamask/controller-utils'; import { NetworkController, NetworkClientType, } from '@metamask/network-controller'; +import type { NetworkClientConfiguration } from '@metamask/network-controller'; import nock from 'nock'; import type { SinonFakeTimers } from 'sinon'; import { useFakeTimers } from 'sinon'; @@ -30,30 +32,32 @@ import * as etherscanUtils from './utils/etherscan'; const ACCOUNT_MOCK = '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207'; const ACCOUNT_2_MOCK = '0x08f137f335ea1b8f193b8f6ea92561a60d23a211'; const ACCOUNT_3_MOCK = '0xe688b84b23f322a994a53dbf8e15fa82cdb71127'; -const infuraProjectId = '341eacb578dd44a1a049cbc5f6fd4035'; +const infuraProjectId = 'fake-infura-project-id'; + +const BLOCK_TRACKER_POLLING_INTERVAL = 20000; + +/** + * Builds the Infura network client configuration. + * @param network - The Infura network type. + * @returns The network client configuration. + */ +function buildInfuraNetworkClientConfiguration( + network: InfuraNetworkType, +): NetworkClientConfiguration { + return { + type: NetworkClientType.Infura, + network, + chainId: BUILT_IN_NETWORKS[network].chainId, + infuraProjectId, + ticker: BUILT_IN_NETWORKS[network].ticker, + }; +} -const networkClientConfiguration = { - type: NetworkClientType.Infura, - network: NetworkType.goerli, +const customGoerliNetworkClientConfiguration = { + type: NetworkClientType.Custom, chainId: BUILT_IN_NETWORKS[NetworkType.goerli].chainId, - infuraProjectId, ticker: BUILT_IN_NETWORKS[NetworkType.goerli].ticker, -} as const; - -const sepoliaNetworkClientConfiguration = { - type: NetworkClientType.Infura, - network: NetworkType.sepolia, - chainId: BUILT_IN_NETWORKS[NetworkType.sepolia].chainId, - infuraProjectId, - ticker: BUILT_IN_NETWORKS[NetworkType.sepolia].ticker, -} as const; - -const mainnetNetworkClientConfiguration = { - type: NetworkClientType.Infura, - network: NetworkType.mainnet, - chainId: BUILT_IN_NETWORKS[NetworkType.mainnet].chainId, - infuraProjectId, - ticker: BUILT_IN_NETWORKS[NetworkType.mainnet].ticker, + rpcUrl: 'https://mock.rpc.url', } as const; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -91,6 +95,7 @@ const newController = async (options: any = {}) => { getCurrentNetworkEIP1559Compatibility: networkController.getEIP1559Compatibility.bind(networkController), getNetworkClientRegistry: + opts.getNetworkClientRegistrySpy || networkController.getNetworkClientRegistry.bind(networkController), findNetworkClientIdByChainId: networkController.findNetworkClientIdByChainId.bind(networkController), @@ -129,7 +134,9 @@ describe('TransactionController Integration', () => { describe('constructor', () => { it('should create a new instance of TransactionController', async () => { mockNetwork({ - networkClientConfiguration: mainnetNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), mocks: [ // NetworkController // BlockTracker @@ -151,7 +158,9 @@ describe('TransactionController Integration', () => { it('should submit all approved transactions in state', async () => { mockNetwork({ - networkClientConfiguration: mainnetNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), mocks: [ // NetworkController // BlockTracker @@ -168,7 +177,9 @@ describe('TransactionController Integration', () => { }); mockNetwork({ - networkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.goerli, + ), mocks: [ // NetworkController // BlockTracker @@ -197,7 +208,9 @@ describe('TransactionController Integration', () => { }); mockNetwork({ - networkClientConfiguration: sepoliaNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.sepolia, + ), mocks: [ // NetworkController // BlockTracker @@ -319,7 +332,9 @@ describe('TransactionController Integration', () => { describe('when a transaction is added with a networkClientId that does not match the globally selected network', () => { it('should add a new unapproved transaction', async () => { mockNetwork({ - networkClientConfiguration: mainnetNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), mocks: [ // NetworkController // BlockTracker @@ -335,7 +350,9 @@ describe('TransactionController Integration', () => { ], }); mockNetwork({ - networkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.goerli, + ), mocks: [ // NetworkController // BlockTracker @@ -387,7 +404,9 @@ describe('TransactionController Integration', () => { }); it('should be able to get to submitted state', async () => { mockNetwork({ - networkClientConfiguration: mainnetNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), mocks: [ // NetworkController // BlockTracker @@ -403,7 +422,9 @@ describe('TransactionController Integration', () => { ], }); mockNetwork({ - networkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.goerli, + ), mocks: [ // NetworkController // BlockTracker @@ -525,7 +546,9 @@ describe('TransactionController Integration', () => { }); it('should be able to get to confirmed state', async () => { mockNetwork({ - networkClientConfiguration: mainnetNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), mocks: [ // NetworkController // BlockTracker @@ -541,7 +564,9 @@ describe('TransactionController Integration', () => { ], }); mockNetwork({ - networkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.goerli, + ), mocks: [ // NetworkController // BlockTracker @@ -681,7 +706,7 @@ describe('TransactionController Integration', () => { await result; // blocktracker polling is 20s - await advanceTime({ clock, duration: 20000 }); + await advanceTime({ clock, duration: BLOCK_TRACKER_POLLING_INTERVAL }); await advanceTime({ clock, duration: 1 }); await advanceTime({ clock, duration: 1 }); @@ -693,7 +718,9 @@ describe('TransactionController Integration', () => { }); it('should be able to send and confirm transactions on different chains', async () => { mockNetwork({ - networkClientConfiguration: mainnetNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), mocks: [ // NetworkController // BlockTracker @@ -709,7 +736,9 @@ describe('TransactionController Integration', () => { ], }); mockNetwork({ - networkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.goerli, + ), mocks: [ // NetworkController // BlockTracker @@ -834,7 +863,9 @@ describe('TransactionController Integration', () => { ], }); mockNetwork({ - networkClientConfiguration: sepoliaNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.sepolia, + ), mocks: [ // NetworkController // BlockTracker @@ -985,7 +1016,7 @@ describe('TransactionController Integration', () => { await Promise.all([firstTransaction.result, secondTransaction.result]); // blocktracker polling is 20s - await advanceTime({ clock, duration: 20000 }); + await advanceTime({ clock, duration: BLOCK_TRACKER_POLLING_INTERVAL }); await advanceTime({ clock, duration: 1 }); await advanceTime({ clock, duration: 1 }); @@ -1006,7 +1037,9 @@ describe('TransactionController Integration', () => { }); it('should be able to cancel a transaction', async () => { mockNetwork({ - networkClientConfiguration: mainnetNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), mocks: [ // NetworkController // BlockTracker @@ -1022,7 +1055,9 @@ describe('TransactionController Integration', () => { ], }); mockNetwork({ - networkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.goerli, + ), mocks: [ // NetworkController // BlockTracker @@ -1208,7 +1243,9 @@ describe('TransactionController Integration', () => { }); it('should be able to confirm a cancelled transaction', async () => { mockNetwork({ - networkClientConfiguration: mainnetNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), mocks: [ // NetworkController // BlockTracker @@ -1224,7 +1261,9 @@ describe('TransactionController Integration', () => { ], }); mockNetwork({ - networkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.goerli, + ), mocks: [ // NetworkController // BlockTracker @@ -1419,10 +1458,10 @@ describe('TransactionController Integration', () => { await transactionController.stopTransaction(transactionMeta.id); // blocktracker polling is 20s - await advanceTime({ clock, duration: 20000 }); + await advanceTime({ clock, duration: BLOCK_TRACKER_POLLING_INTERVAL }); await advanceTime({ clock, duration: 1 }); await advanceTime({ clock, duration: 1 }); - await advanceTime({ clock, duration: 20000 }); + await advanceTime({ clock, duration: BLOCK_TRACKER_POLLING_INTERVAL }); await advanceTime({ clock, duration: 1 }); await advanceTime({ clock, duration: 1 }); @@ -1434,7 +1473,9 @@ describe('TransactionController Integration', () => { }); it('should be able to get to speedup state', async () => { mockNetwork({ - networkClientConfiguration: mainnetNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), mocks: [ // NetworkController // BlockTracker @@ -1450,7 +1491,9 @@ describe('TransactionController Integration', () => { ], }); mockNetwork({ - networkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.goerli, + ), mocks: [ // NetworkController // BlockTracker @@ -1646,10 +1689,10 @@ describe('TransactionController Integration', () => { await transactionController.speedUpTransaction(transactionMeta.id); // blocktracker polling is 20s - await advanceTime({ clock, duration: 20000 }); + await advanceTime({ clock, duration: BLOCK_TRACKER_POLLING_INTERVAL }); await advanceTime({ clock, duration: 1 }); await advanceTime({ clock, duration: 1 }); - await advanceTime({ clock, duration: 20000 }); + await advanceTime({ clock, duration: BLOCK_TRACKER_POLLING_INTERVAL }); await advanceTime({ clock, duration: 1 }); await advanceTime({ clock, duration: 1 }); @@ -1671,7 +1714,9 @@ describe('TransactionController Integration', () => { describe('when transactions are added concurrently with different networkClientIds but on the same chainId', () => { it('should add each transaction with consecutive nonces', async () => { mockNetwork({ - networkClientConfiguration: mainnetNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), mocks: [ // NetworkController // BlockTracker @@ -1687,7 +1732,9 @@ describe('TransactionController Integration', () => { ], }); mockNetwork({ - networkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.goerli, + ), mocks: [ // NetworkController // BlockTracker @@ -1813,13 +1860,18 @@ describe('TransactionController Integration', () => { }); mockNetwork({ - networkClientConfiguration: { - ...networkClientConfiguration, - type: NetworkClientType.Custom, - rpcUrl: 'https://mock.rpc.url', - }, + networkClientConfiguration: customGoerliNetworkClientConfiguration, mocks: [ // NetworkController + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, // BlockTracker { request: { @@ -1951,8 +2003,8 @@ describe('TransactionController Integration', () => { await networkController.upsertNetworkConfiguration( { rpcUrl: 'https://mock.rpc.url', - chainId: networkClientConfiguration.chainId, - ticker: networkClientConfiguration.ticker, + chainId: BUILT_IN_NETWORKS[NetworkType.goerli].chainId, + ticker: BUILT_IN_NETWORKS[NetworkType.goerli].ticker, }, { referrer: 'https://mock.referrer', @@ -1997,7 +2049,9 @@ describe('TransactionController Integration', () => { describe('when transactions are added concurrently with the same networkClientId', () => { it('should add each transaction with consecutive nonces', async () => { mockNetwork({ - networkClientConfiguration: mainnetNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), mocks: [ // NetworkController // BlockTracker @@ -2013,7 +2067,9 @@ describe('TransactionController Integration', () => { ], }); mockNetwork({ - networkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.goerli, + ), mocks: [ // NetworkController // BlockTracker @@ -2224,7 +2280,9 @@ describe('TransactionController Integration', () => { describe('when changing rpcUrl of networkClient', () => { it('should start tracking when a new network is added', async () => { mockNetwork({ - networkClientConfiguration: mainnetNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), mocks: [ // NetworkController // BlockTracker @@ -2240,13 +2298,18 @@ describe('TransactionController Integration', () => { ], }); mockNetwork({ - networkClientConfiguration: { - ...networkClientConfiguration, - type: NetworkClientType.Custom, - rpcUrl: 'https://mock.rpc.url', - }, + networkClientConfiguration: customGoerliNetworkClientConfiguration, mocks: [ // NetworkController + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, // BlockTracker { request: { @@ -2298,10 +2361,7 @@ describe('TransactionController Integration', () => { const otherNetworkClientIdOnGoerli = await networkController.upsertNetworkConfiguration( - { - ...networkClientConfiguration, - rpcUrl: 'https://mock.rpc.url', - }, + customGoerliNetworkClientConfiguration, { setActive: false, referrer: 'https://mock.referrer', @@ -2328,7 +2388,9 @@ describe('TransactionController Integration', () => { }); it('should stop tracking when a network is removed', async () => { mockNetwork({ - networkClientConfiguration: mainnetNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), mocks: [ // NetworkController // BlockTracker @@ -2358,10 +2420,7 @@ describe('TransactionController Integration', () => { const configurationId = await networkController.upsertNetworkConfiguration( - { - ...networkClientConfiguration, - rpcUrl: 'https://mock.rpc.url', - }, + customGoerliNetworkClientConfiguration, { setActive: false, referrer: 'https://mock.referrer', @@ -2389,9 +2448,11 @@ describe('TransactionController Integration', () => { }); describe('feature flag', () => { - it('should not track multichain transactions on network stateChange when feature flag is disabled', async () => { + it('should not allow transaction to be added with a networkClientId when feature flag is disabled', async () => { mockNetwork({ - networkClientConfiguration: mainnetNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), mocks: [ // NetworkController // BlockTracker @@ -2454,10 +2515,7 @@ describe('TransactionController Integration', () => { const configurationId = await networkController.upsertNetworkConfiguration( - { - ...networkClientConfiguration, - rpcUrl: 'https://mock.rpc.url', - }, + customGoerliNetworkClientConfiguration, { setActive: false, referrer: 'https://mock.referrer', @@ -2488,13 +2546,137 @@ describe('TransactionController Integration', () => { ).toBeDefined(); transactionController.destroy(); }); + it('should not call getNetworkClientRegistry on networkController:stateChange when feature flag is disabled', async () => { + mockNetwork({ + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), + mocks: [ + // NetworkController + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + ], + }); + const getNetworkClientRegistrySpy = jest.fn().mockImplementation(() => { + return { + [NetworkType.goerli]: { + configuration: customGoerliNetworkClientConfiguration, + }, + }; + }); + + const { networkController, transactionController } = await newController({ + enableMultichain: false, + getNetworkClientRegistrySpy, + }); + + await networkController.upsertNetworkConfiguration( + customGoerliNetworkClientConfiguration, + { + setActive: false, + referrer: 'https://mock.referrer', + source: 'dapp', + }, + ); + + expect(getNetworkClientRegistrySpy).not.toHaveBeenCalled(); + transactionController.destroy(); + }); + it('should call getNetworkClientRegistry on networkController:stateChange when feature flag is enabled', async () => { + mockNetwork({ + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), + mocks: [ + // NetworkController + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + ], + }); + const getNetworkClientRegistrySpy = jest.fn().mockImplementation(() => { + return { + [NetworkType.goerli]: { + configuration: BUILT_IN_NETWORKS[NetworkType.goerli], + }, + }; + }); + + const { networkController, transactionController } = await newController({ + enableMultichain: true, + getNetworkClientRegistrySpy, + }); + + await networkController.upsertNetworkConfiguration( + customGoerliNetworkClientConfiguration, + { + setActive: false, + referrer: 'https://mock.referrer', + source: 'dapp', + }, + ); + + expect(getNetworkClientRegistrySpy).toHaveBeenCalled(); + transactionController.destroy(); + }); + it('should call getNetworkClientRegistry on construction when feature flag is enabled', async () => { + mockNetwork({ + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), + mocks: [ + // NetworkController + // BlockTracker + { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result: '0x1', + }, + }, + ], + }); + const getNetworkClientRegistrySpy = jest.fn().mockImplementation(() => { + return { + [NetworkType.goerli]: { + configuration: BUILT_IN_NETWORKS[NetworkType.goerli], + }, + }; + }); + + await newController({ + enableMultichain: true, + getNetworkClientRegistrySpy, + }); + + expect(getNetworkClientRegistrySpy).toHaveBeenCalled(); + }); }); describe('startIncomingTransactionPolling', () => { // TODO(JL): IncomingTransactionHelper doesn't populate networkClientId on the generated tx object. Should it?.. it('should add incoming transactions to state with the correct chainId for the given networkClientId on the next block', async () => { mockNetwork({ - networkClientConfiguration: mainnetNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), mocks: [ // NetworkController // BlockTracker @@ -2532,7 +2714,7 @@ describe('TransactionController Integration', () => { const networkClients = networkController.getNetworkClientRegistry(); // Skip the globally selected provider because we can't use nock to mock it twice const networkClientIds = Object.keys(networkClients).filter( - (v) => v !== networkClientConfiguration.network, + (v) => v !== NetworkType.goerli, ); await Promise.all( networkClientIds.map(async (networkClientId) => { @@ -2591,7 +2773,7 @@ describe('TransactionController Integration', () => { }); }), ); - await advanceTime({ clock, duration: 20000 }); + await advanceTime({ clock, duration: BLOCK_TRACKER_POLLING_INTERVAL }); expect(transactionController.state.transactions).toHaveLength( 2 * networkClientIds.length, @@ -2607,6 +2789,11 @@ describe('TransactionController Integration', () => { transactionController.destroy(); }); + // Unclear if we need this test + it.todo( + 'should start the global incoming transaction helper when no networkClientIds provided', + ); + describe('when called with multiple networkClients which share the same chainId', () => { it('should only call the etherscan API max every 5 seconds, alternating between the token and txlist endpoints', async () => { const fetchEtherscanNativeTxFetchSpy = jest.spyOn( @@ -2621,7 +2808,9 @@ describe('TransactionController Integration', () => { // mocking infura mainnet mockNetwork({ - networkClientConfiguration: mainnetNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), mocks: [ // NetworkController // BlockTracker @@ -2649,7 +2838,9 @@ describe('TransactionController Integration', () => { // mocking infura goerli mockNetwork({ - networkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.goerli, + ), mocks: [ // NetworkController // BlockTracker @@ -2678,8 +2869,9 @@ describe('TransactionController Integration', () => { // mock the other goerli network client node requests mockNetwork({ networkClientConfiguration: { - ...networkClientConfiguration, type: NetworkClientType.Custom, + chainId: BUILT_IN_NETWORKS[NetworkType.goerli].chainId, + ticker: BUILT_IN_NETWORKS[NetworkType.goerli].ticker, rpcUrl: 'https://mock.rpc.url', }, mocks: [ @@ -2733,11 +2925,7 @@ describe('TransactionController Integration', () => { const otherGoerliClientNetworkClientId = await networkController.upsertNetworkConfiguration( - { - rpcUrl: 'https://mock.rpc.url', - chainId: networkClientConfiguration.chainId, - ticker: networkClientConfiguration.ticker, - }, + customGoerliNetworkClientConfiguration, { referrer: 'https://mock.referrer', source: 'dapp', @@ -2747,7 +2935,7 @@ describe('TransactionController Integration', () => { // Etherscan API Mocks // Non-token transactions - nock(getEtherscanApiHost(networkClientConfiguration.chainId)) + nock(getEtherscanApiHost(BUILT_IN_NETWORKS[NetworkType.goerli].chainId)) .get( `/api?module=account&address=${ETHERSCAN_TRANSACTION_BASE_MOCK.to}&offset=40&sort=desc&action=txlist&tag=latest&page=1`, ) @@ -2766,7 +2954,7 @@ describe('TransactionController Integration', () => { .persist(); // token transactions - nock(getEtherscanApiHost(networkClientConfiguration.chainId)) + nock(getEtherscanApiHost(BUILT_IN_NETWORKS[NetworkType.goerli].chainId)) .get( `/api?module=account&address=${ETHERSCAN_TRANSACTION_BASE_MOCK.to}&offset=40&sort=desc&action=tokentx&tag=latest&page=1`, ) @@ -2785,7 +2973,7 @@ describe('TransactionController Integration', () => { // start polling with two clients which share the same chainId transactionController.startIncomingTransactionPolling([ - networkClientConfiguration.network, // 'goerli' + NetworkType.goerli, otherGoerliClientNetworkClientId, ]); await advanceTime({ clock, duration: 1 }); @@ -2822,9 +3010,15 @@ describe('TransactionController Integration', () => { }); describe('stopIncomingTransactionPolling', () => { + // Unclear if we need this test + it.todo( + 'should stop the global incoming transaction helper when no networkClientIds provided', + ); it('should not poll for new incoming transactions for the given networkClientId', async () => { mockNetwork({ - networkClientConfiguration: mainnetNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), mocks: [ // NetworkController // BlockTracker @@ -2859,7 +3053,7 @@ describe('TransactionController Integration', () => { const networkClients = networkController.getNetworkClientRegistry(); // Skip the globally selected provider because we can't use nock to mock it twice const networkClientIds = Object.keys(networkClients).filter( - (v) => v !== networkClientConfiguration.network, + (v) => v !== NetworkType.goerli, ); await Promise.all( networkClientIds.map(async (networkClientId) => { @@ -2904,7 +3098,7 @@ describe('TransactionController Integration', () => { ]); }), ); - await advanceTime({ clock, duration: 20000 }); + await advanceTime({ clock, duration: BLOCK_TRACKER_POLLING_INTERVAL }); expect(transactionController.state.transactions).toStrictEqual([]); expect(transactionController.state.lastFetchedBlockNumbers).toStrictEqual( @@ -2917,7 +3111,9 @@ describe('TransactionController Integration', () => { describe('stopAllIncomingTransactionPolling', () => { it('should not poll for incoming transactions on any network client', async () => { mockNetwork({ - networkClientConfiguration: mainnetNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), mocks: [ // NetworkController // BlockTracker @@ -2952,7 +3148,7 @@ describe('TransactionController Integration', () => { const networkClients = networkController.getNetworkClientRegistry(); // Skip the globally selected provider because we can't use nock to mock it twice const networkClientIds = Object.keys(networkClients).filter( - (v) => v !== networkClientConfiguration.network, + (v) => v !== NetworkType.goerli, ); await Promise.all( networkClientIds.map(async (networkClientId) => { @@ -2995,7 +3191,7 @@ describe('TransactionController Integration', () => { ); transactionController.stopAllIncomingTransactionPolling(); - await advanceTime({ clock, duration: 20000 }); + await advanceTime({ clock, duration: BLOCK_TRACKER_POLLING_INTERVAL }); expect(transactionController.state.transactions).toStrictEqual([]); expect(transactionController.state.lastFetchedBlockNumbers).toStrictEqual( @@ -3008,7 +3204,9 @@ describe('TransactionController Integration', () => { describe('updateIncomingTransactions', () => { it('should add incoming transactions to state with the correct chainId for the given networkClientId without waiting for the next block', async () => { mockNetwork({ - networkClientConfiguration: mainnetNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), mocks: [ // NetworkController // BlockTracker @@ -3046,7 +3244,7 @@ describe('TransactionController Integration', () => { const networkClients = networkController.getNetworkClientRegistry(); // Skip the globally selected provider because we can't use nock to mock it twice const networkClientIds = Object.keys(networkClients).filter( - (v) => v !== networkClientConfiguration.network, + (v) => v !== NetworkType.goerli, ); await Promise.all( networkClientIds.map(async (networkClientId) => { @@ -3115,7 +3313,9 @@ describe('TransactionController Integration', () => { describe('getNonceLock', () => { it('should get the nonce lock from the nonceTracker for the given networkClientId', async () => { mockNetwork({ - networkClientConfiguration: mainnetNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), mocks: [ // NetworkController // BlockTracker @@ -3148,7 +3348,7 @@ describe('TransactionController Integration', () => { const networkClients = networkController.getNetworkClientRegistry(); // Skip the globally selected provider because we can't use nock to mock it twice const networkClientIds = Object.keys(networkClients).filter( - (v) => v !== networkClientConfiguration.network, + (v) => v !== NetworkType.goerli, ); await Promise.all( networkClientIds.map(async (networkClientId) => { @@ -3195,7 +3395,9 @@ describe('TransactionController Integration', () => { it('should block attempts to get the nonce lock for the same address from the nonceTracker for the networkClientId until the previous lock is released', async () => { mockNetwork({ - networkClientConfiguration: mainnetNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), mocks: [ // NetworkController // BlockTracker @@ -3228,7 +3430,7 @@ describe('TransactionController Integration', () => { const networkClients = networkController.getNetworkClientRegistry(); // Skip the globally selected provider because we can't use nock to mock it twice const networkClientIds = Object.keys(networkClients).filter( - (v) => v !== networkClientConfiguration.network, + (v) => v !== NetworkType.goerli, ); await Promise.all( networkClientIds.map(async (networkClientId) => { @@ -3300,7 +3502,9 @@ describe('TransactionController Integration', () => { it('should block attempts to get the nonce lock for the same address from the nonceTracker for the different networkClientIds on the same chainId until the previous lock is released', async () => { mockNetwork({ - networkClientConfiguration: mainnetNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), mocks: [ // NetworkController // BlockTracker @@ -3330,7 +3534,9 @@ describe('TransactionController Integration', () => { {}, ); mockNetwork({ - networkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.goerli, + ), mocks: [ // BlockTracker { @@ -3356,11 +3562,7 @@ describe('TransactionController Integration', () => { }); mockNetwork({ - networkClientConfiguration: { - ...networkClientConfiguration, - type: NetworkClientType.Custom, - rpcUrl: 'https://mock.rpc.url', - }, + networkClientConfiguration: customGoerliNetworkClientConfiguration, mocks: [ // BlockTracker { @@ -3387,11 +3589,7 @@ describe('TransactionController Integration', () => { const otherNetworkClientIdOnGoerli = await networkController.upsertNetworkConfiguration( - { - rpcUrl: 'https://mock.rpc.url', - chainId: networkClientConfiguration.chainId, - ticker: networkClientConfiguration.ticker, - }, + customGoerliNetworkClientConfiguration, { referrer: 'https://mock.referrer', source: 'dapp', @@ -3438,7 +3636,9 @@ describe('TransactionController Integration', () => { it('should not block attempts to get the nonce lock for the same addresses from the nonceTracker for different networkClientIds', async () => { mockNetwork({ - networkClientConfiguration: mainnetNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), mocks: [ // NetworkController // BlockTracker @@ -3467,7 +3667,9 @@ describe('TransactionController Integration', () => { const { transactionController } = await newController({}); mockNetwork({ - networkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.goerli, + ), mocks: [ // BlockTracker { @@ -3493,7 +3695,9 @@ describe('TransactionController Integration', () => { }); mockNetwork({ - networkClientConfiguration: sepoliaNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.sepolia, + ), mocks: [ // BlockTracker { @@ -3543,7 +3747,9 @@ describe('TransactionController Integration', () => { it('should not block attempts to get the nonce lock for different addresses from the nonceTracker for the networkClientId', async () => { mockNetwork({ - networkClientConfiguration: mainnetNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), mocks: [ // NetworkController // BlockTracker @@ -3576,7 +3782,7 @@ describe('TransactionController Integration', () => { const networkClients = networkController.getNetworkClientRegistry(); // Skip the globally selected provider because we can't use nock to mock it twice const networkClientIds = Object.keys(networkClients).filter( - (v) => v !== networkClientConfiguration.network, + (v) => v !== NetworkType.goerli, ); await Promise.all( networkClientIds.map(async (networkClientId) => { @@ -3643,7 +3849,9 @@ describe('TransactionController Integration', () => { it('should get the nonce lock from the globally selected nonceTracker if no networkClientId is provided', async () => { mockNetwork({ - networkClientConfiguration: mainnetNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), mocks: [ // NetworkController // BlockTracker @@ -3682,7 +3890,9 @@ describe('TransactionController Integration', () => { it('should block attempts to get the nonce lock from the globally selected NonceTracker for the same address until the previous lock is released', async () => { mockNetwork({ - networkClientConfiguration: mainnetNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), mocks: [ // NetworkController // BlockTracker @@ -3744,7 +3954,9 @@ describe('TransactionController Integration', () => { it('should not block attempts to get the nonce lock from the globally selected nonceTracker for different addresses', async () => { mockNetwork({ - networkClientConfiguration: mainnetNetworkClientConfiguration, + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), mocks: [ // NetworkController // BlockTracker From ff0896148afb5ca06f0885eb139510e1f1dfb4b8 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 2 Feb 2024 10:54:03 -0600 Subject: [PATCH 083/100] remove updateIncomingTransactions test from unit tests, add a new related todo test to the integration tests --- .../src/TransactionController.test.ts | 9 --------- .../src/TransactionControllerIntegration.test.ts | 3 +++ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index f0a280bccc..ee0dce9e80 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -4779,15 +4779,6 @@ describe('TransactionController', () => { }); }); - describe('updateIncomingTransactions', () => { - it('should update the global incoming transactions when no networkClientIds provided', async () => { - const controller = newController(); - await controller.updateIncomingTransactions([]); - - expect(incomingTransactionHelperMocks[0].update).toHaveBeenCalledTimes(1); - }); - }); - describe('abortTransactionSigning', () => { it('throws if transaction does not exist', () => { const controller = newController(); diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index 9e8a30e618..3805fade66 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -3308,6 +3308,9 @@ describe('TransactionController Integration', () => { ); transactionController.destroy(); }); + it.todo( + 'should update the incoming transactions for the gloablly selected network when no networkClientIds provided', + ); }); describe('getNonceLock', () => { From 00be49ab2abd2df465b5ac6c4515f5701e8321d1 Mon Sep 17 00:00:00 2001 From: jiexi Date: Fri, 2 Feb 2024 13:51:30 -0800 Subject: [PATCH 084/100] Jl/transaction multichain messenger actions (#3887) ## Explanation * Moves `findNetworkClientIdByChainId` and `getNetworkClientById` callback params into messenger calls ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- .../src/TransactionController.test.ts | 165 ++++++++++-------- .../src/TransactionController.ts | 55 +++--- 2 files changed, 128 insertions(+), 92 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index aa4ec07e9e..24b4a304fd 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -15,6 +15,8 @@ import { import HttpProvider from '@metamask/ethjs-provider-http'; import type { BlockTracker, + NetworkControllerFindNetworkClientIdByChainIdAction, + NetworkControllerGetNetworkClientByIdAction, NetworkState, Provider, } from '@metamask/network-controller'; @@ -224,23 +226,36 @@ function buildMockResultCallbacks(): AcceptResultCallbacks { }; } +/** + * @type AddRequestOptions + * @property approved - Whether transactions should immediately be approved or rejected. + * @property delay - Whether to delay approval or rejection until the returned functions are called. + * @property resultCallbacks - The result callbacks to return when a request is approved. + */ +type AddRequestOptions = { + approved?: boolean; + delay?: boolean; + resultCallbacks?: AcceptResultCallbacks; +}; + /** * Create a mock controller messenger. * * @param opts - Options to customize the mock messenger. - * @param opts.approved - Whether transactions should immediately be approved or rejected. - * @param opts.delay - Whether to delay approval or rejection until the returned functions are called. - * @param opts.resultCallbacks - The result callbacks to return when a request is approved. + * @param opts.addRequest - Options for ApprovalController.addRequest mock. + * @param opts.getNetworkClientById - The function to use as the NetworkController:getNetworkClientById mock. + * @param opts.findNetworkClientIdByChainId - The function to use as the NetworkController:findNetworkClientIdByChainId mock. * @returns The mock controller messenger. */ +// function buildMockMessenger({ - approved, - delay, - resultCallbacks, + addRequest: { approved, delay, resultCallbacks }, + getNetworkClientById, + findNetworkClientIdByChainId, }: { - approved?: boolean; - delay?: boolean; - resultCallbacks?: AcceptResultCallbacks; + addRequest: AddRequestOptions; + getNetworkClientById: NetworkControllerGetNetworkClientByIdAction['handler']; + findNetworkClientIdByChainId: NetworkControllerFindNetworkClientIdByChainIdAction['handler']; }): { messenger: TransactionControllerMessenger; approve: () => void; @@ -273,19 +288,33 @@ function buildMockMessenger({ const messenger = { subscribe: mockSubscribe, - call: jest.fn().mockImplementation(() => { - if (approved) { - return Promise.resolve({ resultCallbacks }); - } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + call: jest.fn().mockImplementation((actionType: string, ...args: any[]) => { + switch (actionType) { + case 'ApprovalController:addRequest': + if (approved) { + return Promise.resolve({ resultCallbacks }); + } - if (delay) { - return promise; - } + if (delay) { + return promise; + } - // eslint-disable-next-line prefer-promise-reject-errors - return Promise.reject({ - code: errorCodes.provider.userRejectedRequest, - }); + // eslint-disable-next-line prefer-promise-reject-errors + return Promise.reject({ + code: errorCodes.provider.userRejectedRequest, + }); + case 'NetworkController:getNetworkClientById': + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (getNetworkClientById as any)(...args); + case 'NetworkController:findNetworkClientIdByChainId': + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (findNetworkClientIdByChainId as any)(...args); + default: + throw new Error( + `A handler for ${actionType} has not been registered`, + ); + } }), } as unknown as TransactionControllerMessenger; @@ -499,8 +528,6 @@ describe('TransactionController', () => { let resultCallbacksMock: AcceptResultCallbacks; let messengerMock: TransactionControllerMessenger; - let rejectMessengerMock: TransactionControllerMessenger; - let delayMessengerMock: TransactionControllerMessenger; // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any let approveTransaction: (value?: any) => void; @@ -549,14 +576,24 @@ describe('TransactionController', () => { state?: Partial; } = {}): TransactionController { const finalNetwork = network ?? MOCK_NETWORK; - let messenger = delayMessengerMock; + resultCallbacksMock = buildMockResultCallbacks(); + let addRequestMockOptions: AddRequestOptions; if (approve) { - messenger = messengerMock; - } - - if (reject) { - messenger = rejectMessengerMock; + addRequestMockOptions = { + approved: true, + resultCallbacks: resultCallbacksMock, + }; + } else if (reject) { + addRequestMockOptions = { + approved: false, + resultCallbacks: resultCallbacksMock, + }; + } else { + addRequestMockOptions = { + delay: true, + resultCallbacks: resultCallbacksMock, + }; } const mockGetNetworkClientById = jest @@ -600,7 +637,13 @@ describe('TransactionController', () => { } }); - const mockFindNetworkClientIdByChainId = jest.fn(); + ({ messenger: messengerMock, approve: approveTransaction } = + buildMockMessenger({ + addRequest: addRequestMockOptions, + getNetworkClientById: mockGetNetworkClientById, + findNetworkClientIdByChainId: jest.fn(), + })); + return new TransactionController( { blockTracker: finalNetwork.blockTracker, @@ -632,11 +675,9 @@ describe('TransactionController', () => { }, }, }), - messenger, + messenger: messengerMock, onNetworkStateChange: finalNetwork.subscribe, provider: finalNetwork.provider, - getNetworkClientById: mockGetNetworkClientById, - findNetworkClientIdByChainId: mockFindNetworkClientIdByChainId, ...options, }, { @@ -668,24 +709,6 @@ describe('TransactionController', () => { mockFlags[key] = null; } - resultCallbacksMock = buildMockResultCallbacks(); - - messengerMock = buildMockMessenger({ - approved: true, - resultCallbacks: resultCallbacksMock, - }).messenger; - - rejectMessengerMock = buildMockMessenger({ - approved: false, - resultCallbacks: resultCallbacksMock, - }).messenger; - - ({ messenger: delayMessengerMock, approve: approveTransaction } = - buildMockMessenger({ - delay: true, - resultCallbacks: resultCallbacksMock, - })); - getNonceLockSpy = jest.fn().mockResolvedValue({ nextNonce: NONCE_MOCK, releaseLock: () => Promise.resolve(), @@ -967,8 +990,8 @@ describe('TransactionController', () => { const secondTransactionCount = controller.state.transactions.length; expect(firstTransactionCount).toStrictEqual(secondTransactionCount); - expect(delayMessengerMock.call).toHaveBeenCalledTimes(1); - expect(delayMessengerMock.call).toHaveBeenCalledWith( + expect(messengerMock.call).toHaveBeenCalledTimes(1); + expect(messengerMock.call).toHaveBeenCalledWith( 'ApprovalController:addRequest', { id: expect.any(String), @@ -1079,7 +1102,7 @@ describe('TransactionController', () => { const { transactions } = controller.state; expect(transactions).toHaveLength(expectedTransactionCount); - expect(delayMessengerMock.call).toHaveBeenCalledTimes( + expect(messengerMock.call).toHaveBeenCalledTimes( expectedRequestApprovalCalledTimes, ); }, @@ -1451,8 +1474,8 @@ describe('TransactionController', () => { to: ACCOUNT_MOCK, }); - expect(delayMessengerMock.call).toHaveBeenCalledTimes(1); - expect(delayMessengerMock.call).toHaveBeenCalledWith( + expect(messengerMock.call).toHaveBeenCalledTimes(1); + expect(messengerMock.call).toHaveBeenCalledWith( 'ApprovalController:addRequest', { id: expect.any(String), @@ -1478,7 +1501,7 @@ describe('TransactionController', () => { }, ); - expect(delayMessengerMock.call).toHaveBeenCalledTimes(0); + expect(messengerMock.call).toHaveBeenCalledTimes(0); }); it('calls security provider with transaction meta and sets response in to securityProviderResponse', async () => { @@ -1702,7 +1725,7 @@ describe('TransactionController', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any - const callMock = delayMessengerMock.call as jest.MockedFunction; + const callMock = messengerMock.call as jest.MockedFunction; callMock.mockImplementationOnce(() => { throw new Error('Unknown problem'); }); @@ -1723,7 +1746,7 @@ describe('TransactionController', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any - const callMock = delayMessengerMock.call as jest.MockedFunction; + const callMock = messengerMock.call as jest.MockedFunction; callMock.mockImplementationOnce(() => { throw new Error('TestError'); }); @@ -1744,7 +1767,7 @@ describe('TransactionController', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any - const callMock = delayMessengerMock.call as jest.MockedFunction; + const callMock = messengerMock.call as jest.MockedFunction; callMock.mockImplementationOnce(() => { controller.state.transactions = []; throw new Error('Unknown problem'); @@ -4320,8 +4343,8 @@ describe('TransactionController', () => { controller.initApprovals(); await flushPromises(); - expect(delayMessengerMock.call).toHaveBeenCalledTimes(2); - expect(delayMessengerMock.call).toHaveBeenCalledWith( + expect(messengerMock.call).toHaveBeenCalledTimes(2); + expect(messengerMock.call).toHaveBeenCalledWith( 'ApprovalController:addRequest', { expectsResult: true, @@ -4332,7 +4355,7 @@ describe('TransactionController', () => { }, false, ); - expect(delayMessengerMock.call).toHaveBeenCalledWith( + expect(messengerMock.call).toHaveBeenCalledWith( 'ApprovalController:addRequest', { expectsResult: true, @@ -4428,12 +4451,18 @@ describe('TransactionController', () => { lastFetchedBlockNumbers: {}, }; + const controller = newController({ + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + state: mockedControllerState as any, + }); + const mockedErrorMessage = 'mocked error'; // Expect both calls to throw error, one with code property to check if it is handled // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any - (delayMessengerMock.call as jest.MockedFunction) + (messengerMock.call as jest.MockedFunction) .mockImplementationOnce(() => { // eslint-disable-next-line @typescript-eslint/no-throw-literal throw { message: mockedErrorMessage }; @@ -4447,12 +4476,6 @@ describe('TransactionController', () => { }); const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); - const controller = newController({ - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - state: mockedControllerState as any, - }); - controller.initApprovals(); await flushPromises(); @@ -4462,14 +4485,14 @@ describe('TransactionController', () => { 'Error during persisted transaction approval', new Error(mockedErrorMessage), ); - expect(delayMessengerMock.call).toHaveBeenCalledTimes(2); + expect(messengerMock.call).toHaveBeenCalledTimes(2); }); it('does not create any approval when there is no unapproved transaction', async () => { const controller = newController(); controller.initApprovals(); await flushPromises(); - expect(delayMessengerMock.call).not.toHaveBeenCalled(); + expect(messengerMock.call).not.toHaveBeenCalled(); }); }); diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 27579227ea..eea59e67ea 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -29,6 +29,8 @@ import type { NetworkState, Provider, NetworkClient, + NetworkControllerFindNetworkClientIdByChainIdAction, + NetworkControllerGetNetworkClientByIdAction, } from '@metamask/network-controller'; import { NetworkClientType } from '@metamask/network-controller'; import { errorCodes, rpcErrors, providerErrors } from '@metamask/rpc-errors'; @@ -231,7 +233,10 @@ const controllerName = 'TransactionController'; /** * The external actions available to the {@link TransactionController}. */ -type AllowedActions = AddApprovalRequest; +type AllowedActions = + | AddApprovalRequest + | NetworkControllerFindNetworkClientIdByChainIdAction + | NetworkControllerGetNetworkClientByIdAction; type AllowedEvents = NetworkControllerStateChangeEvent; @@ -352,10 +357,6 @@ export class TransactionController extends BaseControllerV1< transactionMeta: TransactionMeta, ) => (TransactionMeta | undefined)[]; - private readonly findNetworkClientIdByChainId: NetworkController['findNetworkClientIdByChainId']; - - private readonly getNetworkClientById: NetworkController['getNetworkClientById']; - private readonly getNetworkClientRegistry: NetworkController['getNetworkClientRegistry']; private failTransaction( @@ -446,8 +447,6 @@ export class TransactionController extends BaseControllerV1< * @param options.provider - The provider used to create the underlying EthQuery instance. * @param options.securityProviderRequest - A function for verifying a transaction, whether it is malicious or not. * @param options.speedUpMultiplier - Multiplier used to determine a transaction's increased gas fee during speed up. - * @param options.findNetworkClientIdByChainId - Finds a networkClientId with the given chainId from the NetworkController. - * @param options.getNetworkClientById - Gets the network client with the given id from the NetworkController. * @param options.hooks - The controller hooks. * @param options.hooks.afterSign - Additional logic to execute after signing a transaction. Return false to not change the status to signed. * @param options.hooks.beforeApproveOnInit - Additional logic to execute before starting an approval flow for a transaction during initialization. Return false to skip the transaction. @@ -480,8 +479,6 @@ export class TransactionController extends BaseControllerV1< provider, securityProviderRequest, speedUpMultiplier, - findNetworkClientIdByChainId, - getNetworkClientById, getNetworkClientRegistry, enableMultichain = false, hooks = {}, @@ -509,8 +506,6 @@ export class TransactionController extends BaseControllerV1< provider: Provider; securityProviderRequest?: SecurityProviderRequest; speedUpMultiplier?: number; - findNetworkClientIdByChainId: NetworkController['findNetworkClientIdByChainId']; - getNetworkClientById: NetworkController['getNetworkClientById']; getNetworkClientRegistry: NetworkController['getNetworkClientRegistry']; enableMultichain: boolean; hooks: { @@ -547,8 +542,6 @@ export class TransactionController extends BaseControllerV1< }; this.initialize(); this.enableMultichain = enableMultichain; - this.findNetworkClientIdByChainId = findNetworkClientIdByChainId; - this.getNetworkClientById = getNetworkClientById; this.getNetworkClientRegistry = getNetworkClientRegistry; this.provider = provider; this.messagingSystem = messenger; @@ -1666,7 +1659,10 @@ export class TransactionController extends BaseControllerV1< let releaseLockForChainIdKey: (() => void) | undefined; let { nonceTracker } = this; if (networkClientId && this.enableMultichain) { - const networkClient = this.getNetworkClientById(networkClientId); + const networkClient = this.messagingSystem.call( + `NetworkController:getNetworkClientById`, + networkClientId, + ); releaseLockForChainIdKey = await this.#acquireNonceLockForChainIdKey({ chainId: networkClient.configuration.chainId, key: address, @@ -2119,8 +2115,10 @@ export class TransactionController extends BaseControllerV1< const { networkClientId, chainId } = transactionMeta; const isCustomNetwork = networkClientId - ? this.getNetworkClientById(networkClientId).configuration.type === - NetworkClientType.Custom + ? this.messagingSystem.call( + `NetworkController:getNetworkClientById`, + networkClientId, + ).configuration.type === NetworkClientType.Custom : this.getNetworkState().providerConfig.type === NetworkType.rpc; await updateGas({ @@ -2574,7 +2572,10 @@ export class TransactionController extends BaseControllerV1< private getChainId(networkClientId?: NetworkClientId): Hex { if (networkClientId) { - return this.getNetworkClientById(networkClientId).configuration.chainId; + return this.messagingSystem.call( + `NetworkController:getNetworkClientById`, + networkClientId, + ).configuration.chainId; } const { providerConfig } = this.getNetworkState(); return providerConfig.chainId; @@ -3050,7 +3051,10 @@ export class TransactionController extends BaseControllerV1< if (networkClientId) { try { - networkClient = this.getNetworkClientById(networkClientId); + networkClient = this.messagingSystem.call( + `NetworkController:getNetworkClientById`, + networkClientId, + ); } catch (err) { log('failed to get network client by networkClientId'); } @@ -3058,8 +3062,14 @@ export class TransactionController extends BaseControllerV1< if (!networkClient && chainId) { try { - networkClientId = this.findNetworkClientIdByChainId(chainId); - networkClient = this.getNetworkClientById(networkClientId); + networkClientId = this.messagingSystem.call( + `NetworkController:findNetworkClientIdByChainId`, + chainId, + ); + networkClient = this.messagingSystem.call( + `NetworkController:getNetworkClientById`, + networkClientId, + ); } catch (err) { log('failed to get network client by chainId'); } @@ -3171,7 +3181,10 @@ export class TransactionController extends BaseControllerV1< if (trackers) { return; } - const networkClient = this.getNetworkClientById(networkClientId); + const networkClient = this.messagingSystem.call( + `NetworkController:getNetworkClientById`, + networkClientId, + ); const { chainId } = networkClient.configuration; let etherscanRemoteTransactionSource = From 38cbc03c8e1a2bba02fce0bc11e698af95a8891f Mon Sep 17 00:00:00 2001 From: jiexi Date: Mon, 5 Feb 2024 09:42:40 -0800 Subject: [PATCH 085/100] Jl/transaction multichain dry mock requests (#3888) ## Explanation * Move global mainnet network mock into `newController` (since I'm evidently wrong and nock can stack) * DRY mock requests into helper ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- .../transaction-controller/jest.config.js | 4 +- .../TransactionControllerIntegration.test.ts | 2800 ++--------------- .../test/JsonRpcRequestMocks.ts | 230 ++ tests/mock-network.ts | 2 +- 4 files changed, 518 insertions(+), 2518 deletions(-) create mode 100644 packages/transaction-controller/test/JsonRpcRequestMocks.ts diff --git a/packages/transaction-controller/jest.config.js b/packages/transaction-controller/jest.config.js index 3679c698b0..eeeef9619a 100644 --- a/packages/transaction-controller/jest.config.js +++ b/packages/transaction-controller/jest.config.js @@ -19,8 +19,8 @@ module.exports = merge(baseConfig, { global: { branches: 89.05, functions: 93.89, - lines: 97.85, - statements: 97.81, + lines: 97.73, + statements: 97.76, }, }, diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index f658313d7d..c5d7daf846 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -23,6 +23,17 @@ import { ETHERSCAN_TOKEN_TRANSACTION_MOCK, ETHERSCAN_TRANSACTION_SUCCESS_MOCK, } from '../test/EtherscanMocks'; +import { + buildEthGasPriceRequestMock, + buildEthBlockNumberRequestMock, + buildEthGetCodeRequestMock, + buildEthGetBlockByNumberRequestMock, + buildEthEstimateGasRequestMock, + buildEthGetTransactionCountRequestMock, + buildEthGetBlockByHashRequestMock, + buildEthSendRawTransactionRequestMock, + buildEthGetTransactionReceiptRequestMock, +} from '../test/JsonRpcRequestMocks'; import { TransactionController } from './TransactionController'; import type { TransactionMeta } from './types'; import { TransactionStatus, TransactionType } from './types'; @@ -62,6 +73,19 @@ const customGoerliNetworkClientConfiguration = { // eslint-disable-next-line @typescript-eslint/no-explicit-any const newController = async (options: any = {}) => { + // Mainnet network must be mocked for NetworkController instantiation + mockNetwork({ + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), + mocks: [ + { + ...buildEthBlockNumberRequestMock('0x1'), + discardAfterMatching: false, + }, + ], + }); + const messenger = new ControllerMessenger(); const networkController = new NetworkController({ messenger: messenger.getRestricted({ name: 'NetworkController' }), @@ -133,77 +157,22 @@ describe('TransactionController Integration', () => { describe('constructor', () => { it('should create a new instance of TransactionController', async () => { - mockNetwork({ - networkClientConfiguration: buildInfuraNetworkClientConfiguration( - InfuraNetworkType.mainnet, - ), - mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - ], - }); const { transactionController } = await newController({}); expect(transactionController).toBeDefined(); transactionController.destroy(); }); it('should submit all approved transactions in state', async () => { - mockNetwork({ - networkClientConfiguration: buildInfuraNetworkClientConfiguration( - InfuraNetworkType.mainnet, - ), - mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - ], - }); - mockNetwork({ networkClientConfiguration: buildInfuraNetworkClientConfiguration( InfuraNetworkType.goerli, ), mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // publishTransaction - { - request: { - method: 'eth_sendRawTransaction', - params: [ - '0x02e2050101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', - ], - }, - response: { - result: '0x1', - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthSendRawTransactionRequestMock( + '0x02e2050101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + '0x1', + ), ], }); @@ -212,29 +181,11 @@ describe('TransactionController Integration', () => { InfuraNetworkType.sepolia, ), mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // publishTransaction - { - request: { - method: 'eth_sendRawTransaction', - params: [ - '0x02e583aa36a70101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', - ], - }, - response: { - result: '0x1', - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthSendRawTransactionRequestMock( + '0x02e583aa36a70101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + '0x1', + ), ], }); @@ -331,61 +282,14 @@ describe('TransactionController Integration', () => { describe('multichain transaction lifecycle', () => { describe('when a transaction is added with a networkClientId that does not match the globally selected network', () => { it('should add a new unapproved transaction', async () => { - mockNetwork({ - networkClientConfiguration: buildInfuraNetworkClientConfiguration( - InfuraNetworkType.mainnet, - ), - mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3b3301', - }, - }, - ], - }); mockNetwork({ networkClientConfiguration: buildInfuraNetworkClientConfiguration( InfuraNetworkType.goerli, ), mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // readAddressAsContract - // requiresFixedGas (cached) - { - request: { - method: 'eth_getCode', - params: [ACCOUNT_2_MOCK, '0x1'], - }, - response: { - result: '0x', // non contract - }, - }, - // getSuggestedGasFees - { - request: { - method: 'eth_gasPrice', - params: [], - }, - response: { - result: '0x1', - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthGetCodeRequestMock(ACCOUNT_2_MOCK), + buildEthGasPriceRequestMock(), ], }); const { transactionController } = await newController({}); @@ -403,123 +307,22 @@ describe('TransactionController Integration', () => { transactionController.destroy(); }); it('should be able to get to submitted state', async () => { - mockNetwork({ - networkClientConfiguration: buildInfuraNetworkClientConfiguration( - InfuraNetworkType.mainnet, - ), - mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3b3301', - }, - }, - ], - }); mockNetwork({ networkClientConfiguration: buildInfuraNetworkClientConfiguration( InfuraNetworkType.goerli, ), mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NetworkController - { - request: { - method: 'eth_getBlockByNumber', - params: ['0x1', false], - }, - response: { - result: { - baseFeePerGas: '0x63c498a46', - number: '0x42', - }, - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x2', - }, - }, - // readAddressAsContract - // requiresFixedGas (cached) - { - request: { - method: 'eth_getCode', - params: [ACCOUNT_2_MOCK, '0x1'], - }, - response: { - result: '0x', // non contract - }, - }, - // estimateGas - { - request: { - method: 'eth_estimateGas', - params: [ - { - from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - to: '0x08f137f335ea1b8f193b8f6ea92561a60d23a211', - value: '0x0', - gas: '0x0', - }, - ], - }, - response: { - result: '0x1', - }, - }, - // getSuggestedGasFees - { - request: { - method: 'eth_gasPrice', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NonceTracker - { - request: { - method: 'eth_getTransactionCount', - params: ['0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', '0x1'], - }, - response: { - result: '0x1', - }, - }, - // publishTransaction - { - request: { - method: 'eth_sendRawTransaction', - params: [ - '0x02e2050101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', - ], - }, - response: { - result: '0x1', - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthGetBlockByNumberRequestMock('0x1'), + buildEthBlockNumberRequestMock('0x2'), + buildEthGetCodeRequestMock(ACCOUNT_2_MOCK), + buildEthEstimateGasRequestMock(ACCOUNT_MOCK, ACCOUNT_2_MOCK), + buildEthGasPriceRequestMock(), + buildEthGetTransactionCountRequestMock(ACCOUNT_MOCK), + buildEthSendRawTransactionRequestMock( + '0x02e2050101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + '0x1', + ), ], }); const { transactionController, approvalController } = @@ -545,149 +348,24 @@ describe('TransactionController Integration', () => { transactionController.destroy(); }); it('should be able to get to confirmed state', async () => { - mockNetwork({ - networkClientConfiguration: buildInfuraNetworkClientConfiguration( - InfuraNetworkType.mainnet, - ), - mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3b3301', - }, - }, - ], - }); mockNetwork({ networkClientConfiguration: buildInfuraNetworkClientConfiguration( InfuraNetworkType.goerli, ), mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NetworkController - { - request: { - method: 'eth_getBlockByNumber', - params: ['0x1', false], - }, - response: { - result: { - baseFeePerGas: '0x63c498a46', - number: '0x1', - }, - }, - }, - // readAddressAsContract - // requiresFixedGas (cached) - { - request: { - method: 'eth_getCode', - params: [ACCOUNT_2_MOCK, '0x1'], - }, - response: { - result: '0x', // non contract - }, - }, - // estimateGas - { - request: { - method: 'eth_estimateGas', - params: [ - { - from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - to: '0x08f137f335ea1b8f193b8f6ea92561a60d23a211', - value: '0x0', - gas: '0x0', - }, - ], - }, - response: { - result: '0x1', - }, - }, - // getSuggestedGasFees - { - request: { - method: 'eth_gasPrice', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NonceTracker - { - request: { - method: 'eth_getTransactionCount', - params: ['0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', '0x1'], - }, - response: { - result: '0x1', - }, - }, - // publishTransaction - { - request: { - method: 'eth_sendRawTransaction', - params: [ - '0x02e2050101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', - ], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3', - }, - }, - // PendingTransactionTracker.#checkTransaction - { - request: { - method: 'eth_getTransactionReceipt', - params: ['0x1'], - }, - response: { - result: { - blockHash: '0x1', - blockNumber: '0x3', // we need at least 2 blocks mocked since the first one is used for when the blockTracker is instantied before we have listeners - status: '0x1', // 0x1 = success - }, - }, - }, - // PendingTransactionTracker.#onTransactionConfirmed - { - request: { - method: 'eth_getBlockByHash', - params: ['0x1', false], - }, - response: { - result: { - transactions: [], - }, - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthGetBlockByNumberRequestMock('0x1'), + buildEthGetCodeRequestMock(ACCOUNT_2_MOCK), + buildEthEstimateGasRequestMock(ACCOUNT_MOCK, ACCOUNT_2_MOCK), + buildEthGasPriceRequestMock(), + buildEthGetTransactionCountRequestMock(ACCOUNT_MOCK), + buildEthSendRawTransactionRequestMock( + '0x02e2050101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + '0x1', + ), + buildEthBlockNumberRequestMock('0x3'), + buildEthGetTransactionReceiptRequestMock('0x1', '0x1', '0x3'), + buildEthGetBlockByHashRequestMock('0x1'), ], }); const { transactionController, approvalController } = @@ -717,149 +395,24 @@ describe('TransactionController Integration', () => { transactionController.destroy(); }); it('should be able to send and confirm transactions on different chains', async () => { - mockNetwork({ - networkClientConfiguration: buildInfuraNetworkClientConfiguration( - InfuraNetworkType.mainnet, - ), - mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - ], - }); mockNetwork({ networkClientConfiguration: buildInfuraNetworkClientConfiguration( InfuraNetworkType.goerli, ), mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NetworkController - { - request: { - method: 'eth_getBlockByNumber', - params: ['0x1', false], - }, - response: { - result: { - baseFeePerGas: '0x63c498a46', - number: '0x1', - }, - }, - }, - // readAddressAsContract - // requiresFixedGas (cached) - { - request: { - method: 'eth_getCode', - params: [ACCOUNT_2_MOCK, '0x1'], - }, - response: { - result: '0x', // non contract - }, - }, - // estimateGas - { - request: { - method: 'eth_estimateGas', - params: [ - { - from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - to: '0x08f137f335ea1b8f193b8f6ea92561a60d23a211', - value: '0x0', - gas: '0x0', - }, - ], - }, - response: { - result: '0x1', - }, - }, - // getSuggestedGasFees - { - request: { - method: 'eth_gasPrice', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NonceTracker - { - request: { - method: 'eth_getTransactionCount', - params: ['0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', '0x1'], - }, - response: { - result: '0x1', - }, - }, - // publishTransaction - { - request: { - method: 'eth_sendRawTransaction', - params: [ - '0x02e2050101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', - ], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3', - }, - }, - // PendingTransactionTracker.#checkTransaction - { - request: { - method: 'eth_getTransactionReceipt', - params: ['0x1'], - }, - response: { - result: { - blockHash: '0x1', - blockNumber: '0x3', // we need at least 2 blocks mocked since the first one is used for when the blockTracker is instantied before we have listeners - status: '0x1', // 0x1 = success - }, - }, - }, - // PendingTransactionTracker.#onTransactionConfirmed - { - request: { - method: 'eth_getBlockByHash', - params: ['0x1', false], - }, - response: { - result: { - transactions: [], - }, - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthGetBlockByNumberRequestMock('0x1'), + buildEthGetCodeRequestMock(ACCOUNT_2_MOCK), + buildEthEstimateGasRequestMock(ACCOUNT_MOCK, ACCOUNT_2_MOCK), + buildEthGasPriceRequestMock(), + buildEthGetTransactionCountRequestMock(ACCOUNT_MOCK), + buildEthSendRawTransactionRequestMock( + '0x02e2050101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + '0x1', + ), + buildEthBlockNumberRequestMock('0x3'), + buildEthGetTransactionReceiptRequestMock('0x1', '0x1', '0x3'), + buildEthGetBlockByHashRequestMock('0x1'), ], }); mockNetwork({ @@ -867,126 +420,19 @@ describe('TransactionController Integration', () => { InfuraNetworkType.sepolia, ), mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NetworkController - { - request: { - method: 'eth_getBlockByNumber', - params: ['0x1', false], - }, - response: { - result: { - baseFeePerGas: '0x63c498a46', - number: '0x1', - }, - }, - }, - // readAddressAsContract - // requiresFixedGas (cached) - { - request: { - method: 'eth_getCode', - params: [ACCOUNT_2_MOCK, '0x1'], - }, - response: { - result: '0x', // non contract - }, - }, - // estimateGas - { - request: { - method: 'eth_estimateGas', - params: [ - { - from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - to: '0x08f137f335ea1b8f193b8f6ea92561a60d23a211', - value: '0x0', - gas: '0x0', - }, - ], - }, - response: { - result: '0x1', - }, - }, - // getSuggestedGasFees - { - request: { - method: 'eth_gasPrice', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NonceTracker - { - request: { - method: 'eth_getTransactionCount', - params: ['0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', '0x1'], - }, - response: { - result: '0x1', - }, - }, - // publishTransaction - { - request: { - method: 'eth_sendRawTransaction', - params: [ - '0x02e583aa36a70101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', - ], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3', - }, - }, - // PendingTransactionTracker.#checkTransaction - { - request: { - method: 'eth_getTransactionReceipt', - params: ['0x1'], - }, - response: { - result: { - blockHash: '0x1', - blockNumber: '0x3', // we need at least 2 blocks mocked since the first one is used for when the blockTracker is instantied before we have listeners - status: '0x1', // 0x1 = success - }, - }, - }, - // PendingTransactionTracker.#onTransactionConfirmed - { - request: { - method: 'eth_getBlockByHash', - params: ['0x1', false], - }, - response: { - result: { - transactions: [], - }, - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthGetBlockByNumberRequestMock('0x1'), + buildEthGetCodeRequestMock(ACCOUNT_2_MOCK), + buildEthEstimateGasRequestMock(ACCOUNT_MOCK, ACCOUNT_2_MOCK), + buildEthGasPriceRequestMock(), + buildEthGetTransactionCountRequestMock(ACCOUNT_MOCK), + buildEthSendRawTransactionRequestMock( + '0x02e583aa36a70101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + '0x1', + ), + buildEthBlockNumberRequestMock('0x3'), + buildEthGetTransactionReceiptRequestMock('0x1', '0x1', '0x3'), + buildEthGetBlockByHashRequestMock('0x1'), ], }); const { transactionController, approvalController } = @@ -1038,183 +484,28 @@ describe('TransactionController Integration', () => { it('should be able to cancel a transaction', async () => { mockNetwork({ networkClientConfiguration: buildInfuraNetworkClientConfiguration( - InfuraNetworkType.mainnet, + InfuraNetworkType.goerli, ), mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3b3301', - }, - }, - ], - }); - mockNetwork({ - networkClientConfiguration: buildInfuraNetworkClientConfiguration( - InfuraNetworkType.goerli, - ), - mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NetworkController - { - request: { - method: 'eth_getBlockByNumber', - params: ['0x1', false], - }, - response: { - result: { - baseFeePerGas: '0x63c498a46', - number: '0x1', - }, - }, - }, - // readAddressAsContract - // requiresFixedGas (cached) - { - request: { - method: 'eth_getCode', - params: [ACCOUNT_2_MOCK, '0x1'], - }, - response: { - result: '0x', // non contract - }, - }, - // estimateGas - { - request: { - method: 'eth_estimateGas', - params: [ - { - from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - to: '0x08f137f335ea1b8f193b8f6ea92561a60d23a211', - value: '0x0', - gas: '0x0', - }, - ], - }, - response: { - result: '0x1', - }, - }, - // getSuggestedGasFees - { - request: { - method: 'eth_gasPrice', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NonceTracker - { - request: { - method: 'eth_getTransactionCount', - params: ['0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', '0x1'], - }, - response: { - result: '0x1', - }, - }, - // publishTransaction - { - request: { - method: 'eth_sendRawTransaction', - params: [ - '0x02e2050101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', - ], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3', - }, - }, - // PendingTransactionTracker.#checkTransaction - { - request: { - method: 'eth_getTransactionReceipt', - params: ['0x1'], - }, - response: { - result: { - blockHash: '0x1', - blockNumber: '0x3', // we need at least 2 blocks mocked since the first one is used for when the blockTracker is instantied before we have listeners - status: '0x1', // 0x1 = success - }, - }, - }, - // PendingTransactionTracker.#onTransactionConfirmed - { - request: { - method: 'eth_getBlockByHash', - params: ['0x1', false], - }, - response: { - result: { - transactions: [], - }, - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3', - }, - }, - // publishTransaction - { - request: { - method: 'eth_sendRawTransaction', - params: [ - '0x02e205010101825208946bf137f335ea1b8f193b8f6ea92561a60d23a2078080c0808080', - ], - }, - response: { - result: '0x2', - }, - }, - // PendingTransactionTracker.#checkTransaction - { - request: { - method: 'eth_getTransactionReceipt', - params: ['0x2'], - }, - response: { - result: { - blockHash: '0x1', - blockNumber: '0x3', // we need at least 2 blocks mocked since the first one is used for when the blockTracker is instantied before we have listeners - status: '0x1', // 0x1 = success - }, - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthGetBlockByNumberRequestMock('0x1'), + buildEthGetCodeRequestMock(ACCOUNT_2_MOCK), + buildEthEstimateGasRequestMock(ACCOUNT_MOCK, ACCOUNT_2_MOCK), + buildEthGasPriceRequestMock(), + buildEthGetTransactionCountRequestMock(ACCOUNT_MOCK), + buildEthSendRawTransactionRequestMock( + '0x02e2050101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + '0x1', + ), + buildEthBlockNumberRequestMock('0x3'), + buildEthGetTransactionReceiptRequestMock('0x1', '0x1', '0x3'), + buildEthGetBlockByHashRequestMock('0x1'), + buildEthBlockNumberRequestMock('0x3'), + buildEthSendRawTransactionRequestMock( + '0x02e205010101825208946bf137f335ea1b8f193b8f6ea92561a60d23a2078080c0808080', + '0x2', + ), + buildEthGetTransactionReceiptRequestMock('0x2', '0x1', '0x3'), ], }); const { transactionController, approvalController } = @@ -1242,201 +533,38 @@ describe('TransactionController Integration', () => { transactionController.destroy(); }); it('should be able to confirm a cancelled transaction', async () => { - mockNetwork({ - networkClientConfiguration: buildInfuraNetworkClientConfiguration( - InfuraNetworkType.mainnet, - ), - mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3b3301', - }, - }, - ], - }); mockNetwork({ networkClientConfiguration: buildInfuraNetworkClientConfiguration( InfuraNetworkType.goerli, ), mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NetworkController - { - request: { - method: 'eth_getBlockByNumber', - params: ['0x1', false], - }, - response: { - result: { - baseFeePerGas: '0x63c498a46', - number: '0x1', - }, - }, - }, - // readAddressAsContract - // requiresFixedGas (cached) - { - request: { - method: 'eth_getCode', - params: [ACCOUNT_2_MOCK, '0x1'], - }, - response: { - result: '0x', // non contract - }, - }, - // estimateGas - { - request: { - method: 'eth_estimateGas', - params: [ - { - from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - to: '0x08f137f335ea1b8f193b8f6ea92561a60d23a211', - value: '0x0', - gas: '0x0', - }, - ], - }, - response: { - result: '0x1', - }, - }, - // getSuggestedGasFees - { - request: { - method: 'eth_gasPrice', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NonceTracker - { - request: { - method: 'eth_getTransactionCount', - params: ['0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', '0x1'], - }, - response: { - result: '0x1', - }, - }, - // publishTransaction - { - request: { - method: 'eth_sendRawTransaction', - params: [ - '0x02e2050101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', - ], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3', - }, - }, - // publishTransaction - { - request: { - method: 'eth_sendRawTransaction', - params: [ - '0x02e205010101825208946bf137f335ea1b8f193b8f6ea92561a60d23a2078080c0808080', - ], - }, - response: { - result: '0x2', - }, - }, - // PendingTransactionTracker.#checkTransaction - { - request: { - method: 'eth_getTransactionReceipt', - params: ['0x1'], - }, - response: { - result: null, - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x4', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x4', - }, - }, - // PendingTransactionTracker.#checkTransaction - { - request: { - method: 'eth_getTransactionReceipt', - params: ['0x1'], - }, - response: { - result: null, - }, - }, - // PendingTransactionTracker.#checkTransaction - { - request: { - method: 'eth_getTransactionReceipt', - params: ['0x2'], - }, - response: { - result: { - blockHash: '0x2', - blockNumber: '0x4', // we need at least 2 blocks mocked since the first one is used for when the blockTracker is instantied before we have listeners - status: '0x1', // 0x1 = success - }, - }, - }, - // PendingTransactionTracker.#onTransactionConfirmed - { - request: { - method: 'eth_getBlockByHash', - params: ['0x2', false], - }, - response: { - result: { - transactions: [], - }, - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthGetBlockByNumberRequestMock('0x1'), + buildEthGetCodeRequestMock(ACCOUNT_2_MOCK), + buildEthEstimateGasRequestMock(ACCOUNT_MOCK, ACCOUNT_2_MOCK), + buildEthGasPriceRequestMock(), + buildEthGetTransactionCountRequestMock(ACCOUNT_MOCK), + buildEthSendRawTransactionRequestMock( + '0x02e2050101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + '0x1', + ), + buildEthBlockNumberRequestMock('0x3'), + buildEthSendRawTransactionRequestMock( + '0x02e205010101825208946bf137f335ea1b8f193b8f6ea92561a60d23a2078080c0808080', + '0x2', + ), + { + ...buildEthGetTransactionReceiptRequestMock('0x1', '0x0', '0x0'), + response: { result: null }, + }, + buildEthBlockNumberRequestMock('0x4'), + buildEthBlockNumberRequestMock('0x4'), + { + ...buildEthGetTransactionReceiptRequestMock('0x1', '0x0', '0x0'), + response: { result: null }, + }, + buildEthGetTransactionReceiptRequestMock('0x2', '0x2', '0x4'), + buildEthGetBlockByHashRequestMock('0x2'), ], }); const { transactionController, approvalController } = @@ -1472,201 +600,38 @@ describe('TransactionController Integration', () => { transactionController.destroy(); }); it('should be able to get to speedup state', async () => { - mockNetwork({ - networkClientConfiguration: buildInfuraNetworkClientConfiguration( - InfuraNetworkType.mainnet, - ), - mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3b3301', - }, - }, - ], - }); mockNetwork({ networkClientConfiguration: buildInfuraNetworkClientConfiguration( InfuraNetworkType.goerli, ), mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NetworkController - { - request: { - method: 'eth_getBlockByNumber', - params: ['0x1', false], - }, - response: { - result: { - baseFeePerGas: '0x63c498a46', - number: '0x1', - }, - }, - }, - // readAddressAsContract - // requiresFixedGas (cached) - { - request: { - method: 'eth_getCode', - params: [ACCOUNT_2_MOCK, '0x1'], - }, - response: { - result: '0x', // non contract - }, - }, - // estimateGas - { - request: { - method: 'eth_estimateGas', - params: [ - { - from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - to: '0x08f137f335ea1b8f193b8f6ea92561a60d23a211', - value: '0x0', - gas: '0x0', - }, - ], - }, - response: { - result: '0x1', - }, - }, - // getSuggestedGasFees - { - request: { - method: 'eth_gasPrice', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NonceTracker - { - request: { - method: 'eth_getTransactionCount', - params: ['0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', '0x1'], - }, - response: { - result: '0x1', - }, - }, - // publishTransaction - { - request: { - method: 'eth_sendRawTransaction', - params: [ - '0x02e605018203e88203e88252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', - ], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3', - }, - }, - // PendingTransactionTracker.#checkTransaction - { - request: { - method: 'eth_getTransactionReceipt', - params: ['0x1'], - }, - response: { - result: null, - }, - }, - // publishTransaction - { - request: { - method: 'eth_sendRawTransaction', - params: [ - '0x02e6050182044c82044c8252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', - ], - }, - response: { - result: '0x2', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x4', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x4', - }, - }, - // PendingTransactionTracker.#checkTransaction - { - request: { - method: 'eth_getTransactionReceipt', - params: ['0x1'], - }, - response: { - result: null, - }, - }, - // PendingTransactionTracker.#checkTransaction - { - request: { - method: 'eth_getTransactionReceipt', - params: ['0x2'], - }, - response: { - result: { - blockHash: '0x2', - blockNumber: '0x4', // we need at least 2 blocks mocked since the first one is used for when the blockTracker is instantied before we have listeners - status: '0x1', // 0x1 = success - }, - }, - }, - // PendingTransactionTracker.#onTransactionConfirmed - { - request: { - method: 'eth_getBlockByHash', - params: ['0x2', false], - }, - response: { - result: { - transactions: [], - }, - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthGetBlockByNumberRequestMock('0x1'), + buildEthGetCodeRequestMock(ACCOUNT_2_MOCK), + buildEthEstimateGasRequestMock(ACCOUNT_MOCK, ACCOUNT_2_MOCK), + buildEthGasPriceRequestMock(), + buildEthGetTransactionCountRequestMock(ACCOUNT_MOCK), + buildEthSendRawTransactionRequestMock( + '0x02e605018203e88203e88252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + '0x1', + ), + buildEthBlockNumberRequestMock('0x3'), + { + ...buildEthGetTransactionReceiptRequestMock('0x1', '0x0', '0x0'), + response: { result: null }, + }, + buildEthSendRawTransactionRequestMock( + '0x02e6050182044c82044c8252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + '0x2', + ), + buildEthBlockNumberRequestMock('0x4'), + buildEthBlockNumberRequestMock('0x4'), + { + ...buildEthGetTransactionReceiptRequestMock('0x1', '0x0', '0x0'), + response: { result: null }, + }, + buildEthGetTransactionReceiptRequestMock('0x2', '0x2', '0x4'), + buildEthGetBlockByHashRequestMock('0x2'), ], }); const { transactionController, approvalController } = @@ -1713,294 +678,45 @@ describe('TransactionController Integration', () => { describe('when transactions are added concurrently with different networkClientIds but on the same chainId', () => { it('should add each transaction with consecutive nonces', async () => { - mockNetwork({ - networkClientConfiguration: buildInfuraNetworkClientConfiguration( - InfuraNetworkType.mainnet, - ), - mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '1', - }, - }, - ], - }); mockNetwork({ networkClientConfiguration: buildInfuraNetworkClientConfiguration( InfuraNetworkType.goerli, ), mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NetworkController - { - request: { - method: 'eth_getBlockByNumber', - params: ['0x1', false], - }, - response: { - result: { - baseFeePerGas: '0x63c498a46', - number: '0x1', - }, - }, - }, - // readAddressAsContract - // requiresFixedGas (cached) - { - request: { - method: 'eth_getCode', - params: [ACCOUNT_2_MOCK, '0x1'], - }, - response: { - result: '0x', // non contract - }, - }, - // estimateGas - { - request: { - method: 'eth_estimateGas', - params: [ - { - from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - to: '0x08f137f335ea1b8f193b8f6ea92561a60d23a211', - value: '0x0', - gas: '0x0', - }, - ], - }, - response: { - result: '0x1', - }, - }, - // getSuggestedGasFees - { - request: { - method: 'eth_gasPrice', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NonceTracker - { - request: { - method: 'eth_getTransactionCount', - params: ['0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', '0x1'], - }, - response: { - result: '0x1', - }, - }, - // publishTransaction - { - request: { - method: 'eth_sendRawTransaction', - params: [ - '0x02e2050101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', - ], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3', - }, - }, - // PendingTransactionTracker.#checkTransaction - { - request: { - method: 'eth_getTransactionReceipt', - params: ['0x1'], - }, - response: { - result: { - blockHash: '0x1', - blockNumber: '0x3', // we need at least 2 blocks mocked since the first one is used for when the blockTracker is instantied before we have listeners - status: '0x1', // 0x1 = success - }, - }, - }, - // PendingTransactionTracker.#onTransactionConfirmed - { - request: { - method: 'eth_getBlockByHash', - params: ['0x1', false], - }, - response: { - result: { - transactions: [], - }, - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3', - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthGetBlockByNumberRequestMock('0x1'), + buildEthGetCodeRequestMock(ACCOUNT_2_MOCK), + buildEthEstimateGasRequestMock(ACCOUNT_MOCK, ACCOUNT_2_MOCK), + buildEthGasPriceRequestMock(), + buildEthGetTransactionCountRequestMock(ACCOUNT_MOCK), + buildEthSendRawTransactionRequestMock( + '0x02e2050101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + '0x1', + ), + buildEthBlockNumberRequestMock('0x3'), + buildEthGetTransactionReceiptRequestMock('0x1', '0x1', '0x3'), + buildEthGetBlockByHashRequestMock('0x1'), + buildEthBlockNumberRequestMock('0x3'), ], }); mockNetwork({ networkClientConfiguration: customGoerliNetworkClientConfiguration, mocks: [ - // NetworkController - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NetworkController - { - request: { - method: 'eth_getBlockByNumber', - params: ['0x1', false], - }, - response: { - result: { - baseFeePerGas: '0x63c498a46', - number: '0x1', - }, - }, - }, - // readAddressAsContract - // requiresFixedGas (cached) - { - request: { - method: 'eth_getCode', - params: [ACCOUNT_2_MOCK, '0x1'], - }, - response: { - result: '0x', // non contract - }, - }, - // estimateGas - { - request: { - method: 'eth_estimateGas', - params: [ - { - from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - to: '0x08f137f335ea1b8f193b8f6ea92561a60d23a211', - value: '0x0', - gas: '0x0', - }, - ], - }, - response: { - result: '0x1', - }, - }, - // getSuggestedGasFees - { - request: { - method: 'eth_gasPrice', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NonceTracker - { - request: { - method: 'eth_getTransactionCount', - params: ['0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', '0x1'], - }, - response: { - result: '0x1', - }, - }, - // publishTransaction - { - request: { - method: 'eth_sendRawTransaction', - params: [ - '0x02e0050201018094e688b84b23f322a994a53dbf8e15fa82cdb711278080c0808080', - ], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3', - }, - }, - // PendingTransactionTracker.#checkTransaction - { - request: { - method: 'eth_getTransactionReceipt', - params: ['0x1'], - }, - response: { - result: { - blockHash: '0x1', - blockNumber: '0x3', // we need at least 2 blocks mocked since the first one is used for when the blockTracker is instantied before we have listeners - status: '0x1', // 0x1 = success - }, - }, - }, - // PendingTransactionTracker.#onTransactionConfirmed - { - request: { - method: 'eth_getBlockByHash', - params: ['0x1', false], - }, - response: { - result: { - transactions: [], - }, - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthBlockNumberRequestMock('0x1'), + buildEthGetBlockByNumberRequestMock('0x1'), + buildEthGetCodeRequestMock(ACCOUNT_2_MOCK), + buildEthEstimateGasRequestMock(ACCOUNT_MOCK, ACCOUNT_2_MOCK), + buildEthGasPriceRequestMock(), + buildEthGetTransactionCountRequestMock(ACCOUNT_MOCK), + buildEthSendRawTransactionRequestMock( + '0x02e0050201018094e688b84b23f322a994a53dbf8e15fa82cdb711278080c0808080', + '0x1', + ), + buildEthBlockNumberRequestMock('0x3'), + buildEthGetTransactionReceiptRequestMock('0x1', '0x1', '0x3'), + buildEthGetBlockByHashRequestMock('0x1'), ], }); @@ -2058,186 +774,30 @@ describe('TransactionController Integration', () => { describe('when transactions are added concurrently with the same networkClientId', () => { it('should add each transaction with consecutive nonces', async () => { - mockNetwork({ - networkClientConfiguration: buildInfuraNetworkClientConfiguration( - InfuraNetworkType.mainnet, - ), - mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - ], - }); mockNetwork({ networkClientConfiguration: buildInfuraNetworkClientConfiguration( InfuraNetworkType.goerli, ), mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NetworkController - { - request: { - method: 'eth_getBlockByNumber', - params: ['0x1', false], - }, - response: { - result: { - baseFeePerGas: '0x63c498a46', - number: '0x1', - }, - }, - }, - // readAddressAsContract - // requiresFixedGas (cached) - { - request: { - method: 'eth_getCode', - params: [ACCOUNT_2_MOCK, '0x1'], - }, - response: { - result: '0x', // non contract - }, - }, - // readAddressAsContract - // requiresFixedGas (cached) - { - request: { - method: 'eth_getCode', - params: [ACCOUNT_3_MOCK, '0x1'], - }, - response: { - result: '0x', // non contract - }, - }, - // estimateGas - { - request: { - method: 'eth_estimateGas', - params: [ - { - from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - to: '0x08f137f335ea1b8f193b8f6ea92561a60d23a211', - value: '0x0', - gas: '0x0', - }, - ], - }, - response: { - result: '0x1', - }, - }, - // getSuggestedGasFees - { - request: { - method: 'eth_gasPrice', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NonceTracker - { - request: { - method: 'eth_getTransactionCount', - params: ['0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', '0x1'], - }, - response: { - result: '0x1', - }, - }, - // publishTransaction - { - request: { - method: 'eth_sendRawTransaction', - params: [ - '0x02e2050101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', - ], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3', - }, - }, - // PendingTransactionTracker.#checkTransaction - { - request: { - method: 'eth_getTransactionReceipt', - params: ['0x1'], - }, - response: { - result: { - blockHash: '0x1', - blockNumber: '0x3', // we need at least 2 blocks mocked since the first one is used for when the blockTracker is instantied before we have listeners - status: '0x1', // 0x1 = success - }, - }, - }, - // PendingTransactionTracker.#onTransactionConfirmed - { - request: { - method: 'eth_getBlockByHash', - params: ['0x1', false], - }, - response: { - result: { - transactions: [], - }, - }, - }, - // publishTransaction - { - request: { - method: 'eth_sendRawTransaction', - params: [ - '0x02e20502010182520894e688b84b23f322a994a53dbf8e15fa82cdb711278080c0808080', - ], - }, - response: { - result: '0x2', - }, - }, - // PendingTransactionTracker.#checkTransaction - { - request: { - method: 'eth_getTransactionReceipt', - params: ['0x2'], - }, - response: { - result: { - blockHash: '0x2', - blockNumber: '0x4', // we need at least 2 blocks mocked since the first one is used for when the blockTracker is instantied before we have listeners - status: '0x1', // 0x1 = success - }, - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthGetBlockByNumberRequestMock('0x1'), + buildEthGetCodeRequestMock(ACCOUNT_2_MOCK), + buildEthGetCodeRequestMock(ACCOUNT_3_MOCK), + buildEthEstimateGasRequestMock(ACCOUNT_MOCK, ACCOUNT_2_MOCK), + buildEthGasPriceRequestMock(), + buildEthGetTransactionCountRequestMock(ACCOUNT_MOCK), + buildEthSendRawTransactionRequestMock( + '0x02e2050101018252089408f137f335ea1b8f193b8f6ea92561a60d23a2118080c0808080', + '0x1', + ), + buildEthBlockNumberRequestMock('0x3'), + buildEthGetTransactionReceiptRequestMock('0x1', '0x1', '0x3'), + buildEthGetBlockByHashRequestMock('0x1'), + buildEthSendRawTransactionRequestMock( + '0x02e20502010182520894e688b84b23f322a994a53dbf8e15fa82cdb711278080c0808080', + '0x2', + ), + buildEthGetTransactionReceiptRequestMock('0x2', '0x2', '0x4'), ], }); const { approvalController, transactionController } = @@ -2289,81 +849,14 @@ describe('TransactionController Integration', () => { describe('when changing rpcUrl of networkClient', () => { it('should start tracking when a new network is added', async () => { - mockNetwork({ - networkClientConfiguration: buildInfuraNetworkClientConfiguration( - InfuraNetworkType.mainnet, - ), - mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - ], - }); mockNetwork({ networkClientConfiguration: customGoerliNetworkClientConfiguration, mocks: [ - // NetworkController - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NetworkController - { - request: { - method: 'eth_getBlockByNumber', - params: ['0x1', false], - }, - response: { - result: { - baseFeePerGas: '0x63c498a46', - number: '0x42', - }, - }, - }, - // readAddressAsContract - // requiresFixedGas (cached) - { - request: { - method: 'eth_getCode', - params: [ACCOUNT_2_MOCK, '0x1'], - }, - response: { - result: '0x', // non contract - }, - }, - // getSuggestedGasFees - { - request: { - method: 'eth_gasPrice', - params: [], - }, - response: { - result: '0x1', - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthBlockNumberRequestMock('0x1'), + buildEthGetBlockByNumberRequestMock('0x1'), + buildEthGetCodeRequestMock(ACCOUNT_2_MOCK), + buildEthGasPriceRequestMock(), ], }); const { networkController, transactionController } = @@ -2397,34 +890,6 @@ describe('TransactionController Integration', () => { transactionController.destroy(); }); it('should stop tracking when a network is removed', async () => { - mockNetwork({ - networkClientConfiguration: buildInfuraNetworkClientConfiguration( - InfuraNetworkType.mainnet, - ), - mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x2', - }, - }, - ], - }); const { networkController, transactionController } = await newController(); @@ -2464,58 +929,11 @@ describe('TransactionController Integration', () => { InfuraNetworkType.mainnet, ), mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x2', - }, - }, - { - request: { - method: 'eth_getBlockByNumber', - params: ['0x1', false], - }, - response: { - result: { - baseFeePerGas: '0x63c498a46', - number: '0x42', - }, - }, - }, - { - request: { - method: 'eth_gasPrice', - params: [], - }, - response: { - result: '0x1', - }, - }, - // eth_getCode - { - request: { - method: 'eth_getCode', - params: [ACCOUNT_2_MOCK, '0x1'], - }, - response: { - result: '0x', // non contract - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthBlockNumberRequestMock('0x2'), + buildEthGetBlockByNumberRequestMock('0x1'), + buildEthGasPriceRequestMock(), + buildEthGetCodeRequestMock(ACCOUNT_2_MOCK), ], }); @@ -2557,24 +975,6 @@ describe('TransactionController Integration', () => { transactionController.destroy(); }); it('should not call getNetworkClientRegistry on networkController:stateChange when feature flag is disabled', async () => { - mockNetwork({ - networkClientConfiguration: buildInfuraNetworkClientConfiguration( - InfuraNetworkType.mainnet, - ), - mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - ], - }); const getNetworkClientRegistrySpy = jest.fn().mockImplementation(() => { return { [NetworkType.goerli]: { @@ -2601,24 +1001,6 @@ describe('TransactionController Integration', () => { transactionController.destroy(); }); it('should call getNetworkClientRegistry on networkController:stateChange when feature flag is enabled', async () => { - mockNetwork({ - networkClientConfiguration: buildInfuraNetworkClientConfiguration( - InfuraNetworkType.mainnet, - ), - mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - ], - }); const getNetworkClientRegistrySpy = jest.fn().mockImplementation(() => { return { [NetworkType.goerli]: { @@ -2640,29 +1022,11 @@ describe('TransactionController Integration', () => { source: 'dapp', }, ); - - expect(getNetworkClientRegistrySpy).toHaveBeenCalled(); - transactionController.destroy(); - }); - it('should call getNetworkClientRegistry on construction when feature flag is enabled', async () => { - mockNetwork({ - networkClientConfiguration: buildInfuraNetworkClientConfiguration( - InfuraNetworkType.mainnet, - ), - mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - ], - }); + + expect(getNetworkClientRegistrySpy).toHaveBeenCalled(); + transactionController.destroy(); + }); + it('should call getNetworkClientRegistry on construction when feature flag is enabled', async () => { const getNetworkClientRegistrySpy = jest.fn().mockImplementation(() => { return { [NetworkType.goerli]: { @@ -2688,27 +1052,8 @@ describe('TransactionController Integration', () => { InfuraNetworkType.mainnet, ), mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x2', - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthBlockNumberRequestMock('0x2'), ], }); @@ -2722,36 +1067,15 @@ describe('TransactionController Integration', () => { const expectedTransactions: Partial[] = []; const networkClients = networkController.getNetworkClientRegistry(); - // Skip the globally selected provider because we can't use nock to mock it twice - const networkClientIds = Object.keys(networkClients).filter( - (v) => v !== NetworkType.goerli, - ); + const networkClientIds = Object.keys(networkClients); await Promise.all( networkClientIds.map(async (networkClientId) => { const config = networkClients[networkClientId].configuration; mockNetwork({ networkClientConfiguration: config, mocks: [ - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x2', - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthBlockNumberRequestMock('0x2'), ], }); nock(getEtherscanApiHost(config.chainId)) @@ -2822,27 +1146,8 @@ describe('TransactionController Integration', () => { InfuraNetworkType.mainnet, ), mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x2', - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthBlockNumberRequestMock('0x2'), ], }); @@ -2852,27 +1157,8 @@ describe('TransactionController Integration', () => { InfuraNetworkType.goerli, ), mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x2', - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthBlockNumberRequestMock('0x2'), ], }); @@ -2885,44 +1171,10 @@ describe('TransactionController Integration', () => { rpcUrl: 'https://mock.rpc.url', }, mocks: [ - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x2', - }, - }, - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x3', - }, - }, - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x4', - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthBlockNumberRequestMock('0x2'), + buildEthBlockNumberRequestMock('0x3'), + buildEthBlockNumberRequestMock('0x4'), ], }); @@ -3025,35 +1277,6 @@ describe('TransactionController Integration', () => { 'should stop the global incoming transaction helper when no networkClientIds provided', ); it('should not poll for new incoming transactions for the given networkClientId', async () => { - mockNetwork({ - networkClientConfiguration: buildInfuraNetworkClientConfiguration( - InfuraNetworkType.mainnet, - ), - mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x2', - }, - }, - ], - }); - const selectedAddress = ETHERSCAN_TRANSACTION_BASE_MOCK.to; const { networkController, transactionController } = await newController({ @@ -3061,36 +1284,15 @@ describe('TransactionController Integration', () => { }); const networkClients = networkController.getNetworkClientRegistry(); - // Skip the globally selected provider because we can't use nock to mock it twice - const networkClientIds = Object.keys(networkClients).filter( - (v) => v !== NetworkType.goerli, - ); + const networkClientIds = Object.keys(networkClients); await Promise.all( networkClientIds.map(async (networkClientId) => { const config = networkClients[networkClientId].configuration; mockNetwork({ networkClientConfiguration: config, mocks: [ - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x2', - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthBlockNumberRequestMock('0x2'), ], }); nock(getEtherscanApiHost(config.chainId)) @@ -3120,35 +1322,6 @@ describe('TransactionController Integration', () => { describe('stopAllIncomingTransactionPolling', () => { it('should not poll for incoming transactions on any network client', async () => { - mockNetwork({ - networkClientConfiguration: buildInfuraNetworkClientConfiguration( - InfuraNetworkType.mainnet, - ), - mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - ], - }); - const selectedAddress = ETHERSCAN_TRANSACTION_BASE_MOCK.to; const { networkController, transactionController } = await newController({ @@ -3156,36 +1329,15 @@ describe('TransactionController Integration', () => { }); const networkClients = networkController.getNetworkClientRegistry(); - // Skip the globally selected provider because we can't use nock to mock it twice - const networkClientIds = Object.keys(networkClients).filter( - (v) => v !== NetworkType.goerli, - ); + const networkClientIds = Object.keys(networkClients); await Promise.all( networkClientIds.map(async (networkClientId) => { const config = networkClients[networkClientId].configuration; mockNetwork({ networkClientConfiguration: config, mocks: [ - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x2', - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthBlockNumberRequestMock('0x2'), ], }); nock(getEtherscanApiHost(config.chainId)) @@ -3213,35 +1365,6 @@ describe('TransactionController Integration', () => { describe('updateIncomingTransactions', () => { it('should add incoming transactions to state with the correct chainId for the given networkClientId without waiting for the next block', async () => { - mockNetwork({ - networkClientConfiguration: buildInfuraNetworkClientConfiguration( - InfuraNetworkType.mainnet, - ), - mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - ], - }); - const selectedAddress = ETHERSCAN_TRANSACTION_BASE_MOCK.to; const { networkController, transactionController } = await newController({ @@ -3252,27 +1375,13 @@ describe('TransactionController Integration', () => { const expectedTransactions: Partial[] = []; const networkClients = networkController.getNetworkClientRegistry(); - // Skip the globally selected provider because we can't use nock to mock it twice - const networkClientIds = Object.keys(networkClients).filter( - (v) => v !== NetworkType.goerli, - ); + const networkClientIds = Object.keys(networkClients); await Promise.all( networkClientIds.map(async (networkClientId) => { const config = networkClients[networkClientId].configuration; mockNetwork({ networkClientConfiguration: config, - mocks: [ - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - ], + mocks: [buildEthBlockNumberRequestMock('0x1')], }); nock(getEtherscanApiHost(config.chainId)) .get( @@ -3325,70 +1434,24 @@ describe('TransactionController Integration', () => { describe('getNonceLock', () => { it('should get the nonce lock from the nonceTracker for the given networkClientId', async () => { - mockNetwork({ - networkClientConfiguration: buildInfuraNetworkClientConfiguration( - InfuraNetworkType.mainnet, - ), - mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - ], - }); - const { networkController, transactionController } = await newController( {}, ); const networkClients = networkController.getNetworkClientRegistry(); - // Skip the globally selected provider because we can't use nock to mock it twice - const networkClientIds = Object.keys(networkClients).filter( - (v) => v !== NetworkType.goerli, - ); + const networkClientIds = Object.keys(networkClients); await Promise.all( networkClientIds.map(async (networkClientId) => { const config = networkClients[networkClientId].configuration; mockNetwork({ networkClientConfiguration: config, mocks: [ - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NonceTracker - { - request: { - method: 'eth_getTransactionCount', - params: [ACCOUNT_MOCK, '0x1'], - }, - response: { - result: '0xa', - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthGetTransactionCountRequestMock( + ACCOUNT_MOCK, + '0x1', + '0xa', + ), ], }); @@ -3407,70 +1470,24 @@ describe('TransactionController Integration', () => { }); it('should block attempts to get the nonce lock for the same address from the nonceTracker for the networkClientId until the previous lock is released', async () => { - mockNetwork({ - networkClientConfiguration: buildInfuraNetworkClientConfiguration( - InfuraNetworkType.mainnet, - ), - mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - ], - }); - const { networkController, transactionController } = await newController( {}, ); const networkClients = networkController.getNetworkClientRegistry(); - // Skip the globally selected provider because we can't use nock to mock it twice - const networkClientIds = Object.keys(networkClients).filter( - (v) => v !== NetworkType.goerli, - ); + const networkClientIds = Object.keys(networkClients); await Promise.all( networkClientIds.map(async (networkClientId) => { const config = networkClients[networkClientId].configuration; mockNetwork({ networkClientConfiguration: config, mocks: [ - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NonceTracker - { - request: { - method: 'eth_getTransactionCount', - params: [ACCOUNT_MOCK, '0x1'], - }, - response: { - result: '0xa', - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthGetTransactionCountRequestMock( + ACCOUNT_MOCK, + '0x1', + '0xa', + ), ], }); @@ -3514,35 +1531,6 @@ describe('TransactionController Integration', () => { }); it('should block attempts to get the nonce lock for the same address from the nonceTracker for the different networkClientIds on the same chainId until the previous lock is released', async () => { - mockNetwork({ - networkClientConfiguration: buildInfuraNetworkClientConfiguration( - InfuraNetworkType.mainnet, - ), - mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - ], - }); - const { networkController, transactionController } = await newController( {}, ); @@ -3551,52 +1539,16 @@ describe('TransactionController Integration', () => { InfuraNetworkType.goerli, ), mocks: [ - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NonceTracker - { - request: { - method: 'eth_getTransactionCount', - params: [ACCOUNT_MOCK, '0x1'], - }, - response: { - result: '0xa', - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthGetTransactionCountRequestMock(ACCOUNT_MOCK, '0x1', '0xa'), ], }); mockNetwork({ networkClientConfiguration: customGoerliNetworkClientConfiguration, mocks: [ - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NonceTracker - { - request: { - method: 'eth_getTransactionCount', - params: [ACCOUNT_MOCK, '0x1'], - }, - response: { - result: '0xa', - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthGetTransactionCountRequestMock(ACCOUNT_MOCK, '0x1', '0xa'), ], }); @@ -3648,35 +1600,6 @@ describe('TransactionController Integration', () => { }); it('should not block attempts to get the nonce lock for the same addresses from the nonceTracker for different networkClientIds', async () => { - mockNetwork({ - networkClientConfiguration: buildInfuraNetworkClientConfiguration( - InfuraNetworkType.mainnet, - ), - mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - ], - }); - const { transactionController } = await newController({}); mockNetwork({ @@ -3684,26 +1607,8 @@ describe('TransactionController Integration', () => { InfuraNetworkType.goerli, ), mocks: [ - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NonceTracker - { - request: { - method: 'eth_getTransactionCount', - params: [ACCOUNT_MOCK, '0x1'], - }, - response: { - result: '0xa', - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthGetTransactionCountRequestMock(ACCOUNT_MOCK, '0x1', '0xa'), ], }); @@ -3712,26 +1617,8 @@ describe('TransactionController Integration', () => { InfuraNetworkType.sepolia, ), mocks: [ - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NonceTracker - { - request: { - method: 'eth_getTransactionCount', - params: [ACCOUNT_MOCK, '0x1'], - }, - response: { - result: '0xf', - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthGetTransactionCountRequestMock(ACCOUNT_MOCK, '0x1', '0xf'), ], }); @@ -3759,80 +1646,29 @@ describe('TransactionController Integration', () => { }); it('should not block attempts to get the nonce lock for different addresses from the nonceTracker for the networkClientId', async () => { - mockNetwork({ - networkClientConfiguration: buildInfuraNetworkClientConfiguration( - InfuraNetworkType.mainnet, - ), - mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - ], - }); - const { networkController, transactionController } = await newController( {}, ); const networkClients = networkController.getNetworkClientRegistry(); - // Skip the globally selected provider because we can't use nock to mock it twice - const networkClientIds = Object.keys(networkClients).filter( - (v) => v !== NetworkType.goerli, - ); + const networkClientIds = Object.keys(networkClients); await Promise.all( networkClientIds.map(async (networkClientId) => { const config = networkClients[networkClientId].configuration; mockNetwork({ networkClientConfiguration: config, mocks: [ - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NonceTracker - { - request: { - method: 'eth_getTransactionCount', - params: [ACCOUNT_MOCK, '0x1'], - }, - response: { - result: '0xa', - }, - }, - // NonceTracker - { - request: { - method: 'eth_getTransactionCount', - params: [ACCOUNT_2_MOCK, '0x1'], - }, - response: { - result: '0xf', - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthGetTransactionCountRequestMock( + ACCOUNT_MOCK, + '0x1', + '0xa', + ), + buildEthGetTransactionCountRequestMock( + ACCOUNT_2_MOCK, + '0x1', + '0xf', + ), ], }); @@ -3866,27 +1702,8 @@ describe('TransactionController Integration', () => { InfuraNetworkType.mainnet, ), mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NonceTracker - { - request: { - method: 'eth_getTransactionCount', - params: [ACCOUNT_MOCK, '0x1'], - }, - response: { - result: '0xa', - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthGetTransactionCountRequestMock(ACCOUNT_MOCK, '0x1', '0xa'), ], }); @@ -3907,27 +1724,8 @@ describe('TransactionController Integration', () => { InfuraNetworkType.mainnet, ), mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NonceTracker - { - request: { - method: 'eth_getTransactionCount', - params: [ACCOUNT_MOCK, '0x1'], - }, - response: { - result: '0xa', - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthGetTransactionCountRequestMock(ACCOUNT_MOCK, '0x1', '0xa'), ], }); @@ -3971,37 +1769,9 @@ describe('TransactionController Integration', () => { InfuraNetworkType.mainnet, ), mocks: [ - // NetworkController - // BlockTracker - { - request: { - method: 'eth_blockNumber', - params: [], - }, - response: { - result: '0x1', - }, - }, - // NonceTracker - { - request: { - method: 'eth_getTransactionCount', - params: [ACCOUNT_MOCK, '0x1'], - }, - response: { - result: '0xa', - }, - }, - // NonceTracker - { - request: { - method: 'eth_getTransactionCount', - params: [ACCOUNT_2_MOCK, '0x1'], - }, - response: { - result: '0xf', - }, - }, + buildEthBlockNumberRequestMock('0x1'), + buildEthGetTransactionCountRequestMock(ACCOUNT_MOCK, '0x1', '0xa'), + buildEthGetTransactionCountRequestMock(ACCOUNT_2_MOCK, '0x1', '0xf'), ], }); diff --git a/packages/transaction-controller/test/JsonRpcRequestMocks.ts b/packages/transaction-controller/test/JsonRpcRequestMocks.ts new file mode 100644 index 0000000000..101009fce5 --- /dev/null +++ b/packages/transaction-controller/test/JsonRpcRequestMocks.ts @@ -0,0 +1,230 @@ +import type { Hex } from '@metamask/utils'; + +import type { JsonRpcRequestMock } from '../../../tests/mock-network'; + +/** + * Builds mock eth_gasPrice request. + * Used by getSuggestedGasFees. + * + * @param result - the hex gas price result. + * @returns The mock json rpc request object. + */ +export function buildEthGasPriceRequestMock( + result: Hex = '0x1', +): JsonRpcRequestMock { + return { + request: { + method: 'eth_gasPrice', + params: [], + }, + response: { + result, + }, + }; +} + +/** + * Builds mock eth_blockNumber request. + * Used by NetworkController and BlockTracker. + * + * @param result - the hex block number result. + * @returns The mock json rpc request object. + */ +export function buildEthBlockNumberRequestMock( + result: Hex, +): JsonRpcRequestMock { + return { + request: { + method: 'eth_blockNumber', + params: [], + }, + response: { + result, + }, + }; +} + +/** + * Builds mock eth_getCode request. + * Used by readAddressAsContract and requiresFixedGas. + * + * @param address - The hex address. + * @param blockNumber - The hex block number. + * @param result - the hex code result. + * @returns The mock json rpc request object. + */ +export function buildEthGetCodeRequestMock( + address: Hex, + blockNumber: Hex = '0x1', + result: Hex = '0x', +): JsonRpcRequestMock { + return { + request: { + method: 'eth_getCode', + params: [address, blockNumber], + }, + response: { + result, + }, + }; +} + +/** + * Builds mock eth_getBlockByNumber request. + * Used by NetworkController. + * + * @param number - the hex (block) number. + * @param baseFeePerGas - the hex base fee per gas result. + * @returns The mock json rpc request object. + */ +export function buildEthGetBlockByNumberRequestMock( + number: Hex, + baseFeePerGas: Hex = '0x63c498a46', +): JsonRpcRequestMock { + return { + request: { + method: 'eth_getBlockByNumber', + params: [number, false], + }, + response: { + result: { + baseFeePerGas, + number, + }, + }, + }; +} + +/** + * Builds mock eth_estimateGas request. + * Used by estimateGas. + * + * @param from - The hex from address. + * @param to - The hex to address. + * @param result - the hex gas result. + * @returns The mock json rpc request object. + */ +export function buildEthEstimateGasRequestMock( + from: Hex, + to: Hex, + result: Hex = '0x1', +): JsonRpcRequestMock { + return { + request: { + method: 'eth_estimateGas', + params: [ + { + from, + to, + value: '0x0', + gas: '0x0', + }, + ], + }, + response: { + result, + }, + }; +} + +/** + * Builds mock eth_getTransactionCount request. + * Used by NonceTracker. + * + * @param address - The hex address. + * @param blockNumber - The hex block number. + * @param result - the hex transaction count result. + * @returns The mock json rpc request object. + */ +export function buildEthGetTransactionCountRequestMock( + address: Hex, + blockNumber: Hex = '0x1', + result: Hex = '0x1', +): JsonRpcRequestMock { + return { + request: { + method: 'eth_getTransactionCount', + params: [address, blockNumber], + }, + response: { + result, + }, + }; +} + +/** + * Builds mock eth_getBlockByHash request. + * Used by PendingTransactionTracker.#onTransactionConfirmed. + * + * @param blockhash - The hex block hash. + * @returns The mock json rpc request object. + */ +export function buildEthGetBlockByHashRequestMock( + blockhash: Hex, +): JsonRpcRequestMock { + return { + request: { + method: 'eth_getBlockByHash', + params: [blockhash, false], + }, + response: { + result: { + transactions: [], + }, + }, + }; +} + +/** + * Builds mock eth_sendRawTransaction request. + * Used by publishTransaction. + * + * @param txData - The hex signed transaction data. + * @param result - the hex transaction hash result. + * @returns The mock json rpc request object. + */ +export function buildEthSendRawTransactionRequestMock( + txData: Hex, + result: Hex, +): JsonRpcRequestMock { + return { + request: { + method: 'eth_sendRawTransaction', + params: [txData], + }, + response: { + result, + }, + }; +} + +/** + * Builds mock eth_getTransactionReceipt request. + * Used by PendingTransactionTracker.#checkTransaction. + * + * @param txHash - The hex transaction hash. + * @param blockHash - the hex transaction hash result. + * @param blockNumber - the hex block number result. + * @param status - the hex status result. + * @returns The mock json rpc request object. + */ +export function buildEthGetTransactionReceiptRequestMock( + txHash: Hex, + blockHash: Hex, + blockNumber: Hex, + status: Hex = '0x1', +): JsonRpcRequestMock { + return { + request: { + method: 'eth_getTransactionReceipt', + params: [txHash], + }, + response: { + result: { + blockHash, + blockNumber, + status, + }, + }, + }; +} diff --git a/tests/mock-network.ts b/tests/mock-network.ts index 9581cc6448..b4b5b90fd1 100644 --- a/tests/mock-network.ts +++ b/tests/mock-network.ts @@ -29,7 +29,7 @@ import { NetworkClientType } from '../packages/network-controller/src/types'; * when the promise is initiated but before it is resolved). You can pass an * function (optionally async) to do this. */ -type JsonRpcRequestMock = { +export type JsonRpcRequestMock = { request: { method: string; // TODO: Replace `any` with type From 28c3ac30b57aaf8456aff3dbedf938bce7e4a6e7 Mon Sep 17 00:00:00 2001 From: jiexi Date: Mon, 5 Feb 2024 10:19:27 -0800 Subject: [PATCH 086/100] Make properties and methods # private (#3889) ## Explanation * Make some properties and methods # private ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- .../src/TransactionController.ts | 222 +++++++++--------- 1 file changed, 111 insertions(+), 111 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index eea59e67ea..2e83c8b181 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -313,24 +313,24 @@ export class TransactionController extends BaseControllerV1< chainId?: string, ) => NonceTrackerTransaction[]; - private enableMultichain: boolean; + #enableMultichain: boolean; private readonly messagingSystem: TransactionControllerMessenger; + readonly #incomingTransactionOptions: IncomingTransactionOptions; + private readonly incomingTransactionHelper: IncomingTransactionHelper; private readonly securityProviderRequest?: SecurityProviderRequest; + readonly #pendingTransactionOptions: PendingTransactionOptions; + private readonly pendingTransactionTracker: PendingTransactionTracker; private readonly cancelMultiplier: number; private readonly speedUpMultiplier: number; - private readonly incomingTransactionOptions: IncomingTransactionOptions; - - private readonly pendingTransactionOptions: PendingTransactionOptions; - private readonly signAbortCallbacks: Map void> = new Map(); private readonly afterSign: ( @@ -357,7 +357,7 @@ export class TransactionController extends BaseControllerV1< transactionMeta: TransactionMeta, ) => (TransactionMeta | undefined)[]; - private readonly getNetworkClientRegistry: NetworkController['getNetworkClientRegistry']; + readonly #getNetworkClientRegistry: NetworkController['getNetworkClientRegistry']; private failTransaction( transactionMeta: TransactionMeta, @@ -541,8 +541,8 @@ export class TransactionController extends BaseControllerV1< lastFetchedBlockNumbers: {}, }; this.initialize(); - this.enableMultichain = enableMultichain; - this.getNetworkClientRegistry = getNetworkClientRegistry; + this.#enableMultichain = enableMultichain; + this.#getNetworkClientRegistry = getNetworkClientRegistry; this.provider = provider; this.messagingSystem = messenger; this.getNetworkState = getNetworkState; @@ -566,8 +566,8 @@ export class TransactionController extends BaseControllerV1< this.securityProviderRequest = securityProviderRequest; this.cancelMultiplier = cancelMultiplier ?? CANCEL_RATE; this.speedUpMultiplier = speedUpMultiplier ?? SPEED_UP_RATE; - this.incomingTransactionOptions = incomingTransactions; - this.pendingTransactionOptions = pendingTransactions; + this.#incomingTransactionOptions = incomingTransactions; + this.#pendingTransactionOptions = pendingTransactions; this.afterSign = hooks?.afterSign ?? (() => true); this.beforeApproveOnInit = hooks?.beforeApproveOnInit ?? (() => true); @@ -585,7 +585,7 @@ export class TransactionController extends BaseControllerV1< // @ts-expect-error provider types misaligned: SafeEventEmitterProvider vs Record provider, blockTracker, - getPendingTransactions: this.getNonceTrackerPendingTransactions.bind( + getPendingTransactions: this.#getNonceTrackerPendingTransactions.bind( this, undefined, ), @@ -612,7 +612,7 @@ export class TransactionController extends BaseControllerV1< updateTransactions: incomingTransactions.updateTransactions, }); - this.addIncomingTransactionHelperListeners(); + this.#addIncomingTransactionHelperListeners(); this.pendingTransactionTracker = new PendingTransactionTracker({ approveTransaction: this.approveTransaction.bind(this), @@ -633,7 +633,7 @@ export class TransactionController extends BaseControllerV1< }, }); - this.addPendingTransactionTrackerListeners(); + this.#addPendingTransactionTrackerListeners(); // when transactionsController state changes // check for pending transactions and start polling if there are any @@ -651,8 +651,8 @@ export class TransactionController extends BaseControllerV1< this.messagingSystem.subscribe( 'NetworkController:stateChange', (_, patches) => { - if (this.enableMultichain) { - const networkClients = this.getNetworkClientRegistry(); + if (this.#enableMultichain) { + const networkClients = this.#getNetworkClientRegistry(); patches.forEach(({ op, path }) => { if (op === 'remove' && path[0] === 'networkConfigurations') { const networkClientId = path[1] as NetworkClientId; @@ -664,13 +664,13 @@ export class TransactionController extends BaseControllerV1< } }, ); - if (this.enableMultichain) { + if (this.#enableMultichain) { this.#initTrackingMap(); } } setEnableMultichain(enableMultichain: boolean) { - this.enableMultichain = enableMultichain; + this.#enableMultichain = enableMultichain; if (enableMultichain) { this.#initTrackingMap(); } @@ -859,7 +859,7 @@ export class TransactionController extends BaseControllerV1< } startIncomingTransactionPolling(networkClientIds: NetworkClientId[] = []) { - if (networkClientIds.length === 0 || !this.enableMultichain) { + if (networkClientIds.length === 0 || !this.#enableMultichain) { this.incomingTransactionHelper.start(); return; } @@ -869,7 +869,7 @@ export class TransactionController extends BaseControllerV1< } stopIncomingTransactionPolling(networkClientIds: NetworkClientId[] = []) { - if (networkClientIds.length === 0 || !this.enableMultichain) { + if (networkClientIds.length === 0 || !this.#enableMultichain) { this.incomingTransactionHelper.stop(); return; } @@ -880,7 +880,7 @@ export class TransactionController extends BaseControllerV1< stopAllIncomingTransactionPolling() { this.incomingTransactionHelper.stop(); - if (this.enableMultichain) { + if (this.#enableMultichain) { for (const [, trackingMap] of this.trackingMap) { trackingMap.incomingTransactionHelper.stop(); } @@ -888,7 +888,7 @@ export class TransactionController extends BaseControllerV1< } async updateIncomingTransactions(networkClientIds: NetworkClientId[] = []) { - if (networkClientIds.length === 0 || !this.enableMultichain) { + if (networkClientIds.length === 0 || !this.#enableMultichain) { await this.incomingTransactionHelper.update(); return; } @@ -1658,7 +1658,7 @@ export class TransactionController extends BaseControllerV1< ): Promise { let releaseLockForChainIdKey: (() => void) | undefined; let { nonceTracker } = this; - if (networkClientId && this.enableMultichain) { + if (networkClientId && this.#enableMultichain) { const networkClient = this.messagingSystem.call( `NetworkController:getNetworkClientById`, networkClientId, @@ -2838,61 +2838,6 @@ export class TransactionController extends BaseControllerV1< ); } - private removeIncomingTransactionHelperListeners( - incomingTransactionHelper = this.incomingTransactionHelper, - ) { - incomingTransactionHelper.hub.removeAllListeners('transactions'); - incomingTransactionHelper.hub.removeAllListeners( - 'updatedLastFetchedBlockNumbers', - ); - } - - private addIncomingTransactionHelperListeners( - incomingTransactionHelper = this.incomingTransactionHelper, - ) { - incomingTransactionHelper.hub.on( - 'transactions', - this.onIncomingTransactions.bind(this), - ); - incomingTransactionHelper.hub.on( - 'updatedLastFetchedBlockNumbers', - this.onUpdatedLastFetchedBlockNumbers.bind(this), - ); - } - - private removePendingTransactionTrackerListeners( - pendingTransactionTracker = this.pendingTransactionTracker, - ) { - pendingTransactionTracker.hub.removeAllListeners('transaction-confirmed'); - pendingTransactionTracker.hub.removeAllListeners('transaction-dropped'); - pendingTransactionTracker.hub.removeAllListeners('transaction-failed'); - pendingTransactionTracker.hub.removeAllListeners('transaction-updated'); - } - - private addPendingTransactionTrackerListeners( - pendingTransactionTracker = this.pendingTransactionTracker, - ) { - pendingTransactionTracker.hub.on( - 'transaction-confirmed', - this.onConfirmedTransaction.bind(this), - ); - - pendingTransactionTracker.hub.on( - 'transaction-dropped', - this.setTransactionStatusDropped.bind(this), - ); - - pendingTransactionTracker.hub.on( - 'transaction-failed', - this.failTransaction.bind(this), - ); - - pendingTransactionTracker.hub.on( - 'transaction-updated', - this.updateTransaction.bind(this), - ); - } - private async signTransaction( transactionMeta: TransactionMeta, txParams: TransactionParams, @@ -2963,24 +2908,6 @@ export class TransactionController extends BaseControllerV1< this.hub.emit('transaction-status-update', { transactionMeta }); } - private getNonceTrackerPendingTransactions( - chainId: string | undefined, - address: string, - ) { - const standardPendingTransactions = this.getNonceTrackerTransactions( - TransactionStatus.submitted, - address, - chainId, - ); - - const externalPendingTransactions = this.getExternalPendingTransactions( - address, - chainId, - ); - - return [...standardPendingTransactions, ...externalPendingTransactions]; - } - private getNonceTrackerTransactions( status: TransactionStatus, address: string, @@ -3044,7 +2971,7 @@ export class TransactionController extends BaseControllerV1< chainId?: Hex; }): EthQuery { // if multichain is disabled, use the global ethQuery - if (!this.enableMultichain) { + if (!this.#enableMultichain) { return this.ethQuery; } let networkClient: NetworkClient | undefined; @@ -3131,7 +3058,7 @@ export class TransactionController extends BaseControllerV1< }; #initTrackingMap = () => { - const networkClients = this.getNetworkClientRegistry(); + const networkClients = this.#getNetworkClientRegistry(); const networkClientIds = Object.keys(networkClients); networkClientIds.map((id) => this.#startTrackingByNetworkClientId(id)); this.hub.emit('tracking-map-init', networkClientIds); @@ -3140,7 +3067,7 @@ export class TransactionController extends BaseControllerV1< #checkForPendingTransactionAndStartPolling = () => { // PendingTransactionTracker reads state through its getTransactions hook this.pendingTransactionTracker.startIfPendingTransactions(); - if (this.enableMultichain) { + if (this.#enableMultichain) { for (const [, trackingMap] of this.trackingMap) { trackingMap.pendingTransactionTracker.startIfPendingTransactions(); } @@ -3151,11 +3078,11 @@ export class TransactionController extends BaseControllerV1< const trackers = this.trackingMap.get(networkClientId); if (trackers) { trackers.pendingTransactionTracker.stop(); - this.removePendingTransactionTrackerListeners( + this.#removePendingTransactionTrackerListeners( trackers.pendingTransactionTracker, ); trackers.incomingTransactionHelper.stop(); - this.removeIncomingTransactionHelperListeners( + this.#removeIncomingTransactionHelperListeners( trackers.incomingTransactionHelper, ); this.trackingMap.delete(networkClientId); @@ -3165,11 +3092,11 @@ export class TransactionController extends BaseControllerV1< #stopAllTracking() { this.pendingTransactionTracker.stop(); - this.removePendingTransactionTrackerListeners(); + this.#removePendingTransactionTrackerListeners(); this.incomingTransactionHelper.stop(); - this.removeIncomingTransactionHelperListeners(); + this.#removeIncomingTransactionHelperListeners(); - if (this.enableMultichain) { + if (this.#enableMultichain) { for (const [networkClientId] of this.trackingMap) { this.#stopTrackingByNetworkClientId(networkClientId); } @@ -3192,7 +3119,7 @@ export class TransactionController extends BaseControllerV1< if (!etherscanRemoteTransactionSource) { etherscanRemoteTransactionSource = new EtherscanRemoteTransactionSource({ includeTokenTransfers: - this.incomingTransactionOptions.includeTokenTransfers, + this.#incomingTransactionOptions.includeTokenTransfers, }); this.etherscanRemoteTransactionSourcesMap.set( chainId, @@ -3207,7 +3134,7 @@ export class TransactionController extends BaseControllerV1< // eslint-disable-next-line @typescript-eslint/no-explicit-any provider: networkClient.provider as any, blockTracker: networkClient.blockTracker, - getPendingTransactions: this.getNonceTrackerPendingTransactions.bind( + getPendingTransactions: this.#getNonceTrackerPendingTransactions.bind( this, chainId, ), @@ -3222,14 +3149,14 @@ export class TransactionController extends BaseControllerV1< getCurrentAccount: this.getSelectedAddress, getLastFetchedBlockNumbers: () => this.state.lastFetchedBlockNumbers, getChainId: () => chainId, - isEnabled: this.incomingTransactionOptions.isEnabled, - queryEntireHistory: this.incomingTransactionOptions.queryEntireHistory, + isEnabled: this.#incomingTransactionOptions.isEnabled, + queryEntireHistory: this.#incomingTransactionOptions.queryEntireHistory, remoteTransactionSource: etherscanRemoteTransactionSource, transactionLimit: this.config.txHistoryLimit, - updateTransactions: this.incomingTransactionOptions.updateTransactions, + updateTransactions: this.#incomingTransactionOptions.updateTransactions, }); - this.addIncomingTransactionHelperListeners(incomingTransactionHelper); + this.#addIncomingTransactionHelperListeners(incomingTransactionHelper); const pendingTransactionTracker = new PendingTransactionTracker({ approveTransaction: this.approveTransaction.bind(this), @@ -3237,7 +3164,7 @@ export class TransactionController extends BaseControllerV1< getChainId: () => chainId, getEthQuery: () => ethQuery, getTransactions: () => this.state.transactions, - isResubmitEnabled: this.pendingTransactionOptions.isResubmitEnabled, + isResubmitEnabled: this.#pendingTransactionOptions.isResubmitEnabled, getGlobalLock: this.#acquireNonceLockForChainIdKey.bind(this, { chainId, }), @@ -3249,7 +3176,7 @@ export class TransactionController extends BaseControllerV1< }, }); - this.addPendingTransactionTrackerListeners(pendingTransactionTracker); + this.#addPendingTransactionTrackerListeners(pendingTransactionTracker); this.trackingMap.set(networkClientId, { nonceTracker, @@ -3259,4 +3186,77 @@ export class TransactionController extends BaseControllerV1< this.hub.emit('tracking-map-add', networkClientId); } + + #removeIncomingTransactionHelperListeners( + incomingTransactionHelper = this.incomingTransactionHelper, + ) { + incomingTransactionHelper.hub.removeAllListeners('transactions'); + incomingTransactionHelper.hub.removeAllListeners( + 'updatedLastFetchedBlockNumbers', + ); + } + + #addIncomingTransactionHelperListeners( + incomingTransactionHelper = this.incomingTransactionHelper, + ) { + incomingTransactionHelper.hub.on( + 'transactions', + this.onIncomingTransactions.bind(this), + ); + incomingTransactionHelper.hub.on( + 'updatedLastFetchedBlockNumbers', + this.onUpdatedLastFetchedBlockNumbers.bind(this), + ); + } + + #removePendingTransactionTrackerListeners( + pendingTransactionTracker = this.pendingTransactionTracker, + ) { + pendingTransactionTracker.hub.removeAllListeners('transaction-confirmed'); + pendingTransactionTracker.hub.removeAllListeners('transaction-dropped'); + pendingTransactionTracker.hub.removeAllListeners('transaction-failed'); + pendingTransactionTracker.hub.removeAllListeners('transaction-updated'); + } + + #addPendingTransactionTrackerListeners( + pendingTransactionTracker = this.pendingTransactionTracker, + ) { + pendingTransactionTracker.hub.on( + 'transaction-confirmed', + this.onConfirmedTransaction.bind(this), + ); + + pendingTransactionTracker.hub.on( + 'transaction-dropped', + this.setTransactionStatusDropped.bind(this), + ); + + pendingTransactionTracker.hub.on( + 'transaction-failed', + this.failTransaction.bind(this), + ); + + pendingTransactionTracker.hub.on( + 'transaction-updated', + this.updateTransaction.bind(this), + ); + } + + #getNonceTrackerPendingTransactions( + chainId: string | undefined, + address: string, + ) { + const standardPendingTransactions = this.getNonceTrackerTransactions( + TransactionStatus.submitted, + address, + chainId, + ); + + const externalPendingTransactions = this.getExternalPendingTransactions( + address, + chainId, + ); + + return [...standardPendingTransactions, ...externalPendingTransactions]; + } } From e09e9420c1d5631002f3be5ea0200917c8f4f2b0 Mon Sep 17 00:00:00 2001 From: jiexi Date: Mon, 5 Feb 2024 10:20:14 -0800 Subject: [PATCH 087/100] Update packages/transaction-controller/src/utils/etherscan.ts Co-authored-by: Elliot Winkler --- packages/transaction-controller/src/utils/etherscan.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/transaction-controller/src/utils/etherscan.ts b/packages/transaction-controller/src/utils/etherscan.ts index e4fed4a5c2..cec423cc93 100644 --- a/packages/transaction-controller/src/utils/etherscan.ts +++ b/packages/transaction-controller/src/utils/etherscan.ts @@ -202,9 +202,8 @@ function getEtherscanApiUrl( * @returns host URL to access Etherscan data. */ export function getEtherscanApiHost(chainId: Hex) { - type SupportedChainId = keyof typeof ETHERSCAN_SUPPORTED_NETWORKS; - - const networkInfo = ETHERSCAN_SUPPORTED_NETWORKS[chainId as SupportedChainId]; + // @ts-expect-error We account for `chainId` not being a property below + const networkInfo = ETHERSCAN_SUPPORTED_NETWORKS[chainId]; if (!networkInfo) { throw new Error(`Etherscan does not support chain with ID: ${chainId}`); From 1ca5558357614fa804129f90f55345d6b39cf713 Mon Sep 17 00:00:00 2001 From: jiexi Date: Mon, 5 Feb 2024 10:33:38 -0800 Subject: [PATCH 088/100] remove @type (#3890) ## Explanation * Remove unnecessary `@type` ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- .../src/TransactionController.ts | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 2e83c8b181..32273d8f55 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -104,7 +104,9 @@ import { export const HARDFORK = Hardfork.London; /** - * @type Result + * Object with new transaction's meta and a promise resolving to the + * transaction hash if successful. + * * @property result - Promise resolving to a new transaction hash * @property transactionMeta - Meta information about this new transaction */ @@ -132,9 +134,8 @@ export interface FeeMarketEIP1559Values { } /** - * @type TransactionConfig - * * Transaction controller configuration + * * @property provider - Provider used to create a new underlying EthQuery instance * @property sign - Method used to sign transactions */ @@ -149,9 +150,8 @@ export interface TransactionConfig extends BaseConfig { } /** - * @type MethodData - * * Method data registry object + * * @property registryMethod - Registry method raw string * @property parsedRegistryMethod - Registry method object, containing name and method arguments */ @@ -164,9 +164,8 @@ export interface MethodData { } /** - * @type TransactionState - * * Transaction controller state + * * @property transactions - A list of TransactionMeta objects * @property methodData - Object containing all known method data information */ @@ -190,9 +189,8 @@ export const CANCEL_RATE = 1.5; export const SPEED_UP_RATE = 1.1; /** - * @type IncomingTransactionOptions - * * Configuration options for the IncomingTransactionHelper + * * @property includeTokenTransfers - Whether or not to include ERC20 token transfers. * @property isEnabled - Whether or not incoming transaction retrieval is enabled. * @property queryEntireHistory - Whether to initially query the entire transaction history or only recent blocks. @@ -206,9 +204,8 @@ type IncomingTransactionOptions = { }; /** - * @type PendingTransactionOptions - * * Configuration options for the PendingTransactionTracker + * * @property isResubmitEnabled - Whether transaction publishing is automatically retried. */ type PendingTransactionOptions = { @@ -216,8 +213,6 @@ type PendingTransactionOptions = { }; /** - * @type NetworkClientRegistry - * * Registry of network clients provided by the NetworkController */ From fb85958a8cdbc764b31415fe6ddec53c2ab50815 Mon Sep 17 00:00:00 2001 From: jiexi Date: Mon, 5 Feb 2024 11:39:43 -0800 Subject: [PATCH 089/100] Update packages/transaction-controller/src/TransactionController.ts Co-authored-by: Elliot Winkler --- packages/transaction-controller/src/TransactionController.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 32273d8f55..b649b2429d 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -2965,7 +2965,6 @@ export class TransactionController extends BaseControllerV1< networkClientId?: NetworkClientId; chainId?: Hex; }): EthQuery { - // if multichain is disabled, use the global ethQuery if (!this.#enableMultichain) { return this.ethQuery; } From 6dc68ba8e399ccc268e2e54fe47ff011ebd74d76 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 5 Feb 2024 11:51:41 -0800 Subject: [PATCH 090/100] address trackingMap clarity comment --- .../src/TransactionController.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 32273d8f55..8e792f6aaa 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -876,8 +876,10 @@ export class TransactionController extends BaseControllerV1< stopAllIncomingTransactionPolling() { this.incomingTransactionHelper.stop(); if (this.#enableMultichain) { - for (const [, trackingMap] of this.trackingMap) { - trackingMap.incomingTransactionHelper.stop(); + for (const { incomingTransactionHelper } of Object.values( + this.trackingMap, + )) { + incomingTransactionHelper.stop(); } } } @@ -3063,8 +3065,10 @@ export class TransactionController extends BaseControllerV1< // PendingTransactionTracker reads state through its getTransactions hook this.pendingTransactionTracker.startIfPendingTransactions(); if (this.#enableMultichain) { - for (const [, trackingMap] of this.trackingMap) { - trackingMap.pendingTransactionTracker.startIfPendingTransactions(); + for (const { pendingTransactionTracker } of Object.values( + this.trackingMap, + )) { + pendingTransactionTracker.startIfPendingTransactions(); } } }; From d654b0bb7409ea818d0ca5b0d2ea19299bf03eb3 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 5 Feb 2024 12:07:49 -0800 Subject: [PATCH 091/100] Revert "address trackingMap clarity comment" This reverts commit 6dc68ba8e399ccc268e2e54fe47ff011ebd74d76. --- .../src/TransactionController.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 1265139b31..b649b2429d 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -876,10 +876,8 @@ export class TransactionController extends BaseControllerV1< stopAllIncomingTransactionPolling() { this.incomingTransactionHelper.stop(); if (this.#enableMultichain) { - for (const { incomingTransactionHelper } of Object.values( - this.trackingMap, - )) { - incomingTransactionHelper.stop(); + for (const [, trackingMap] of this.trackingMap) { + trackingMap.incomingTransactionHelper.stop(); } } } @@ -3064,10 +3062,8 @@ export class TransactionController extends BaseControllerV1< // PendingTransactionTracker reads state through its getTransactions hook this.pendingTransactionTracker.startIfPendingTransactions(); if (this.#enableMultichain) { - for (const { pendingTransactionTracker } of Object.values( - this.trackingMap, - )) { - pendingTransactionTracker.startIfPendingTransactions(); + for (const [, trackingMap] of this.trackingMap) { + trackingMap.pendingTransactionTracker.startIfPendingTransactions(); } } }; From e2a7272861f4c6c965c8d936bd75a8442dae5072 Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Mon, 5 Feb 2024 12:09:58 -0800 Subject: [PATCH 092/100] fix trackingMap ambiguity --- .../transaction-controller/src/TransactionController.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index b649b2429d..12f88fe882 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -876,8 +876,8 @@ export class TransactionController extends BaseControllerV1< stopAllIncomingTransactionPolling() { this.incomingTransactionHelper.stop(); if (this.#enableMultichain) { - for (const [, trackingMap] of this.trackingMap) { - trackingMap.incomingTransactionHelper.stop(); + for (const [, trackers] of this.trackingMap) { + trackers.incomingTransactionHelper.stop(); } } } @@ -3062,8 +3062,8 @@ export class TransactionController extends BaseControllerV1< // PendingTransactionTracker reads state through its getTransactions hook this.pendingTransactionTracker.startIfPendingTransactions(); if (this.#enableMultichain) { - for (const [, trackingMap] of this.trackingMap) { - trackingMap.pendingTransactionTracker.startIfPendingTransactions(); + for (const [, trackers] of this.trackingMap) { + trackers.pendingTransactionTracker.startIfPendingTransactions(); } } }; From 85e2a436006bcd4e102e990a52a581ea887814cd Mon Sep 17 00:00:00 2001 From: jiexi Date: Mon, 5 Feb 2024 12:57:24 -0800 Subject: [PATCH 093/100] Rename to isMultichainEnabled. Remove setter (#3892) ## Explanation * Remove feature flag setter * Rename `enableMultichain` to `isMultichainEnabled` ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --------- Co-authored-by: Alex Donesky --- .../src/TransactionController.test.ts | 6 +-- .../src/TransactionController.ts | 37 ++++++++----------- .../TransactionControllerIntegration.test.ts | 10 ++--- 3 files changed, 23 insertions(+), 30 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 24b4a304fd..c455de8223 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -1208,10 +1208,10 @@ describe('TransactionController', () => { ); }); - describe('when enableMultichain: true is specified', () => { + describe('when isMultichainEnabled: true is specified', () => { it('adds unapproved transaction to state when using networkClientId', async () => { const controller = newController({ - options: { enableMultichain: true }, + options: { isMultichainEnabled: true }, }); const sepoliaTxParams: TransactionParams = { chainId: ChainId.sepolia, @@ -1238,7 +1238,7 @@ describe('TransactionController', () => { it('adds unapproved transaction with networkClientId and can be updated to submitted', async () => { const controller = newController({ approve: true, - options: { enableMultichain: true }, + options: { isMultichainEnabled: true }, }); const submittedEventListener = jest.fn(); controller.hub.on('transaction-submitted', submittedEventListener); diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 12f88fe882..6c35c69bc6 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -308,7 +308,7 @@ export class TransactionController extends BaseControllerV1< chainId?: string, ) => NonceTrackerTransaction[]; - #enableMultichain: boolean; + #isMultichainEnabled: boolean; private readonly messagingSystem: TransactionControllerMessenger; @@ -425,7 +425,7 @@ export class TransactionController extends BaseControllerV1< * @param options.disableHistory - Whether to disable storing history in transaction metadata. * @param options.disableSendFlowHistory - Explicitly disable transaction metadata history. * @param options.disableSwaps - Whether to disable additional processing on swaps transactions. - * @param options.enableMultichain - Enable multichain support. + * @param options.isMultichainEnabled - Enable multichain support. * @param options.getCurrentAccountEIP1559Compatibility - Whether or not the account supports EIP-1559. * @param options.getCurrentNetworkEIP1559Compatibility - Whether or not the network supports EIP-1559. * @param options.getExternalPendingTransactions - Callback to retrieve pending transactions from external sources. @@ -475,7 +475,7 @@ export class TransactionController extends BaseControllerV1< securityProviderRequest, speedUpMultiplier, getNetworkClientRegistry, - enableMultichain = false, + isMultichainEnabled = false, hooks = {}, }: { blockTracker: BlockTracker; @@ -502,7 +502,7 @@ export class TransactionController extends BaseControllerV1< securityProviderRequest?: SecurityProviderRequest; speedUpMultiplier?: number; getNetworkClientRegistry: NetworkController['getNetworkClientRegistry']; - enableMultichain: boolean; + isMultichainEnabled: boolean; hooks: { afterSign?: ( transactionMeta: TransactionMeta, @@ -536,7 +536,7 @@ export class TransactionController extends BaseControllerV1< lastFetchedBlockNumbers: {}, }; this.initialize(); - this.#enableMultichain = enableMultichain; + this.#isMultichainEnabled = isMultichainEnabled; this.#getNetworkClientRegistry = getNetworkClientRegistry; this.provider = provider; this.messagingSystem = messenger; @@ -646,7 +646,7 @@ export class TransactionController extends BaseControllerV1< this.messagingSystem.subscribe( 'NetworkController:stateChange', (_, patches) => { - if (this.#enableMultichain) { + if (this.#isMultichainEnabled) { const networkClients = this.#getNetworkClientRegistry(); patches.forEach(({ op, path }) => { if (op === 'remove' && path[0] === 'networkConfigurations') { @@ -659,14 +659,7 @@ export class TransactionController extends BaseControllerV1< } }, ); - if (this.#enableMultichain) { - this.#initTrackingMap(); - } - } - - setEnableMultichain(enableMultichain: boolean) { - this.#enableMultichain = enableMultichain; - if (enableMultichain) { + if (this.#isMultichainEnabled) { this.#initTrackingMap(); } } @@ -854,7 +847,7 @@ export class TransactionController extends BaseControllerV1< } startIncomingTransactionPolling(networkClientIds: NetworkClientId[] = []) { - if (networkClientIds.length === 0 || !this.#enableMultichain) { + if (networkClientIds.length === 0 || !this.#isMultichainEnabled) { this.incomingTransactionHelper.start(); return; } @@ -864,7 +857,7 @@ export class TransactionController extends BaseControllerV1< } stopIncomingTransactionPolling(networkClientIds: NetworkClientId[] = []) { - if (networkClientIds.length === 0 || !this.#enableMultichain) { + if (networkClientIds.length === 0 || !this.#isMultichainEnabled) { this.incomingTransactionHelper.stop(); return; } @@ -875,7 +868,7 @@ export class TransactionController extends BaseControllerV1< stopAllIncomingTransactionPolling() { this.incomingTransactionHelper.stop(); - if (this.#enableMultichain) { + if (this.#isMultichainEnabled) { for (const [, trackers] of this.trackingMap) { trackers.incomingTransactionHelper.stop(); } @@ -883,7 +876,7 @@ export class TransactionController extends BaseControllerV1< } async updateIncomingTransactions(networkClientIds: NetworkClientId[] = []) { - if (networkClientIds.length === 0 || !this.#enableMultichain) { + if (networkClientIds.length === 0 || !this.#isMultichainEnabled) { await this.incomingTransactionHelper.update(); return; } @@ -1653,7 +1646,7 @@ export class TransactionController extends BaseControllerV1< ): Promise { let releaseLockForChainIdKey: (() => void) | undefined; let { nonceTracker } = this; - if (networkClientId && this.#enableMultichain) { + if (networkClientId && this.#isMultichainEnabled) { const networkClient = this.messagingSystem.call( `NetworkController:getNetworkClientById`, networkClientId, @@ -2965,7 +2958,7 @@ export class TransactionController extends BaseControllerV1< networkClientId?: NetworkClientId; chainId?: Hex; }): EthQuery { - if (!this.#enableMultichain) { + if (!this.#isMultichainEnabled) { return this.ethQuery; } let networkClient: NetworkClient | undefined; @@ -3061,7 +3054,7 @@ export class TransactionController extends BaseControllerV1< #checkForPendingTransactionAndStartPolling = () => { // PendingTransactionTracker reads state through its getTransactions hook this.pendingTransactionTracker.startIfPendingTransactions(); - if (this.#enableMultichain) { + if (this.#isMultichainEnabled) { for (const [, trackers] of this.trackingMap) { trackers.pendingTransactionTracker.startIfPendingTransactions(); } @@ -3090,7 +3083,7 @@ export class TransactionController extends BaseControllerV1< this.incomingTransactionHelper.stop(); this.#removeIncomingTransactionHelperListeners(); - if (this.#enableMultichain) { + if (this.#isMultichainEnabled) { for (const [networkClientId] of this.trackingMap) { this.#stopTrackingByNetworkClientId(networkClientId); } diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index c5d7daf846..3949f62b81 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -128,7 +128,7 @@ const newController = async (options: any = {}) => { getNetworkState: () => networkController.state, getSelectedAddress: () => '0xdeadbeef', getPermittedAccounts: () => [ACCOUNT_MOCK], - enableMultichain: true, + isMultichainEnabled: true, ...opts, }, { @@ -938,7 +938,7 @@ describe('TransactionController Integration', () => { }); const { networkController, transactionController } = await newController({ - enableMultichain: false, + isMultichainEnabled: false, }); const configurationId = @@ -984,7 +984,7 @@ describe('TransactionController Integration', () => { }); const { networkController, transactionController } = await newController({ - enableMultichain: false, + isMultichainEnabled: false, getNetworkClientRegistrySpy, }); @@ -1010,7 +1010,7 @@ describe('TransactionController Integration', () => { }); const { networkController, transactionController } = await newController({ - enableMultichain: true, + isMultichainEnabled: true, getNetworkClientRegistrySpy, }); @@ -1036,7 +1036,7 @@ describe('TransactionController Integration', () => { }); await newController({ - enableMultichain: true, + isMultichainEnabled: true, getNetworkClientRegistrySpy, }); From 9f98280e5dfdd4056572a4780c8615144a050bf7 Mon Sep 17 00:00:00 2001 From: jiexi Date: Mon, 5 Feb 2024 13:03:51 -0800 Subject: [PATCH 094/100] Update packages/transaction-controller/src/TransactionController.ts Co-authored-by: Elliot Winkler --- packages/transaction-controller/src/TransactionController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 6c35c69bc6..96b4c7f148 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -1659,7 +1659,7 @@ export class TransactionController extends BaseControllerV1< if (!trackers) { throw new Error('missing nonceTracker for networkClientId'); } - nonceTracker = trackers?.nonceTracker; + nonceTracker = trackers.nonceTracker; } // Acquires the lock for the chainId + address and the nonceLock from the nonceTracker, then From 113db8b871745c0893ea02cba8ecf96ffebca284 Mon Sep 17 00:00:00 2001 From: jiexi Date: Mon, 5 Feb 2024 14:11:27 -0800 Subject: [PATCH 095/100] log failed promises in updateIncomingTransactions (#3893) ## Explanation * Log failed promises in `updateIncomingTransactions()` ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- .../src/TransactionController.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 96b4c7f148..b9700177ea 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -881,13 +881,22 @@ export class TransactionController extends BaseControllerV1< return; } - await Promise.allSettled( + const promises = await Promise.allSettled( networkClientIds.map(async (networkClientId) => { return await this.trackingMap .get(networkClientId) ?.incomingTransactionHelper.update(); }), ); + + promises + .filter((result) => result.status === 'rejected') + .forEach((result) => { + log( + 'failed to update incoming transactions', + (result as PromiseRejectedResult).reason, + ); + }); } /** From 0c5b093fb68ed99a363631bb41a180c50349aa49 Mon Sep 17 00:00:00 2001 From: jiexi Date: Mon, 5 Feb 2024 15:05:41 -0800 Subject: [PATCH 096/100] Jl/transaction multichain todo specs (#3894) ## Explanation * Add `markNonceDuplicatesDropped` side effect checks to cancelled and speed up scenarios * Add global network scenarios for `startIncomingTransactionPolling`, `stopIncomingTransactionPolling` and `updateIncomingTransactions` ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- .../TransactionControllerIntegration.test.ts | 165 ++++++++++++++++-- 1 file changed, 151 insertions(+), 14 deletions(-) diff --git a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts index 3949f62b81..bffbb78e4a 100644 --- a/packages/transaction-controller/src/TransactionControllerIntegration.test.ts +++ b/packages/transaction-controller/src/TransactionControllerIntegration.test.ts @@ -532,7 +532,7 @@ describe('TransactionController Integration', () => { ); transactionController.destroy(); }); - it('should be able to confirm a cancelled transaction', async () => { + it('should be able to confirm a cancelled transaction and drop the original transaction', async () => { mockNetwork({ networkClientConfiguration: buildInfuraNetworkClientConfiguration( InfuraNetworkType.goerli, @@ -594,12 +594,15 @@ describe('TransactionController Integration', () => { await advanceTime({ clock, duration: 1 }); expect(transactionController.state.transactions).toHaveLength(2); + expect(transactionController.state.transactions[0].status).toBe( + 'dropped', + ); expect(transactionController.state.transactions[1].status).toBe( 'confirmed', ); transactionController.destroy(); }); - it('should be able to get to speedup state', async () => { + it('should be able to get to speedup state and drop the original transaction', async () => { mockNetwork({ networkClientConfiguration: buildInfuraNetworkClientConfiguration( InfuraNetworkType.goerli, @@ -662,6 +665,9 @@ describe('TransactionController Integration', () => { await advanceTime({ clock, duration: 1 }); expect(transactionController.state.transactions).toHaveLength(2); + expect(transactionController.state.transactions[0].status).toBe( + 'dropped', + ); expect(transactionController.state.transactions[1].status).toBe( 'confirmed', ); @@ -844,7 +850,6 @@ describe('TransactionController Integration', () => { transactionController.destroy(); }); }); - it.todo('markNonceDuplicatesDropped'); }); describe('when changing rpcUrl of networkClient', () => { @@ -1123,10 +1128,61 @@ describe('TransactionController Integration', () => { transactionController.destroy(); }); - // Unclear if we need this test - it.todo( - 'should start the global incoming transaction helper when no networkClientIds provided', - ); + it('should start the global incoming transaction helper when no networkClientIds provided', async () => { + const selectedAddress = ETHERSCAN_TRANSACTION_BASE_MOCK.to; + + mockNetwork({ + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), + mocks: [ + buildEthBlockNumberRequestMock('0x1'), + buildEthBlockNumberRequestMock('0x2'), + ], + }); + nock(getEtherscanApiHost(BUILT_IN_NETWORKS[NetworkType.mainnet].chainId)) + .get( + `/api?module=account&address=${selectedAddress}&offset=40&sort=desc&action=txlist&tag=latest&page=1`, + ) + .reply(200, ETHERSCAN_TRANSACTION_RESPONSE_MOCK); + + const { transactionController } = await newController({ + getSelectedAddress: () => selectedAddress, + }); + + transactionController.startIncomingTransactionPolling(); + + await advanceTime({ clock, duration: BLOCK_TRACKER_POLLING_INTERVAL }); + + expect(transactionController.state.transactions).toHaveLength(2); + expect(transactionController.state.transactions).toStrictEqual( + expect.arrayContaining([ + expect.objectContaining({ + blockNumber: ETHERSCAN_TRANSACTION_BASE_MOCK.blockNumber, + chainId: '0x1', + type: TransactionType.incoming, + verifiedOnBlockchain: false, + status: TransactionStatus.confirmed, + }), + expect.objectContaining({ + blockNumber: ETHERSCAN_TRANSACTION_BASE_MOCK.blockNumber, + chainId: '0x1', + type: TransactionType.incoming, + verifiedOnBlockchain: false, + status: TransactionStatus.failed, + }), + ]), + ); + expect(transactionController.state.lastFetchedBlockNumbers).toStrictEqual( + { + [`0x1#${selectedAddress}#normal`]: parseInt( + ETHERSCAN_TRANSACTION_BASE_MOCK.blockNumber, + 10, + ), + }, + ); + transactionController.destroy(); + }); describe('when called with multiple networkClients which share the same chainId', () => { it('should only call the etherscan API max every 5 seconds, alternating between the token and txlist endpoints', async () => { @@ -1272,10 +1328,6 @@ describe('TransactionController Integration', () => { }); describe('stopIncomingTransactionPolling', () => { - // Unclear if we need this test - it.todo( - 'should stop the global incoming transaction helper when no networkClientIds provided', - ); it('should not poll for new incoming transactions for the given networkClientId', async () => { const selectedAddress = ETHERSCAN_TRANSACTION_BASE_MOCK.to; @@ -1318,6 +1370,40 @@ describe('TransactionController Integration', () => { ); transactionController.destroy(); }); + + it('should stop the global incoming transaction helper when no networkClientIds provided', async () => { + const selectedAddress = ETHERSCAN_TRANSACTION_BASE_MOCK.to; + + const { transactionController } = await newController({ + getSelectedAddress: () => selectedAddress, + }); + + mockNetwork({ + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), + mocks: [ + buildEthBlockNumberRequestMock('0x1'), + buildEthBlockNumberRequestMock('0x2'), + ], + }); + nock(getEtherscanApiHost(BUILT_IN_NETWORKS[NetworkType.mainnet].chainId)) + .get( + `/api?module=account&address=${selectedAddress}&offset=40&sort=desc&action=txlist&tag=latest&page=1`, + ) + .reply(200, ETHERSCAN_TRANSACTION_RESPONSE_MOCK); + + transactionController.startIncomingTransactionPolling(); + + transactionController.stopIncomingTransactionPolling(); + await advanceTime({ clock, duration: BLOCK_TRACKER_POLLING_INTERVAL }); + + expect(transactionController.state.transactions).toStrictEqual([]); + expect(transactionController.state.lastFetchedBlockNumbers).toStrictEqual( + {}, + ); + transactionController.destroy(); + }); }); describe('stopAllIncomingTransactionPolling', () => { @@ -1427,9 +1513,60 @@ describe('TransactionController Integration', () => { ); transactionController.destroy(); }); - it.todo( - 'should update the incoming transactions for the gloablly selected network when no networkClientIds provided', - ); + + it('should update the incoming transactions for the gloablly selected network when no networkClientIds provided', async () => { + const selectedAddress = ETHERSCAN_TRANSACTION_BASE_MOCK.to; + + const { transactionController } = await newController({ + getSelectedAddress: () => selectedAddress, + }); + + mockNetwork({ + networkClientConfiguration: buildInfuraNetworkClientConfiguration( + InfuraNetworkType.mainnet, + ), + mocks: [buildEthBlockNumberRequestMock('0x1')], + }); + nock(getEtherscanApiHost(BUILT_IN_NETWORKS[NetworkType.mainnet].chainId)) + .get( + `/api?module=account&address=${selectedAddress}&offset=40&sort=desc&action=txlist&tag=latest&page=1`, + ) + .reply(200, ETHERSCAN_TRANSACTION_RESPONSE_MOCK); + + transactionController.updateIncomingTransactions(); + + // we have to wait for the mutex to be released after the 5 second API rate limit timer + await advanceTime({ clock, duration: 1 }); + + expect(transactionController.state.transactions).toHaveLength(2); + expect(transactionController.state.transactions).toStrictEqual( + expect.arrayContaining([ + expect.objectContaining({ + blockNumber: ETHERSCAN_TRANSACTION_BASE_MOCK.blockNumber, + chainId: '0x1', + type: TransactionType.incoming, + verifiedOnBlockchain: false, + status: TransactionStatus.confirmed, + }), + expect.objectContaining({ + blockNumber: ETHERSCAN_TRANSACTION_BASE_MOCK.blockNumber, + chainId: '0x1', + type: TransactionType.incoming, + verifiedOnBlockchain: false, + status: TransactionStatus.failed, + }), + ]), + ); + expect(transactionController.state.lastFetchedBlockNumbers).toStrictEqual( + { + [`0x1#${selectedAddress}#normal`]: parseInt( + ETHERSCAN_TRANSACTION_BASE_MOCK.blockNumber, + 10, + ), + }, + ); + transactionController.destroy(); + }); }); describe('getNonceLock', () => { From e07e01ac69b0f8dd0531f31281eafe97d6b0b8ad Mon Sep 17 00:00:00 2001 From: jiexi Date: Mon, 5 Feb 2024 15:19:18 -0800 Subject: [PATCH 097/100] Add TransactionControllerOptions (#3895) ## Explanation * Add and export `TransactionControllerOptions` type * Export existing option types ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- .../src/TransactionController.ts | 164 +++++++++--------- 1 file changed, 81 insertions(+), 83 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index b9700177ea..e6eb93970b 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -196,7 +196,7 @@ export const SPEED_UP_RATE = 1.1; * @property queryEntireHistory - Whether to initially query the entire transaction history or only recent blocks. * @property updateTransactions - Whether to update local transactions using remote transaction data. */ -type IncomingTransactionOptions = { +export type IncomingTransactionOptions = { includeTokenTransfers?: boolean; isEnabled?: () => boolean; queryEntireHistory?: boolean; @@ -208,14 +208,91 @@ type IncomingTransactionOptions = { * * @property isResubmitEnabled - Whether transaction publishing is automatically retried. */ -type PendingTransactionOptions = { +export type PendingTransactionOptions = { isResubmitEnabled?: boolean; }; /** - * Registry of network clients provided by the NetworkController + * TransactionController constructor options. + * + * @property blockTracker - The block tracker used to poll for new blocks data. + * @property cancelMultiplier - Multiplier used to determine a transaction's increased gas fee during cancellation. + * @property disableHistory - Whether to disable storing history in transaction metadata. + * @property disableSendFlowHistory - Explicitly disable transaction metadata history. + * @property disableSwaps - Whether to disable additional processing on swaps transactions. + * @property isMultichainEnabled - Enable multichain support. + * @property getCurrentAccountEIP1559Compatibility - Whether or not the account supports EIP-1559. + * @property getCurrentNetworkEIP1559Compatibility - Whether or not the network supports EIP-1559. + * @property getExternalPendingTransactions - Callback to retrieve pending transactions from external sources. + * @property getGasFeeEstimates - Callback to retrieve gas fee estimates. + * @property getNetworkClientRegistry - Gets the network client registry. + * @property getNetworkState - Gets the state of the network controller. + * @property getPermittedAccounts - Get accounts that a given origin has permissions for. + * @property getSavedGasFees - Gets the saved gas fee config. + * @property getSelectedAddress - Gets the address of the currently selected account. + * @property incomingTransactions - Configuration options for incoming transaction support. + * @property messenger - The controller messenger. + * @property onNetworkStateChange - Allows subscribing to network controller state changes. + * @property pendingTransactions - Configuration options for pending transaction support. + * @property provider - The provider used to create the underlying EthQuery instance. + * @property securityProviderRequest - A function for verifying a transaction, whether it is malicious or not. + * @property speedUpMultiplier - Multiplier used to determine a transaction's increased gas fee during speed up. + * @property hooks - The controller hooks. + * @property hooks.afterSign - Additional logic to execute after signing a transaction. Return false to not change the status to signed. + * @property hooks.beforeApproveOnInit - Additional logic to execute before starting an approval flow for a transaction during initialization. Return false to skip the transaction. + * @property hooks.beforeCheckPendingTransaction - Additional logic to execute before checking pending transactions. Return false to prevent the broadcast of the transaction. + * @property hooks.beforePublish - Additional logic to execute before publishing a transaction. Return false to prevent the broadcast of the transaction. + * @property hooks.getAdditionalSignArguments - Returns additional arguments required to sign a transaction. + * @property hooks.publish - Alternate logic to publish a transaction. */ +export type TransactionControllerOptions = { + blockTracker: BlockTracker; + cancelMultiplier?: number; + disableHistory: boolean; + disableSendFlowHistory: boolean; + disableSwaps: boolean; + getCurrentAccountEIP1559Compatibility?: () => Promise; + getCurrentNetworkEIP1559Compatibility: () => Promise; + getExternalPendingTransactions?: ( + address: string, + chainId?: string, + ) => NonceTrackerTransaction[]; + getGasFeeEstimates?: () => Promise; + getNetworkState: () => NetworkState; + getPermittedAccounts: (origin?: string) => Promise; + getSavedGasFees?: (chainId: Hex) => SavedGasFees | undefined; + getSelectedAddress: () => string; + incomingTransactions?: IncomingTransactionOptions; + messenger: TransactionControllerMessenger; + onNetworkStateChange: (listener: (state: NetworkState) => void) => void; + pendingTransactions?: PendingTransactionOptions; + provider: Provider; + securityProviderRequest?: SecurityProviderRequest; + speedUpMultiplier?: number; + getNetworkClientRegistry: NetworkController['getNetworkClientRegistry']; + isMultichainEnabled: boolean; + hooks: { + afterSign?: ( + transactionMeta: TransactionMeta, + signedTx: TypedTransaction, + ) => boolean; + beforeApproveOnInit?: (transactionMeta: TransactionMeta) => boolean; + beforeCheckPendingTransaction?: ( + transactionMeta: TransactionMeta, + ) => boolean; + beforePublish?: (transactionMeta: TransactionMeta) => boolean; + getAdditionalSignArguments?: ( + transactionMeta: TransactionMeta, + ) => (TransactionMeta | undefined)[]; + publish?: ( + transactionMeta: TransactionMeta, + ) => Promise<{ transactionHash: string }>; + }; +}; +/** + * Registry of network clients provided by the NetworkController + */ type NetworkClientRegistry = ReturnType< NetworkController['getNetworkClientRegistry'] >; @@ -416,42 +493,6 @@ export class TransactionController extends BaseControllerV1< transactionMeta?: TransactionMeta, ) => Promise; - /** - * Creates a TransactionController instance. - * - * @param options - The controller options. - * @param options.blockTracker - The block tracker used to poll for new blocks data. - * @param options.cancelMultiplier - Multiplier used to determine a transaction's increased gas fee during cancellation. - * @param options.disableHistory - Whether to disable storing history in transaction metadata. - * @param options.disableSendFlowHistory - Explicitly disable transaction metadata history. - * @param options.disableSwaps - Whether to disable additional processing on swaps transactions. - * @param options.isMultichainEnabled - Enable multichain support. - * @param options.getCurrentAccountEIP1559Compatibility - Whether or not the account supports EIP-1559. - * @param options.getCurrentNetworkEIP1559Compatibility - Whether or not the network supports EIP-1559. - * @param options.getExternalPendingTransactions - Callback to retrieve pending transactions from external sources. - * @param options.getGasFeeEstimates - Callback to retrieve gas fee estimates. - * @param options.getNetworkClientRegistry - Gets the network client registry. - * @param options.getNetworkState - Gets the state of the network controller. - * @param options.getPermittedAccounts - Get accounts that a given origin has permissions for. - * @param options.getSavedGasFees - Gets the saved gas fee config. - * @param options.getSelectedAddress - Gets the address of the currently selected account. - * @param options.incomingTransactions - Configuration options for incoming transaction support. - * @param options.messenger - The controller messenger. - * @param options.onNetworkStateChange - Allows subscribing to network controller state changes. - * @param options.pendingTransactions - Configuration options for pending transaction support. - * @param options.provider - The provider used to create the underlying EthQuery instance. - * @param options.securityProviderRequest - A function for verifying a transaction, whether it is malicious or not. - * @param options.speedUpMultiplier - Multiplier used to determine a transaction's increased gas fee during speed up. - * @param options.hooks - The controller hooks. - * @param options.hooks.afterSign - Additional logic to execute after signing a transaction. Return false to not change the status to signed. - * @param options.hooks.beforeApproveOnInit - Additional logic to execute before starting an approval flow for a transaction during initialization. Return false to skip the transaction. - * @param options.hooks.beforeCheckPendingTransaction - Additional logic to execute before checking pending transactions. Return false to prevent the broadcast of the transaction. - * @param options.hooks.beforePublish - Additional logic to execute before publishing a transaction. Return false to prevent the broadcast of the transaction. - * @param options.hooks.getAdditionalSignArguments - Returns additional arguments required to sign a transaction. - * @param options.hooks.publish - Alternate logic to publish a transaction. - * @param config - Initial options used to configure this controller. - * @param state - Initial state to set on this controller. - */ constructor( { blockTracker, @@ -477,50 +518,7 @@ export class TransactionController extends BaseControllerV1< getNetworkClientRegistry, isMultichainEnabled = false, hooks = {}, - }: { - blockTracker: BlockTracker; - cancelMultiplier?: number; - disableHistory: boolean; - disableSendFlowHistory: boolean; - disableSwaps: boolean; - getCurrentAccountEIP1559Compatibility?: () => Promise; - getCurrentNetworkEIP1559Compatibility: () => Promise; - getExternalPendingTransactions?: ( - address: string, - chainId?: string, - ) => NonceTrackerTransaction[]; - getGasFeeEstimates?: () => Promise; - getNetworkState: () => NetworkState; - getPermittedAccounts: (origin?: string) => Promise; - getSavedGasFees?: (chainId: Hex) => SavedGasFees | undefined; - getSelectedAddress: () => string; - incomingTransactions?: IncomingTransactionOptions; - messenger: TransactionControllerMessenger; - onNetworkStateChange: (listener: (state: NetworkState) => void) => void; - pendingTransactions?: PendingTransactionOptions; - provider: Provider; - securityProviderRequest?: SecurityProviderRequest; - speedUpMultiplier?: number; - getNetworkClientRegistry: NetworkController['getNetworkClientRegistry']; - isMultichainEnabled: boolean; - hooks: { - afterSign?: ( - transactionMeta: TransactionMeta, - signedTx: TypedTransaction, - ) => boolean; - beforeApproveOnInit?: (transactionMeta: TransactionMeta) => boolean; - beforeCheckPendingTransaction?: ( - transactionMeta: TransactionMeta, - ) => boolean; - beforePublish?: (transactionMeta: TransactionMeta) => boolean; - getAdditionalSignArguments?: ( - transactionMeta: TransactionMeta, - ) => (TransactionMeta | undefined)[]; - publish?: ( - transactionMeta: TransactionMeta, - ) => Promise<{ transactionHash: string }>; - }; - }, + }: TransactionControllerOptions, config?: Partial, state?: Partial, ) { From fae7acc825998875772413c04ff048a1459f7a9f Mon Sep 17 00:00:00 2001 From: jiexi Date: Wed, 7 Feb 2024 11:11:48 -0800 Subject: [PATCH 098/100] Jl/transaction multichain approveTransactionsWithSameNonce networkClientId resolution (#3898) ## Explanation Currently `approveTransactionsWithSameNonce` may need to populate the txParams it is passed with a nonce, but the current implementation only works correctly when calling this method while globally selected network is on the same chainId. To get this to work with multichain, we attempt to resolve the txParams chainId to a networkClientId which we then use to pull a NonceTracker on the right chain out of the trackingMap. One downside of this approach is that it's possible to use the NonceTracker for a different network client that's also on the same chain than the network client that this tx is being submitted on. I recommend reading my comment in the issue linked below for a more thorough explanation of current state and options. ## References See: https://github.com/MetaMask/MetaMask-planning/issues/2023#issuecomment-1930555110 ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --- .../src/TransactionController.test.ts | 52 ++++++++++++++++++- .../src/TransactionController.ts | 17 +++++- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index c455de8223..812d89805b 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -637,11 +637,28 @@ describe('TransactionController', () => { } }); + const mockFindNetworkClientIdByChainId = jest + .fn() + .mockImplementation((chainId) => { + switch (chainId) { + case '0x1': + return 'mainnet'; + case ChainId.sepolia: + return 'sepolia'; + case ChainId.goerli: + return 'goerli'; + case '0xa': + return 'customNetworkClientId-1'; + default: + throw new Error("Couldn't find networkClientId for chainId"); + } + }); + ({ messenger: messengerMock, approve: approveTransaction } = buildMockMessenger({ addRequest: addRequestMockOptions, getNetworkClientById: mockGetNetworkClientById, - findNetworkClientIdByChainId: jest.fn(), + findNetworkClientIdByChainId: mockFindNetworkClientIdByChainId, })); return new TransactionController( @@ -3862,6 +3879,39 @@ describe('TransactionController', () => { expect(getNonceLockMock).not.toHaveBeenCalled(); }); + + it('uses the nonceTracker for the networkClientId matching the chainId', async () => { + const controller = newController({ + options: { isMultichainEnabled: true }, + }); + + const getNonceLockMock = jest.spyOn(controller, 'getNonceLock'); + + const mockTransactionParam = { + from: ACCOUNT_MOCK, + nonce: '0x1', + gas: '0x111', + to: ACCOUNT_2_MOCK, + value: '0x0', + chainId: MOCK_NETWORK.state.providerConfig.chainId, + }; + + const mockTransactionParam2 = { + from: ACCOUNT_MOCK, + nonce: '0x1', + gas: '0x222', + to: ACCOUNT_2_MOCK, + value: '0x1', + chainId: MOCK_NETWORK.state.providerConfig.chainId, + }; + + await controller.approveTransactionsWithSameNonce([ + mockTransactionParam, + mockTransactionParam2, + ]); + + expect(getNonceLockMock).toHaveBeenCalledWith(ACCOUNT_MOCK, 'goerli'); + }); }); describe('with hooks', () => { diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index e6eb93970b..952cf3cacf 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -1783,6 +1783,21 @@ export class TransactionController extends BaseControllerV1< const initialTx = listOfTxParams[0]; const common = this.getCommonConfiguration(initialTx.chainId); + // We need to ensure we get the nonce using the the NonceTracker on the chain matching + // the txParams. In this context we only have chainId available to us, but the + // NonceTrackers are keyed by networkClientId. To workaround this, we attempt to find + // a networkClientId that matches the chainId. As a fallback, the globally selected + // network's NonceTracker will be used instead. + let networkClientId: NetworkClientId | undefined; + try { + networkClientId = this.messagingSystem.call( + `NetworkController:findNetworkClientIdByChainId`, + initialTx.chainId, + ); + } catch (err) { + log('failed to find networkClientId from chainId', err); + } + const initialTxAsEthTx = TransactionFactory.fromTxData(initialTx, { common, }); @@ -1801,7 +1816,7 @@ export class TransactionController extends BaseControllerV1< const requiresNonce = hasNonce !== true; nonceLock = requiresNonce - ? await this.getNonceLock(fromAddress) + ? await this.getNonceLock(fromAddress, networkClientId) : undefined; const nonce = nonceLock From 7a3a42997ae9a178267bc247d0a24fe2236693d4 Mon Sep 17 00:00:00 2001 From: jiexi Date: Tue, 13 Feb 2024 07:36:49 -0800 Subject: [PATCH 099/100] Add MultichainTrackingHelper (#3896) ## Explanation Example of what the MultichainHelper would look like. There is tight coupling between the TransactionController and the MultichainHelper due to the dependencies of instantiating the IncomingTxHelper, PendingTxHelper, and nonceTracker. Even if the global trackers/helpers are also pushed into the MultichainHelper, many of TransactionControllers methods need to passed into MultichainHelper. This also effectively makes MultichainHelper really the TrackingHelper. Note that the Integration tests still pass. ## References ## Changelog ### `@metamask/package-a` - ****: Your change here - ****: Your change here ### `@metamask/package-b` - ****: Your change here - ****: Your change here ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've highlighted breaking changes using the "BREAKING" category above as appropriate --------- Co-authored-by: Alex --- .../src/TransactionController.test.ts | 117 +-- .../src/TransactionController.ts | 561 ++++------- .../src/helpers/IncomingTransactionHelper.ts | 15 + .../helpers/MultichainTrackingHelper.test.ts | 869 ++++++++++++++++++ .../src/helpers/MultichainTrackingHelper.ts | 454 +++++++++ packages/transaction-controller/src/types.ts | 3 - 6 files changed, 1539 insertions(+), 480 deletions(-) create mode 100644 packages/transaction-controller/src/helpers/MultichainTrackingHelper.test.ts create mode 100644 packages/transaction-controller/src/helpers/MultichainTrackingHelper.ts diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 812d89805b..d323154ea2 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -12,6 +12,7 @@ import { BUILT_IN_NETWORKS, ORIGIN_METAMASK, } from '@metamask/controller-utils'; +import EthQuery from '@metamask/eth-query'; import HttpProvider from '@metamask/ethjs-provider-http'; import type { BlockTracker, @@ -29,6 +30,7 @@ import { FakeBlockTracker } from '../../../tests/fake-block-tracker'; import { flushPromises } from '../../../tests/helpers'; import { mockNetwork } from '../../../tests/mock-network'; import { IncomingTransactionHelper } from './helpers/IncomingTransactionHelper'; +import { MultichainTrackingHelper } from './helpers/MultichainTrackingHelper'; import { PendingTransactionTracker } from './helpers/PendingTransactionTracker'; import type { TransactionControllerMessenger, @@ -199,6 +201,7 @@ jest.mock('@metamask/eth-query', () => jest.mock('./helpers/IncomingTransactionHelper'); jest.mock('./helpers/PendingTransactionTracker'); +jest.mock('./helpers/MultichainTrackingHelper'); /** * Builds a mock block tracker with a canned block number that can be used in @@ -532,8 +535,9 @@ describe('TransactionController', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any let approveTransaction: (value?: any) => void; let getNonceLockSpy: jest.Mock; - let incomingTransactionHelperMocks: jest.Mocked[]; - let pendingTransactionTrackerMocks: jest.Mocked[]; + let incomingTransactionHelperMock: jest.Mocked; + let pendingTransactionTrackerMock: jest.Mocked; + let multichainTrackingHelperMock: jest.Mocked; let timeCounter = 0; const incomingTransactionHelperClassMock = @@ -546,6 +550,11 @@ describe('TransactionController', () => { typeof PendingTransactionTracker >; + const multichainTrackingHelperClassMock = + MultichainTrackingHelper as jest.MockedClass< + typeof MultichainTrackingHelper + >; + /** * Create a new instance of the TransactionController. * @@ -670,28 +679,7 @@ describe('TransactionController', () => { getGasFeeEstimates: () => Promise.resolve({}), getPermittedAccounts: () => [ACCOUNT_MOCK], getSelectedAddress: () => ACCOUNT_MOCK, - getNetworkClientRegistry: () => ({ - mainnet: { - configuration: { - chainId: toHex(1), - }, - }, - sepolia: { - configuration: { - chainId: ChainId.sepolia, - }, - }, - goerli: { - configuration: { - chainId: ChainId.goerli, - }, - }, - 'customNetworkClientId-1': { - configuration: { - chainId: '0xa', - }, - }, - }), + getNetworkClientRegistry: jest.fn(), messenger: messengerMock, onNetworkStateChange: finalNetwork.subscribe, provider: finalNetwork.provider, @@ -731,11 +719,8 @@ describe('TransactionController', () => { releaseLock: () => Promise.resolve(), }); - NonceTrackerPackage.NonceTracker.prototype.getNonceLock = getNonceLockSpy; - - incomingTransactionHelperMocks = []; incomingTransactionHelperClassMock.mockImplementation(() => { - const incomingTransactionHelperMock = { + incomingTransactionHelperMock = { start: jest.fn(), stop: jest.fn(), update: jest.fn(), @@ -744,13 +729,11 @@ describe('TransactionController', () => { removeAllListeners: jest.fn(), }, } as unknown as jest.Mocked; - incomingTransactionHelperMocks.push(incomingTransactionHelperMock); return incomingTransactionHelperMock; }); - pendingTransactionTrackerMocks = []; pendingTransactionTrackerClassMock.mockImplementation(() => { - const pendingTransactionTrackerMock = { + pendingTransactionTrackerMock = { start: jest.fn(), stop: jest.fn(), startIfPendingTransactions: jest.fn(), @@ -760,10 +743,21 @@ describe('TransactionController', () => { }, onStateChange: jest.fn(), } as unknown as jest.Mocked; - - pendingTransactionTrackerMocks.push(pendingTransactionTrackerMock); return pendingTransactionTrackerMock; }); + + multichainTrackingHelperClassMock.mockImplementation(({ provider }) => { + multichainTrackingHelperMock = { + getEthQuery: jest.fn().mockImplementation(() => { + return new EthQuery(provider); + }), + checkForPendingTransactionAndStartPolling: jest.fn(), + getNonceLock: getNonceLockSpy, + initialize: jest.fn(), + has: jest.fn().mockReturnValue(false), + } as unknown as jest.Mocked; + return multichainTrackingHelperMock; + }); }); afterEach(() => { @@ -1225,7 +1219,7 @@ describe('TransactionController', () => { ); }); - describe('when isMultichainEnabled: true is specified', () => { + describe('networkClientId exists in the MultichainTrackingHelper', () => { it('adds unapproved transaction to state when using networkClientId', async () => { const controller = newController({ options: { isMultichainEnabled: true }, @@ -1236,6 +1230,8 @@ describe('TransactionController', () => { to: ACCOUNT_2_MOCK, }; + multichainTrackingHelperMock.has.mockReturnValue(true); + await controller.addTransaction(sepoliaTxParams, { origin: 'metamask', actionId: ACTION_ID_MOCK, @@ -1257,6 +1253,9 @@ describe('TransactionController', () => { approve: true, options: { isMultichainEnabled: true }, }); + + multichainTrackingHelperMock.has.mockReturnValue(true); + const submittedEventListener = jest.fn(); controller.hub.on('transaction-submitted', submittedEventListener); @@ -1454,12 +1453,10 @@ describe('TransactionController', () => { const firstTransaction = controller.state.transactions[0]; // eslint-disable-next-line jest/prefer-spy-on - NonceTrackerPackage.NonceTracker.prototype.getNonceLock = jest - .fn() - .mockResolvedValue({ - nextNonce: NONCE_MOCK + 1, - releaseLock: () => Promise.resolve(), - }); + multichainTrackingHelperMock.getNonceLock = jest.fn().mockResolvedValue({ + nextNonce: NONCE_MOCK + 1, + releaseLock: () => Promise.resolve(), + }); const { result: secondResult } = await controller.addTransaction({ from: ACCOUNT_MOCK, @@ -2540,20 +2537,6 @@ describe('TransactionController', () => { }); }); - describe('getNonceLock', () => { - it('gets the next nonce from the globally selected nonceTracker when no networkClientId is provided', async () => { - const controller = newController({ - network: MOCK_LINEA_MAINNET_NETWORK, - }); - - const { nextNonce } = await controller.getNonceLock(ACCOUNT_MOCK); - - expect(getNonceLockSpy).toHaveBeenCalledTimes(1); - expect(getNonceLockSpy).toHaveBeenCalledWith(ACCOUNT_MOCK); - expect(nextNonce).toBe(NONCE_MOCK); - }); - }); - describe('confirmExternalTransaction', () => { it('adds external transaction to the state as confirmed', async () => { const controller = newController(); @@ -3196,7 +3179,7 @@ describe('TransactionController', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (incomingTransactionHelperMocks[0].hub.on as any).mock.calls[0][1]({ + await (incomingTransactionHelperMock.hub.on as any).mock.calls[0][1]({ added: [TRANSACTION_META_MOCK, TRANSACTION_META_2_MOCK], updated: [], }); @@ -3222,7 +3205,7 @@ describe('TransactionController', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (incomingTransactionHelperMocks[0].hub.on as any).mock.calls[0][1]({ + await (incomingTransactionHelperMock.hub.on as any).mock.calls[0][1]({ added: [], updated: [updatedTransaction], }); @@ -3238,7 +3221,7 @@ describe('TransactionController', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (incomingTransactionHelperMocks[0].hub.on as any).mock.calls[0][1]({ + await (incomingTransactionHelperMock.hub.on as any).mock.calls[0][1]({ added: [TRANSACTION_META_MOCK, TRANSACTION_META_2_MOCK], updated: [], }); @@ -3259,7 +3242,7 @@ describe('TransactionController', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (incomingTransactionHelperMocks[0].hub.on as any).mock.calls[1][1]({ + await (incomingTransactionHelperMock.hub.on as any).mock.calls[1][1]({ lastFetchedBlockNumbers, blockNumber: 123, }); @@ -3278,7 +3261,7 @@ describe('TransactionController', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (incomingTransactionHelperMocks[0].hub.on as any).mock.calls[1][1]({ + await (incomingTransactionHelperMock.hub.on as any).mock.calls[1][1]({ lastFetchedBlockNumbers: { key: 234, }, @@ -3497,7 +3480,7 @@ describe('TransactionController', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any ...args: any ) { - (pendingTransactionTrackerMocks[0].hub.on as jest.Mock).mock.calls.find( + (pendingTransactionTrackerMock.hub.on as jest.Mock).mock.calls.find( (call) => call[0] === eventName, )[1](...args); } @@ -3848,10 +3831,6 @@ describe('TransactionController', () => { }); it('does not create nonce lock if hasNonce set', async () => { - const getNonceLockMock = jest - .spyOn(NonceTrackerPackage.NonceTracker.prototype, 'getNonceLock') - .mockImplementation(); - const controller = newController(); const mockTransactionParam = { @@ -3877,15 +3856,11 @@ describe('TransactionController', () => { { hasNonce: true }, ); - expect(getNonceLockMock).not.toHaveBeenCalled(); + expect(getNonceLockSpy).not.toHaveBeenCalled(); }); it('uses the nonceTracker for the networkClientId matching the chainId', async () => { - const controller = newController({ - options: { isMultichainEnabled: true }, - }); - - const getNonceLockMock = jest.spyOn(controller, 'getNonceLock'); + const controller = newController(); const mockTransactionParam = { from: ACCOUNT_MOCK, @@ -3910,7 +3885,7 @@ describe('TransactionController', () => { mockTransactionParam2, ]); - expect(getNonceLockMock).toHaveBeenCalledWith(ACCOUNT_MOCK, 'goerli'); + expect(getNonceLockSpy).toHaveBeenCalledWith(ACCOUNT_MOCK, 'goerli'); }); }); diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 952cf3cacf..ae586b501c 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -28,7 +28,6 @@ import type { NetworkControllerStateChangeEvent, NetworkState, Provider, - NetworkClient, NetworkControllerFindNetworkClientIdByChainIdAction, NetworkControllerGetNetworkClientByIdAction, } from '@metamask/network-controller'; @@ -48,7 +47,9 @@ import type { import { v1 as random } from 'uuid'; import { EtherscanRemoteTransactionSource } from './helpers/EtherscanRemoteTransactionSource'; +import type { IncomingTransactionOptions } from './helpers/IncomingTransactionHelper'; import { IncomingTransactionHelper } from './helpers/IncomingTransactionHelper'; +import { MultichainTrackingHelper } from './helpers/MultichainTrackingHelper'; import { PendingTransactionTracker } from './helpers/PendingTransactionTracker'; import { projectLogger as log } from './logger'; import type { @@ -188,21 +189,6 @@ export const CANCEL_RATE = 1.5; */ export const SPEED_UP_RATE = 1.1; -/** - * Configuration options for the IncomingTransactionHelper - * - * @property includeTokenTransfers - Whether or not to include ERC20 token transfers. - * @property isEnabled - Whether or not incoming transaction retrieval is enabled. - * @property queryEntireHistory - Whether to initially query the entire transaction history or only recent blocks. - * @property updateTransactions - Whether to update local transactions using remote transaction data. - */ -export type IncomingTransactionOptions = { - includeTokenTransfers?: boolean; - isEnabled?: () => boolean; - queryEntireHistory?: boolean; - updateTransactions?: boolean; -}; - /** * Configuration options for the PendingTransactionTracker * @@ -290,13 +276,6 @@ export type TransactionControllerOptions = { }; }; -/** - * Registry of network clients provided by the NetworkController - */ -type NetworkClientRegistry = ReturnType< - NetworkController['getNetworkClientRegistry'] ->; - /** * The name of the {@link TransactionController}. */ @@ -342,8 +321,6 @@ export class TransactionController extends BaseControllerV1< TransactionConfig, TransactionState > { - private readonly ethQuery: EthQuery; - private readonly isHistoryDisabled: boolean; private readonly isSwapsDisabled: boolean; @@ -358,12 +335,8 @@ export class TransactionController extends BaseControllerV1< // eslint-disable-next-line @typescript-eslint/no-explicit-any private readonly registry: any; - private readonly provider: Provider; - private readonly mutex = new Mutex(); - private readonly nonceMutexesByChainId = new Map>(); - private readonly getSavedGasFees: (chainId: Hex) => SavedGasFees | undefined; private readonly getNetworkState: () => NetworkState; @@ -385,8 +358,6 @@ export class TransactionController extends BaseControllerV1< chainId?: string, ) => NonceTrackerTransaction[]; - #isMultichainEnabled: boolean; - private readonly messagingSystem: TransactionControllerMessenger; readonly #incomingTransactionOptions: IncomingTransactionOptions; @@ -429,8 +400,6 @@ export class TransactionController extends BaseControllerV1< transactionMeta: TransactionMeta, ) => (TransactionMeta | undefined)[]; - readonly #getNetworkClientRegistry: NetworkController['getNetworkClientRegistry']; - private failTransaction( transactionMeta: TransactionMeta, error: Error, @@ -460,6 +429,8 @@ export class TransactionController extends BaseControllerV1< return { registryMethod, parsedRegistryMethod }; } + #multichainTrackingHelper: MultichainTrackingHelper; + /** * EventEmitter instance used to listen to specific transactional events */ @@ -470,20 +441,6 @@ export class TransactionController extends BaseControllerV1< */ override name = 'TransactionController'; - private readonly trackingMap: Map< - NetworkClientId, - { - nonceTracker: NonceTracker; - pendingTransactionTracker: PendingTransactionTracker; - incomingTransactionHelper: IncomingTransactionHelper; - } - > = new Map(); - - readonly etherscanRemoteTransactionSourcesMap: Map< - Hex, - EtherscanRemoteTransactionSource - > = new Map(); - /** * Method used to sign transactions */ @@ -534,12 +491,8 @@ export class TransactionController extends BaseControllerV1< lastFetchedBlockNumbers: {}, }; this.initialize(); - this.#isMultichainEnabled = isMultichainEnabled; - this.#getNetworkClientRegistry = getNetworkClientRegistry; - this.provider = provider; this.messagingSystem = messenger; this.getNetworkState = getNetworkState; - this.ethQuery = new EthQuery(provider); this.isSendFlowHistoryDisabled = disableSendFlowHistory ?? false; this.isHistoryDisabled = disableHistory ?? false; this.isSwapsDisabled = disableSwaps ?? false; @@ -574,60 +527,62 @@ export class TransactionController extends BaseControllerV1< this.publish = hooks?.publish ?? (() => Promise.resolve({ transactionHash: undefined })); - this.nonceTracker = new NonceTracker({ - // @ts-expect-error provider types misaligned: SafeEventEmitterProvider vs Record + this.nonceTracker = this.#createNonceTracker({ provider, blockTracker, - getPendingTransactions: this.#getNonceTrackerPendingTransactions.bind( - this, - undefined, - ), - getConfirmedTransactions: this.getNonceTrackerTransactions.bind( - this, - TransactionStatus.confirmed, - ), }); + this.#multichainTrackingHelper = new MultichainTrackingHelper({ + isMultichainEnabled, + provider, + nonceTracker: this.nonceTracker, + incomingTransactionOptions: incomingTransactions, + findNetworkClientIdByChainId: (chainId: Hex) => { + return this.messagingSystem.call( + `NetworkController:findNetworkClientIdByChainId`, + chainId, + ); + }, + getNetworkClientById: ((networkClientId: NetworkClientId) => { + return this.messagingSystem.call( + `NetworkController:getNetworkClientById`, + networkClientId, + ); + }) as NetworkController['getNetworkClientById'], + getNetworkClientRegistry, + removeIncomingTransactionHelperListeners: + this.#removeIncomingTransactionHelperListeners.bind(this), + removePendingTransactionTrackerListeners: + this.#removePendingTransactionTrackerListeners.bind(this), + createNonceTracker: this.#createNonceTracker.bind(this), + createIncomingTransactionHelper: + this.#createIncomingTransactionHelper.bind(this), + createPendingTransactionTracker: + this.#createPendingTransactionTracker.bind(this), + onNetworkStateChange: (listener) => { + this.messagingSystem.subscribe( + 'NetworkController:stateChange', + listener, + ); + }, + }); + this.#multichainTrackingHelper.initialize(); + const etherscanRemoteTransactionSource = new EtherscanRemoteTransactionSource({ includeTokenTransfers: incomingTransactions.includeTokenTransfers, }); - this.incomingTransactionHelper = new IncomingTransactionHelper({ + this.incomingTransactionHelper = this.#createIncomingTransactionHelper({ blockTracker, - getCurrentAccount: getSelectedAddress, - getLastFetchedBlockNumbers: () => this.state.lastFetchedBlockNumbers, - getChainId: this.getChainId.bind(this), - isEnabled: incomingTransactions.isEnabled, - queryEntireHistory: incomingTransactions.queryEntireHistory, - remoteTransactionSource: etherscanRemoteTransactionSource, - transactionLimit: this.config.txHistoryLimit, - updateTransactions: incomingTransactions.updateTransactions, + etherscanRemoteTransactionSource, }); - this.#addIncomingTransactionHelperListeners(); - - this.pendingTransactionTracker = new PendingTransactionTracker({ - approveTransaction: this.approveTransaction.bind(this), + this.pendingTransactionTracker = this.#createPendingTransactionTracker({ + provider, blockTracker, - getChainId: this.getChainId.bind(this), - getEthQuery: () => this.ethQuery, - getTransactions: () => this.state.transactions, - isResubmitEnabled: pendingTransactions.isResubmitEnabled, - getGlobalLock: async () => - this.#acquireNonceLockForChainIdKey({ - chainId: this.getChainId(), - }), - publishTransaction: this.publishTransaction.bind(this), - hooks: { - beforeCheckPendingTransaction: - this.beforeCheckPendingTransaction.bind(this), - beforePublish: this.beforePublish.bind(this), - }, }); - this.#addPendingTransactionTrackerListeners(); - // when transactionsController state changes // check for pending transactions and start polling if there are any this.subscribe(this.#checkForPendingTransactionAndStartPolling); @@ -641,25 +596,6 @@ export class TransactionController extends BaseControllerV1< }); this.onBootCleanup(); - this.messagingSystem.subscribe( - 'NetworkController:stateChange', - (_, patches) => { - if (this.#isMultichainEnabled) { - const networkClients = this.#getNetworkClientRegistry(); - patches.forEach(({ op, path }) => { - if (op === 'remove' && path[0] === 'networkConfigurations') { - const networkClientId = path[1] as NetworkClientId; - delete networkClients[networkClientId]; - } - }); - - this.#refreshTrackingMap(networkClients); - } - }, - ); - if (this.#isMultichainEnabled) { - this.#initTrackingMap(); - } } /** @@ -748,7 +684,10 @@ export class TransactionController extends BaseControllerV1< log('Adding transaction', txParams); txParams = normalizeTxParams(txParams); - if (networkClientId && !this.trackingMap.has(networkClientId)) { + if ( + networkClientId && + !this.#multichainTrackingHelper.has(networkClientId) + ) { throw new Error( 'The networkClientId for this transaction could not be found', ); @@ -775,7 +714,10 @@ export class TransactionController extends BaseControllerV1< ); const chainId = this.getChainId(networkClientId); - const ethQuery = this.#getEthQuery({ networkClientId, chainId }); + const ethQuery = this.#multichainTrackingHelper.getEthQuery({ + networkClientId, + chainId, + }); const transactionType = type ?? (await determineTransactionType(txParams, ethQuery)).type; @@ -845,56 +787,38 @@ export class TransactionController extends BaseControllerV1< } startIncomingTransactionPolling(networkClientIds: NetworkClientId[] = []) { - if (networkClientIds.length === 0 || !this.#isMultichainEnabled) { + if (networkClientIds.length === 0) { this.incomingTransactionHelper.start(); return; } - networkClientIds.forEach((networkClientId) => { - this.trackingMap.get(networkClientId)?.incomingTransactionHelper.start(); - }); + this.#multichainTrackingHelper.startIncomingTransactionPolling( + networkClientIds, + ); } stopIncomingTransactionPolling(networkClientIds: NetworkClientId[] = []) { - if (networkClientIds.length === 0 || !this.#isMultichainEnabled) { + if (networkClientIds.length === 0) { this.incomingTransactionHelper.stop(); return; } - networkClientIds.forEach((networkClientId) => { - this.trackingMap.get(networkClientId)?.incomingTransactionHelper.stop(); - }); + this.#multichainTrackingHelper.stopIncomingTransactionPolling( + networkClientIds, + ); } stopAllIncomingTransactionPolling() { this.incomingTransactionHelper.stop(); - if (this.#isMultichainEnabled) { - for (const [, trackers] of this.trackingMap) { - trackers.incomingTransactionHelper.stop(); - } - } + this.#multichainTrackingHelper.stopAllIncomingTransactionPolling(); } async updateIncomingTransactions(networkClientIds: NetworkClientId[] = []) { - if (networkClientIds.length === 0 || !this.#isMultichainEnabled) { + if (networkClientIds.length === 0) { await this.incomingTransactionHelper.update(); return; } - - const promises = await Promise.allSettled( - networkClientIds.map(async (networkClientId) => { - return await this.trackingMap - .get(networkClientId) - ?.incomingTransactionHelper.update(); - }), + await this.#multichainTrackingHelper.updateIncomingTransactions( + networkClientIds, ); - - promises - .filter((result) => result.status === 'rejected') - .forEach((result) => { - log( - 'failed to update incoming transactions', - (result as PromiseRejectedResult).reason, - ); - }); } /** @@ -1025,7 +949,7 @@ export class TransactionController extends BaseControllerV1< txParams: newTxParams, }); - const ethQuery = this.#getEthQuery({ + const ethQuery = this.#multichainTrackingHelper.getEthQuery({ networkClientId: transactionMeta.networkClientId, chainId: transactionMeta.chainId, }); @@ -1185,7 +1109,7 @@ export class TransactionController extends BaseControllerV1< log('Submitting speed up transaction', { oldFee, newFee, txParams }); - const ethQuery = this.#getEthQuery({ + const ethQuery = this.#multichainTrackingHelper.getEthQuery({ networkClientId: transactionMeta.networkClientId, chainId: transactionMeta.chainId, }); @@ -1248,7 +1172,9 @@ export class TransactionController extends BaseControllerV1< transaction: TransactionParams, networkClientId?: NetworkClientId, ) { - const ethQuery = this.#getEthQuery({ networkClientId }); + const ethQuery = this.#multichainTrackingHelper.getEthQuery({ + networkClientId, + }); const { estimatedGas, simulationFails } = await estimateGas( transaction, ethQuery, @@ -1269,7 +1195,9 @@ export class TransactionController extends BaseControllerV1< multiplier: number, networkClientId?: NetworkClientId, ) { - const ethQuery = this.#getEthQuery({ networkClientId }); + const ethQuery = this.#multichainTrackingHelper.getEthQuery({ + networkClientId, + }); const { blockGasLimit, estimatedGas, simulationFails } = await estimateGas( transaction, ethQuery, @@ -1610,82 +1538,14 @@ export class TransactionController extends BaseControllerV1< return this.getTransaction(transactionId) as TransactionMeta; } - /** - * Gets the mutex intended to guard the nonceTracker for a particular chainId and key . - * - * @param opts - The options object. - * @param opts.chainId - The hex chainId. - * @param opts.key - The hex address (or constant) pertaining to the chainId - * @returns Mutex instance for the given chainId and key pair - */ - async #acquireNonceLockForChainIdKey({ - chainId, - key = 'global', - }: { - chainId: Hex; - key?: string; - }): Promise<() => void> { - let nonceMutexesForChainId = this.nonceMutexesByChainId.get(chainId); - if (!nonceMutexesForChainId) { - nonceMutexesForChainId = new Map(); - this.nonceMutexesByChainId.set(chainId, nonceMutexesForChainId); - } - let nonceMutexForKey = nonceMutexesForChainId.get(key); - if (!nonceMutexForKey) { - nonceMutexForKey = new Mutex(); - nonceMutexesForChainId.set(key, nonceMutexForKey); - } - - return await nonceMutexForKey.acquire(); - } - - /** - * Gets the next nonce according to the nonce-tracker. - * Ensure `releaseLock` is called once processing of the `nonce` value is complete. - * - * @param address - The hex string address for the transaction. - * @param networkClientId - The network client ID for the transaction, used to fetch the correct nonce tracker. - * @returns object with the `nextNonce` `nonceDetails`, and the releaseLock. - */ async getNonceLock( address: string, networkClientId?: NetworkClientId, ): Promise { - let releaseLockForChainIdKey: (() => void) | undefined; - let { nonceTracker } = this; - if (networkClientId && this.#isMultichainEnabled) { - const networkClient = this.messagingSystem.call( - `NetworkController:getNetworkClientById`, - networkClientId, - ); - releaseLockForChainIdKey = await this.#acquireNonceLockForChainIdKey({ - chainId: networkClient.configuration.chainId, - key: address, - }); - const trackers = this.trackingMap.get(networkClientId); - if (!trackers) { - throw new Error('missing nonceTracker for networkClientId'); - } - nonceTracker = trackers.nonceTracker; - } - - // Acquires the lock for the chainId + address and the nonceLock from the nonceTracker, then - // couples them together by replacing the nonceLock's releaseLock method with - // an anonymous function that calls releases both the original nonceLock and the - // lock for the chainId. - try { - const nonceLock = await nonceTracker.getNonceLock(address); - return { - ...nonceLock, - releaseLock: () => { - nonceLock.releaseLock(); - releaseLockForChainIdKey?.(); - }, - }; - } catch (err) { - releaseLockForChainIdKey?.(); - throw err; - } + return this.#multichainTrackingHelper.getNonceLock( + address, + networkClientId, + ); } /** @@ -1746,7 +1606,7 @@ export class TransactionController extends BaseControllerV1< const updatedTransaction = merge(transactionMeta, editableParams); const { type } = await determineTransactionType( updatedTransaction.txParams, - this.#getEthQuery({ + this.#multichainTrackingHelper.getEthQuery({ networkClientId: transactionMeta.networkClientId, chainId: transactionMeta.chainId, }), @@ -2132,7 +1992,10 @@ export class TransactionController extends BaseControllerV1< : this.getNetworkState().providerConfig.type === NetworkType.rpc; await updateGas({ - ethQuery: this.#getEthQuery({ networkClientId, chainId }), + ethQuery: this.#multichainTrackingHelper.getEthQuery({ + networkClientId, + chainId, + }), chainId, isCustomNetwork, txMeta: transactionMeta, @@ -2140,7 +2003,10 @@ export class TransactionController extends BaseControllerV1< await updateGasFees({ eip1559: isEIP1559Compatible, - ethQuery: this.#getEthQuery({ networkClientId, chainId }), + ethQuery: this.#multichainTrackingHelper.getEthQuery({ + networkClientId, + chainId, + }), getSavedGasFees: this.getSavedGasFees.bind(this), getGasFeeEstimates: this.getGasFeeEstimates.bind(this), txMeta: transactionMeta, @@ -2325,7 +2191,8 @@ export class TransactionController extends BaseControllerV1< const [nonce, releaseNonce] = await getNextNonce( transactionMeta, - (address: string) => this.getNonceLock(address, networkClientId), + (address: string) => + this.#multichainTrackingHelper.getNonceLock(address, networkClientId), ); releaseNonceLock = releaseNonce; @@ -2368,7 +2235,7 @@ export class TransactionController extends BaseControllerV1< return; } - const ethQuery = this.#getEthQuery({ + const ethQuery = this.#multichainTrackingHelper.getEthQuery({ networkClientId: transactionMeta.networkClientId, chainId: transactionMeta.chainId, }); @@ -2952,7 +2819,7 @@ export class TransactionController extends BaseControllerV1< return; } - const ethQuery = this.#getEthQuery({ + const ethQuery = this.#multichainTrackingHelper.getEthQuery({ networkClientId: transactionMeta.networkClientId, chainId: transactionMeta.chainId, }); @@ -2973,176 +2840,20 @@ export class TransactionController extends BaseControllerV1< } } - #getEthQuery({ - networkClientId, + #createNonceTracker({ + provider, + blockTracker, chainId, }: { - networkClientId?: NetworkClientId; + provider: Provider; + blockTracker: BlockTracker; chainId?: Hex; - }): EthQuery { - if (!this.#isMultichainEnabled) { - return this.ethQuery; - } - let networkClient: NetworkClient | undefined; - - if (networkClientId) { - try { - networkClient = this.messagingSystem.call( - `NetworkController:getNetworkClientById`, - networkClientId, - ); - } catch (err) { - log('failed to get network client by networkClientId'); - } - } - - if (!networkClient && chainId) { - try { - networkClientId = this.messagingSystem.call( - `NetworkController:findNetworkClientIdByChainId`, - chainId, - ); - networkClient = this.messagingSystem.call( - `NetworkController:getNetworkClientById`, - networkClientId, - ); - } catch (err) { - log('failed to get network client by chainId'); - } - } - - if (networkClient) { - return new EthQuery(networkClient.provider); - } - - // NOTE(JL): we're not ready to drop globally selected ethQuery yet. - // Some calls to getEthQuery only have access to optional networkClientId - // throw new Error('failed to get eth query instance'); - return this.ethQuery; - } - - #refreshEtherscanRemoteTransactionSources = ( - networkClients: NetworkClientRegistry, - ) => { - // this will be prettier when we have consolidated network clients with a single chainId: - // check if there are still other network clients using the same chainId - // if not remove the etherscanRemoteTransaction source from the map - const chainIdsInRegistry = new Set(); - Object.values(networkClients).forEach((networkClient) => - chainIdsInRegistry.add(networkClient.configuration.chainId), - ); - const existingChainIds = Array.from( - this.etherscanRemoteTransactionSourcesMap.keys(), - ); - const chainIdsToRemove = existingChainIds.filter( - (chainId) => !chainIdsInRegistry.has(chainId), - ); - - chainIdsToRemove.forEach((chainId) => { - this.etherscanRemoteTransactionSourcesMap.delete(chainId); - }); - }; - - #refreshTrackingMap = (networkClients: NetworkClientRegistry) => { - this.#refreshEtherscanRemoteTransactionSources(networkClients); - - const networkClientIds = Object.keys(networkClients); - const existingNetworkClientIds = Array.from(this.trackingMap.keys()); - - // Remove tracking for NetworkClientIds that no longer exist - const networkClientIdsToRemove = existingNetworkClientIds.filter( - (id) => !networkClientIds.includes(id), - ); - networkClientIdsToRemove.forEach((id) => { - this.#stopTrackingByNetworkClientId(id); - }); - - // Start tracking new NetworkClientIds from the registry - const networkClientIdsToAdd = networkClientIds.filter( - (id) => !existingNetworkClientIds.includes(id), - ); - networkClientIdsToAdd.forEach((id) => { - this.#startTrackingByNetworkClientId(id); - }); - }; - - #initTrackingMap = () => { - const networkClients = this.#getNetworkClientRegistry(); - const networkClientIds = Object.keys(networkClients); - networkClientIds.map((id) => this.#startTrackingByNetworkClientId(id)); - this.hub.emit('tracking-map-init', networkClientIds); - }; - - #checkForPendingTransactionAndStartPolling = () => { - // PendingTransactionTracker reads state through its getTransactions hook - this.pendingTransactionTracker.startIfPendingTransactions(); - if (this.#isMultichainEnabled) { - for (const [, trackers] of this.trackingMap) { - trackers.pendingTransactionTracker.startIfPendingTransactions(); - } - } - }; - - #stopTrackingByNetworkClientId(networkClientId: NetworkClientId) { - const trackers = this.trackingMap.get(networkClientId); - if (trackers) { - trackers.pendingTransactionTracker.stop(); - this.#removePendingTransactionTrackerListeners( - trackers.pendingTransactionTracker, - ); - trackers.incomingTransactionHelper.stop(); - this.#removeIncomingTransactionHelperListeners( - trackers.incomingTransactionHelper, - ); - this.trackingMap.delete(networkClientId); - this.hub.emit('tracking-map-remove', networkClientId); - } - } - - #stopAllTracking() { - this.pendingTransactionTracker.stop(); - this.#removePendingTransactionTrackerListeners(); - this.incomingTransactionHelper.stop(); - this.#removeIncomingTransactionHelperListeners(); - - if (this.#isMultichainEnabled) { - for (const [networkClientId] of this.trackingMap) { - this.#stopTrackingByNetworkClientId(networkClientId); - } - } - } - - #startTrackingByNetworkClientId(networkClientId: NetworkClientId) { - const trackers = this.trackingMap.get(networkClientId); - if (trackers) { - return; - } - const networkClient = this.messagingSystem.call( - `NetworkController:getNetworkClientById`, - networkClientId, - ); - const { chainId } = networkClient.configuration; - - let etherscanRemoteTransactionSource = - this.etherscanRemoteTransactionSourcesMap.get(chainId); - if (!etherscanRemoteTransactionSource) { - etherscanRemoteTransactionSource = new EtherscanRemoteTransactionSource({ - includeTokenTransfers: - this.#incomingTransactionOptions.includeTokenTransfers, - }); - this.etherscanRemoteTransactionSourcesMap.set( - chainId, - etherscanRemoteTransactionSource, - ); - } - - const ethQuery = new EthQuery(networkClient.provider); - - const nonceTracker = new NonceTracker({ + }): NonceTracker { + return new NonceTracker({ // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any - provider: networkClient.provider as any, - blockTracker: networkClient.blockTracker, + provider: provider as any, + blockTracker, getPendingTransactions: this.#getNonceTrackerPendingTransactions.bind( this, chainId, @@ -3152,12 +2863,22 @@ export class TransactionController extends BaseControllerV1< TransactionStatus.confirmed, ), }); + } + #createIncomingTransactionHelper({ + blockTracker, + etherscanRemoteTransactionSource, + chainId, + }: { + blockTracker: BlockTracker; + etherscanRemoteTransactionSource: EtherscanRemoteTransactionSource; + chainId?: Hex; + }): IncomingTransactionHelper { const incomingTransactionHelper = new IncomingTransactionHelper({ - blockTracker: networkClient.blockTracker, + blockTracker, getCurrentAccount: this.getSelectedAddress, getLastFetchedBlockNumbers: () => this.state.lastFetchedBlockNumbers, - getChainId: () => chainId, + getChainId: chainId ? () => chainId : this.getChainId.bind(this), isEnabled: this.#incomingTransactionOptions.isEnabled, queryEntireHistory: this.#incomingTransactionOptions.queryEntireHistory, remoteTransactionSource: etherscanRemoteTransactionSource, @@ -3167,16 +2888,32 @@ export class TransactionController extends BaseControllerV1< this.#addIncomingTransactionHelperListeners(incomingTransactionHelper); + return incomingTransactionHelper; + } + + #createPendingTransactionTracker({ + provider, + blockTracker, + chainId, + }: { + provider: Provider; + blockTracker: BlockTracker; + chainId?: Hex; + }): PendingTransactionTracker { + const ethQuery = new EthQuery(provider); + const getChainId = chainId ? () => chainId : this.getChainId.bind(this); + const pendingTransactionTracker = new PendingTransactionTracker({ approveTransaction: this.approveTransaction.bind(this), - blockTracker: networkClient.blockTracker, - getChainId: () => chainId, + blockTracker, + getChainId, getEthQuery: () => ethQuery, getTransactions: () => this.state.transactions, isResubmitEnabled: this.#pendingTransactionOptions.isResubmitEnabled, - getGlobalLock: this.#acquireNonceLockForChainIdKey.bind(this, { - chainId, - }), + getGlobalLock: () => + this.#multichainTrackingHelper.acquireNonceLockForChainIdKey({ + chainId: getChainId(), + }), publishTransaction: this.publishTransaction.bind(this), hooks: { beforeCheckPendingTransaction: @@ -3187,17 +2924,30 @@ export class TransactionController extends BaseControllerV1< this.#addPendingTransactionTrackerListeners(pendingTransactionTracker); - this.trackingMap.set(networkClientId, { - nonceTracker, - incomingTransactionHelper, - pendingTransactionTracker, - }); + return pendingTransactionTracker; + } - this.hub.emit('tracking-map-add', networkClientId); + #checkForPendingTransactionAndStartPolling = () => { + // PendingTransactionTracker reads state through its getTransactions hook + this.pendingTransactionTracker.startIfPendingTransactions(); + this.#multichainTrackingHelper.checkForPendingTransactionAndStartPolling(); + }; + + #stopAllTracking() { + this.pendingTransactionTracker.stop(); + this.#removePendingTransactionTrackerListeners( + this.pendingTransactionTracker, + ); + this.incomingTransactionHelper.stop(); + this.#removeIncomingTransactionHelperListeners( + this.incomingTransactionHelper, + ); + + this.#multichainTrackingHelper.stopAllTracking(); } #removeIncomingTransactionHelperListeners( - incomingTransactionHelper = this.incomingTransactionHelper, + incomingTransactionHelper: IncomingTransactionHelper, ) { incomingTransactionHelper.hub.removeAllListeners('transactions'); incomingTransactionHelper.hub.removeAllListeners( @@ -3206,7 +2956,7 @@ export class TransactionController extends BaseControllerV1< } #addIncomingTransactionHelperListeners( - incomingTransactionHelper = this.incomingTransactionHelper, + incomingTransactionHelper: IncomingTransactionHelper, ) { incomingTransactionHelper.hub.on( 'transactions', @@ -3219,7 +2969,7 @@ export class TransactionController extends BaseControllerV1< } #removePendingTransactionTrackerListeners( - pendingTransactionTracker = this.pendingTransactionTracker, + pendingTransactionTracker: PendingTransactionTracker, ) { pendingTransactionTracker.hub.removeAllListeners('transaction-confirmed'); pendingTransactionTracker.hub.removeAllListeners('transaction-dropped'); @@ -3228,7 +2978,7 @@ export class TransactionController extends BaseControllerV1< } #addPendingTransactionTrackerListeners( - pendingTransactionTracker = this.pendingTransactionTracker, + pendingTransactionTracker: PendingTransactionTracker, ) { pendingTransactionTracker.hub.on( 'transaction-confirmed', @@ -3265,7 +3015,6 @@ export class TransactionController extends BaseControllerV1< address, chainId, ); - return [...standardPendingTransactions, ...externalPendingTransactions]; } } diff --git a/packages/transaction-controller/src/helpers/IncomingTransactionHelper.ts b/packages/transaction-controller/src/helpers/IncomingTransactionHelper.ts index dd4c790f29..bd8b66aeaf 100644 --- a/packages/transaction-controller/src/helpers/IncomingTransactionHelper.ts +++ b/packages/transaction-controller/src/helpers/IncomingTransactionHelper.ts @@ -15,6 +15,21 @@ const UPDATE_CHECKS: ((txMeta: TransactionMeta) => any)[] = [ (txMeta) => txMeta.txParams.gasUsed, ]; +/** + * Configuration options for the IncomingTransactionHelper + * + * @property includeTokenTransfers - Whether or not to include ERC20 token transfers. + * @property isEnabled - Whether or not incoming transaction retrieval is enabled. + * @property queryEntireHistory - Whether to initially query the entire transaction history or only recent blocks. + * @property updateTransactions - Whether to update local transactions using remote transaction data. + */ +export type IncomingTransactionOptions = { + includeTokenTransfers?: boolean; + isEnabled?: () => boolean; + queryEntireHistory?: boolean; + updateTransactions?: boolean; +}; + export class IncomingTransactionHelper { hub: EventEmitter; diff --git a/packages/transaction-controller/src/helpers/MultichainTrackingHelper.test.ts b/packages/transaction-controller/src/helpers/MultichainTrackingHelper.test.ts new file mode 100644 index 0000000000..133258eb6c --- /dev/null +++ b/packages/transaction-controller/src/helpers/MultichainTrackingHelper.test.ts @@ -0,0 +1,869 @@ +/* eslint-disable jsdoc/require-jsdoc */ +import { ChainId } from '@metamask/controller-utils'; +import type { NetworkClientId, Provider } from '@metamask/network-controller'; +import type { Hex } from '@metamask/utils'; +import type { NonceTracker } from 'nonce-tracker'; +import { useFakeTimers } from 'sinon'; + +import { advanceTime } from '../../../../tests/helpers'; +import { EtherscanRemoteTransactionSource } from './EtherscanRemoteTransactionSource'; +import type { IncomingTransactionHelper } from './IncomingTransactionHelper'; +import { MultichainTrackingHelper } from './MultichainTrackingHelper'; +import type { PendingTransactionTracker } from './PendingTransactionTracker'; + +jest.mock( + '@metamask/eth-query', + () => + function (provider: Provider) { + return { provider }; + }, +); + +function buildMockProvider(networkClientId: NetworkClientId) { + return { + mockProvider: networkClientId, + }; +} + +function buildMockBlockTracker(networkClientId: NetworkClientId) { + return { + mockBlockTracker: networkClientId, + }; +} + +const MOCK_BLOCK_TRACKERS = { + mainnet: buildMockBlockTracker('mainnet'), + sepolia: buildMockBlockTracker('sepolia'), + goerli: buildMockBlockTracker('goerli'), + 'customNetworkClientId-1': buildMockBlockTracker('customNetworkClientId-1'), +}; + +const MOCK_PROVIDERS = { + mainnet: buildMockProvider('mainnet'), + sepolia: buildMockProvider('sepolia'), + goerli: buildMockProvider('goerli'), + 'customNetworkClientId-1': buildMockProvider('customNetworkClientId-1'), +}; + +/** + * Create a new instance of the MultichainTrackingHelper. + * + * @param opts - Options to use when creating the instance. + * @param opts.options - Any options to override the test defaults. + * @returns The new MultichainTrackingHelper instance. + */ +function newMultichainTrackingHelper( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + opts: any = {}, +) { + const mockGetNetworkClientById = jest + .fn() + .mockImplementation((networkClientId) => { + switch (networkClientId) { + case 'mainnet': + return { + configuration: { + chainId: '0x1', + }, + blockTracker: MOCK_BLOCK_TRACKERS.mainnet, + provider: MOCK_PROVIDERS.mainnet, + }; + case 'sepolia': + return { + configuration: { + chainId: ChainId.sepolia, + }, + blockTracker: MOCK_BLOCK_TRACKERS.sepolia, + provider: MOCK_PROVIDERS.sepolia, + }; + case 'goerli': + return { + configuration: { + chainId: ChainId.goerli, + }, + blockTracker: MOCK_BLOCK_TRACKERS.goerli, + provider: MOCK_PROVIDERS.goerli, + }; + case 'customNetworkClientId-1': + return { + configuration: { + chainId: '0xa', + }, + blockTracker: MOCK_BLOCK_TRACKERS['customNetworkClientId-1'], + provider: MOCK_PROVIDERS['customNetworkClientId-1'], + }; + default: + throw new Error(`Invalid network client id ${networkClientId}`); + } + }); + + const mockFindNetworkClientIdByChainId = jest + .fn() + .mockImplementation((chainId) => { + switch (chainId) { + case '0x1': + return 'mainnet'; + case ChainId.sepolia: + return 'sepolia'; + case ChainId.goerli: + return 'goerli'; + case '0xa': + return 'customNetworkClientId-1'; + default: + throw new Error("Couldn't find networkClientId for chainId"); + } + }); + + const mockGetNetworkClientRegistry = jest.fn().mockReturnValue({ + mainnet: { + configuration: { + chainId: '0x1', + }, + }, + sepolia: { + configuration: { + chainId: ChainId.sepolia, + }, + }, + goerli: { + configuration: { + chainId: ChainId.goerli, + }, + }, + 'customNetworkClientId-1': { + configuration: { + chainId: '0xa', + }, + }, + }); + + const mockNonceLock = { releaseLock: jest.fn() }; + const mockNonceTrackers: Record> = {}; + const mockCreateNonceTracker = jest + .fn() + .mockImplementation(({ chainId }: { chainId: Hex }) => { + const mockNonceTracker = { + getNonceLock: jest.fn().mockResolvedValue(mockNonceLock), + } as unknown as jest.Mocked; + mockNonceTrackers[chainId] = mockNonceTracker; + return mockNonceTracker; + }); + + const mockIncomingTransactionHelpers: Record< + Hex, + jest.Mocked + > = {}; + const mockCreateIncomingTransactionHelper = jest + .fn() + .mockImplementation(({ chainId }: { chainId: Hex }) => { + const mockIncomingTransactionHelper = { + start: jest.fn(), + stop: jest.fn(), + update: jest.fn(), + } as unknown as jest.Mocked; + mockIncomingTransactionHelpers[chainId] = mockIncomingTransactionHelper; + return mockIncomingTransactionHelper; + }); + + const mockPendingTransactionTrackers: Record< + Hex, + jest.Mocked + > = {}; + const mockCreatePendingTransactionTracker = jest + .fn() + .mockImplementation(({ chainId }: { chainId: Hex }) => { + const mockPendingTransactionTracker = { + start: jest.fn(), + stop: jest.fn(), + } as unknown as jest.Mocked; + mockPendingTransactionTrackers[chainId] = mockPendingTransactionTracker; + return mockPendingTransactionTracker; + }); + + const options = { + isMultichainEnabled: true, + provider: MOCK_PROVIDERS.mainnet, + nonceTracker: { + getNonceLock: jest.fn().mockResolvedValue(mockNonceLock), + }, + incomingTransactionOptions: { + // make this a comparable reference + includeTokenTransfers: true, + isEnabled: () => true, + queryEntireHistory: true, + updateTransactions: true, + }, + findNetworkClientIdByChainId: mockFindNetworkClientIdByChainId, + getNetworkClientById: mockGetNetworkClientById, + getNetworkClientRegistry: mockGetNetworkClientRegistry, + removeIncomingTransactionHelperListeners: jest.fn(), + removePendingTransactionTrackerListeners: jest.fn(), + createNonceTracker: mockCreateNonceTracker, + createIncomingTransactionHelper: mockCreateIncomingTransactionHelper, + createPendingTransactionTracker: mockCreatePendingTransactionTracker, + onNetworkStateChange: jest.fn(), + ...opts, + }; + + const helper = new MultichainTrackingHelper(options); + + return { + helper, + options, + mockNonceLock, + mockNonceTrackers, + mockIncomingTransactionHelpers, + mockPendingTransactionTrackers, + }; +} + +describe('MultichainTrackingHelper', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('onNetworkStateChange', () => { + it('refreshes the tracking map', () => { + const { options, helper } = newMultichainTrackingHelper(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (options.onNetworkStateChange as any).mock.calls[0][0]({}, []); + + expect(options.getNetworkClientRegistry).toHaveBeenCalledTimes(1); + expect(helper.has('mainnet')).toBe(true); + expect(helper.has('goerli')).toBe(true); + expect(helper.has('sepolia')).toBe(true); + expect(helper.has('customNetworkClientId-1')).toBe(true); + }); + + it('refreshes the tracking map and excludes removed networkClientIds in the patches', () => { + const { options, helper } = newMultichainTrackingHelper(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (options.onNetworkStateChange as any).mock.calls[0][0]({}, [ + { + op: 'remove', + path: ['networkConfigurations', 'mainnet'], + value: 'foo', + }, + ]); + + expect(options.getNetworkClientRegistry).toHaveBeenCalledTimes(1); + expect(helper.has('mainnet')).toBe(false); + expect(helper.has('goerli')).toBe(true); + expect(helper.has('sepolia')).toBe(true); + expect(helper.has('customNetworkClientId-1')).toBe(true); + }); + + it('does not refresh the tracking map when isMultichainEnabled: false', () => { + const { options, helper } = newMultichainTrackingHelper({ + isMultichainEnabled: false, + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (options.onNetworkStateChange as any).mock.calls[0][0]({}, []); + + expect(options.getNetworkClientRegistry).not.toHaveBeenCalled(); + expect(helper.has('mainnet')).toBe(false); + expect(helper.has('goerli')).toBe(false); + expect(helper.has('sepolia')).toBe(false); + expect(helper.has('customNetworkClientId-1')).toBe(false); + }); + }); + + describe('initialize', () => { + it('initializes the tracking map', () => { + const { options, helper } = newMultichainTrackingHelper(); + + helper.initialize(); + + expect(options.getNetworkClientRegistry).toHaveBeenCalledTimes(1); + expect(helper.has('mainnet')).toBe(true); + expect(helper.has('goerli')).toBe(true); + expect(helper.has('sepolia')).toBe(true); + expect(helper.has('customNetworkClientId-1')).toBe(true); + }); + + it('does not initialize the tracking map when isMultichainEnabled: false', () => { + const { options, helper } = newMultichainTrackingHelper({ + isMultichainEnabled: false, + }); + + helper.initialize(); + + expect(options.getNetworkClientRegistry).not.toHaveBeenCalled(); + expect(helper.has('mainnet')).toBe(false); + expect(helper.has('goerli')).toBe(false); + expect(helper.has('sepolia')).toBe(false); + expect(helper.has('customNetworkClientId-1')).toBe(false); + }); + }); + + describe('stopAllTracking', () => { + it('clears the tracking map', () => { + const { helper } = newMultichainTrackingHelper(); + + helper.initialize(); + + expect(helper.has('mainnet')).toBe(true); + expect(helper.has('goerli')).toBe(true); + expect(helper.has('sepolia')).toBe(true); + expect(helper.has('customNetworkClientId-1')).toBe(true); + + helper.stopAllTracking(); + + expect(helper.has('mainnet')).toBe(false); + expect(helper.has('goerli')).toBe(false); + expect(helper.has('sepolia')).toBe(false); + expect(helper.has('customNetworkClientId-1')).toBe(false); + }); + }); + + describe('#startTrackingByNetworkClientId', () => { + it('instantiates trackers and adds them to the tracking map', () => { + const { options, helper } = newMultichainTrackingHelper({ + getNetworkClientRegistry: jest.fn().mockReturnValue({ + mainnet: { + configuration: { + chainId: '0x1', + }, + }, + }), + }); + + helper.initialize(); + + expect(options.createNonceTracker).toHaveBeenCalledTimes(1); + expect(options.createNonceTracker).toHaveBeenCalledWith({ + provider: MOCK_PROVIDERS.mainnet, + blockTracker: MOCK_BLOCK_TRACKERS.mainnet, + chainId: '0x1', + }); + + expect(options.createIncomingTransactionHelper).toHaveBeenCalledTimes(1); + expect(options.createIncomingTransactionHelper).toHaveBeenCalledWith({ + blockTracker: MOCK_BLOCK_TRACKERS.mainnet, + etherscanRemoteTransactionSource: expect.any( + EtherscanRemoteTransactionSource, + ), + chainId: '0x1', + }); + + expect(options.createPendingTransactionTracker).toHaveBeenCalledTimes(1); + expect(options.createPendingTransactionTracker).toHaveBeenCalledWith({ + provider: MOCK_PROVIDERS.mainnet, + blockTracker: MOCK_BLOCK_TRACKERS.mainnet, + chainId: '0x1', + }); + + expect(helper.has('mainnet')).toBe(true); + }); + }); + + describe('#stopTrackingByNetworkClientId', () => { + it('stops trackers and removes them from the tracking map', () => { + const { + options, + mockIncomingTransactionHelpers, + mockPendingTransactionTrackers, + helper, + } = newMultichainTrackingHelper({ + getNetworkClientRegistry: jest.fn().mockReturnValue({ + mainnet: { + configuration: { + chainId: '0x1', + }, + }, + }), + }); + + helper.initialize(); + + expect(helper.has('mainnet')).toBe(true); + + helper.stopAllTracking(); + + expect(mockPendingTransactionTrackers['0x1'].stop).toHaveBeenCalled(); + expect( + options.removePendingTransactionTrackerListeners, + ).toHaveBeenCalledWith(mockPendingTransactionTrackers['0x1']); + expect(mockIncomingTransactionHelpers['0x1'].stop).toHaveBeenCalled(); + expect( + options.removeIncomingTransactionHelperListeners, + ).toHaveBeenCalledWith(mockIncomingTransactionHelpers['0x1']); + expect(helper.has('mainnet')).toBe(false); + }); + }); + + describe('startIncomingTransactionPolling', () => { + it('starts polling on the IncomingTransactionHelper for the networkClientIds', () => { + const { mockIncomingTransactionHelpers, helper } = + newMultichainTrackingHelper(); + + helper.initialize(); + + helper.startIncomingTransactionPolling(['mainnet', 'goerli']); + expect(mockIncomingTransactionHelpers['0x1'].start).toHaveBeenCalled(); + expect( + mockIncomingTransactionHelpers[ChainId.goerli].start, + ).toHaveBeenCalled(); + expect( + mockIncomingTransactionHelpers[ChainId.sepolia].start, + ).not.toHaveBeenCalled(); + expect( + mockIncomingTransactionHelpers['0xa'].start, + ).not.toHaveBeenCalled(); + }); + }); + + describe('stopIncomingTransactionPolling', () => { + it('stops polling on the IncomingTransactionHelper for the networkClientIds', () => { + const { mockIncomingTransactionHelpers, helper } = + newMultichainTrackingHelper(); + + helper.initialize(); + + helper.stopIncomingTransactionPolling(['mainnet', 'goerli']); + expect(mockIncomingTransactionHelpers['0x1'].stop).toHaveBeenCalled(); + expect( + mockIncomingTransactionHelpers[ChainId.goerli].stop, + ).toHaveBeenCalled(); + expect( + mockIncomingTransactionHelpers[ChainId.sepolia].stop, + ).not.toHaveBeenCalled(); + expect(mockIncomingTransactionHelpers['0xa'].stop).not.toHaveBeenCalled(); + }); + }); + + describe('stopAllIncomingTransactionPolling', () => { + it('stops polling on all IncomingTransactionHelpers', () => { + const { mockIncomingTransactionHelpers, helper } = + newMultichainTrackingHelper(); + + helper.initialize(); + + helper.stopAllIncomingTransactionPolling(); + expect(mockIncomingTransactionHelpers['0x1'].stop).toHaveBeenCalled(); + expect( + mockIncomingTransactionHelpers[ChainId.goerli].stop, + ).toHaveBeenCalled(); + expect( + mockIncomingTransactionHelpers[ChainId.sepolia].stop, + ).toHaveBeenCalled(); + expect(mockIncomingTransactionHelpers['0xa'].stop).toHaveBeenCalled(); + }); + }); + + describe('updateIncomingTransactions', () => { + it('calls update on the IncomingTransactionHelper for the networkClientIds', () => { + const { mockIncomingTransactionHelpers, helper } = + newMultichainTrackingHelper(); + + helper.initialize(); + + helper.updateIncomingTransactions(['mainnet', 'goerli']); + expect(mockIncomingTransactionHelpers['0x1'].update).toHaveBeenCalled(); + expect( + mockIncomingTransactionHelpers[ChainId.goerli].update, + ).toHaveBeenCalled(); + expect( + mockIncomingTransactionHelpers[ChainId.sepolia].update, + ).not.toHaveBeenCalled(); + expect( + mockIncomingTransactionHelpers['0xa'].update, + ).not.toHaveBeenCalled(); + }); + }); + + describe('getNonceLock', () => { + describe('when given a networkClientId', () => { + it('gets the shared nonce lock by chainId for the networkClientId', async () => { + const { helper } = newMultichainTrackingHelper(); + + const mockAcquireNonceLockForChainIdKey = jest + .spyOn(helper, 'acquireNonceLockForChainIdKey') + .mockResolvedValue(jest.fn()); + + helper.initialize(); + + await helper.getNonceLock('0xdeadbeef', 'mainnet'); + + expect(mockAcquireNonceLockForChainIdKey).toHaveBeenCalledWith({ + chainId: '0x1', + key: '0xdeadbeef', + }); + }); + + it('gets the nonce lock from the NonceTracker for the networkClientId', async () => { + const { mockNonceTrackers, helper } = newMultichainTrackingHelper({}); + + jest + .spyOn(helper, 'acquireNonceLockForChainIdKey') + .mockResolvedValue(jest.fn()); + + helper.initialize(); + + await helper.getNonceLock('0xdeadbeef', 'mainnet'); + + expect(mockNonceTrackers['0x1'].getNonceLock).toHaveBeenCalledWith( + '0xdeadbeef', + ); + }); + + it('merges the nonce lock by chainId release with the NonceTracker releaseLock function', async () => { + const { mockNonceLock, helper } = newMultichainTrackingHelper({}); + + const releaseLockForChainIdKey = jest.fn(); + jest + .spyOn(helper, 'acquireNonceLockForChainIdKey') + .mockResolvedValue(releaseLockForChainIdKey); + + helper.initialize(); + + const nonceLock = await helper.getNonceLock('0xdeadbeef', 'mainnet'); + + expect(releaseLockForChainIdKey).not.toHaveBeenCalled(); + expect(mockNonceLock.releaseLock).not.toHaveBeenCalled(); + + nonceLock.releaseLock(); + + expect(releaseLockForChainIdKey).toHaveBeenCalled(); + expect(mockNonceLock.releaseLock).toHaveBeenCalled(); + }); + + it('throws an error if the networkClientId does not exist in the tracking map', async () => { + const { helper } = newMultichainTrackingHelper(); + + jest + .spyOn(helper, 'acquireNonceLockForChainIdKey') + .mockResolvedValue(jest.fn()); + + await expect( + helper.getNonceLock('0xdeadbeef', 'mainnet'), + ).rejects.toThrow('missing nonceTracker for networkClientId'); + }); + + it('throws an error and releases nonce lock by chainId if unable to acquire nonce lock from the NonceTracker', async () => { + const { mockNonceTrackers, helper } = newMultichainTrackingHelper({}); + + const releaseLockForChainIdKey = jest.fn(); + jest + .spyOn(helper, 'acquireNonceLockForChainIdKey') + .mockResolvedValue(releaseLockForChainIdKey); + + helper.initialize(); + + mockNonceTrackers['0x1'].getNonceLock.mockRejectedValue( + 'failed to acquire lock from nonceTracker', + ); + + // for some reason jest expect().rejects.toThrow doesn't work here + let error = ''; + try { + await helper.getNonceLock('0xdeadbeef', 'mainnet'); + } catch (err: unknown) { + error = err as string; + } + expect(error).toBe('failed to acquire lock from nonceTracker'); + expect(releaseLockForChainIdKey).toHaveBeenCalled(); + }); + }); + + describe('when no networkClientId given', () => { + it('does not get the shared nonce lock by chainId', async () => { + const { helper } = newMultichainTrackingHelper(); + + const mockAcquireNonceLockForChainIdKey = jest + .spyOn(helper, 'acquireNonceLockForChainIdKey') + .mockResolvedValue(jest.fn()); + + helper.initialize(); + + await helper.getNonceLock('0xdeadbeef'); + + expect(mockAcquireNonceLockForChainIdKey).not.toHaveBeenCalled(); + }); + + it('gets the nonce lock from the global NonceTracker', async () => { + const { options, helper } = newMultichainTrackingHelper({}); + + helper.initialize(); + + await helper.getNonceLock('0xdeadbeef'); + + expect(options.nonceTracker.getNonceLock).toHaveBeenCalledWith( + '0xdeadbeef', + ); + }); + + it('throws an error if unable to acquire nonce lock from the global NonceTracker', async () => { + const { options, helper } = newMultichainTrackingHelper({}); + + helper.initialize(); + + options.nonceTracker.getNonceLock.mockRejectedValue( + 'failed to acquire lock from nonceTracker', + ); + + // for some reason jest expect().rejects.toThrow doesn't work here + let error = ''; + try { + await helper.getNonceLock('0xdeadbeef'); + } catch (err: unknown) { + error = err as string; + } + expect(error).toBe('failed to acquire lock from nonceTracker'); + }); + }); + + describe('when passed a networkClientId and isMultichainEnabled: false', () => { + it('does not get the shared nonce lock by chainId', async () => { + const { helper } = newMultichainTrackingHelper({ + isMultichainEnabled: false, + }); + + const mockAcquireNonceLockForChainIdKey = jest + .spyOn(helper, 'acquireNonceLockForChainIdKey') + .mockResolvedValue(jest.fn()); + + helper.initialize(); + + await helper.getNonceLock('0xdeadbeef', '0xabc'); + + expect(mockAcquireNonceLockForChainIdKey).not.toHaveBeenCalled(); + }); + + it('gets the nonce lock from the global NonceTracker', async () => { + const { options, helper } = newMultichainTrackingHelper({ + isMultichainEnabled: false, + }); + + helper.initialize(); + + await helper.getNonceLock('0xdeadbeef', '0xabc'); + + expect(options.nonceTracker.getNonceLock).toHaveBeenCalledWith( + '0xdeadbeef', + ); + }); + + it('throws an error if unable to acquire nonce lock from the global NonceTracker', async () => { + const { options, helper } = newMultichainTrackingHelper({ + isMultichainEnabled: false, + }); + + helper.initialize(); + + options.nonceTracker.getNonceLock.mockRejectedValue( + 'failed to acquire lock from nonceTracker', + ); + + // for some reason jest expect().rejects.toThrow doesn't work here + let error = ''; + try { + await helper.getNonceLock('0xdeadbeef', '0xabc'); + } catch (err: unknown) { + error = err as string; + } + expect(error).toBe('failed to acquire lock from nonceTracker'); + }); + }); + }); + + describe('acquireNonceLockForChainIdKey', () => { + it('returns a unqiue mutex for each chainId and key combination', async () => { + const { helper } = newMultichainTrackingHelper(); + + await helper.acquireNonceLockForChainIdKey({ chainId: '0x1', key: 'a' }); + await helper.acquireNonceLockForChainIdKey({ chainId: '0x1', key: 'b' }); + await helper.acquireNonceLockForChainIdKey({ chainId: '0xa', key: 'a' }); + await helper.acquireNonceLockForChainIdKey({ chainId: '0xa', key: 'b' }); + + // nothing to exepect as this spec will pass if all locks are acquired + }); + + it('should block on attempts to get the lock for the same chainId and key combination', async () => { + const clock = useFakeTimers(); + const { helper } = newMultichainTrackingHelper(); + + const firstReleaseLockPromise = helper.acquireNonceLockForChainIdKey({ + chainId: '0x1', + key: 'a', + }); + + const firstReleaseLock = await firstReleaseLockPromise; + + const secondReleaseLockPromise = helper.acquireNonceLockForChainIdKey({ + chainId: '0x1', + key: 'a', + }); + + const delay = () => + new Promise(async (resolve) => { + await advanceTime({ clock, duration: 100 }); + resolve(null); + }); + + let secondReleaseLockIfAcquired = await Promise.race([ + secondReleaseLockPromise, + delay(), + ]); + expect(secondReleaseLockIfAcquired).toBeNull(); + + await firstReleaseLock(); + await advanceTime({ clock, duration: 1 }); + + secondReleaseLockIfAcquired = await Promise.race([ + secondReleaseLockPromise, + delay(), + ]); + + expect(secondReleaseLockIfAcquired).toStrictEqual(expect.any(Function)); + + clock.restore(); + }); + }); + + describe('getEthQuery', () => { + describe('when given networkClientId and chainId', () => { + it('returns EthQuery with the networkClientId provider when available', () => { + const { options, helper } = newMultichainTrackingHelper(); + + const ethQuery = helper.getEthQuery({ + networkClientId: 'goerli', + chainId: '0xa', + }); + expect(ethQuery.provider).toBe(MOCK_PROVIDERS.goerli); + + expect(options.getNetworkClientById).toHaveBeenCalledTimes(1); + expect(options.getNetworkClientById).toHaveBeenCalledWith('goerli'); + }); + + it('returns EthQuery with a fallback networkClient provider matching the chainId when available', () => { + const { options, helper } = newMultichainTrackingHelper(); + + const ethQuery = helper.getEthQuery({ + networkClientId: 'missingNetworkClientId', + chainId: '0xa', + }); + expect(ethQuery.provider).toBe( + MOCK_PROVIDERS['customNetworkClientId-1'], + ); + + expect(options.getNetworkClientById).toHaveBeenCalledTimes(2); + expect(options.getNetworkClientById).toHaveBeenCalledWith( + 'missingNetworkClientId', + ); + expect(options.findNetworkClientIdByChainId).toHaveBeenCalledWith( + '0xa', + ); + expect(options.getNetworkClientById).toHaveBeenCalledWith( + 'customNetworkClientId-1', + ); + }); + + it('returns EthQuery with the fallback global provider if networkClientId and chainId cannot be satisfied', () => { + const { options, helper } = newMultichainTrackingHelper(); + + const ethQuery = helper.getEthQuery({ + networkClientId: 'missingNetworkClientId', + chainId: '0xdeadbeef', + }); + expect(ethQuery.provider).toBe(MOCK_PROVIDERS.mainnet); + + expect(options.getNetworkClientById).toHaveBeenCalledTimes(1); + expect(options.getNetworkClientById).toHaveBeenCalledWith( + 'missingNetworkClientId', + ); + expect(options.findNetworkClientIdByChainId).toHaveBeenCalledWith( + '0xdeadbeef', + ); + }); + }); + + describe('when given only networkClientId', () => { + it('returns EthQuery with the networkClientId provider when available', () => { + const { options, helper } = newMultichainTrackingHelper(); + + const ethQuery = helper.getEthQuery({ networkClientId: 'goerli' }); + expect(ethQuery.provider).toBe(MOCK_PROVIDERS.goerli); + + expect(options.getNetworkClientById).toHaveBeenCalledTimes(1); + expect(options.getNetworkClientById).toHaveBeenCalledWith('goerli'); + }); + + it('returns EthQuery with the fallback global provider if networkClientId cannot be satisfied', () => { + const { options, helper } = newMultichainTrackingHelper(); + + const ethQuery = helper.getEthQuery({ + networkClientId: 'missingNetworkClientId', + }); + expect(ethQuery.provider).toBe(MOCK_PROVIDERS.mainnet); + + expect(options.getNetworkClientById).toHaveBeenCalledTimes(1); + expect(options.getNetworkClientById).toHaveBeenCalledWith( + 'missingNetworkClientId', + ); + }); + }); + + describe('when given only chainId', () => { + it('returns EthQuery with a fallback networkClient provider matching the chainId when available', () => { + const { options, helper } = newMultichainTrackingHelper(); + + const ethQuery = helper.getEthQuery({ chainId: '0xa' }); + expect(ethQuery.provider).toBe( + MOCK_PROVIDERS['customNetworkClientId-1'], + ); + + expect(options.getNetworkClientById).toHaveBeenCalledTimes(1); + expect(options.findNetworkClientIdByChainId).toHaveBeenCalledWith( + '0xa', + ); + expect(options.getNetworkClientById).toHaveBeenCalledWith( + 'customNetworkClientId-1', + ); + }); + + it('returns EthQuery with the fallback global provider if chainId cannot be satisfied', () => { + const { options, helper } = newMultichainTrackingHelper(); + + const ethQuery = helper.getEthQuery({ chainId: '0xdeadbeef' }); + expect(ethQuery.provider).toBe(MOCK_PROVIDERS.mainnet); + + expect(options.findNetworkClientIdByChainId).toHaveBeenCalledWith( + '0xdeadbeef', + ); + }); + }); + + it('returns EthQuery with the global provider when no arguments are provided', () => { + const { options, helper } = newMultichainTrackingHelper(); + + const ethQuery = helper.getEthQuery(); + expect(ethQuery.provider).toBe(MOCK_PROVIDERS.mainnet); + + expect(options.getNetworkClientById).not.toHaveBeenCalled(); + }); + + it('always returns EthQuery with the global provider when isMultichainEnabled: false', () => { + const { options, helper } = newMultichainTrackingHelper({ + isMultichainEnabled: false, + }); + + let ethQuery = helper.getEthQuery({ + networkClientId: 'goerli', + chainId: '0x5', + }); + expect(ethQuery.provider).toBe(MOCK_PROVIDERS.mainnet); + ethQuery = helper.getEthQuery({ networkClientId: 'goerli' }); + expect(ethQuery.provider).toBe(MOCK_PROVIDERS.mainnet); + ethQuery = helper.getEthQuery({ chainId: '0x5' }); + expect(ethQuery.provider).toBe(MOCK_PROVIDERS.mainnet); + ethQuery = helper.getEthQuery(); + expect(ethQuery.provider).toBe(MOCK_PROVIDERS.mainnet); + + expect(options.getNetworkClientById).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/transaction-controller/src/helpers/MultichainTrackingHelper.ts b/packages/transaction-controller/src/helpers/MultichainTrackingHelper.ts new file mode 100644 index 0000000000..3af5c2c09a --- /dev/null +++ b/packages/transaction-controller/src/helpers/MultichainTrackingHelper.ts @@ -0,0 +1,454 @@ +import EthQuery from '@metamask/eth-query'; +import type { + NetworkClientId, + NetworkController, + NetworkClient, + BlockTracker, + Provider, + NetworkControllerStateChangeEvent, +} from '@metamask/network-controller'; +import type { Hex } from '@metamask/utils'; +import { Mutex } from 'async-mutex'; +import type { NonceLock, NonceTracker } from 'nonce-tracker'; + +import { incomingTransactionsLogger as log } from '../logger'; +import { EtherscanRemoteTransactionSource } from './EtherscanRemoteTransactionSource'; +import type { + IncomingTransactionHelper, + IncomingTransactionOptions, +} from './IncomingTransactionHelper'; +import type { PendingTransactionTracker } from './PendingTransactionTracker'; + +/** + * Registry of network clients provided by the NetworkController + */ +type NetworkClientRegistry = ReturnType< + NetworkController['getNetworkClientRegistry'] +>; + +export type MultichainTrackingHelperOptions = { + isMultichainEnabled: boolean; + provider: Provider; + nonceTracker: NonceTracker; + incomingTransactionOptions: IncomingTransactionOptions; + + findNetworkClientIdByChainId: NetworkController['findNetworkClientIdByChainId']; + getNetworkClientById: NetworkController['getNetworkClientById']; + getNetworkClientRegistry: NetworkController['getNetworkClientRegistry']; + + removeIncomingTransactionHelperListeners: ( + IncomingTransactionHelper: IncomingTransactionHelper, + ) => void; + removePendingTransactionTrackerListeners: ( + pendingTransactionTracker: PendingTransactionTracker, + ) => void; + createNonceTracker: (opts: { + provider: Provider; + blockTracker: BlockTracker; + chainId?: Hex; + }) => NonceTracker; + createIncomingTransactionHelper: (opts: { + blockTracker: BlockTracker; + etherscanRemoteTransactionSource: EtherscanRemoteTransactionSource; + chainId?: Hex; + }) => IncomingTransactionHelper; + createPendingTransactionTracker: (opts: { + provider: Provider; + blockTracker: BlockTracker; + chainId?: Hex; + }) => PendingTransactionTracker; + onNetworkStateChange: ( + listener: ( + ...payload: NetworkControllerStateChangeEvent['payload'] + ) => void, + ) => void; +}; + +export class MultichainTrackingHelper { + #isMultichainEnabled: boolean; + + readonly #provider: Provider; + + readonly #nonceTracker: NonceTracker; + + readonly #incomingTransactionOptions: IncomingTransactionOptions; + + readonly #findNetworkClientIdByChainId: NetworkController['findNetworkClientIdByChainId']; + + readonly #getNetworkClientById: NetworkController['getNetworkClientById']; + + readonly #getNetworkClientRegistry: NetworkController['getNetworkClientRegistry']; + + readonly #removeIncomingTransactionHelperListeners: ( + IncomingTransactionHelper: IncomingTransactionHelper, + ) => void; + + readonly #removePendingTransactionTrackerListeners: ( + pendingTransactionTracker: PendingTransactionTracker, + ) => void; + + readonly #createNonceTracker: (opts: { + provider: Provider; + blockTracker: BlockTracker; + chainId?: Hex; + }) => NonceTracker; + + readonly #createIncomingTransactionHelper: (opts: { + blockTracker: BlockTracker; + chainId?: Hex; + etherscanRemoteTransactionSource: EtherscanRemoteTransactionSource; + }) => IncomingTransactionHelper; + + readonly #createPendingTransactionTracker: (opts: { + provider: Provider; + blockTracker: BlockTracker; + chainId?: Hex; + }) => PendingTransactionTracker; + + readonly #nonceMutexesByChainId = new Map>(); + + readonly #trackingMap: Map< + NetworkClientId, + { + nonceTracker: NonceTracker; + pendingTransactionTracker: PendingTransactionTracker; + incomingTransactionHelper: IncomingTransactionHelper; + } + > = new Map(); + + readonly #etherscanRemoteTransactionSourcesMap: Map< + Hex, + EtherscanRemoteTransactionSource + > = new Map(); + + constructor({ + isMultichainEnabled, + provider, + nonceTracker, + incomingTransactionOptions, + findNetworkClientIdByChainId, + getNetworkClientById, + getNetworkClientRegistry, + removeIncomingTransactionHelperListeners, + removePendingTransactionTrackerListeners, + createNonceTracker, + createIncomingTransactionHelper, + createPendingTransactionTracker, + onNetworkStateChange, + }: MultichainTrackingHelperOptions) { + this.#isMultichainEnabled = isMultichainEnabled; + this.#provider = provider; + this.#nonceTracker = nonceTracker; + this.#incomingTransactionOptions = incomingTransactionOptions; + + this.#findNetworkClientIdByChainId = findNetworkClientIdByChainId; + this.#getNetworkClientById = getNetworkClientById; + this.#getNetworkClientRegistry = getNetworkClientRegistry; + + this.#removeIncomingTransactionHelperListeners = + removeIncomingTransactionHelperListeners; + this.#removePendingTransactionTrackerListeners = + removePendingTransactionTrackerListeners; + this.#createNonceTracker = createNonceTracker; + this.#createIncomingTransactionHelper = createIncomingTransactionHelper; + this.#createPendingTransactionTracker = createPendingTransactionTracker; + + onNetworkStateChange((_, patches) => { + if (this.#isMultichainEnabled) { + const networkClients = this.#getNetworkClientRegistry(); + patches.forEach(({ op, path }) => { + if (op === 'remove' && path[0] === 'networkConfigurations') { + const networkClientId = path[1] as NetworkClientId; + delete networkClients[networkClientId]; + } + }); + + this.#refreshTrackingMap(networkClients); + } + }); + } + + initialize() { + if (!this.#isMultichainEnabled) { + return; + } + const networkClients = this.#getNetworkClientRegistry(); + this.#refreshTrackingMap(networkClients); + } + + has(networkClientId: NetworkClientId) { + return this.#trackingMap.has(networkClientId); + } + + getEthQuery({ + networkClientId, + chainId, + }: { + networkClientId?: NetworkClientId; + chainId?: Hex; + } = {}): EthQuery { + if (!this.#isMultichainEnabled) { + return new EthQuery(this.#provider); + } + let networkClient: NetworkClient | undefined; + + if (networkClientId) { + try { + networkClient = this.#getNetworkClientById(networkClientId); + } catch (err) { + log('failed to get network client by networkClientId'); + } + } + if (!networkClient && chainId) { + try { + networkClientId = this.#findNetworkClientIdByChainId(chainId); + networkClient = this.#getNetworkClientById(networkClientId); + } catch (err) { + log('failed to get network client by chainId'); + } + } + + if (networkClient) { + return new EthQuery(networkClient.provider); + } + + // NOTE(JL): we're not ready to drop globally selected ethQuery yet. + // Some calls to getEthQuery only have access to optional networkClientId + // throw new Error('failed to get eth query instance'); + return new EthQuery(this.#provider); + } + + /** + * Gets the mutex intended to guard the nonceTracker for a particular chainId and key . + * + * @param opts - The options object. + * @param opts.chainId - The hex chainId. + * @param opts.key - The hex address (or constant) pertaining to the chainId + * @returns Mutex instance for the given chainId and key pair + */ + async acquireNonceLockForChainIdKey({ + chainId, + key = 'global', + }: { + chainId: Hex; + key?: string; + }): Promise<() => void> { + let nonceMutexesForChainId = this.#nonceMutexesByChainId.get(chainId); + if (!nonceMutexesForChainId) { + nonceMutexesForChainId = new Map(); + this.#nonceMutexesByChainId.set(chainId, nonceMutexesForChainId); + } + let nonceMutexForKey = nonceMutexesForChainId.get(key); + if (!nonceMutexForKey) { + nonceMutexForKey = new Mutex(); + nonceMutexesForChainId.set(key, nonceMutexForKey); + } + + return await nonceMutexForKey.acquire(); + } + + /** + * Gets the next nonce according to the nonce-tracker. + * Ensure `releaseLock` is called once processing of the `nonce` value is complete. + * + * @param address - The hex string address for the transaction. + * @param networkClientId - The network client ID for the transaction, used to fetch the correct nonce tracker. + * @returns object with the `nextNonce` `nonceDetails`, and the releaseLock. + */ + async getNonceLock( + address: string, + networkClientId?: NetworkClientId, + ): Promise { + let releaseLockForChainIdKey: (() => void) | undefined; + let nonceTracker = this.#nonceTracker; + if (networkClientId && this.#isMultichainEnabled) { + const networkClient = this.#getNetworkClientById(networkClientId); + releaseLockForChainIdKey = await this.acquireNonceLockForChainIdKey({ + chainId: networkClient.configuration.chainId, + key: address, + }); + const trackers = this.#trackingMap.get(networkClientId); + if (!trackers) { + throw new Error('missing nonceTracker for networkClientId'); + } + nonceTracker = trackers.nonceTracker; + } + + // Acquires the lock for the chainId + address and the nonceLock from the nonceTracker, then + // couples them together by replacing the nonceLock's releaseLock method with + // an anonymous function that calls releases both the original nonceLock and the + // lock for the chainId. + try { + const nonceLock = await nonceTracker.getNonceLock(address); + return { + ...nonceLock, + releaseLock: () => { + nonceLock.releaseLock(); + releaseLockForChainIdKey?.(); + }, + }; + } catch (err) { + releaseLockForChainIdKey?.(); + throw err; + } + } + + startIncomingTransactionPolling(networkClientIds: NetworkClientId[] = []) { + networkClientIds.forEach((networkClientId) => { + this.#trackingMap.get(networkClientId)?.incomingTransactionHelper.start(); + }); + } + + stopIncomingTransactionPolling(networkClientIds: NetworkClientId[] = []) { + networkClientIds.forEach((networkClientId) => { + this.#trackingMap.get(networkClientId)?.incomingTransactionHelper.stop(); + }); + } + + stopAllIncomingTransactionPolling() { + for (const [, trackers] of this.#trackingMap) { + trackers.incomingTransactionHelper.stop(); + } + } + + async updateIncomingTransactions(networkClientIds: NetworkClientId[] = []) { + const promises = await Promise.allSettled( + networkClientIds.map(async (networkClientId) => { + return await this.#trackingMap + .get(networkClientId) + ?.incomingTransactionHelper.update(); + }), + ); + + promises + .filter((result) => result.status === 'rejected') + .forEach((result) => { + log( + 'failed to update incoming transactions', + (result as PromiseRejectedResult).reason, + ); + }); + } + + checkForPendingTransactionAndStartPolling = () => { + for (const [, trackers] of this.#trackingMap) { + trackers.pendingTransactionTracker.startIfPendingTransactions(); + } + }; + + stopAllTracking() { + for (const [networkClientId] of this.#trackingMap) { + this.#stopTrackingByNetworkClientId(networkClientId); + } + } + + #refreshTrackingMap = (networkClients: NetworkClientRegistry) => { + this.#refreshEtherscanRemoteTransactionSources(networkClients); + + const networkClientIds = Object.keys(networkClients); + const existingNetworkClientIds = Array.from(this.#trackingMap.keys()); + + // Remove tracking for NetworkClientIds that no longer exist + const networkClientIdsToRemove = existingNetworkClientIds.filter( + (id) => !networkClientIds.includes(id), + ); + networkClientIdsToRemove.forEach((id) => { + this.#stopTrackingByNetworkClientId(id); + }); + + // Start tracking new NetworkClientIds from the registry + const networkClientIdsToAdd = networkClientIds.filter( + (id) => !existingNetworkClientIds.includes(id), + ); + networkClientIdsToAdd.forEach((id) => { + this.#startTrackingByNetworkClientId(id); + }); + }; + + #stopTrackingByNetworkClientId(networkClientId: NetworkClientId) { + const trackers = this.#trackingMap.get(networkClientId); + if (trackers) { + trackers.pendingTransactionTracker.stop(); + this.#removePendingTransactionTrackerListeners( + trackers.pendingTransactionTracker, + ); + trackers.incomingTransactionHelper.stop(); + this.#removeIncomingTransactionHelperListeners( + trackers.incomingTransactionHelper, + ); + this.#trackingMap.delete(networkClientId); + } + } + + #startTrackingByNetworkClientId(networkClientId: NetworkClientId) { + const trackers = this.#trackingMap.get(networkClientId); + if (trackers) { + return; + } + + const { + provider, + blockTracker, + configuration: { chainId }, + } = this.#getNetworkClientById(networkClientId); + + let etherscanRemoteTransactionSource = + this.#etherscanRemoteTransactionSourcesMap.get(chainId); + if (!etherscanRemoteTransactionSource) { + etherscanRemoteTransactionSource = new EtherscanRemoteTransactionSource({ + includeTokenTransfers: + this.#incomingTransactionOptions.includeTokenTransfers, + }); + this.#etherscanRemoteTransactionSourcesMap.set( + chainId, + etherscanRemoteTransactionSource, + ); + } + + const nonceTracker = this.#createNonceTracker({ + provider, + blockTracker, + chainId, + }); + + const incomingTransactionHelper = this.#createIncomingTransactionHelper({ + blockTracker, + etherscanRemoteTransactionSource, + chainId, + }); + + const pendingTransactionTracker = this.#createPendingTransactionTracker({ + provider, + blockTracker, + chainId, + }); + + this.#trackingMap.set(networkClientId, { + nonceTracker, + incomingTransactionHelper, + pendingTransactionTracker, + }); + } + + #refreshEtherscanRemoteTransactionSources = ( + networkClients: NetworkClientRegistry, + ) => { + // this will be prettier when we have consolidated network clients with a single chainId: + // check if there are still other network clients using the same chainId + // if not remove the etherscanRemoteTransaction source from the map + const chainIdsInRegistry = new Set(); + Object.values(networkClients).forEach((networkClient) => + chainIdsInRegistry.add(networkClient.configuration.chainId), + ); + const existingChainIds = Array.from( + this.#etherscanRemoteTransactionSourcesMap.keys(), + ); + const chainIdsToRemove = existingChainIds.filter( + (chainId) => !chainIdsInRegistry.has(chainId), + ); + + chainIdsToRemove.forEach((chainId) => { + this.#etherscanRemoteTransactionSourcesMap.delete(chainId); + }); + }; +} diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index 1c75e3fc53..c805f776d9 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -4,9 +4,6 @@ import type { Hex } from '@metamask/utils'; import type { Operation } from 'fast-json-patch'; export type Events = { - ['tracking-map-init']: [networkClientIds: NetworkClientId[]]; - ['tracking-map-add']: [networkClientId: NetworkClientId]; - ['tracking-map-remove']: [networkClientId: NetworkClientId]; ['incomingTransactionBlock']: [blockNumber: number]; ['post-transaction-balance-updated']: [ { From 280e51c70e72ea1d3869222a2dd963d96f0c5e3a Mon Sep 17 00:00:00 2001 From: Jiexi Luan Date: Tue, 13 Feb 2024 09:41:27 -0800 Subject: [PATCH 100/100] lint --- .../src/TransactionController.test.ts | 2 +- .../src/TransactionController.ts | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 693bde3065..2df7c26044 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -742,7 +742,7 @@ describe('TransactionController', () => { removeAllListeners: jest.fn(), }, onStateChange: jest.fn(), - forceCheckTransaction: jest.fn(), + forceCheckTransaction: jest.fn(), } as unknown as jest.Mocked; return pendingTransactionTrackerMock; }); diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index afc4b6e205..f7eeb0105a 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -941,7 +941,11 @@ export class TransactionController extends BaseControllerV1< networkClientId: transactionMeta.networkClientId, chainId: transactionMeta.chainId, }); - const hash = await this.publishTransactionForRetry(ethQuery, rawTx, transactionMeta); + const hash = await this.publishTransactionForRetry( + ethQuery, + rawTx, + transactionMeta, + ); const cancelTransactionMeta: TransactionMeta = { actionId, @@ -1101,7 +1105,11 @@ export class TransactionController extends BaseControllerV1< networkClientId: transactionMeta.networkClientId, chainId: transactionMeta.chainId, }); - const hash = await this.publishTransactionForRetry(ethQuery, rawTx, transactionMeta); + const hash = await this.publishTransactionForRetry( + ethQuery, + rawTx, + transactionMeta, + ); const baseTransactionMeta: TransactionMeta = { ...transactionMeta,