Real-time Event Tracking
Get instant notifications when VRF results are ready using WebSocket subscriptions or the shreds npm module.
Why Real-time Tracking?
Traditional blockchain event monitoring requires polling, which is slow and inefficient. RISE Chain's shred-based architecture enables:
- Instant Updates: Get VRF results in 3-5ms
- No Polling: Push-based notifications via WebSocket
- Lower Latency: Direct shred subscriptions bypass block confirmations
- Better UX: Update UI immediately when randomness is ready
Using the Shreds Module
The easiest way to track VRF events is with the shreds
npm module.
Installation
npm install shreds viem
Watch Contract Events
import { createPublicClient, webSocket } from 'viem';
import { watchContractEvent } from 'shreds/viem';
const client = createPublicClient({
chain: riseTestnet,
transport: webSocket('wss://testnet.riselabs.xyz/ws')
});
// Your VRF contract ABI
const vrfAbi = [
{
type: 'event',
name: 'DiceRollRequested',
inputs: [
{ name: 'player', type: 'address', indexed: true },
{ name: 'requestId', type: 'uint256', indexed: true }
]
},
{
type: 'event',
name: 'DiceRollCompleted',
inputs: [
{ name: 'player', type: 'address', indexed: true },
{ name: 'requestId', type: 'uint256', indexed: true },
{ name: 'result', type: 'uint256', indexed: false },
{ name: 'currentStreak', type: 'uint256', indexed: false },
{ name: 'topStreak', type: 'uint256', indexed: false }
]
}
] as const;
// Watch for all VRF events
const unsubscribe = client.watchContractEvent({
abi: vrfAbi,
address: '0x...', // Your VRF consumer contract
onLogs: (logs) => {
logs.forEach((log) => {
console.log(`${log.eventName}:`, log.args);
if (log.eventName === 'DiceRollCompleted') {
updateUI(log.args.result, log.args.currentStreak);
}
});
}
});
// Watch specific event only
const unsubscribeRolls = client.watchContractEvent({
abi: vrfAbi,
eventName: 'DiceRollCompleted',
address: '0x...',
onLogs: (logs) => {
logs.forEach((log) => {
console.log('Roll result:', log.args.result);
});
}
});
Direct WebSocket Subscription
For more control, use direct WebSocket connections with eth_subscribe
.
Basic Setup
const ws = new WebSocket('wss://testnet.riselabs.xyz/ws');
ws.onopen = () => {
// Subscribe to logs from your VRF contract
ws.send(JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'eth_subscribe',
params: ['logs', {
address: '0x...', // Your VRF consumer contract
topics: [] // Optional: filter by specific events
}]
}));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
// Subscription confirmation
if (msg.id === 1) {
console.log('Subscribed with ID:', msg.result);
}
// Incoming events (from shreds)
if (msg.method === 'rise_subscription') {
handleVRFEvent(msg.params.result);
}
};
Event Filtering
Filter for specific VRF events using topic hashes:
import { ethers } from 'ethers';
// Calculate event signatures
const DICE_REQUESTED = ethers.id('DiceRollRequested(address,uint256)');
const DICE_COMPLETED = ethers.id('DiceRollCompleted(address,uint256,uint256,uint256,uint256)');
// Subscribe with topic filter
ws.send(JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'eth_subscribe',
params: ['logs', {
address: '0x...',
topics: [
[DICE_REQUESTED, DICE_COMPLETED] // Match either event
]
}]
}));
Decoding Event Data
import { decodeEventLog } from 'viem';
function handleVRFEvent(log) {
try {
const decoded = decodeEventLog({
abi: vrfAbi,
data: log.data,
topics: log.topics
});
console.log('Event:', decoded.eventName);
console.log('Args:', decoded.args);
switch (decoded.eventName) {
case 'DiceRollRequested':
showPendingState(decoded.args.requestId);
break;
case 'DiceRollCompleted':
showResult(decoded.args);
break;
}
} catch (error) {
console.error('Failed to decode event:', error);
}
}
React Integration
Custom Hook for VRF Events
import { useEffect, useState, useCallback } from 'react';
import { createPublicClient, webSocket } from 'viem';
interface VRFResult {
requestId: bigint;
result: number;
timestamp: number;
}
export function useVRFEvents(contractAddress: `0x${string}`) {
const [isConnected, setIsConnected] = useState(false);
const [lastResult, setLastResult] = useState<VRFResult | null>(null);
const [pending, setPending] = useState<Set<bigint>>(new Set());
useEffect(() => {
const client = createPublicClient({
chain: riseTestnet,
transport: webSocket('wss://testnet.riselabs.xyz/ws', {
reconnect: true,
reconnectDelay: 1000
})
});
// Watch for request events
const unsubRequests = client.watchContractEvent({
abi: vrfAbi,
address: contractAddress,
eventName: 'DiceRollRequested',
onLogs: (logs) => {
logs.forEach(log => {
setPending(prev => new Set(prev).add(log.args.requestId));
});
}
});
// Watch for completion events
const unsubCompletions = client.watchContractEvent({
abi: vrfAbi,
address: contractAddress,
eventName: 'DiceRollCompleted',
onLogs: (logs) => {
logs.forEach(log => {
setPending(prev => {
const next = new Set(prev);
next.delete(log.args.requestId);
return next;
});
setLastResult({
requestId: log.args.requestId,
result: Number(log.args.result),
timestamp: Date.now()
});
});
}
});
setIsConnected(true);
return () => {
unsubRequests();
unsubCompletions();
setIsConnected(false);
};
}, [contractAddress]);
return {
isConnected,
lastResult,
isPending: pending.size > 0,
pendingRequests: Array.from(pending)
};
}
Component Example
function DiceGame() {
const { isConnected, lastResult, isPending } = useVRFEvents(CONTRACT_ADDRESS);
const [history, setHistory] = useState<number[]>([]);
useEffect(() => {
if (lastResult) {
setHistory(prev => [...prev.slice(-9), lastResult.result]);
}
}, [lastResult]);
return (
<div>
<div className="status">
{isConnected ? '🟢 Connected' : '🔴 Disconnected'}
</div>
<div className="dice">
{isPending ? '🎲' : lastResult ? `${lastResult.result}` : '?'}
</div>
<div className="history">
{history.map((roll, i) => (
<span key={i} className="roll">{roll}</span>
))}
</div>
<button onClick={rollDice} disabled={isPending}>
{isPending ? 'Rolling...' : 'Roll Dice'}
</button>
</div>
);
}
Advanced Patterns
Multi-Contract Monitoring
Track VRF events from multiple contracts:
const contracts = [
{ address: '0x...', name: 'DiceGame' },
{ address: '0x...', name: 'Lottery' },
{ address: '0x...', name: 'NFTMint' }
];
contracts.forEach(({ address, name }) => {
client.watchContractEvent({
abi: vrfAbi,
address,
onLogs: (logs) => {
logs.forEach(log => {
console.log(`[${name}] ${log.eventName}:`, log.args);
});
}
});
});
Event Aggregation
Aggregate VRF statistics in real-time:
class VRFStats {
private totalRequests = 0;
private totalFulfilled = 0;
private avgResponseTime = 0;
private pendingRequests = new Map<bigint, number>();
onRequest(requestId: bigint) {
this.totalRequests++;
this.pendingRequests.set(requestId, Date.now());
}
onFulfillment(requestId: bigint) {
const startTime = this.pendingRequests.get(requestId);
if (startTime) {
const responseTime = Date.now() - startTime;
this.avgResponseTime =
(this.avgResponseTime * this.totalFulfilled + responseTime) /
(this.totalFulfilled + 1);
this.totalFulfilled++;
this.pendingRequests.delete(requestId);
}
}
getStats() {
return {
totalRequests: this.totalRequests,
totalFulfilled: this.totalFulfilled,
pending: this.pendingRequests.size,
avgResponseTime: Math.round(this.avgResponseTime)
};
}
}
Reconnection Handling
Robust WebSocket connection management:
class ReliableVRFSubscription {
constructor(url, contractAddress) {
this.url = url;
this.contractAddress = contractAddress;
this.subscriptionId = null;
this.reconnectDelay = 1000;
this.maxReconnectDelay = 30000;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('Connected to RISE WebSocket');
this.reconnectDelay = 1000;
this.subscribe();
};
this.ws.onclose = () => {
console.log('Disconnected, reconnecting...');
setTimeout(() => this.connect(), this.reconnectDelay);
this.reconnectDelay = Math.min(
this.reconnectDelay * 2,
this.maxReconnectDelay
);
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
this.ws.onmessage = (event) => {
this.handleMessage(JSON.parse(event.data));
};
}
subscribe() {
this.ws.send(JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'eth_subscribe',
params: ['logs', { address: this.contractAddress }]
}));
}
handleMessage(msg) {
if (msg.id === 1) {
this.subscriptionId = msg.result;
} else if (msg.method === 'rise_subscription') {
this.onEvent(msg.params.result);
}
}
onEvent(log) {
// Override this method
}
}
Performance Tips
- Use Shreds Module: Optimized for RISE Chain's architecture
- Filter Events: Only subscribe to events you need
- Batch Updates: Aggregate multiple events before UI updates
- Debounce: Prevent UI thrashing with rapid events
- Memory Management: Clean up old event data periodically
Troubleshooting
No Events Received
- Verify contract address is correct
- Check event signatures match ABI
- Ensure WebSocket connection is established
- Confirm contract is emitting events
Delayed Events
- Check network connectivity
- Verify WebSocket URL is correct
- Monitor subscription status
Connection Drops
- Implement automatic reconnection
- Store subscription state for recovery
- Use exponential backoff for retries