Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support atomic batch transactions #30271

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
42 changes: 42 additions & 0 deletions .yarn/patches/@ethereumjs-tx-npm-5.4.0-0c4a0f973e.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
diff --git a/dist/cjs/util.js b/dist/cjs/util.js
index d60cf924ee8144ee49dd07e8e910d87a2c4bfb6a..5651d71e272db4c507956d162b89da7a70b9c97f 100644
--- a/dist/cjs/util.js
+++ b/dist/cjs/util.js
@@ -119,10 +119,7 @@ class AuthorizationLists {
}
const chainId = (0, util_1.hexToBytes)(item.chainId);
const addressBytes = (0, util_1.hexToBytes)(item.address);
- const nonceList = [];
- for (let j = 0; j < item.nonce.length; j++) {
- nonceList.push((0, util_1.hexToBytes)(item.nonce[j]));
- }
+ const nonceList = (0, util_1.hexToBytes)(item.nonce);
const yParity = (0, util_1.hexToBytes)(item.yParity);
const r = (0, util_1.hexToBytes)(item.r);
const s = (0, util_1.hexToBytes)(item.s);
@@ -138,11 +135,7 @@ class AuthorizationLists {
const data = bufferAuthorizationList[i];
const chainId = (0, util_1.bytesToHex)(data[0]);
const address = (0, util_1.bytesToHex)(data[1]);
- const nonces = data[2];
- const nonceList = [];
- for (let j = 0; j < nonces.length; j++) {
- nonceList.push((0, util_1.bytesToHex)(nonces[j]));
- }
+ const nonceList = (0, util_1.bytesToHex)(data[2]);
const yParity = (0, util_1.bytesToHex)(data[3]);
const r = (0, util_1.bytesToHex)(data[4]);
const s = (0, util_1.bytesToHex)(data[5]);
@@ -175,12 +168,6 @@ class AuthorizationLists {
if (address.length !== 20) {
throw new Error('Invalid EIP-7702 transaction: address length should be 20 bytes');
}
- if (nonceList.length > 1) {
- throw new Error('Invalid EIP-7702 transaction: nonce list should consist of at most 1 item');
- }
- else if (nonceList.length === 1) {
- (0, util_1.validateNoLeadingZeroes)({ nonce: nonceList[0] });
- }
}
}
static getDataFeeEIP7702(authorityList, common) {

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { AccountsControllerGetSelectedAccountAction } from '@metamask/accounts-controller';
import {
AccountsControllerGetSelectedAccountAction,
AccountsControllerGetStateAction,
} from '@metamask/accounts-controller';
import { ApprovalControllerActions } from '@metamask/approval-controller';
import { Messenger } from '@metamask/base-controller';
import {
Expand All @@ -21,6 +24,8 @@ import {
TransactionControllerUnapprovedTransactionAddedEvent,
} from '@metamask/transaction-controller';
import { SmartTransactionsControllerSmartTransactionEvent } from '@metamask/smart-transactions-controller';
import { RemoteFeatureFlagControllerGetStateAction } from '@metamask/remote-feature-flag-controller';
import { KeyringControllerSignEip7702AuthorizationAction } from '@metamask/keyring-controller';
import {
SwapsControllerSetApproveTxIdAction,
SwapsControllerSetTradeTxIdAction,
Expand All @@ -29,9 +34,12 @@ import {
type MessengerActions =
| ApprovalControllerActions
| AccountsControllerGetSelectedAccountAction
| AccountsControllerGetStateAction
| KeyringControllerSignEip7702AuthorizationAction
| NetworkControllerFindNetworkClientIdByChainIdAction
| NetworkControllerGetEIP1559CompatibilityAction
| NetworkControllerGetNetworkClientByIdAction
| RemoteFeatureFlagControllerGetStateAction
| SwapsControllerSetApproveTxIdAction
| SwapsControllerSetTradeTxIdAction;

Expand Down Expand Up @@ -60,9 +68,12 @@ export function getTransactionControllerMessenger(
name: 'TransactionController',
allowedActions: [
'AccountsController:getSelectedAccount',
'AccountsController:getState',
`ApprovalController:addRequest`,
'KeyringController:signEip7702Authorization',
'NetworkController:findNetworkClientIdByChainId',
'NetworkController:getNetworkClientById',
'RemoteFeatureFlagController:getState',
],
allowedEvents: [`NetworkController:stateChange`],
});
Expand Down Expand Up @@ -92,6 +103,7 @@ export function getTransactionControllerInitMessenger(
'ApprovalController:startFlow',
'ApprovalController:updateRequestState',
'NetworkController:getEIP1559Compatibility',
'RemoteFeatureFlagController:getState',
'SwapsController:setApproveTxId',
'SwapsController:setTradeTxId',
],
Expand Down
3 changes: 3 additions & 0 deletions app/scripts/controllers/permissions/specifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,13 @@ export const unrestrictedMethods = Object.freeze([
'personal_ecRecover',
'personal_sign',
'wallet_addEthereumChain',
'wallet_getCallsStatus',
'wallet_getCapabilities',
'wallet_getPermissions',
'wallet_requestPermissions',
'wallet_revokePermissions',
'wallet_registerOnboarding',
'wallet_sendCalls',
'wallet_switchEthereumChain',
'wallet_watchAsset',
'web3_clientVersion',
Expand Down
6 changes: 6 additions & 0 deletions app/scripts/lib/createMetamaskMiddleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ import {
export default function createMetamaskMiddleware({
version,
getAccounts,
getCapabilities,
getTransactionReceiptsByBatchId,
processTransaction,
processTypedMessage,
processTypedMessageV3,
processTypedMessageV4,
processPersonalMessage,
processDecryptMessage,
processEncryptionPublicKey,
processSendCalls,
getPendingNonce,
getPendingTransactionByHash,
}) {
Expand All @@ -28,13 +31,16 @@ export default function createMetamaskMiddleware({
}),
createWalletMiddleware({
getAccounts,
getCapabilities,
getTransactionReceiptsByBatchId,
processTransaction,
processTypedMessage,
processTypedMessageV3,
processTypedMessageV4,
processPersonalMessage,
processDecryptMessage,
processEncryptionPublicKey,
processSendCalls,
}),
createPendingNonceMiddleware({ getPendingNonce }),
createPendingTxMiddleware({ getPendingTransactionByHash }),
Expand Down
185 changes: 185 additions & 0 deletions app/scripts/lib/transaction/eip5792.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import {
TransactionController,
TransactionControllerGetStateAction,
TransactionControllerState,
} from '@metamask/transaction-controller';
import {
AutoManagedNetworkClient,
CustomNetworkClientConfiguration,
NetworkControllerGetNetworkClientByIdAction,
} from '@metamask/network-controller';
import { SendCalls, SendCallsParams } from '@metamask/eth-json-rpc-middleware';
import { JsonRpcRequest } from '@metamask/utils';
import { Messenger } from '@metamask/base-controller';
import {
EIP5792Messenger,
getCapabilities,
getTransactionReceiptsByBatchId,
processSendCalls,
} from './eip5792';

const CHAIN_ID_MOCK = '0x123';
const CHAIN_ID_2_MOCK = '0xabc';
const BATCH_ID_MOCK = '123-456';
const NETWORK_CLIENT_ID_MOCK = 'test-client';
const ORIGIN_MOCK = 'test.com';

const RECEIPT_MOCK = {
status: '0x1',
transactionHash: '0x123',
};

const RECEIPT_2_MOCK = {
status: '0x0',
transactionHash: '0x123456',
};

const SEND_CALLS_MOCK: SendCalls = {
version: '1.0',
calls: [{ to: '0x123' }],
chainId: CHAIN_ID_MOCK,
from: '0x123',
};

const REQUEST_MOCK = {
id: 1,
jsonrpc: '2.0',
method: 'wallet_sendCalls',
networkClientId: NETWORK_CLIENT_ID_MOCK,
origin: ORIGIN_MOCK,
params: [SEND_CALLS_MOCK],
} as JsonRpcRequest<SendCallsParams> & { networkClientId: string };

describe('EIP-5792', () => {
let addTransactionBatchMock: jest.MockedFn<
TransactionController['addTransactionBatch']
>;

let isAtomicBatchSupportedMock: jest.MockedFn<
TransactionController['isAtomicBatchSupported']
>;

let getNetworkClientByIdMock: jest.MockedFn<
NetworkControllerGetNetworkClientByIdAction['handler']
>;

let getTransactionControllerStateMock: jest.MockedFn<
TransactionControllerGetStateAction['handler']
>;

let messenger: EIP5792Messenger;

beforeEach(() => {
jest.resetAllMocks();

addTransactionBatchMock = jest.fn();
isAtomicBatchSupportedMock = jest.fn();
getTransactionControllerStateMock = jest.fn();
getNetworkClientByIdMock = jest.fn();

messenger = new Messenger();

messenger.registerActionHandler(
'NetworkController:getNetworkClientById',
getNetworkClientByIdMock,
);

messenger.registerActionHandler(
'TransactionController:getState',
getTransactionControllerStateMock,
);

getNetworkClientByIdMock.mockReturnValue({
configuration: {
chainId: CHAIN_ID_MOCK,
},
} as unknown as AutoManagedNetworkClient<CustomNetworkClientConfiguration>);

addTransactionBatchMock.mockResolvedValue({ batchId: BATCH_ID_MOCK });
});

describe('processSendCalls', () => {
it('calls adds transaction batch hook', async () => {
await processSendCalls(
{ addTransactionBatch: addTransactionBatchMock },
messenger,
SEND_CALLS_MOCK,
REQUEST_MOCK,
);

expect(addTransactionBatchMock).toHaveBeenCalledWith({
from: SEND_CALLS_MOCK.from,
networkClientId: NETWORK_CLIENT_ID_MOCK,
origin: ORIGIN_MOCK,
transactions: [{ params: SEND_CALLS_MOCK.calls[0] }],
});
});

it('returns batch ID from hook', async () => {
expect(
await processSendCalls(
{ addTransactionBatch: addTransactionBatchMock },
messenger,
SEND_CALLS_MOCK,
REQUEST_MOCK,
),
).toBe(BATCH_ID_MOCK);
});

it('throws if chain ID does not match network client', async () => {
await expect(
processSendCalls(
{ addTransactionBatch: addTransactionBatchMock },
messenger,
{ ...SEND_CALLS_MOCK, chainId: CHAIN_ID_2_MOCK },
REQUEST_MOCK,
),
).rejects.toThrow(
`Chain ID must match the dApp selected network: Got ${CHAIN_ID_2_MOCK}, expected ${CHAIN_ID_MOCK}`,
);
});
});

describe('getTransactionReceiptsByBatchId', () => {
it('returns transaction receipts from transaction controller with matching ID', () => {
getTransactionControllerStateMock.mockReturnValueOnce({
transactions: [
{ id: BATCH_ID_MOCK, txReceipt: RECEIPT_MOCK },
{ id: '456-789', txReceipt: {} },
{ id: BATCH_ID_MOCK, txReceipt: RECEIPT_2_MOCK },
],
} as TransactionControllerState);

expect(
getTransactionReceiptsByBatchId(messenger, BATCH_ID_MOCK),
).toStrictEqual([RECEIPT_MOCK, RECEIPT_2_MOCK]);
});
});

describe('getCapabilities', () => {
it('returns atomic batch capabilities using hook', async () => {
isAtomicBatchSupportedMock.mockResolvedValueOnce([
CHAIN_ID_MOCK,
CHAIN_ID_2_MOCK,
]);

expect(
await getCapabilities(
{ isAtomicBatchSupported: isAtomicBatchSupportedMock },
'0x123',
),
).toStrictEqual({
[CHAIN_ID_MOCK]: {
atomicBatch: {
supported: true,
},
},
[CHAIN_ID_2_MOCK]: {
atomicBatch: {
supported: true,
},
},
});
});
});
});
Loading
Loading