CChinyereee

chinyereee/ethers-v5-to-v6

Automates 90.3% of the ethers.js v5 to v6 migration. Handles BigNumber to bigint, utils namespace, provider renames, constants, contract method buckets, and more.

Public
0 executions

Run locally

npx codemod @chinyereee/ethers-v5-to-v6

ethers.js v5 → v6 Codemod

npm version
license
tests

An automated jscodeshift codemod that migrates ethers.js v5 codebases to v6. Run it once, get a clean diff, review the flagged edge cases with AI.


What this automates

Patternv5v6Status
Utils namespace — parseEtherethers.utils.parseEther(v)ethers.parseEther(v)✅ Automated
Utils namespace — formatEtherethers.utils.formatEther(v)ethers.formatEther(v)✅ Automated
Utils namespace — parseUnitsethers.utils.parseUnits(v, d)ethers.parseUnits(v, d)✅ Automated
Utils namespace — formatUnitsethers.utils.formatUnits(v, d)ethers.formatUnits(v, d)✅ Automated
Utils namespace — keccak256ethers.utils.keccak256(data)ethers.keccak256(data)✅ Automated
Utils namespace — sha256ethers.utils.sha256(data)ethers.sha256(data)✅ Automated
Utils namespace — toUtf8Bytesethers.utils.toUtf8Bytes(s)ethers.toUtf8Bytes(s)✅ Automated
Utils namespace — toUtf8Stringethers.utils.toUtf8String(b)ethers.toUtf8String(b)✅ Automated
Utils namespace — hexlifyethers.utils.hexlify(b)ethers.hexlify(b)✅ Automated
Utils namespace — isAddressethers.utils.isAddress(a)ethers.isAddress(a)✅ Automated
Utils namespace — getAddressethers.utils.getAddress(a)ethers.getAddress(a)✅ Automated
Utils namespace — idethers.utils.id(s)ethers.id(s)✅ Automated
Utils rename — arrayifyethers.utils.arrayify(hex)ethers.getBytes(hex)✅ Automated
Utils rename — hexZeroPadethers.utils.hexZeroPad(v, n)ethers.zeroPadValue(v, n)✅ Automated
Utils rename — zeroPadethers.utils.zeroPad(arr, n)ethers.zeroPadBytes(arr, n)✅ Automated
Utils special — splitSignatureethers.utils.splitSignature(sig)ethers.Signature.from(sig)✅ Automated
Utils special — defaultAbiCoderethers.utils.defaultAbiCoderethers.AbiCoder.defaultAbiCoder()✅ Automated
Destructured utils — all 17 aboveimport { utils } from 'ethers'; utils.parseEther(v)import { ethers } from 'ethers'; ethers.parseEther(v)✅ Automated
BigNumber — constructionBigNumber.from(x)BigInt(x)✅ Automated
BigNumber — construction namespaceethers.BigNumber.from(x)BigInt(x)✅ Automated
BigNumber — toNumber() on tracked varbn.toNumber()Number(bn)✅ Automated
BigNumber — toHexString() on tracked varbn.toHexString()bn.toString(16)✅ Automated
BigNumber — arithmetic on tracked varbn.add(x) / bn.sub(x) / bn.mul(x) / bn.div(x)bn + x / bn - x / bn * x / bn / x✅ Automated
BigNumber — comparisons on tracked varbn.gt(x) / bn.gte(x) / bn.lt(x) / bn.lte(x) / bn.eq(x)bn > x / bn >= x / bn < x / bn <= x / bn === x✅ Automated
BigNumber — remove import specifierimport { BigNumber } from 'ethers'(removed)✅ Automated
BigNumber — toNumber() untrackedcontractReturn.toNumber()(flagged)⚠️ AI-assisted
BigNumber — toHexString() untrackedcontractReturn.toHexString()(flagged)⚠️ AI-assisted
Provider rename — Web3Providernew ethers.providers.Web3Provider(w)new ethers.BrowserProvider(w)✅ Automated
Provider rename — JsonRpcProvidernew ethers.providers.JsonRpcProvider(url)new ethers.JsonRpcProvider(url)✅ Automated
Provider rename — WebSocketProvidernew ethers.providers.WebSocketProvider(url)new ethers.WebSocketProvider(url)✅ Automated
Provider rename — FallbackProvidernew ethers.providers.FallbackProvider([a, b])new ethers.FallbackProvider([a, b])✅ Automated
Provider merge — StaticJsonRpcProvidernew ethers.providers.StaticJsonRpcProvider(url)new ethers.JsonRpcProvider(url)✅ Automated
Provider rename — AlchemyProvidernew ethers.providers.AlchemyProvider(net, key)new ethers.AlchemyProvider(net, key)✅ Automated
Provider rename — InfuraProvidernew ethers.providers.InfuraProvider(net, id)new ethers.InfuraProvider(net, id)✅ Automated
Provider type annotations — all 7 abovelet p: ethers.providers.Web3Providerlet p: ethers.BrowserProvider✅ Automated
Provider destructured — new + typesimport { providers }; new providers.Web3Provider(w)import { BrowserProvider }; new BrowserProvider(w)✅ Automated
Gas price — awaitedawait provider.getGasPrice()(await provider.getFeeData()).gasPrice✅ Automated
Gas price — FeeData typeethers.providers.FeeDataethers.FeeData✅ Automated
Gas price — non-awaitedprovider.getGasPrice().then(...)(flagged)⚠️ AI-assisted
Import cleanup — utilsimport { utils } from 'ethers'(removed)✅ Automated
Import cleanup — BigNumberimport { BigNumber } from 'ethers'(removed)✅ Automated
Import cleanup — providersimport { providers } from 'ethers'(removed)✅ Automated
Event filterscontract.filters.Transfer(from, to)contract.filters.Transfer(from, to) (API reworked)❌ Manual
Error handlingerror.data / Logger.errorserror.info / ethers.errors❌ Manual
Signer asyncprovider.getSigner()await provider.getSigner()❌ Manual

