# Event Watching (/docs/cookbook/rise-slots/event-watching)

import { Steps, Step } from 'fumadocs-ui/components/steps';
import { Callout } from 'fumadocs-ui/components/callout';

## The Challenge: Ultra-Fast VRF

RISE VRF is **incredibly fast** - it fulfills randomness requests in 3-5 milliseconds. This creates a unique challenge:

The VRF result often arrives **before** your code finishes processing the transaction receipt! Traditional event watching patterns won't work.

## The Solution: Results Caching

We'll use a pattern called **results caching**:

1. Start watching for SpinResult events **before** sending the transaction
2. Cache ALL events as they arrive (in a Map by requestId)
3. After getting the requestId from the receipt, check if result is already cached
4. If cached: return immediately (most common case)
5. If not cached: poll the cache every 100ms until result arrives

This gives users a **sub-millisecond** experience - results feel instant!

<Callout type="info">
  **Why This Works**: Shreds uses WebSocket for real-time event streaming. Events arrive via push notification as soon as they're emitted, while receipt fetching is pull-based and slightly slower.
</Callout>

## Shreds Overview

Shreds is RISE's real-time event streaming library. Instead of polling every second (like traditional `eth_getLogs`), Shreds:

* Opens a WebSocket connection to RISE nodes
* Receives events as soon as they're emitted (push-based)
* Handles reconnection and error recovery automatically
* Filters events efficiently on the server side

## Implementation

<Steps>
  <Step>
    ### Create Shreds Client

    Add the Shreds client setup to your main page (`src/app/page.tsx`):

    ```typescript
    import { createPublicClient, webSocket, decodeEventLog } from 'viem'
    import { riseTestnet } from 'viem/chains'
    import { shredActions } from 'shreds/viem'
    import { SLOT_MACHINE_ADDRESS, SLOT_MACHINE_ABI } from '@/constants'

    // Create shreds client for event watching
    const shredClient = createPublicClient({
      chain: riseTestnet,
      transport: webSocket('wss://testnet.riselabs.xyz/ws'),
    }).extend(shredActions)
    ```

    **Key Points**:

    * Uses WebSocket transport (`wss://`) not HTTP
    * `.extend(shredActions)` adds `watchShreds` and `waitForTransactionReceipt` methods
    * This client is for read-only operations (watching events, getting receipts)
  </Step>

  <Step>
    ### Set Up Results Cache

    Add global cache and watcher to your component:

    ```typescript
    export default function Home() {
      // ... existing state ...

      // Global results cache for SpinResult events
      const resultsCache = useRef(new Map<string, { result: [number, number, number], payout: bigint }>()).current

      // Start watching for SpinResult events 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) => {
                try {
                  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

                    console.log('📦 Cached SpinResult for requestId:', requestId, 'result:', result, 'payout:', payout.toString())
                    resultsCache.set(requestId, { result, payout })
                  }
                } catch (error) {
                  console.error('Error decoding SpinResult:', error)
                }
              })
            })
          },
          onError: (error) => {
            console.error('Shred error:', error)
          },
        })

        return () => unwatch()
      }, [])

      // ... rest of component
    }
    ```

    **What's Happening**:

    1. **useRef for cache**: Persists across renders but doesn't trigger re-renders
    2. **watchShreds on mount**: Starts listening immediately when page loads
    3. **Event filtering**:
       * Check contract address matches
       * Check event signature matches SpinResult (`0xc95f6b2ec3fb7d188682755102792da57a8ec34918faaab4b1b925f0c4556123`)
    4. **Decode and cache**: Extract requestId, result, and payout, store in Map
    5. **Cleanup on unmount**: `unwatch()` stops the WebSocket connection

    <Callout type="info">
      **Event Signature**: Use `cast sig-event "SpinResult(uint256,uint8[3],uint256)"` to get the event signature. The first topic (topics\[0]) is always the event signature hash.
    </Callout>
  </Step>

  <Step>
    ### Implement Spin Handler with Cache Check

    Now implement the spin function that uses the cache:

    ```typescript
    const handleSpin = async (): Promise<[number, number, number]> => {
      if (!hasSession) throw new Error("No session key")

      // Send the spin request (watcher is already running from useEffect)
      const hash = await sendWithSessionKey([{
        to: SLOT_MACHINE_ADDRESS,
        data: encodeFunctionData({ abi: SLOT_MACHINE_ABI, functionName: "spin", args: [] }),
      }])

      console.log('Spin transaction:', hash)

      // Wait for tx receipt to get requestId
      const receipt = await shredClient.waitForTransactionReceipt({ hash: hash as `0x${string}` })

      // Find SpinRequested event to extract requestId
      const requestedLog = receipt.logs.find(log =>
        log.address.toLowerCase() === SLOT_MACHINE_ADDRESS.toLowerCase() &&
        log.topics[0] === '0x616d9204a230e2286a32eb3295c697372bd4cc093a7de2ef2455df4bffbc97c9' // SpinRequested
      )

      if (!requestedLog) throw new Error('SpinRequested event not found')

      const requestedEvent = decodeEventLog({
        abi: SLOT_MACHINE_ABI,
        data: requestedLog.data,
        topics: requestedLog.topics,
      })

      const myRequestId = requestedEvent.args.requestId.toString()
      console.log('📍 My requestId:', myRequestId)

      // Check if result already in cache (VERY likely!)
      if (resultsCache.has(myRequestId)) {
        console.log('✅ Result already in cache!')
        return resultsCache.get(myRequestId)!.result
      }

      // Wait for result to arrive (rare case - only if VRF was slower than receipt)
      const spinData = await new Promise<{ result: [number, number, number], payout: bigint }>((resolve, reject) => {
        const timeout = setTimeout(() => {
          reject(new Error('Timeout'))
        }, 10000)

        const checkInterval = setInterval(() => {
          if (resultsCache.has(myRequestId!)) {
            console.log('✅ Result arrived!')
            clearTimeout(timeout)
            clearInterval(checkInterval)
            resolve(resultsCache.get(myRequestId!)!)
          }
        }, 100) // Check every 100ms
      })

      return spinData.result
    }
    ```

    **Flow Breakdown**:

    1. **Send spin transaction** via session key (gasless)
    2. **Wait for receipt** from Shreds client (uses WebSocket, very fast)
    3. **Extract requestId** from SpinRequested event in the receipt
    4. **Check cache first** - 90%+ of the time, result is already there!
    5. **Poll cache if needed** - Check every 100ms for up to 10 seconds (rarely needed)
  </Step>

  <Step>
    ### Understanding the Flow

    Here's what actually happens in practice:

    ```
    - User clicks SPIN
    - Transaction sent via session key
    - VRF fulfills, SpinResult event emitted
    - Shreds WebSocket receives event, caches it
    - waitForTransactionReceipt returns
    - Extract requestId from receipt
    - Check cache → Result is there! ✅
    - UI updates with result
    ```

    The result is cached at 3ms, but we don't know the requestId until 6ms. That's why we need the cache!

    **Without caching**:

    ```
    - User clicks SPIN
    - Get receipt and requestId
    - Start watching for SpinResult event
    - Wait for event... (might have already missed it!)
    ```
  </Step>
