--- 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

Connect your wallet to buy tickets.

; return (

Ticket price: {ticketPrice ? `${Number(ticketPrice) / 1e6} USDC` : 'Loading...'}

Total for 5 tickets: {totalCost ? `${Number(totalCost) / 1e6} USDC` : 'Loading...'}

{needsApproval ? ( ) : ( )} {receipt && !successEvent &&

Transaction confirmed: {receipt.transactionHash}

} {successEvent && (

Success! Bought {String(successEvent.count)} ticket(s) for drawing #{String(successEvent.drawingId)}. Ticket IDs: {successEvent.ticketIds.map(String).join(', ')}

)}
); } ``` > 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`)