Real-world results

Tested against Uniswap v3-periphery (ethers ^5.0.8) — a production-grade codebase with 16,000+ lines across 50+ TypeScript files.

MetricResult
RepositoryUniswap/v3-periphery
Ethers files scanned31
Files automatically migrated28
Files needing AI review3
Automated coverage90.3%
False positives0
Lines changed502 (231 insertions, 271 deletions)

Zero-false-positive claim validated separately by running against scaffold-eth-2 (already on ethers v6): 0 files changed, 0 TODOs.


Quickstart

bash

The runner will:

  1. Recursively find all .ts, .tsx, .js, .jsx files
  2. Skip files with no ethers imports (no false positives on unrelated code)
  3. Run all 5 transforms in the correct dependency order
  4. Print a colorized summary with per-transform change counts
  5. List any files flagged with TODO comments for AI-assisted review

AI-Assisted Edge Cases

For the patterns the codemod flags with TODO comments, paste the relevant file content into Claude or ChatGPT along with one of these prompts.

1. BigNumber method chains on untracked variables

Use when the codemod flagged .toNumber() or .toHexString() on a variable it couldn't statically confirm came from BigNumber.from() — e.g. values returned from contracts, function parameters, or cross-scope assignments.

text

2. Non-awaited getGasPrice() calls

Use when provider.getGasPrice() appears without await — usually inside .then() chains or Promise combinators.

text

3. Event filter API

The event filter system was significantly reworked in ethers v6. This requires manual migration.

text

4. Signer async changes

provider.getSigner() is now async in ethers v6 and must be awaited.

text

5. Error handling

ethers v6 changed error types, properties, and the Logger API.

text

How it works

rename-utils — 17 patterns

Detects whether the file uses import * as ethers (namespace) or import { utils } from 'ethers' (destructured) and rewrites all ethers.utils.X(...) or utils.X(...) calls to their v6 equivalents. Fifteen patterns are simple name promotions (e.g. parseEther stays parseEther, just moves up one level). Two are renames (arrayify → getBytes, hexZeroPad → zeroPadValue, zeroPad → zeroPadBytes). Two are structural transformations: splitSignature(x) becomes Signature.from(x), and the property access defaultAbiCoder becomes a method call AbiCoder.defaultAbiCoder(). For destructured usage, the transform rewrites the import declaration — removing utils and adding ethers — then rewrites all call sites.

