Skip to content

emrestay/prediction-market-reactivity

Repository files navigation

Prediction Market — Somnia Reactivity Example

A simple Yes/No prediction market that demonstrates Somnia Reactivity for both on-chain automation and off-chain real-time monitoring.

Live Demo: https://prediction-market-reactivity.vercel.app/

How It Works

User calls resolve()
        │
        ▼
PredictionMarket emits MarketResolved
        │
        ▼  (Somnia Reactivity — within ~2 seconds)
Validator invokes PayoutHandler._onEvent()
        │
        ▼
PayoutHandler calls market.distribute()
        │
        ▼
Winners receive payouts automatically

Two contracts:

Contract Role
PredictionMarket.sol Market logic — create, bet, resolve, distribute
PayoutHandler.sol Reactivity handler — auto-triggers payouts on resolve

Two Reactivity modes demonstrated:

  • On-chain: Subscription triggers PayoutHandler when MarketResolved is emitted → automatic payout in ~2 seconds
  • Off-chain: WebSocket subscription streams live events to the frontend and CLI

Project Structure

prediction-market/
├── contracts/
│   ├── PredictionMarket.sol       # Market logic
│   └── PayoutHandler.sol          # Reactivity event handler (with try/catch safety)
├── scripts/
│   ├── config.ts                  # Shared config (chain, SDK, ABIs)
│   ├── 1-deploy.ts                # Deploy both contracts
│   ├── 2-setup-subscription.ts    # Create on-chain Reactivity subscription
│   ├── 3-interact.ts              # CLI demo (create → bet → resolve)
│   └── 4-watch.ts                 # Live CLI event watcher
├── frontend/                      # React + Vite UI
│   ├── src/
│   │   ├── App.tsx                # Wallet connection + layout
│   │   ├── config.ts              # Chain & contract config
│   │   └── components/
│   │       ├── MarketPanel.tsx     # Create, bet, resolve, distribute UI
│   │       └── EventFeed.tsx       # Live WebSocket event stream
│   └── ...
├── .env.example                   # Environment template
├── package.json                   # Backend scripts
└── hardhat.config.ts              # Solidity compiler (0.8.30)

Quick Start

Prerequisites

  • Node.js 20+
  • MetaMask (for the frontend)
  • Somnia Testnet wallet with 32+ STT

1. Install & Compile

npm install
npm run compile

cd frontend && npm install && cd ..

2. Configure

cp .env.example .env

Edit .env with your private key.

3. Deploy

npm run deploy

Copy the printed addresses into:

  • .envMARKET_CONTRACT and HANDLER_CONTRACT
  • frontend/src/config.tsMARKET_ADDRESS and HANDLER_ADDRESS

4. Setup Reactivity Subscription

npm run setup

This creates an on-chain subscription: when MarketResolved is emitted, validators automatically invoke PayoutHandler which calls distribute().

5. Run the Frontend

cd frontend
npm run dev

Open the URL shown, connect MetaMask (Somnia Testnet), and interact with the market.

5b. CLI Alternative

# Terminal 1 — live event watcher
npm run watch

# Terminal 2 — create market, bet, resolve
npm run interact

Key Reactivity Concepts

On-Chain Subscription (Automatic Payouts)

await sdk.createSoliditySubscription({
  handlerContractAddress: handlerAddr,
  emitter: marketAddr,
  eventTopics: [keccak256(toHex('MarketResolved(uint256,bool)'))],
  gasLimit: 2_000_000n,   // Must cover handler + distribute() gas
  isGuaranteed: true,
  isCoalesced: false,
  ...gasParams,
})

Solidity Handler (with try/catch safety)

contract PayoutHandler is SomniaEventHandler {
    function _onEvent(address, bytes32[] calldata topics, bytes calldata)
        internal override
    {
        uint256 marketId = uint256(topics[1]);
        // try/catch prevents revert → no infinite retry loop
        try market.distribute(marketId) {
            emit AutoPayout(marketId, true);
        } catch {
            emit AutoPayout(marketId, false);
        }
    }
}

Off-Chain Subscription (Live UI via WebSocket)

wsClient.watchContractEvent({
  address: MARKET_ADDRESS,
  abi: MarketABI,
  onLogs: (logs) => {
    // Real-time updates: MarketCreated, BetPlaced, MarketResolved, PayoutSent
  },
})

Lessons Learned

Issue Cause Fix
Handler reverts → infinite retry isGuaranteed: true retries on revert Wrap external calls in try/catch
distribute() fails from handler 500K gas wasn't enough for storage + ETH transfers Increased to 2M based on estimateContractGas

Deployed Contracts (Somnia Testnet)

Contract Address
PredictionMarket 0xe7a5bb2078ccb8ae7ed60f9dd027b11c92d65665
PayoutHandler 0x409116f1489f1e0f649a4efffad78bf619f7bc14

Dependencies

Links

Releases

No releases published

Packages

 
 
 

Contributors