Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/transaction-controller/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ module.exports = merge(baseConfig, {
coverageThreshold: {
global: {
branches: 81.29,
functions: 95.14,
functions: 94.49,
lines: 95.24,
statements: 95.34,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1528,4 +1528,56 @@ describe('TransactionController', () => {
},
);
});

describe('initApprovals', () => {
it('creates approvals for all unapproved transaction', async () => {
const transaction = {
from: ACCOUNT_MOCK,
id: 'mocked',
networkID: '5',
status: TransactionStatus.unapproved,
transactionHash: '1337',
};
const controller = newController();
controller.state.transactions.push(transaction as any);
controller.state.transactions.push({
...transaction,
id: 'mocked1',
transactionHash: '1338',
} as any);

controller.initApprovals();

expect(delayMessengerMock.call).toHaveBeenCalledTimes(2);
expect(delayMessengerMock.call).toHaveBeenCalledWith(
'ApprovalController:addRequest',
{
expectsResult: true,
id: 'mocked',
origin: 'metamask',
requestData: { txId: 'mocked' },
type: 'transaction',
},
false,
);
expect(delayMessengerMock.call).toHaveBeenCalledWith(
'ApprovalController:addRequest',
{
expectsResult: true,
id: 'mocked1',
origin: 'metamask',
requestData: { txId: 'mocked1' },
type: 'transaction',
},
false,
);
});

it('does not create any approval when there is no unapproved transaction', async () => {
const controller = newController();
controller.initApprovals();

expect(delayMessengerMock.call).not.toHaveBeenCalled();
});
});
});
62 changes: 49 additions & 13 deletions packages/transaction-controller/src/TransactionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type {
NetworkState,
Provider,
} from '@metamask/network-controller';
import type { Hex } from '@metamask/utils';
import { Mutex } from 'async-mutex';
import MethodRegistry from 'eth-method-registry';
import { errorCodes, ethErrors } from 'eth-rpc-errors';
Expand All @@ -53,6 +54,7 @@ import {
validateGasValues,
validateMinimumIncrease,
ESTIMATE_GAS_ERROR,
transactionMatchesNetwork,
} from './utils';

export const HARDFORK = Hardfork.London;
Expand Down Expand Up @@ -350,15 +352,15 @@ export class TransactionController extends BaseController<
origin?: string;
} = {},
): Promise<Result> {
const { providerConfig, networkId } = this.getNetworkState();
const { chainId, networkId } = this.getChainAndNetworkId();
const { transactions } = this.state;
transaction = normalizeTransaction(transaction);
validateTransaction(transaction);

const transactionMeta: TransactionMeta = {
id: random(),
networkID: networkId ?? undefined,
chainId: providerConfig.chainId,
chainId,
origin,
status: TransactionStatus.unapproved as TransactionStatus.unapproved,
time: Date.now(),
Expand Down Expand Up @@ -393,6 +395,27 @@ export class TransactionController extends BaseController<
});
}

/**
* Creates approvals for all unapproved transactions persisted.
*/
initApprovals() {
const { networkId, chainId } = this.getChainAndNetworkId();
const unapprovedTxs = this.state.transactions.filter(
(transaction) =>
transaction.status === TransactionStatus.unapproved &&
transactionMatchesNetwork(transaction, chainId, networkId),
);

for (const txMeta of unapprovedTxs) {
this.processApproval(txMeta, {
shouldShowRequest: false,
}).catch((error) => {
/* istanbul ignore next */
console.error('Error during persisted transaction approval', error);
});
}
}

