Architecture

How the smart contract, backend, and frontend work together to create a trustless, provably fair spin wheel game.

System Overview

Lucky Spin is a three-tier application: a Solidity smart contract for on-chain logic, a Node.js backend for seed management and blockchain interaction, and a React frontend with a 3D Three.js wheel for the user interface.

┌─────────────────────────────────┐ │ PLAYER BROWSER │ │ │ │ React Frontend (Vite + TS) │ │ ┌───────────┐ ┌──────────────┐ │ │ │ SpinGame │ │ useQtumWallet │ │ │ │ component │ │ (Snap hook) │ │ │ └─────┬─────┘ └──────┬───────┘ │ │ │ │ │ └────────┼───────────────┼─────────┘ │ HTTP/WS │ wallet_invokeSnap ▼ ▼ ┌────────────────┐ ┌──────────────────┐ │ BACKEND │ │ METAMASK │ │ Express + │ │ + qtum-wallet │ │ WebSocket │ │ │ │ │ │ Signs & broadcasts│ │ Spin Manager │ │ transactions │ │ Contract Mon. │ └────────┬─────────┘ │ QTUM RPC │ │ └───────┬────────┘ │ │ RPC calls │ Signed TX ▼ ▼ ┌─────────────────────────────────┐ │ QTUM TESTNET NODE │ │ │ │ ┌─────────────────────────┐ │ │ │ SpinGame Contract │ │ │ │ 33531da3d5d75c0dc1c... │ │ │ │ │ │ │ │ placeSpin() │ │ │ │ revealAndResolve() │ │ │ │ claimRefund() │ │ │ └─────────────────────────┘ │ └─────────────────────────────────┘

Component Roles

Frontend (React + Three.js)

The frontend handles all user-facing functionality:

  • Wallet connection — Uses useQtumWallet hook to connect MetaMask via qtum-wallet-connector
  • Seed generation — Generates 32 random bytes via crypto.getRandomValues() and hashes with keccak256
  • ABI encoding — Encodes placeSpin(bytes32, bytes32) calls using ethers.js AbiCoder
  • Transaction submission — Sends 1 QTUM to the SpinGame contract via the snap
  • Polling — Polls backend for on-chain confirmation and spin resolution
  • 3D visualization — Three.js wheel with bloom effects, particle bursts, and animated landing
  • Recovery — Stores pending spin data in localStorage for page reload recovery

Backend (Node.js + Express)

The backend serves as the trusted house operator:

  • House seed generation — Generates cryptographically random seeds and stores them in memory
  • Contract monitoring — Polls the blockchain every 5 seconds for SpinPlaced and SpinResolved events
  • Spin registration — When a SpinPlaced event is detected, links it to the stored house seed
  • Reveal coordination — When the player submits their seed, calls revealAndResolve() on-chain
  • QTUM RPC — Wraps sendtocontract, callcontract, and searchlogs RPC calls

Smart Contract (SpinGame.sol)

The contract is the source of truth for all game logic:

  • Spin placement — Accepts 1 QTUM, stores seed hashes, locks funds for max payout
  • Seed verification — Verifies that revealed seeds match committed hashes
  • Segment calculation — Computes keccak256(houseSeed + playerSeed + spinId) % 8
  • Automatic payouts — Sends prize amount directly to the player's address
  • Refund mechanism — Players can claim a refund if their spin is not resolved within 1 hour

Provably Fair Scheme

The commit-reveal pattern is the foundation of trustless gaming. It ensures that neither party can influence the outcome after both have committed:

Why Commit-Reveal?

If the house chose the result after seeing the player's seed, it could cheat. If the player chose after seeing the house seed, they could cheat. By having both parties commit (hash) their seeds before revealing them, neither can manipulate the result.

Detailed Flow

1

House Seed Generation

Backend generates 32 random bytes (houseSeed), computes keccak256(houseSeed) = houseSeedHash, stores the seed, and returns the hash to the frontend.

2

Player Seed Generation

Frontend generates 32 random bytes (playerSeed) via crypto.getRandomValues(), computes keccak256(playerSeed) = playerSeedHash.

3

On-Chain Commit

Player calls placeSpin(playerSeedHash, houseSeedHash) with 1 QTUM. Both hashes are now permanently recorded on-chain — neither party can change their seed.

4

Reveal & Resolve

Player sends playerSeed to backend. Backend calls revealAndResolve(spinId, houseSeed, playerSeed). The contract verifies both seeds match their hashes, then computes the result:

segment = uint8(uint256(keccak256(houseSeed, playerSeed, spinId)) % 8)
payout  = prizes[segment]

Data Flow

Frontend Backend Blockchain │ │ │ │ GET /api/spin/house-seed │ │ │─────────────────────────▶│ generate houseSeed │ │ { houseSeedHash } │ store houseSeed │ │◄─────────────────────────│ │ │ │ │ │ generate playerSeed │ │ │ ABI-encode placeSpin() │ │ │ │ │ │ placeSpin(hashes) + 1 QTUM via MetaMask Snap │ │────────────────────────────────────────────────▶│ │ │ │ │ │ poll for SpinPlaced event │ │ │───────────────────────▶│ │ │ { event: SpinPlaced } │ │ │◄───────────────────────│ │ │ link houseSeed to spinId │ │ │ │ │ POST submit-player-seed │ │ │─────────────────────────▶│ │ │ │ revealAndResolve(seeds) │ │ │───────────────────────▶│ │ │ compute segment, payout │ │ │ transfer prize to player │ │ │ emit SpinResolved │ │ poll spin details │◄───────────────────────│ │─────────────────────────▶│ │ │ { status: resolved } │ │ │◄─────────────────────────│ │ │ animate wheel landing │ │ │ display result │ │ ▼ ▼ ▼

Security Measures

Reentrancy Guard

revealAndResolve and claimRefund use a nonReentrant modifier to prevent recursive calls during fund transfers.

Locked Funds

The contract tracks lockedFunds to ensure it always reserves enough to cover the maximum payout (2 QTUM) for each pending spin.

Owner-Only Reveal

Only the contract owner can call revealAndResolve(), preventing unauthorized resolution attempts.

Player Refunds

If a spin is not resolved within 1 hour, the player can call claimRefund() to recover their 1 QTUM.