Skip to content

Commit

Permalink
feat: add updateAtomicBatchData method (#5380)
Browse files Browse the repository at this point in the history
## Explanation

Add `updateAtomicBatchData` method to update the transaction data of a
single nested transaction within an atomic batch transaction.

Required by the client to update a token approval allowance for example.

## References

## Changelog

See `CHANGELOG.md`.

## Checklist

- [x] I've updated the test suite for new or updated code as appropriate
- [x] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- [x] I've highlighted breaking changes using the "BREAKING" category
above as appropriate
- [x] I've prepared draft pull requests for clients and consumer
packages to resolve any breaking changes
  • Loading branch information
matthewwalsh0 authored Mar 3, 2025
1 parent f3636ad commit c50a4c6
Show file tree
Hide file tree
Showing 3 changed files with 230 additions and 13 deletions.
1 change: 1 addition & 0 deletions packages/transaction-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
123 changes: 123 additions & 0 deletions packages/transaction-controller/src/TransactionController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
);
});
});
});
119 changes: 106 additions & 13 deletions packages/transaction-controller/src/TransactionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) => {
Expand All @@ -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);
},
);

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
});
}
}

0 comments on commit c50a4c6

Please sign in to comment.