</Steps>

## Event Signatures Reference

You'll need these event signatures for filtering:

```bash
# Get SpinResult signature
cast sig-event "SpinResult(uint256,uint8[3],uint256)"
# Output: 0xc95f6b2ec3fb7d188682755102792da57a8ec34918faaab4b1b925f0c4556123

# Get SpinRequested signature
cast sig-event "SpinRequested(uint256,address)"
# Output: 0x616d9204a230e2286a32eb3295c697372bd4cc093a7de2ef2455df4bffbc97c9
```

Hardcode these in your filtering logic for better performance.

## Advanced: Handling Multiple Users

The current implementation caches ALL SpinResult events from ALL users. This is fine because:

1. Each result is keyed by unique requestId
2. We only look up our specific requestId
3. Old entries can be garbage collected periodically

If you want to clean up old cache entries:

```typescript
// Clean cache entries older than 1 minute
const cleanCache = () => {
  const now = Date.now()
  const maxAge = 60000 // 1 minute

  // Store cache with timestamps
  resultsCache.forEach((value, key) => {
    if (now - value.timestamp > maxAge) {
      resultsCache.delete(key)
    }
  })
}

// Run cleanup every 30 seconds
useEffect(() => {
  const interval = setInterval(cleanCache, 30000)
  return () => clearInterval(interval)
}, [])
```

## Debugging Tips

### Check if Events Are Being Received

```typescript
onShred: (shred) => {
  console.log('🔵 Shred received at', new Date().toISOString())
  console.log('  Transactions:', shred.transactions.length)

  shred.transactions.forEach((tx) => {
    console.log('  TX:', tx.hash)
    console.log('  Logs:', tx.logs.length)
  })

  // ... rest of handler
}
```

### Log Cache State

```typescript
// After caching
resultsCache.set(requestId, { result, payout })
console.log('Cache size:', resultsCache.size)
console.log('Cache keys:', Array.from(resultsCache.keys()))
```

### Test with Delay

To verify polling works (when result isn't already cached):

```typescript
// Artificially delay cache lookup
await new Promise(resolve => setTimeout(resolve, 100))
if (resultsCache.has(myRequestId)) {
  // ...
}
```

## Next Steps

Event watching is complete! Next, we'll build the frontend UI with animated slot reels, win detection, and integrate everything together.
