# Ekx Viem Wagmi

> Reading and writing contracts from Ekinoxis frontends with viem and wagmi — chain/transport config, useReadContract and useWriteContract, ERC-20 approve-then-spend flows, decimal handling for USDC, transaction receipt waiting, and event logs. Use when wiring a dApp UI to a contract, debugging a failing or pending transaction, formatting token amounts, handling chain switching, or choosing between a viem public client and a wagmi hook.

- Skill: `ekinoxis-evm/ekx-viem-wagmi` (Agent Skill)
- Install (CLI): `npx skillmds@latest add ekinoxis-evm/ekx-viem-wagmi`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ekinoxis-evm/ekx-viem-wagmi/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Ekinoxis-evm (https://skillmd.com/u/ekinoxis-evm)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/ekinoxis-evm/ekx-viem-wagmi

---


# viem + wagmi

The contract-interaction layer for every Ekinoxis dApp frontend.
`viem` in 8 projects, `wagmi` in 6.

Docs: https://viem.sh · https://wagmi.sh — or ask the `context7` MCP server, since
viem's API changed meaningfully between v1 and v2.

---

## The split

- **viem** — the primitive. Use directly in server code, scripts, route handlers.
- **wagmi** — React hooks over viem, with caching via TanStack Query. Use in components.

When Privy is the wallet, wagmi comes from `@privy-io/wagmi`, not `wagmi` —
see [`../ekx-privy/SKILL.md`](../ekx-privy/SKILL.md).

---

## Config

```ts
// lib/chain.ts
import { base, baseSepolia } from "viem/chains";

// One chain at a time, chosen by env var — our standard pattern.
// No chain switcher in the UI; it is a support burden and a source of wrong-network txs.
export const CHAIN = process.env.NEXT_PUBLIC_CHAIN === "mainnet" ? base : baseSepolia;

export const RPC_URL =
  process.env.NEXT_PUBLIC_CHAIN === "mainnet"
    ? process.env.NEXT_PUBLIC_BASE_MAINNET_RPC_URL!
    : process.env.NEXT_PUBLIC_BASE_SEPOLIA_RPC_URL!;
```

```ts
// lib/viem.ts  — server-side reads
import { createPublicClient, http } from "viem";

export const publicClient = createPublicClient({
  chain: CHAIN,
  transport: http(RPC_URL),
});
```

Always pass an explicit RPC URL. `http()` with no argument uses the chain's public
endpoint, which rate-limits under real traffic and produces intermittent,
hard-to-reproduce read failures in production.

---

## Decimals — read this before writing any amount

```ts
import { parseUnits, formatUnits } from "viem";

parseUnits("10.50", 6)     // 10500000n  — USDC. CORRECT.
parseEther("10.50")        // 10500000000000000000n — 10.5 QUINTILLION units of USDC. WRONG.

formatUnits(10500000n, 6)  // "10.5"
```

**USDC is 6 decimals.** ETH and most ERC-20s are 18. Mixing them up is our single
most frequent onchain bug. Read the decimals from the contract if you are unsure:

```ts
const decimals = await publicClient.readContract({ address: USDC, abi: erc20Abi, functionName: "decimals" });
```

---

## Reading

```tsx
import { useReadContract } from "wagmi";

const { data: bid, isLoading } = useReadContract({
  address: auctionAddress,
  abi: auctionAbi,
  functionName: "highestBid",
  query: { refetchInterval: 5_000 },   // live auctions need polling
});
```

Batch reads with `useReadContracts` — one multicall instead of N round-trips. Base
supports multicall3; viem uses it automatically when `batch: { multicall: true }` is
set on the transport.

---

## Writing — the approve-then-spend dance

Every USDC payment in our apps is two transactions. Users do not expect this; the UI
must say so.

```tsx
const { writeContractAsync } = useWriteContract();

async function placeBid(amount: string) {
  const value = parseUnits(amount, 6);

  // 1. Check existing allowance — skip the approve if it is already enough
  const allowance = await publicClient.readContract({
    address: USDC, abi: erc20Abi, functionName: "allowance",
    args: [userAddress, auctionAddress],
  });

  if (allowance < value) {
    const approveHash = await writeContractAsync({
      address: USDC, abi: erc20Abi, functionName: "approve",
      args: [auctionAddress, value],
    });
    await publicClient.waitForTransactionReceipt({ hash: approveHash });
  }

  // 2. The actual call
  const hash = await writeContractAsync({
    address: auctionAddress, abi: auctionAbi, functionName: "bid", args: [value],
  });
  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  if (receipt.status === "reverted") throw new Error("La transacción falló");
  return receipt;
}
```

**Do not skip `waitForTransactionReceipt` between the two.** Sending `bid` before the
approve is mined reverts, and the user sees a confusing failure on the second popup.

**Check `receipt.status`.** A mined transaction can still have reverted; `viem` does
not throw for that.

---

## Simulate before writing

```ts
const { request } = await publicClient.simulateContract({
  address, abi, functionName: "bid", args: [value], account: userAddress,
});
const hash = await walletClient.writeContract(request);
```

Simulation surfaces the revert reason *before* the user pays gas and before the wallet
popup. Worth it on any call that can fail for business reasons (auction ended, bid too
low, not whitelisted).

---

## Events

```ts
const logs = await publicClient.getLogs({
  address: auctionAddress,
  event: parseAbiItem("event BidPlaced(address indexed bidder, uint256 amount)"),
  fromBlock: deployBlock,
  toBlock: "latest",
});
```

Most RPCs cap the block range (Alchemy: 10k blocks). For anything historical, page the
range — or use a subgraph, see [`../ekx-thegraph/SKILL.md`](../ekx-thegraph/SKILL.md).

---

## Gotchas

1. **`parseEther` on USDC.** The bug. Always `parseUnits(x, 6)`.
2. **Public RPC in production.** Intermittent failures under load. Use Alchemy.
3. **Wrong chain.** Call `switchChain` before writes; Privy embedded wallets do not switch on their own.
4. **`receipt.status === "reverted"`** on a mined tx — check it.
5. **`bigint` everywhere.** No `+` with a number; `JSON.stringify` throws on bigint. Serialize with `.toString()`.
6. **ABI must be `as const`** (or generated) for viem's type inference to work. A plain `any[]` ABI compiles but gives you no argument checking.
7. **Block-range caps on `getLogs`.**

