Quickstart
Build a dice game with verifiable randomness in 15 minutes
VRF Quickstart
Build a dice game with verifiable randomness and real-time updates in under 15 minutes. This tutorial demonstrates VRF integration with instant result notifications.
What We'll Build
A dice game that:
- Requests verifiable random numbers
- Tracks player streaks for rolling sixes
- Updates UI in real-time via WebSocket
- Handles VRF callbacks securely
Prerequisites
- Node.js 16+ installed
- Basic Solidity knowledge
- MetaMask or similar wallet
- RISE testnet tokens
Smart Contract Setup
Create the Project
mkdir vrf-dice-game
cd vrf-dice-game
forge initWrite the Dice Game Contract
Create src/DiceGame.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IVRFCoordinator {
function requestRandomNumbers(uint32 numNumbers, uint256 seed) external returns (uint256);
}
interface IVRFConsumer {
function rawFulfillRandomNumbers(
uint256 requestId,
uint256[] memory randomNumbers
) external;
}
contract DiceGame is IVRFConsumer {
IVRFCoordinator public coordinator;
mapping(address => bool) public hasPendingRoll;
mapping(address => uint256) public currentStreak;
mapping(address => uint256) public topStreak;
mapping(uint256 => address) public requestOwners;
uint256 public requestCount = 0;
uint8 private constant DICE_SIDES = 6;
event DiceRollRequested(address indexed player, uint256 indexed requestId);
event DiceRollCompleted(
address indexed player,
uint256 indexed requestId,
uint256 result,
uint256 currentStreak,
uint256 topStreak
);
event NewTopScore(address indexed player, uint256 newTopStreak);
constructor(address _coordinator) {
coordinator = IVRFCoordinator(_coordinator);
}
function rollDice() external returns (uint256 requestId) {
require(!hasPendingRoll[msg.sender], "Already has a pending roll");
hasPendingRoll[msg.sender] = true;
uint256 seed = requestCount++;
requestId = coordinator.requestRandomNumbers(1, seed);
requestOwners[requestId] = msg.sender;
emit DiceRollRequested(msg.sender, requestId);
return requestId;
}
function rawFulfillRandomNumbers(
uint256 requestId,
uint256[] memory randomNumbers
) external {
require(msg.sender == address(coordinator), "Only coordinator can fulfill");
require(randomNumbers.length > 0, "No random numbers provided");
address player = requestOwners[requestId];
require(player != address(0), "Unknown request ID");
require(hasPendingRoll[player], "No pending roll");
uint256 diceRoll = (randomNumbers[0] % DICE_SIDES) + 1;
if (diceRoll == 6) {
currentStreak[player] += 1;
if (currentStreak[player] > topStreak[player]) {
topStreak[player] = currentStreak[player];
emit NewTopScore(player, topStreak[player]);
}
} else {
currentStreak[player] = 0;
}
hasPendingRoll[player] = false;
emit DiceRollCompleted(
player,
requestId,
diceRoll,
currentStreak[player],
topStreak[player]
);
delete requestOwners[requestId];
}
}Deploy to RISE Testnet
Configure foundry.toml:
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc_version = "0.8.19"
[rpc_endpoints]
rise_testnet = "https://testnet.riselabs.xyz"
[etherscan]
rise_testnet = { key = "", url = "https://explorer.testnet.riselabs.xyz/api" }Deploy:
forge create --rpc-url rise_testnet \
--private-key $PRIVATE_KEY \
--constructor-args 0x9d57aB4517ba97349551C876a01a7580B1338909 \
src/DiceGame.sol:DiceGameVerify on Blockscout:
forge verify-contract <DEPLOYED_ADDRESS> \
src/DiceGame.sol:DiceGame \
--chain 11155931 \
--verifier blockscout \
--verifier-url https://explorer.testnet.riselabs.xyz/api \
--constructor-args $(cast abi-encode "constructor(address)" 0x9d57aB4517ba97349551C876a01a7580B1338909)Frontend Integration
Setup Frontend
npm install viempnpm add viemyarn add viembun add viemMonitor VRF Events
Create monitor.ts to track dice roll events:
import { createPublicClient, webSocket } from 'viem';
import { riseTestnet } from 'viem/chains';
const wsUrl = 'wss://testnet.riselabs.xyz/ws';
const diceGameAddress = '0x...'; // Your deployed contract
const vrfAbi = [
{
type: 'event',
name: 'DiceRollCompleted',
inputs: [
{ name: 'player', type: 'address', indexed: true },
{ name: 'requestId', type: 'uint256', indexed: true },
{ name: 'result', type: 'uint256' },
{ name: 'currentStreak', type: 'uint256' },
{ name: 'topStreak', type: 'uint256' }
]
}
] as const;
const client = createPublicClient({
chain: riseTestnet,
transport: webSocket(wsUrl)
});
const unwatch = client.watchContractEvent({
address: diceGameAddress,
abi: vrfAbi,
eventName: 'DiceRollCompleted',
onLogs: (logs) => {
logs.forEach((log) => {
console.log(`Player rolled: ${log.args.result}`);
console.log(`Streak: ${log.args.currentStreak}`);
});
}
});How It Works
- User initiates: Click roll button in the DApp
- Request randomness: DApp calls
rollDice()on the contract - VRF processing: Contract requests random numbers from VRF coordinator
- Instant callback: VRF calls
rawFulfillRandomNumbers()with result - Event emission: Contract emits
DiceRollCompletedevent - Real-time update: WebSocket delivers event to DApp in ~4ms
- UI update: DApp shows the dice result immediately
Key Takeaways
- Instant Results: VRF responds in milliseconds via shreds
- Real-time Updates: WebSocket delivers events instantly
- Secure Randomness: Cryptographically verifiable numbers
- Simple Integration: Standard Solidity interfaces
Next Steps
- Add betting mechanics with token stakes
- Create multiplayer dice battles
- Implement leaderboards
- Add different game modes