Source Code

Key source files powering Lucky Spin — from the wallet hook to the smart contract to the 3D wheel visualization.

Security Features

Reentrancy Guard

All functions that transfer funds use a nonReentrant modifier that prevents recursive calls.

Access Control

revealAndResolve restricted to contract owner. Only the authorized backend can resolve spins.

Seed Verification

On-chain keccak256 verification ensures revealed seeds match committed hashes.

Locked Fund Tracking

Contract reserves max payout (2 QTUM) per pending spin. Withdrawals respect this reservation.

useQtumWallet.ts

The React hook that manages the full wallet lifecycle: MetaMask detection, snap installation, address retrieval, balance queries, and transaction signing.

frontend/src/hooks/useQtumWallet.ts
import { useState, useCallback, useEffect, useRef } from 'react';
import { ethers } from 'ethers';
import {
  QtumWallet, RPCMethods, fromBase58Check, isMetamaskInstalled,
} from 'qtum-wallet-connector';

export interface WalletState {
  isMetaMaskInstalled: boolean;
  isSnapInstalled: boolean;
  isConnected: boolean;
  address: string | null;
  addressHex: string | null;
  balance: string | null;
  error: string | null;
  isLoading: boolean;
}

export function useQtumWallet() {
  const walletRef = useRef<QtumWallet>(new QtumWallet());

  const [state, setState] = useState<WalletState>({
    isMetaMaskInstalled: false, isSnapInstalled: false,
    isConnected: false, address: null, addressHex: null,
    balance: null, error: null, isLoading: true,
  });

  const checkMetaMask = useCallback(async (): Promise<boolean> => {
    try { return await isMetamaskInstalled(); }
    catch { return false; }
  }, []);

  const checkSnapInstalled = useCallback(async (): Promise<boolean> => {
    try { return await walletRef.current.isInstalled(); }
    catch { return false; }
  }, []);

  const installSnap = useCallback(async (): Promise<boolean> => {
    setState(s => ({ ...s, isLoading: true, error: null }));
    try {
      await walletRef.current.enable();
      setState(s => ({ ...s, isSnapInstalled: true, isLoading: false }));
      return true;
    } catch (err) {
      const msg = err instanceof Error ? err.message : 'Failed to install snap';
      setState(s => ({ ...s, error: msg, isLoading: false }));
      return false;
    }
  }, []);

  const getAddress = useCallback(async () => {
    const address = await walletRef.current.request({
      method: RPCMethods.WalletGetAddress,
    }) as string;
    if (address) {
      return { qtum: address, hex: fromBase58Check(address) };
    }
    return null;
  }, []);

  const getBalance = useCallback(async (address: string) => {
    return await walletRef.current.request({
      method: RPCMethods.EthGetBalance,
      params: [address, 'latest'],
    }) as string;
  }, []);

  const connect = useCallback(async () => {
    setState(s => ({ ...s, isLoading: true, error: null }));
    const hasMM = await checkMetaMask();
    if (!hasMM) {
      setState(s => ({ ...s, error: 'Please install MetaMask', isLoading: false }));
      return null;
    }
    const isInstalled = await checkSnapInstalled();
    if (!isInstalled) { await installSnap(); }
    const addr = await getAddress();
    if (!addr) { setState(s => ({ ...s, error: 'Failed to get address', isLoading: false })); return null; }
    const balance = await getBalance(addr.qtum);
    setState(s => ({
      ...s, isConnected: true, address: addr.qtum, addressHex: addr.hex,
      balance, error: null, isLoading: false,
    }));
    return addr.qtum;
  }, [checkMetaMask, checkSnapInstalled, installSnap, getAddress, getBalance]);

  const sendContractTransaction = useCallback(async (
    contractAddress: string, data: string, valueQtum = '0'
  ) => {
    const toHex = contractAddress.startsWith('0x') ? contractAddress : '0x' + contractAddress;
    const dataHex = data.startsWith('0x') ? data : '0x' + data;
    const valueInSatoshis = ethers.parseUnits(valueQtum, 8);
    return await walletRef.current.request({
      method: RPCMethods.EthSendTransaction,
      params: [{
        from: state.addressHex,
        to: toHex,
        value: '0x' + valueInSatoshis.toString(16),
        data: dataHex,
        gas: '0x' + (500000).toString(16),
      }],
    }) as string;
  }, [state.addressHex]);

  const refreshBalance = useCallback(async () => {
    if (state.address) {
      const balance = await getBalance(state.address);
      setState(s => ({ ...s, balance }));
    }
  }, [state.address, getBalance]);

  const disconnect = useCallback(() => {
    setState(s => ({ ...s, isConnected: false, address: null, addressHex: null, balance: null }));
  }, []);

  useEffect(() => {
    (async () => {
      const hasMM = await checkMetaMask();
      const hasSnap = hasMM ? await checkSnapInstalled() : false;
      setState(s => ({ ...s, isMetaMaskInstalled: hasMM, isSnapInstalled: hasSnap, isLoading: false }));
    })();
  }, [checkMetaMask, checkSnapInstalled]);

  const formatBalance = useCallback((bal: string | null) => {
    if (!bal) return '0';
    try { return ethers.formatEther(bal); }
    catch { return '0'; }
  }, []);

  return {
    ...state, connect, disconnect, sendContractTransaction,
    refreshBalance, formatBalance, installSnap,
  };
}

SpinGame.sol

The complete Solidity smart contract. See the Smart Contract page for a detailed function-by-function breakdown.

contracts/SpinGame.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract SpinGame {
    address public owner;
    uint256 public constant SPIN_COST = 100000000;
    uint8 public constant NUM_SEGMENTS = 8;

    uint256[8] public prizes = [
        40000000, 70000000, 110000000, 130000000,
        40000000, 70000000, 110000000, 200000000
    ];

    struct Spin {
        address player; bytes32 playerSeedHash; bytes32 houseSeedHash;
        uint256 timestamp; bool resolved; uint8 segment; uint256 payout;
    }

    mapping(uint256 => Spin) public spins;
    mapping(bytes32 => uint256) public houseSeedHashToSpinId;
    uint256 public nextSpinId = 1;
    uint256 public lockedFunds;
    bool private _locked;
    address public pendingOwner;

    // Events, modifiers, constructor omitted for brevity
    // See smart-contract.html for the complete annotated source

    function placeSpin(bytes32 playerSeedHash, bytes32 houseSeedHash) external payable { ... }
    function revealAndResolve(uint256 spinId, bytes32 houseSeed, bytes32 playerSeed) external onlyOwner nonReentrant { ... }
    function claimRefund(uint256 spinId) external nonReentrant { ... }
    function getSpin(uint256 spinId) external view returns (...) { ... }
    function getBalance() external view returns (uint256) { ... }
}

Other Key Files

SpinGame.tsx

The main React component for the spin game. Orchestrates the full spin flow: seed generation, house seed request, ABI encoding, transaction submission, polling, and 3D wheel animation. See the Snap Integration guide for a step-by-step walkthrough of the betting logic.

  • Location: frontend/src/components/SpinGame.tsx
  • Key functions: handleSpin(), encodePlaceSpin(), pollUntil()
  • State machine: idle → placing → confirming → resolving → spinning → done

spinApi.ts

TypeScript API client for the Lucky Spin backend endpoints. Provides typed functions for all API calls with proper error handling.

  • Location: frontend/src/utils/spinApi.ts
  • Functions: getSpinStatus(), getHouseSeed(), submitPlayerSeed(), getSpinDetails(), findSpinByHouseSeedHash(), getPrizesInfo(), getSpinHistory()

SpinWheel3D.ts

The Three.js 3D wheel engine. Builds the wheel texture, gold rim, light bulb ring, pointer, particles, and handles animation (loop spin, landing animation with easing, bloom effects, camera shake on win).

  • Location: frontend/src/lib/SpinWheel3D.ts
  • Key methods: start(), startLoopSpin(), landOnSegment(), showWinEffect(), stopSpin(), dispose()
  • Features: Post-processing bloom (UnrealBloomPass), particle burst on win, easeOutBack landing animation, procedural textures