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 (available from faucet)
Smart Contract Setup
Create the Project
mkdir vrf-dice-game
cd vrf-dice-game
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npx hardhat init
Write the Dice Game Contract
Create contracts/DiceGame.sol
:
contracts/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
Create scripts/deploy.js
:
scripts/deploy.js
const hre = require("hardhat");
async function main() {
const VRF_COORDINATOR = "0x9d57aB4517ba97349551C876a01a7580B1338909";
const DiceGame = await hre.ethers.getContractFactory("DiceGame");
const diceGame = await DiceGame.deploy(VRF_COORDINATOR);
await diceGame.waitForDeployment();
console.log("DiceGame deployed to:", await diceGame.getAddress());
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Configure hardhat.config.js
:
hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
module.exports = {
solidity: "0.8.19",
networks: {
riseTestnet: {
url: "https://testnet.riselabs.xyz",
chainId: 11155931,
accounts: [process.env.PRIVATE_KEY]
}
}
};
Deploy:
npx hardhat run scripts/deploy.js --network riseTestnet
Frontend Integration
Setup Frontend
npm install shreds viem ethers@6
Create the UI
Create index.html
:
index.html
<!DOCTYPE html>
<html>
<head>
<title>RISE VRF Dice Game</title>
<style>
body { font-family: monospace; padding: 20px; }
.dice { font-size: 72px; margin: 20px 0; }
.stats { margin: 20px 0; }
button { padding: 10px 20px; font-size: 16px; }
.pending { opacity: 0.5; }
.log { margin-top: 20px; padding: 10px; background: #f0f0f0; }
</style>
</head>
<body>
<h1>🎲 RISE VRF Dice Game</h1>
<div id="dice" class="dice">?</div>
<div class="stats">
<p>Current Streak: <span id="streak">0</span></p>
<p>Top Streak: <span id="topStreak">0</span></p>
</div>
<button id="rollBtn" onclick="rollDice()">Roll Dice</button>
<div id="log" class="log"></div>
<script type="module" src="app.js"></script>
</body>
</html>
Monitor VRF Events
Create monitor.ts
to track dice roll events:
monitor.ts
import { ethers } from 'ethers';
const wsUrl = 'wss://testnet.riselabs.xyz/ws';
const diceGameAddress = '0x...'; // Your deployed contract
// Event signatures
const DICE_ROLL_COMPLETED = ethers.id('DiceRollCompleted(address,uint256,uint256,uint256,uint256)');
const DICE_ROLL_REQUESTED = ethers.id('DiceRollRequested(address,uint256)');
const ws = new WebSocket(wsUrl);
ws.onopen = () => {
// Subscribe to contract events
ws.send(JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'eth_subscribe',
params: ['logs', { address: diceGameAddress }]
}));
};
ws.onmessage = (event) => {
const response = JSON.parse(event.data);
if (response.method === 'eth_subscription') {
const log = response.params.result;
const eventTopic = log.topics[0];
if (eventTopic === DICE_ROLL_COMPLETED) {
// Decode event data
const playerAddress = '0x' + log.topics[1].slice(26);
const requestId = BigInt(log.topics[2]).toString();
const [result, currentStreak, topStreak] = ethers.AbiCoder.defaultAbiCoder().decode(
['uint256', 'uint256', 'uint256'],
log.data
);
console.log(`Player ${playerAddress} rolled ${result}`);
console.log(`Streak: ${currentStreak}, Top: ${topStreak}`);
}
}
};
Testing the Game
- Open the Application: Load
index.html
in your browser - Connect Wallet: Ensure you have RISE testnet tokens
- Roll the Dice: Click the button and watch the instant result
- Track Streaks: Try to roll consecutive sixes!
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
DiceRollCompleted
event - 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
Resources
- Smart Contract Guide - Detailed integration
- Real-time Tracking - Advanced WebSocket usage