---
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(
  <WagmiProvider config={config}>
    <QueryClientProvider client={queryClient}>
      <RainbowKitProvider>
        <App />
      </RainbowKitProvider>
    </QueryClientProvider>
  </WagmiProvider>
);
```

> **`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<typeof approve>[0],
    executeArgs: Parameters<typeof execute>[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<WalletStats>` 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<WalletStats>({
    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<WalletStats>;
    },
    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, <resource>, ...args]` — the URL slot lets a fork point at a different deploy (e.g. an internal mirror) without invalidating the production cache, and the `<resource>` 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 <p>Connect your wallet to buy tickets.</p>;

  return (
    <div>
      <p>Ticket price: {ticketPrice ? `${Number(ticketPrice) / 1e6} USDC` : 'Loading...'}</p>
      <p>Total for 5 tickets: {totalCost ? `${Number(totalCost) / 1e6} USDC` : 'Loading...'}</p>

      {needsApproval ? (
        <button onClick={handleApprove} disabled={isPending || isConfirming}>
          {isPending || isConfirming ? 'Approving...' : 'Approve USDC'}
        </button>
      ) : (
        <button onClick={handleBuy} disabled={isPending || isConfirming || !totalCost}>
          {isPending || isConfirming ? 'Buying...' : 'Buy 5 Random Tickets'}
        </button>
      )}

      {receipt && !successEvent && <p>Transaction confirmed: {receipt.transactionHash}</p>}

      {successEvent && (
        <p>
          Success! Bought {String(successEvent.count)} ticket(s) for drawing #{String(successEvent.drawingId)}.
          Ticket IDs: {successEvent.ticketIds.map(String).join(', ')}
        </p>
      )}
    </div>
  );
}
```

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

## 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`)
