# Session Keys (/docs/cookbook/rise-slots/session-keys)

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

## What Are Session Keys?

Session keys are temporary signing keys that you grant limited permissions to. Instead of asking for wallet approval on every spin, users:

1. Grant permissions once (approve session key for 7 days)
2. App signs transactions locally with the session key
3. All future spins are completely gasless and instant - no popups!

This is perfect for gaming where you want rapid, uninterrupted gameplay.

<Callout type="info">
  **Security**: Session keys have scoped permissions. You control exactly which functions they can call and how much they can spend. If stolen, attackers are limited to those permissions.
</Callout>

## How Session Keys Work

```
┌─────────────┐
│   User      │
│  (Wallet)   │
└──────┬──────┘
       │ 1. Grant permissions (ONE popup)
       ▼
┌─────────────────────────────────┐
│  On-chain Permission Registry   │
│  • Public Key: 0xabc...         │
│  • Can call: spin(), faucet()   │
│  • Can spend: 10k RCT/hour      │
│  • Expires: 7 days              │
└─────────────────────────────────┘
       │
       │ 2. App stores private key in localStorage
       ▼
┌─────────────────────────────────┐
│  Frontend Application           │
│  • Private key: localStorage    │
│  • Signs all spins locally      │
│  • NO MORE POPUPS!              │
└─────────────────────────────────┘
```

## Implementation

