Repository navigation
Description
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 }); // throwsOK 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 andTransactionControllernormalizes 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
- metamask-extension 13.45.0+ — [Bug]: Dapp transactions rejected with
Invalid params / chainId - Expected a stringsince 13.45.0 — breaks Uniswap swaps metamask-extension#45763, labeledSev1-high/regression-prod-13.45.0. Uniswap's swap and approval paths always sendchainIdas a number (ValidatedTransactionRequestrequireschainId: number), so swaps fail with no confirmation window ever opening. 13.44.0 is unaffected. - metamask-mobile 8.8.0+ — same bump in fix: Update
eth-json-rpc-middlewarepackage to latest metamask-mobile#34392, which additionally validates on the WalletConnect path.
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.
Activity
truehazker commentedon Aug 25, 2026
Fix is up and ready for review: #9965.
One line in
packages/eth-json-rpc-middleware/src/utils/validation.tsplus tests and a changelog entry — 3 files, +38/-1.Verified locally against this repo: the package suite is 45/45 green with the fix, and reverting just that line fails exactly one test (the new numeric-
chainIdcase), so the added coverage is load-bearing. A further test asserts that an extraneous top-level key alongside a numericchainIdstill throws, demonstrating the hardening from #9482 is intact.Note the PR's workflow runs are sitting at
action_required— it's from a fork by a first-time contributor, so CI needs a maintainer to approve the run before anything executes.