Session Key Flow
Implement background signing with pre-authorized permissions
Implementing Session Key Flow
The session key flow enables background signing without popups by using pre-authorized permissions.
Create the Session Key Page
"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:

How It Works
Session Key Creation:
- User clicks "Create Session Key"
- Generate P256 keypair locally with
P256.randomPrivateKey() - Call
grantPermissions()with scoped permissions:- Allowed contract functions (mint, transfer, increment)
- Spending limits (1 ETH per minute)
- Expiry (1 day)
- Store private key in localStorage, display public key
When creating a session key, the user approves permissions in the wallet:

Transaction Flow:
- User clicks action button (no popup appears!)
- Call
wallet_prepareCallsto get digest - Sign digest locally with
P256.sign() - Send via
wallet_sendPreparedCalls - Poll
wallet_getCallsStatusuntil confirmed - 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:
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 - Realtime blockchain events
- Reaction Time Game - 3ms confirmations showcase
- RISEx Telegram Bot - AI-powered trading bot
Resources
- GitHub Repository - Complete source code
- RISE Wallet Documentation - Complete wallet integration guide
- Wagmi Documentation - React hooks for Ethereum
- Viem Documentation - Low-level Ethereum library
- RISE Testnet Faucet - Get testnet ETH