# Session Key Flow (/docs/cookbook/rise-wallet-quickstart/session-key-flow)





import { Callout } from 'fumadocs-ui/components/callout';

## Implementing Session Key Flow

The session key flow enables background signing without popups by using pre-authorized permissions.

### Create the Session Key Page

```typescript title="src/app/session/page.tsx"
"use client";

import { useEffect, useState } from "react";
import { useChainId, useConnection, useReadContract } from "wagmi";
import { createWalletClient, custom, encodeFunctionData, parseEther } from "viem";
import { P256, PublicKey, Signature } from "ox";
import { Hooks } from "rise-wallet/wagmi";
import { Navbar } from "@/components/Navbar";
import {
  SIMPLE_TOKEN_ADDRESS,
  SIMPLE_TOKEN_ABI,
  COUNTER_ADDRESS,
  COUNTER_ABI,
} from "@/constants";

const SESSION_KEY_STORAGE = "rise-session-key";

export default function SessionKeyPage() {
  const [mounted, setMounted] = useState(false);
  const [sessionKey, setSessionKey] = useState<string>();
  const [txHash, setTxHash] = useState<string>();
  const [pendingAction, setPendingAction] = useState<string>();

  const { address, connector } = useConnection();
  const chainId = useChainId();

  // Read token balance
  const { data: balance, refetch: refetchBalance } = useReadContract({
    address: SIMPLE_TOKEN_ADDRESS,
    abi: SIMPLE_TOKEN_ABI,
    functionName: "balanceOf",
    args: address ? [address] : undefined,
    query: { enabled: !!address },
  });

  // Read counter value
  const { data: count, refetch: refetchCount } = useReadContract({
    address: COUNTER_ADDRESS,
    abi: COUNTER_ABI,
    functionName: "count",
    query: { enabled: !!address },
  });

  // Check for existing permissions
  const { data: permissions } = Hooks.usePermissions({
    query: { enabled: !!address },
  });

  // Grant permissions mutation
  const { mutateAsync: grantPermissions } = Hooks.useGrantPermissions();

  // Revoke permissions mutation
  const { mutateAsync: revokePermissions } = Hooks.useRevokePermissions();

  useEffect(() => {
    setMounted(true);
    const stored = localStorage.getItem(SESSION_KEY_STORAGE);
    if (stored) setSessionKey(stored);
  }, []);

  const createSessionKey = async () => {
    if (!address) return;

    try {
      // Generate P256 keypair
      const privateKey = P256.randomPrivateKey();
      const publicKey = P256.getPublicKey({ privateKey });
      const publicKeyHex = PublicKey.toHex(publicKey);

      // Grant permissions
      await grantPermissions({
        account: address,
        expiry: Date.now() + 24 * 60 * 60 * 1000, // 1 day
        permissions: [
          {
            type: "native-token-recurring-allowance",
            data: {
              allowance: parseEther("1"),
              start: Date.now(),
              period: 60 * 1000, // 1 minute
            },
          },
          {
            type: "allowed-contract-selector",
            data: {
              contract: SIMPLE_TOKEN_ADDRESS,
              selector: "0x40c10f19", // mint(address,uint256)
            },
          },
          {
            type: "allowed-contract-selector",
            data: {
              contract: SIMPLE_TOKEN_ADDRESS,
              selector: "0xa9059cbb", // transfer(address,uint256)
            },
          },
          {
            type: "allowed-contract-selector",
            data: {
              contract: COUNTER_ADDRESS,
              selector: "0xd09de08a", // increment()
            },
          },
        ],
        signer: {
          type: "key",
          data: {
            id: publicKeyHex,
          },
        },
      });

      // Store private key
      localStorage.setItem(SESSION_KEY_STORAGE, privateKey);
      setSessionKey(privateKey);
    } catch (error) {
      console.error("Failed to create session key:", error);
    }
  };

  const revokeSessionKey = async () => {
    if (!permissions?.[0]?.id) return;

    try {
      await revokePermissions({ id: permissions[0].id });
      localStorage.removeItem(SESSION_KEY_STORAGE);
      setSessionKey(undefined);
    } catch (error) {
      console.error("Failed to revoke session key:", error);
    }
  };

  const sendWithSessionKey = async (
    action: string,
    contractAddress: string,
    data: string
  ) => {
    if (!connector?.provider || !address || !sessionKey) return;

    try {
      setPendingAction(action);
      setTxHash(undefined);

      const walletClient = createWalletClient({
        account: address,
        chain: { id: chainId } as any,
        transport: custom(connector.provider),
      });

      // Prepare calls
      const prepareResult = await walletClient.request({
        method: "wallet_prepareCalls",
        params: [
          {
            chainId: `0x${chainId.toString(16)}`,
            from: address,
            calls: [
              {
                to: contractAddress,
                data,
              },
            ],
          },
        ],
      });

      // Sign with session key
      const signature = P256.sign({
        payload: prepareResult.prepareCallsDigest,
        privateKey: sessionKey as `0x${string}`,
      });

      // Send prepared calls
      const bundleId = await walletClient.request({
        method: "wallet_sendPreparedCalls",
        params: [
          {
            context: prepareResult.context,
            signature: Signature.toHex(signature),
          },
        ],
      });

      // Poll for status
      let status;
      do {
        await new Promise((resolve) => setTimeout(resolve, 500));
        status = await walletClient.request({
          method: "wallet_getCallsStatus",
          params: [bundleId],
        });
      } while (status.status === 100); // 100 = Pending

      if (status.status === 200 && status.receipts?.[0]?.transactionHash) {
        setTxHash(status.receipts[0].transactionHash);
      }

      // Refresh contract state
      refetchBalance();
      refetchCount();
    } catch (error) {
      console.error(`${action} failed:`, error);
    } finally {
      setPendingAction(undefined);
    }
  };

  const mintTokens = async () => {
    if (!address) return;
    const data = encodeFunctionData({
      abi: SIMPLE_TOKEN_ABI,
      functionName: "mint",
      args: [address, parseEther("1000")],
    });
    await sendWithSessionKey("Minting", SIMPLE_TOKEN_ADDRESS, data);
  };

  const spendTokens = async () => {
    if (!address) return;
    const data = encodeFunctionData({
      abi: SIMPLE_TOKEN_ABI,
      functionName: "transfer",
      args: ["0x0000000000000000000000000000000000000000", parseEther("5")],
    });
    await sendWithSessionKey("Spending", SIMPLE_TOKEN_ADDRESS, data);
  };

  const incrementCounter = async () => {
    const data = encodeFunctionData({
      abi: COUNTER_ABI,
      functionName: "increment",
    });
    await sendWithSessionKey("Incrementing", COUNTER_ADDRESS, data);
  };

  if (!mounted) return null;

  if (!address) {
    return (
      <div className="min-h-screen bg-black text-white">
        <Navbar />
        <div className="max-w-4xl mx-auto px-4 py-16 text-center">
          <p className="text-xl text-gray-400">
            Connect your wallet to use session key flow
          </p>
        </div>
      </div>
    );
  }

  if (!sessionKey || !permissions?.[0]) {
    return (
      <div className="min-h-screen bg-black text-white">
        <Navbar />
        <div className="max-w-4xl mx-auto px-4 py-16">
          <h1 className="text-4xl font-bold mb-8 text-green-500">
            Session Key Flow
          </h1>
          <div className="p-8 bg-gray-900 rounded-xl border border-gray-800 text-center">
            <div className="text-6xl mb-6">🔑</div>
            <h2 className="text-2xl font-bold mb-4">
              Create a Session Key
            </h2>
            <p className="text-gray-400 mb-6">
              Generate a local keypair and grant on-chain permissions for
              seamless transactions without popups
            </p>
            <button
              onClick={createSessionKey}
              className="px-8 py-4 bg-green-600 text-white font-semibold rounded-lg hover:bg-green-700 transition-colors"
            >
              Create Session Key
            </button>
          </div>
        </div>
      </div>
    );
  }

  const publicKey = P256.getPublicKey({ privateKey: sessionKey as `0x${string}` });

  return (
    <div className="min-h-screen bg-black text-white">
      <Navbar />
      <div className="max-w-4xl mx-auto px-4 py-16">
        <div className="flex items-center justify-between mb-8">
          <h1 className="text-4xl font-bold text-green-500">
            Session Key Flow
          </h1>
          <button
            onClick={revokeSessionKey}
            className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
          >
            Revoke Session
          </button>
        </div>

        <div className="p-4 bg-gray-900 rounded-lg border border-gray-800 mb-8">
          <p className="text-sm text-gray-400 mb-2">Session Public Key:</p>
          <p className="text-green-500 font-mono text-sm break-all">
            {PublicKey.toHex(publicKey)}
          </p>
        </div>

        <p className="text-gray-400 mb-8">
          Transactions are signed locally without popups
        </p>

        <div className="grid md:grid-cols-2 gap-6 mb-8">
          <div className="p-6 bg-gray-900 rounded-xl border border-gray-800">
            <h3 className="text-sm text-gray-400 mb-2">STK Balance</h3>
            <p className="text-3xl font-bold text-green-500">
              {balance ? (Number(balance) / 1e18).toLocaleString() : "0"}
            </p>
          </div>

          <div className="p-6 bg-gray-900 rounded-xl border border-gray-800">
            <h3 className="text-sm text-gray-400 mb-2">Counter Value</h3>
            <p className="text-3xl font-bold text-green-500">
              {count?.toString() || "0"}
            </p>
          </div>
        </div>

        <div className="space-y-4">
          <button
            onClick={mintTokens}
            disabled={!!pendingAction}
            className="w-full px-6 py-4 bg-green-600 text-white font-semibold rounded-lg hover:bg-green-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
          >
            {pendingAction === "Minting" ? "Minting..." : "Mint 1000 STK"}
          </button>

          <button
            onClick={spendTokens}
            disabled={!!pendingAction}
            className="w-full px-6 py-4 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
          >
            {pendingAction === "Spending" ? "Spending..." : "Spend 5 STK"}
          </button>

          <button
            onClick={incrementCounter}
            disabled={!!pendingAction}
            className="w-full px-6 py-4 bg-purple-600 text-white font-semibold rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
          >
            {pendingAction === "Incrementing"
              ? "Incrementing..."
              : "Increment Counter"}
          </button>
        </div>

        {txHash && (
          <div className="mt-8 p-4 bg-gray-900 rounded-lg border border-green-500">
            <p className="text-sm text-gray-400 mb-2">Transaction Hash:</p>
            <a
              href={`https://testnet.risescan.com/tx/${txHash}`}
              target="_blank"
              rel="noopener noreferrer"
              className="text-green-500 hover:underline break-all"
            >
              {txHash}
            </a>
          </div>
        )}
      </div>
    </div>
  );
}
```

Once the session key is created, the page displays the active session:

<img alt="Session Key Active Page" src={__img0} placeholder="blur" />

## How It Works

**Session Key Creation:**

1. User clicks "Create Session Key"
2. Generate P256 keypair locally with `P256.randomPrivateKey()`
3. Call `grantPermissions()` with scoped permissions:
   * Allowed contract functions (mint, transfer, increment)
   * Spending limits (1 ETH per minute)
   * Expiry (1 day)
4. Store private key in localStorage, display public key

When creating a session key, the user approves permissions in the wallet:

<img alt="RISE Wallet Session Key Permission" src={__img1} placeholder="blur" />

**Transaction Flow:**

1. User clicks action button (no popup appears!)
2. Call `wallet_prepareCalls` to get digest
3. Sign digest locally with `P256.sign()`
4. Send via `wallet_sendPreparedCalls`
5. Poll `wallet_getCallsStatus` until confirmed
6. Display transaction hash

**Revocation:**

* User can "Revoke Session" anytime to remove on-chain permissions
* Private key is removed from localStorage
* User must create new session key to transact again

## Security Considerations

### Permission Scoping

Always scope session key permissions tightly:

```typescript
permissions: [
  {
    type: "allowed-contract-selector",
    data: {
      contract: YOUR_CONTRACT_ADDRESS,
      selector: "0x12345678", // Specific function only
    },
  },
  {
    type: "native-token-recurring-allowance",
    data: {
      allowance: parseEther("0.1"), // Low limit
      period: 60 * 1000, // Short period
    },
  },
]
```

**Best practices:**

* Grant minimal permissions needed for the use case
* Implement spending limits appropriate for your app
* Allow users to revoke permissions easily

## Next Steps

Congratulations! You've successfully implemented both passkey and session key flows. You now understand how to build secure, user-friendly wallet-integrated applications.

### Related Tutorials

* [Shred Ninja](/docs/cookbook/shred-ninja) - Realtime blockchain events
* [Reaction Time Game](/docs/cookbook/reaction-time-game) - 3ms confirmations showcase
* [RISEx Telegram Bot](/docs/cookbook/risex-telegram-bot) - AI-powered trading bot

### Resources

* [GitHub Repository](https://github.com/awesamarth/rise-cookbook-wallet) - Complete source code
* [RISE Wallet Documentation](/docs/rise-wallet) - Complete wallet integration guide
* [Wagmi Documentation](https://wagmi.sh) - React hooks for Ethereum
* [Viem Documentation](https://viem.sh) - Low-level Ethereum library
* [RISE Testnet Faucet](https://faucet.testnet.riselabs.xyz) - Get testnet ETH
