# Backend API (/docs/cookbook/vrf-rock-paper-scissors/backend)

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

## Backend Architecture

The backend serves as the game orchestrator. It:

1. Receives player moves from the frontend
2. Calls the smart contract's `request()` function (as the sponsor)
3. Watches for VRF fulfillment events with 5ms polling
4. Determines the winner
5. Sends ETH rewards to winners
6. Returns game results to the frontend

<Callout type="info">
  **Why a backend?** This pattern lets us focus on VRF, event watching and automated reward distribution.
</Callout>

## API Implementation

<Steps>
  <Step>
    ### Create API Route

    Create `src/app/api/route.ts`:

    ```typescript
    import { NextRequest, NextResponse } from 'next/server'
    import { createWalletClient, http, createPublicClient, parseAbiItem, decodeEventLog } from 'viem'
    import { privateKeyToAccount } from 'viem/accounts'
    import { riseTestnet } from 'viem/chains'
    import { RPS_ADDRESS, ABI } from '@/constants'

    const sponsor = privateKeyToAccount(process.env.DEV_PRIVATE_KEY! as `0x${string}`)

    const walletClient = createWalletClient({
      chain: riseTestnet,
      transport: http(),
      account: sponsor
    })

    const publicClient = createPublicClient({
      chain: riseTestnet,
      transport: http()
    })

    export async function POST(request: NextRequest) {
      try {
        const body = await request.json()
        const { address, choice } = body

        // Validate inputs
        if (!address) {
          return NextResponse.json(
            { error: 'Player address is required' },
            { status: 400 }
          )
        }

        if (!choice) {
          return NextResponse.json(
            { error: 'No choice given' },
            { status: 400 }
          )
        }

        console.log('Game started - Player:', address, 'Choice:', choice)

        // Call request function
        console.log('Requesting VRF...')
        const hash = await walletClient.writeContract({
          address: RPS_ADDRESS as `0x${string}`,
          abi: ABI,
          functionName: 'request',
          args: [address as `0x${string}`]
        })

        console.log('Request transaction sent:', hash)

        // Wait for transaction receipt
        const receipt = await publicClient.waitForTransactionReceipt({ hash })

        if (receipt.status === 'reverted') {
          console.error('Request transaction reverted')
          return NextResponse.json(
            { error: 'Transaction reverted - player may not have tickets' },
            { status: 400 }
          )
        }

        console.log('Request transaction confirmed')

        // Parse ChoiceRequested event to get request ID
        const choiceRequestedEvent = receipt.logs.find(log => {
          try {
            const decoded = decodeEventLog({
              abi: ABI,
              data: log.data,
              topics: log.topics
            })
            return decoded.eventName === 'ChoiceRequested'
          } catch {
            return false
          }
        })

        if (!choiceRequestedEvent) {
          console.error('ChoiceRequested event not found in transaction logs')
          throw new Error('ChoiceRequested event not found')
        }

        const decodedChoiceRequested = decodeEventLog({
          abi: ABI,
          data: choiceRequestedEvent.data,
          topics: choiceRequestedEvent.topics
        })

        //@ts-ignore
        const requestId = decodedChoiceRequested!.args!.requestId

        console.log('VRF Request ID:', requestId.toString())
        console.log('Waiting for VRF result...')

        // Watch for ChoiceResult event with matching request ID
        const result = await new Promise<number>((resolve, reject) => {
          const timeout = setTimeout(() => {
            unwatch()
            console.error('Timeout waiting for VRF result')
            reject(new Error('Timeout waiting for VRF result (30s)'))
          }, 30000) // 30 second timeout

          const unwatch = publicClient.watchEvent({
            address: RPS_ADDRESS as `0x${string}`,
            event: parseAbiItem('event ChoiceResult(uint256 indexed requestId, uint256 result)'),
            pollingInterval: 5,
            onLogs: (logs) => {
              logs.forEach(log => {
                if (log.args.requestId === requestId) {
                  console.log('VRF Result received:', log.args.result)
                  clearTimeout(timeout)
                  unwatch()
                  resolve(Number(log.args.result))
                }
              })
            }
          })
        })

        const aiChoiceMap = ["rock", "paper", "scissors"]
        console.log('AI chose:', aiChoiceMap[result])

        // Determine if player won and send reward
        const playerWon = determineWin(choice, result)

        if (playerWon) {
          console.log('Player won! Sending reward...')
          try {
            const rewardHash = await walletClient.sendTransaction({
              to: address as `0x${string}`,
              value: BigInt(2000000000000000) // 0.002 ETH
            })

            const rewardReceipt = await publicClient.waitForTransactionReceipt({ hash: rewardHash })

            if (rewardReceipt.status === 'reverted') {
              console.error('Reward transaction reverted')
              return NextResponse.json(
                { error: 'Failed to send reward - transaction reverted' },
                { status: 500 }
              )
            }

            console.log('Reward sent:', rewardHash)
          } catch (rewardError) {
            console.error('Failed to send reward:', rewardError)
            return NextResponse.json(
              { error: 'Game completed but reward failed to send' },
              { status: 500 }
            )
          }
        } else {
          console.log('Player lost or tied')
        }

        return NextResponse.json({
          result,
          requestId: Number(requestId),
          playerChoice: choice,
          success: true
        })

      } catch (error) {
        console.error('API Error:', error)

        // More specific error messages
        if (error instanceof Error) {
          if (error.message.includes('Timeout')) {
            return NextResponse.json(
              { error: 'VRF request timed out - please try again' },
              { status: 408 }
            )
          }
          if (error.message.includes('reverted')) {
            return NextResponse.json(
              { error: 'Transaction failed - check if you have tickets' },
              { status: 400 }
            )
          }
        }

        return NextResponse.json(
          { error: 'Internal server error - check console logs' },
          { status: 500 }
        )
      }
    }

    function determineWin(playerChoice: string, aiResult: number): boolean {
      const aiChoiceMap = ["rock", "paper", "scissors"]
      const aiChoice = aiChoiceMap[aiResult]

      if (playerChoice === aiChoice) return false // tie

      const winConditions = {
        rock: "scissors",
        paper: "rock",
        scissors: "paper"
      }

      return winConditions[playerChoice as keyof typeof winConditions] === aiChoice
    }
    ```
  </Step>

  <Step>
    ### Understanding the Flow

    **1. Request Initiation**:

    ```typescript
    const hash = await walletClient.writeContract({
      address: RPS_ADDRESS,
      abi: ABI,
      functionName: 'request',
      args: [address]
    })
    ```

    The sponsor wallet calls the contract's `request()` function on behalf of the player.

    **2. Parse Request ID**:

    ```typescript
    const choiceRequestedEvent = receipt.logs.find(log => {
      const decoded = decodeEventLog({ abi: ABI, data: log.data, topics: log.topics })
      return decoded.eventName === 'ChoiceRequested'
    })
    ```

    Extract the `requestId` from the `ChoiceRequested` event emitted by the contract.

    **3. Watch for VRF Result**:

    ```typescript
    const unwatch = publicClient.watchEvent({
      address: RPS_ADDRESS,
      event: parseAbiItem('event ChoiceResult(uint256 indexed requestId, uint256 result)'),
      pollingInterval: 5,  // Poll every 5ms for fast feedback
      onLogs: (logs) => {
        // Find matching requestId and resolve
      }
    })
    ```

    This uses viem's `watchEvent` to poll for the `ChoiceResult` event. The 5ms polling interval ensures we catch the VRF result as soon as it's available.

    **4. Game Logic**:

    ```typescript
    function determineWin(playerChoice: string, aiResult: number): boolean {
      const aiChoiceMap = ["rock", "paper", "scissors"]
      const aiChoice = aiChoiceMap[aiResult]

      if (playerChoice === aiChoice) return false // tie

      const winConditions = {
        rock: "scissors",
        paper: "rock",
        scissors: "paper"
      }

      return winConditions[playerChoice as keyof typeof winConditions] === aiChoice
    }
    ```

    Standard rock-paper-scissors logic: rock beats scissors, paper beats rock, scissors beats paper.

    **5. Reward Distribution**:

    ```typescript
    if (playerWon) {
      const rewardHash = await walletClient.sendTransaction({
        to: address,
        value: BigInt(2000000000000000) // 0.002 ETH
      })
    }
    ```

    Winners automatically receive 0.002 ETH (double their 0.001 ETH ticket cost).
  </Step>

  <Step>
    ### Key Concepts

    **Polling Interval**:

    ```typescript
    pollingInterval: 5
    ```

    This is in **milliseconds**. We use 5ms for extremely fast feedback. Standard is 1000ms (1 second). RISE's fast confirmations make aggressive polling viable.

    **Event Filtering**:

    ```typescript
    if (log.args.requestId === requestId) {
      resolve(Number(log.args.result))
    }
    ```

    Multiple VRF requests may be in flight. We filter by `requestId` to match this specific game.

    **Timeout Handling**:

    ```typescript
    const timeout = setTimeout(() => {
      unwatch()
      reject(new Error('Timeout waiting for VRF result (30s)'))
    }, 30000)
    ```

    VRF should respond in 3-5ms, but we allow 30 seconds as a safety net for network issues.
  </Step>
</Steps>

## Error Handling

The API handles several error cases:

**No Tickets**:

```json
{
  "error": "Transaction reverted - player may not have tickets",
  "status": 400
}
```

**VRF Timeout**:

```json
{
  "error": "VRF request timed out - please try again",
  "status": 408
}
```

**Reward Failed**:

```json
{
  "error": "Game completed but reward failed to send",
  "status": 500
}
```

## Performance Optimization

**Why 5ms polling?**

RISE's shreds deliver confirmations in 3ms. With 5ms polling, we're guaranteed to catch the event in the first poll cycle after VRF fulfillment. This creates a nearly instant game experience.

## Next Steps

The backend is complete! Next, we'll build the frontend UI that connects the wallet, displays the game, and calls this API.

<Callout type="info">
  Make sure your sponsor wallet has enough testnet ETH to pay for VRF requests and rewards.
</Callout>
