Skip to content

Session Keys

Session Keys enable apps to submit transactions on behalf of users.

Session Key transactions can avoid typical user friction in web3 apps like:

  1. Wallet popup windows
  2. Passkey biometric scans
  3. User presence in-app

These unlock experiences that keep all of the unique properties of wallets (eg self-custody, data portability) without sacrificng on user experience compared to web2:

  1. Sign-in and never see mention of a wallet again
  2. High-frequency transactions (eg gaming, social)
  3. Background transactions (eg subscriptions, automated trading)

Using Wagmi + Vite + React

Set Up Your Paymaster

To use session keys, you'll need to use a paymaster. We recommend the Coinbase Developer Platform paymaster, currently offering up to $15k in gas credits as part of the Base Gasless Campaign.

We also recommend setting up a paymaster proxy, which you can read more about in our guide on paymasters.

Contract Integration

For the purpose of quickly getting started, you can start testing with this contract deployed on Base Sepolia.

Session Keys V1 can only call contracts that implement IPermissionCallable.

Our goal is to enable apps to permissionlessly add Session Key support, while reducing risk of apps asking users to approve malicious permissions.

It works by only allowing interactions with contracts that implement the following function:

function permissionedCall(bytes calldata call) external payable returns (bytes memory);

We have a template implementation which your contracts can inherit in this integration guide.

Set Up Your App

In this tutorial we'll use Vite + React to scaffold an app.

pnpm create vite

Select React and Typescript when prompted.

This will scaffold a Vite + React template app for you with necessary dependencies.

Install Additional Dependencies

Currently only web is supported. Apps will need to have the following packages installed at the specified versions:

cd <your-app-directory> && pnpm install viem@npm:@lukasrosario/viem@2.17.4-sessionkeys.7 wagmi@npm:@lukasrosario/wagmi@2.11.1-sessionkeys.6 @tanstack/react-query@5.0.5 buffer@6.0.3

Additionally, if you don't already have a key management system you plan on using for session keys, we recommend the following version of wevm's webauthn-p256. You can use this package to create P256 key pairs with non-exportable private keys you can sign with and store in the browser (self-custodial). The rest of this guide will use this package, but you can use any other key management system with EOA or P256 keys you might already have set up.

pnpm install webauthn-p256@npm:@lukasrosario/webauthn-p256@0.0.10-sessionkeys.6

After installing the additional requirements, your package.json should have the following dependencies:

package.json
"dependencies": {
  "@tanstack/react-query": "5.0.5",
  "viem": "npm:@lukasrosario/viem@2.17.4-sessionkeys.7",
  "wagmi": "npm:@lukasrosario/wagmi@2.11.1-sessionkeys.6",
  "webauthn-p256": "npm:@lukasrosario/webauthn-p256@0.0.10-sessionkeys.6",
  "buffer": "6.0.3"
}

At this point you should start your app and make sure you can see the template app page. You can do so by running

pnpm dev

Set Up Your Wagmi Provider

Now that you have your dependencies installed, you'll need to set up your wagmi provider.

main.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Buffer } from "buffer";
import React from "react";
import ReactDOM from "react-dom/client";
import { WagmiProvider } from "wagmi";
 
import App from "./App.tsx";
import { config } from "./wagmi.ts";
 
import "./index.css";
 
globalThis.Buffer = Buffer;
 
const queryClient = new QueryClient();
 
ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <WagmiProvider config={config}>
      <QueryClientProvider client={queryClient}>
        <App />
      </QueryClientProvider>
    </WagmiProvider>
  </React.StrictMode>
);

Connect Your Smart Wallet

Before you can request permissions from your wallet, you'll need to connect your Smart Wallet to your app. To do so, make the following changes to the App.tsx file to create a "Log in" button:

App.tsx
import { useAccount, useConnect } from "wagmi";
 
