DEV Community

Cover image for I Automated 85% of the wagmi v1-v2 Migration Using Codemods. Here's How.
Waleed
Waleed

Posted on

I Automated 85% of the wagmi v1-v2 Migration Using Codemods. Here's How.

wagmi v2 broke everything. If you've tried migrating a real DeFi frontend from wagmi v1 to v2, you know exactly what I mean.
Every hook was renamed. The provider component changed. Connectors went from classes to factory functions. TanStack Query params moved into a nested query:{} object. configureChains was removed entirely. The official migration guide is 4,000+ words long.
I built a codemod that automates 85% of this migration deterministically — with zero false positives — and leaves clear AI-ready TODO comments for the remaining 15%. Here's exactly how I did it, what worked, what didn't, and the real numbers from running it against the official wagmi v1 examples repo.

The Problem

wagmi v2 is a ground-up rewrite built around two new primitives:

Viem instead of ethers.js
TanStack Query instead of internal state management

This meant every single hook had a breaking change. Not just renames — the entire API contract changed. useContractWrite in v1 accepted the contract config at the hook level. In v2 (useWriteContract), the contract config moves to the writeContract() call site. useNetwork() was removed entirely — replaced by useChainId() (returns a number, not a Chain object) and useChains().
For a team with 50+ components using wagmi, this is a multi-day migration. For a team with 200+, it's a week of careful, error-prone manual work.

The Approach: Deterministic First, AI Second

The core principle: anything mechanical goes in the codemod, anything semantic goes to AI.
A rename is mechanical. Moving enabled: true into query: { enabled: true } is mechanical. But figuring out what Viem transport to use as a replacement for alchemyProvider({ apiKey }) — that requires understanding the developer's infrastructure. That's AI territory.
This gave us a clean split:
Deterministic (codemod handles): ~85% of patterns
AI/manual (TODO comments): ~15% of patterns

The Tool: Codemod + JSSG

I used the Codemod platform with their JSSG engine (ast-grep based). JSSG uses Tree-sitter under the hood — it parses TypeScript/TSX into a concrete syntax tree and lets you write transforms that match and replace code patterns.
The key advantage over regex: it understands code structure. It knows the difference between useContractRead in an import statement, a function call, and a string literal. Regex doesn't.

The 8 Transforms

Transform 01: Hook Renames
The largest transform — 11 hook renames in one pass.
useContractRead → useReadContract
useContractWrite → useWriteContract
useContractEvent → useWatchContractEvent
useContractReads → useReadContracts
useContractInfiniteReads → useInfiniteReadContracts
useWaitForTransaction → useWaitForTransactionReceipt
useSwitchNetwork → useSwitchChain
useSigner → useWalletClient
useProvider → usePublicClient
useWebSocketProvider → usePublicClient
useFeeData → useEstimateFeesPerGas
The tricky part: import source verification. If a developer has their own hook named useProvider in a file that also imports from wagmi, we must NOT rename their hook. The transform confirms each hook is actually imported from 'wagmi' before renaming anything.
Another tricky part: position detection. The pattern import { $$$A, useSwitchNetwork, $$$B } from 'wagmi' fails when useSwitchNetwork is the last specifier (because $$$B requires at least one node). Solution: use text-based import detection with a regex word-boundary check instead.
For hooks where the return shape also changed, we add a TODO comment at the call site:

ts// TODO(wagmi-codemod): useWriteContract API changed significantly.
// 1. Rename: write → writeContract, writeAsync → writeContractAsync
// 2. Contract config moves from hook args to writeContract() call site.
//    Before: const { write } = useContractWrite({ address, abi, functionName })
//    After:  const { writeContract } = useWriteContract()
//            writeContract({ address, abi, functionName, args })
const { write } = useWriteContract({ ... })
Enter fullscreen mode Exit fullscreen mode

Transform 02: WagmiConfig → WagmiProvider

tsx// Before
<WagmiConfig config={config}>

// After
<WagmiProvider config={config}>
Enter fullscreen mode Exit fullscreen mode

Handles both the import specifier rename and all JSX opening/closing tag occurrences.
Transform 03: Prepare Hooks

ts// Before
const { config } = usePrepareContractWrite({ address, abi, functionName })
const { write } = useContractWrite(config)

