diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 28f942eb7d5..76552eb66be 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Fail required transactions of any approved and signed transactions during initialisation ([#7251](https://github.com/MetaMask/core/pull/7251)) + - Include `isExternalSign` when fetching gas fee tokens in messenger action or before publish check. + ## [62.3.0] ### Changed diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index aaa202a217d..1d0bec05a34 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -1131,6 +1131,72 @@ describe('TransactionController', () => { expect(transactions[5].status).toBe(TransactionStatus.failed); }); + it('fails required transactions of approved and signed transactions', async () => { + const mockTransactionMeta = { + from: ACCOUNT_MOCK, + txParams: { + from: ACCOUNT_MOCK, + to: ACCOUNT_2_MOCK, + }, + }; + const mockedTransactions = [ + { + id: '123', + history: [{ ...mockTransactionMeta, id: '123' }], + chainId: toHex(5), + status: TransactionStatus.approved, + requiredTransactionIds: ['222', '333'], + ...mockTransactionMeta, + }, + { + id: '111', + history: [{ ...mockTransactionMeta, id: '111' }], + chainId: toHex(5), + status: TransactionStatus.signed, + ...mockTransactionMeta, + }, + { + id: '222', + history: [{ ...mockTransactionMeta, id: '222' }], + chainId: toHex(1), + status: TransactionStatus.confirmed, + ...mockTransactionMeta, + }, + { + id: '333', + history: [{ ...mockTransactionMeta, id: '333' }], + chainId: toHex(16), + status: TransactionStatus.submitted, + ...mockTransactionMeta, + }, + ]; + + const mockedControllerState = { + transactions: mockedTransactions, + methodData: {}, + lastFetchedBlockNumbers: {}, + }; + + const { controller } = setupController({ + options: { + // 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.map((t) => [t.id, t.status])).toStrictEqual([ + ['333', TransactionStatus.failed], + ['222', TransactionStatus.confirmed], + ['111', TransactionStatus.failed], + ['123', TransactionStatus.failed], + ]); + }); + it('removes unapproved transactions', async () => { const mockTransactionMeta = { from: ACCOUNT_MOCK, @@ -8498,7 +8564,7 @@ describe('TransactionController', () => { expect(result).toStrictEqual([GAS_FEE_TOKEN_MOCK]); }); - it('includes delegation address in request', async () => { + it('includes delegation address and isExternalSign in request', async () => { const { messenger } = setupController(); getGasFeeTokensMock.mockResolvedValueOnce({ @@ -8520,6 +8586,7 @@ describe('TransactionController', () => { expect.objectContaining({ transactionMeta: expect.objectContaining({ delegationAddress: ACCOUNT_2_MOCK, + isExternalSign: true, }), }), ); diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 0d76fd01814..aaf8812d6fa 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -2996,6 +2996,27 @@ export class TransactionController extends BaseController< transactionMeta, new Error('Transaction incomplete at startup'), ); + + const requiredTransactionIds = + transactionMeta.requiredTransactionIds ?? []; + + for (const requiredTransactionId of requiredTransactionIds) { + const requiredTransactionMeta = this.#getTransaction( + requiredTransactionId, + ); + + if ( + !requiredTransactionMeta || + this.#isFinalState(requiredTransactionMeta.status) + ) { + continue; + } + + this.#failTransaction( + requiredTransactionMeta, + new Error('Parent transaction incomplete at startup'), + ); + } } } @@ -3422,7 +3443,8 @@ export class TransactionController extends BaseController< return ( status === TransactionStatus.rejected || status === TransactionStatus.confirmed || - status === TransactionStatus.failed + status === TransactionStatus.failed || + status === TransactionStatus.dropped ); } @@ -4737,6 +4759,7 @@ export class TransactionController extends BaseController< const transaction = { chainId, delegationAddress, + isExternalSign: true, txParams: { data, from, diff --git a/packages/transaction-controller/src/utils/gas-fee-tokens.test.ts b/packages/transaction-controller/src/utils/gas-fee-tokens.test.ts index cd89a1eafc5..de2908ddfd5 100644 --- a/packages/transaction-controller/src/utils/gas-fee-tokens.test.ts +++ b/packages/transaction-controller/src/utils/gas-fee-tokens.test.ts @@ -431,6 +431,11 @@ describe('Gas Fee Tokens Utils', () => { await checkGasFeeTokenBeforePublish(request); expect(request.fetchGasFeeTokens).toHaveBeenCalledTimes(1); + expect(request.fetchGasFeeTokens).toHaveBeenCalledWith( + expect.objectContaining({ + isExternalSign: true, + }), + ); }); it('sets external sign to true if gas fee token found', async () => { diff --git a/packages/transaction-controller/src/utils/gas-fee-tokens.ts b/packages/transaction-controller/src/utils/gas-fee-tokens.ts index 1f7d6e94852..c1cec4736a9 100644 --- a/packages/transaction-controller/src/utils/gas-fee-tokens.ts +++ b/packages/transaction-controller/src/utils/gas-fee-tokens.ts @@ -166,7 +166,10 @@ export async function checkGasFeeTokenBeforePublish({ return; } - const gasFeeTokens = await fetchGasFeeTokens(transaction); + const gasFeeTokens = await fetchGasFeeTokens({ + ...transaction, + isExternalSign: true, + }); updateTransaction(transaction.id, (tx) => { tx.gasFeeTokens = gasFeeTokens;