bigNumber-to-bigint — conservative static analysis

Replaces BigNumber.from(x) with BigInt(x) and migrates instance methods to native operators and built-ins. To avoid false positives, the transform first builds a knownBigNumbers set by scanning all VariableDeclarator and AssignmentExpression nodes for direct BigNumber.from() assignments. Method calls (.add(), .toNumber(), etc.) are only automatically transformed when the receiver is in this set or is a direct BigNumber.from() call. For .toNumber() and .toHexString() on untracked receivers, a TODO comment is inserted. Generic arithmetic methods (.add(), .sub(), etc.) on untracked receivers are silently skipped — they're too common outside BigNumber contexts to flag safely. The transform runs in two phases: method rewrites first (to handle chained calls like BigNumber.from(x).toNumber() atomically), then standalone BigNumber.from() replacement.

rename-providers — 7 providers + type annotations

Maps all 7 ethers.providers.* provider classes to their v6 equivalents. The key rename is Web3Provider → BrowserProvider; StaticJsonRpcProvider merges into JsonRpcProvider. Beyond new expressions, the transform also rewrites TypeScript type annotations (TSTypeReference nodes) in variable declarations, function parameters, return types, and as casts — covering both the ethers.providers.X qualified form and the bare providers.X destructured form. For destructured imports, it tracks which v6 names were introduced and updates the import declaration accordingly, deduplicating if a v6 name was already present.

gasPrice-to-feeData — await-aware rewrite

Replaces await provider.getGasPrice() with (await provider.getFeeData()).gasPrice. The transform targets the AwaitExpression node rather than the inner CallExpression so the result slots naturally into any expression context — variable declarations, object literals, ternaries, etc. Recast's printer automatically adds the wrapping parentheses because AwaitExpression has lower precedence than MemberExpression. Non-awaited getGasPrice() calls (Promise chains, fire-and-forget) are flagged with a TODO comment rather than transformed, since their async context needs human judgment. The transform also migrates ethers.providers.FeeData type annotations to ethers.FeeData.

update-imports — cleanup pass

Removes deprecated v5 import specifiers (utils, BigNumber, providers) from import { ... } from 'ethers' declarations. Namespace imports (import * as ethers) and default imports (import ethers) are always preserved unchanged. If removing deprecated specifiers leaves an import declaration with zero specifiers, the entire import statement is removed. This transform runs last so the earlier transforms have already finished introducing replacement names — making this pass purely a cleanup step with no risk of removing something that's still needed.


Transform pipeline order

text

Order matters: bigNumber-to-bigint must run before update-imports (which removes the BigNumber import), and rename-utils/rename-providers must run before update-imports (which removes the utils/providers imports). Running update-imports last ensures it only removes specifiers that are genuinely no longer needed.


Testing

bash

100 tests across 5 suites — one suite per transform. Tests cover:

  • All named patterns (inline unit tests per transform)
  • Namespace import style (import * as ethers)
  • Destructured import style (import { utils }, import { providers }, import { BigNumber })
  • TypeScript type annotations
  • Import declaration rewriting and deduplication
  • Edge cases: non-ethers imports untouched, idempotency (running twice = same result), files with no ethers import
  • Fixture-based integration tests using full input/output .ts files in tests/fixtures/

Contributing

To add a new transform:

  1. Create src/transforms/your-transform-name.ts — export a default jscodeshift Transform function. Use root.toSource({ quote: 'single' }) as the return value. Follow the existing pattern: detect import style first, then walk the AST, then update imports last.

  2. Add it to the pipeline in src/transforms/index.ts and src/run.ts (the PIPELINE array). Place it before update-imports, and consider whether it needs to run before or after bigNumber-to-bigint based on whether it touches the same nodes.

  3. Write tests in tests/your-transform-name.test.ts — cover both namespace and destructured import styles, at least one fixture file in tests/fixtures/your-transform-name/, and an idempotency test. Run npm test to confirm all 100 existing tests still pass.


License

MIT

Ready to contribute?

Build your own codemod and share it with the community.