diff --git a/eslint-warning-thresholds.json b/eslint-warning-thresholds.json index 5658cfd8e1a..e777270690c 100644 --- a/eslint-warning-thresholds.json +++ b/eslint-warning-thresholds.json @@ -575,12 +575,8 @@ "promise/always-return": 2 }, "packages/transaction-controller/src/TransactionController.ts": { - "@typescript-eslint/prefer-readonly": 11, "jsdoc/check-tag-names": 35, - "jsdoc/require-returns": 5, - "jsdoc/tag-lines": 1, - "prettier/prettier": 1, - "no-unused-private-class-members": 1 + "jsdoc/require-returns": 5 }, "packages/transaction-controller/src/TransactionControllerIntegration.test.ts": { "import-x/order": 4, @@ -612,9 +608,6 @@ "import-x/order": 1, "jsdoc/tag-lines": 1 }, - "packages/transaction-controller/src/gas-flows/OracleLayer1GasFeeFlow.ts": { - "@typescript-eslint/prefer-readonly": 2 - }, "packages/transaction-controller/src/gas-flows/ScrollLayer1GasFeeFlow.test.ts": { "import-x/order": 1 }, @@ -673,9 +666,6 @@ "@typescript-eslint/prefer-readonly": 1, "jsdoc/tag-lines": 2 }, - "packages/transaction-controller/src/types.ts": { - "jsdoc/tag-lines": 4 - }, "packages/transaction-controller/src/utils/external-transactions.test.ts": { "import-x/order": 1 }, @@ -764,13 +754,6 @@ "packages/transaction-controller/src/utils/utils.test.ts": { "import-x/order": 1 }, - "packages/transaction-controller/src/utils/validation.test.ts": { - "import-x/order": 1 - }, - "packages/transaction-controller/src/utils/validation.ts": { - "@typescript-eslint/no-unsafe-enum-comparison": 2, - "import-x/order": 1 - }, "packages/user-operation-controller/src/UserOperationController.test.ts": { "jsdoc/tag-lines": 4 }, diff --git a/packages/transaction-controller/jest.config.js b/packages/transaction-controller/jest.config.js index e6f555ff0c9..8072dc420d7 100644 --- a/packages/transaction-controller/jest.config.js +++ b/packages/transaction-controller/jest.config.js @@ -18,7 +18,7 @@ module.exports = merge(baseConfig, { coverageThreshold: { global: { branches: 91.76, - functions: 94.76, + functions: 94.62, lines: 96.83, statements: 96.82, }, diff --git a/packages/transaction-controller/package.json b/packages/transaction-controller/package.json index 5297b14771f..5816cb5720f 100644 --- a/packages/transaction-controller/package.json +++ b/packages/transaction-controller/package.json @@ -47,8 +47,8 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@ethereumjs/common": "^3.2.0", - "@ethereumjs/tx": "^4.2.0", + "@ethereumjs/common": "^4.4.0", + "@ethereumjs/tx": "^5.4.0", "@ethereumjs/util": "^8.1.0", "@ethersproject/abi": "^5.7.0", "@ethersproject/contracts": "^5.7.0", diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 729454d13db..54e763c1077 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -1,7 +1,4 @@ -import { Hardfork, Common, type ChainConfig } from '@ethereumjs/common'; import type { TypedTransaction } from '@ethereumjs/tx'; -import { TransactionFactory } from '@ethereumjs/tx'; -import { bufferToHex } from '@ethereumjs/util'; import type { AccountsControllerGetSelectedAccountAction } from '@metamask/accounts-controller'; import type { AcceptResultCallbacks, @@ -94,6 +91,8 @@ import { TransactionStatus, SimulationErrorCode, } from './types'; +import type { KeyringControllerSignAuthorization } from './utils/eip7702'; +import { signAuthorizationList } from './utils/eip7702'; import { validateConfirmedExternalTransaction } from './utils/external-transactions'; import { addGasBuffer, estimateGas, updateGas } from './utils/gas'; import { updateGasFees } from './utils/gas-fees'; @@ -110,6 +109,7 @@ import { getAndFormatTransactionsForNonceTracker, getNextNonce, } from './utils/nonce'; +import { prepareTransaction, serializeTransaction } from './utils/prepare'; import type { ResimulateResponse } from './utils/resimulate'; import { hasSimulationDataChanged, shouldResimulate } from './utils/resimulate'; import { getTransactionParamsWithIncreasedGasFee } from './utils/retry'; @@ -156,7 +156,6 @@ const metadata = { }, }; -export const HARDFORK = Hardfork.London; const SUBMIT_HISTORY_LIMIT = 100; /** @@ -338,10 +337,11 @@ const controllerName = 'TransactionController'; * The external actions available to the {@link TransactionController}. */ export type AllowedActions = + | AccountsControllerGetSelectedAccountAction | AddApprovalRequest + | KeyringControllerSignAuthorization | NetworkControllerFindNetworkClientIdByChainIdAction - | NetworkControllerGetNetworkClientByIdAction - | AccountsControllerGetSelectedAccountAction; + | NetworkControllerGetNetworkClientByIdAction; /** * The external events available to the {@link TransactionController}. @@ -578,7 +578,7 @@ export class TransactionController extends BaseController< TransactionControllerState, TransactionControllerMessenger > { - #internalEvents = new EventEmitter(); + readonly #internalEvents = new EventEmitter(); private readonly isHistoryDisabled: boolean; @@ -588,7 +588,7 @@ export class TransactionController extends BaseController< private readonly approvingTransactionIds: Set = new Set(); - #methodDataHelper: MethodDataHelper; + readonly #methodDataHelper: MethodDataHelper; private readonly mutex = new Mutex(); @@ -617,9 +617,9 @@ export class TransactionController extends BaseController< chainId?: string, ) => NonceTrackerTransaction[]; - #incomingTransactionChainIds: Set = new Set(); + readonly #incomingTransactionChainIds: Set = new Set(); - #incomingTransactionHelper: IncomingTransactionHelper; + readonly #incomingTransactionHelper: IncomingTransactionHelper; private readonly layer1GasFeeFlows: Layer1GasFeeFlow[]; @@ -633,15 +633,15 @@ export class TransactionController extends BaseController< private readonly signAbortCallbacks: Map void> = new Map(); - #trace: TraceCallback; + readonly #trace: TraceCallback; - #transactionHistoryLimit: number; + readonly #transactionHistoryLimit: number; - #isFirstTimeInteractionEnabled: () => boolean; + readonly #isFirstTimeInteractionEnabled: () => boolean; - #isSimulationEnabled: () => boolean; + readonly #isSimulationEnabled: () => boolean; - #testGasFeeFlows: boolean; + readonly #testGasFeeFlows: boolean; private readonly afterSign: ( transactionMeta: TransactionMeta, @@ -716,7 +716,7 @@ export class TransactionController extends BaseController< ); } - #multichainTrackingHelper: MultichainTrackingHelper; + readonly #multichainTrackingHelper: MultichainTrackingHelper; /** * Method used to sign transactions @@ -1016,20 +1016,25 @@ export class TransactionController extends BaseController< ); } - const isEIP1559Compatible = await this.getEIP1559Compatibility( - networkClientId, - ); + const permittedAddresses = + origin === undefined + ? undefined + : await this.getPermittedAccounts?.(origin); - validateTxParams(txParams, isEIP1559Compatible); + const selectedAddress = this.#getSelectedAccount().address; - if (origin && this.getPermittedAccounts) { - await validateTransactionOrigin( - await this.getPermittedAccounts(origin), - this.#getSelectedAccount().address, - txParams.from, - origin, - ); - } + await validateTransactionOrigin({ + from: txParams.from, + origin, + permittedAddresses, + selectedAddress, + txParams, + }); + + const isEIP1559Compatible = + await this.getEIP1559Compatibility(networkClientId); + + validateTxParams(txParams, isEIP1559Compatible); const dappSuggestedGasFees = this.generateDappSuggestedGasFees( txParams, @@ -1309,7 +1314,7 @@ export class TransactionController extends BaseController< prepareTransactionParams?.(newTxParams); - const unsignedEthTx = this.prepareUnsignedEthTx( + const unsignedEthTx = prepareTransaction( transactionMeta.chainId, newTxParams, ); @@ -1324,7 +1329,7 @@ export class TransactionController extends BaseController< signedTx, ); - const rawTx = bufferToHex(signedTx.serialize()); + const rawTx = serializeTransaction(signedTx); const newFee = newTxParams.maxFeePerGas ?? newTxParams.gasPrice; const oldFee = newTxParams.maxFeePerGas @@ -1879,18 +1884,14 @@ export class TransactionController extends BaseController< const initialTx = listOfTxParams[0]; const { chainId } = initialTx; - const common = this.getCommonConfiguration(chainId); const networkClientId = this.#getNetworkClientId({ chainId }); - - const initialTxAsEthTx = TransactionFactory.fromTxData(initialTx, { - common, - }); - - const initialTxAsSerializedHex = bufferToHex(initialTxAsEthTx.serialize()); + const initialTxAsEthTx = prepareTransaction(chainId, initialTx); + const initialTxAsSerializedHex = serializeTransaction(initialTxAsEthTx); if (this.approvingTransactionIds.has(initialTxAsSerializedHex)) { return ''; } + this.approvingTransactionIds.add(initialTxAsSerializedHex); let rawTransactions, nonceLock; @@ -2199,14 +2200,15 @@ export class TransactionController extends BaseController< }; const { from } = updatedTransactionParams; - const common = this.getCommonConfiguration(chainId); - const unsignedTransaction = TransactionFactory.fromTxData( + + const unsignedTransaction = prepareTransaction( + chainId, updatedTransactionParams, - { common }, ); + const signedTransaction = await this.sign(unsignedTransaction, from); + const rawTransaction = serializeTransaction(signedTransaction); - const rawTransaction = bufferToHex(signedTransaction.serialize()); return rawTransaction; } @@ -2225,6 +2227,7 @@ export class TransactionController extends BaseController< /** * Stop the signing process for a specific transaction. * Throws an error causing the transaction status to be set to failed. + * * @param transactionId - The ID of the transaction to stop signing. */ abortTransactionSigning(transactionId: string) { @@ -2513,18 +2516,17 @@ export class TransactionController extends BaseController< note: 'TransactionController#approveTransaction - Transaction approved', }, (draftTxMeta) => { - const { txParams, chainId } = draftTxMeta; + const { chainId, txParams } = draftTxMeta; + const { gas, type } = txParams; draftTxMeta.status = TransactionStatus.approved; - draftTxMeta.txParams = { - ...txParams, - nonce, - chainId, - gasLimit: txParams.gas, - ...(isEIP1559Transaction(txParams) && { - type: TransactionEnvelopeType.feeMarket, - }), - }; + draftTxMeta.txParams.chainId = chainId; + draftTxMeta.txParams.gasLimit = gas; + draftTxMeta.txParams.nonce = nonce; + + if (!type && isEIP1559Transaction(txParams)) { + draftTxMeta.txParams.type = TransactionEnvelopeType.feeMarket; + } }, ); @@ -2669,8 +2671,6 @@ export class TransactionController extends BaseController< updatedTransactionMeta, ); this.#internalEvents.emit( - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `${transactionMeta.id}:finished`, updatedTransactionMeta, ); @@ -2706,8 +2706,6 @@ export class TransactionController extends BaseController< const { chainId, status, txParams, time } = tx; if (txParams) { - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions const key = `${String(txParams.nonce)}-${convertHexToDecimal( chainId, )}-${new Date(time).toDateString()}`; @@ -2873,35 +2871,6 @@ export class TransactionController extends BaseController< }).provider; } - private prepareUnsignedEthTx( - chainId: Hex, - txParams: TransactionParams, - ): TypedTransaction { - return TransactionFactory.fromTxData(txParams, { - freeze: false, - common: this.getCommonConfiguration(chainId), - }); - } - - /** - * `@ethereumjs/tx` uses `@ethereumjs/common` as a configuration tool for - * specifying which chain, network, hardfork and EIPs to support for - * a transaction. By referencing this configuration, and analyzing the fields - * specified in txParams, @ethereumjs/tx is able to determine which EIP-2718 - * transaction type to use. - * - * @param chainId - The chainId to use for the configuration. - * @returns common configuration object - */ - private getCommonConfiguration(chainId: Hex): Common { - const customChainParams: Partial = { - chainId: parseInt(chainId, 16), - defaultHardfork: HARDFORK, - }; - - return Common.custom(customChainParams); - } - private onIncomingTransactions(transactions: TransactionMeta[]) { if (!transactions.length) { return; @@ -3157,9 +3126,18 @@ export class TransactionController extends BaseController< ): Promise { log('Signing transaction', txParams); - const unsignedEthTx = this.prepareUnsignedEthTx( + const { authorizationList, from } = txParams; + const finalTxParams = { ...txParams }; + + finalTxParams.authorizationList = await signAuthorizationList({ + authorizationList, + messenger: this.messagingSystem, + transactionMeta, + }); + + const unsignedEthTx = prepareTransaction( transactionMeta.chainId, - txParams, + finalTxParams, ); this.approvingTransactionIds.add(transactionMeta.id); @@ -3167,7 +3145,7 @@ export class TransactionController extends BaseController< const signedTx = await new Promise((resolve, reject) => { this.sign?.( unsignedEthTx, - txParams.from, + from, ...this.getAdditionalSignArguments(transactionMeta), ).then(resolve, reject); @@ -3207,7 +3185,7 @@ export class TransactionController extends BaseController< this.onTransactionStatusChange(transactionMetaWithRsv); - const rawTx = bufferToHex(signedTx.serialize()); + const rawTx = serializeTransaction(signedTx); const transactionMetaWithRawTx = merge({}, transactionMetaWithRsv, { rawTx, @@ -3356,7 +3334,7 @@ export class TransactionController extends BaseController< return pendingTransactionTracker; } - #checkForPendingTransactionAndStartPolling = () => { + readonly #checkForPendingTransactionAndStartPolling = () => { this.#multichainTrackingHelper.checkForPendingTransactionAndStartPolling(); }; @@ -3364,15 +3342,6 @@ export class TransactionController extends BaseController< this.#multichainTrackingHelper.stopAllTracking(); } - #removeIncomingTransactionHelperListeners( - incomingTransactionHelper: IncomingTransactionHelper, - ) { - incomingTransactionHelper.hub.removeAllListeners('transactions'); - incomingTransactionHelper.hub.removeAllListeners( - 'updated-last-fetched-timestamp', - ); - } - #addIncomingTransactionHelperListeners( incomingTransactionHelper: IncomingTransactionHelper, ) { diff --git a/packages/transaction-controller/src/gas-flows/OracleLayer1GasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/OracleLayer1GasFeeFlow.ts index 6b1cc4b9820..a4b80adb26e 100644 --- a/packages/transaction-controller/src/gas-flows/OracleLayer1GasFeeFlow.ts +++ b/packages/transaction-controller/src/gas-flows/OracleLayer1GasFeeFlow.ts @@ -1,5 +1,3 @@ -import { Common, Hardfork } from '@ethereumjs/common'; -import { TransactionFactory } from '@ethereumjs/tx'; import { Contract } from '@ethersproject/contracts'; import { Web3Provider, type ExternalProvider } from '@ethersproject/providers'; import type { Hex } from '@metamask/utils'; @@ -13,6 +11,7 @@ import type { Layer1GasFeeFlowResponse, TransactionMeta, } from '../types'; +import { prepareTransaction } from '../utils/prepare'; const log = createModuleLogger(projectLogger, 'oracle-layer1-gas-fee-flow'); @@ -33,9 +32,9 @@ const GAS_PRICE_ORACLE_ABI = [ * Layer 1 gas fee flow that obtains gas fee estimate using an oracle smart contract. */ export abstract class OracleLayer1GasFeeFlow implements Layer1GasFeeFlow { - #oracleAddress: Hex; + readonly #oracleAddress: Hex; - #signTransaction: boolean; + readonly #signTransaction: boolean; constructor(oracleAddress: Hex, signTransaction?: boolean) { this.#oracleAddress = oracleAddress; @@ -88,11 +87,9 @@ export abstract class OracleLayer1GasFeeFlow implements Layer1GasFeeFlow { sign: boolean, ) { const txParams = this.#buildTransactionParams(transactionMeta); - const common = this.#buildTransactionCommon(transactionMeta); + const { chainId } = transactionMeta; - let unserializedTransaction = TransactionFactory.fromTxData(txParams, { - common, - }); + let unserializedTransaction = prepareTransaction(chainId, txParams); if (sign) { const keyBuffer = Buffer.from(DUMMY_KEY, 'hex'); @@ -110,13 +107,4 @@ export abstract class OracleLayer1GasFeeFlow implements Layer1GasFeeFlow { gasLimit: transactionMeta.txParams.gas, }; } - - #buildTransactionCommon(transactionMeta: TransactionMeta) { - const chainId = Number(transactionMeta.chainId); - - return Common.custom({ - chainId, - defaultHardfork: Hardfork.London, - }); - } } diff --git a/packages/transaction-controller/src/index.ts b/packages/transaction-controller/src/index.ts index d2b3eeab45c..dcac9daa701 100644 --- a/packages/transaction-controller/src/index.ts +++ b/packages/transaction-controller/src/index.ts @@ -25,12 +25,13 @@ export type { TransactionControllerOptions, } from './TransactionController'; export { - HARDFORK, CANCEL_RATE, SPEED_UP_RATE, TransactionController, } from './TransactionController'; export type { + Authorization, + AuthorizationList, DappSuggestedGasFees, DefaultGasEstimates, FeeMarketEIP1559Values, @@ -81,3 +82,4 @@ export { } from './utils/utils'; export { CHAIN_IDS } from './constants'; export { SUPPORTED_CHAIN_IDS as INCOMING_TRANSACTIONS_SUPPORTED_CHAIN_IDS } from './helpers/AccountsApiRemoteTransactionSource'; +export { HARDFORK } from './utils/prepare'; diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index acf29eace65..5d7ec9d5895 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -9,8 +9,6 @@ import type { Operation } from 'fast-json-patch'; /** * Given a record, ensures that each property matches the `Json` type. */ -// TODO: Either fix this lint violation or explain why it's necessary to ignore. -// eslint-disable-next-line @typescript-eslint/naming-convention type MakeJsonCompatible = T extends Json ? T : { @@ -478,70 +476,52 @@ export enum TransactionStatus { /** * The initial state of a transaction before user approval. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention unapproved = 'unapproved', /** * The transaction has been approved by the user but is not yet signed. * This status is usually brief but may be longer for scenarios like hardware wallet usage. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention approved = 'approved', /** * The transaction is signed and in the process of being submitted to the network. * This status is typically short-lived but can be longer for certain cases, such as smart transactions. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention signed = 'signed', /** * The transaction has been submitted to the network and is awaiting confirmation. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention submitted = 'submitted', /** * The transaction has been successfully executed and confirmed on the blockchain. * This is a final state. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention confirmed = 'confirmed', /** * The transaction encountered an error during execution on the blockchain and failed. * This is a final state. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention failed = 'failed', /** * The transaction was superseded by another transaction, resulting in its dismissal. * This is a final state. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention dropped = 'dropped', /** * The transaction was rejected by the user and not processed further. * This is a final state. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention rejected = 'rejected', /** * @deprecated This status is no longer used. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention cancelled = 'cancelled', } @@ -549,16 +529,11 @@ export enum TransactionStatus { * Options for wallet device. */ export enum WalletDevice { - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention MM_MOBILE = 'metamask_mobile', - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention MM_EXTENSION = 'metamask_extension', OTHER = 'other_device', } -/* eslint-disable @typescript-eslint/naming-convention */ /** * The type of the transaction. */ @@ -707,7 +682,6 @@ export enum TransactionType { */ tokenMethodIncreaseAllowance = 'increaseAllowance', } -/* eslint-enable @typescript-eslint/naming-convention */ /** * Standard data concerning a transaction to be processed by the blockchain. @@ -718,6 +692,13 @@ export type TransactionParams = { */ accessList?: AccessList; + /** + * Array of authorizations to set code on EOA accounts. + * Only supported in `setCode` transactions. + * Introduced in EIP-7702. + */ + authorizationList?: AuthorizationList; + /** * Network ID as per EIP-155. */ @@ -1009,8 +990,6 @@ export enum TransactionEnvelopeType { /** * A legacy transaction, the very first type. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention legacy = '0x0', /** @@ -1018,8 +997,6 @@ export enum TransactionEnvelopeType { * specifying the state that a transaction would act upon in advance and * theoretically save on gas fees. */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention accessList = '0x1', /** @@ -1030,9 +1007,14 @@ export enum TransactionEnvelopeType { * the maxPriorityFeePerGas (maximum amount of gwei per gas from the * transaction fee to distribute to miner). */ - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention feeMarket = '0x2', + + /** + * Adds code to externally owned accounts according to the signed authorizations + * in the new `authorizationList` parameter. + * Introduced in EIP-7702. + */ + setCode = '0x4', } /** @@ -1040,8 +1022,6 @@ export enum TransactionEnvelopeType { */ export enum UserFeeLevel { CUSTOM = 'custom', - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention DAPP_SUGGESTED = 'dappSuggested', MEDIUM = 'medium', } @@ -1117,8 +1097,6 @@ export type TransactionError = { export type SecurityAlertResponse = { reason: string; features?: string[]; - // This is API specific hence naming convention is not followed. - // eslint-disable-next-line @typescript-eslint/naming-convention result_type: string; providerRequestsCount?: Record; }; @@ -1196,6 +1174,7 @@ export type GasFeeFlowResponse = { export type GasFeeFlow = { /** * Determine if the gas fee flow supports the specified transaction. + * * @param transactionMeta - The transaction metadata. * @returns Whether the gas fee flow supports the transaction. */ @@ -1203,6 +1182,7 @@ export type GasFeeFlow = { /** * Get gas fee estimates for a specific transaction. + * * @param request - The gas fee flow request. * @returns The gas fee flow response containing the gas fee estimates. */ @@ -1228,6 +1208,7 @@ export type Layer1GasFeeFlowResponse = { export type Layer1GasFeeFlow = { /** * Determine if the gas fee flow supports the specified transaction. + * * @param transactionMeta - The transaction metadata. * @returns Whether the layer1 gas fee flow supports the transaction. */ @@ -1235,6 +1216,7 @@ export type Layer1GasFeeFlow = { /** * Get layer 1 gas fee estimates for a specific transaction. + * * @param request - The gas fee flow request. * @returns The gas fee flow response containing the layer 1 gas fee estimate. */ @@ -1260,14 +1242,8 @@ export type SimulationBalanceChange = { /** Token standards supported by simulation. */ export enum SimulationTokenStandard { - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention erc20 = 'erc20', - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention erc721 = 'erc721', - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/naming-convention erc1155 = 'erc1155', } @@ -1373,3 +1349,40 @@ export type SubmitHistoryEntry = { export type InternalAccount = ReturnType< AccountsController['getSelectedAccount'] >; + +/** + * An authorization to be included in a `setCode` transaction. + * Specifies code to be added to the authorization signer's EOA account. + * Introduced in EIP-7702. + */ +export type Authorization = { + /** Address of a smart contract that contains the code to be set. */ + address: Hex; + + /** + * Specific chain the authorization applies to. + * If not provided, defaults to the chain ID of the transaction. + */ + chainId?: Hex; + + /** + * Nonce at which the authorization will be valid. + * If not provided, defaults to the nonce following the transaction's nonce. + */ + nonce?: Hex; + + /** R component of the signature. */ + r?: Hex; + + /** S component of the signature. */ + s?: Hex; + + /** Y parity generated from the signature. */ + yParity?: Hex; +}; + +/** + * An array of authorizations to be included in a `setCode` transaction. + * Introduced in EIP-7702. + */ +export type AuthorizationList = Authorization[]; diff --git a/packages/transaction-controller/src/utils/eip7702.test.ts b/packages/transaction-controller/src/utils/eip7702.test.ts new file mode 100644 index 00000000000..6db398b3ff8 --- /dev/null +++ b/packages/transaction-controller/src/utils/eip7702.test.ts @@ -0,0 +1,175 @@ +import type { KeyringControllerSignAuthorization } from './eip7702'; +import { signAuthorizationList } from './eip7702'; +import { Messenger } from '../../../base-controller/src'; +import type { TransactionControllerMessenger } from '../TransactionController'; +import type { AuthorizationList } from '../types'; +import { TransactionStatus, type TransactionMeta } from '../types'; + +const AUTHORIZATION_SIGNATURE_MOCK = + '0xf85c827a6994663f3ad617193148711d28f5334ee4ed070166028080a040e292da533253143f134643a03405f1af1de1d305526f44ed27e62061368d4ea051cfb0af34e491aa4d6796dececf95569088322e116c4b2f312bb23f20699269'; + +const AUTHORIZATION_SIGNATURE_2_MOCK = + '0x82d5b4845dfc808802480749c30b0e02d6d7817061ba141d2d1dcd520f9b65c59d0b985134dc2958a9981ce3b5d1061176313536e6da35852cfae41404f53ef31b624206f3bc543ca6710e02d58b909538d6e2445cea94dfd39737fbc0b3'; + +const TRANSACTION_META_MOCK: TransactionMeta = { + chainId: '0x1', + id: '123-456', + networkClientId: 'network-client-id', + status: TransactionStatus.unapproved, + time: 1234567890, + txParams: { + from: '0x', + nonce: '0x123', + }, +}; + +const AUTHORIZATION_LIST_MOCK: AuthorizationList = [ + { + address: '0x1234567890123456789012345678901234567890', + chainId: '0x123', + nonce: '0x456', + }, +]; + +describe('EIP-7702 Utils', () => { + let baseMessenger: Messenger; + let controllerMessenger: TransactionControllerMessenger; + let signAuthorizationMock: jest.MockedFn< + KeyringControllerSignAuthorization['handler'] + >; + + beforeEach(() => { + baseMessenger = new Messenger(); + + signAuthorizationMock = jest + .fn() + .mockResolvedValue(AUTHORIZATION_SIGNATURE_MOCK); + + baseMessenger.registerActionHandler( + 'KeyringController:signAuthorization', + signAuthorizationMock, + ); + + controllerMessenger = baseMessenger.getRestricted({ + name: 'TransactionController', + allowedActions: ['KeyringController:signAuthorization'], + allowedEvents: [], + }); + }); + + describe('signAuthorizationList', () => { + it('returns undefined if no authorization list is provided', async () => { + expect( + await signAuthorizationList({ + authorizationList: undefined, + messenger: controllerMessenger, + transactionMeta: TRANSACTION_META_MOCK, + }), + ).toBeUndefined(); + }); + + it('populates signature properties', async () => { + const result = await signAuthorizationList({ + authorizationList: AUTHORIZATION_LIST_MOCK, + messenger: controllerMessenger, + transactionMeta: TRANSACTION_META_MOCK, + }); + + expect(result).toStrictEqual([ + { + address: AUTHORIZATION_LIST_MOCK[0].address, + chainId: AUTHORIZATION_LIST_MOCK[0].chainId, + nonce: AUTHORIZATION_LIST_MOCK[0].nonce, + r: '0xf85c827a6994663f3ad617193148711d28f5334ee4ed070166028080a040e292', + s: '0xda533253143f134643a03405f1af1de1d305526f44ed27e62061368d4ea051cf', + yParity: '0x1', + }, + ]); + }); + + it('populates signature properties for multiple authorizations', async () => { + signAuthorizationMock + .mockReset() + .mockResolvedValueOnce(AUTHORIZATION_SIGNATURE_MOCK) + .mockResolvedValueOnce(AUTHORIZATION_SIGNATURE_2_MOCK); + + const result = await signAuthorizationList({ + authorizationList: [ + AUTHORIZATION_LIST_MOCK[0], + AUTHORIZATION_LIST_MOCK[0], + ], + messenger: controllerMessenger, + transactionMeta: TRANSACTION_META_MOCK, + }); + + expect(result).toStrictEqual([ + { + address: AUTHORIZATION_LIST_MOCK[0].address, + chainId: AUTHORIZATION_LIST_MOCK[0].chainId, + nonce: AUTHORIZATION_LIST_MOCK[0].nonce, + r: '0xf85c827a6994663f3ad617193148711d28f5334ee4ed070166028080a040e292', + s: '0xda533253143f134643a03405f1af1de1d305526f44ed27e62061368d4ea051cf', + yParity: '0x1', + }, + { + address: AUTHORIZATION_LIST_MOCK[0].address, + chainId: AUTHORIZATION_LIST_MOCK[0].chainId, + nonce: AUTHORIZATION_LIST_MOCK[0].nonce, + r: '0x82d5b4845dfc808802480749c30b0e02d6d7817061ba141d2d1dcd520f9b65c5', + s: '0x9d0b985134dc2958a9981ce3b5d1061176313536e6da35852cfae41404f53ef3', + yParity: '0x', + }, + ]); + }); + + it('uses transaction chain ID if not specified', async () => { + const result = await signAuthorizationList({ + authorizationList: [ + { ...AUTHORIZATION_LIST_MOCK[0], chainId: undefined }, + ], + messenger: controllerMessenger, + transactionMeta: TRANSACTION_META_MOCK, + }); + + expect(result?.[0]?.chainId).toStrictEqual(TRANSACTION_META_MOCK.chainId); + }); + + it('uses transaction nonce + 1 if not specified', async () => { + const result = await signAuthorizationList({ + authorizationList: [ + { ...AUTHORIZATION_LIST_MOCK[0], nonce: undefined }, + ], + messenger: controllerMessenger, + transactionMeta: TRANSACTION_META_MOCK, + }); + + expect(result?.[0]?.nonce).toBe('0x124'); + }); + + it('uses incrementing transaction nonce for multiple authorizations if not specified', async () => { + const result = await signAuthorizationList({ + authorizationList: [ + { ...AUTHORIZATION_LIST_MOCK[0], nonce: undefined }, + { ...AUTHORIZATION_LIST_MOCK[0], nonce: undefined }, + { ...AUTHORIZATION_LIST_MOCK[0], nonce: undefined }, + ], + messenger: controllerMessenger, + transactionMeta: TRANSACTION_META_MOCK, + }); + + expect(result?.[0]?.nonce).toBe('0x124'); + expect(result?.[1]?.nonce).toBe('0x125'); + expect(result?.[2]?.nonce).toBe('0x126'); + }); + + it('normalizes nonce to 0x if zero', async () => { + const result = await signAuthorizationList({ + authorizationList: [{ ...AUTHORIZATION_LIST_MOCK[0], nonce: '0x0' }], + messenger: controllerMessenger, + transactionMeta: TRANSACTION_META_MOCK, + }); + + expect(result?.[0]?.nonce).toBe('0x'); + }); + }); +}); diff --git a/packages/transaction-controller/src/utils/eip7702.ts b/packages/transaction-controller/src/utils/eip7702.ts new file mode 100644 index 00000000000..67b29643946 --- /dev/null +++ b/packages/transaction-controller/src/utils/eip7702.ts @@ -0,0 +1,148 @@ +import { toHex } from '@metamask/controller-utils'; +import { createModuleLogger, type Hex } from '@metamask/utils'; + +import { projectLogger } from '../logger'; +import type { TransactionControllerMessenger } from '../TransactionController'; +import type { + Authorization, + AuthorizationList, + TransactionMeta, +} from '../types'; + +export type KeyringControllerAuthorization = [ + chainId: number, + contractAddress: string, + nonce: number, +]; + +export type KeyringControllerSignAuthorization = { + type: 'KeyringController:signAuthorization'; + handler: (authorization: KeyringControllerAuthorization) => Promise; +}; + +const log = createModuleLogger(projectLogger, 'eip-7702'); + +/** + * Sign an authorization list. + * + * @param options - Options bag. + * @param options.authorizationList - The authorization list to sign. + * @param options.messenger - The controller messenger. + * @param options.transactionMeta - The transaction metadata. + * @returns The signed authorization list. + */ +export async function signAuthorizationList({ + authorizationList, + messenger, + transactionMeta, +}: { + authorizationList?: AuthorizationList; + messenger: TransactionControllerMessenger; + transactionMeta: TransactionMeta; +}): Promise> { + if (!authorizationList) { + return undefined; + } + + const signedAuthorizationList: Required = []; + let index = 0; + + for (const authorization of authorizationList) { + const signedAuthorization = await signAuthorization( + authorization, + transactionMeta, + messenger, + index, + ); + + signedAuthorizationList.push(signedAuthorization); + index += 1; + } + + return signedAuthorizationList; +} + +/** + * Signs an authorization. + * + * @param authorization - The authorization to sign. + * @param transactionMeta - The associated transaction metadata. + * @param messenger - The messenger to use for signing. + * @param index - The index of the authorization in the list. + * @returns The signed authorization. + */ +async function signAuthorization( + authorization: Authorization, + transactionMeta: TransactionMeta, + messenger: TransactionControllerMessenger, + index: number, +): Promise> { + const finalAuthorization = prepareAuthorization( + authorization, + transactionMeta, + index, + ); + + const { address, chainId, nonce } = finalAuthorization; + const chainIdDecimal = parseInt(chainId, 16); + const nonceDecimal = parseInt(nonce, 16); + + const signature = await messenger.call( + 'KeyringController:signAuthorization', + [chainIdDecimal, address, nonceDecimal], + ); + + const r = signature.slice(0, 66) as Hex; + const s = `0x${signature.slice(66, 130)}` as Hex; + const v = parseInt(signature.slice(130, 132), 16); + const yParity = v - 27 === 0 ? '0x' : '0x1'; + const finalNonce = nonceDecimal === 0 ? '0x' : nonce; + + const result: Required = { + address, + chainId, + nonce: finalNonce, + r, + s, + yParity, + }; + + log('Signed authorization', result); + + return result; +} + +/** + * Prepares an authorization for signing by populating the chainId and nonce. + * + * @param authorization - The authorization to prepare. + * @param transactionMeta - The associated transaction metadata. + * @param index - The index of the authorization in the list. + * @returns The prepared authorization. + */ +function prepareAuthorization( + authorization: Authorization, + transactionMeta: TransactionMeta, + index: number, +): Authorization & { chainId: Hex; nonce: Hex } { + const { chainId: existingChainId, nonce: existingNonce } = authorization; + const { txParams, chainId: transactionChainId } = transactionMeta; + const { nonce: transactionNonce } = txParams; + + const chainId = existingChainId ?? transactionChainId; + let nonce = existingNonce; + + if (nonce === undefined) { + nonce = toHex(parseInt(transactionNonce as string, 16) + 1 + index); + } + + const result = { + ...authorization, + chainId, + nonce, + }; + + log('Prepared authorization', result); + + return result; +} diff --git a/packages/transaction-controller/src/utils/prepare.test.ts b/packages/transaction-controller/src/utils/prepare.test.ts new file mode 100644 index 00000000000..840e847482e --- /dev/null +++ b/packages/transaction-controller/src/utils/prepare.test.ts @@ -0,0 +1,67 @@ +import { FeeMarketEIP1559Transaction, LegacyTransaction } from '@ethereumjs/tx'; + +import { prepareTransaction, serializeTransaction } from './prepare'; +import { TransactionEnvelopeType, type TransactionParams } from '../types'; + +const CHAIN_ID_MOCK = '0x123'; + +const SERIALIZED_TRANSACTION = + '0xea808301234582012394123456789012345678901234567890123456789084123456788412345678808080'; + +const SERIALIZED_TRANSACTION_FEE_MARKET = + '0x02f4820123808401234567841234567882012394123456789012345678901234567890123456789084123456788412345678c0808080'; + +const TRANSACTION_PARAMS_MOCK: TransactionParams = { + data: '0x12345678', + from: '0x1234567890123456789012345678901234567890', + gasLimit: '0x123', + gasPrice: '0x12345', + to: '0x1234567890123456789012345678901234567890', + value: '0x12345678', +}; + +const TRANSACTION_PARAMS_FEE_MARKET_MOCK: TransactionParams = { + ...TRANSACTION_PARAMS_MOCK, + type: TransactionEnvelopeType.feeMarket, + maxFeePerGas: '0x12345678', + maxPriorityFeePerGas: '0x1234567', +}; + +describe('Prepare Utils', () => { + describe('prepareTransaction', () => { + it('returns legacy transaction object', () => { + const result = prepareTransaction(CHAIN_ID_MOCK, TRANSACTION_PARAMS_MOCK); + expect(result).toBeInstanceOf(LegacyTransaction); + }); + + it('returns fee market transaction object', () => { + const result = prepareTransaction( + CHAIN_ID_MOCK, + TRANSACTION_PARAMS_FEE_MARKET_MOCK, + ); + expect(result).toBeInstanceOf(FeeMarketEIP1559Transaction); + }); + }); + + describe('serializeTransaction', () => { + it('returns hex string for legacy transaction', () => { + const transaction = prepareTransaction( + CHAIN_ID_MOCK, + TRANSACTION_PARAMS_MOCK, + ); + + const result = serializeTransaction(transaction); + expect(result).toStrictEqual(SERIALIZED_TRANSACTION); + }); + + it('returns hex string for fee market transaction', () => { + const transaction = prepareTransaction( + CHAIN_ID_MOCK, + TRANSACTION_PARAMS_FEE_MARKET_MOCK, + ); + + const result = serializeTransaction(transaction); + expect(result).toStrictEqual(SERIALIZED_TRANSACTION_FEE_MARKET); + }); + }); +}); diff --git a/packages/transaction-controller/src/utils/prepare.ts b/packages/transaction-controller/src/utils/prepare.ts new file mode 100644 index 00000000000..4db930d3292 --- /dev/null +++ b/packages/transaction-controller/src/utils/prepare.ts @@ -0,0 +1,57 @@ +import type { ChainConfig } from '@ethereumjs/common'; +import { Common, Hardfork } from '@ethereumjs/common'; +import type { TypedTransaction, TypedTxData } from '@ethereumjs/tx'; +import { TransactionFactory } from '@ethereumjs/tx'; +import { bytesToHex } from '@metamask/utils'; +import type { Hex } from '@metamask/utils'; + +import type { TransactionParams } from '../types'; + +export const HARDFORK = Hardfork.Prague; + +/** + * Creates an `etheruemjs/tx` transaction object from the raw transaction parameters. + * + * @param chainId - Chain ID of the transaction. + * @param txParams - Transaction parameters. + * @returns The transaction object. + */ +export function prepareTransaction( + chainId: Hex, + txParams: TransactionParams, +): TypedTransaction { + // Does not allow `gasPrice` on type 4 transactions. + const data = txParams as TypedTxData; + + return TransactionFactory.fromTxData(data, { + freeze: false, + common: getCommonConfiguration(chainId), + }); +} + +/** + * Serializes a transaction object into a hex string. + * + * @param transaction - The transaction object. + * @returns The prefixed hex string. + */ +export function serializeTransaction(transaction: TypedTransaction) { + return bytesToHex(transaction.serialize()); +} + +/** + * Generates the configuration used to prepare transactions. + * + * @param chainId - Chain ID. + * @returns The common configuration. + */ +function getCommonConfiguration(chainId: Hex): Common { + const customChainParams: Partial = { + chainId: parseInt(chainId, 16), + defaultHardfork: HARDFORK, + }; + + return Common.custom(customChainParams, { + eips: [7702], + }); +} diff --git a/packages/transaction-controller/src/utils/utils.ts b/packages/transaction-controller/src/utils/utils.ts index 360f5260dcd..ac61345e8df 100644 --- a/packages/transaction-controller/src/utils/utils.ts +++ b/packages/transaction-controller/src/utils/utils.ts @@ -1,3 +1,4 @@ +import type { AuthorizationList } from '@ethereumjs/common'; import { add0x, getKnownPropertyNames, @@ -20,6 +21,8 @@ export const ESTIMATE_GAS_ERROR = 'eth_estimateGas rpc method error'; // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any const NORMALIZERS: { [param in keyof TransactionParams]: any } = { + authorizationList: (authorizationList?: AuthorizationList) => + authorizationList, data: (data: string) => add0x(padHexToEvenLength(data)), from: (from: string) => add0x(from).toLowerCase(), gas: (gas: string) => add0x(gas), @@ -83,8 +86,6 @@ export const validateGasValues = ( const value = (gasValues as any)[key]; if (typeof value !== 'string' || !isStrictHexString(value)) { throw new TypeError( - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `expected hex string for ${key} but received: ${value}`, ); } @@ -104,8 +105,6 @@ export function validateIfTransactionUnapproved( ) { if (transactionMeta?.status !== TransactionStatus.unapproved) { throw new Error( - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `TransactionsController: Can only call ${fnName} on an unapproved transaction.\n Current tx status: ${transactionMeta?.status}`, ); } diff --git a/packages/transaction-controller/src/utils/validation.test.ts b/packages/transaction-controller/src/utils/validation.test.ts index 8b6d65e5eec..91da8d9cc6e 100644 --- a/packages/transaction-controller/src/utils/validation.test.ts +++ b/packages/transaction-controller/src/utils/validation.test.ts @@ -1,8 +1,16 @@ +import { ORIGIN_METAMASK } from '@metamask/controller-utils'; import { rpcErrors } from '@metamask/rpc-errors'; +import { + validateParamTo, + validateTransactionOrigin, + validateTxParams, +} from './validation'; import { TransactionEnvelopeType } from '../types'; import type { TransactionParams } from '../types'; -import { validateTxParams } from './validation'; + +const FROM_MOCK = '0x1678a085c290ebd122dc42cba69373b5953b831d'; +const TO_MOCK = '0xfbb5595c18ca76bab52d66188e4ca50c7d95f77a'; describe('validation', () => { describe('validateTxParams', () => { @@ -10,7 +18,7 @@ describe('validation', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any expect(() => validateTxParams({ type: '0x3' } as any)).toThrow( rpcErrors.invalidParams( - 'Invalid transaction envelope type: "0x3". Must be one of: 0x0, 0x1, 0x2', + 'Invalid transaction envelope type: "0x3". Must be one of: 0x0, 0x1, 0x2, 0x4', ), ); }); @@ -44,7 +52,7 @@ describe('validation', () => { it('should throw if no data', () => { expect(() => validateTxParams({ - from: '0x3244e191f1b4903970224322180f1fbbc415696b', + from: FROM_MOCK, to: '0x', // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -53,7 +61,7 @@ describe('validation', () => { expect(() => validateTxParams({ - from: '0x3244e191f1b4903970224322180f1fbbc415696b', + from: FROM_MOCK, // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any), @@ -63,7 +71,7 @@ describe('validation', () => { it('should delete data', () => { const transaction = { data: 'foo', - from: '0x3244e191f1b4903970224322180f1fbbc415696b', + from: TO_MOCK, to: '0x', }; validateTxParams(transaction); @@ -73,7 +81,7 @@ describe('validation', () => { it('should throw if invalid to address', () => { expect(() => validateTxParams({ - from: '0x3244e191f1b4903970224322180f1fbbc415696b', + from: FROM_MOCK, to: '1337', // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -84,8 +92,8 @@ describe('validation', () => { it('should throw if value is invalid', () => { expect(() => validateTxParams({ - from: '0x3244e191f1b4903970224322180f1fbbc415696b', - to: '0x3244e191f1b4903970224322180f1fbbc415696b', + from: FROM_MOCK, + to: TO_MOCK, value: '133-7', // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -98,8 +106,8 @@ describe('validation', () => { expect(() => validateTxParams({ - from: '0x3244e191f1b4903970224322180f1fbbc415696b', - to: '0x3244e191f1b4903970224322180f1fbbc415696b', + from: FROM_MOCK, + to: TO_MOCK, value: '133.7', // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -112,8 +120,8 @@ describe('validation', () => { expect(() => validateTxParams({ - from: '0x3244e191f1b4903970224322180f1fbbc415696b', - to: '0x3244e191f1b4903970224322180f1fbbc415696b', + from: FROM_MOCK, + to: TO_MOCK, value: 'hello', // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -186,8 +194,8 @@ describe('validation', () => { it('throws if data is invalid', () => { expect(() => validateTxParams({ - from: '0x1678a085c290ebd122dc42cba69373b5953b831d', - to: '0xfbb5595c18ca76bab52d66188e4ca50c7d95f77a', + from: FROM_MOCK, + to: TO_MOCK, data: '0xa9059cbb00000000000000000000000011b6A5fE2906F3354145613DB0d99CEB51f604C90000000000000000000000000000000000000000000000004563918244F400', // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -200,7 +208,7 @@ describe('validation', () => { expect(() => validateTxParams({ - from: '0x1678a085c290ebd122dc42cba69373b5953b831d', + from: FROM_MOCK, value: '0x01', data: 'INVALID_ARGUMENT', // TODO: Replace `any` with type @@ -213,8 +221,8 @@ describe('validation', () => { it('throws if gasPrice is defined but type is feeMarket', () => { expect(() => validateTxParams({ - from: '0x1678a085c290ebd122dc42cba69373b5953b831d', - to: '0xfbb5595c18ca76bab52d66188e4ca50c7d95f77a', + from: FROM_MOCK, + to: TO_MOCK, gasPrice: '0x01', type: TransactionEnvelopeType.feeMarket, // TODO: Replace `any` with type @@ -227,8 +235,34 @@ describe('validation', () => { ); expect(() => validateTxParams({ - from: '0x1678a085c290ebd122dc42cba69373b5953b831d', - to: '0xfbb5595c18ca76bab52d66188e4ca50c7d95f77a', + from: FROM_MOCK, + to: TO_MOCK, + gasPrice: '0x01', + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any), + ).not.toThrow(); + }); + + it('throws if gasPrice is defined but type is setCode', () => { + expect(() => + validateTxParams({ + from: FROM_MOCK, + to: TO_MOCK, + gasPrice: '0x01', + type: TransactionEnvelopeType.setCode, + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any), + ).toThrow( + rpcErrors.invalidParams( + 'Invalid transaction envelope type: specified type "0x4" but included a gasPrice instead of maxFeePerGas and maxPriorityFeePerGas', + ), + ); + expect(() => + validateTxParams({ + from: FROM_MOCK, + to: TO_MOCK, gasPrice: '0x01', // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -239,8 +273,8 @@ describe('validation', () => { it('throws if gasPrice is defined along with maxFeePerGas or maxPriorityFeePerGas', () => { expect(() => validateTxParams({ - from: '0x1678a085c290ebd122dc42cba69373b5953b831d', - to: '0xfbb5595c18ca76bab52d66188e4ca50c7d95f77a', + from: FROM_MOCK, + to: TO_MOCK, gasPrice: '0x01', maxFeePerGas: '0x01', // TODO: Replace `any` with type @@ -254,8 +288,8 @@ describe('validation', () => { expect(() => validateTxParams({ - from: '0x1678a085c290ebd122dc42cba69373b5953b831d', - to: '0xfbb5595c18ca76bab52d66188e4ca50c7d95f77a', + from: FROM_MOCK, + to: TO_MOCK, gasPrice: '0x01', maxPriorityFeePerGas: '0x01', // TODO: Replace `any` with type @@ -268,46 +302,46 @@ describe('validation', () => { ); }); - it('throws if gasPrice, maxPriorityFeePerGas or maxFeePerGas is not a valid hexadecimal', () => { + it('throws if gasPrice, maxPriorityFeePerGas or maxFeePerGas is not a valid hexadecimal string', () => { expect(() => validateTxParams({ - from: '0x1678a085c290ebd122dc42cba69373b5953b831d', - to: '0xfbb5595c18ca76bab52d66188e4ca50c7d95f77a', + from: FROM_MOCK, + to: TO_MOCK, gasPrice: 1, // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any), ).toThrow( rpcErrors.invalidParams( - 'Invalid transaction params: gasPrice is not a valid hexadecimal. got: (1)', + 'Invalid transaction params: gasPrice is not a valid hexadecimal string. got: (1)', ), ); expect(() => validateTxParams({ - from: '0x1678a085c290ebd122dc42cba69373b5953b831d', - to: '0xfbb5595c18ca76bab52d66188e4ca50c7d95f77a', + from: FROM_MOCK, + to: TO_MOCK, maxPriorityFeePerGas: 1, // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any), ).toThrow( rpcErrors.invalidParams( - 'Invalid transaction params: maxPriorityFeePerGas is not a valid hexadecimal. got: (1)', + 'Invalid transaction params: maxPriorityFeePerGas is not a valid hexadecimal string. got: (1)', ), ); expect(() => validateTxParams({ - from: '0x1678a085c290ebd122dc42cba69373b5953b831d', - to: '0xfbb5595c18ca76bab52d66188e4ca50c7d95f77a', + from: FROM_MOCK, + to: TO_MOCK, maxFeePerGas: 1, // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any), ).toThrow( rpcErrors.invalidParams( - 'Invalid transaction params: maxFeePerGas is not a valid hexadecimal. got: (1)', + 'Invalid transaction params: maxFeePerGas is not a valid hexadecimal string. got: (1)', ), ); }); @@ -315,8 +349,8 @@ describe('validation', () => { it('throws if maxPriorityFeePerGas is defined but type is not feeMarket', () => { expect(() => validateTxParams({ - from: '0x1678a085c290ebd122dc42cba69373b5953b831d', - to: '0xfbb5595c18ca76bab52d66188e4ca50c7d95f77a', + from: FROM_MOCK, + to: TO_MOCK, maxPriorityFeePerGas: '0x01', type: TransactionEnvelopeType.accessList, // TODO: Replace `any` with type @@ -324,13 +358,13 @@ describe('validation', () => { } as any), ).toThrow( rpcErrors.invalidParams( - 'Invalid transaction envelope type: specified type "0x1" but including maxFeePerGas and maxPriorityFeePerGas requires type: "0x2"', + 'Invalid transaction envelope type: specified type "0x1" but including maxFeePerGas and maxPriorityFeePerGas requires type: "0x2, 0x4"', ), ); expect(() => validateTxParams({ - from: '0x1678a085c290ebd122dc42cba69373b5953b831d', - to: '0xfbb5595c18ca76bab52d66188e4ca50c7d95f77a', + from: FROM_MOCK, + to: TO_MOCK, maxPriorityFeePerGas: '0x01', // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -341,8 +375,8 @@ describe('validation', () => { it('throws if maxFeePerGas is defined but type is not feeMarket', () => { expect(() => validateTxParams({ - from: '0x1678a085c290ebd122dc42cba69373b5953b831d', - to: '0xfbb5595c18ca76bab52d66188e4ca50c7d95f77a', + from: FROM_MOCK, + to: TO_MOCK, maxFeePerGas: '0x01', type: TransactionEnvelopeType.accessList, // TODO: Replace `any` with type @@ -350,13 +384,13 @@ describe('validation', () => { } as any), ).toThrow( rpcErrors.invalidParams( - 'Invalid transaction envelope type: specified type "0x1" but including maxFeePerGas and maxPriorityFeePerGas requires type: "0x2"', + 'Invalid transaction envelope type: specified type "0x1" but including maxFeePerGas and maxPriorityFeePerGas requires type: "0x2, 0x4"', ), ); expect(() => validateTxParams({ - from: '0x1678a085c290ebd122dc42cba69373b5953b831d', - to: '0xfbb5595c18ca76bab52d66188e4ca50c7d95f77a', + from: FROM_MOCK, + to: TO_MOCK, maxFeePerGas: '0x01', // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -367,8 +401,8 @@ describe('validation', () => { it('throws if gasLimit is defined but not a valid hexadecimal', () => { expect(() => validateTxParams({ - from: '0x1678a085c290ebd122dc42cba69373b5953b831d', - to: '0xfbb5595c18ca76bab52d66188e4ca50c7d95f77a', + from: FROM_MOCK, + to: TO_MOCK, maxFeePerGas: '0x01', gasLimit: 'zzzzz', // TODO: Replace `any` with type @@ -376,13 +410,13 @@ describe('validation', () => { } as any), ).toThrow( rpcErrors.invalidParams( - 'Invalid transaction params: gasLimit is not a valid hexadecimal. got: (zzzzz)', + 'Invalid transaction params: gasLimit is not a valid hexadecimal string. got: (zzzzz)', ), ); expect(() => validateTxParams({ - from: '0x1678a085c290ebd122dc42cba69373b5953b831d', - to: '0xfbb5595c18ca76bab52d66188e4ca50c7d95f77a', + from: FROM_MOCK, + to: TO_MOCK, maxFeePerGas: '0x01', gasLimit: '0x0', // TODO: Replace `any` with type @@ -394,29 +428,248 @@ describe('validation', () => { it('throws if gas is defined but not a valid hexadecimal', () => { expect(() => validateTxParams({ - from: '0x1678a085c290ebd122dc42cba69373b5953b831d', - to: '0xfbb5595c18ca76bab52d66188e4ca50c7d95f77a', + from: FROM_MOCK, + to: TO_MOCK, maxFeePerGas: '0x01', gas: 'zzzzz', // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any } as unknown as TransactionParams), ).toThrow( rpcErrors.invalidParams( - 'Invalid transaction params: gas is not a valid hexadecimal. got: (zzzzz)', + 'Invalid transaction params: gas is not a valid hexadecimal string. got: (zzzzz)', ), ); expect(() => validateTxParams({ - from: '0x1678a085c290ebd122dc42cba69373b5953b831d', - to: '0xfbb5595c18ca76bab52d66188e4ca50c7d95f77a', + from: FROM_MOCK, + to: TO_MOCK, maxFeePerGas: '0x01', gas: '0x0', // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any } as unknown as TransactionParams), ).not.toThrow(); }); }); + + describe('authorizationList', () => { + it('throws if type is not 0x4', () => { + expect(() => + validateTxParams({ + authorizationList: [], + from: FROM_MOCK, + to: TO_MOCK, + type: TransactionEnvelopeType.feeMarket, + }), + ).toThrow( + rpcErrors.invalidParams( + 'Invalid transaction envelope type: specified type "0x2" but including authorizationList requires type: "0x4"', + ), + ); + }); + + it('throws if not array', () => { + expect(() => + validateTxParams({ + authorizationList: 123 as never, + from: FROM_MOCK, + to: TO_MOCK, + type: TransactionEnvelopeType.setCode, + }), + ).toThrow( + rpcErrors.invalidParams( + 'Invalid transaction params: authorizationList must be an array', + ), + ); + }); + + it('throws if address missing', () => { + expect(() => + validateTxParams({ + authorizationList: [ + { + address: undefined as never, + }, + ], + from: FROM_MOCK, + to: TO_MOCK, + type: TransactionEnvelopeType.setCode, + }), + ).toThrow( + rpcErrors.invalidParams( + 'Invalid transaction params: address is not a valid hexadecimal string. got: (undefined)', + ), + ); + }); + + it('throws if address not hexadecimal string', () => { + expect(() => + validateTxParams({ + authorizationList: [ + { + address: 'test' as never, + }, + ], + from: FROM_MOCK, + to: TO_MOCK, + type: TransactionEnvelopeType.setCode, + }), + ).toThrow( + rpcErrors.invalidParams( + 'Invalid transaction params: address is not a valid hexadecimal string. got: (test)', + ), + ); + }); + + it('throws if address wrong length', () => { + expect(() => + validateTxParams({ + authorizationList: [ + { + address: FROM_MOCK.slice(0, -2) as never, + }, + ], + from: FROM_MOCK, + to: TO_MOCK, + type: TransactionEnvelopeType.setCode, + }), + ).toThrow( + rpcErrors.invalidParams( + 'Invalid transaction params: address must be 20 bytes. got: 19 bytes', + ), + ); + }); + + it.each(['chainId', 'nonce', 'r', 's', 'yParity'])( + 'throws if %s provided but not hexadecimal', + (property) => { + expect(() => + validateTxParams({ + authorizationList: [ + { + address: FROM_MOCK, + [property]: 'test' as never, + }, + ], + from: FROM_MOCK, + to: TO_MOCK, + type: TransactionEnvelopeType.setCode, + }), + ).toThrow( + rpcErrors.invalidParams( + `Invalid transaction params: ${property} is not a valid hexadecimal string. got: (test)`, + ), + ); + }, + ); + }); + }); + + describe('validateTransactionOrigin', () => { + it('throws if internal and from address not selected', async () => { + await expect( + validateTransactionOrigin({ + from: FROM_MOCK, + origin: ORIGIN_METAMASK, + permittedAddresses: undefined, + selectedAddress: '0x123', + txParams: {} as TransactionParams, + }), + ).rejects.toThrow( + rpcErrors.invalidParams( + 'Internally initiated transaction is using invalid account.', + ), + ); + }); + + it('does not throw if internal and from address is selected', async () => { + expect( + await validateTransactionOrigin({ + from: FROM_MOCK, + origin: ORIGIN_METAMASK, + permittedAddresses: undefined, + selectedAddress: FROM_MOCK, + txParams: {} as TransactionParams, + }), + ).toBeUndefined(); + }); + + it('throws if external and from not permitted', async () => { + await expect( + validateTransactionOrigin({ + from: FROM_MOCK, + origin: 'test-origin', + permittedAddresses: ['0x123', '0x456'], + selectedAddress: '0x123', + txParams: {} as TransactionParams, + }), + ).rejects.toThrow( + rpcErrors.invalidParams( + 'The requested account and/or method has not been authorized by the user.', + ), + ); + }); + + it('does not throw if external and from is permitted', async () => { + expect( + await validateTransactionOrigin({ + from: FROM_MOCK, + origin: 'test-origin', + permittedAddresses: ['0x123', FROM_MOCK], + selectedAddress: '0x123', + txParams: {} as TransactionParams, + }), + ).toBeUndefined(); + }); + + it('throw if external and type 4', async () => { + await expect( + validateTransactionOrigin({ + from: FROM_MOCK, + origin: 'test-origin', + permittedAddresses: [FROM_MOCK], + selectedAddress: '0x123', + txParams: { + type: TransactionEnvelopeType.setCode, + } as TransactionParams, + }), + ).rejects.toThrow( + rpcErrors.invalidParams( + 'External EIP-7702 transactions are not supported', + ), + ); + }); + + it('throw if external and authorization list provided', async () => { + await expect( + validateTransactionOrigin({ + from: FROM_MOCK, + origin: 'test-origin', + permittedAddresses: [FROM_MOCK], + selectedAddress: '0x123', + txParams: { + authorizationList: [], + from: FROM_MOCK, + } as TransactionParams, + }), + ).rejects.toThrow( + rpcErrors.invalidParams( + 'External EIP-7702 transactions are not supported', + ), + ); + }); + }); + + describe('validateParamTo', () => { + it('throws if no type', () => { + expect(() => validateParamTo(undefined as never)).toThrow( + rpcErrors.invalidParams('Invalid "to" address'), + ); + }); + + it('throws if type is not string', () => { + expect(() => validateParamTo(123 as never)).toThrow( + rpcErrors.invalidParams('Invalid "to" address'), + ); + }); }); }); diff --git a/packages/transaction-controller/src/utils/validation.ts b/packages/transaction-controller/src/utils/validation.ts index fbef756b319..caee53dc454 100644 --- a/packages/transaction-controller/src/utils/validation.ts +++ b/packages/transaction-controller/src/utils/validation.ts @@ -2,10 +2,16 @@ import { Interface } from '@ethersproject/abi'; import { ORIGIN_METAMASK, isValidHexAddress } from '@metamask/controller-utils'; import { abiERC20 } from '@metamask/metamask-eth-abis'; import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; -import { isStrictHexString } from '@metamask/utils'; +import { isStrictHexString, remove0x } from '@metamask/utils'; -import { TransactionEnvelopeType, type TransactionParams } from '../types'; import { isEIP1559Transaction } from './utils'; +import type { Authorization } from '../types'; +import { TransactionEnvelopeType, type TransactionParams } from '../types'; + +const TRANSACTION_ENVELOPE_TYPES_FEE_MARKET = [ + TransactionEnvelopeType.feeMarket, + TransactionEnvelopeType.setCode, +]; type GasFieldsToValidate = | 'gasPrice' @@ -17,37 +23,54 @@ type GasFieldsToValidate = /** * Validates whether a transaction initiated by a specific 'from' address is permitted by the origin. * - * @param permittedAddresses - The permitted accounts for the given origin. - * @param selectedAddress - The currently selected Ethereum address in the wallet. - * @param from - The address from which the transaction is initiated. - * @param origin - The origin or source of the transaction. + * @param options - Options bag. + * @param options.from - The address from which the transaction is initiated. + * @param options.origin - The origin or source of the transaction. + * @param options.permittedAddresses - The permitted accounts for the given origin. + * @param options.selectedAddress - The currently selected Ethereum address in the wallet. + * @param options.txParams - The transaction parameters. * @throws Throws an error if the transaction is not permitted. */ -export async function validateTransactionOrigin( - permittedAddresses: string[], - selectedAddress: string, - from: string, - origin: string, -) { - if (origin === ORIGIN_METAMASK) { - // Ensure the 'from' address matches the currently selected address - if (from !== selectedAddress) { - throw rpcErrors.internal({ - message: `Internally initiated transaction is using invalid account.`, - data: { - origin, - fromAddress: from, - selectedAddress, - }, - }); - } - return; +export async function validateTransactionOrigin({ + from, + origin, + permittedAddresses, + selectedAddress, + txParams, +}: { + from: string; + origin?: string; + permittedAddresses?: string[]; + selectedAddress: string; + txParams: TransactionParams; +}) { + const isInternal = origin === ORIGIN_METAMASK; + const isExternal = origin && origin !== ORIGIN_METAMASK; + const { authorizationList, type } = txParams; + + if (isInternal && from !== selectedAddress) { + throw rpcErrors.internal({ + message: `Internally initiated transaction is using invalid account.`, + data: { + origin, + fromAddress: from, + selectedAddress, + }, + }); } - // Check if the origin has permissions to initiate transactions from the specified address - if (!permittedAddresses.includes(from)) { + if (isExternal && permittedAddresses && !permittedAddresses.includes(from)) { throw providerErrors.unauthorized({ data: { origin } }); } + + if ( + isExternal && + (authorizationList || type === TransactionEnvelopeType.setCode) + ) { + throw rpcErrors.invalidParams( + 'External EIP-7702 transactions are not supported', + ); + } } /** @@ -69,6 +92,7 @@ export function validateTxParams( validateParamData(txParams.data); validateParamChainId(txParams.chainId); validateGasFeeParams(txParams); + validateAuthorizationList(txParams); } /** @@ -308,28 +332,41 @@ function validateGasFeeParams(txParams: TransactionParams) { */ function ensureProperTransactionEnvelopeTypeProvided( txParams: TransactionParams, - field: GasFieldsToValidate, + field: keyof TransactionParams, ) { + const type = txParams.type as TransactionEnvelopeType | undefined; + switch (field) { + case 'authorizationList': + if (type && type !== TransactionEnvelopeType.setCode) { + throw rpcErrors.invalidParams( + `Invalid transaction envelope type: specified type "${type}" but including authorizationList requires type: "${TransactionEnvelopeType.setCode}"`, + ); + } + break; case 'maxFeePerGas': case 'maxPriorityFeePerGas': if ( - txParams.type && - txParams.type !== TransactionEnvelopeType.feeMarket + type && + !TRANSACTION_ENVELOPE_TYPES_FEE_MARKET.includes( + type as TransactionEnvelopeType, + ) ) { throw rpcErrors.invalidParams( - `Invalid transaction envelope type: specified type "${txParams.type}" but including maxFeePerGas and maxPriorityFeePerGas requires type: "${TransactionEnvelopeType.feeMarket}"`, + `Invalid transaction envelope type: specified type "${type}" but including maxFeePerGas and maxPriorityFeePerGas requires type: "${TRANSACTION_ENVELOPE_TYPES_FEE_MARKET.join(', ')}"`, ); } break; case 'gasPrice': default: if ( - txParams.type && - txParams.type === TransactionEnvelopeType.feeMarket + type && + TRANSACTION_ENVELOPE_TYPES_FEE_MARKET.includes( + type as TransactionEnvelopeType, + ) ) { throw rpcErrors.invalidParams( - `Invalid transaction envelope type: specified type "${txParams.type}" but included a gasPrice instead of maxFeePerGas and maxPriorityFeePerGas`, + `Invalid transaction envelope type: specified type "${type}" but included a gasPrice instead of maxFeePerGas and maxPriorityFeePerGas`, ); } } @@ -361,20 +398,79 @@ function ensureMutuallyExclusiveFieldsNotProvided( * Ensures that the provided value for field is a valid hexadecimal. * Throws an invalidParams error if field is not a valid hexadecimal. * - * @param txParams - The transaction parameters object + * @param data - The object containing the field * @param field - The current field being validated * @throws {rpcErrors.invalidParams} Throws if field is not a valid hexadecimal */ -function ensureFieldIsValidHex( - txParams: TransactionParams, - field: GasFieldsToValidate, -) { - const value = txParams[field]; +function ensureFieldIsValidHex(data: T, field: keyof T) { + const value = data[field]; if (typeof value !== 'string' || !isStrictHexString(value)) { throw rpcErrors.invalidParams( - `Invalid transaction params: ${field} is not a valid hexadecimal. got: (${String( + `Invalid transaction params: ${String(field)} is not a valid hexadecimal string. got: (${String( value, )})`, ); } } + +/** + * Validate the authorization list property in the transaction parameters. + * + * @param txParams - The transaction parameters containing the authorization list to validate. + */ +function validateAuthorizationList(txParams: TransactionParams) { + const { authorizationList } = txParams; + + if (!authorizationList) { + return; + } + + ensureProperTransactionEnvelopeTypeProvided(txParams, 'authorizationList'); + + if (!Array.isArray(authorizationList)) { + throw rpcErrors.invalidParams( + `Invalid transaction params: authorizationList must be an array`, + ); + } + + for (const authorization of authorizationList) { + validateAuthorization(authorization); + } +} + +/** + * Validate an authorization object. + * + * @param authorization - The authorization object to validate. + */ +function validateAuthorization(authorization: Authorization) { + ensureFieldIsValidHex(authorization, 'address'); + validateHexLength(authorization.address, 20, 'address'); + + for (const field of ['chainId', 'nonce', 'r', 's', 'yParity'] as const) { + if (authorization[field]) { + ensureFieldIsValidHex(authorization, field); + } + } +} + +/** + * Validate the number of bytes in a hex string. + * + * @param value - The hex string to validate. + * @param lengthBytes - The expected length in bytes. + * @param fieldName - The name of the field being validated. + */ +function validateHexLength( + value: string, + lengthBytes: number, + fieldName: string, +) { + const actualLengthBytes = remove0x(value).length / 2; + + if (actualLengthBytes !== lengthBytes) { + throw rpcErrors.invalidParams( + `Invalid transaction params: ${fieldName} must be ${lengthBytes} bytes. got: ${actualLengthBytes} bytes`, + ); + } +} diff --git a/yarn.config.cjs b/yarn.config.cjs index aa8f334e172..9d6fe4526f7 100644 --- a/yarn.config.cjs +++ b/yarn.config.cjs @@ -24,6 +24,9 @@ const { inspect } = require('util'); */ const ALLOWED_INCONSISTENT_DEPENDENCIES = { // '@metamask/json-rpc-engine': ['^9.0.3'], + // Temporary to allow separate keyring API and keyring-controller upgrade. + '@ethereumjs/common': ['^3.2.0'], + '@ethereumjs/tx': ['^4.2.0'], }; /** diff --git a/yarn.lock b/yarn.lock index afb813337a7..77fe8916e48 100644 --- a/yarn.lock +++ b/yarn.lock @@ -823,6 +823,15 @@ __metadata: languageName: node linkType: hard +"@ethereumjs/common@npm:^4.4.0": + version: 4.4.0 + resolution: "@ethereumjs/common@npm:4.4.0" + dependencies: + "@ethereumjs/util": "npm:^9.1.0" + checksum: 10/dd5cc78575a762b367601f94d6af7e36cb3a5ecab45eec0c1259c433e755a16c867753aa88f331e3963791a18424ad0549682a3a6a0a160640fe846db6ce8014 + languageName: node + linkType: hard + "@ethereumjs/rlp@npm:^4.0.1": version: 4.0.1 resolution: "@ethereumjs/rlp@npm:4.0.1" @@ -832,6 +841,15 @@ __metadata: languageName: node linkType: hard +"@ethereumjs/rlp@npm:^5.0.2": + version: 5.0.2 + resolution: "@ethereumjs/rlp@npm:5.0.2" + bin: + rlp: bin/rlp.cjs + checksum: 10/2af80d98faf7f64dfb6d739c2df7da7350ff5ad52426c3219897e843ee441215db0ffa346873200a6be6d11142edb9536e66acd62436b5005fa935baaf7eb6bd + languageName: node + linkType: hard + "@ethereumjs/tx@npm:^4.0.2, @ethereumjs/tx@npm:^4.2.0": version: 4.2.0 resolution: "@ethereumjs/tx@npm:4.2.0" @@ -844,6 +862,18 @@ __metadata: languageName: node linkType: hard +"@ethereumjs/tx@npm:^5.4.0": + version: 5.4.0 + resolution: "@ethereumjs/tx@npm:5.4.0" + dependencies: + "@ethereumjs/common": "npm:^4.4.0" + "@ethereumjs/rlp": "npm:^5.0.2" + "@ethereumjs/util": "npm:^9.1.0" + ethereum-cryptography: "npm:^2.2.1" + checksum: 10/8d2c0a69ab37015f945f9de065cfb9f05e8e79179efeed725ea0a14760c3eb8ff900bcf915bb71ec29fe2f753db35d1b78a15ac4ddec489e87c995dec1ba6e85 + languageName: node + linkType: hard + "@ethereumjs/util@npm:^8.0.0, @ethereumjs/util@npm:^8.1.0": version: 8.1.0 resolution: "@ethereumjs/util@npm:8.1.0" @@ -855,6 +885,16 @@ __metadata: languageName: node linkType: hard +"@ethereumjs/util@npm:^9.1.0": + version: 9.1.0 + resolution: "@ethereumjs/util@npm:9.1.0" + dependencies: + "@ethereumjs/rlp": "npm:^5.0.2" + ethereum-cryptography: "npm:^2.2.1" + checksum: 10/4e22c4081c63eebb808eccd54f7f91cd3407f4cac192da5f30a0d6983fe07d51f25e6a9d08624f1376e604bb7dce574aafcf0fbf0becf42f62687c11e710ac41 + languageName: node + linkType: hard + "@ethersproject/abi@npm:^5.7.0": version: 5.7.0 resolution: "@ethersproject/abi@npm:5.7.0" @@ -4111,8 +4151,8 @@ __metadata: resolution: "@metamask/transaction-controller@workspace:packages/transaction-controller" dependencies: "@babel/runtime": "npm:^7.23.9" - "@ethereumjs/common": "npm:^3.2.0" - "@ethereumjs/tx": "npm:^4.2.0" + "@ethereumjs/common": "npm:^4.4.0" + "@ethereumjs/tx": "npm:^5.4.0" "@ethereumjs/util": "npm:^8.1.0" "@ethersproject/abi": "npm:^5.7.0" "@ethersproject/contracts": "npm:^5.7.0" @@ -8013,7 +8053,7 @@ __metadata: languageName: node linkType: hard -"ethereum-cryptography@npm:^2.0.0, ethereum-cryptography@npm:^2.1.2": +"ethereum-cryptography@npm:^2.0.0, ethereum-cryptography@npm:^2.1.2, ethereum-cryptography@npm:^2.2.1": version: 2.2.1 resolution: "ethereum-cryptography@npm:2.2.1" dependencies: