Overview
useChipiSession provides a unified API for managing session keys, enabling gasless transactions without requiring the owner’s signature for each operation.
This hook combines all session-related operations:
- Session creation (local keypair generation)
- Session registration (on-chain)
- Transaction execution (using session key)
- Session revocation (on-chain)
- Status checking (remaining calls, expiration)
Session keys only work with CHIPI wallets. READY wallets do not support session keys.
Prerequisites
Before using useChipiSession, you need:
- A CHIPI wallet (check
wallet.walletType === "CHIPI" or wallet.supportsSessionKeys)
- The user’s encryption key (PIN)
- An authentication token (from Clerk, Firebase, etc.)
Session Lifecycle
| State | Description |
|---|
none | No session exists |
created | Session created locally, not yet registered on-chain |
active | Session registered and usable |
expired | Session has expired (time or call limit) |
revoked | Session has been revoked on-chain |
Quick Start
import { useAuth } from "@clerk/nextjs";
import { useChipiWallet, useChipiSession } from "@chipi-stack/nextjs";
function SessionComponent() {
const { userId, getToken } = useAuth();
const [pin, setPin] = useState("");
const { wallet } = useChipiWallet({
externalUserId: userId,
getBearerToken: getToken,
});
const {
session,
sessionState,
hasActiveSession,
remainingCalls,
createSession,
registerSession,
executeWithSession,
isCreating,
isRegistering,
isExecuting,
} = useChipiSession({
wallet,
encryptKey: pin,
getBearerToken: getToken,
});
// Setup session (create + register)
const handleSetup = async () => {
await createSession();
await registerSession();
};
// Execute a transfer using the session
const handleTransfer = async () => {
await executeWithSession([{
contractAddress: "0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8",
entrypoint: "transfer",
calldata: [recipientAddress, amount, "0x0"],
}]);
};
return (
<div>
<p>Session: {sessionState}</p>
{hasActiveSession && <p>Calls remaining: {remainingCalls}</p>}
{!hasActiveSession ? (
<button onClick={handleSetup} disabled={isCreating || isRegistering}>
Setup Session
</button>
) : (
<button onClick={handleTransfer} disabled={isExecuting}>
Transfer (Gasless)
</button>
)}
</div>
);
}
Configuration Options
interface UseChipiSessionConfig {
// Required
wallet: SessionWallet | null | undefined;
encryptKey: string;
getBearerToken: () => Promise<string | null | undefined>;
// Optional
storedSession?: SessionKeyData | null;
onSessionCreated?: (session: SessionKeyData) => void | Promise<void>;
defaultDurationSeconds?: number; // Default: 21600 (6 hours)
defaultMaxCalls?: number; // Default: 1000
autoCheckStatus?: boolean; // Default: true
}
| Option | Type | Default | Description |
|---|
wallet | SessionWallet | Required | Wallet data (must be CHIPI type) |
encryptKey | string | Required | User’s PIN for signing |
getBearerToken | () => Promise<string> | Required | Auth token function |
storedSession | SessionKeyData | null | Previously stored session |
onSessionCreated | (session) => void | - | Callback to persist new session |
defaultDurationSeconds | number | 21600 | Session duration (6 hours) |
defaultMaxCalls | number | 1000 | Max calls per session |
autoCheckStatus | boolean | true | Auto-fetch on-chain status |
Return Values
Session Data
| Property | Type | Description |
|---|
session | SessionKeyData | null | Current session data |
sessionStatus | SessionDataResponse | On-chain session status |
sessionState | SessionState | Lifecycle state |
hasActiveSession | boolean | Whether session is usable |
isSessionExpired | boolean | Whether session has expired |
remainingCalls | number | undefined | Calls left in session |
supportsSession | boolean | Whether wallet supports sessions |
Actions
| Method | Returns | Description |
|---|
createSession(config?) | Promise<SessionKeyData> | Create new session locally |
registerSession(config?) | Promise<string> | Register on-chain (returns txHash) |
revokeSession() | Promise<string> | Revoke on-chain (returns txHash) |
executeWithSession(calls) | Promise<string> | Execute calls (returns txHash) |
clearSession() | void | Clear local session state |
refetchStatus() | Promise<void> | Refresh on-chain status |
Loading States
| Property | Description |
|---|
isCreating | Creating session locally |
isRegistering | Registering session on-chain |
isRevoking | Revoking session on-chain |
isExecuting | Executing transaction |
isLoadingStatus | Fetching on-chain status |
Persisting Sessions
Sessions should be persisted between page loads. Here’s how to use Clerk’s metadata:
import { useUser } from "@clerk/nextjs";
import { useChipiWallet, useChipiSession } from "@chipi-stack/nextjs";
function PersistentSession() {
const { user } = useUser();
const { wallet } = useChipiWallet({ ... });
// Get stored session from Clerk metadata
const storedSession = user?.unsafeMetadata?.chipiSession as SessionKeyData | undefined;
const {
session,
hasActiveSession,
createSession,
registerSession,
} = useChipiSession({
wallet,
encryptKey: pin,
getBearerToken: getToken,
storedSession,
onSessionCreated: async (newSession) => {
// Persist to Clerk metadata
await user?.update({
unsafeMetadata: {
...user.unsafeMetadata,
chipiSession: newSession,
},
});
},
});
// Session is automatically restored on mount!
}
Executing Transactions
The executeWithSession method accepts an array of Starknet calls:
// Single transfer
await executeWithSession([{
contractAddress: USDC_CONTRACT,
entrypoint: "transfer",
calldata: [recipient, amount, "0x0"],
}]);
// Batch multiple calls
await executeWithSession([
{
contractAddress: USDC_CONTRACT,
entrypoint: "approve",
calldata: [spender, amount, "0x0"],
},
{
contractAddress: DEX_CONTRACT,
entrypoint: "swap",
calldata: [...],
},
]);
Custom Session Configuration
Custom Duration
// Create session valid for 1 hour
await createSession({ durationSeconds: 3600 });
Custom Max Calls
// Register with 500 max calls
await registerSession({ maxCalls: 500 });
Allowed Entrypoints
Restrict which contract methods the session can call:
await registerSession({
allowedEntrypoints: [
"0x..." // Only allow specific function selectors
],
});
Error Handling
const { error, session } = useChipiSession({...});
// Check for errors
if (error) {
console.error("Session error:", error.message);
}
// Handle specific errors
const handleSetup = async () => {
try {
await createSession();
await registerSession();
} catch (err) {
if (err.message.includes("does not support sessions")) {
alert("Please use a CHIPI wallet for sessions");
} else if (err.message.includes("expired")) {
alert("Session has expired, please create a new one");
} else {
alert(`Error: ${err.message}`);
}
}
};
Complete Example
"use client";
import { useState, useEffect } from "react";
import { useAuth, useUser } from "@clerk/nextjs";
import { useChipiWallet, useChipiSession } from "@chipi-stack/nextjs";
export function GaslessTransfers() {
const { getToken, userId } = useAuth();
const { user } = useUser();
const [pin, setPin] = useState("");
const [recipient, setRecipient] = useState("");
const [amount, setAmount] = useState("");
const { wallet, formattedBalance } = useChipiWallet({
externalUserId: userId,
getBearerToken: getToken,
});
const storedSession = user?.unsafeMetadata?.chipiSession;
const {
session,
sessionState,
hasActiveSession,
remainingCalls,
supportsSession,
createSession,
registerSession,
executeWithSession,
revokeSession,
isCreating,
isRegistering,
isExecuting,
isRevoking,
error,
} = useChipiSession({
wallet,
encryptKey: pin,
getBearerToken: getToken,
storedSession,
onSessionCreated: async (newSession) => {
await user?.update({
unsafeMetadata: { chipiSession: newSession },
});
},
});
if (!wallet?.supportsSessionKeys) {
return <p>Sessions require a CHIPI wallet</p>;
}
return (
<div className="space-y-6">
{/* PIN Input */}
<input
type="password"
placeholder="Enter PIN"
value={pin}
onChange={(e) => setPin(e.target.value)}
maxLength={4}
/>
{/* Session Status */}
<div className="p-4 bg-gray-100 rounded">
<p>State: <strong>{sessionState}</strong></p>
{hasActiveSession && (
<p>Remaining calls: {remainingCalls}</p>
)}
</div>
{/* Session Setup */}
{!hasActiveSession && (
<button
onClick={async () => {
await createSession();
await registerSession();
}}
disabled={isCreating || isRegistering || !pin}
>
{isCreating ? "Creating..." : isRegistering ? "Registering..." : "Setup Session"}
</button>
)}
{/* Transfer Form */}
{hasActiveSession && (
<div className="space-y-4">
<input
placeholder="Recipient address"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
/>
<input
type="number"
placeholder="Amount"
value={amount}
onChange={(e) => setAmount(e.target.value)}
/>
<button
onClick={async () => {
await executeWithSession([{
contractAddress: "0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8",
entrypoint: "transfer",
calldata: [recipient, amount, "0x0"],
}]);
}}
disabled={isExecuting}
>
{isExecuting ? "Sending..." : "Send USDC (Gasless)"}
</button>
</div>
)}
{/* Revoke Session */}
{hasActiveSession && (
<button
onClick={revokeSession}
disabled={isRevoking}
className="text-red-600"
>
{isRevoking ? "Revoking..." : "Revoke Session"}
</button>
)}
{/* Error Display */}
{error && (
<p className="text-red-500">{error.message}</p>
)}
</div>
);
}