function App() {
  const account = useAccount();
  const { connectors, connect } = useConnect();
 
  const login = async () => {
    connect({ connector: connectors[0] });
  };
 
  return (
    <div>
      <div>
        {account.address && <span>{account.address}</span>}
        {!account.address && (
          <button onClick={login} type="button">
            Log in
          </button>
        )}
      </div>
    </div>
  );
}
 
export default App;

At this point, you should see a "Log in" button that prompts you to connect your Smart Wallet to your app when clicked. After doing so, you should see your Smart Wallet's address on the page.

You might need to clear your app's local storage if you have other wallets installed and see an address on the page prior to connecting your Smart Wallet.

Grant Permissions

Session Keys are built on top of ERC-7715 which introduces a new RPC for apps to request permissions from wallets, wallet_grantPermissions.

A permission is defined by:

  • address: Smart account the permission is valid for.
  • chainId: Chain the permision is valid for.
  • expiry: Timestamp this permission is valid until (unix seconds).
  • signer: The entity that has limited control of account in the permission (i.e. the "session key").
  • permissions: Composable scopes this signer can take action for the account.

Once you've connected your Smart Wallet to your app, we recommend using our experimental useGrantPermissions hook in wagmi.

The following example also shows how you can webauthn-p256 to create a P256 key pair that you can store in the browser (using local state).

App.tsx
import { useState } from "react";
import { useAccount, useConnect } from "wagmi";
import { Hex, parseEther, toFunctionSelector } from "viem";
import { useGrantPermissions } from "wagmi/experimental";
import { createCredential, P256Credential } from "webauthn-p256";
import { clickAddress } from "./click";
 
function App() {
  const [permissionsContext, setPermissionsContext] = useState<
    Hex | undefined
  >();
  const [credential, setCredential] = useState<
    undefined | P256Credential<"cryptokey">
  >();
 
  const account = useAccount();
  const { connectors, connect } = useConnect();
  const { grantPermissionsAsync } = useGrantPermissions();
 
  const login = async () => {
    connect({ connector: connectors[0] });
  };
 
  const grantPermissions = async () => { 
    if (account.address) { 
      const newCredential = await createCredential({ type: "cryptoKey" }); 
      const response = await grantPermissionsAsync({ 
        permissions: [ 
          { 
            address: account.address, 
            chainId: 84532, 
            expiry: 17218875770, 
            signer: { 
              type: "key", 
              data: { 
                type: "secp256r1", 
                publicKey: newCredential.publicKey, 
              }, 
            }, 
            permissions: [ 
              { 
                type: "native-token-recurring-allowance", 
                data: { 
                  allowance: parseEther("0.1"), 
                  start: Math.floor(Date.now() / 1000), 
                  period: 86400, 
                }, 
              }, 
              { 
                type: "allowed-contract-selector", 
                data: { 
                  contract: clickAddress, 
                  selector: toFunctionSelector( 
                    "permissionedCall(bytes calldata call)"
                  ), 
                }, 
              }, 
            ], 
          }, 
        ], 
      }); 
      const context = response[0].context as Hex; 
      console.log(context) 
      setPermissionsContext(context); 
      setCredential(newCredential); 
    } 
  }; 
 
  return (
    <div>
      <div>
        {account.address && <span>{account.address}</span>}
        {!account.address && (
          <button onClick={login} type="button">
            Log in
          </button>
        )}
      </div>
 
      <div>
        {account.address && ( 
          <button type="button" onClick={grantPermissions}>
            Grant Permissions
          </button> 
        )}
      </div>
    </div>
  );
}
 
export default App;

Submit Transaction

After you have granted permissions to a contract that implements our dedicated permissionedCall function, you can now submit Session Key transactions using useSendCalls.

As a reminder, you'll need to set up a paymaster service. After doing so, set the corresponding environment variable used below.

App.tsx
import { useState } from "react";
import { encodeFunctionData, Hex, parseEther, toFunctionSelector } from "viem";
import { useAccount, useConnect, useWalletClient } from "wagmi";
import {
  useCallsStatus,
  useGrantPermissions,
  useSendCalls,
} from "wagmi/experimental";
import {
  createCredential,
  P256Credential,
  signWithCredential,
} from "webauthn-p256";
import { clickAddress, clickAbi } from "./click";
 