/**
* `@ethereumjs/tx` uses `@ethereumjs/common` as a configuration tool for
* specifying which chain, network, hardfork and EIPs to support for
Expand Down Expand Up @@ -741,9 +764,8 @@ export class TransactionController extends BaseController<
*/
async queryTransactionStatuses() {
const { transactions } = this.state;
const { providerConfig, networkId: currentNetworkID } =
this.getNetworkState();
const { chainId: currentChainId } = providerConfig;
const { chainId: currentChainId, networkId: currentNetworkID } =
this.getChainAndNetworkId();
let gotUpdates = false;
await safelyExecute(() =>
Promise.all(
Expand Down Expand Up @@ -804,9 +826,8 @@ export class TransactionController extends BaseController<
this.update({ transactions: [] });
return;
}
const { providerConfig, networkId: currentNetworkID } =
this.getNetworkState();
const { chainId: currentChainId } = providerConfig;
const { chainId: currentChainId, networkId: currentNetworkID } =
this.getChainAndNetworkId();
const newTransactions = this.state.transactions.filter(
({ networkID, chainId, transaction }) => {
// Using fallback to networkID only when there is no chainId present. Should be removed when networkID is completely removed.
Expand Down Expand Up @@ -865,12 +886,15 @@ export class TransactionController extends BaseController<

private async processApproval(
transactionMeta: TransactionMeta,
{ shouldShowRequest = true } = {},
): Promise<string> {
const transactionId = transactionMeta.id;
let resultCallbacks: AcceptResultCallbacks | undefined;

try {
const acceptResult = await this.requestApproval(transactionMeta);
const acceptResult = await this.requestApproval(transactionMeta, {
shouldShowRequest,
});
resultCallbacks = acceptResult.resultCallbacks;

const { meta, isCompleted } = this.isTransactionCompleted(transactionId);
Expand Down Expand Up @@ -936,8 +960,7 @@ export class TransactionController extends BaseController<
private async approveTransaction(transactionID: string) {
const { transactions } = this.state;
const releaseLock = await this.mutex.acquire();
const { providerConfig } = this.getNetworkState();
const { chainId } = providerConfig;
const { chainId } = this.getChainAndNetworkId();
const index = transactions.findIndex(({ id }) => transactionID === id);
const transactionMeta = transactions[index];
const {
Expand Down Expand Up @@ -1201,7 +1224,10 @@ export class TransactionController extends BaseController<
return Number(txReceipt.status) === 0;
}

private async requestApproval(txMeta: TransactionMeta): Promise<AddResult> {
private async requestApproval(
txMeta: TransactionMeta,
{ shouldShowRequest }: { shouldShowRequest: boolean },
): Promise<AddResult> {
const id = this.getApprovalId(txMeta);
const { origin } = txMeta;
const type = ApprovalType.Transaction;
Expand All @@ -1216,7 +1242,7 @@ export class TransactionController extends BaseController<
requestData,
expectsResult: true,
},
true,
shouldShowRequest,
)) as Promise<AddResult>;
}

Expand All @@ -1243,6 +1269,16 @@ export class TransactionController extends BaseController<

return { meta: transaction, isCompleted };
}

private getChainAndNetworkId(): {
networkId: string | null;
chainId: Hex;
} {
const { networkId, providerConfig } = this.getNetworkState();
const chainId = providerConfig?.chainId;

return { networkId, chainId };
}
}

export default TransactionController;
76 changes: 75 additions & 1 deletion packages/transaction-controller/src/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import type {
import type { Transaction, TransactionMeta } from './types';
import { TransactionStatus } from './types';
import * as util from './utils';
import { getAndFormatTransactionsForNonceTracker } from './utils';
import {
getAndFormatTransactionsForNonceTracker,
transactionMatchesNetwork,
} from './utils';

const MAX_FEE_PER_GAS = 'maxFeePerGas';
const MAX_PRIORITY_FEE_PER_GAS = 'maxPriorityFeePerGas';
Expand Down Expand Up @@ -306,4 +309,75 @@ describe('utils', () => {
expect(result).toStrictEqual(expectedResult);
});
});

describe('transactionMatchesNetwork', () => {
const transaction: TransactionMeta = {
chainId: '0x1',
networkID: '1',
id: '1',
time: 123456,
transaction: {
from: '0x123',
gas: '0x100',
value: '0x200',
nonce: '0x1',
},
status: TransactionStatus.unapproved,
};
it('returns true if chainId matches', () => {
const chainId = '0x1';
const networkId = '1';
expect(transactionMatchesNetwork(transaction, chainId, networkId)).toBe(
true,
);
});

it('returns false if chainId does not match', () => {
const chainId = '0x1';
const networkId = '1';
expect(
transactionMatchesNetwork(
{ ...transaction, chainId: '0x2' },
chainId,
networkId,
),
).toBe(false);
});

it('returns true if networkID matches', () => {
const chainId = '0x1';
const networkId = '1';
expect(
transactionMatchesNetwork(
{ ...transaction, chainId: undefined },
chainId,
networkId,
),
).toBe(true);
});

it('returns false if networkID does not match', () => {
const chainId = '0x1';
const networkId = '1';
expect(
transactionMatchesNetwork(
{ ...transaction, networkID: '2', chainId: undefined },
chainId,
networkId,
),
).toBe(false);
});

it('returns true if chainId and networkID are undefined', () => {
const chainId = '0x2';
const networkId = '1';
expect(
transactionMatchesNetwork(
{ ...transaction, chainId: undefined, networkID: undefined },
chainId,
networkId,
),
).toBe(false);
});
});
});
24 changes: 24 additions & 0 deletions packages/transaction-controller/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
convertHexToDecimal,
isValidHexAddress,
} from '@metamask/controller-utils';
import type { Hex } from '@metamask/utils';
import { addHexPrefix, isHexString } from 'ethereumjs-util';
import type { Transaction as NonceTrackerTransaction } from 'nonce-tracker/dist/NonceTracker';

Expand Down Expand Up @@ -207,3 +208,26 @@ export function getAndFormatTransactionsForNonceTracker(
};
});
}

/**
* Checks whether a given transaction matches the specified network or chain ID.
* This function is used to determine if a transaction is relevant to the current network or chain.
*
* @param transaction - The transaction metadata to check.
* @param chainId - The chain ID of the current network.
* @param networkId - The network ID of the current network.
* @returns A boolean value indicating whether the transaction matches the current network or chain ID.
*/
export function transactionMatchesNetwork(
transaction: TransactionMeta,
chainId: Hex,
networkId: string | null,
) {
if (transaction.chainId) {
return transaction.chainId === chainId;
}
if (transaction.networkID) {
return transaction.networkID === networkId;
}
return false;
}