How to use session keys using Smart Sessions
The Smart Sessions Module allows developers to create session keys with scoped permissions and access rights on a users account. This allows the user to determine exactly what an app is allowed to do on its' behalf and enforce this onchain. This guide will show you how to set up and use Smart Sessions with a Safe smart account using permissionless.js SDK. You can also view the entire code (opens in a new tab) of this guide.
We will first set up the smart account, install the Smart Sessions Module, create a session and then use this new session to execute a UserOperation.
Install the packages
First, install the required packages. We use the latest version of module sdk, permissionless ^0.2 and viem ^2.21.
npm i viem @rhinestone/module-sdk permissionless
Import the required functions and constants
import {
getSmartSessionsValidator,
OWNABLE_VALIDATOR_ADDRESS,
getSudoPolicy,
Session,
getAccount,
encodeSmartSessionSignature,
getOwnableValidatorMockSignature,
RHINESTONE_ATTESTER_ADDRESS,
MOCK_ATTESTER_ADDRESS,
encodeValidatorNonce,
getOwnableValidator,
encodeValidationData,
getEnableSessionDetails,
} from "@rhinestone/module-sdk";
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
import {
toHex,
Address,
Hex,
createPublicClient,
http,
Chain,
toBytes,
} from "viem";
import { createSmartAccountClient } from "permissionless";
import { erc7579Actions } from "permissionless/actions/erc7579";
import { createPimlicoClient } from "permissionless/clients/pimlico";
import {
entryPoint07Address,
getUserOperationHash,
createPaymasterClient,
} from "viem/account-abstraction";
import { toSafeSmartAccount } from "permissionless/accounts";
import { getAccountNonce } from "permissionless/actions";
Create the clients
Create the smart account client, the bundler client and the paymaster client. You will need to add your own urls here.
const publicClient = createPublicClient({
transport: http(rpcUrl),
chain: chain,
})
const pimlicoClient = createPimlicoClient({
transport: http(bundlerUrl),
entryPoint: {
address: entryPoint07Address,
version: '0.7',
},
})
const paymasterClient = createPaymasterClient({
transport: http(paymasterUrl),
})
Create the signer
The Safe account will need to have a signer to sign user operations. In permissionless.js, the default Safe account validates ECDSA signatures.
For example, to create a signer based on a private key:
const owner = privateKeyToAccount(generatePrivateKey())
Create the initial validator
We will also create and add an initial validator. In this guide, we will use the ownable validator to create the signature to enable the session on the user's account.
const ownableValidator = getOwnableValidator({
owners: [owner.address],
threshold: 1,
})
Create the Safe account
Create the Safe account object using the signer. Note that you should only use the MockAttester
on testnets.
const safeAccount = await toSafeSmartAccount({
client: publicClient,
owners: [owner],
version: '1.4.1',
entryPoint: {
address: entryPoint07Address,
version: '0.7',
},
safe4337ModuleAddress: '0x7579EE8307284F293B1927136486880611F20002',
erc7579LaunchpadAddress: '0x7579011aB74c46090561ea277Ba79D510c6C00ff',
attesters: [
RHINESTONE_ATTESTER_ADDRESS, // Rhinestone Attester
MOCK_ATTESTER_ADDRESS, // Mock Attester - do not use in production
],
attestersThreshold: 1,
validators: [
{
address: ownableValidator.address,
context: ownableValidator.initData,
},
],
})
Create the smart account client
The smart account client is used to interact with the smart account. You will need to add your own bundler url and the chain that you are using.
const smartAccountClient = createSmartAccountClient({
account: safeAccount,
chain: chain,
bundlerTransport: http(bundlerUrl),
paymaster: paymasterClient,
userOperation: {
estimateFeesPerGas: async () => {
return (await pimlicoClient.getUserOperationGasPrice()).fast
},
},
}).extend(erc7579Actions())
Install the Smart Sessions Module
Next, we will install the Smart Sessions Module on the account. We could pass it a new session to create when installing, but in this guide we will instead demonstrate the "enable" flow of smart sessions.
const smartSessions = getSmartSessionsValidator({})
const opHash = await smartAccountClient.installModule(smartSessions)
await pimlicoClient.waitForUserOperationReceipt({
hash: opHash,
})
Create the session to enable
Now we will create a new session to enable. This session will be scoped to allow a single action, to the specified target and with the (in this case empty) target selector. For this action, we install the SudoPolicy
which will always allow this action.
const sessionOwner = privateKeyToAccount(generatePrivateKey())
const session: Session = {
sessionValidator: OWNABLE_VALIDATOR_ADDRESS,
sessionValidatorInitData: encodeValidationData({
threshold: 1,
owners: [sessionOwner.address],
}),
salt: toHex(toBytes('0', { size: 32 })),
userOpPolicies: [],
erc7739Policies: {
allowedERC7739Content: [],
erc1271Policies: [],
},
actions: [
{
actionTarget: '0xa564cB165815937967a7d018B7F34B907B52fcFd' as Address, // an address as the target of the session execution
actionTargetSelector: '0x00000000' as Hex, // function selector to be used in the execution, in this case no function selector is used
actionPolicies: [getSudoPolicy()],
},
],
chainId: BigInt(chain.id),
}
Get the session details
Now that we have the session, we will generate the details to enable the session. For this, we will need to pass an account and client object. These details include the correctly formulated data about the exact session to encode. However, all of this is abstracted away and you can just use the function below to easily get these details and then use them to enable a new session.
const account = getAccount({
address: safeAccount.address,
type: 'safe',
})
const sessionDetails = await getEnableSessionDetails({
sessions: [session],
account,
clients: [publicClient],
})
Have the user sign the enable signature
Next, the user will need to sign the permissionEnableHash
. How exactly this is done depends on how your application works and which validator the user currently has installed.
In this case, we will assume that an ownable validator is already installed on the account, so we can use the users' EOA to sign the hash.
sessionDetails.enableSessionData.enableSession.permissionEnableSig =
await owner.signMessage({
message: { raw: sessionDetails.permissionEnableHash },
})
Create the UserOperation to execute
Now that we have the session set up, we can create the UserOperation. Since we scope this session for a specific target and selector, we will use those to create the calldata for the UserOperation.
const nonce = await getAccountNonce(publicClient, {
address: safeAccount.address,
entryPointAddress: entryPoint07Address,
key: encodeValidatorNonce({
account,
validator: smartSessions,
}),
})
sessionDetails.signature = getOwnableValidatorMockSignature({
threshold: 1,
})
const userOperation = await smartAccountClient.prepareUserOperation({
account: safeAccount,
calls: [
{
to: session.actions[0].actionTarget,
value: BigInt(0),
data: session.actions[0].actionTargetSelector,
},
],
nonce,
signature: encodeSmartSessionSignature(sessionDetails),
})
Create the session key signature
Next, we will use the session key to sign the UserOperation.
const userOpHashToSign = getUserOperationHash({
chainId: chain.id,
entryPointAddress: entryPoint07Address,
entryPointVersion: '0.7',
userOperation,
})
sessionDetails.signature = await sessionOwner.signMessage({
message: { raw: userOpHashToSign },
})
userOperation.signature = encodeSmartSessionSignature(sessionDetails)
Execute the UserOperation
Finally, we can execute the UserOperation to recover the account.
const userOpHash = await smartAccountClient.sendUserOperation(userOperation)
const receipt = await pimlicoClient.waitForUserOperationReceipt({
hash: userOpHash,
})