---
name: megapot-react-setup
description: Set up a React app with wagmi and RainbowKit for Megapot on Base mainnet. Shows how to translate viem patterns from other Megapot skills into React hooks (useReadContract, useWriteContract, useReadContracts).
---
# React + wagmi Setup for Megapot
## What This Does
Bridges the viem script patterns used in every other Megapot task skill into React hooks (`useReadContract`, `useWriteContract`, `useReadContracts`, `useWaitForTransactionReceipt`) so you can build a Megapot-powered React UI without rewriting your integration logic.
## Prerequisites
- React 18+, TypeScript
- wagmi v2, viem, @tanstack/react-query, @rainbow-me/rainbowkit
```bash
npm install wagmi@2 viem @tanstack/react-query @rainbow-me/rainbowkit
npm install tslib
```
> **`tslib` is required separately.** RainbowKit's dependency tree needs it but doesn't always install it. Without it, `npm run dev` works but `npm run build` fails. If your production build fails with a `tslib` resolution error, run `npm install tslib` again.
> Use wagmi v2 (not v3) for RainbowKit compatibility.
## Setup — Provider Boilerplate
### `wagmi.ts`
```tsx
import { getDefaultConfig } from '@rainbow-me/rainbowkit';
import { http } from 'viem';
import { base } from 'viem/chains';
const RPC_URL = import.meta.env.VITE_RPC_URL ?? 'https://mainnet.base.org';
export const config = getDefaultConfig({
appName: 'My Megapot App',
projectId: 'YOUR_WALLETCONNECT_PROJECT_ID', // optional for local dev — injected wallets (MetaMask) work without it
chains: [base],
transports: {
// HTTP batching coalesces independent reads issued in the same tick into
// one JSON-RPC call (`eth_call` multicall under the hood). Without it,
// the lifecycle multicall + per-drawing reads + balance polls hammer the
// public Base RPC and trip its per-IP rate limit fast.
[base.id]: http(RPC_URL, { batch: { batchSize: 100, wait: 16 } }),
},
// wagmi defaults to ~4s polling for `useReadContract` / `useBalance`.
// 30s is the right cadence for the Megapot UI: phase-aware polling in
// `read-state` already drops to 5s during `awaiting` / `settling`, so the
// base interval only affects the long `open` phase where 30s is plenty.
pollingInterval: 30_000,
});
```
> **WalletConnect Project ID:** Get a free project ID at [cloud.walletconnect.com](https://cloud.walletconnect.com) (takes ~2 minutes). Without a real ID, the WalletConnect QR modal won't work — but injected wallets (MetaMask, Coinbase Wallet) still work for local development.
### `main.tsx`
```tsx
import { WagmiProvider } from 'wagmi';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { hashFn } from 'wagmi/query';
import { RainbowKitProvider } from '@rainbow-me/rainbowkit';
import '@rainbow-me/rainbowkit/styles.css';
import { config } from './wagmi';
// Megapot reads return uint256 → bigint everywhere. Patch BigInt's JSON
// serializer so wallet SES shims, error reporters, and devtools never throw.
(BigInt.prototype as any).toJSON = function () { return this.toString(); };
const queryClient = new QueryClient({
defaultOptions: {
queries: { queryKeyHashFn: hashFn },
},
});
ReactDOM.createRoot(document.getElementById('root')!).render(
);
```
> **`queryKeyHashFn: hashFn` is required for any Megapot integration.** TanStack Query's default key-hash function uses `JSON.stringify`, which throws on `bigint` values. Every Megapot protocol read returns or accepts at least one `uint256` → `bigint` — `currentDrawingId`, `prizePool`, the full `getDrawingState` struct, ticket IDs, allowance amounts, and so on. Without `hashFn` from `wagmi/query`, the first refetch crashes with `TypeError: BigInt value can't be serialized in JSON` and the React render loop is interrupted; typical symptoms are tabs failing to switch, components not refetching, and countdowns freezing. `hashFn` is the canonical bigint-safe replacement and is documented at [wagmi.sh — TanStack Query setup](https://wagmi.sh/react/api/createConfig#queryclient).
> **`BigInt.prototype.toJSON` polyfill is also required.** `hashFn` only fixes TanStack Query's queryKey hashing — `JSON.stringify` is still called on `bigint` along several other paths a Megapot integration exercises. The most common culprit is the SES (Hardened JavaScript) shims that wallet extensions inject for telemetry and error capture (Coinbase Wallet, MetaMask LavaMoat); other paths include TanStack Query devtools, observer-notification diagnostics, error reporters (Sentry et al.) auto-serializing context, and any plain `console.log(state)` where `state` is a Megapot read result. Without the polyfill, those paths throw `TypeError: BigInt value can't be serialized in JSON` and React's scheduler unwinds with `Should not already be working`, typically on the first navigation after `getDrawingState` resolves — even with `hashFn` already applied. **Trade-off:** the polyfill monkey-patches a global. Apps that prefer not to patch globals can wrap their tree in a custom serializer instead, but missing a single instrumentation path resurfaces the same crash, so the polyfill is the recommended path for a starter integration.
## Pattern Mapping — viem → wagmi Hooks
### Reading a single value
```ts
// viem (from task skills):
const ticketPrice = await publicClient.readContract({
address: JACKPOT, abi, functionName: 'ticketPrice'
});
// wagmi equivalent:
const { data: ticketPrice } = useReadContract({
address: JACKPOT, abi, functionName: 'ticketPrice'
});
```
### Reading multiple values (multicall)
```ts
// viem:
const results = await publicClient.multicall({ contracts: [...] });
// wagmi:
const { data: results } = useReadContracts({ contracts: [...] });
```
### Writing (transaction)
```ts
// viem:
const hash = await walletClient.writeContract({
address: JACKPOT, abi, functionName: 'buyTickets', args: [...]
});
// wagmi:
const { writeContract, data: hash } = useWriteContract();
// then call: writeContract({ address: JACKPOT, abi, functionName: 'buyTickets', args: [...] })
```
### Waiting for receipt
```ts
// viem:
const receipt = await publicClient.waitForTransactionReceipt({ hash });
// wagmi:
const { data: receipt } = useWaitForTransactionReceipt({ hash });
```
### Dependent reads (two-round pattern)
Some Megapot reads depend on a prior result — for example, `getExpectedDrawingTierPayouts` needs `prizePool`, `ballMax`, and `bonusballMax` from `getDrawingState`, which itself needs `currentDrawingId`. In React, chain these with the `enabled` option:
```tsx
import { useReadContract, useReadContracts } from 'wagmi';
function useDrawingState() {
// Round 1: get the current drawing ID
const { data: drawingId } = useReadContract({
address: JACKPOT_ADDRESS,
abi: jackpotAbi,
functionName: 'currentDrawingId',
});
// Round 2: get drawing state (only runs when drawingId is available)
const { data: drawingState } = useReadContract({
address: JACKPOT_ADDRESS,
abi: jackpotAbi,
functionName: 'getDrawingState',
args: drawingId !== undefined ? [drawingId] : undefined,
query: { enabled: drawingId !== undefined },
});
// Round 3: get tier payouts (only runs when drawingState is available)
const { data: tierPayouts } = useReadContract({
address: PAYOUT_CALC_ADDRESS,
abi: payoutCalcAbi,
functionName: 'getExpectedDrawingTierPayouts',
args: drawingState
? [drawingId!, drawingState.prizePool, drawingState.ballMax, drawingState.bonusballMax]
: undefined,
query: { enabled: !!drawingState },
});
return { drawingId, drawingState, tierPayouts };
}
```
Each hook renders independently — earlier rounds show data while later rounds load. This is the standard wagmi pattern for dependent reads.
### Sequential writes (approve → transaction)
Most Megapot write operations require a prior USDC approval. In React, use `useWriteContract` with a state machine pattern:
```tsx
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi';
import { useEffect, useState } from 'react';
function useApproveAndExecute() {
const [step, setStep] = useState<'idle' | 'approving' | 'executing' | 'done'>('idle');
const { writeContract: approve, data: approveHash, reset: resetApprove } = useWriteContract();
const { isSuccess: approveConfirmed } = useWaitForTransactionReceipt({ hash: approveHash });
const { writeContract: execute, data: executeHash, reset: resetExecute } = useWriteContract();
const { isSuccess: executeConfirmed } = useWaitForTransactionReceipt({ hash: executeHash });
// When approval confirms, move to execute step
useEffect(() => {
if (approveConfirmed && step === 'approving') {
setStep('executing');
}
}, [approveConfirmed, step]);
// When execution confirms, we're done
useEffect(() => {
if (executeConfirmed && step === 'executing') {
setStep('done');
}
}, [executeConfirmed, step]);
function start(
approveArgs: Parameters[0],
executeArgs: Parameters[0],
) {
resetApprove();
resetExecute();
setStep('approving');
approve(approveArgs);
// executeArgs is stored and called in the useEffect above
// In practice, call execute(executeArgs) in the approving→executing transition
}
return { step, approve, execute, approveHash, executeHash };
}
```
The key insight: use **two separate `useWriteContract` hooks** (one for approve, one for the actual call) and chain them via `useEffect` on the confirmation state. Call `reset()` on both before starting a new flow to clear stale transaction hashes.
## Reading Off-Chain Aggregates (Data API)
Cross-drawing aggregates — wallet lifetime stats, round-by-round history, per-round leaderboards — go through the **Megapot Data API** at `api.megapot.io/v1`, not RPC. Hop to `megapot-data-api` for the full surface; this section covers the React-specific wiring.
### Single `QueryClient` for both wagmi and the API
The same `QueryClient` you set up above for wagmi can host the Data API queries — don't spawn a second one. wagmi reserves the `['readContract', ...]` queryKey shape for its cache; namespace your API queries under `['megapot-api', ...]` so the two caches stay co-located but never collide.
`WalletStats` (and its sibling response types) are exported from a hand-written `lib/api.ts` per `megapot-data-api` § "Helpers / Shape Parity" — keep one source of truth for response shapes across hooks. The example below threads that type through `useQuery` so a copy-paste matches the typed style of the wagmi examples earlier in this skill rather than landing with implicit `any`:
```tsx
import { useQuery } from '@tanstack/react-query';
import type { WalletStats } from './lib/api'; // exported per megapot-data-api § Helpers / Shape Parity
const API_BASE_URL = 'https://api.megapot.io/v1';
function useWalletStats(address: `0x${string}` | undefined) {
return useQuery({
queryKey: ['megapot-api', API_BASE_URL, 'wallet-stats', address],
queryFn: async () => {
const res = await fetch(`${API_BASE_URL}/wallets/${address}/stats`, {
headers: { Accept: 'application/json' },
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${res.status} ${error.code}: ${error.message}`);
}
return res.json() as Promise;
},
enabled: !!address,
staleTime: 60_000, // wallet aggregates churn slowly — 1 min is fine
});
}
```
### `staleTime` per resource
API resources have very different change rates. Set `staleTime` so refetches don't hammer the API on every focus event:
| Resource | Suggested `staleTime` | Rationale |
| --- | --- | --- |
| `walletStats` | 60s | Aggregates only change when the wallet acts. |
| `walletWins` / `walletTickets` | 60s | Same. |
| `listRounds` | 5 min | Round list churns only on settlement. |
| `activeRound` | 15s | Time-sensitive (countdown UIs). |
| `round(id)` (settled) | `Infinity` | Settled rounds are immutable. |
### Query keys
`['megapot-api', API_BASE_URL, , ...args]` — the URL slot lets a fork point at a different deploy (e.g. an internal mirror) without invalidating the production cache, and the `` slot is what you'd pass to `queryClient.invalidateQueries({ queryKey: ['megapot-api', API_BASE_URL, 'wallet-stats'] })` to nuke a single resource.
### `hashFn` is still required
The `queryKeyHashFn: hashFn` config from above already covers the Data API queries. API responses use string `Amount` (`{ amount, decimals }`) and don't need bigint handling on the wire, but mixing wagmi reads (which return `bigint`) and API reads in the same component still requires `hashFn` for the wagmi side.
### Deeper recipes
For `useInfiniteQuery` with cursor pagination and `Retry-After`-aware retry config, see the data-api skill ([/data-api](https://llms.megapot.io/data-api)) § Recipes 4 and 5. The patterns there port directly into this provider tree.
## Complete Example — Buy 5 Random Tickets Component
```tsx
import { parseAbi, decodeEventLog } from 'viem';
import {
useReadContract,
useWriteContract,
useWaitForTransactionReceipt,
useAccount,
} from 'wagmi';
// Base mainnet addresses
const RANDOM_BUYER = '0xb9560b43b91dE2c1DaF5dfbb76b2CFcDaFc13aBd' as const;
const JACKPOT = '0x3bAe643002069dBCbcd62B1A4eb4C4A397d042a2' as const;
const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' as const;
// Your referrer address — earns a share of ticket sales and user winnings
// Replace with your wallet address before deploying.
const REFERRER_ADDRESS = '0x0000000000000000000000000000000000000001' as const; // placeholder — replace with your address
const abi = parseAbi([
// Jackpot read
'function ticketPrice() view returns (uint256)',
// USDC
'function allowance(address owner, address spender) view returns (uint256)',
'function approve(address spender, uint256 amount) returns (bool)',
// JackpotRandomTicketBuyer write
'function buyTickets(uint256 _count, address _recipient, address[] _referrers, uint256[] _referralSplitBps, bytes32 _source) returns (uint256[] ticketIds)',
// Events
'event RandomTicketsBought(address indexed recipient, uint256 indexed drawingId, uint256 count, uint256 cost, uint256[] ticketIds)',
]);
const TICKET_COUNT = 5n;
const ZERO_SOURCE = '0x0000000000000000000000000000000000000000000000000000000000000000' as `0x${string}`;
export function BuyRandomTickets() {
const { address } = useAccount();
// 1. Read ticket price from Jackpot
const { data: ticketPrice } = useReadContract({
address: JACKPOT,
abi,
functionName: 'ticketPrice',
});
// 2. Check current USDC allowance granted to RANDOM_BUYER
const { data: allowance, refetch: refetchAllowance } = useReadContract({
address: USDC,
abi,
functionName: 'allowance',
args: address ? [address, RANDOM_BUYER] : undefined,
query: { enabled: !!address },
});
const totalCost = ticketPrice !== undefined ? ticketPrice * TICKET_COUNT : undefined;
const needsApproval = totalCost !== undefined && allowance !== undefined && allowance < totalCost;
// 3. Approve + buy flow via useWriteContract
const { writeContract, data: txHash, isPending } = useWriteContract();
// 4. Wait for receipt
const { data: receipt, isLoading: isConfirming } = useWaitForTransactionReceipt({
hash: txHash,
});
const handleApprove = () => {
if (!totalCost) return;
writeContract({
address: USDC,
abi,
functionName: 'approve',
args: [RANDOM_BUYER, totalCost],
});
};
const handleBuy = () => {
if (!address) return;
writeContract({
address: RANDOM_BUYER,
abi,
functionName: 'buyTickets',
args: [
TICKET_COUNT,
address,
[REFERRER_ADDRESS],
[1000000000000000000n],
ZERO_SOURCE,
],
});
};
// 5. Show success with ticket count from event log
const successEvent = receipt?.logs
.map((log) => {
try {
const { eventName, args } = decodeEventLog({ abi, eventName: 'RandomTicketsBought', ...log });
return eventName === 'RandomTicketsBought' ? args : null;
} catch {
return null;
}
})
.find(Boolean);
if (!address) return
);
}
```
> Note: `decodeEventLog` and `parseAbi` are both imported from `'viem'` at the top of the component.
## Dependencies
```bash
npm install wagmi@2 viem @tanstack/react-query @rainbow-me/rainbowkit
npm install tslib
```
> Use wagmi v2 (not v3) for RainbowKit compatibility.
> **`tslib` is required separately.** RainbowKit's dependency tree uses `tslib` but doesn't always declare it — Vite's production bundler fails without it even though the dev server works fine. Install it as a separate step to ensure it's present.
## Complete reference app
The patterns in this skill are wired end-to-end in the [Megapot Starter
Kit](https://llms.megapot.io/starter-kit) — a five-page React + wagmi
reference, running live at [demo.megapot.io](https://demo.megapot.io/).
Provider setup mirrors the `wagmi.ts` / `main.tsx` snippets above;
cross-drawing reads use the same `QueryClient` and `staleTime` table;
the buy / claim / LP / subscribe write flows are all wired into pages
you can study or fork wholesale.
- [`src/main.tsx`](https://github.com/coordinationlabs/megapot-starter-kit/blob/main/src/main.tsx) — providers + `BigInt.prototype.toJSON` + `hashFn`
- [`src/config/wagmi.ts`](https://github.com/coordinationlabs/megapot-starter-kit/blob/main/src/config/wagmi.ts) — chain config with HTTP batching and polling cadence
- [`src/lib/api.ts`](https://github.com/coordinationlabs/megapot-starter-kit/blob/main/src/lib/api.ts) — typed Data API client + `QK` query-key constants
## Related
- Every other `megapot-*` skill — provides the viem patterns this skill translates into React hooks
- `megapot-data-api` — off-chain reads (wallet lifetime stats, round history, per-round leaderboards); host alongside wagmi in the same `QueryClient`
- `megapot-contracts-reference` — full ABIs and addresses for all 13 contracts
- `megapot-buy-random` — the viem version of the buy-random flow shown above
- `megapot-read-state` — multicall patterns for reading protocol state (translates directly to `useReadContracts`)