Init.
This commit is contained in:
20
backend/app/blockchain/__init__.py
Normal file
20
backend/app/blockchain/__init__.py
Normal file
@ -0,0 +1,20 @@
|
||||
"""
|
||||
Blockchain Integration Package
|
||||
|
||||
This package provides smart contract integration for the H2H betting platform,
|
||||
implementing a hybrid architecture where escrow and settlement occur on-chain
|
||||
while maintaining fast queries through database caching.
|
||||
|
||||
Main components:
|
||||
- contracts/: Smart contract pseudocode and architecture
|
||||
- services/: Web3 integration, event indexing, and oracle network
|
||||
- config.py: Blockchain configuration
|
||||
"""
|
||||
|
||||
from .config import get_blockchain_config, CHAIN_IDS, BLOCK_EXPLORERS
|
||||
|
||||
__all__ = [
|
||||
"get_blockchain_config",
|
||||
"CHAIN_IDS",
|
||||
"BLOCK_EXPLORERS",
|
||||
]
|
||||
189
backend/app/blockchain/config.py
Normal file
189
backend/app/blockchain/config.py
Normal file
@ -0,0 +1,189 @@
|
||||
"""
|
||||
Blockchain Configuration
|
||||
|
||||
Centralized configuration for blockchain integration including:
|
||||
- RPC endpoints
|
||||
- Contract addresses
|
||||
- Oracle node addresses
|
||||
- Network settings
|
||||
|
||||
In production, these would be loaded from environment variables.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
import os
|
||||
|
||||
|
||||
# Network Configuration
|
||||
NETWORK = os.getenv("BLOCKCHAIN_NETWORK", "sepolia") # sepolia, polygon-mumbai, mainnet, polygon
|
||||
|
||||
# RPC Endpoints
|
||||
RPC_URLS = {
|
||||
"mainnet": os.getenv("ETHEREUM_MAINNET_RPC", "https://eth-mainnet.alchemyapi.io/v2/YOUR-API-KEY"),
|
||||
"sepolia": os.getenv("ETHEREUM_SEPOLIA_RPC", "https://eth-sepolia.alchemyapi.io/v2/YOUR-API-KEY"),
|
||||
"polygon": os.getenv("POLYGON_MAINNET_RPC", "https://polygon-mainnet.g.alchemy.com/v2/YOUR-API-KEY"),
|
||||
"polygon-mumbai": os.getenv("POLYGON_MUMBAI_RPC", "https://polygon-mumbai.g.alchemy.com/v2/YOUR-API-KEY"),
|
||||
}
|
||||
|
||||
# Contract Addresses (per network)
|
||||
CONTRACT_ADDRESSES = {
|
||||
"mainnet": {
|
||||
"bet_escrow": os.getenv("MAINNET_BET_ESCROW_ADDRESS", "0x0000000000000000000000000000000000000000"),
|
||||
"bet_oracle": os.getenv("MAINNET_BET_ORACLE_ADDRESS", "0x0000000000000000000000000000000000000000"),
|
||||
},
|
||||
"sepolia": {
|
||||
"bet_escrow": os.getenv("SEPOLIA_BET_ESCROW_ADDRESS", "0x1234567890abcdef1234567890abcdef12345678"),
|
||||
"bet_oracle": os.getenv("SEPOLIA_BET_ORACLE_ADDRESS", "0xfedcba0987654321fedcba0987654321fedcba09"),
|
||||
},
|
||||
"polygon": {
|
||||
"bet_escrow": os.getenv("POLYGON_BET_ESCROW_ADDRESS", "0x0000000000000000000000000000000000000000"),
|
||||
"bet_oracle": os.getenv("POLYGON_BET_ORACLE_ADDRESS", "0x0000000000000000000000000000000000000000"),
|
||||
},
|
||||
"polygon-mumbai": {
|
||||
"bet_escrow": os.getenv("MUMBAI_BET_ESCROW_ADDRESS", "0xabcdef1234567890abcdef1234567890abcdef12"),
|
||||
"bet_oracle": os.getenv("MUMBAI_BET_ORACLE_ADDRESS", "0x234567890abcdef1234567890abcdef123456789"),
|
||||
},
|
||||
}
|
||||
|
||||
# Backend Signer Configuration
|
||||
# IMPORTANT: In production, use a secure key management system (AWS KMS, HashiCorp Vault, etc.)
|
||||
# Never commit private keys to version control
|
||||
BACKEND_PRIVATE_KEY = os.getenv("BLOCKCHAIN_BACKEND_PRIVATE_KEY", "")
|
||||
|
||||
# Oracle Node Configuration
|
||||
ORACLE_NODES = {
|
||||
"node1": {
|
||||
"address": os.getenv("ORACLE_NODE1_ADDRESS", "0xNode1Address..."),
|
||||
"endpoint": os.getenv("ORACLE_NODE1_ENDPOINT", "https://oracle1.h2h.com"),
|
||||
},
|
||||
"node2": {
|
||||
"address": os.getenv("ORACLE_NODE2_ADDRESS", "0xNode2Address..."),
|
||||
"endpoint": os.getenv("ORACLE_NODE2_ENDPOINT", "https://oracle2.h2h.com"),
|
||||
},
|
||||
"node3": {
|
||||
"address": os.getenv("ORACLE_NODE3_ADDRESS", "0xNode3Address..."),
|
||||
"endpoint": os.getenv("ORACLE_NODE3_ENDPOINT", "https://oracle3.h2h.com"),
|
||||
},
|
||||
"node4": {
|
||||
"address": os.getenv("ORACLE_NODE4_ADDRESS", "0xNode4Address..."),
|
||||
"endpoint": os.getenv("ORACLE_NODE4_ENDPOINT", "https://oracle4.h2h.com"),
|
||||
},
|
||||
"node5": {
|
||||
"address": os.getenv("ORACLE_NODE5_ADDRESS", "0xNode5Address..."),
|
||||
"endpoint": os.getenv("ORACLE_NODE5_ENDPOINT", "https://oracle5.h2h.com"),
|
||||
},
|
||||
}
|
||||
|
||||
# Oracle Consensus Configuration
|
||||
ORACLE_CONSENSUS_THRESHOLD = int(os.getenv("ORACLE_CONSENSUS_THRESHOLD", "3")) # 3 of 5 nodes must agree
|
||||
ORACLE_TIMEOUT_SECONDS = int(os.getenv("ORACLE_TIMEOUT_SECONDS", "3600")) # 1 hour
|
||||
|
||||
# Indexer Configuration
|
||||
INDEXER_POLL_INTERVAL = int(os.getenv("INDEXER_POLL_INTERVAL", "10")) # seconds
|
||||
INDEXER_START_BLOCK = int(os.getenv("INDEXER_START_BLOCK", "0")) # 0 = contract deployment block
|
||||
|
||||
# Gas Configuration
|
||||
GAS_PRICE_MULTIPLIER = float(os.getenv("GAS_PRICE_MULTIPLIER", "1.2")) # 20% above current gas price
|
||||
MAX_GAS_PRICE_GWEI = int(os.getenv("MAX_GAS_PRICE_GWEI", "500")) # Never pay more than 500 gwei
|
||||
|
||||
# Transaction Configuration
|
||||
TRANSACTION_TIMEOUT = int(os.getenv("TRANSACTION_TIMEOUT", "300")) # 5 minutes
|
||||
CONFIRMATION_BLOCKS = int(os.getenv("CONFIRMATION_BLOCKS", "2")) # Wait for 2 block confirmations
|
||||
|
||||
# API Endpoints for Oracle Data Sources
|
||||
API_ENDPOINTS = {
|
||||
"espn": os.getenv("ESPN_API_KEY", "https://api.espn.com/v1"),
|
||||
"odds_api": os.getenv("ODDS_API_KEY", "https://api.the-odds-api.com/v4"),
|
||||
"oscars": os.getenv("OSCARS_API_KEY", "https://api.oscars.com"),
|
||||
}
|
||||
|
||||
|
||||
def get_blockchain_config(network: str = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Get blockchain configuration for specified network.
|
||||
|
||||
Args:
|
||||
network: Network name (mainnet, sepolia, polygon, polygon-mumbai)
|
||||
If None, uses NETWORK from environment
|
||||
|
||||
Returns:
|
||||
Dict with all blockchain configuration
|
||||
"""
|
||||
if network is None:
|
||||
network = NETWORK
|
||||
|
||||
if network not in RPC_URLS:
|
||||
raise ValueError(f"Unknown network: {network}")
|
||||
|
||||
return {
|
||||
"network": network,
|
||||
"rpc_url": RPC_URLS[network],
|
||||
"bet_escrow_address": CONTRACT_ADDRESSES[network]["bet_escrow"],
|
||||
"bet_oracle_address": CONTRACT_ADDRESSES[network]["bet_oracle"],
|
||||
"backend_private_key": BACKEND_PRIVATE_KEY,
|
||||
"oracle_nodes": ORACLE_NODES,
|
||||
"oracle_consensus_threshold": ORACLE_CONSENSUS_THRESHOLD,
|
||||
"oracle_timeout": ORACLE_TIMEOUT_SECONDS,
|
||||
"indexer_poll_interval": INDEXER_POLL_INTERVAL,
|
||||
"indexer_start_block": INDEXER_START_BLOCK,
|
||||
"gas_price_multiplier": GAS_PRICE_MULTIPLIER,
|
||||
"max_gas_price_gwei": MAX_GAS_PRICE_GWEI,
|
||||
"transaction_timeout": TRANSACTION_TIMEOUT,
|
||||
"confirmation_blocks": CONFIRMATION_BLOCKS,
|
||||
"api_endpoints": API_ENDPOINTS,
|
||||
}
|
||||
|
||||
|
||||
# Network Chain IDs (for frontend)
|
||||
CHAIN_IDS = {
|
||||
"mainnet": 1,
|
||||
"sepolia": 11155111,
|
||||
"polygon": 137,
|
||||
"polygon-mumbai": 80001,
|
||||
}
|
||||
|
||||
|
||||
# Block Explorer URLs (for frontend links)
|
||||
BLOCK_EXPLORERS = {
|
||||
"mainnet": "https://etherscan.io",
|
||||
"sepolia": "https://sepolia.etherscan.io",
|
||||
"polygon": "https://polygonscan.com",
|
||||
"polygon-mumbai": "https://mumbai.polygonscan.com",
|
||||
}
|
||||
|
||||
|
||||
# Example .env file content:
|
||||
"""
|
||||
# Blockchain Configuration
|
||||
BLOCKCHAIN_NETWORK=sepolia
|
||||
|
||||
# RPC Endpoints (get API keys from Alchemy, Infura, or QuickNode)
|
||||
ETHEREUM_MAINNET_RPC=https://eth-mainnet.alchemyapi.io/v2/YOUR-API-KEY
|
||||
ETHEREUM_SEPOLIA_RPC=https://eth-sepolia.alchemyapi.io/v2/YOUR-API-KEY
|
||||
POLYGON_MAINNET_RPC=https://polygon-mainnet.g.alchemy.com/v2/YOUR-API-KEY
|
||||
POLYGON_MUMBAI_RPC=https://polygon-mumbai.g.alchemy.com/v2/YOUR-API-KEY
|
||||
|
||||
# Contract Addresses (update after deployment)
|
||||
SEPOLIA_BET_ESCROW_ADDRESS=0x1234567890abcdef1234567890abcdef12345678
|
||||
SEPOLIA_BET_ORACLE_ADDRESS=0xfedcba0987654321fedcba0987654321fedcba09
|
||||
|
||||
# Backend Signer (NEVER commit to git!)
|
||||
BLOCKCHAIN_BACKEND_PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE
|
||||
|
||||
# Oracle Nodes
|
||||
ORACLE_NODE1_ADDRESS=0xNode1Address...
|
||||
ORACLE_NODE1_ENDPOINT=https://oracle1.h2h.com
|
||||
ORACLE_CONSENSUS_THRESHOLD=3
|
||||
|
||||
# Indexer
|
||||
INDEXER_START_BLOCK=12345678
|
||||
INDEXER_POLL_INTERVAL=10
|
||||
|
||||
# Gas
|
||||
GAS_PRICE_MULTIPLIER=1.2
|
||||
MAX_GAS_PRICE_GWEI=500
|
||||
|
||||
# External APIs
|
||||
ESPN_API_KEY=YOUR_ESPN_API_KEY
|
||||
ODDS_API_KEY=YOUR_ODDS_API_KEY
|
||||
"""
|
||||
563
backend/app/blockchain/contracts/BetEscrow.pseudocode.md
Normal file
563
backend/app/blockchain/contracts/BetEscrow.pseudocode.md
Normal file
@ -0,0 +1,563 @@
|
||||
# BetEscrow Smart Contract (Pseudocode)
|
||||
|
||||
## Overview
|
||||
|
||||
The BetEscrow contract manages the entire lifecycle of peer-to-peer bets on the blockchain. It handles bet creation, escrow locking, settlement, and fund distribution in a trustless manner.
|
||||
|
||||
## State Variables
|
||||
|
||||
```solidity
|
||||
// Contract state
|
||||
mapping(uint256 => Bet) public bets;
|
||||
mapping(address => uint256[]) public userBetIds;
|
||||
uint256 public nextBetId;
|
||||
address public oracleContract;
|
||||
address public owner;
|
||||
|
||||
// Escrow tracking
|
||||
mapping(uint256 => uint256) public escrowBalance; // betId => total locked amount
|
||||
```
|
||||
|
||||
## Data Structures
|
||||
|
||||
```solidity
|
||||
enum BetStatus {
|
||||
OPEN, // Created, waiting for opponent
|
||||
MATCHED, // Opponent accepted, funds locked
|
||||
PENDING_ORACLE, // Waiting for oracle settlement
|
||||
COMPLETED, // Settled and paid out
|
||||
CANCELLED, // Cancelled before matching
|
||||
DISPUTED // Settlement disputed
|
||||
}
|
||||
|
||||
struct Bet {
|
||||
uint256 betId;
|
||||
address creator;
|
||||
address opponent;
|
||||
uint256 stakeAmount; // Amount each party stakes (in wei)
|
||||
BetStatus status;
|
||||
uint256 creatorOdds; // Multiplier for creator (scaled by 100, e.g., 150 = 1.5x)
|
||||
uint256 opponentOdds; // Multiplier for opponent
|
||||
uint256 createdAt; // Block timestamp
|
||||
uint256 eventTimestamp; // When the real-world event occurs
|
||||
bytes32 eventId; // External event identifier for oracle
|
||||
address winner; // Winner address (null until settled)
|
||||
uint256 settledAt; // Settlement timestamp
|
||||
}
|
||||
```
|
||||
|
||||
## Events
|
||||
|
||||
```solidity
|
||||
event BetCreated(
|
||||
uint256 indexed betId,
|
||||
address indexed creator,
|
||||
uint256 stakeAmount,
|
||||
bytes32 eventId,
|
||||
uint256 eventTimestamp
|
||||
);
|
||||
|
||||
event BetMatched(
|
||||
uint256 indexed betId,
|
||||
address indexed opponent,
|
||||
uint256 totalEscrow
|
||||
);
|
||||
|
||||
event BetSettled(
|
||||
uint256 indexed betId,
|
||||
address indexed winner,
|
||||
uint256 payoutAmount
|
||||
);
|
||||
|
||||
event BetCancelled(
|
||||
uint256 indexed betId,
|
||||
address indexed creator
|
||||
);
|
||||
|
||||
event BetDisputed(
|
||||
uint256 indexed betId,
|
||||
address indexed disputedBy,
|
||||
uint256 timestamp
|
||||
);
|
||||
|
||||
event EscrowLocked(
|
||||
uint256 indexed betId,
|
||||
address indexed user,
|
||||
uint256 amount
|
||||
);
|
||||
|
||||
event EscrowReleased(
|
||||
uint256 indexed betId,
|
||||
address indexed user,
|
||||
uint256 amount
|
||||
);
|
||||
```
|
||||
|
||||
## Modifiers
|
||||
|
||||
```solidity
|
||||
modifier onlyOracle() {
|
||||
require(msg.sender == oracleContract, "Only oracle can call this");
|
||||
_;
|
||||
}
|
||||
|
||||
modifier onlyOwner() {
|
||||
require(msg.sender == owner, "Only owner can call this");
|
||||
_;
|
||||
}
|
||||
|
||||
modifier betExists(uint256 betId) {
|
||||
require(betId < nextBetId, "Bet does not exist");
|
||||
_;
|
||||
}
|
||||
|
||||
modifier onlyParticipant(uint256 betId) {
|
||||
Bet storage bet = bets[betId];
|
||||
require(
|
||||
msg.sender == bet.creator || msg.sender == bet.opponent,
|
||||
"Not a participant"
|
||||
);
|
||||
_;
|
||||
}
|
||||
```
|
||||
|
||||
## Core Functions
|
||||
|
||||
### 1. Create Bet
|
||||
|
||||
```solidity
|
||||
function createBet(
|
||||
uint256 stakeAmount,
|
||||
uint256 creatorOdds,
|
||||
uint256 opponentOdds,
|
||||
uint256 eventTimestamp,
|
||||
bytes32 eventId
|
||||
) external returns (uint256) {
|
||||
// Validation
|
||||
require(stakeAmount > 0, "Stake must be positive");
|
||||
require(stakeAmount <= 10000 ether, "Stake exceeds maximum");
|
||||
require(creatorOdds > 0, "Creator odds must be positive");
|
||||
require(opponentOdds > 0, "Opponent odds must be positive");
|
||||
require(eventTimestamp > block.timestamp, "Event must be in future");
|
||||
|
||||
// Generate bet ID
|
||||
uint256 betId = nextBetId++;
|
||||
|
||||
// Create bet (NO funds locked yet)
|
||||
bets[betId] = Bet({
|
||||
betId: betId,
|
||||
creator: msg.sender,
|
||||
opponent: address(0),
|
||||
stakeAmount: stakeAmount,
|
||||
status: BetStatus.OPEN,
|
||||
creatorOdds: creatorOdds,
|
||||
opponentOdds: opponentOdds,
|
||||
createdAt: block.timestamp,
|
||||
eventTimestamp: eventTimestamp,
|
||||
eventId: eventId,
|
||||
winner: address(0),
|
||||
settledAt: 0
|
||||
});
|
||||
|
||||
// Track user's bets
|
||||
userBetIds[msg.sender].push(betId);
|
||||
|
||||
// Emit event
|
||||
emit BetCreated(betId, msg.sender, stakeAmount, eventId, eventTimestamp);
|
||||
|
||||
return betId;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Accept Bet (Lock Escrow for Both Parties)
|
||||
|
||||
```solidity
|
||||
function acceptBet(uint256 betId)
|
||||
external
|
||||
payable
|
||||
betExists(betId)
|
||||
{
|
||||
Bet storage bet = bets[betId];
|
||||
|
||||
// Validation
|
||||
require(bet.status == BetStatus.OPEN, "Bet not open");
|
||||
require(msg.sender != bet.creator, "Cannot accept own bet");
|
||||
require(msg.value == bet.stakeAmount, "Incorrect stake amount");
|
||||
|
||||
// CRITICAL: Lock creator's funds from their account
|
||||
// NOTE: In production, creator would approve this contract to spend their funds
|
||||
// For this pseudocode, assume creator has pre-approved the transfer
|
||||
require(
|
||||
transferFrom(bet.creator, address(this), bet.stakeAmount),
|
||||
"Creator funds transfer failed"
|
||||
);
|
||||
|
||||
// Lock opponent's funds (already sent with msg.value)
|
||||
// Both stakes now held in contract
|
||||
|
||||
// Update escrow balance
|
||||
escrowBalance[betId] = bet.stakeAmount * 2;
|
||||
|
||||
// Update bet state
|
||||
bet.opponent = msg.sender;
|
||||
bet.status = BetStatus.MATCHED;
|
||||
|
||||
// Track opponent's bets
|
||||
userBetIds[msg.sender].push(betId);
|
||||
|
||||
// Emit events
|
||||
emit EscrowLocked(betId, bet.creator, bet.stakeAmount);
|
||||
emit EscrowLocked(betId, msg.sender, bet.stakeAmount);
|
||||
emit BetMatched(betId, msg.sender, escrowBalance[betId]);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Request Oracle Settlement
|
||||
|
||||
```solidity
|
||||
function requestSettlement(uint256 betId)
|
||||
external
|
||||
betExists(betId)
|
||||
onlyParticipant(betId)
|
||||
{
|
||||
Bet storage bet = bets[betId];
|
||||
|
||||
// Validation
|
||||
require(
|
||||
bet.status == BetStatus.MATCHED,
|
||||
"Bet must be matched"
|
||||
);
|
||||
require(
|
||||
block.timestamp >= bet.eventTimestamp,
|
||||
"Event has not occurred yet"
|
||||
);
|
||||
|
||||
// Update status
|
||||
bet.status = BetStatus.PENDING_ORACLE;
|
||||
|
||||
// Call oracle contract to request settlement
|
||||
IOracleContract(oracleContract).requestSettlement(
|
||||
betId,
|
||||
bet.eventId
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Settle Bet (Called by Oracle)
|
||||
|
||||
```solidity
|
||||
function settleBet(uint256 betId, address winnerId)
|
||||
external
|
||||
betExists(betId)
|
||||
onlyOracle
|
||||
{
|
||||
Bet storage bet = bets[betId];
|
||||
|
||||
// Validation
|
||||
require(
|
||||
bet.status == BetStatus.MATCHED ||
|
||||
bet.status == BetStatus.PENDING_ORACLE,
|
||||
"Invalid bet status for settlement"
|
||||
);
|
||||
require(
|
||||
winnerId == bet.creator || winnerId == bet.opponent,
|
||||
"Winner must be a participant"
|
||||
);
|
||||
|
||||
// Calculate payout
|
||||
uint256 totalPayout = escrowBalance[betId]; // Both stakes
|
||||
|
||||
// Transfer funds to winner
|
||||
require(
|
||||
payable(winnerId).transfer(totalPayout),
|
||||
"Payout transfer failed"
|
||||
);
|
||||
|
||||
// Update bet state
|
||||
bet.winner = winnerId;
|
||||
bet.status = BetStatus.COMPLETED;
|
||||
bet.settledAt = block.timestamp;
|
||||
|
||||
// Clear escrow
|
||||
escrowBalance[betId] = 0;
|
||||
|
||||
// Emit events
|
||||
emit EscrowReleased(betId, winnerId, totalPayout);
|
||||
emit BetSettled(betId, winnerId, totalPayout);
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Dispute Settlement
|
||||
|
||||
```solidity
|
||||
function disputeBet(uint256 betId)
|
||||
external
|
||||
betExists(betId)
|
||||
onlyParticipant(betId)
|
||||
{
|
||||
Bet storage bet = bets[betId];
|
||||
|
||||
// Validation
|
||||
require(
|
||||
bet.status == BetStatus.PENDING_ORACLE ||
|
||||
bet.status == BetStatus.COMPLETED,
|
||||
"Can only dispute pending or completed bets"
|
||||
);
|
||||
|
||||
// If completed, must dispute within 48 hours
|
||||
if (bet.status == BetStatus.COMPLETED) {
|
||||
require(
|
||||
block.timestamp <= bet.settledAt + 48 hours,
|
||||
"Dispute window expired"
|
||||
);
|
||||
}
|
||||
|
||||
// Mark as disputed
|
||||
bet.status = BetStatus.DISPUTED;
|
||||
|
||||
emit BetDisputed(betId, msg.sender, block.timestamp);
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Cancel Bet (Before Matching)
|
||||
|
||||
```solidity
|
||||
function cancelBet(uint256 betId)
|
||||
external
|
||||
betExists(betId)
|
||||
{
|
||||
Bet storage bet = bets[betId];
|
||||
|
||||
// Validation
|
||||
require(msg.sender == bet.creator, "Only creator can cancel");
|
||||
require(bet.status == BetStatus.OPEN, "Can only cancel open bets");
|
||||
|
||||
// Mark as cancelled (no funds to refund since none were locked)
|
||||
bet.status = BetStatus.CANCELLED;
|
||||
|
||||
emit BetCancelled(betId, msg.sender);
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Admin Settlement (For Disputed Bets)
|
||||
|
||||
```solidity
|
||||
function adminSettleBet(uint256 betId, address winnerId)
|
||||
external
|
||||
betExists(betId)
|
||||
onlyOwner
|
||||
{
|
||||
Bet storage bet = bets[betId];
|
||||
|
||||
// Validation
|
||||
require(bet.status == BetStatus.DISPUTED, "Only for disputed bets");
|
||||
require(
|
||||
winnerId == bet.creator || winnerId == bet.opponent,
|
||||
"Winner must be a participant"
|
||||
);
|
||||
|
||||
// Calculate payout
|
||||
uint256 totalPayout = escrowBalance[betId];
|
||||
|
||||
// Transfer funds to winner
|
||||
require(
|
||||
payable(winnerId).transfer(totalPayout),
|
||||
"Payout transfer failed"
|
||||
);
|
||||
|
||||
// Update bet state
|
||||
bet.winner = winnerId;
|
||||
bet.status = BetStatus.COMPLETED;
|
||||
bet.settledAt = block.timestamp;
|
||||
|
||||
// Clear escrow
|
||||
escrowBalance[betId] = 0;
|
||||
|
||||
// Emit events
|
||||
emit EscrowReleased(betId, winnerId, totalPayout);
|
||||
emit BetSettled(betId, winnerId, totalPayout);
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Manual Settlement (Fallback if Oracle Fails)
|
||||
|
||||
```solidity
|
||||
function manualSettleAfterTimeout(uint256 betId, address winnerId)
|
||||
external
|
||||
betExists(betId)
|
||||
onlyParticipant(betId)
|
||||
{
|
||||
Bet storage bet = bets[betId];
|
||||
|
||||
// Validation
|
||||
require(
|
||||
bet.status == BetStatus.PENDING_ORACLE,
|
||||
"Must be waiting for oracle"
|
||||
);
|
||||
require(
|
||||
block.timestamp >= bet.eventTimestamp + 24 hours,
|
||||
"Oracle timeout not reached"
|
||||
);
|
||||
require(
|
||||
winnerId == bet.creator || winnerId == bet.opponent,
|
||||
"Winner must be a participant"
|
||||
);
|
||||
|
||||
// Settle manually (without oracle)
|
||||
// Note: Other participant can dispute within 48 hours
|
||||
|
||||
uint256 totalPayout = escrowBalance[betId];
|
||||
|
||||
require(
|
||||
payable(winnerId).transfer(totalPayout),
|
||||
"Payout transfer failed"
|
||||
);
|
||||
|
||||
bet.winner = winnerId;
|
||||
bet.status = BetStatus.COMPLETED;
|
||||
bet.settledAt = block.timestamp;
|
||||
|
||||
escrowBalance[betId] = 0;
|
||||
|
||||
emit EscrowReleased(betId, winnerId, totalPayout);
|
||||
emit BetSettled(betId, winnerId, totalPayout);
|
||||
}
|
||||
```
|
||||
|
||||
## View Functions
|
||||
|
||||
```solidity
|
||||
function getBet(uint256 betId)
|
||||
external
|
||||
view
|
||||
betExists(betId)
|
||||
returns (Bet memory)
|
||||
{
|
||||
return bets[betId];
|
||||
}
|
||||
|
||||
function getUserBets(address user)
|
||||
external
|
||||
view
|
||||
returns (uint256[] memory)
|
||||
{
|
||||
return userBetIds[user];
|
||||
}
|
||||
|
||||
function getUserEscrow(address user)
|
||||
external
|
||||
view
|
||||
returns (uint256 totalEscrow)
|
||||
{
|
||||
uint256[] memory betIds = userBetIds[user];
|
||||
|
||||
for (uint256 i = 0; i < betIds.length; i++) {
|
||||
Bet memory bet = bets[betIds[i]];
|
||||
|
||||
// Only count matched bets where user is a participant
|
||||
if (bet.status == BetStatus.MATCHED || bet.status == BetStatus.PENDING_ORACLE) {
|
||||
if (bet.creator == user || bet.opponent == user) {
|
||||
totalEscrow += bet.stakeAmount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return totalEscrow;
|
||||
}
|
||||
|
||||
function getBetsByStatus(BetStatus status)
|
||||
external
|
||||
view
|
||||
returns (uint256[] memory)
|
||||
{
|
||||
// Count matching bets
|
||||
uint256 count = 0;
|
||||
for (uint256 i = 0; i < nextBetId; i++) {
|
||||
if (bets[i].status == status) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Populate array
|
||||
uint256[] memory matchingBets = new uint256[](count);
|
||||
uint256 index = 0;
|
||||
for (uint256 i = 0; i < nextBetId; i++) {
|
||||
if (bets[i].status == status) {
|
||||
matchingBets[index] = i;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
return matchingBets;
|
||||
}
|
||||
```
|
||||
|
||||
## State Machine Diagram
|
||||
|
||||
```
|
||||
createBet()
|
||||
↓
|
||||
[OPEN]
|
||||
↓
|
||||
┌──────┴──────┐
|
||||
│ │
|
||||
cancelBet() acceptBet()
|
||||
│ │
|
||||
↓ ↓
|
||||
[CANCELLED] [MATCHED]
|
||||
↓
|
||||
requestSettlement()
|
||||
↓
|
||||
[PENDING_ORACLE]
|
||||
↓
|
||||
┌────────┴────────┐
|
||||
│ │
|
||||
settleBet() disputeBet()
|
||||
│ │
|
||||
↓ ↓
|
||||
[COMPLETED] [DISPUTED]
|
||||
│
|
||||
adminSettleBet()
|
||||
│
|
||||
↓
|
||||
[COMPLETED]
|
||||
```
|
||||
|
||||
## Security Features
|
||||
|
||||
1. **Reentrancy Protection**: Use checks-effects-interactions pattern
|
||||
2. **Atomic Escrow**: Both parties' funds locked in single transaction
|
||||
3. **Role-Based Access**: Only oracle can settle, only owner can admin-settle
|
||||
4. **Dispute Window**: 48-hour window to dispute settlement
|
||||
5. **Timeout Fallback**: Manual settlement if oracle fails after 24 hours
|
||||
6. **Event Logging**: All state changes emit events for transparency
|
||||
|
||||
## Gas Optimization Notes
|
||||
|
||||
- Use `storage` pointers instead of loading full structs
|
||||
- Pack struct fields to minimize storage slots
|
||||
- Batch operations where possible
|
||||
- Consider using `uint128` for stake amounts if max is known
|
||||
- Use events instead of storing historical data on-chain
|
||||
|
||||
## Integration with Oracle
|
||||
|
||||
The BetEscrow contract delegates settlement authority to the Oracle contract. The oracle:
|
||||
1. Listens for `requestSettlement()` calls
|
||||
2. Fetches external API data
|
||||
3. Determines winner via multi-node consensus
|
||||
4. Calls `settleBet()` with the winner address
|
||||
|
||||
## Deployment Configuration
|
||||
|
||||
```solidity
|
||||
constructor(address _oracleContract) {
|
||||
owner = msg.sender;
|
||||
oracleContract = _oracleContract;
|
||||
nextBetId = 0;
|
||||
}
|
||||
|
||||
function setOracleContract(address _newOracle) external onlyOwner {
|
||||
oracleContract = _newOracle;
|
||||
}
|
||||
```
|
||||
617
backend/app/blockchain/contracts/BetOracle.pseudocode.md
Normal file
617
backend/app/blockchain/contracts/BetOracle.pseudocode.md
Normal file
@ -0,0 +1,617 @@
|
||||
# BetOracle Smart Contract (Pseudocode)
|
||||
|
||||
## Overview
|
||||
|
||||
The BetOracle contract acts as a bridge between the blockchain and external data sources. It implements a decentralized oracle network where multiple independent nodes fetch data from APIs, reach consensus, and automatically settle bets based on real-world event outcomes.
|
||||
|
||||
## State Variables
|
||||
|
||||
```solidity
|
||||
// Contract references
|
||||
address public betEscrowContract;
|
||||
address public owner;
|
||||
|
||||
// Oracle network
|
||||
address[] public trustedNodes;
|
||||
mapping(address => bool) public isNodeTrusted;
|
||||
uint256 public consensusThreshold; // e.g., 3 of 5 nodes must agree
|
||||
|
||||
// Oracle requests
|
||||
mapping(uint256 => OracleRequest) public requests;
|
||||
uint256 public nextRequestId;
|
||||
|
||||
// Node submissions
|
||||
mapping(uint256 => NodeSubmission[]) public submissions; // requestId => submissions
|
||||
mapping(uint256 => mapping(address => bool)) public hasSubmitted; // requestId => node => submitted
|
||||
```
|
||||
|
||||
## Data Structures
|
||||
|
||||
```solidity
|
||||
enum RequestStatus {
|
||||
PENDING, // Request created, waiting for nodes
|
||||
FULFILLED, // Consensus reached, bet settled
|
||||
DISPUTED, // No consensus or disputed result
|
||||
TIMED_OUT // Oracle failed to respond in time
|
||||
}
|
||||
|
||||
struct OracleRequest {
|
||||
uint256 requestId;
|
||||
uint256 betId;
|
||||
bytes32 eventId; // External event identifier
|
||||
string apiEndpoint; // URL to fetch result from
|
||||
uint256 requestedAt; // Block timestamp
|
||||
RequestStatus status;
|
||||
address consensusWinner; // Agreed-upon winner
|
||||
uint256 fulfilledAt; // Settlement timestamp
|
||||
}
|
||||
|
||||
struct NodeSubmission {
|
||||
address nodeAddress;
|
||||
address proposedWinner; // Who the node thinks won
|
||||
bytes resultData; // Raw API response data
|
||||
bytes signature; // Node's signature of the result
|
||||
uint256 submittedAt; // Block timestamp
|
||||
}
|
||||
|
||||
struct ApiAdapter {
|
||||
string apiEndpoint;
|
||||
string resultPath; // JSON path to extract winner (e.g., "data.winner")
|
||||
mapping(string => address) resultMapping; // Map API result to bet participant
|
||||
}
|
||||
```
|
||||
|
||||
## Events
|
||||
|
||||
```solidity
|
||||
event OracleRequested(
|
||||
uint256 indexed requestId,
|
||||
uint256 indexed betId,
|
||||
bytes32 eventId,
|
||||
string apiEndpoint
|
||||
);
|
||||
|
||||
event NodeSubmissionReceived(
|
||||
uint256 indexed requestId,
|
||||
address indexed node,
|
||||
address proposedWinner
|
||||
);
|
||||
|
||||
event ConsensusReached(
|
||||
uint256 indexed requestId,
|
||||
address indexed winner,
|
||||
uint256 nodeCount
|
||||
);
|
||||
|
||||
event OracleFulfilled(
|
||||
uint256 indexed requestId,
|
||||
uint256 indexed betId,
|
||||
address winner
|
||||
);
|
||||
|
||||
event OracleDisputed(
|
||||
uint256 indexed requestId,
|
||||
string reason
|
||||
);
|
||||
|
||||
event TrustedNodeAdded(address indexed node);
|
||||
event TrustedNodeRemoved(address indexed node);
|
||||
```
|
||||
|
||||
## Modifiers
|
||||
|
||||
```solidity
|
||||
modifier onlyBetEscrow() {
|
||||
require(msg.sender == betEscrowContract, "Only BetEscrow can call");
|
||||
_;
|
||||
}
|
||||
|
||||
modifier onlyTrustedNode() {
|
||||
require(isNodeTrusted[msg.sender], "Not a trusted oracle node");
|
||||
_;
|
||||
}
|
||||
|
||||
modifier onlyOwner() {
|
||||
require(msg.sender == owner, "Only owner can call");
|
||||
_;
|
||||
}
|
||||
|
||||
modifier requestExists(uint256 requestId) {
|
||||
require(requestId < nextRequestId, "Request does not exist");
|
||||
_;
|
||||
}
|
||||
```
|
||||
|
||||
## Core Functions
|
||||
|
||||
### 1. Request Settlement (Called by BetEscrow)
|
||||
|
||||
```solidity
|
||||
function requestSettlement(
|
||||
uint256 betId,
|
||||
bytes32 eventId
|
||||
) external onlyBetEscrow returns (uint256) {
|
||||
// Fetch bet details from BetEscrow contract
|
||||
IBetEscrow.Bet memory bet = IBetEscrow(betEscrowContract).getBet(betId);
|
||||
|
||||
// Determine API endpoint based on event type
|
||||
string memory apiEndpoint = getApiEndpointForEvent(eventId);
|
||||
|
||||
// Create oracle request
|
||||
uint256 requestId = nextRequestId++;
|
||||
|
||||
requests[requestId] = OracleRequest({
|
||||
requestId: requestId,
|
||||
betId: betId,
|
||||
eventId: eventId,
|
||||
apiEndpoint: apiEndpoint,
|
||||
requestedAt: block.timestamp,
|
||||
status: RequestStatus.PENDING,
|
||||
consensusWinner: address(0),
|
||||
fulfilledAt: 0
|
||||
});
|
||||
|
||||
// Emit event for off-chain oracle nodes to listen
|
||||
emit OracleRequested(requestId, betId, eventId, apiEndpoint);
|
||||
|
||||
return requestId;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Submit Oracle Response (Called by Trusted Nodes)
|
||||
|
||||
```solidity
|
||||
function submitOracleResponse(
|
||||
uint256 requestId,
|
||||
address proposedWinner,
|
||||
bytes calldata resultData,
|
||||
bytes calldata signature
|
||||
)
|
||||
external
|
||||
onlyTrustedNode
|
||||
requestExists(requestId)
|
||||
{
|
||||
OracleRequest storage request = requests[requestId];
|
||||
|
||||
// Validation
|
||||
require(request.status == RequestStatus.PENDING, "Request not pending");
|
||||
require(!hasSubmitted[requestId][msg.sender], "Already submitted");
|
||||
|
||||
// Verify signature
|
||||
require(
|
||||
verifyNodeSignature(requestId, proposedWinner, resultData, signature, msg.sender),
|
||||
"Invalid signature"
|
||||
);
|
||||
|
||||
// Store submission
|
||||
submissions[requestId].push(NodeSubmission({
|
||||
nodeAddress: msg.sender,
|
||||
proposedWinner: proposedWinner,
|
||||
resultData: resultData,
|
||||
signature: signature,
|
||||
submittedAt: block.timestamp
|
||||
}));
|
||||
|
||||
hasSubmitted[requestId][msg.sender] = true;
|
||||
|
||||
emit NodeSubmissionReceived(requestId, msg.sender, proposedWinner);
|
||||
|
||||
// Check if consensus threshold reached
|
||||
if (submissions[requestId].length >= consensusThreshold) {
|
||||
_checkConsensusAndSettle(requestId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Check Consensus and Settle
|
||||
|
||||
```solidity
|
||||
function _checkConsensusAndSettle(uint256 requestId) internal {
|
||||
OracleRequest storage request = requests[requestId];
|
||||
NodeSubmission[] storage subs = submissions[requestId];
|
||||
|
||||
// Count votes for each proposed winner
|
||||
mapping(address => uint256) memory voteCounts;
|
||||
address[] memory candidates = new address[](subs.length);
|
||||
uint256 candidateCount = 0;
|
||||
|
||||
for (uint256 i = 0; i < subs.length; i++) {
|
||||
address candidate = subs[i].proposedWinner;
|
||||
|
||||
// Add to candidates if not already present
|
||||
bool exists = false;
|
||||
for (uint256 j = 0; j < candidateCount; j++) {
|
||||
if (candidates[j] == candidate) {
|
||||
exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!exists) {
|
||||
candidates[candidateCount] = candidate;
|
||||
candidateCount++;
|
||||
}
|
||||
|
||||
voteCounts[candidate]++;
|
||||
}
|
||||
|
||||
// Find winner with most votes
|
||||
address consensusWinner = address(0);
|
||||
uint256 maxVotes = 0;
|
||||
|
||||
for (uint256 i = 0; i < candidateCount; i++) {
|
||||
if (voteCounts[candidates[i]] > maxVotes) {
|
||||
maxVotes = voteCounts[candidates[i]];
|
||||
consensusWinner = candidates[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Verify consensus threshold
|
||||
if (maxVotes >= consensusThreshold) {
|
||||
// Consensus reached - settle the bet
|
||||
request.consensusWinner = consensusWinner;
|
||||
request.status = RequestStatus.FULFILLED;
|
||||
request.fulfilledAt = block.timestamp;
|
||||
|
||||
// Call BetEscrow to settle
|
||||
IBetEscrow(betEscrowContract).settleBet(request.betId, consensusWinner);
|
||||
|
||||
emit ConsensusReached(requestId, consensusWinner, maxVotes);
|
||||
emit OracleFulfilled(requestId, request.betId, consensusWinner);
|
||||
} else {
|
||||
// No consensus - mark as disputed
|
||||
request.status = RequestStatus.DISPUTED;
|
||||
|
||||
emit OracleDisputed(requestId, "No consensus reached");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Verify Node Signature
|
||||
|
||||
```solidity
|
||||
function verifyNodeSignature(
|
||||
uint256 requestId,
|
||||
address proposedWinner,
|
||||
bytes memory resultData,
|
||||
bytes memory signature,
|
||||
address nodeAddress
|
||||
) internal pure returns (bool) {
|
||||
// Reconstruct the message hash
|
||||
bytes32 messageHash = keccak256(
|
||||
abi.encodePacked(requestId, proposedWinner, resultData)
|
||||
);
|
||||
|
||||
// Add Ethereum signed message prefix
|
||||
bytes32 ethSignedMessageHash = keccak256(
|
||||
abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)
|
||||
);
|
||||
|
||||
// Recover signer from signature
|
||||
address recoveredSigner = recoverSigner(ethSignedMessageHash, signature);
|
||||
|
||||
// Verify signer matches node address
|
||||
return recoveredSigner == nodeAddress;
|
||||
}
|
||||
|
||||
function recoverSigner(bytes32 ethSignedMessageHash, bytes memory signature)
|
||||
internal
|
||||
pure
|
||||
returns (address)
|
||||
{
|
||||
(bytes32 r, bytes32 s, uint8 v) = splitSignature(signature);
|
||||
return ecrecover(ethSignedMessageHash, v, r, s);
|
||||
}
|
||||
|
||||
function splitSignature(bytes memory sig)
|
||||
internal
|
||||
pure
|
||||
returns (bytes32 r, bytes32 s, uint8 v)
|
||||
{
|
||||
require(sig.length == 65, "Invalid signature length");
|
||||
|
||||
assembly {
|
||||
r := mload(add(sig, 32))
|
||||
s := mload(add(sig, 64))
|
||||
v := byte(0, mload(add(sig, 96)))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Manual Dispute Resolution
|
||||
|
||||
```solidity
|
||||
function resolveDispute(uint256 requestId, address winner)
|
||||
external
|
||||
onlyOwner
|
||||
requestExists(requestId)
|
||||
{
|
||||
OracleRequest storage request = requests[requestId];
|
||||
|
||||
require(
|
||||
request.status == RequestStatus.DISPUTED,
|
||||
"Only for disputed requests"
|
||||
);
|
||||
|
||||
// Admin manually resolves dispute
|
||||
request.consensusWinner = winner;
|
||||
request.status = RequestStatus.FULFILLED;
|
||||
request.fulfilledAt = block.timestamp;
|
||||
|
||||
// Settle the bet via BetEscrow admin function
|
||||
IBetEscrow(betEscrowContract).adminSettleBet(request.betId, winner);
|
||||
|
||||
emit OracleFulfilled(requestId, request.betId, winner);
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Handle Timeout
|
||||
|
||||
```solidity
|
||||
function markAsTimedOut(uint256 requestId)
|
||||
external
|
||||
requestExists(requestId)
|
||||
{
|
||||
OracleRequest storage request = requests[requestId];
|
||||
|
||||
require(request.status == RequestStatus.PENDING, "Not pending");
|
||||
require(
|
||||
block.timestamp >= request.requestedAt + 24 hours,
|
||||
"Timeout period not reached"
|
||||
);
|
||||
|
||||
request.status = RequestStatus.TIMED_OUT;
|
||||
|
||||
emit OracleDisputed(requestId, "Oracle timed out");
|
||||
|
||||
// Note: BetEscrow contract allows manual settlement after timeout
|
||||
}
|
||||
```
|
||||
|
||||
## Oracle Network Management
|
||||
|
||||
### Add Trusted Node
|
||||
|
||||
```solidity
|
||||
function addTrustedNode(address node) external onlyOwner {
|
||||
require(!isNodeTrusted[node], "Node already trusted");
|
||||
|
||||
trustedNodes.push(node);
|
||||
isNodeTrusted[node] = true;
|
||||
|
||||
emit TrustedNodeAdded(node);
|
||||
}
|
||||
```
|
||||
|
||||
### Remove Trusted Node
|
||||
|
||||
```solidity
|
||||
function removeTrustedNode(address node) external onlyOwner {
|
||||
require(isNodeTrusted[node], "Node not trusted");
|
||||
|
||||
isNodeTrusted[node] = false;
|
||||
|
||||
// Remove from array
|
||||
for (uint256 i = 0; i < trustedNodes.length; i++) {
|
||||
if (trustedNodes[i] == node) {
|
||||
trustedNodes[i] = trustedNodes[trustedNodes.length - 1];
|
||||
trustedNodes.pop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
emit TrustedNodeRemoved(node);
|
||||
}
|
||||
```
|
||||
|
||||
### Update Consensus Threshold
|
||||
|
||||
```solidity
|
||||
function setConsensusThreshold(uint256 newThreshold) external onlyOwner {
|
||||
require(newThreshold > 0, "Threshold must be positive");
|
||||
require(newThreshold <= trustedNodes.length, "Threshold too high");
|
||||
|
||||
consensusThreshold = newThreshold;
|
||||
}
|
||||
```
|
||||
|
||||
## API Adapter Functions
|
||||
|
||||
### Get API Endpoint for Event
|
||||
|
||||
```solidity
|
||||
mapping(bytes32 => ApiAdapter) public apiAdapters;
|
||||
|
||||
function getApiEndpointForEvent(bytes32 eventId)
|
||||
internal
|
||||
view
|
||||
returns (string memory)
|
||||
{
|
||||
ApiAdapter storage adapter = apiAdapters[eventId];
|
||||
require(bytes(adapter.apiEndpoint).length > 0, "No adapter for event");
|
||||
|
||||
return adapter.apiEndpoint;
|
||||
}
|
||||
```
|
||||
|
||||
### Register API Adapter
|
||||
|
||||
```solidity
|
||||
function registerApiAdapter(
|
||||
bytes32 eventId,
|
||||
string memory apiEndpoint,
|
||||
string memory resultPath
|
||||
) external onlyOwner {
|
||||
apiAdapters[eventId] = ApiAdapter({
|
||||
apiEndpoint: apiEndpoint,
|
||||
resultPath: resultPath
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## View Functions
|
||||
|
||||
```solidity
|
||||
function getRequest(uint256 requestId)
|
||||
external
|
||||
view
|
||||
requestExists(requestId)
|
||||
returns (OracleRequest memory)
|
||||
{
|
||||
return requests[requestId];
|
||||
}
|
||||
|
||||
function getSubmissions(uint256 requestId)
|
||||
external
|
||||
view
|
||||
requestExists(requestId)
|
||||
returns (NodeSubmission[] memory)
|
||||
{
|
||||
return submissions[requestId];
|
||||
}
|
||||
|
||||
function getTrustedNodes() external view returns (address[] memory) {
|
||||
return trustedNodes;
|
||||
}
|
||||
|
||||
function getVoteCounts(uint256 requestId)
|
||||
external
|
||||
view
|
||||
requestExists(requestId)
|
||||
returns (address[] memory candidates, uint256[] memory votes)
|
||||
{
|
||||
NodeSubmission[] storage subs = submissions[requestId];
|
||||
|
||||
// Count unique candidates
|
||||
address[] memory tempCandidates = new address[](subs.length);
|
||||
uint256[] memory tempVotes = new uint256[](subs.length);
|
||||
uint256 candidateCount = 0;
|
||||
|
||||
for (uint256 i = 0; i < subs.length; i++) {
|
||||
address candidate = subs[i].proposedWinner;
|
||||
|
||||
// Find or create candidate
|
||||
bool found = false;
|
||||
for (uint256 j = 0; j < candidateCount; j++) {
|
||||
if (tempCandidates[j] == candidate) {
|
||||
tempVotes[j]++;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
tempCandidates[candidateCount] = candidate;
|
||||
tempVotes[candidateCount] = 1;
|
||||
candidateCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Trim arrays to actual size
|
||||
candidates = new address[](candidateCount);
|
||||
votes = new uint256[](candidateCount);
|
||||
|
||||
for (uint256 i = 0; i < candidateCount; i++) {
|
||||
candidates[i] = tempCandidates[i];
|
||||
votes[i] = tempVotes[i];
|
||||
}
|
||||
|
||||
return (candidates, votes);
|
||||
}
|
||||
```
|
||||
|
||||
## Oracle Flow Diagram
|
||||
|
||||
```
|
||||
BetEscrow.requestSettlement()
|
||||
↓
|
||||
OracleRequested event emitted
|
||||
↓
|
||||
┌──────────┴──────────┐
|
||||
│ │
|
||||
Node 1 Node 2 Node 3 ... Node N
|
||||
│ │ │
|
||||
Fetch API Fetch API Fetch API
|
||||
│ │ │
|
||||
Sign Result Sign Result Sign Result
|
||||
│ │ │
|
||||
└──────────┬──────────┘ │
|
||||
↓ │
|
||||
submitOracleResponse() ←────────────────────┘
|
||||
↓
|
||||
Check submissions count
|
||||
↓
|
||||
┌──────────┴──────────┐
|
||||
│ │
|
||||
Count < threshold Count >= threshold
|
||||
│ │
|
||||
Wait for more ↓
|
||||
submissions _checkConsensusAndSettle()
|
||||
↓
|
||||
Count votes per winner
|
||||
↓
|
||||
┌─────────┴─────────┐
|
||||
│ │
|
||||
Consensus No consensus
|
||||
│ │
|
||||
↓ ↓
|
||||
BetEscrow.settleBet() DISPUTED status
|
||||
↓
|
||||
Funds distributed
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Sybil Resistance**: Only trusted nodes can submit results
|
||||
2. **Signature Verification**: All node submissions must be cryptographically signed
|
||||
3. **Consensus Mechanism**: Require majority agreement (e.g., 3 of 5 nodes)
|
||||
4. **Replay Protection**: Track submissions per request to prevent double-voting
|
||||
5. **Timeout Handling**: Allow manual settlement if oracle network fails
|
||||
6. **Admin Override**: Owner can resolve disputes for edge cases
|
||||
|
||||
## Example API Adapters
|
||||
|
||||
### Sports (ESPN API)
|
||||
|
||||
```solidity
|
||||
Event ID: keccak256("nfl-super-bowl-2024")
|
||||
API Endpoint: "https://api.espn.com/v1/sports/football/nfl/scoreboard?event=12345"
|
||||
Result Path: "events[0].competitions[0].winner.team.name"
|
||||
Result Mapping: {
|
||||
"San Francisco 49ers": creatorAddress,
|
||||
"Kansas City Chiefs": opponentAddress
|
||||
}
|
||||
```
|
||||
|
||||
### Entertainment (Awards API)
|
||||
|
||||
```solidity
|
||||
Event ID: keccak256("oscars-2024-best-picture")
|
||||
API Endpoint: "https://api.oscars.com/winners/2024"
|
||||
Result Path: "categories.best_picture.winner"
|
||||
Result Mapping: {
|
||||
"Oppenheimer": creatorAddress,
|
||||
"Other": opponentAddress
|
||||
}
|
||||
```
|
||||
|
||||
## Deployment Configuration
|
||||
|
||||
```solidity
|
||||
constructor(address _betEscrowContract, uint256 _consensusThreshold) {
|
||||
owner = msg.sender;
|
||||
betEscrowContract = _betEscrowContract;
|
||||
consensusThreshold = _consensusThreshold;
|
||||
}
|
||||
|
||||
function setBetEscrowContract(address _newContract) external onlyOwner {
|
||||
betEscrowContract = _newContract;
|
||||
}
|
||||
```
|
||||
|
||||
## Gas Optimization Notes
|
||||
|
||||
- Minimize storage writes in submission loop
|
||||
- Use events for off-chain data indexing instead of storing all submissions
|
||||
- Consider using Chainlink oracle infrastructure for production
|
||||
- Batch consensus checks instead of checking on every submission
|
||||
455
backend/app/blockchain/contracts/README.md
Normal file
455
backend/app/blockchain/contracts/README.md
Normal file
@ -0,0 +1,455 @@
|
||||
# H2H Blockchain Smart Contract Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
This directory contains pseudocode specifications for the H2H betting platform's smart contract layer. The contracts implement a **hybrid blockchain architecture** where critical escrow and settlement operations occur on-chain for trustlessness, while user experience and auxiliary features remain off-chain for performance.
|
||||
|
||||
## Architecture Goals
|
||||
|
||||
1. **Trustless Escrow**: Funds are held by smart contracts, not a centralized authority
|
||||
2. **Automatic Settlement**: Oracle network fetches real-world event results and settles bets
|
||||
3. **Transparency**: All bet states and fund movements are publicly verifiable on-chain
|
||||
4. **Decentralization**: Multiple independent oracle nodes prevent single point of failure
|
||||
5. **Dispute Resolution**: Multi-layered fallback for edge cases and disagreements
|
||||
|
||||
## Contract Components
|
||||
|
||||
### 1. BetEscrow Contract
|
||||
|
||||
**Purpose**: Manages the complete lifecycle of bets including creation, matching, escrow, and settlement.
|
||||
|
||||
**Key Features**:
|
||||
- Bet creation with configurable odds and stake amounts
|
||||
- Atomic escrow locking for both parties upon bet acceptance
|
||||
- Oracle-delegated settlement
|
||||
- Manual settlement fallback if oracle fails
|
||||
- Dispute mechanism with 48-hour window
|
||||
- Admin override for disputed bets
|
||||
|
||||
**State Machine**:
|
||||
```
|
||||
OPEN → MATCHED → PENDING_ORACLE → COMPLETED
|
||||
↓ ↑
|
||||
CANCELLED DISPUTED
|
||||
```
|
||||
|
||||
[See BetEscrow.pseudocode.md for full implementation](./BetEscrow.pseudocode.md)
|
||||
|
||||
### 2. BetOracle Contract
|
||||
|
||||
**Purpose**: Bridges blockchain and external data sources through a decentralized oracle network.
|
||||
|
||||
**Key Features**:
|
||||
- Multi-node consensus mechanism (e.g., 3 of 5 nodes must agree)
|
||||
- Cryptographic signature verification for all submissions
|
||||
- Support for multiple API adapters (sports, entertainment, politics, etc.)
|
||||
- Automatic settlement upon consensus
|
||||
- Timeout handling with manual fallback
|
||||
|
||||
**Oracle Flow**:
|
||||
```
|
||||
Request → Nodes Fetch Data → Submit Results → Verify Consensus → Settle Bet
|
||||
```
|
||||
|
||||
[See BetOracle.pseudocode.md for full implementation](./BetOracle.pseudocode.md)
|
||||
|
||||
## System Architecture Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Frontend (React + Web3) │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌────────────────────────┐ │
|
||||
│ │ MetaMask │ │ Transaction │ │ Blockchain Status │ │
|
||||
│ │ Connection │ │ Signing │ │ Badges & Gas Estimates │ │
|
||||
│ └──────────────┘ └──────────────┘ └────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
↓ ↑
|
||||
Web3 JSON-RPC
|
||||
↓ ↑
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Blockchain Layer (Smart Contracts) │
|
||||
│ │
|
||||
│ ┌────────────────────────┐ ┌─────────────────────────┐ │
|
||||
│ │ BetEscrow Contract │ ←────→ │ BetOracle Contract │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ • createBet() │ │ • requestSettlement() │ │
|
||||
│ │ • acceptBet() │ │ • submitResponse() │ │
|
||||
│ │ • settleBet() │ │ • checkConsensus() │ │
|
||||
│ │ • disputeBet() │ │ • verifySignatures() │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ Escrow: ETH/tokens │ │ Trusted Nodes: 5 │ │
|
||||
│ └────────────────────────┘ └─────────────────────────┘ │
|
||||
│ ↓ Events ↑ API Calls │
|
||||
└──────────────┼──────────────────────────────┼──────────────────────┘
|
||||
│ │
|
||||
│ (BetCreated, BetMatched, │ (Oracle Requests)
|
||||
│ BetSettled events) │
|
||||
↓ │
|
||||
┌─────────────────────────────────────────────┼──────────────────────┐
|
||||
│ Backend (FastAPI) │ │
|
||||
│ │ │
|
||||
│ ┌────────────────────┐ ┌───────────────┴────────┐ │
|
||||
│ │ Event Indexer │ │ Oracle Aggregator │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ • Poll blocks │ │ • Collect node votes │ │
|
||||
│ │ • Index events │ │ • Verify consensus │ │
|
||||
│ │ • Sync to DB │ │ • Submit to chain │ │
|
||||
│ └────────────────────┘ └────────────────────────┘ │
|
||||
│ ↓ ↑ │
|
||||
│ PostgreSQL Oracle Nodes (3-5) │
|
||||
│ ┌────────────────────┐ ┌────────────────────┐ │
|
||||
│ │ Cached Bet Data │ │ • Fetch ESPN API │ │
|
||||
│ │ (for fast queries)│ │ • Sign results │ │
|
||||
│ └────────────────────┘ │ • Submit to chain │ │
|
||||
│ └────────────────────┘ │
|
||||
│ ↑ │
|
||||
└──────────────────────────────────────┼──────────────────────────────┘
|
||||
│
|
||||
External APIs
|
||||
┌─────────────────┴────────────────┐
|
||||
│ │
|
||||
┌──────┴──────┐ ┌─────────┴────────┐
|
||||
│ ESPN API │ │ Odds API │
|
||||
│ (Sports) │ │ (Politics, etc.)│
|
||||
└─────────────┘ └──────────────────┘
|
||||
```
|
||||
|
||||
## Complete Bet Lifecycle
|
||||
|
||||
### 1. Bet Creation
|
||||
|
||||
```
|
||||
User (Alice)
|
||||
↓
|
||||
Frontend: Fill out CreateBetModal
|
||||
↓
|
||||
Web3: Sign transaction
|
||||
↓
|
||||
BetEscrow.createBet(stake=100, odds=1.5, eventId="super-bowl-2024")
|
||||
↓
|
||||
Emit BetCreated event
|
||||
↓
|
||||
Backend Indexer: Sync to PostgreSQL
|
||||
↓
|
||||
Marketplace: Bet appears as "OPEN"
|
||||
```
|
||||
|
||||
**On-Chain State**:
|
||||
- Bet ID: 42
|
||||
- Status: OPEN
|
||||
- Creator: Alice's address
|
||||
- Opponent: null
|
||||
- Escrow: 0 ETH (no funds locked yet)
|
||||
|
||||
**Off-Chain State (DB)**:
|
||||
- Title: "Super Bowl LVIII Winner"
|
||||
- Description: "49ers vs Chiefs..."
|
||||
- blockchain_bet_id: 42
|
||||
- blockchain_tx_hash: "0xabc123..."
|
||||
|
||||
### 2. Bet Acceptance
|
||||
|
||||
```
|
||||
User (Bob)
|
||||
↓
|
||||
Frontend: Click "Accept Bet" on BetDetails page
|
||||
↓
|
||||
Display GasFeeEstimate component
|
||||
↓
|
||||
User confirms
|
||||
↓
|
||||
Web3: Sign transaction with stake amount (100 ETH)
|
||||
↓
|
||||
BetEscrow.acceptBet(betId=42) payable
|
||||
↓
|
||||
Contract transfers Alice's 100 ETH from her approved balance
|
||||
Contract receives Bob's 100 ETH from msg.value
|
||||
↓
|
||||
Escrow locked: 200 ETH total
|
||||
↓
|
||||
Emit BetMatched event
|
||||
↓
|
||||
Frontend: Show TransactionModal ("Confirming...")
|
||||
↓
|
||||
Wait for block confirmation (10-30 seconds)
|
||||
↓
|
||||
TransactionModal: "Success!" with Etherscan link
|
||||
↓
|
||||
Backend Indexer: Update DB status to MATCHED
|
||||
↓
|
||||
Frontend: Refresh bet details, show "Matched" status
|
||||
```
|
||||
|
||||
**On-Chain State**:
|
||||
- Bet ID: 42
|
||||
- Status: MATCHED
|
||||
- Creator: Alice's address
|
||||
- Opponent: Bob's address
|
||||
- Escrow: 200 ETH
|
||||
|
||||
**Off-Chain State (DB)**:
|
||||
- status: matched
|
||||
- opponent_id: Bob's user ID
|
||||
- blockchain_status: "MATCHED"
|
||||
|
||||
### 3. Oracle Settlement (Automatic)
|
||||
|
||||
```
|
||||
Event occurs in real world (Super Bowl game ends)
|
||||
↓
|
||||
BetEscrow.requestSettlement(betId=42)
|
||||
↓
|
||||
BetOracle.requestSettlement(betId=42, eventId="super-bowl-2024")
|
||||
↓
|
||||
Emit OracleRequested event
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ Oracle Nodes (listening for events) │
|
||||
└─────────────────────────────────────┘
|
||||
↓ ↓ ↓
|
||||
Node 1 Node 2 Node 3
|
||||
│ │ │
|
||||
Fetch ESPN Fetch ESPN Fetch ESPN
|
||||
API result API result API result
|
||||
│ │ │
|
||||
"49ers won" "49ers won" "49ers won"
|
||||
│ │ │
|
||||
Map to Alice Map to Alice Map to Alice
|
||||
│ │ │
|
||||
Sign result Sign result Sign result
|
||||
↓ ↓ ↓
|
||||
BetOracle.submitOracleResponse(requestId, winner=Alice, signature)
|
||||
↓
|
||||
Check submissions count (3/3)
|
||||
↓
|
||||
Check consensus (3 votes for Alice)
|
||||
↓
|
||||
Consensus reached!
|
||||
↓
|
||||
BetEscrow.settleBet(betId=42, winner=Alice)
|
||||
↓
|
||||
Transfer 200 ETH to Alice
|
||||
↓
|
||||
Emit BetSettled event
|
||||
↓
|
||||
Backend Indexer: Update DB
|
||||
↓
|
||||
Frontend: Show "Completed" status
|
||||
↓
|
||||
Alice's wallet balance: +200 ETH
|
||||
```
|
||||
|
||||
**On-Chain State**:
|
||||
- Bet ID: 42
|
||||
- Status: COMPLETED
|
||||
- Winner: Alice's address
|
||||
- Escrow: 0 ETH (paid out)
|
||||
|
||||
**Off-Chain State (DB)**:
|
||||
- status: completed
|
||||
- winner_id: Alice's user ID
|
||||
- settled_at: timestamp
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Frontend → Blockchain
|
||||
|
||||
**Files**: `frontend/src/blockchain/hooks/useBlockchainBet.ts`
|
||||
|
||||
```typescript
|
||||
// Example: Accepting a bet
|
||||
const { acceptBet } = useBlockchainBet()
|
||||
|
||||
await acceptBet(betId=42, stakeAmount=100)
|
||||
↓
|
||||
MetaMask popup for user signature
|
||||
↓
|
||||
Transaction submitted to blockchain
|
||||
↓
|
||||
Wait for confirmation
|
||||
↓
|
||||
Update UI with new bet status
|
||||
```
|
||||
|
||||
### Backend → Blockchain
|
||||
|
||||
**Files**: `backend/app/blockchain/services/blockchain_service.py`
|
||||
|
||||
```python
|
||||
# Example: Creating bet on-chain
|
||||
blockchain_service.create_bet_on_chain(
|
||||
stake_amount=100,
|
||||
event_id="super-bowl-2024"
|
||||
)
|
||||
↓
|
||||
Build transaction with Web3.py
|
||||
↓
|
||||
Sign with backend hot wallet
|
||||
↓
|
||||
Submit to RPC endpoint
|
||||
↓
|
||||
Return transaction hash
|
||||
```
|
||||
|
||||
### Blockchain → Backend (Event Indexing)
|
||||
|
||||
**Files**: `backend/app/blockchain/services/blockchain_indexer.py`
|
||||
|
||||
```python
|
||||
# Continuous background process
|
||||
while True:
|
||||
latest_block = get_latest_block()
|
||||
events = get_events_in_block(latest_block)
|
||||
|
||||
for event in events:
|
||||
if event.type == "BetCreated":
|
||||
sync_bet_created_to_db(event)
|
||||
elif event.type == "BetMatched":
|
||||
sync_bet_matched_to_db(event)
|
||||
elif event.type == "BetSettled":
|
||||
sync_bet_settled_to_db(event)
|
||||
|
||||
sleep(10 seconds)
|
||||
```
|
||||
|
||||
## Data Flow: On-Chain vs Off-Chain
|
||||
|
||||
| Data | Storage Location | Reason |
|
||||
|------|------------------|--------|
|
||||
| Bet escrow funds | ⛓️ On-Chain | Trustless, no centralized custody |
|
||||
| Bet status (OPEN/MATCHED/COMPLETED) | ⛓️ On-Chain + 💾 DB Cache | Source of truth on-chain, cached for speed |
|
||||
| Bet participants (creator/opponent) | ⛓️ On-Chain + 💾 DB | Enforced by smart contract |
|
||||
| Settlement winner | ⛓️ On-Chain | Oracle consensus, immutable |
|
||||
| Bet title/description | 💾 DB only | Not needed for contract logic |
|
||||
| User email/password | 💾 DB only | Authentication separate from blockchain |
|
||||
| Transaction history | ⛓️ On-Chain (events) + 💾 DB | Events logged on-chain, indexed to DB |
|
||||
| Search/filters | 💾 DB only | Too expensive to query blockchain |
|
||||
|
||||
## Security Model
|
||||
|
||||
### On-Chain Security
|
||||
|
||||
1. **Escrow Protection**: Funds locked in smart contract, not controlled by any individual
|
||||
2. **Atomic Operations**: Both parties' funds locked in single transaction (no partial states)
|
||||
3. **Role-Based Access**: Only oracle can settle, only owner can admin-override
|
||||
4. **Reentrancy Guards**: Prevent malicious contracts from draining escrow
|
||||
5. **Signature Verification**: All oracle submissions cryptographically signed
|
||||
|
||||
### Oracle Security
|
||||
|
||||
1. **Multi-Node Consensus**: Require 3 of 5 nodes to agree (prevents single node manipulation)
|
||||
2. **Signature Verification**: Each node signs results with private key
|
||||
3. **Timeout Fallback**: If oracle fails, users can manually settle after 24 hours
|
||||
4. **Dispute Window**: 48 hours to dispute automatic settlement
|
||||
5. **Admin Override**: Final fallback for edge cases
|
||||
|
||||
### Threat Model
|
||||
|
||||
| Threat | Mitigation |
|
||||
|--------|-----------|
|
||||
| Malicious oracle node | Multi-node consensus (3/5 threshold) |
|
||||
| All oracle nodes fail | 24-hour timeout → manual settlement allowed |
|
||||
| Disputed result | 48-hour dispute window → admin resolution |
|
||||
| Smart contract bug | Audited code, time-lock for upgrades |
|
||||
| User loses private key | Non-custodial, user responsible (standard Web3) |
|
||||
| Front-running | Commit-reveal scheme (future enhancement) |
|
||||
|
||||
## Gas Costs & Optimization
|
||||
|
||||
### Estimated Gas Usage (Ethereum Mainnet)
|
||||
|
||||
| Operation | Gas Limit | Cost @ 50 gwei | Cost @ 100 gwei |
|
||||
|-----------|-----------|----------------|-----------------|
|
||||
| Create Bet | 120,000 | 0.006 ETH ($12) | 0.012 ETH ($24) |
|
||||
| Accept Bet | 180,000 | 0.009 ETH ($18) | 0.018 ETH ($36) |
|
||||
| Settle Bet (Oracle) | 150,000 | 0.0075 ETH ($15) | 0.015 ETH ($30) |
|
||||
| Submit Oracle Response | 80,000 | 0.004 ETH ($8) | 0.008 ETH ($16) |
|
||||
|
||||
### Layer 2 Optimization
|
||||
|
||||
**Recommendation**: Deploy on Polygon or Arbitrum for:
|
||||
- **90-95% lower gas costs**: Accept bet costs ~$2 instead of $36
|
||||
- **Faster confirmations**: 2 seconds vs 12-15 seconds
|
||||
- **Same security**: Inherits Ethereum security via rollups
|
||||
- **No code changes**: Same Solidity contracts work on L2
|
||||
|
||||
**Example L2 Costs (Polygon)**:
|
||||
- Create Bet: ~$0.50
|
||||
- Accept Bet: ~$1.00
|
||||
- Settle Bet: ~$0.75
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
- Test each contract function individually
|
||||
- Verify state transitions (OPEN → MATCHED → COMPLETED)
|
||||
- Test edge cases (insufficient funds, unauthorized access, etc.)
|
||||
|
||||
### Integration Tests
|
||||
- Test BetEscrow ↔ BetOracle integration
|
||||
- Test oracle consensus mechanism with simulated nodes
|
||||
- Test timeout and fallback scenarios
|
||||
|
||||
### End-to-End Tests
|
||||
- Deploy contracts to testnet (Sepolia/Mumbai)
|
||||
- Create bet through frontend
|
||||
- Accept bet with second account
|
||||
- Trigger oracle settlement
|
||||
- Verify funds distributed correctly
|
||||
|
||||
## Deployment Plan
|
||||
|
||||
### Phase 1: Testnet Deployment
|
||||
1. Deploy contracts to Sepolia (Ethereum) or Mumbai (Polygon)
|
||||
2. Verify contracts on Etherscan
|
||||
3. Set up 3 oracle nodes
|
||||
4. Configure API adapters for test events
|
||||
5. Test full lifecycle with test ETH
|
||||
|
||||
### Phase 2: Security Audit
|
||||
1. Engage smart contract auditor (CertiK, OpenZeppelin, Trail of Bits)
|
||||
2. Fix any discovered vulnerabilities
|
||||
3. Re-audit critical changes
|
||||
|
||||
### Phase 3: Mainnet Launch
|
||||
1. Deploy to Ethereum mainnet or Polygon
|
||||
2. Transfer ownership to multi-sig wallet
|
||||
3. Launch with limited beta users
|
||||
4. Monitor for 2 weeks before full launch
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **EIP-2771 Meta-Transactions**: Backend pays gas for user actions
|
||||
2. **Batch Settlement**: Settle multiple bets in single transaction
|
||||
3. **Flexible Odds**: Support decimal odds like 1.75x instead of whole numbers
|
||||
4. **Partial Matching**: Allow bets to be partially filled by multiple opponents
|
||||
5. **NFT Receipts**: Issue NFTs representing bet participation
|
||||
6. **DAO Governance**: Community votes on oracle disputes
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
backend/app/blockchain/
|
||||
├── contracts/
|
||||
│ ├── BetEscrow.pseudocode.md # This file
|
||||
│ ├── BetOracle.pseudocode.md # Oracle contract spec
|
||||
│ └── README.md # Architecture overview (you are here)
|
||||
├── services/
|
||||
│ ├── blockchain_service.py # Web3 integration
|
||||
│ ├── blockchain_indexer.py # Event listener
|
||||
│ ├── oracle_node.py # Oracle node implementation
|
||||
│ └── oracle_aggregator.py # Consensus aggregator
|
||||
└── config.py # Contract addresses, RPC URLs
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [Ethereum Smart Contracts](https://ethereum.org/en/developers/docs/smart-contracts/)
|
||||
- [Chainlink Oracles](https://chain.link/education/blockchain-oracles)
|
||||
- [OpenZeppelin Contracts](https://docs.openzeppelin.com/contracts/)
|
||||
- [Solidity by Example](https://solidity-by-example.org/)
|
||||
- [Web3.py Documentation](https://web3py.readthedocs.io/)
|
||||
|
||||
---
|
||||
|
||||
**Note**: This is pseudocode for architectural planning. For production deployment, these contracts would need to be written in actual Solidity, audited for security, and thoroughly tested on testnets before mainnet launch.
|
||||
24
backend/app/blockchain/services/__init__.py
Normal file
24
backend/app/blockchain/services/__init__.py
Normal file
@ -0,0 +1,24 @@
|
||||
"""
|
||||
Blockchain Services
|
||||
|
||||
Core services for blockchain integration:
|
||||
- BlockchainService: Web3 provider and contract interactions
|
||||
- BlockchainIndexer: Event listener and database sync
|
||||
- OracleNode: Fetches external API data
|
||||
- OracleAggregator: Achieves consensus among oracle nodes
|
||||
"""
|
||||
|
||||
from .blockchain_service import BlockchainService, get_blockchain_service
|
||||
from .blockchain_indexer import BlockchainIndexer, get_blockchain_indexer
|
||||
from .oracle_node import OracleNode
|
||||
from .oracle_aggregator import OracleAggregator, get_oracle_aggregator
|
||||
|
||||
__all__ = [
|
||||
"BlockchainService",
|
||||
"get_blockchain_service",
|
||||
"BlockchainIndexer",
|
||||
"get_blockchain_indexer",
|
||||
"OracleNode",
|
||||
"OracleAggregator",
|
||||
"get_oracle_aggregator",
|
||||
]
|
||||
427
backend/app/blockchain/services/blockchain_indexer.py
Normal file
427
backend/app/blockchain/services/blockchain_indexer.py
Normal file
@ -0,0 +1,427 @@
|
||||
"""
|
||||
Blockchain Event Indexer
|
||||
|
||||
This service continuously polls the blockchain for new events emitted by
|
||||
BetEscrow and BetOracle contracts, then syncs them to the PostgreSQL database.
|
||||
|
||||
This allows the hybrid architecture to:
|
||||
1. Use blockchain as source of truth for escrow and settlement
|
||||
2. Maintain fast queries through cached database records
|
||||
3. Provide real-time updates to users via WebSocket
|
||||
|
||||
NOTE: This is pseudocode/skeleton showing the architecture.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import Dict, Any, List
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class BlockchainIndexer:
|
||||
"""
|
||||
Indexes blockchain events and syncs to database.
|
||||
|
||||
Continuously polls for new blocks, extracts events, and updates
|
||||
the PostgreSQL database to match on-chain state.
|
||||
"""
|
||||
|
||||
def __init__(self, blockchain_service, database_session):
|
||||
"""
|
||||
Initialize indexer.
|
||||
|
||||
Args:
|
||||
blockchain_service: Instance of BlockchainService
|
||||
database_session: SQLAlchemy async session
|
||||
"""
|
||||
self.blockchain_service = blockchain_service
|
||||
self.db = database_session
|
||||
self.is_running = False
|
||||
self.poll_interval = 10 # seconds
|
||||
|
||||
async def start(self):
|
||||
"""
|
||||
Start the indexer background worker.
|
||||
|
||||
This runs continuously, polling for new blocks every 10 seconds.
|
||||
"""
|
||||
self.is_running = True
|
||||
print("Blockchain indexer started")
|
||||
|
||||
try:
|
||||
while self.is_running:
|
||||
await self._index_new_blocks()
|
||||
await asyncio.sleep(self.poll_interval)
|
||||
except Exception as e:
|
||||
print(f"Indexer error: {e}")
|
||||
self.is_running = False
|
||||
|
||||
async def stop(self):
|
||||
"""Stop the indexer."""
|
||||
self.is_running = False
|
||||
print("Blockchain indexer stopped")
|
||||
|
||||
async def _index_new_blocks(self):
|
||||
"""
|
||||
Index all new blocks since last indexed block.
|
||||
|
||||
Flow:
|
||||
1. Get last indexed block from database
|
||||
2. Get current blockchain height
|
||||
3. For each new block, extract and process events
|
||||
4. Update last indexed block
|
||||
"""
|
||||
# Pseudocode:
|
||||
#
|
||||
# # Get last indexed block from database
|
||||
# last_indexed_block = await self.db.execute(
|
||||
# select(BlockchainSync).order_by(BlockchainSync.block_number.desc()).limit(1)
|
||||
# )
|
||||
# last_block = last_indexed_block.scalar_one_or_none()
|
||||
#
|
||||
# if last_block:
|
||||
# start_block = last_block.block_number + 1
|
||||
# else:
|
||||
# # First time indexing - start from contract deployment block
|
||||
# start_block = DEPLOYMENT_BLOCK_NUMBER
|
||||
#
|
||||
# # Get current blockchain height
|
||||
# current_block = await self.blockchain_service.web3.eth.block_number
|
||||
#
|
||||
# if start_block > current_block:
|
||||
# return # No new blocks
|
||||
#
|
||||
# print(f"Indexing blocks {start_block} to {current_block}")
|
||||
#
|
||||
# # Index each block
|
||||
# for block_num in range(start_block, current_block + 1):
|
||||
# await self._index_block(block_num)
|
||||
#
|
||||
# # Update last indexed block
|
||||
# sync_record = BlockchainSync(
|
||||
# block_number=current_block,
|
||||
# indexed_at=datetime.utcnow()
|
||||
# )
|
||||
# self.db.add(sync_record)
|
||||
# await self.db.commit()
|
||||
|
||||
# Placeholder for pseudocode
|
||||
print("[Indexer] Checking for new blocks...")
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
async def _index_block(self, block_number: int):
|
||||
"""
|
||||
Index all events in a specific block.
|
||||
|
||||
Args:
|
||||
block_number: Block number to index
|
||||
"""
|
||||
# Pseudocode:
|
||||
#
|
||||
# # Get BetCreated events
|
||||
# bet_created_events = await self.blockchain_service.bet_escrow_contract.events.BetCreated.getLogs(
|
||||
# fromBlock=block_number,
|
||||
# toBlock=block_number
|
||||
# )
|
||||
#
|
||||
# for event in bet_created_events:
|
||||
# await self._handle_bet_created(event)
|
||||
#
|
||||
# # Get BetMatched events
|
||||
# bet_matched_events = await self.blockchain_service.bet_escrow_contract.events.BetMatched.getLogs(
|
||||
# fromBlock=block_number,
|
||||
# toBlock=block_number
|
||||
# )
|
||||
#
|
||||
# for event in bet_matched_events:
|
||||
# await self._handle_bet_matched(event)
|
||||
#
|
||||
# # Get BetSettled events
|
||||
# bet_settled_events = await self.blockchain_service.bet_escrow_contract.events.BetSettled.getLogs(
|
||||
# fromBlock=block_number,
|
||||
# toBlock=block_number
|
||||
# )
|
||||
#
|
||||
# for event in bet_settled_events:
|
||||
# await self._handle_bet_settled(event)
|
||||
#
|
||||
# # Get BetDisputed events
|
||||
# bet_disputed_events = await self.blockchain_service.bet_escrow_contract.events.BetDisputed.getLogs(
|
||||
# fromBlock=block_number,
|
||||
# toBlock=block_number
|
||||
# )
|
||||
#
|
||||
# for event in bet_disputed_events:
|
||||
# await self._handle_bet_disputed(event)
|
||||
|
||||
print(f"[Indexer] Indexing block {block_number}")
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
async def _handle_bet_created(self, event: Dict[str, Any]):
|
||||
"""
|
||||
Handle BetCreated event.
|
||||
|
||||
Event structure:
|
||||
{
|
||||
'args': {
|
||||
'betId': 42,
|
||||
'creator': '0xAlice...',
|
||||
'stakeAmount': 100000000000000000000, # 100 ETH in wei
|
||||
'eventId': '0xsuper-bowl-2024',
|
||||
'eventTimestamp': 1704067200
|
||||
},
|
||||
'transactionHash': '0xabc123...',
|
||||
'blockNumber': 12345
|
||||
}
|
||||
|
||||
Syncs to database by updating the bet record with blockchain fields.
|
||||
"""
|
||||
# Pseudocode:
|
||||
#
|
||||
# bet_id = event['args']['betId']
|
||||
# creator_address = event['args']['creator']
|
||||
# stake_amount = self.blockchain_service.web3.fromWei(event['args']['stakeAmount'], 'ether')
|
||||
# tx_hash = event['transactionHash'].hex()
|
||||
# block_number = event['blockNumber']
|
||||
#
|
||||
# # Find bet in database by creator address
|
||||
# # (Bet was already created via backend API with local ID)
|
||||
# user = await self.db.execute(
|
||||
# select(User).where(User.wallet_address == creator_address)
|
||||
# )
|
||||
# user = user.scalar_one_or_none()
|
||||
#
|
||||
# if user:
|
||||
# # Find the most recent bet created by this user without blockchain_bet_id
|
||||
# bet = await self.db.execute(
|
||||
# select(Bet)
|
||||
# .where(Bet.creator_id == user.id, Bet.blockchain_bet_id.is_(None))
|
||||
# .order_by(Bet.created_at.desc())
|
||||
# .limit(1)
|
||||
# )
|
||||
# bet = bet.scalar_one_or_none()
|
||||
#
|
||||
# if bet:
|
||||
# # Update with blockchain data
|
||||
# bet.blockchain_bet_id = bet_id
|
||||
# bet.blockchain_tx_hash = tx_hash
|
||||
# bet.blockchain_status = 'OPEN'
|
||||
# await self.db.commit()
|
||||
#
|
||||
# print(f"[Indexer] BetCreated: bet_id={bet_id}, tx={tx_hash}")
|
||||
|
||||
print(f"[Indexer] BetCreated event: {event}")
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
async def _handle_bet_matched(self, event: Dict[str, Any]):
|
||||
"""
|
||||
Handle BetMatched event.
|
||||
|
||||
Event structure:
|
||||
{
|
||||
'args': {
|
||||
'betId': 42,
|
||||
'opponent': '0xBob...',
|
||||
'totalEscrow': 200000000000000000000 # 200 ETH in wei
|
||||
},
|
||||
'transactionHash': '0xdef456...',
|
||||
'blockNumber': 12346
|
||||
}
|
||||
|
||||
Updates bet status to MATCHED and records opponent.
|
||||
"""
|
||||
# Pseudocode:
|
||||
#
|
||||
# bet_id = event['args']['betId']
|
||||
# opponent_address = event['args']['opponent']
|
||||
# total_escrow = self.blockchain_service.web3.fromWei(event['args']['totalEscrow'], 'ether')
|
||||
# tx_hash = event['transactionHash'].hex()
|
||||
#
|
||||
# # Find bet by blockchain_bet_id
|
||||
# bet = await self.db.execute(
|
||||
# select(Bet).where(Bet.blockchain_bet_id == bet_id)
|
||||
# )
|
||||
# bet = bet.scalar_one_or_none()
|
||||
#
|
||||
# if bet:
|
||||
# # Find opponent user by wallet address
|
||||
# opponent = await self.db.execute(
|
||||
# select(User).where(User.wallet_address == opponent_address)
|
||||
# )
|
||||
# opponent = opponent.scalar_one_or_none()
|
||||
#
|
||||
# if opponent:
|
||||
# bet.opponent_id = opponent.id
|
||||
# bet.status = BetStatus.MATCHED
|
||||
# bet.blockchain_status = 'MATCHED'
|
||||
# await self.db.commit()
|
||||
#
|
||||
# # Create transaction records for both parties
|
||||
# await create_escrow_lock_transaction(bet.creator_id, bet.stake_amount, bet.id)
|
||||
# await create_escrow_lock_transaction(opponent.id, bet.stake_amount, bet.id)
|
||||
#
|
||||
# # Send WebSocket notification
|
||||
# await send_websocket_event("bet_matched", {"bet_id": bet.id})
|
||||
#
|
||||
# print(f"[Indexer] BetMatched: bet_id={bet_id}, opponent={opponent_address}")
|
||||
|
||||
print(f"[Indexer] BetMatched event: {event}")
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
async def _handle_bet_settled(self, event: Dict[str, Any]):
|
||||
"""
|
||||
Handle BetSettled event.
|
||||
|
||||
Event structure:
|
||||
{
|
||||
'args': {
|
||||
'betId': 42,
|
||||
'winner': '0xAlice...',
|
||||
'payoutAmount': 200000000000000000000 # 200 ETH in wei
|
||||
},
|
||||
'transactionHash': '0xghi789...',
|
||||
'blockNumber': 12347
|
||||
}
|
||||
|
||||
Updates bet to COMPLETED and records winner.
|
||||
"""
|
||||
# Pseudocode:
|
||||
#
|
||||
# bet_id = event['args']['betId']
|
||||
# winner_address = event['args']['winner']
|
||||
# payout_amount = self.blockchain_service.web3.fromWei(event['args']['payoutAmount'], 'ether')
|
||||
# tx_hash = event['transactionHash'].hex()
|
||||
#
|
||||
# # Find bet
|
||||
# bet = await self.db.execute(
|
||||
# select(Bet).where(Bet.blockchain_bet_id == bet_id)
|
||||
# )
|
||||
# bet = bet.scalar_one_or_none()
|
||||
#
|
||||
# if bet:
|
||||
# # Find winner user
|
||||
# winner = await self.db.execute(
|
||||
# select(User).where(User.wallet_address == winner_address)
|
||||
# )
|
||||
# winner = winner.scalar_one_or_none()
|
||||
#
|
||||
# if winner:
|
||||
# bet.winner_id = winner.id
|
||||
# bet.status = BetStatus.COMPLETED
|
||||
# bet.blockchain_status = 'COMPLETED'
|
||||
# bet.settled_at = datetime.utcnow()
|
||||
# await self.db.commit()
|
||||
#
|
||||
# # Create transaction records
|
||||
# loser_id = bet.opponent_id if winner.id == bet.creator_id else bet.creator_id
|
||||
#
|
||||
# await create_bet_won_transaction(winner.id, payout_amount, bet.id)
|
||||
# await create_bet_lost_transaction(loser_id, bet.stake_amount, bet.id)
|
||||
#
|
||||
# # Update user stats
|
||||
# await update_user_stats(winner.id, won=True)
|
||||
# await update_user_stats(loser_id, won=False)
|
||||
#
|
||||
# # Send WebSocket notifications
|
||||
# await send_websocket_event("bet_settled", {"bet_id": bet.id, "winner_id": winner.id})
|
||||
#
|
||||
# print(f"[Indexer] BetSettled: bet_id={bet_id}, winner={winner_address}")
|
||||
|
||||
print(f"[Indexer] BetSettled event: {event}")
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
async def _handle_bet_disputed(self, event: Dict[str, Any]):
|
||||
"""
|
||||
Handle BetDisputed event.
|
||||
|
||||
Event structure:
|
||||
{
|
||||
'args': {
|
||||
'betId': 42,
|
||||
'disputedBy': '0xBob...',
|
||||
'timestamp': 1704153600
|
||||
},
|
||||
'transactionHash': '0xjkl012...',
|
||||
'blockNumber': 12348
|
||||
}
|
||||
|
||||
Updates bet status to DISPUTED.
|
||||
"""
|
||||
# Pseudocode:
|
||||
#
|
||||
# bet_id = event['args']['betId']
|
||||
# disputed_by_address = event['args']['disputedBy']
|
||||
#
|
||||
# bet = await self.db.execute(
|
||||
# select(Bet).where(Bet.blockchain_bet_id == bet_id)
|
||||
# )
|
||||
# bet = bet.scalar_one_or_none()
|
||||
#
|
||||
# if bet:
|
||||
# bet.status = BetStatus.DISPUTED
|
||||
# bet.blockchain_status = 'DISPUTED'
|
||||
# await self.db.commit()
|
||||
#
|
||||
# # Notify admins for manual review
|
||||
# await send_admin_notification("Bet disputed", {"bet_id": bet.id})
|
||||
#
|
||||
# print(f"[Indexer] BetDisputed: bet_id={bet_id}, disputed_by={disputed_by_address}")
|
||||
|
||||
print(f"[Indexer] BetDisputed event: {event}")
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
async def reindex_from_block(self, start_block: int):
|
||||
"""
|
||||
Reindex blockchain events from a specific block.
|
||||
|
||||
Useful for recovering from indexer downtime or bugs.
|
||||
|
||||
Args:
|
||||
start_block: Block number to start reindexing from
|
||||
"""
|
||||
# Pseudocode:
|
||||
#
|
||||
# current_block = await self.blockchain_service.web3.eth.block_number
|
||||
#
|
||||
# print(f"Reindexing from block {start_block} to {current_block}")
|
||||
#
|
||||
# for block_num in range(start_block, current_block + 1):
|
||||
# await self._index_block(block_num)
|
||||
# if block_num % 100 == 0:
|
||||
# print(f"Progress: {block_num}/{current_block}")
|
||||
#
|
||||
# print("Reindexing complete")
|
||||
|
||||
print(f"[Indexer] Reindexing from block {start_block}")
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
|
||||
# Singleton instance
|
||||
_indexer: BlockchainIndexer | None = None
|
||||
|
||||
|
||||
def get_blockchain_indexer(blockchain_service, database_session) -> BlockchainIndexer:
|
||||
"""Get singleton indexer instance."""
|
||||
global _indexer
|
||||
|
||||
if _indexer is None:
|
||||
_indexer = BlockchainIndexer(blockchain_service, database_session)
|
||||
|
||||
return _indexer
|
||||
|
||||
|
||||
async def start_indexer_background_task(blockchain_service, database_session):
|
||||
"""
|
||||
Start the indexer as a background task.
|
||||
|
||||
This would be called from main.py on application startup:
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
blockchain_service = get_blockchain_service()
|
||||
db_session = get_database_session()
|
||||
|
||||
indexer = get_blockchain_indexer(blockchain_service, db_session)
|
||||
asyncio.create_task(indexer.start())
|
||||
"""
|
||||
indexer = get_blockchain_indexer(blockchain_service, database_session)
|
||||
await indexer.start()
|
||||
466
backend/app/blockchain/services/blockchain_service.py
Normal file
466
backend/app/blockchain/services/blockchain_service.py
Normal file
@ -0,0 +1,466 @@
|
||||
"""
|
||||
Blockchain Service - Bridge between H2H Backend and Smart Contracts
|
||||
|
||||
This service provides a Web3 integration layer for interacting with BetEscrow
|
||||
and BetOracle smart contracts. It handles transaction building, signing, and
|
||||
submission to the blockchain.
|
||||
|
||||
NOTE: This is pseudocode/skeleton showing the architecture.
|
||||
In production, you would use web3.py library.
|
||||
"""
|
||||
|
||||
from decimal import Decimal
|
||||
from typing import Dict, Any, Optional
|
||||
import asyncio
|
||||
|
||||
|
||||
class BlockchainService:
|
||||
"""
|
||||
Main service for blockchain interactions.
|
||||
|
||||
Responsibilities:
|
||||
- Connect to blockchain RPC endpoint
|
||||
- Load contract ABIs and addresses
|
||||
- Build and sign transactions
|
||||
- Estimate gas costs
|
||||
- Submit transactions and wait for confirmation
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
rpc_url: str,
|
||||
bet_escrow_address: str,
|
||||
bet_oracle_address: str,
|
||||
private_key: str
|
||||
):
|
||||
"""
|
||||
Initialize blockchain service.
|
||||
|
||||
Args:
|
||||
rpc_url: Blockchain RPC endpoint (e.g., Infura, Alchemy)
|
||||
bet_escrow_address: Deployed BetEscrow contract address
|
||||
bet_oracle_address: Deployed BetOracle contract address
|
||||
private_key: Private key for backend signer account
|
||||
"""
|
||||
# Pseudocode: Initialize Web3 connection
|
||||
# self.web3 = Web3(HTTPProvider(rpc_url))
|
||||
# self.account = Account.from_key(private_key)
|
||||
|
||||
# Load contract ABIs and create contract instances
|
||||
# self.bet_escrow_contract = self.web3.eth.contract(
|
||||
# address=bet_escrow_address,
|
||||
# abi=load_abi("BetEscrow")
|
||||
# )
|
||||
# self.bet_oracle_contract = self.web3.eth.contract(
|
||||
# address=bet_oracle_address,
|
||||
# abi=load_abi("BetOracle")
|
||||
# )
|
||||
|
||||
self.rpc_url = rpc_url
|
||||
self.bet_escrow_address = bet_escrow_address
|
||||
self.bet_oracle_address = bet_oracle_address
|
||||
self.signer_address = "DERIVED_FROM_PRIVATE_KEY"
|
||||
|
||||
async def create_bet_on_chain(
|
||||
self,
|
||||
stake_amount: Decimal,
|
||||
creator_odds: float,
|
||||
opponent_odds: float,
|
||||
event_timestamp: int,
|
||||
event_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a bet on the blockchain.
|
||||
|
||||
This function:
|
||||
1. Builds the createBet transaction
|
||||
2. Estimates gas cost
|
||||
3. Signs the transaction with backend wallet
|
||||
4. Submits to blockchain
|
||||
5. Waits for confirmation
|
||||
6. Parses event logs to get bet ID
|
||||
|
||||
Args:
|
||||
stake_amount: Amount each party stakes (in ETH or tokens)
|
||||
creator_odds: Odds multiplier for creator (e.g., 1.5)
|
||||
opponent_odds: Odds multiplier for opponent (e.g., 2.0)
|
||||
event_timestamp: Unix timestamp when event occurs
|
||||
event_id: External event identifier for oracle
|
||||
|
||||
Returns:
|
||||
Dict containing:
|
||||
- bet_id: On-chain bet ID
|
||||
- tx_hash: Transaction hash
|
||||
- block_number: Block number where bet was created
|
||||
- gas_used: Actual gas consumed
|
||||
"""
|
||||
# Pseudocode implementation:
|
||||
#
|
||||
# # Convert stake to wei
|
||||
# stake_wei = self.web3.toWei(stake_amount, 'ether')
|
||||
#
|
||||
# # Build transaction
|
||||
# tx = self.bet_escrow_contract.functions.createBet(
|
||||
# stake_wei,
|
||||
# int(creator_odds * 100), # Scale to avoid decimals
|
||||
# int(opponent_odds * 100),
|
||||
# event_timestamp,
|
||||
# self.web3.toBytes(text=event_id)
|
||||
# ).buildTransaction({
|
||||
# 'from': self.account.address,
|
||||
# 'gas': 200000,
|
||||
# 'gasPrice': await self.web3.eth.gas_price,
|
||||
# 'nonce': await self.web3.eth.get_transaction_count(self.account.address)
|
||||
# })
|
||||
#
|
||||
# # Sign transaction
|
||||
# signed_tx = self.web3.eth.account.sign_transaction(tx, self.account.key)
|
||||
#
|
||||
# # Send transaction
|
||||
# tx_hash = await self.web3.eth.send_raw_transaction(signed_tx.rawTransaction)
|
||||
#
|
||||
# # Wait for receipt
|
||||
# receipt = await self.web3.eth.wait_for_transaction_receipt(tx_hash)
|
||||
#
|
||||
# # Parse BetCreated event
|
||||
# event_log = self.bet_escrow_contract.events.BetCreated().processReceipt(receipt)
|
||||
# bet_id = event_log[0]['args']['betId']
|
||||
#
|
||||
# return {
|
||||
# 'bet_id': bet_id,
|
||||
# 'tx_hash': tx_hash.hex(),
|
||||
# 'block_number': receipt['blockNumber'],
|
||||
# 'gas_used': receipt['gasUsed']
|
||||
# }
|
||||
|
||||
# Placeholder return for pseudocode
|
||||
await asyncio.sleep(0.1) # Simulate async operation
|
||||
return {
|
||||
'bet_id': 42,
|
||||
'tx_hash': '0xabc123...',
|
||||
'block_number': 12345,
|
||||
'gas_used': 150000
|
||||
}
|
||||
|
||||
async def prepare_accept_bet_transaction(
|
||||
self,
|
||||
bet_id: int,
|
||||
user_address: str,
|
||||
stake_amount: Decimal
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Prepare acceptBet transaction for user to sign in frontend.
|
||||
|
||||
This returns an unsigned transaction that the frontend will
|
||||
send to MetaMask for the user to sign.
|
||||
|
||||
Args:
|
||||
bet_id: On-chain bet ID
|
||||
user_address: User's wallet address (from MetaMask)
|
||||
stake_amount: Stake amount to send with transaction
|
||||
|
||||
Returns:
|
||||
Unsigned transaction dict ready for MetaMask
|
||||
"""
|
||||
# Pseudocode:
|
||||
#
|
||||
# stake_wei = self.web3.toWei(stake_amount, 'ether')
|
||||
#
|
||||
# tx = self.bet_escrow_contract.functions.acceptBet(bet_id).buildTransaction({
|
||||
# 'from': user_address,
|
||||
# 'value': stake_wei,
|
||||
# 'gas': 300000,
|
||||
# 'gasPrice': await self.web3.eth.gas_price,
|
||||
# 'nonce': await self.web3.eth.get_transaction_count(user_address)
|
||||
# })
|
||||
#
|
||||
# return tx # Frontend will sign this with MetaMask
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
return {
|
||||
'to': self.bet_escrow_address,
|
||||
'from': user_address,
|
||||
'value': int(stake_amount * 10**18), # wei
|
||||
'gas': 300000,
|
||||
'gasPrice': 50000000000, # 50 gwei
|
||||
'data': '0x...' # Encoded function call
|
||||
}
|
||||
|
||||
async def request_settlement(
|
||||
self,
|
||||
bet_id: int
|
||||
) -> str:
|
||||
"""
|
||||
Request oracle settlement for a bet.
|
||||
|
||||
Called after the real-world event has occurred.
|
||||
This triggers the oracle network to fetch results and settle.
|
||||
|
||||
Args:
|
||||
bet_id: On-chain bet ID
|
||||
|
||||
Returns:
|
||||
Transaction hash
|
||||
"""
|
||||
# Pseudocode:
|
||||
#
|
||||
# tx = self.bet_escrow_contract.functions.requestSettlement(bet_id).buildTransaction({
|
||||
# 'from': self.account.address,
|
||||
# 'gas': 100000,
|
||||
# 'gasPrice': await self.web3.eth.gas_price,
|
||||
# 'nonce': await self.web3.eth.get_transaction_count(self.account.address)
|
||||
# })
|
||||
#
|
||||
# signed_tx = self.web3.eth.account.sign_transaction(tx, self.account.key)
|
||||
# tx_hash = await self.web3.eth.send_raw_transaction(signed_tx.rawTransaction)
|
||||
#
|
||||
# return tx_hash.hex()
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
return '0xdef456...'
|
||||
|
||||
async def settle_bet_via_oracle(
|
||||
self,
|
||||
request_id: int,
|
||||
bet_id: int,
|
||||
winner_address: str,
|
||||
result_data: bytes,
|
||||
signatures: list
|
||||
) -> str:
|
||||
"""
|
||||
Submit oracle settlement to blockchain.
|
||||
|
||||
Called by oracle aggregator after consensus is reached.
|
||||
|
||||
Args:
|
||||
request_id: Oracle request ID
|
||||
bet_id: On-chain bet ID
|
||||
winner_address: Winner's wallet address
|
||||
result_data: Raw API result data
|
||||
signatures: Signatures from oracle nodes
|
||||
|
||||
Returns:
|
||||
Transaction hash
|
||||
"""
|
||||
# Pseudocode:
|
||||
#
|
||||
# tx = self.bet_oracle_contract.functions.fulfillSettlement(
|
||||
# request_id,
|
||||
# bet_id,
|
||||
# winner_address,
|
||||
# result_data,
|
||||
# signatures
|
||||
# ).buildTransaction({
|
||||
# 'from': self.account.address,
|
||||
# 'gas': 250000,
|
||||
# 'gasPrice': await self.web3.eth.gas_price,
|
||||
# 'nonce': await self.web3.eth.get_transaction_count(self.account.address)
|
||||
# })
|
||||
#
|
||||
# signed_tx = self.web3.eth.account.sign_transaction(tx, self.account.key)
|
||||
# tx_hash = await self.web3.eth.send_raw_transaction(signed_tx.rawTransaction)
|
||||
#
|
||||
# return tx_hash.hex()
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
return '0xghi789...'
|
||||
|
||||
async def get_bet_from_chain(self, bet_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Fetch bet details from blockchain.
|
||||
|
||||
Calls the getBet view function on BetEscrow contract.
|
||||
|
||||
Args:
|
||||
bet_id: On-chain bet ID
|
||||
|
||||
Returns:
|
||||
Dict with bet details:
|
||||
- bet_id
|
||||
- creator (address)
|
||||
- opponent (address)
|
||||
- stake_amount
|
||||
- status (enum value)
|
||||
- winner (address)
|
||||
- created_at (timestamp)
|
||||
"""
|
||||
# Pseudocode:
|
||||
#
|
||||
# bet = await self.bet_escrow_contract.functions.getBet(bet_id).call()
|
||||
#
|
||||
# return {
|
||||
# 'bet_id': bet[0],
|
||||
# 'creator': bet[1],
|
||||
# 'opponent': bet[2],
|
||||
# 'stake_amount': self.web3.fromWei(bet[3], 'ether'),
|
||||
# 'status': bet[4], # Enum value (0=OPEN, 1=MATCHED, etc.)
|
||||
# 'creator_odds': bet[5] / 100.0,
|
||||
# 'opponent_odds': bet[6] / 100.0,
|
||||
# 'created_at': bet[7],
|
||||
# 'event_timestamp': bet[8],
|
||||
# 'event_id': bet[9],
|
||||
# 'winner': bet[10],
|
||||
# 'settled_at': bet[11]
|
||||
# }
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
return {
|
||||
'bet_id': bet_id,
|
||||
'creator': '0xAlice...',
|
||||
'opponent': '0xBob...',
|
||||
'stake_amount': Decimal('100'),
|
||||
'status': 1, # MATCHED
|
||||
'winner': None
|
||||
}
|
||||
|
||||
async def get_user_escrow(self, user_address: str) -> Decimal:
|
||||
"""
|
||||
Get total amount user has locked in escrow across all bets.
|
||||
|
||||
Args:
|
||||
user_address: User's wallet address
|
||||
|
||||
Returns:
|
||||
Total escrow amount in ETH
|
||||
"""
|
||||
# Pseudocode:
|
||||
#
|
||||
# escrow_wei = await self.bet_escrow_contract.functions.getUserEscrow(
|
||||
# user_address
|
||||
# ).call()
|
||||
#
|
||||
# return self.web3.fromWei(escrow_wei, 'ether')
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
return Decimal('75.00')
|
||||
|
||||
async def estimate_gas(
|
||||
self,
|
||||
transaction_type: str,
|
||||
**kwargs
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Estimate gas cost for a transaction.
|
||||
|
||||
Args:
|
||||
transaction_type: Type of transaction (create_bet, accept_bet, etc.)
|
||||
**kwargs: Transaction-specific parameters
|
||||
|
||||
Returns:
|
||||
Dict with:
|
||||
- gas_limit: Estimated gas units
|
||||
- gas_price: Current gas price in wei
|
||||
- cost_eth: Total cost in ETH
|
||||
- cost_usd: Total cost in USD (fetched from price oracle)
|
||||
"""
|
||||
# Pseudocode:
|
||||
#
|
||||
# # Build unsigned transaction
|
||||
# if transaction_type == "create_bet":
|
||||
# tx = self.bet_escrow_contract.functions.createBet(...).buildTransaction(...)
|
||||
# elif transaction_type == "accept_bet":
|
||||
# tx = self.bet_escrow_contract.functions.acceptBet(...).buildTransaction(...)
|
||||
#
|
||||
# # Estimate gas
|
||||
# gas_limit = await self.web3.eth.estimate_gas(tx)
|
||||
# gas_price = await self.web3.eth.gas_price
|
||||
#
|
||||
# cost_wei = gas_limit * gas_price
|
||||
# cost_eth = self.web3.fromWei(cost_wei, 'ether')
|
||||
#
|
||||
# # Fetch ETH price from oracle
|
||||
# eth_price_usd = await self.get_eth_price_usd()
|
||||
# cost_usd = float(cost_eth) * eth_price_usd
|
||||
#
|
||||
# return {
|
||||
# 'gas_limit': gas_limit,
|
||||
# 'gas_price': gas_price,
|
||||
# 'cost_eth': cost_eth,
|
||||
# 'cost_usd': cost_usd
|
||||
# }
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# Gas estimates for different operations
|
||||
estimates = {
|
||||
'create_bet': 120000,
|
||||
'accept_bet': 180000,
|
||||
'settle_bet': 150000
|
||||
}
|
||||
|
||||
gas_limit = estimates.get(transaction_type, 100000)
|
||||
gas_price = 50000000000 # 50 gwei
|
||||
cost_wei = gas_limit * gas_price
|
||||
cost_eth = Decimal(cost_wei) / Decimal(10**18)
|
||||
|
||||
return {
|
||||
'gas_limit': gas_limit,
|
||||
'gas_price': gas_price,
|
||||
'cost_eth': cost_eth,
|
||||
'cost_usd': float(cost_eth) * 2000 # Assume $2000/ETH
|
||||
}
|
||||
|
||||
async def get_transaction_receipt(self, tx_hash: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get transaction receipt (confirmation status).
|
||||
|
||||
Args:
|
||||
tx_hash: Transaction hash
|
||||
|
||||
Returns:
|
||||
Receipt dict or None if not yet mined
|
||||
"""
|
||||
# Pseudocode:
|
||||
#
|
||||
# try:
|
||||
# receipt = await self.web3.eth.get_transaction_receipt(tx_hash)
|
||||
# return {
|
||||
# 'status': receipt['status'], # 1 = success, 0 = failed
|
||||
# 'block_number': receipt['blockNumber'],
|
||||
# 'gas_used': receipt['gasUsed'],
|
||||
# 'logs': receipt['logs']
|
||||
# }
|
||||
# except TransactionNotFound:
|
||||
# return None
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
return {
|
||||
'status': 1,
|
||||
'block_number': 12345,
|
||||
'gas_used': 150000,
|
||||
'logs': []
|
||||
}
|
||||
|
||||
|
||||
# Singleton instance
|
||||
_blockchain_service: Optional[BlockchainService] = None
|
||||
|
||||
|
||||
def get_blockchain_service() -> BlockchainService:
|
||||
"""
|
||||
Get singleton blockchain service instance.
|
||||
|
||||
In production, this would load config from environment variables.
|
||||
"""
|
||||
global _blockchain_service
|
||||
|
||||
if _blockchain_service is None:
|
||||
# Pseudocode: Load from config
|
||||
# from app.blockchain.config import BLOCKCHAIN_CONFIG
|
||||
#
|
||||
# _blockchain_service = BlockchainService(
|
||||
# rpc_url=BLOCKCHAIN_CONFIG['rpc_url'],
|
||||
# bet_escrow_address=BLOCKCHAIN_CONFIG['bet_escrow_address'],
|
||||
# bet_oracle_address=BLOCKCHAIN_CONFIG['bet_oracle_address'],
|
||||
# private_key=BLOCKCHAIN_CONFIG['backend_private_key']
|
||||
# )
|
||||
|
||||
# Placeholder for pseudocode
|
||||
_blockchain_service = BlockchainService(
|
||||
rpc_url="https://eth-mainnet.alchemyapi.io/v2/YOUR-API-KEY",
|
||||
bet_escrow_address="0x1234567890abcdef...",
|
||||
bet_oracle_address="0xfedcba0987654321...",
|
||||
private_key="BACKEND_PRIVATE_KEY"
|
||||
)
|
||||
|
||||
return _blockchain_service
|
||||
481
backend/app/blockchain/services/oracle_aggregator.py
Normal file
481
backend/app/blockchain/services/oracle_aggregator.py
Normal file
@ -0,0 +1,481 @@
|
||||
"""
|
||||
Oracle Aggregator - Consensus Coordinator
|
||||
|
||||
This service collects oracle submissions from multiple nodes, verifies consensus,
|
||||
and submits the final result to the blockchain.
|
||||
|
||||
Flow:
|
||||
1. Receive submissions from oracle nodes (via HTTP API)
|
||||
2. Verify each submission's signature
|
||||
3. Count votes for each proposed winner
|
||||
4. Check if consensus threshold is met (e.g., 3 of 5 nodes)
|
||||
5. Submit consensus result to BetOracle contract
|
||||
6. If no consensus, mark request as disputed
|
||||
|
||||
NOTE: This is pseudocode/skeleton showing the architecture.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import Dict, List, Any, Optional
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class OracleSubmission:
|
||||
"""Represents a single oracle node's submission."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
node_id: str,
|
||||
node_address: str,
|
||||
winner_address: str,
|
||||
result_data: Dict[str, Any],
|
||||
signature: str,
|
||||
submitted_at: datetime
|
||||
):
|
||||
self.node_id = node_id
|
||||
self.node_address = node_address
|
||||
self.winner_address = winner_address
|
||||
self.result_data = result_data
|
||||
self.signature = signature
|
||||
self.submitted_at = submitted_at
|
||||
|
||||
|
||||
class OracleAggregator:
|
||||
"""
|
||||
Aggregates oracle submissions and achieves consensus.
|
||||
|
||||
Maintains state of all active oracle requests and their submissions.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
blockchain_service,
|
||||
consensus_threshold: int = 3,
|
||||
total_nodes: int = 5
|
||||
):
|
||||
"""
|
||||
Initialize aggregator.
|
||||
|
||||
Args:
|
||||
blockchain_service: Instance of BlockchainService
|
||||
consensus_threshold: Minimum nodes that must agree (e.g., 3)
|
||||
total_nodes: Total number of oracle nodes (e.g., 5)
|
||||
"""
|
||||
self.blockchain_service = blockchain_service
|
||||
self.consensus_threshold = consensus_threshold
|
||||
self.total_nodes = total_nodes
|
||||
|
||||
# Track submissions per request
|
||||
# request_id => [OracleSubmission, ...]
|
||||
self.submissions: Dict[int, List[OracleSubmission]] = defaultdict(list)
|
||||
|
||||
# Track which nodes have submitted for each request
|
||||
# request_id => {node_address: bool}
|
||||
self.node_submitted: Dict[int, Dict[str, bool]] = defaultdict(dict)
|
||||
|
||||
# Trusted oracle node addresses
|
||||
self.trusted_nodes = set([
|
||||
"0xNode1Address...",
|
||||
"0xNode2Address...",
|
||||
"0xNode3Address...",
|
||||
"0xNode4Address...",
|
||||
"0xNode5Address..."
|
||||
])
|
||||
|
||||
async def receive_submission(
|
||||
self,
|
||||
request_id: int,
|
||||
node_id: str,
|
||||
node_address: str,
|
||||
winner_address: str,
|
||||
result_data: Dict[str, Any],
|
||||
signature: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Receive and process an oracle node submission.
|
||||
|
||||
This is called by the HTTP API endpoint when nodes submit results.
|
||||
|
||||
Args:
|
||||
request_id: Oracle request ID
|
||||
node_id: Node identifier
|
||||
node_address: Node's wallet address
|
||||
winner_address: Proposed winner address
|
||||
result_data: API result data
|
||||
signature: Node's cryptographic signature
|
||||
|
||||
Returns:
|
||||
Dict with status and any consensus result
|
||||
"""
|
||||
# Verify node is trusted
|
||||
if node_address not in self.trusted_nodes:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Untrusted node'
|
||||
}
|
||||
|
||||
# Verify node hasn't already submitted
|
||||
if self.node_submitted[request_id].get(node_address, False):
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Node already submitted'
|
||||
}
|
||||
|
||||
# Verify signature
|
||||
is_valid = await self._verify_signature(
|
||||
request_id,
|
||||
winner_address,
|
||||
result_data,
|
||||
signature,
|
||||
node_address
|
||||
)
|
||||
|
||||
if not is_valid:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Invalid signature'
|
||||
}
|
||||
|
||||
# Store submission
|
||||
submission = OracleSubmission(
|
||||
node_id=node_id,
|
||||
node_address=node_address,
|
||||
winner_address=winner_address,
|
||||
result_data=result_data,
|
||||
signature=signature,
|
||||
submitted_at=datetime.utcnow()
|
||||
)
|
||||
|
||||
self.submissions[request_id].append(submission)
|
||||
self.node_submitted[request_id][node_address] = True
|
||||
|
||||
print(f"[Aggregator] Received submission from {node_id} for request {request_id}")
|
||||
print(f"[Aggregator] {len(self.submissions[request_id])}/{self.total_nodes} nodes submitted")
|
||||
|
||||
# Check if consensus threshold reached
|
||||
if len(self.submissions[request_id]) >= self.consensus_threshold:
|
||||
result = await self._check_consensus_and_settle(request_id)
|
||||
return {
|
||||
'status': 'success',
|
||||
'consensus': result
|
||||
}
|
||||
|
||||
return {
|
||||
'status': 'pending',
|
||||
'submissions_count': len(self.submissions[request_id]),
|
||||
'threshold': self.consensus_threshold
|
||||
}
|
||||
|
||||
async def _verify_signature(
|
||||
self,
|
||||
request_id: int,
|
||||
winner_address: str,
|
||||
result_data: Dict[str, Any],
|
||||
signature: str,
|
||||
node_address: str
|
||||
) -> bool:
|
||||
"""
|
||||
Verify that the signature is valid for this node.
|
||||
|
||||
Args:
|
||||
request_id: Oracle request ID
|
||||
winner_address: Proposed winner
|
||||
result_data: API result
|
||||
signature: Signature to verify
|
||||
node_address: Expected signer address
|
||||
|
||||
Returns:
|
||||
True if signature is valid
|
||||
"""
|
||||
# Pseudocode:
|
||||
#
|
||||
# from eth_account.messages import encode_defunct, defunct_hash_message
|
||||
# from eth_account import Account
|
||||
# import json
|
||||
# import hashlib
|
||||
#
|
||||
# # Reconstruct message
|
||||
# message = f"{request_id}{winner_address}{json.dumps(result_data, sort_keys=True)}"
|
||||
# message_hash = hashlib.sha256(message.encode()).hexdigest()
|
||||
#
|
||||
# # Recover signer from signature
|
||||
# signable_message = encode_defunct(text=message_hash)
|
||||
# recovered_address = Account.recover_message(signable_message, signature=signature)
|
||||
#
|
||||
# # Verify signer matches node address
|
||||
# return recovered_address.lower() == node_address.lower()
|
||||
|
||||
# Placeholder - assume valid
|
||||
return True
|
||||
|
||||
async def _check_consensus_and_settle(self, request_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Check if consensus is reached and settle the bet.
|
||||
|
||||
Steps:
|
||||
1. Count votes for each proposed winner
|
||||
2. Find winner with most votes
|
||||
3. Verify threshold met
|
||||
4. Submit to blockchain if consensus
|
||||
5. Mark as disputed if no consensus
|
||||
|
||||
Args:
|
||||
request_id: Oracle request ID
|
||||
|
||||
Returns:
|
||||
Dict with consensus result
|
||||
"""
|
||||
submissions = self.submissions[request_id]
|
||||
|
||||
# Count votes for each winner
|
||||
vote_counts: Dict[str, int] = defaultdict(int)
|
||||
vote_details: Dict[str, List[str]] = defaultdict(list)
|
||||
|
||||
for submission in submissions:
|
||||
winner = submission.winner_address
|
||||
vote_counts[winner] += 1
|
||||
vote_details[winner].append(submission.node_id)
|
||||
|
||||
# Find winner with most votes
|
||||
consensus_winner = max(vote_counts, key=vote_counts.get)
|
||||
consensus_votes = vote_counts[consensus_winner]
|
||||
|
||||
print(f"[Aggregator] Vote results for request {request_id}:")
|
||||
for winner, count in vote_counts.items():
|
||||
nodes = vote_details[winner]
|
||||
print(f" {winner}: {count} votes from {nodes}")
|
||||
|
||||
# Check if threshold met
|
||||
if consensus_votes >= self.consensus_threshold:
|
||||
print(f"[Aggregator] Consensus reached! Winner: {consensus_winner} ({consensus_votes}/{self.total_nodes} votes)")
|
||||
|
||||
# Get bet ID from request
|
||||
# In production, fetch from blockchain or database
|
||||
bet_id = await self._get_bet_id_for_request(request_id)
|
||||
|
||||
# Collect signatures for blockchain submission
|
||||
signatures = [
|
||||
sub.signature
|
||||
for sub in submissions
|
||||
if sub.winner_address == consensus_winner
|
||||
]
|
||||
|
||||
# Get result data from consensus submissions
|
||||
result_data = submissions[0].result_data # Use first submission's data
|
||||
|
||||
# Submit to blockchain
|
||||
tx_hash = await self.blockchain_service.settle_bet_via_oracle(
|
||||
request_id=request_id,
|
||||
bet_id=bet_id,
|
||||
winner_address=consensus_winner,
|
||||
result_data=str(result_data).encode(),
|
||||
signatures=signatures
|
||||
)
|
||||
|
||||
print(f"[Aggregator] Submitted settlement to blockchain: {tx_hash}")
|
||||
|
||||
# Clean up submissions for this request
|
||||
del self.submissions[request_id]
|
||||
del self.node_submitted[request_id]
|
||||
|
||||
return {
|
||||
'consensus': True,
|
||||
'winner': consensus_winner,
|
||||
'votes': consensus_votes,
|
||||
'tx_hash': tx_hash
|
||||
}
|
||||
|
||||
else:
|
||||
print(f"[Aggregator] No consensus for request {request_id}")
|
||||
print(f" Best: {consensus_winner} with {consensus_votes} votes (need {self.consensus_threshold})")
|
||||
|
||||
# Mark as disputed
|
||||
await self._mark_as_disputed(request_id)
|
||||
|
||||
return {
|
||||
'consensus': False,
|
||||
'reason': 'Insufficient consensus',
|
||||
'votes': dict(vote_counts)
|
||||
}
|
||||
|
||||
async def _get_bet_id_for_request(self, request_id: int) -> int:
|
||||
"""
|
||||
Get bet ID associated with oracle request.
|
||||
|
||||
In production, this would query the BetOracle contract
|
||||
or database to get the bet_id for this request_id.
|
||||
|
||||
Args:
|
||||
request_id: Oracle request ID
|
||||
|
||||
Returns:
|
||||
Bet ID
|
||||
"""
|
||||
# Pseudocode:
|
||||
#
|
||||
# request = await self.blockchain_service.bet_oracle_contract.functions.getRequest(
|
||||
# request_id
|
||||
# ).call()
|
||||
#
|
||||
# return request['betId']
|
||||
|
||||
# Placeholder
|
||||
return 42
|
||||
|
||||
async def _mark_as_disputed(self, request_id: int):
|
||||
"""
|
||||
Mark oracle request as disputed.
|
||||
|
||||
Sends notification to admin for manual review.
|
||||
|
||||
Args:
|
||||
request_id: Oracle request ID
|
||||
"""
|
||||
# Pseudocode:
|
||||
#
|
||||
# # Update database
|
||||
# await db.execute(
|
||||
# update(OracleRequest)
|
||||
# .where(OracleRequest.request_id == request_id)
|
||||
# .values(status='DISPUTED')
|
||||
# )
|
||||
#
|
||||
# # Notify admins
|
||||
# await send_admin_notification(
|
||||
# "Oracle Dispute",
|
||||
# f"Request {request_id} - no consensus reached"
|
||||
# )
|
||||
#
|
||||
# # Could also call blockchain contract to mark as disputed
|
||||
# await self.blockchain_service.bet_oracle_contract.functions.markAsDisputed(
|
||||
# request_id
|
||||
# ).send()
|
||||
|
||||
print(f"[Aggregator] Marked request {request_id} as disputed")
|
||||
|
||||
async def get_request_status(self, request_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Get status of an oracle request.
|
||||
|
||||
Args:
|
||||
request_id: Oracle request ID
|
||||
|
||||
Returns:
|
||||
Dict with current status and submissions
|
||||
"""
|
||||
submissions = self.submissions.get(request_id, [])
|
||||
|
||||
vote_counts: Dict[str, int] = defaultdict(int)
|
||||
for sub in submissions:
|
||||
vote_counts[sub.winner_address] += 1
|
||||
|
||||
return {
|
||||
'request_id': request_id,
|
||||
'submissions_count': len(submissions),
|
||||
'threshold': self.consensus_threshold,
|
||||
'votes': dict(vote_counts),
|
||||
'nodes_submitted': [sub.node_id for sub in submissions]
|
||||
}
|
||||
|
||||
async def handle_timeout(self, request_id: int):
|
||||
"""
|
||||
Handle oracle request timeout.
|
||||
|
||||
Called if nodes fail to respond within time limit (e.g., 1 hour).
|
||||
|
||||
Args:
|
||||
request_id: Oracle request ID
|
||||
"""
|
||||
submissions = self.submissions.get(request_id, [])
|
||||
|
||||
if len(submissions) == 0:
|
||||
print(f"[Aggregator] Request {request_id} timed out with no submissions")
|
||||
else:
|
||||
print(f"[Aggregator] Request {request_id} timed out with {len(submissions)} submissions")
|
||||
# Try to settle with available submissions
|
||||
await self._check_consensus_and_settle(request_id)
|
||||
|
||||
# Mark as timed out on blockchain
|
||||
# Pseudocode:
|
||||
# await self.blockchain_service.bet_oracle_contract.functions.markAsTimedOut(
|
||||
# request_id
|
||||
# ).send()
|
||||
|
||||
|
||||
# Singleton instance
|
||||
_aggregator: Optional[OracleAggregator] = None
|
||||
|
||||
|
||||
def get_oracle_aggregator(blockchain_service) -> OracleAggregator:
|
||||
"""Get singleton aggregator instance."""
|
||||
global _aggregator
|
||||
|
||||
if _aggregator is None:
|
||||
_aggregator = OracleAggregator(
|
||||
blockchain_service=blockchain_service,
|
||||
consensus_threshold=3,
|
||||
total_nodes=5
|
||||
)
|
||||
|
||||
return _aggregator
|
||||
|
||||
|
||||
# FastAPI endpoint example
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class OracleSubmissionRequest(BaseModel):
|
||||
request_id: int
|
||||
node_id: str
|
||||
node_address: str
|
||||
winner_address: str
|
||||
result_data: dict
|
||||
signature: str
|
||||
|
||||
|
||||
@router.post("/oracle/submit")
|
||||
async def submit_oracle_result(submission: OracleSubmissionRequest):
|
||||
\"""
|
||||
API endpoint for oracle nodes to submit results.
|
||||
|
||||
Called by oracle_node.py when a node has fetched and signed a result.
|
||||
\"""
|
||||
from app.blockchain.services.blockchain_service import get_blockchain_service
|
||||
|
||||
blockchain_service = get_blockchain_service()
|
||||
aggregator = get_oracle_aggregator(blockchain_service)
|
||||
|
||||
result = await aggregator.receive_submission(
|
||||
request_id=submission.request_id,
|
||||
node_id=submission.node_id,
|
||||
node_address=submission.node_address,
|
||||
winner_address=submission.winner_address,
|
||||
result_data=submission.result_data,
|
||||
signature=submission.signature
|
||||
)
|
||||
|
||||
if result['status'] == 'error':
|
||||
raise HTTPException(status_code=400, detail=result['message'])
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/oracle/status/{request_id}")
|
||||
async def get_oracle_status(request_id: int):
|
||||
\"""
|
||||
Get status of an oracle request.
|
||||
|
||||
Shows how many nodes have submitted and current vote counts.
|
||||
\"""
|
||||
from app.blockchain.services.blockchain_service import get_blockchain_service
|
||||
|
||||
blockchain_service = get_blockchain_service()
|
||||
aggregator = get_oracle_aggregator(blockchain_service)
|
||||
|
||||
status = await aggregator.get_request_status(request_id)
|
||||
return status
|
||||
"""
|
||||
471
backend/app/blockchain/services/oracle_node.py
Normal file
471
backend/app/blockchain/services/oracle_node.py
Normal file
@ -0,0 +1,471 @@
|
||||
"""
|
||||
Oracle Node - Decentralized Data Provider
|
||||
|
||||
This service acts as one node in the oracle network. It:
|
||||
1. Listens for OracleRequested events from BetOracle contract
|
||||
2. Fetches data from external APIs (sports, entertainment, etc.)
|
||||
3. Determines the winner based on API results
|
||||
4. Signs the result with node's private key
|
||||
5. Submits the signed result to oracle aggregator
|
||||
|
||||
Multiple independent oracle nodes run this code to create a decentralized
|
||||
oracle network with consensus-based settlement.
|
||||
|
||||
NOTE: This is pseudocode/skeleton showing the architecture.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
from typing import Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
|
||||
class OracleNode:
|
||||
"""
|
||||
Oracle node for fetching and submitting external data.
|
||||
|
||||
Each node operates independently and signs its results.
|
||||
The aggregator collects results from all nodes and achieves consensus.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
node_id: str,
|
||||
private_key: str,
|
||||
blockchain_service,
|
||||
aggregator_url: str
|
||||
):
|
||||
"""
|
||||
Initialize oracle node.
|
||||
|
||||
Args:
|
||||
node_id: Unique identifier for this node
|
||||
private_key: Node's private key for signing results
|
||||
blockchain_service: Instance of BlockchainService
|
||||
aggregator_url: URL of oracle aggregator service
|
||||
"""
|
||||
self.node_id = node_id
|
||||
self.private_key = private_key
|
||||
self.blockchain_service = blockchain_service
|
||||
self.aggregator_url = aggregator_url
|
||||
self.is_running = False
|
||||
|
||||
# API adapters for different event types
|
||||
self.api_adapters = {
|
||||
'sports': self._fetch_sports_result,
|
||||
'entertainment': self._fetch_entertainment_result,
|
||||
'politics': self._fetch_politics_result,
|
||||
'custom': self._fetch_custom_result
|
||||
}
|
||||
|
||||
async def start(self):
|
||||
"""
|
||||
Start the oracle node.
|
||||
|
||||
Continuously listens for OracleRequested events.
|
||||
"""
|
||||
self.is_running = True
|
||||
print(f"Oracle node {self.node_id} started")
|
||||
|
||||
try:
|
||||
while self.is_running:
|
||||
await self._listen_for_oracle_requests()
|
||||
await asyncio.sleep(5)
|
||||
except Exception as e:
|
||||
print(f"Oracle node error: {e}")
|
||||
self.is_running = False
|
||||
|
||||
async def stop(self):
|
||||
"""Stop the oracle node."""
|
||||
self.is_running = False
|
||||
print(f"Oracle node {self.node_id} stopped")
|
||||
|
||||
async def _listen_for_oracle_requests(self):
|
||||
"""
|
||||
Listen for OracleRequested events from blockchain.
|
||||
|
||||
When an event is detected, process the oracle request.
|
||||
"""
|
||||
# Pseudocode:
|
||||
#
|
||||
# # Create event filter for OracleRequested
|
||||
# event_filter = self.blockchain_service.bet_oracle_contract.events.OracleRequested.create_filter(
|
||||
# fromBlock='latest'
|
||||
# )
|
||||
#
|
||||
# # Get new events
|
||||
# events = event_filter.get_new_entries()
|
||||
#
|
||||
# for event in events:
|
||||
# request_id = event['args']['requestId']
|
||||
# bet_id = event['args']['betId']
|
||||
# event_id = event['args']['eventId']
|
||||
# api_endpoint = event['args']['apiEndpoint']
|
||||
#
|
||||
# print(f"[Oracle {self.node_id}] Received request {request_id} for bet {bet_id}")
|
||||
#
|
||||
# # Process request asynchronously
|
||||
# asyncio.create_task(
|
||||
# self._process_oracle_request(request_id, bet_id, event_id, api_endpoint)
|
||||
# )
|
||||
|
||||
# Placeholder
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
async def _process_oracle_request(
|
||||
self,
|
||||
request_id: int,
|
||||
bet_id: int,
|
||||
event_id: str,
|
||||
api_endpoint: str
|
||||
):
|
||||
"""
|
||||
Process an oracle request.
|
||||
|
||||
Steps:
|
||||
1. Fetch bet details from blockchain
|
||||
2. Call external API
|
||||
3. Parse result to determine winner
|
||||
4. Sign the result
|
||||
5. Submit to aggregator
|
||||
|
||||
Args:
|
||||
request_id: Oracle request ID
|
||||
bet_id: On-chain bet ID
|
||||
event_id: External event identifier
|
||||
api_endpoint: API URL to fetch result from
|
||||
"""
|
||||
try:
|
||||
# Get bet details
|
||||
bet = await self.blockchain_service.get_bet_from_chain(bet_id)
|
||||
|
||||
# Determine API adapter type from event_id
|
||||
# event_id format: "sports:nfl-super-bowl-2024"
|
||||
adapter_type = event_id.split(':')[0] if ':' in event_id else 'custom'
|
||||
|
||||
# Fetch API result
|
||||
api_result = await self._fetch_api_result(adapter_type, api_endpoint, event_id)
|
||||
|
||||
if not api_result:
|
||||
print(f"[Oracle {self.node_id}] Failed to fetch API result for {event_id}")
|
||||
return
|
||||
|
||||
# Determine winner from API result
|
||||
winner_address = await self._determine_winner(api_result, bet)
|
||||
|
||||
if not winner_address:
|
||||
print(f"[Oracle {self.node_id}] Could not determine winner from API result")
|
||||
return
|
||||
|
||||
# Sign the result
|
||||
signature = self._sign_result(request_id, winner_address, api_result)
|
||||
|
||||
# Submit to aggregator
|
||||
await self._submit_to_aggregator(
|
||||
request_id,
|
||||
winner_address,
|
||||
api_result,
|
||||
signature
|
||||
)
|
||||
|
||||
print(f"[Oracle {self.node_id}] Submitted result for request {request_id}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Oracle {self.node_id}] Error processing request {request_id}: {e}")
|
||||
|
||||
async def _fetch_api_result(
|
||||
self,
|
||||
adapter_type: str,
|
||||
api_endpoint: str,
|
||||
event_id: str
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Fetch result from external API.
|
||||
|
||||
Args:
|
||||
adapter_type: Type of API (sports, entertainment, etc.)
|
||||
api_endpoint: API URL
|
||||
event_id: Event identifier
|
||||
|
||||
Returns:
|
||||
Parsed API response dict or None if failed
|
||||
"""
|
||||
adapter_function = self.api_adapters.get(adapter_type, self._fetch_custom_result)
|
||||
return await adapter_function(api_endpoint, event_id)
|
||||
|
||||
async def _fetch_sports_result(
|
||||
self,
|
||||
api_endpoint: str,
|
||||
event_id: str
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Fetch sports event result from ESPN API or similar.
|
||||
|
||||
Example ESPN API response:
|
||||
{
|
||||
"event_id": "nfl-super-bowl-2024",
|
||||
"status": "final",
|
||||
"winner": {
|
||||
"team_name": "San Francisco 49ers",
|
||||
"score": 24
|
||||
},
|
||||
"loser": {
|
||||
"team_name": "Kansas City Chiefs",
|
||||
"score": 21
|
||||
}
|
||||
}
|
||||
|
||||
Args:
|
||||
api_endpoint: ESPN API URL
|
||||
event_id: Event identifier
|
||||
|
||||
Returns:
|
||||
Parsed result dict
|
||||
"""
|
||||
# Pseudocode:
|
||||
#
|
||||
# import httpx
|
||||
#
|
||||
# async with httpx.AsyncClient() as client:
|
||||
# response = await client.get(api_endpoint, timeout=10.0)
|
||||
#
|
||||
# if response.status_code != 200:
|
||||
# return None
|
||||
#
|
||||
# data = response.json()
|
||||
#
|
||||
# # Extract winner from response
|
||||
# # Path depends on API structure
|
||||
# winner_team = data['events'][0]['competitions'][0]['winner']['team']['displayName']
|
||||
#
|
||||
# return {
|
||||
# 'event_id': event_id,
|
||||
# 'winner': winner_team,
|
||||
# 'status': 'final',
|
||||
# 'timestamp': datetime.utcnow().isoformat()
|
||||
# }
|
||||
|
||||
# Placeholder for pseudocode
|
||||
await asyncio.sleep(0.1)
|
||||
return {
|
||||
'event_id': event_id,
|
||||
'winner': 'San Francisco 49ers',
|
||||
'status': 'final'
|
||||
}
|
||||
|
||||
async def _fetch_entertainment_result(
|
||||
self,
|
||||
api_endpoint: str,
|
||||
event_id: str
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Fetch entertainment event result (awards, box office, etc.).
|
||||
|
||||
Example Oscars API response:
|
||||
{
|
||||
"year": 2024,
|
||||
"category": "Best Picture",
|
||||
"winner": "Oppenheimer",
|
||||
"nominees": ["Oppenheimer", "Killers of the Flower Moon", ...]
|
||||
}
|
||||
"""
|
||||
# Pseudocode:
|
||||
#
|
||||
# async with httpx.AsyncClient() as client:
|
||||
# response = await client.get(api_endpoint, timeout=10.0)
|
||||
# data = response.json()
|
||||
#
|
||||
# winner = data['categories']['best_picture']['winner']
|
||||
#
|
||||
# return {
|
||||
# 'event_id': event_id,
|
||||
# 'winner': winner,
|
||||
# 'category': 'Best Picture'
|
||||
# }
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
return {
|
||||
'event_id': event_id,
|
||||
'winner': 'Oppenheimer',
|
||||
'category': 'Best Picture'
|
||||
}
|
||||
|
||||
async def _fetch_politics_result(
|
||||
self,
|
||||
api_endpoint: str,
|
||||
event_id: str
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Fetch political event result (elections, votes, etc.)."""
|
||||
await asyncio.sleep(0.1)
|
||||
return {
|
||||
'event_id': event_id,
|
||||
'winner': 'Candidate A'
|
||||
}
|
||||
|
||||
async def _fetch_custom_result(
|
||||
self,
|
||||
api_endpoint: str,
|
||||
event_id: str
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Fetch result from custom user-specified API."""
|
||||
await asyncio.sleep(0.1)
|
||||
return {
|
||||
'event_id': event_id,
|
||||
'result': 'Custom result'
|
||||
}
|
||||
|
||||
async def _determine_winner(
|
||||
self,
|
||||
api_result: Dict[str, Any],
|
||||
bet: Dict[str, Any]
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Map API result to bet participant address.
|
||||
|
||||
Compares API result with bet positions to determine winner.
|
||||
|
||||
Args:
|
||||
api_result: Result from external API
|
||||
bet: Bet details from blockchain
|
||||
|
||||
Returns:
|
||||
Winner's wallet address or None if cannot determine
|
||||
"""
|
||||
# Pseudocode:
|
||||
#
|
||||
# # Get creator and opponent positions from bet metadata (stored off-chain)
|
||||
# bet_metadata = await get_bet_metadata_from_db(bet['bet_id'])
|
||||
#
|
||||
# creator_position = bet_metadata['creator_position'] # e.g., "49ers win"
|
||||
# opponent_position = bet_metadata['opponent_position'] # e.g., "Chiefs win"
|
||||
#
|
||||
# api_winner = api_result.get('winner')
|
||||
#
|
||||
# # Simple string matching (production would use NLP/fuzzy matching)
|
||||
# if "49ers" in creator_position and "49ers" in api_winner:
|
||||
# return bet['creator']
|
||||
# elif "Chiefs" in creator_position and "Chiefs" in api_winner:
|
||||
# return bet['creator']
|
||||
# else:
|
||||
# return bet['opponent']
|
||||
|
||||
# Placeholder - assume creator won
|
||||
return bet['creator']
|
||||
|
||||
def _sign_result(
|
||||
self,
|
||||
request_id: int,
|
||||
winner_address: str,
|
||||
result_data: Dict[str, Any]
|
||||
) -> str:
|
||||
"""
|
||||
Sign the oracle result with node's private key.
|
||||
|
||||
This proves that this specific oracle node submitted this result.
|
||||
|
||||
Args:
|
||||
request_id: Oracle request ID
|
||||
winner_address: Winner's address
|
||||
result_data: API result data
|
||||
|
||||
Returns:
|
||||
Hex-encoded signature
|
||||
"""
|
||||
# Pseudocode:
|
||||
#
|
||||
# from eth_account.messages import encode_defunct
|
||||
# from eth_account import Account
|
||||
#
|
||||
# # Create message hash
|
||||
# message = f"{request_id}{winner_address}{json.dumps(result_data, sort_keys=True)}"
|
||||
# message_hash = hashlib.sha256(message.encode()).hexdigest()
|
||||
#
|
||||
# # Sign with private key
|
||||
# signable_message = encode_defunct(text=message_hash)
|
||||
# signed_message = Account.sign_message(signable_message, private_key=self.private_key)
|
||||
#
|
||||
# return signed_message.signature.hex()
|
||||
|
||||
# Placeholder
|
||||
message = f"{request_id}{winner_address}{json.dumps(result_data, sort_keys=True)}"
|
||||
signature_hash = hashlib.sha256(message.encode()).hexdigest()
|
||||
return f"0x{signature_hash}"
|
||||
|
||||
async def _submit_to_aggregator(
|
||||
self,
|
||||
request_id: int,
|
||||
winner_address: str,
|
||||
result_data: Dict[str, Any],
|
||||
signature: str
|
||||
):
|
||||
"""
|
||||
Submit signed result to oracle aggregator.
|
||||
|
||||
The aggregator collects results from all nodes and checks consensus.
|
||||
|
||||
Args:
|
||||
request_id: Oracle request ID
|
||||
winner_address: Winner's address
|
||||
result_data: API result data
|
||||
signature: Node's signature
|
||||
"""
|
||||
# Pseudocode:
|
||||
#
|
||||
# import httpx
|
||||
#
|
||||
# payload = {
|
||||
# 'request_id': request_id,
|
||||
# 'node_id': self.node_id,
|
||||
# 'node_address': self.node_address, # Derived from private_key
|
||||
# 'winner_address': winner_address,
|
||||
# 'result_data': result_data,
|
||||
# 'signature': signature
|
||||
# }
|
||||
#
|
||||
# async with httpx.AsyncClient() as client:
|
||||
# response = await client.post(
|
||||
# f"{self.aggregator_url}/oracle/submit",
|
||||
# json=payload,
|
||||
# timeout=10.0
|
||||
# )
|
||||
#
|
||||
# if response.status_code == 200:
|
||||
# print(f"[Oracle {self.node_id}] Result submitted successfully")
|
||||
# else:
|
||||
# print(f"[Oracle {self.node_id}] Failed to submit: {response.text}")
|
||||
|
||||
print(f"[Oracle {self.node_id}] Submitting result to aggregator...")
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
|
||||
# Example: Running multiple oracle nodes
|
||||
async def start_oracle_node(node_id: str, private_key: str):
|
||||
"""
|
||||
Start an oracle node instance.
|
||||
|
||||
In production, you would run 3-5 nodes on different servers
|
||||
with different API keys and infrastructure for redundancy.
|
||||
|
||||
Example:
|
||||
# Node 1 (AWS us-east-1)
|
||||
await start_oracle_node("oracle-node-1", "PRIVATE_KEY_1")
|
||||
|
||||
# Node 2 (GCP us-west-2)
|
||||
await start_oracle_node("oracle-node-2", "PRIVATE_KEY_2")
|
||||
|
||||
# Node 3 (Azure eu-west-1)
|
||||
await start_oracle_node("oracle-node-3", "PRIVATE_KEY_3")
|
||||
"""
|
||||
from .blockchain_service import get_blockchain_service
|
||||
|
||||
blockchain_service = get_blockchain_service()
|
||||
|
||||
node = OracleNode(
|
||||
node_id=node_id,
|
||||
private_key=private_key,
|
||||
blockchain_service=blockchain_service,
|
||||
aggregator_url="https://aggregator.h2h.com"
|
||||
)
|
||||
|
||||
await node.start()
|
||||
Reference in New Issue
Block a user