Frontend
Build the animated slot machine UI with real-time updates and game flow
Frontend Architecture
The frontend brings together everything we've built: session keys for gasless gameplay, event watching with Shreds for instant results, and an animated UI built with Framer Motion. The UI dynamically adapts to the user's state, showing different buttons based on whether they're connected, have a session key, or need tokens.
Complete Source Code: The full implementation is available at github.com/awesamarth/rise-slots. This page covers the key concepts and patterns.
Key Components
SlotMachine Component
The main game UI handles animated reels, win detection, and dynamic button states. The most important pattern here is the symbol mapping system.
Symbol Mapping:
function getSymbol(index: number): string {
if (index <= 3) return '🍒' // Cherry (0-3)
if (index <= 6) return '🍋' // Lemon (4-6)
if (index <= 8) return '💎' // Diamond (7-8)
return '9️⃣' // Lucky 9 (9)
}This mapping is crucial for win detection. Instead of comparing raw numbers, we compare symbols:
const symbol0 = getSymbol(result[0])
const symbol1 = getSymbol(result[1])
const symbol2 = getSymbol(result[2])
const isWin = symbol0 === symbol1 && symbol1 === symbol2This ensures [0, 1, 2] correctly wins as triple cherries, since all three map to 🍒.
Reel Animation: Each reel spins independently at 80ms intervals, showing random symbols. When the VRF result arrives, reels stop one by one with a 200ms stagger for dramatic effect. Wins celebrate for 3 seconds with a banner, while losses return to idle immediately.
Main Page Integration
The main page (src/app/page.tsx) coordinates session keys, event watching, and game flow. Here's how the critical pieces work together.
Event Watching Setup runs on mount:
useEffect(() => {
const unwatch = shredClient.watchShreds({
includeStateChanges: false,
onShred: (shred) => {
shred.transactions.forEach((tx) => {
const spinResultLogs = tx.logs.filter(
(log) =>
log.address.toLowerCase() === SLOT_MACHINE_ADDRESS.toLowerCase() &&
log.topics[0] === '0xc95f6b2ec3fb7d188682755102792da57a8ec34918faaab4b1b925f0c4556123'
)
spinResultLogs.forEach((log) => {
const decoded = decodeEventLog({ abi: SLOT_MACHINE_ABI, data: log.data, topics: log.topics })
if (decoded.eventName === 'SpinResult') {
const requestId = decoded.args.requestId.toString()
const result = decoded.args.result as [number, number, number]
const payout = decoded.args.payout as bigint
resultsCache.set(requestId, { result, payout })
}
})
})
}
})
return () => unwatch()
}, [])The watcher runs continuously in the background, caching all SpinResult events as they arrive. This is what makes the UX feel instant.
Spin Handler leverages the cache:
const handleSpin = async (): Promise<[number, number, number]> => {
// Send spin transaction via session key (gasless!)
const hash = await sendWithSessionKey([{
to: SLOT_MACHINE_ADDRESS,
data: encodeFunctionData({ abi: SLOT_MACHINE_ABI, functionName: "spin", args: [] }),
}])
// Wait for receipt and extract requestId
const receipt = await shredClient.waitForTransactionReceipt({ hash: hash as `0x${string}` })
const requestedLog = receipt.logs.find(log =>
log.address.toLowerCase() === SLOT_MACHINE_ADDRESS.toLowerCase() &&
log.topics[0] === '0x616d9204a230e2286a32eb3295c697372bd4cc093a7de2ef2455df4bffbc97c9'
)
const requestedEvent = decodeEventLog({ abi: SLOT_MACHINE_ABI, data: requestedLog.data, topics: requestedLog.topics })
const myRequestId = requestedEvent.args.requestId.toString()
// Check cache - result is almost always already there!
if (resultsCache.has(myRequestId)) {
return resultsCache.get(myRequestId)!.result
}
// Fallback: poll cache if not found (rare)
return await new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('Timeout')), 10000)
const checkInterval = setInterval(() => {
if (resultsCache.has(myRequestId!)) {
clearTimeout(timeout)
clearInterval(checkInterval)
resolve(resultsCache.get(myRequestId!)!.result)
}
}, 100)
})
}The pattern is: send transaction, get requestId from receipt, check cache. In 90%+ of spins, the result is already cached by the time we check.
Token Management
Claiming RCT and using the faucet both use session keys for gasless transactions:
const claimRCT = async () => {
const hash = await sendWithSessionKey([{
to: RCT_TOKEN_ADDRESS,
data: encodeFunctionData({ abi: RCT_ABI, functionName: "mintInitial", args: [] }),
}])
await new Promise(resolve => setTimeout(resolve, 1000))
refetchRCT()
refetchHasMinted()
}
const useFaucet = async () => {
const hash = await sendWithSessionKey([{
to: RCT_TOKEN_ADDRESS,
data: encodeFunctionData({ abi: RCT_ABI, functionName: "faucet", args: [] }),
}])
await new Promise(resolve => setTimeout(resolve, 1000))
refetchRCT()
}Both functions are completely gasless thanks to session keys. No wallet popups, instant execution.
UI Flow States
The app shows different buttons based on user state:
- Not Connected: "Connect Wallet" button in place of spin button
- No Session Key: "Create Session Key to Play"
- No RCT: "Claim 1000 RCT"
- Broke (0 RCT): "Get More RCT (Broke? 😅)"
- Ready: "SPIN"
This progression ensures users are guided through setup before they can play. The SlotMachine component handles this through conditional rendering based on props.
Animation Details
The reel animation creates anticipation through timing. All three reels start spinning simultaneously with random symbols updating every 80ms. When VRF results arrive, reels stop sequentially with a 200ms delay between each stop. This staggered reveal builds tension and makes wins more satisfying.
Win celebrations use Framer Motion for smooth animations:
<AnimatePresence>
{gameState === 'result' && isWin && lastWin !== null && (
<motion.div
initial={{ scale: 0.8, opacity: 0, y: 20 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.8, opacity: 0 }}
>
<div className="bg-gradient-to-r from-[#4ade80]/20 via-[#4ade80]/30 to-[#4ade80]/20 border border-[#4ade80]/50 rounded-2xl p-6 text-center">
<div className="text-4xl font-black text-[#4ade80] mb-2">
+{getPayout(lastWin)} RCT!
</div>
<div className="text-zinc-400">
Triple {getSymbol(lastWin)} • Instant VRF Result
</div>
</div>
</motion.div>
)}
</AnimatePresence>The banner scales in, displays for 3 seconds, then fades out. Losses skip the celebration entirely and return to idle immediately.
Stats Tracking
The app tracks wins and losses throughout the session:
const [wins, setWins] = useState(0)
const [losses, setLosses] = useState(0)
const handleResult = (result: [number, number, number], isWin: boolean, payout: number) => {
if (isWin) {
setWins(prev => prev + 1)
} else {
setLosses(prev => prev + 1)
}
refetchRCT()
}These stats display in the sidebar along with calculated win rate. It's simple but effective for showing progress.
Complete Implementation
The GitHub repository contains the full frontend code including the complete SlotMachine component with all animations, the main page with all features integrated, navbar with wallet connection UI, and sidebar with stats and session key management.
Key files to explore:
src/components/SlotMachine.tsx- Main game componentsrc/app/page.tsx- Main page with all integrationssrc/app/layout.tsx- Root layout with providerssrc/app/globals.css- Custom animations
Visit github.com/awesamarth/rise-slots for the complete source.
Testing the Game
Start the development server with bun dev and test the complete flow: connect with RISE Wallet, create a session key (one popup), claim 1000 RCT tokens (gasless), then spin multiple times with zero popups. Experience the instant VRF results, win and see payouts, go broke and use the faucet.

