Skip to main content

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:
  1. A CHIPI wallet (check wallet.walletType === "CHIPI" or wallet.supportsSessionKeys)
  2. The user’s encryption key (PIN)
  3. An authentication token (from Clerk, Firebase, etc.)

Session Lifecycle

StateDescription
noneNo session exists
createdSession created locally, not yet registered on-chain
activeSession registered and usable
expiredSession has expired (time or call limit)
revokedSession 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
}
OptionTypeDefaultDescription
walletSessionWalletRequiredWallet data (must be CHIPI type)
encryptKeystringRequiredUser’s PIN for signing
getBearerToken() => Promise<string>RequiredAuth token function
storedSessionSessionKeyDatanullPreviously stored session
onSessionCreated(session) => void-Callback to persist new session
defaultDurationSecondsnumber21600Session duration (6 hours)
defaultMaxCallsnumber1000Max calls per session
autoCheckStatusbooleantrueAuto-fetch on-chain status

Return Values

Session Data

PropertyTypeDescription
sessionSessionKeyData | nullCurrent session data
sessionStatusSessionDataResponseOn-chain session status
sessionStateSessionStateLifecycle state
hasActiveSessionbooleanWhether session is usable
isSessionExpiredbooleanWhether session has expired
remainingCallsnumber | undefinedCalls left in session
supportsSessionbooleanWhether wallet supports sessions

Actions

MethodReturnsDescription
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()voidClear local session state
refetchStatus()Promise<void>Refresh on-chain status

Loading States

PropertyDescription
isCreatingCreating session locally
isRegisteringRegistering session on-chain
isRevokingRevoking session on-chain
isExecutingExecuting transaction
isLoadingStatusFetching 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>
  );
}