Skip to content

perf(wallet): port MetaMask hook off web3 to viem - #1182

Open
Danswar wants to merge 3 commits into
developfrom
feat/metamask-viem-migration
Open

perf(wallet): port MetaMask hook off web3 to viem#1182
Danswar wants to merge 3 commits into
developfrom
feat/metamask-viem-migration

Conversation

@Danswar

@Danswar Danswar commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • src/hooks/wallets/metamask.hook.ts: replaced all web3.eth.* / Contract.methods.* calls with viem's publicClient/walletClient (getBalance, readContract, writeContract, signMessage, sendTransaction, getAddresses), mirroring the pattern already used in wallet-connect.hook.ts.
  • src/hooks/web3.hook.ts: the one remaining web3.utils.toHex call replaced with viem's numberToHex.
  • Removed web3 from package.json (the only one of the three that was ever a direct dependency — web3-core/web3-eth-contract were transitive/type-only imports resolved through it) — confirmed no other file in the repo imports any of them.
  • Added a TextEncoder/TextDecoder polyfill to src/setupTests.ts: jsdom's test environment doesn't provide these, and viem needs them at import time. This was latent — wallet-connect.hook.ts already used viem but had no test coverage, so nothing had hit this before.
  • Removed the now-unused jest.mock('web3', ...) blocks from the two existing test files, and added real unit test coverage for readBalance, createTransaction, and sign against mocked viem clients (this logic had zero coverage before this PR).

Why

web3 is CommonJS-only with no sideEffects: false declaration (confirmed via its published package.json), so it cannot be tree-shaken — importing any part of it always ships the whole package, plus its own legacy crypto-polyfill dependency tree (elliptic, bn.js, eth-lib, secp256k1, asn1.js, IPFS-era multihashes/multibase for the long-dead web3-bzz Swarm support, etc.). Since useMetaMask is wired into the app-wide WalletContextProvider (not behind a lazy route), all of that shipped in the main entry chunk for every visitor, regardless of wallet choice.

Measured impact

Same-checkout before/after production build:

main.js
Before (web3) 3,583,689 bytes (3.42 MiB)
After (viem) 2,516,089 bytes (2.40 MiB)
Cut 1,067,600 bytes (1.02 MiB) — 29.8%

Behavior preserved intentionally

  • The original code's .send()/.sendTransaction() blocks until the transaction is mined before returning the hash (web3's PromiEvent resolves with the receipt). viem's writeContract/sendTransaction return the hash immediately instead, so an explicit publicClient.waitForTransactionReceipt({ hash }) was added to keep today's wait-for-mined behavior unchanged. Note this means MetaMask and WalletConnect (wallet-connect.hook.ts, which does not wait) already behave differently here — out of scope for this PR, flagging for awareness.
  • The original code explicitly nulled maxFeePerGas/maxPriorityFeePerGas on every send (added in [DEV-2129] Metamask fees [DEV-2109] WC sign message [DEV-2067] Limit request [DEV-2142] Signature hint #163/DEV-2129, for MetaMask/chain combinations that misbehaved with EIP-1559 fee fields). The first version of this port dropped that whenever no gasPrice override was given, silently reverting to viem's default fee estimation. Fixed in a follow-up commit: createTransaction now resolves gasPrice via publicClient.getGasPrice() when no override is passed, so gasPrice is always a concrete value and the EIP-1559 fields are never set. Note this restores the same request shape as before at the code level, but — same as the rest of this PR — hasn't been independently verified against a live wallet.
  • readBalance()'s native-coin branch had a pre-existing bug carried over from the web3 code (return promise.then(...) inside a try doesn't get caught by that try's own catch), which the ERC20 branch didn't have. Fixed to match.

Test plan

  • npm test — all 431 tests pass (36 suites), including new coverage for readBalance/createTransaction/sign
  • tsc --noEmit clean on all non-test application code
  • Local production build succeeds; main.js measured before/after in the same checkout
  • Manual QA against a live MetaMask wallet (balance reads, native + ERC20 transfer, personal_sign, and confirming the broadcast transaction is actually type 0x0/legacy) on testnet before merging — not done as part of this PR

@github-actions

Copy link
Copy Markdown

🤖 PR Review Bot

❌ Security: 1 critical vulnerabilities


This is an automated review. Please address the issues above.

@Danswar

Danswar commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

This PR went through 3 review passes before reaching 0 outstanding issues.

Fixed along the way:

@Danswar
Danswar marked this pull request as ready for review July 21, 2026 02:05
@Danswar

Danswar commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

Manual QA checklist before merge

This PR hasn't been tested against a live wallet yet (see test plan). Checklist for whoever picks that up:

  • Connect/disconnect MetaMask; confirm wallet detection works (and Rabby/Trust/Coinbase in-app-browser variants if reachable)
  • Read native-coin balance — compare against the wallet's own displayed balance
  • Read an ERC20 balance (e.g. USDT/USDC/dEURO) — compare against the wallet's own displayed balance
  • Send a native-coin transaction end-to-end (Buy/Sell/Swap) — confirm it broadcasts and the returned hash matches what actually lands on-chain
  • Send an ERC20 transfer end-to-end — confirm the transferred amount is correct (decimals scaling) and the hash matches
  • Confirm the broadcast transaction is actually type 0x0 (legacy), not EIP-1559 — check via a block explorer or eth_getTransactionByHash. This is the one behavior this PR restores by code shape only and hasn't been verified against real wallet behavior.
  • personal_sign message signing — confirm the signature validates server-side (whatever auth/session flow consumes it)
  • "Add token to MetaMask" (wallet_watchAsset) for an ERC20 asset — confirm symbol/decimals show up correctly in the wallet
  • Network/chain-switch flow across a couple of supported chains, including the "add chain" fallback for one not yet added to the wallet
  • Payment-link "Pay with MetaMask" flow specifically (uses the gasPrice override path) — confirm it still works with the resolved-gasPrice change

Danswar added 3 commits July 23, 2026 17:15
web3 is CommonJS-only with no sideEffects declaration, so it can't be
tree-shaken and ships in full wherever it's imported. Since the MetaMask
hook is wired into the app-wide WalletContextProvider (not lazy-loaded),
this pulled the entire web3 dependency tree into the main bundle for
every visitor regardless of wallet choice. Cuts main.js by ~1.02 MiB
(29.8%) in a local production build.
The web3-based implementation explicitly nulled maxFeePerGas/
maxPriorityFeePerGas on every send, forcing legacy pricing regardless of
whether a gasPrice override was given (see #163, DEV-2129: some MetaMask/
chain combinations misbehave with EIP-1559 fields). The viem port dropped
this for the no-override path, letting viem's default fee estimation pick
EIP-1559 again. Resolve gasPrice via publicClient.getGasPrice() when no
override is passed, so every transaction still resolves to a legacy-type
send.
…ed logic

readBalance()'s native-coin branch returned an un-awaited promise from
inside a try block, so a rejection (e.g. RPC failure) never hit the
function's own catch and the documented throwExceptions=false fallback
never fired. Mirror the already-correct ERC20 branch by awaiting the
balance before returning.

Also add unit tests for readBalance, createTransaction (including the
gasPrice/legacy-pricing resolution), and sign against mocked viem clients
-- this logic previously had no coverage at all. Soften the gasPrice
comment to not overstate certainty about wallet-side behavior that isn't
independently verified in this PR.
@Danswar
Danswar force-pushed the feat/metamask-viem-migration branch from c0da4c9 to 95ecfb3 Compare July 23, 2026 20:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant