
pragma solidity ^0.8.7;
import {ResultsConsumer} from "./ResultsConsumer.sol";
import {NativeTokenSender} from "./ccip/NativeTokenSender.sol";
import {AutomationCompatibleInterface} from "@chainlink/contracts/src/v0.8/AutomationCompatible.sol";
struct Config {
  address oracle;
  address ccipRouter;
  address link;
  address weth9Token;
  address exchangeToken;
  address uniswapV3Router;
  uint64 subscriptionId;
  uint64 destinationChainSelector;
  uint32 gasLimit;
  bytes secrets;
  string source;
}
contract SportsPredictionGame is ResultsConsumer, NativeTokenSender, AutomationCompatibleInterface {
  uint256 private constant MIN_WAGER = 0.00001 ether;
  uint256 private constant MAX_WAGER = 0.01 ether;
  uint256 private constant GAME_RESOLVE_DELAY = 2 hours;
  mapping(uint256 => Game) private games;
  mapping(address => mapping(uint256 => Prediction[])) private predictions;
  mapping(uint256 => bytes32) private pendingRequests;
  uint256[] private activeGames;
  uint256[] private resolvedGames;
  struct Game {
    uint256 sportId;
    uint256 externalId;
    uint256 timestamp;
    uint256 homeWagerAmount;
    uint256 awayWagerAmount;
    bool resolved;
    Result result;
  }
  struct Prediction {
    uint256 gameId;
    Result result;
    uint256 amount;
    bool claimed;
  }
  enum Result {
    None,
    Home,
    Away 
  }
  event GameRegistered(uint256 indexed gameId);
  event GameResolved(uint256 indexed gameId, Result result);
  event Predicted(address indexed user, uint256 indexed gameId, Result result, uint256 amount);
  event Claimed(address indexed user, uint256 indexed gameId, uint256 amount);
  error GameAlreadyRegistered();
  error TimestampInPast();
  error GameNotRegistered();
  error GameIsResolved();
  error GameAlreadyStarted();
  error InsufficientValue();
  error ValueTooHigh();
  error InvalidResult();
  error GameNotResolved();
  error GameNotReadyToResolve();
  error ResolveAlreadyRequested();
  error NothingToClaim();
  constructor(
    Config memory config
  )
    ResultsConsumer(config.oracle, config.subscriptionId, config.source, config.secrets, config.gasLimit)
    NativeTokenSender(
      config.ccipRouter,
      config.link,
      config.weth9Token,
      config.exchangeToken,
      config.uniswapV3Router,
      config.destinationChainSelector
    )
  {}
  function predict(uint256 gameId, Result result) public payable {
    Game memory game = games[gameId];
    uint256 wagerAmount = msg.value;
    if (game.externalId == 0) revert GameNotRegistered();
    if (game.resolved) revert GameIsResolved();
    if (game.timestamp < block.timestamp) revert GameAlreadyStarted();
    if (wagerAmount < MIN_WAGER) revert InsufficientValue();
    if (wagerAmount > MAX_WAGER) revert ValueTooHigh();
    if (result == Result.Home) games[gameId].homeWagerAmount += wagerAmount;
    else if (result == Result.Away) games[gameId].awayWagerAmount += wagerAmount;
    else revert InvalidResult();
    predictions[msg.sender][gameId].push(Prediction(gameId, result, wagerAmount, false));
    emit Predicted(msg.sender, gameId, result, wagerAmount);
  }
  function registerAndPredict(uint256 sportId, uint256 externalId, uint256 timestamp, Result result) external payable {
    uint256 gameId = _registerGame(sportId, externalId, timestamp);
    predict(gameId, result);
  }
  function claim(uint256 gameId, bool transfer) external {
    Game memory game = games[gameId];
    address user = msg.sender;
    if (!game.resolved) revert GameNotResolved();
    uint256 totalWinnings = 0;
    Prediction[] memory userPredictions = predictions[user][gameId];
    for (uint256 i = 0; i < userPredictions.length; i++) {
      Prediction memory prediction = userPredictions[i];
      if (prediction.claimed) continue;
      if (game.result == Result.None) {
        totalWinnings += prediction.amount;
      } else if (prediction.result == game.result) {
        uint256 winnings = calculateWinnings(gameId, prediction.amount, prediction.result);
        totalWinnings += winnings;
      }
      predictions[user][gameId][i].claimed = true;
    }
    if (totalWinnings == 0) revert NothingToClaim();
    if (transfer) {
      _sendTransferRequest(user, totalWinnings);
    } else {
      payable(user).transfer(totalWinnings);
    }
    emit Claimed(user, gameId, totalWinnings);
  }
  function _registerGame(uint256 sportId, uint256 externalId, uint256 timestamp) internal returns (uint256 gameId) {
    gameId = getGameId(sportId, externalId);
    if (games[gameId].externalId != 0) revert GameAlreadyRegistered();
    if (timestamp < block.timestamp) revert TimestampInPast();
    games[gameId] = Game(sportId, externalId, timestamp, 0, 0, false, Result.None);
    activeGames.push(gameId);
    emit GameRegistered(gameId);
  }
  function _requestResolve(uint256 gameId) internal {
    Game memory game = games[gameId];
    if (pendingRequests[gameId] != 0) revert ResolveAlreadyRequested();
    if (game.externalId == 0) revert GameNotRegistered();
    if (game.resolved) revert GameIsResolved();
    if (!readyToResolve(gameId)) revert GameNotReadyToResolve();
    pendingRequests[gameId] = _requestResult(game.sportId, game.externalId);
  }
  function _processResult(uint256 sportId, uint256 externalId, bytes memory response) internal override {
    uint256 gameId = getGameId(sportId, externalId);
    Result result = Result(uint256(bytes32(response)));
    _resolveGame(gameId, result);
  }
  function _resolveGame(uint256 gameId, Result result) internal {
    games[gameId].result = result;
    games[gameId].resolved = true;
    resolvedGames.push(gameId);
    _removeFromActiveGames(gameId);
    emit GameResolved(gameId, result);
  }
  function _removeFromActiveGames(uint256 gameId) internal {
    uint256 index;
    for (uint256 i = 0; i < activeGames.length; i++) {
      if (activeGames[i] == gameId) {
        index = i;
        break;
      }
    }
    for (uint256 i = index; i < activeGames.length - 1; i++) {
      activeGames[i] = activeGames[i + 1];
    }
    activeGames.pop();
  }
  function getGameId(uint256 sportId, uint256 externalId) public pure returns (uint256) {
    return (sportId << 128) | externalId;
  }
  function getGame(uint256 gameId) external view returns (Game memory) {
    return games[gameId];
  }
  function getActiveGames() public view returns (Game[] memory) {
    Game[] memory activeGamesArray = new Game[](activeGames.length);
    for (uint256 i = 0; i < activeGames.length; i++) {
      activeGamesArray[i] = games[activeGames[i]];
    }
    return activeGamesArray;
  }
  function getActivePredictions(address user) external view returns (Prediction[] memory) {
    uint256 totalPredictions = 0;
    for (uint256 i = 0; i < activeGames.length; i++) {
      totalPredictions += predictions[user][activeGames[i]].length;
    }
    uint256 index = 0;
    Prediction[] memory userPredictions = new Prediction[](totalPredictions);
    for (uint256 i = 0; i < activeGames.length; i++) {
      Prediction[] memory gamePredictions = predictions[user][activeGames[i]];
      for (uint256 j = 0; j < gamePredictions.length; j++) {
        userPredictions[index] = gamePredictions[j];
        index++;
      }
    }
    return userPredictions;
  }
  function getPastPredictions(address user) external view returns (Prediction[] memory) {
    uint256 totalPredictions = 0;
    for (uint256 i = 0; i < resolvedGames.length; i++) {
      totalPredictions += predictions[user][resolvedGames[i]].length;
    }
    uint256 index = 0;
    Prediction[] memory userPredictions = new Prediction[](totalPredictions);
    for (uint256 i = 0; i < resolvedGames.length; i++) {
      Prediction[] memory gamePredictions = predictions[user][resolvedGames[i]];
      for (uint256 j = 0; j < gamePredictions.length; j++) {
        userPredictions[index] = gamePredictions[j];
        index++;
      }
    }
    return userPredictions;
  }
  function isPredictionCorrect(address user, uint256 gameId, uint32 predictionIdx) external view returns (bool) {
    Game memory game = games[gameId];
    if (!game.resolved) return false;
    Prediction memory prediction = predictions[user][gameId][predictionIdx];
    return prediction.result == game.result;
  }
  function calculateWinnings(uint256 gameId, uint256 wager, Result result) public view returns (uint256) {
    Game memory game = games[gameId];
    uint256 totalWager = game.homeWagerAmount + game.awayWagerAmount;
    uint256 winnings = (wager * totalWager) / (result == Result.Home ? game.homeWagerAmount : game.awayWagerAmount);
    return winnings;
  }
  function readyToResolve(uint256 gameId) public view returns (bool) {
    return games[gameId].timestamp + GAME_RESOLVE_DELAY < block.timestamp;
  }
  function checkUpkeep(bytes memory) public view override returns (bool, bytes memory) {
    Game[] memory activeGamesArray = getActiveGames();
    for (uint256 i = 0; i < activeGamesArray.length; i++) {
      uint256 gameId = getGameId(activeGamesArray[i].sportId, activeGamesArray[i].externalId);
      if (readyToResolve(gameId) && pendingRequests[gameId] == 0) {
        return (true, abi.encodePacked(gameId));
      }
    }
    return (false, "");
  }
  function performUpkeep(bytes calldata data) external override {
    uint256 gameId = abi.decode(data, (uint256));
    _requestResolve(gameId);
  }
  function deletePendingRequest(uint256 gameId) external onlyOwner {
    delete pendingRequests[gameId];
  }
}
See how Chainlink CCIP, Functions, and Automation combine to power a prediction dApp that uses real-time sports data to trigger automatic payouts.
From beginner tutorials and Quickstarts, videos, and courses, find the learning path that suits you.
