diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 2d675e408e3..335e3deb6db 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add `updateAtomicBatchData` method ([#5380](https://github.com/MetaMask/core/pull/5380)) - Support atomic batch transactions ([#5306](https://github.com/MetaMask/core/pull/5306)) - Add methods: - `addTransactionBatch` diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index c317b4e326e..b6325809ee7 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -6136,4 +6136,127 @@ describe('TransactionController', () => { expect(addTransactionBatchMock).toHaveBeenCalledTimes(1); }); }); + + describe('updateAtomicBatchData', () => { + /** + * Template for updateAtomicBatchData test. + * + * @returns The controller instance and function result; + */ + async function updateAtomicBatchDataTemplate() { + const { controller } = setupController({ + options: { + state: { + transactions: [ + { + ...TRANSACTION_META_MOCK, + nestedTransactions: [ + { + to: ACCOUNT_2_MOCK, + data: '0x1234', + }, + { + to: ACCOUNT_2_MOCK, + data: '0x4567', + }, + ], + }, + ], + }, + }, + }); + + const result = await controller.updateAtomicBatchData({ + transactionId: TRANSACTION_META_MOCK.id, + transactionIndex: 1, + transactionData: '0x89AB', + }); + + return { controller, result }; + } + + it('updates transaction params', async () => { + const { controller } = await updateAtomicBatchDataTemplate(); + + expect(controller.state.transactions[0]?.txParams.data).toContain('89ab'); + expect(controller.state.transactions[0]?.txParams.data).not.toContain( + '4567', + ); + }); + + it('updates nested transaction', async () => { + const { controller } = await updateAtomicBatchDataTemplate(); + + expect( + controller.state.transactions[0]?.nestedTransactions?.[1]?.data, + ).toBe('0x89AB'); + }); + + it('returns updated batch transaction data', async () => { + const { result } = await updateAtomicBatchDataTemplate(); + + expect(result).toContain('89ab'); + expect(result).not.toContain('4567'); + }); + + it('updates gas', async () => { + const gasMock = '0x1234'; + const gasLimitNoBufferMock = '0x123'; + const simulationFailsMock = { reason: 'testReason', debug: {} }; + + updateGasMock.mockImplementationOnce(async (request) => { + request.txMeta.txParams.gas = gasMock; + request.txMeta.simulationFails = simulationFailsMock; + request.txMeta.gasLimitNoBuffer = gasLimitNoBufferMock; + }); + + const { controller } = await updateAtomicBatchDataTemplate(); + + const stateTransaction = controller.state.transactions[0]; + + expect(stateTransaction.txParams.gas).toBe(gasMock); + expect(stateTransaction.simulationFails).toStrictEqual( + simulationFailsMock, + ); + expect(stateTransaction.gasLimitNoBuffer).toBe(gasLimitNoBufferMock); + }); + + it('throws if nested transaction does not exist', async () => { + const { controller } = setupController({ + options: { + state: { + transactions: [TRANSACTION_META_MOCK], + }, + }, + }); + + await expect( + controller.updateAtomicBatchData({ + transactionId: TRANSACTION_META_MOCK.id, + transactionIndex: 0, + transactionData: '0x89AB', + }), + ).rejects.toThrow('Nested transaction not found'); + }); + + it('throws if batch transaction does not exist', async () => { + const { controller } = setupController({ + options: { + state: { + transactions: [TRANSACTION_META_MOCK], + }, + }, + }); + + await expect( + controller.updateAtomicBatchData({ + transactionId: 'invalidId', + transactionIndex: 0, + transactionData: '0x89AB', + }), + ).rejects.toThrow( + 'Cannot update transaction as ID not found - invalidId', + ); + }); + }); }); diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 436e20a1ccb..a1a994e6b1c 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -106,7 +106,10 @@ import { SimulationErrorCode, } from './types'; import { addTransactionBatch, isAtomicBatchSupported } from './utils/batch'; -import { signAuthorizationList } from './utils/eip7702'; +import { + generateEIP7702BatchTransaction, + signAuthorizationList, +} from './utils/eip7702'; import { validateConfirmedExternalTransaction } from './utils/external-transactions'; import { addGasBuffer, estimateGas, updateGas } from './utils/gas'; import { updateGasFees } from './utils/gas-fees'; @@ -2351,6 +2354,83 @@ export class TransactionController extends BaseController< this.signAbortCallbacks.delete(transactionId); } + /** + * Update the transaction data of a single nested transaction within an atomic batch transaction. + * + * @param options - The options bag. + * @param options.transactionId - ID of the atomic batch transaction. + * @param options.transactionIndex - Index of the nested transaction within the atomic batch transaction. + * @param options.transactionData - New data to set for the nested transaction. + * @returns The updated data for the atomic batch transaction. + */ + async updateAtomicBatchData({ + transactionId, + transactionIndex, + transactionData, + }: { + transactionId: string; + transactionIndex: number; + transactionData: Hex; + }) { + log('Updating atomic batch data', { + transactionId, + transactionIndex, + transactionData, + }); + + const updatedTransactionMeta = this.#updateTransactionInternal( + { + transactionId, + note: 'TransactionController#updateAtomicBatchData - Atomic batch data updated', + }, + (transactionMeta) => { + const { nestedTransactions, txParams } = transactionMeta; + const from = txParams.from as Hex; + const nestedTransaction = nestedTransactions?.[transactionIndex]; + + if (!nestedTransaction) { + throw new Error( + `Nested transaction not found with index - ${transactionIndex}`, + ); + } + + nestedTransaction.data = transactionData; + + const batchTransaction = generateEIP7702BatchTransaction( + from, + nestedTransactions, + ); + + transactionMeta.txParams.data = batchTransaction.data; + }, + ); + + const draftTransaction = cloneDeep({ + ...updatedTransactionMeta, + txParams: { + ...updatedTransactionMeta.txParams, + // Clear existing gas to force estimation + gas: undefined, + }, + }); + + await this.#updateGasEstimate(draftTransaction); + + this.#updateTransactionInternal( + { + transactionId, + note: 'TransactionController#updateAtomicBatchData - Gas estimate updated', + }, + (transactionMeta) => { + transactionMeta.txParams.gas = draftTransaction.txParams.gas; + transactionMeta.simulationFails = draftTransaction.simulationFails; + transactionMeta.gasLimitNoBuffer = draftTransaction.gasLimitNoBuffer; + }, + ); + + return updatedTransactionMeta.txParams.data as Hex; + } + private addMetadata(transactionMeta: TransactionMeta) { validateTxParams(transactionMeta.txParams); this.update((state) => { @@ -2369,24 +2449,14 @@ export class TransactionController extends BaseController< transactionMeta.txParams.type !== TransactionEnvelopeType.legacy && (await this.getEIP1559Compatibility(transactionMeta.networkClientId)); - const { networkClientId, chainId } = transactionMeta; - - const isCustomNetwork = - this.#multichainTrackingHelper.getNetworkClient({ networkClientId }) - .configuration.type === NetworkClientType.Custom; - + const { networkClientId } = transactionMeta; const ethQuery = this.#getEthQuery({ networkClientId }); const provider = this.#getProvider({ networkClientId }); await this.#trace( { name: 'Update Gas', parentContext: traceContext }, async () => { - await updateGas({ - ethQuery, - chainId, - isCustomNetwork, - txMeta: transactionMeta, - }); + await this.#updateGasEstimate(transactionMeta); }, ); @@ -3569,6 +3639,12 @@ export class TransactionController extends BaseController< ({ id }) => id === transactionId, ); + if (index === -1) { + throw new Error( + `Cannot update transaction as ID not found - ${transactionId}`, + ); + } + let transactionMeta = state.transactions[index]; const originalTransactionMeta = cloneDeep(transactionMeta); @@ -3860,4 +3936,21 @@ export class TransactionController extends BaseController< submitHistory.unshift(submitHistoryEntry); }); } + + async #updateGasEstimate(transactionMeta: TransactionMeta) { + const { chainId, networkClientId } = transactionMeta; + + const isCustomNetwork = + this.#multichainTrackingHelper.getNetworkClient({ networkClientId }) + .configuration.type === NetworkClientType.Custom; + + const ethQuery = this.#getEthQuery({ networkClientId }); + + await updateGas({ + chainId, + ethQuery, + isCustomNetwork, + txMeta: transactionMeta, + }); + } }