Skip to content

eth-json-rpc-middleware: TransactionParamsStruct rejects numeric chainId, breaking dapp transactions #9964

Description

@truehazker

TransactionParamsStruct types chainId as optional(string()) while every sibling quantity field uses QuantityStruct = union([string(), number()]). Dapps that send chainId as a JSON number in eth_sendTransaction therefore get -32602, where they previously succeeded.

Introduced in 24.0.0 by #9482.

Reproduction

Against @metamask/eth-json-rpc-middleware@24.0.0:

const { validateTransactionParams } = require('@metamask/eth-json-rpc-middleware');

const base = {
  from: '0x1234567890123456789012345678901234567890',
  to: '0x0987654321098765432109876543210987654321',
  value: '0x0',
};

validateTransactionParams({ ...base, chainId: '0x1237' }); // ok
validateTransactionParams({ ...base, gas: 21000 });        // ok — number accepted
validateTransactionParams({ ...base, chainId: 4663 });     // throws
OK      chainId as hex string
OK      gas as number
REJECT  chainId as number
        Invalid params | chainId - Expected a string, but received: 4663

gas as a number is accepted; chainId as a number is not. Both are quantities.

Why this looks unintended

QuantityStruct is introduced at src/utils/validation.ts:248 with:

// Numerical fields accept both hex strings and numbers, as some dapps send numbers and TransactionController normalizes them downstream.

That rationale applies to chainId as much as to gas or value, and chainId is the only quantity in the struct that did not get the union. In the #9482 review thread the union was added in response to a comment naming value, gasPrice and gasLimit specifically; chainId was not in that list. There is also no test in validation.test.ts covering the number branch of QuantityStruct for any field — every fixture is hex-only — so nothing flagged the gap.

Why relaxing it is safe

The value is discarded downstream regardless. chainId is not present in the NORMALIZERS map used by normalizeTransactionParams in @metamask/transaction-controller, which rebuilds the params object from a fixed allow-list. Consumers derive the chain from their own network state — the extension does so from networkClientId — and never read the dapp-supplied value.

Impact

24.0.0 is currently latest on npm, so both are broken in production with no released fix.

Proposed fix

src/utils/validation.ts:

- chainId: optional(string()),
+ chainId: optional(QuantityStruct),

Suitable for a 24.0.1 patch — it restores prior behavior and changes no API surface. I'll open a PR with this plus the missing numeric-branch tests.

I'd suggest leaving authorizationList[].chainId/nonce/yParity alone for now: viem and ethers v6 both hex-encode those, so there's no evidence anything breaks there.

Possibly worth separate discussion

Not proposed here, but the same struct newly rejects shapes that normalizeTransactionParams has always stripped harmlessly — null for optional fields, to: null on contract deployments, and unknown top-level keys such as web3.js's input/networkId/common, viem's blobs/maxFeePerBlobGas, and ethers v6's blobVersionedHashes. Happy to open a separate issue if that's of interest.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions