Skip to content
Draft
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
4 changes: 4 additions & 0 deletions packages/transaction-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Export `generateEIP7702BatchTransaction` for building EIP-7702 batch transaction calldata from nested calls ([#9143](https://github.com/MetaMask/core/pull/9143))

## [68.0.0]

### Changed
Expand Down
5 changes: 4 additions & 1 deletion packages/transaction-controller/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,10 @@ export {
WalletDevice,
} from './types';
export { mergeGasFeeEstimates } from './utils/gas-flow';
export { decodeAuthorizationSignature } from './utils/eip7702';
export {
decodeAuthorizationSignature,
generateEIP7702BatchTransaction,
} from './utils/eip7702';
export {
isEIP1559Transaction,
normalizeTransactionParams,
Expand Down
162 changes: 98 additions & 64 deletions packages/transaction-controller/src/utils/batch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,17 @@ type IsAtomicBatchSupportedRequestInternal = {
publicKeyEIP7702?: Hex;
};

type PrepareEIP7702BatchTransactionRequest = {
messenger: TransactionControllerMessenger;
publicKeyEIP7702?: Hex;
request: TransactionBatchRequest;
};

type EIP7702BatchTransaction = {
nestedTransactions: NestedTransactionMetadata[];
txParams: TransactionParams;
};

const log = createModuleLogger(projectLogger, 'batch');

export const ERROR_MESSAGE_NO_UPGRADE_CONTRACT =
Expand Down Expand Up @@ -238,79 +249,19 @@ export function generateBatchId(): Hex {
return bytesToHex(idBytes);
}

/**
* Generate the metadata for a nested transaction.
*
* @param request - The batch request.
* @param singleRequest - The request for a single transaction.
* @param messenger - The transaction controller messenger.
* @param networkClientId - The network client ID.
* @returns The metadata for the nested transaction.
*/
async function getNestedTransactionMeta(
request: TransactionBatchRequest,
singleRequest: TransactionBatchSingleRequest,
messenger: TransactionControllerMessenger,
networkClientId: NetworkClientId,
): Promise<NestedTransactionMetadata> {
const { from } = request;
const { params, type: requestedType } = singleRequest;

if (requestedType) {
return {
...params,
type: requestedType,
};
}

const { type: determinedType } = await determineTransactionType(
{ from, ...params },
{ messenger, networkClientId },
);

return {
...params,
type: determinedType,
};
}

/**
* Process a batch transaction using an EIP-7702 transaction.
*
* @param request - The request object including the user request and necessary callbacks.
* @returns The batch result object including the batch ID.
*/
async function addTransactionBatchWith7702(
request: AddTransactionBatchRequest,
): Promise<TransactionBatchResult> {
const {
addTransaction,
messenger,
publicKeyEIP7702,
request: userRequest,
} = request;
async function prepareEIP7702BatchTransaction(
request: PrepareEIP7702BatchTransactionRequest,
): Promise<EIP7702BatchTransaction> {
const { messenger, publicKeyEIP7702, request: userRequest } = request;

const {
atomic,
batchId: batchIdOverride,
disableUpgrade,
from,
gasFeeToken,
gasLimit7702,
isInternal,
networkClientId,
origin,
overwriteUpgrade,
requestId,
requiredAssets,
requireApproval,
securityAlertId,
skipInitialGasEstimate,
transactions,
excludeNativeTokenForFee,
isGasFeeIncluded,
isGasFeeSponsored,
validateSecurity,
} = userRequest;

const chainId = getChainId({ messenger, networkClientId });
Expand Down Expand Up @@ -389,6 +340,89 @@ async function addTransactionBatchWith7702(
txParams.authorizationList = [{ address: upgradeContractAddress }];
}

return { nestedTransactions, txParams };
}

/**
* Generate the metadata for a nested transaction.
*
* @param request - The batch request.
* @param singleRequest - The request for a single transaction.
* @param messenger - The transaction controller messenger.
* @param networkClientId - The network client ID.
* @returns The metadata for the nested transaction.
*/
async function getNestedTransactionMeta(
request: TransactionBatchRequest,
singleRequest: TransactionBatchSingleRequest,
messenger: TransactionControllerMessenger,
networkClientId: NetworkClientId,
): Promise<NestedTransactionMetadata> {
const { from } = request;
const { params, type: requestedType } = singleRequest;

if (requestedType) {
return {
...params,
type: requestedType,
};
}

const { type: determinedType } = await determineTransactionType(
{ from, ...params },
{ messenger, networkClientId },
);

return {
...params,
type: determinedType,
};
}

/**
* Process a batch transaction using an EIP-7702 transaction.
*
* @param request - The request object including the user request and necessary callbacks.
* @returns The batch result object including the batch ID.
*/
async function addTransactionBatchWith7702(
request: AddTransactionBatchRequest,
): Promise<TransactionBatchResult> {
const {
addTransaction,
messenger,
publicKeyEIP7702,
request: userRequest,
} = request;

const {
batchId: batchIdOverride,
gasFeeToken,
isInternal,
networkClientId,
origin,
requestId,
requiredAssets,
requireApproval,
securityAlertId,
skipInitialGasEstimate,
transactions,
excludeNativeTokenForFee,
isGasFeeIncluded,
isGasFeeSponsored,
validateSecurity,
} = userRequest;

const { nestedTransactions, txParams } = await prepareEIP7702BatchTransaction(
{
messenger,
publicKeyEIP7702,
request: userRequest,
},
);

const chainId = getChainId({ messenger, networkClientId });

if (validateSecurity) {
const securityRequest: ValidateSecurityRequest = {
method: 'eth_sendTransaction',
Expand Down
4 changes: 4 additions & 0 deletions packages/transaction-pay-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add Relay quote validation and transaction simulation before Transaction Pay quotes are surfaced ([#9143](https://github.com/MetaMask/core/pull/9143))

## [23.8.0]

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ describe('AcrossStrategy', () => {
transaction: TRANSACTION_META_MOCK,
});

expect(result).toBe(false);
expect(result).toStrictEqual({ isSupported: false });
});

it('supports source 7702 authorization lists for Predict withdraw post-quote quotes without Across actions', () => {
Expand All @@ -390,7 +390,9 @@ describe('AcrossStrategy', () => {
} as TransactionMeta,
} as PayStrategyCheckQuoteSupportRequest<AcrossQuote>;

expect(strategy.checkQuoteSupport(request)).toBe(true);
expect(strategy.checkQuoteSupport(request)).toStrictEqual({
isSupported: true,
});
});

it('does not support first-time 7702 authorization lists for non-post-quote Across quotes', () => {
Expand All @@ -404,7 +406,9 @@ describe('AcrossStrategy', () => {
} as TransactionMeta,
} as PayStrategyCheckQuoteSupportRequest<AcrossQuote>;

expect(strategy.checkQuoteSupport(request)).toBe(false);
expect(strategy.checkQuoteSupport(request)).toStrictEqual({
isSupported: false,
});
});

it('does not support first-time 7702 authorization lists when the Across quote has embedded destination actions', () => {
Expand Down Expand Up @@ -441,7 +445,9 @@ describe('AcrossStrategy', () => {
} as TransactionMeta,
} as PayStrategyCheckQuoteSupportRequest<AcrossQuote>;

expect(strategy.checkQuoteSupport(request)).toBe(false);
expect(strategy.checkQuoteSupport(request)).toStrictEqual({
isSupported: false,
});
});

it('supports 7702 quotes that do not require an authorization list', () => {
Expand All @@ -461,7 +467,7 @@ describe('AcrossStrategy', () => {
quotes: [quote],
transaction: TRANSACTION_META_MOCK,
}),
).toBe(true);
).toStrictEqual({ isSupported: true });
});

it('returns false for unsupported destination actions', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
PayStrategyCheckQuoteSupportRequest,
PayStrategyExecuteRequest,
PayStrategyGetQuotesRequest,
PayStrategyQuoteSupportResult,
TransactionPayQuote,
} from '../../types';
import { getPayStrategiesConfig } from '../../utils/feature-flags';
Expand Down Expand Up @@ -79,7 +80,7 @@ export class AcrossStrategy implements PayStrategy<AcrossQuote> {

checkQuoteSupport(
request: PayStrategyCheckQuoteSupportRequest<AcrossQuote>,
): boolean {
): PayStrategyQuoteSupportResult {
// Gas planning can discover that TransactionController would add an
// authorization list for a first-time 7702 upgrade. `is7702` alone is not a
// blocker because it also covers already-upgraded accounts.
Expand All @@ -88,21 +89,23 @@ export class AcrossStrategy implements PayStrategy<AcrossQuote> {
);

if (!requiresAuthorizationList) {
return true;
return { isSupported: true };
}

if (!isPredictWithdrawTransaction(request.transaction)) {
return false;
return { isSupported: false };
}

// A first-time 7702 authorization list is acceptable here only because it is
// attached to MetaMask's source-chain batch transaction. It must not be
// smuggled into Across destination post-swap actions.
return request.quotes.every(
(quote) =>
quote.request.isPostQuote === true &&
quote.original.request.actions.length === 0,
);
return {
isSupported: request.quotes.every(
(quote) =>
quote.request.isPostQuote === true &&
quote.original.request.actions.length === 0,
),
};
}

async getQuotes(
Expand Down
Loading
Loading