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 keys enable faster transactions without biometric confirmation
"use client";import { Hooks } from "rise-wallet/wagmi";import { P256, PublicKey } from "ox";export function SessionKeysWidget() { const grantPermissions = Hooks.useGrantPermissions(); const createSession = async () => { // 1. Generate Local Key Pair const privateKey = P256.randomPrivateKey(); const publicKey = PublicKey.toHex( P256.getPublicKey({ privateKey }), { includePrefix: false } ); // 2. Define Permissions const permissions = { calls: [ { to: "0x...", // Token Contract signature: "0x...", // transfer(address,uint256) } ] }; // 3. Grant Permissions on Chain await grantPermissions.mutateAsync({ key: { publicKey, type: "p256" }, expiry: Math.floor(Date.now() / 1000) + 3600, permissions, }); // 4. Store Private Key Locally localStorage.setItem("session_key", privateKey); }; return ( <Button onClick={createSession}> Create Session Key </Button> );}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:
- Prepare - Call
wallet_prepareCallsto simulate the transaction and get a digest - Sign - Sign the digest with your session key's private key using P256
- Send - Call
wallet_sendPreparedCallswith the signature to execute without popup