Winning Results
When you hit a winning combination, payouts are instant thanks to session keys:

Watch the console to see the caching pattern in action:
📦 Cached SpinResult for requestId: 4685 result: [2,2,2] payout: 15000000000000000000
Spin transaction: 0x...
📍 My requestId: 4685
✅ Result already in cache!The result arrives and gets cached before we even finish extracting the requestId from the receipt.
Key Production Patterns
This tutorial demonstrates several production-ready patterns: results caching for handling race conditions, WebSocket event streaming for real-time updates, session key management with localStorage, responsive UI with proper loading states, comprehensive error handling with timeouts, and token balance tracking throughout gameplay.
The UX achievements include sub-millisecond perceived latency, zero wallet popups during gameplay, instant visual feedback, and smooth animations with a clear game flow that guides users through setup.
Next Steps
You've built a complete production-ready slot machine on RISE with VRF integration for provably fair randomness, session keys for gasless gameplay, Shreds for real-time event streaming, results caching for ultra-fast UX, and proper token economics.
Deploy to production by deploying contracts to RISE Mainnet, deploying the frontend to Vercel or Netlify, updating contract addresses, and testing end-to-end.
Consider enhancements like leaderboards for tracking top players and biggest wins, daily free spins to encourage return visits, multiplayer tournaments for competitive play, NFT rewards for big wins, and an analytics dashboard for tracking game metrics.
Resources
- GitHub Repository - Complete source code
- RISE VRF Documentation - Learn more about Fast VRF
- Session Keys Guide - Deep dive into session keys
- Shreds Documentation - Real-time event streaming
- RISE Discord - Get support and share your game