// After (automated part)
// TODO(wagmi-codemod): Rename 'config' → 'data', pass data.request to writeContract()
const { config } = useSimulateContract({ address, abi, functionName })
Enter fullscreen mode Exit fullscreen mode

The rename is automated. The destructuring fix (configdata) and the data.request passing is flagged for AI.
Transform 04: TanStack Query Params
In v2, all TanStack Query params must live inside a query: {} property:

ts// Before
useReadContract({
  address,
  abi,
  functionName: 'balanceOf',
  enabled: Boolean(address),
  staleTime: 5_000,
})

// After
useReadContract({
  address,
  abi,
  functionName: 'balanceOf',
  query: {
    enabled: Boolean(address),
    staleTime: 5_000,
  },
})
Enter fullscreen mode Exit fullscreen mode

The hardest part of this transform: correctly splitting object properties by comma. A naive split breaks on functionName: 'transfer,all' (comma inside a string). The solution: a string-aware comma splitter that tracks string literal boundaries and only splits at depth-0 commas outside strings.
We also had to avoid tracking < and > as depth characters — that would break on JSX inside hook args.
Transform 05: Watch Prop Removal
watch: true was removed from most hooks in v2. But — critical safety detail — watch: true is still valid on useBlockNumber and useBlock. The transform explicitly excludes those two hooks.

ts// Before
const { data } = useBalance({ address, watch: true })

// After (automated)
// TODO(wagmi-codemod): 'watch' prop removed. Use useBlockNumber + useEffect to refetch:
//   const { data: blockNumber } = useBlockNumber({ watch: true })
//   useEffect(() => { refetch() }, [blockNumber])
const { data } = useBalance({ address })
Enter fullscreen mode Exit fullscreen mode

*Transform 06: Connector Renames
*

ts// Before
import { MetaMaskConnector } from 'wagmi/connectors/metaMask'
import { WalletConnectConnector } from 'wagmi/connectors/walletConnect'

new MetaMaskConnector({ chains })
new WalletConnectConnector({ chains, options: { projectId: 'abc' } })

// After
import { metaMask, walletConnect } from 'wagmi/connectors'

metaMask()
walletConnect({ options: { projectId: 'abc' } })
Enter fullscreen mode Exit fullscreen mode

