Skip to content

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

  1. Use Shreds Module: Optimized for RISE Chain's architecture
  2. Filter Events: Only subscribe to events you need
  3. Batch Updates: Aggregate multiple events before UI updates
  4. Debounce: Prevent UI thrashing with rapid events
  5. 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