RISE Logo-Light

Session Keys

Enable high-frequency interactions without popups

Session Keys

Session keys are temporary keys that are granted specific permissions. They allow your app to sign transactions on behalf of the user without prompting them for confirmation every time.

This is critical for:

  • High-frequency trading: Place and cancel orders instantly.
  • Games: Move characters or perform actions without interrupting gameplay.
  • Social apps: Like posts or follow users with a single tap.
Session Key Management

Session keys enable faster transactions without biometric confirmation

No active session keys!Create one to enable faster transactions!

Creating a Session Key

You use the useGrantPermissions hook from rise-wallet/wagmi to request a new session key. You must define exactly what this key can do (permissions) and how long it lasts (expiry).

import { Hooks } from "rise-wallet/wagmi";
import { P256, PublicKey } from "ox";
import { keccak256, parseEther, parseUnits, toHex } from "viem";

// ... inside component
const grantPermissions = Hooks.useGrantPermissions();

const createSession = async () => {
  // 1. Generate a local key pair (P256)
  const privateKey = P256.randomPrivateKey();
  const publicKey = PublicKey.toHex(P256.getPublicKey({ privateKey }), {
    includePrefix: false,
  });

  // 2. Request the session key from the wallet with permissions
  await grantPermissions.mutateAsync({
    key: { publicKey, type: "p256" },
    expiry: Math.floor(Date.now() / 1000) + 3600, // 1 hour
    feeToken: null, // use native token for fees, or { limit: "0.01", symbol: "ETH" }

    // the permissions you want the session key to have
    permissions: {
      calls: [
        {
          to: "0x...", // address of the contract on which the function is being called
          signature: keccak256(toHex("transfer(address,uint256)")).slice(0, 10), // function selector (in this case, 0xa9059cbb)
        }
      ],

      // token spend limits
      spend: [
        {
          token: "0x0000000000000000000000000000000000000000", // native ETH
          limit: parseEther("20"),
          period: "hour",
        },
        {
          token: "0xUSDC...", // USDC (6 decimals)
          limit: parseUnits("100", 6),
          period: "day",
        },
      ],
    },
  });

  // 3. Store the private key securely (e.g., localStorage) to sign future requests
  localStorage.setItem("session_key", privateKey);
};

Once created, you can use the stored private key to sign and send wallet_sendPreparedCalls requests directly to the RISE RPC, bypassing the wallet popup completely.

Using a Session Key

After creating a session key, you can use it to sign and execute transactions without wallet popups. Here's a complete example:

import { Hex, P256, Signature } from "ox";
import { useAccount, useChainId } from "wagmi";

// ... inside component
const { connector, address } = useAccount();
const chainId = useChainId();

const executeWithSessionKey = async (calls: any[]) => {
  // 1. Get the stored private key and public key
  const privateKey = localStorage.getItem("session_key");
  const publicKey = PublicKey.toHex(P256.getPublicKey({ privateKey }), {
    includePrefix: false,
  });

  // 2. Get the provider
  const provider = (await connector.getProvider()) as any;

  // 3. Prepare the calls (this simulates and estimates fees)
  const intentParams = [
    {
      calls, // Array of { to: address, data?: hex, value?: bigint }
      chainId: Hex.fromNumber(chainId),
      from: address,
      atomicRequired: true,
      key: {
        publicKey,
        type: "p256",
      },
    },
  ];

  // wallet_prepareCalls returns: { digest, capabilities, ...request }
  // digest: The hash you need to sign
  // capabilities: Optional wallet capabilities
  // request: Other fields needed for sending the transaction
  const { digest, capabilities, ...request } = await provider.request({
    method: "wallet_prepareCalls",
    params: intentParams,
  });

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

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

  console.log("Transaction sent:", result);
  return result;
};

// Example usage: Mint an NFT
(async () => {
  await executeWithSessionKey([
    {
      to: "0x..." as `0x${string}`, // NFT contract address
      data: "0x..." as `0x${string}`, // Encoded mint function call
      value: 0n,
    },
  ]);
})();

The flow is:

  1. Prepare - Call wallet_prepareCalls to simulate the transaction and get a digest
  2. Sign - Sign the digest with your session key's private key using P256
  3. Send - Call wallet_sendPreparedCalls with the signature to execute without popup