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
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,
});
}
}
Loading