<Steps>
  <Step>
    ### Session Key State Management

    Add session key state to your main page (`src/app/page.tsx`):

    ```typescript
    "use client"

    import { useState, useEffect, useCallback, useRef } from 'react'
    import { useConnection, useConnectors, useChainId } from 'wagmi'
    import { Hex, P256, PublicKey, Signature } from 'ox'
    import { Hooks } from 'rise-wallet/wagmi'
    import { encodeFunctionData, parseEther } from 'viem'
    import { SLOT_MACHINE_ADDRESS, RCT_TOKEN_ADDRESS, SLOT_MACHINE_ABI, RCT_ABI } from '@/constants'

    // Storage key for session keys (per address)
    function storageKey(address: string) {
      return `rise_slots_${address}_session_key`
    }

    export default function Home() {
      const { address, isConnected, connector } = useConnection()
      const chainId = useChainId()

      // Session key state
      const [sessionPrivateKey, setSessionPrivateKey] = useState<string | null>(null)
      const [sessionPublicKey, setSessionPublicKey] = useState<string | null>(null)

      // RISE Wallet session key hooks
      const grantPermissions = Hooks.useGrantPermissions()
      const revokePermissions = Hooks.useRevokePermissions()
      const { data: permissions, refetch: refetchPermissions } = Hooks.usePermissions()

      // Find active session permission
      const activePermission = permissions?.find(
        (p) => sessionPublicKey && p.key.publicKey === sessionPublicKey && p.expiry > Math.floor(Date.now() / 1000)
      )
      const hasSession = !!activePermission && !!sessionPrivateKey

      // Load session key from localStorage on mount
      useEffect(() => {
        if (!address) return
        const pk = localStorage.getItem(storageKey(address))
        if (pk) {
          setSessionPrivateKey(pk)
          setSessionPublicKey(PublicKey.toHex(P256.getPublicKey({ privateKey: pk as `0x${string}` }), { includePrefix: false }))
        }
      }, [address])

      // Refetch permissions when connected
      useEffect(() => {
        if (isConnected && connector) {
          refetchPermissions()
        }
      }, [isConnected, connector, refetchPermissions])

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

    **Key Concepts**:

    * Session keys are P256 elliptic curve keys (not secp256k1 like Ethereum)
    * Private key stored in localStorage (per address)
    * Public key registered on-chain with permissions
    * `hasSession` checks: key exists + permissions exist + not expired
  </Step>

  <Step>
    ### Create Session Key Function

    Add the session creation function:

    ```typescript
    // Create session key
    const createSession = async () => {
      // Generate P256 key pair
      const privateKey = P256.randomPrivateKey()
      const publicKey = PublicKey.toHex(P256.getPublicKey({ privateKey }), { includePrefix: false })

      // Grant on-chain permissions for spin(), mintInitial(), and faucet()
      await grantPermissions.mutateAsync({
        connector,
        key: { publicKey, type: "p256" },
        expiry: Math.floor(Date.now() / 1000) + (86400 * 7), // 7 days
        feeToken: null,
        permissions: {
          calls: [
            { to: SLOT_MACHINE_ADDRESS, signature: "0xf0acd7d5" }, // spin()
            { to: RCT_TOKEN_ADDRESS, signature: "0x0d16e21a" }, // mintInitial()
            { to: RCT_TOKEN_ADDRESS, signature: "0xde5f72fd" }, // faucet()
          ],
          spend: [
            { token: RCT_TOKEN_ADDRESS, limit: parseEther("10000"), period: "hour" },
          ],
        },
      })

      localStorage.setItem(storageKey(address!), privateKey)
      setSessionPrivateKey(privateKey)
      setSessionPublicKey(publicKey)
      refetchPermissions()
    }
    ```

    **Permission Breakdown**:

    * **calls**: Which functions the session key can call
      * `spin()`: Main gameplay function
      * `mintInitial()`: One-time RCT claim
      * `faucet()`: Refill when broke

    * **spend**: Token spending limits
      * Max 10,000 RCT per hour
      * Prevents abuse if session key is compromised

    * **expiry**: Session key valid for 7 days
      * Automatic expiration for security
      * Users can revoke earlier if needed

    <Callout type="info">
      **Function Signatures**: Use `cast sig "functionName()"` to get function signatures. For example: `cast sig "spin()"` returns `0xf0acd7d5`.
    </Callout>
  </Step>

  <Step>
    ### Revoke Session Key Function

    Add the ability to revoke sessions:

    ```typescript
    // Revoke session key
    const revokeSession = async () => {
      if (!activePermission) return
      await revokePermissions.mutateAsync({ connector, id: activePermission.id })
      localStorage.removeItem(storageKey(address!))
      setSessionPrivateKey(null)
      setSessionPublicKey(null)
      refetchPermissions()
    }
    ```

    This lets users revoke permissions if:

    * They want to stop playing
    * They suspect their session key was compromised
    * They want to create a new session with different permissions
  </Step>

  <Step>
    ### Send Transactions with Session Keys

    Add the core function that signs transactions with session keys:

    ```typescript
    // Send transaction with session key
    const sendWithSessionKey = useCallback(async (calls: { to: string; data: string }[]) => {
      const privateKey = sessionPrivateKey! as `0x${string}`
      const publicKey = sessionPublicKey!
      const provider = (await connector!.getProvider()) as any

      // Prepare the calls (get digest to sign)
      const { digest, capabilities, ...request } = await provider.request({
        method: "wallet_prepareCalls",
        params: [{
          calls,
          chainId: Hex.fromNumber(chainId),
          from: address,
          atomicRequired: true,  // All calls must succeed or all fail
          key: { publicKey, type: "p256" },
        }],
      })

      // Sign the digest with session private key
      const signature = Signature.toHex(
        P256.sign({ payload: digest as `0x${string}`, privateKey })
      )

      // Send the signed calls
      const result = await provider.request({
        method: "wallet_sendPreparedCalls",
        params: [{ ...request, ...(capabilities ? { capabilities } : {}), signature }],
      })

      // Get transaction hash from result
      const id = Array.isArray(result) ? result[0].id : result.id
      const status = await provider.request({
        method: "wallet_getCallsStatus",
        params: [id],
      })

      if (status.status !== 200) {
        throw new Error(`Call failed: status ${status.status}`)
      }

      return status.receipts[0].transactionHash
    }, [address, chainId, connector, sessionPrivateKey, sessionPublicKey])
    ```

    **Flow Explained**:

    1. **Prepare Calls**: RISE Wallet prepares the transaction and returns a digest
    2. **Sign Locally**: App signs digest with session private key (P256.sign)
    3. **Send Prepared**: RISE Wallet executes the pre-signed transaction
    4. **Get Receipt**: Wait for transaction confirmation

    This all happens **without any wallet popups** - signing is done locally with your session key!
  </Step>

  <Step>
    ### Example: Claim RCT Function

    Here's how to use session keys to claim RCT tokens:

    ```typescript
    const claimRCT = async () => {
      if (!hasSession) throw new Error("No session key")

      try {
        setIsClaimingRCT(true)
        const hash = await sendWithSessionKey([{
          to: RCT_TOKEN_ADDRESS,
          data: encodeFunctionData({ abi: RCT_ABI, functionName: "mintInitial", args: [] }),
        }])

        console.log('Claim RCT transaction:', hash)

        // Wait a bit then refetch balances
        await new Promise(resolve => setTimeout(resolve, 1000))
        refetchRCT()
        refetchHasMinted()
      } finally {
        setIsClaimingRCT(false)
      }
    }
    ```

    **No Wallet Popup!** The transaction is signed locally and executed instantly.
  </Step>

  <Step>
    ### Example: Faucet Function

    Similarly for the faucet:

    ```typescript
    const useFaucet = async () => {
      if (!hasSession) throw new Error("No session key")

      try {
        setIsClaimingRCT(true)
        const hash = await sendWithSessionKey([{
          to: RCT_TOKEN_ADDRESS,
          data: encodeFunctionData({ abi: RCT_ABI, functionName: "faucet", args: [] }),
        }])

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

        await new Promise(resolve => setTimeout(resolve, 1000))
        refetchRCT()
      } finally {
        setIsClaimingRCT(false)
      }
    }
    ```
  </Step>
</Steps>

## Security Considerations

### Limited Scope

Session keys can only:

* Call specific functions (spin, mintInitial, faucet)
* Spend up to 10k RCT per hour
* Work for 7 days before expiring

They **cannot**:

* Transfer ETH
* Call other contracts
* Spend unlimited tokens
* Work after expiration

### Client-Side Storage

Session private keys are stored in localStorage:

* ✅ Convenient for users (no re-authorization)
* ✅ Cleared when browser cache is cleared
* ⚠️ Lost if user clears browser data

### Revocation

Users can revoke session keys at any time:

```typescript
await revokePermissions.mutateAsync({ connector, id: activePermission.id })
```

This removes on-chain permissions, making the session key useless even if someone still has the private key.

## UI Integration

Show session key status to users:

```typescript
{!hasSession ? (
  <button onClick={createSession} disabled={grantPermissions.isPending}>
    {grantPermissions.isPending ? "Creating Session..." : "Create Session Key to Play"}
  </button>
) : (
  <div>
    <span>Session Active (Expires: {new Date(activePermission.expiry * 1000).toLocaleDateString()})</span>
    <button onClick={revokeSession}>Revoke</button>
  </div>
)}
```

## Testing Session Keys

Test the flow:

1. **Create Session**: Click button → One wallet popup → Key created
2. **Claim RCT**: No popup, instant transaction
3. **Spin**: No popups, completely gasless
4. **Check Permissions**: Verify in wallet or on-chain
5. **Revoke**: Remove permissions, verify spins fail

## Next Steps

Session keys are set up! Next, we'll implement event watching with Shreds to capture VRF results in real-time and build the results caching pattern for ultra-fast UX.