export function App() {
  const [permissionsContext, setPermissionsContext] = useState<
    Hex | undefined
  >();
  const [credential, setCredential] = useState<
    undefined | P256Credential<"cryptokey">
  >();
  const [callsId, setCallsId] = useState<string>();
  const [submitted, setSubmitted] = useState(false);
 
  const account = useAccount();
  const { connectors, connect } = useConnect();
  const { grantPermissionsAsync } = useGrantPermissions();
  const { data: walletClient } = useWalletClient({ chainId: 84532 });
  const { sendCallsAsync } = useSendCalls();
  const { data: callsStatus } = useCallsStatus({
    id: callsId as string,
    query: {
      enabled: !!callsId,
      refetchInterval: (data) =>
        data.state.data?.status === "PENDING" ? 500 : false,
    },
  });
 
  const login = async () => {
    connect({ connector: connectors[0] });
  };
 
  const grantPermissions = async () => {
    if (account.address) {
      const newCredential = await createCredential({ type: "cryptoKey" });
      const response = await grantPermissionsAsync({
        permissions: [
          {
            address: account.address,
            chainId: 84532,
            expiry: 17218875770,
            signer: {
              type: "key",
              data: {
                type: "secp256r1",
                publicKey: newCredential.publicKey,
              },
            },
            permissions: [
              {
                type: "native-token-recurring-allowance",
                data: {
                  allowance: parseEther("0.1"),
                  start: Math.floor(Date.now() / 1000),
                  period: 86400,
                },
              },
              {
                type: "allowed-contract-selector",
                data: {
                  contract: clickAddress,
                  selector: toFunctionSelector(
                    "permissionedCall(bytes calldata call)"
                  ),
                },
              },
            ],
          },
        ],
      });
      const context = response[0].context as Hex;
      setPermissionsContext(context);
      setCredential(newCredential);
    }
  };
 
  const click = async () => { 
    if (account.address && permissionsContext && credential && walletClient) { 
      setSubmitted(true); 
      setCallsId(undefined); 
      try { 
        const callsId = await sendCallsAsync({ 
          calls: [ 
            { 
              to: clickAddress, 
              value: BigInt(0), 
              data: encodeFunctionData({ 
                abi: clickAbi, 
                functionName: "click", 
                args: [], 
              }), 
            }, 
          ], 
          capabilities: { 
            permissions: { 
              context: permissionsContext, 
            }, 
            paymasterService: { 
              url: import.meta.env.VITE_PAYMASTER_URL, // Your paymaster service URL
            }, 
          }, 
          signatureOverride: signWithCredential(credential), 
        }); 

        setCallsId(callsId); 
      } catch (e: unknown) { 
        console.error(e); 
      } 
      setSubmitted(false); 
    } 
  }; 
 
  return (
    <div>
      <div>
        {account.address && <span>{account.address}</span>}
        {!account.address && (
          <button onClick={login} type="button">
            Log in
          </button>
        )}
      </div>
 
      <div>
        {account.address &&
          (permissionsContext ? ( 
            <button
              type="button"
              onClick={click}
              disabled={
                submitted ||
                (!!callsId && !(callsStatus?.status === "CONFIRMED")) 
              }
            >
              Click
            </button> 
          ) : ( 
            <button
              type="button"
              onClick={grantPermissions}
              disabled={submitted}
            >
              Grant Permission
            </button> 
          ))}
        {callsStatus && callsStatus.status === "CONFIRMED" && ( 
          <a
            href={`https://base-sepolia.blockscout.com/tx/${callsStatus.receipts?.[0].transactionHash}`}
            target="_blank"
            className="absolute top-8 hover:underline"
          >
            View transaction
          </a> 
        )}
      </div>
    </div>
  );
}
 
export default App;

You should now be able to click the "Click" button, which will submit a session key transaction. After a few seconds you should see a link to view the transaction you submitted.

Fetch Permissions

Coming soon