Three things automated: import path consolidation, new ClassName()factoryFn() conversion, and chains property removal (no longer needed in v2 — chains are set in createConfig).
Also handles @wagmi/core/connectors/* paths for projects using the core package directly.
Transform 07: Config API
Three separate operations:

  1. createClientcreateConfig (import + all call sites)
  2. configureChains → TODO comment with Viem transport example
  3. useNetwork()useChainId() / useChains() based on what's destructured

For useNetwork, we analyze the destructuring pattern:

  • const { chain } = useNetwork() → const chainId = useChainId() + TODO about type change

  • const { chains } = useNetwork() → const chains = useChains()

  • Aliased: const { chain: currentChain } = useNetwork() → fallback TODO

Transform 08: Import Cleanup
After all previous transforms have run, some import specifiers become stale. This transform rebuilds the wagmi import with only the remaining valid specifiers.
Critical design decision: this transform's STALE list only includes hooks that were completely removed (useNetwork, configureChains) or renamed by another transform that also fixes the import (WagmiConfig → renamed by transform 02). It does NOT include hooks like useSwitchNetwork that transform 01 already renames in-place in the import specifier. Adding those would cause a bug where the code says useSwitchChain() but the import has no useSwitchChain.

The AI Step

The workflow.yaml includes an AI step that runs after all 8 deterministic transforms. It only fires on files containing TODO(wagmi-codemod) comments, and includes specific instructions for each pattern:

yaml- name: "AI: resolve remaining TODO(wagmi-codemod) patterns"
  ai:
    prompt: |
      Fix each TODO(wagmi-codemod) comment:

      1. configureChains removed → replace with Viem http transports
      2. watch:true → useBlockNumber + useEffect pattern  
      3. prepare hook destructuring → config → data.request
      4. useSwitchChain result → switchNetwork → switchChain
      5. useWalletClient → WalletClient not ethers Signer
      ...
Enter fullscreen mode Exit fullscreen mode

The key: TODOs are placed at the exact call site, not at the top of the file. This gives the AI maximum context for each fix.

Real-World Results: wevm/wagmi@1.x

I ran the full codemod against the official wagmi v1 examples repo (the 1.x branch of wevm/wagmi on GitHub — the wagmi team's own reference implementations).
Files scanned: 27
Files changed: 10
Lines changed: +62 / -60
TODO comments added: 7
False positives: 0
Here's what each changed file got:

  1. File: ReadContract.tsx / What changed: useContractRead → useReadContract, enabled → query:{enabled}
  2. File: ReadContracts.tsx / What changed: useContractReads → useReadContracts
  3. File: ReadContractsInfinite.tsx / What changed: useContractInfiniteReads → useInfiniteReadContracts
  4. File: WriteContract.tsx / What changed: useContractWrite → useWriteContract + TODO
  5. File: WriteContractPrepared.tsx / What changed: usePrepareContractWrite → useSimulateContract + TODO
  6. File: WatchContractEvents.tsx / What changed: useContractEvent → useWatchContractEvent
  7. File: SendTransactionPrepared.tsx / What changed: usePrepareContractWrite → useSimulateContract + TODO
  8. File: NetworkSwitcher.tsx / What changed: useNetwork → useChainId, useSwitchNetwork → useSwitchChain
  9. File: Balance.tsx / What changed: watch: true removed + TODO
  10. File: _app.tsx / What changed: WagmiConfig → WagmiProvider, createClient → createConfig, connectors

Zero false positives. Every change is correct. The 7 TODO comments are all legitimate patterns that require semantic understanding.

Automation vs Manual Effort

  1. Category: Hook renames (11 hooks) / Effort: 100% automated
  2. Category: Provider rename / Effort: 100% automated
  3. Category: Import cleanup / Effort: 100% automated
  4. Category: Connector migration / Effort: 100% automated
  5. Category: Config API rename / Effort: 100% automated
  6. Category: Query param restructure / Effort: 100% automated
  7. Category: Watch prop removal / Effort: 100% automated
  8. Category: configureChains → Viem transports / Effort: AI/manual (provider-specific)
  9. Category: write → writeContract + args migration / Effort: AI/manual (call-site-specific)
  10. Category: chain → chainId type change / Effort: AI/manual (usage-specific)
  11. Category: useSigner → WalletClient usage / Effort: AI/manual (type-specific)

~85% fully automated. ~15% flagged for AI with exact context.
On a typical 50-file DeFi frontend, this turns a 2-day manual migration into:

30 seconds to run the codemod
1-2 hours to resolve the 15% TODOs with AI assistance

Key Lessons

  1. Import position matters more than you think. Pattern matching with $$$A, hookName, $$$B fails when the hook is the first or last specifier. Text-based import detection with word boundaries is more reliable.
  2. The STALE set in cleanup transforms must be carefully curated. We initially included all v1 hook names in the cleanup transform's STALE set. This caused useSwitchChain to disappear from imports because the cleanup ran after the rename and removed the already-renamed specifier. Only hooks that are genuinely removed (not renamed) belong in the STALE set.
  3. String-aware parsing is non-negotiable. A naive comma split on object properties breaks on any string value containing a comma. Worth the extra 20 lines to implement correctly.
  4. *watch: true is not uniformly removed. *useBlockNumber and useBlock still accept watch: true in v2. An overzealous transform that removes it everywhere would introduce bugs, not fix them.
  5. TODOs at the call site, not the file top. The AI step is much more effective when TODO comments appear immediately before the code that needs changing, with the full context of the surrounding logic visible.

Try It

bashnpx codemod waleedbhattiii-wagmi-v1-to-v2
Enter fullscreen mode Exit fullscreen mode

Or run individual transforms:

bashnpx codemod jssg run --language tsx ./scripts/01-rename-hooks.ts --target ./src
Enter fullscreen mode Exit fullscreen mode

GitHub: https://github.com/Waleedbhattiii/wagmi-v1-to-v2
Registry: https://app.codemod.com/registry/waleedbhattiii-wagmi-v1-to-v2

What's Next
The next step is getting this referenced in the official wagmi migration guide. If the wagmi team adopts it, developers upgrading wagmi won't have to find this manually — it'll be the first thing they see when they open the migration docs.
If you maintain a project that's stuck on wagmi v1, try it and open an issue if anything doesn't work. Every real-world repo has edge cases the test suite didn't cover.

Built for the Codemod Hackathon. If this helped you, consider starring the repo.

Top comments (0)