Skip to content

[SDK] EIP-7702 Session Keys #7432

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/chatty-chairs-mate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---

Introduces Session Keys to EIP-7702-powered In-App Wallets via a new createSessionKey extension
10 changes: 10 additions & 0 deletions packages/thirdweb/src/exports/wallets/in-app.native.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
// --- KEEEP IN SYNC with exports/wallets/in-app.ts ---

//ACCOUNT
export {
type CreateSessionKeyOptions,
createSessionKey,
isCreateSessionKeySupported,
} from "../../extensions/erc7702/account/createSessionKey.js";
export type {
Condition,
LimitType,
} from "../../extensions/erc7702/account/types.js";
export type {
GetAuthenticatedUserParams,
MultiStepAuthArgsType,
Expand Down
10 changes: 10 additions & 0 deletions packages/thirdweb/src/exports/wallets/in-app.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
// --- KEEEP IN SYNC with exports/wallets/in-app.native.ts ---

//ACCOUNT
export {
type CreateSessionKeyOptions,
createSessionKey,
isCreateSessionKeySupported,
} from "../../extensions/erc7702/account/createSessionKey.js";
export type {
Condition,
LimitType,
} from "../../extensions/erc7702/account/types.js";
export {
getSocialIcon,
socialIcons,
Expand Down
181 changes: 181 additions & 0 deletions packages/thirdweb/src/extensions/erc7702/account/createSessionKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import type { BaseTransactionOptions } from "../../../transaction/types.js";
import { randomBytesHex } from "../../../utils/random.js";
import type { Account } from "../../../wallets/interfaces/wallet.js";
import {
createSessionWithSig,
isCreateSessionWithSigSupported,
} from "../__generated__/MinimalAccount/write/createSessionWithSig.js";
import {
type CallSpecInput,
CallSpecRequest,
ConstraintRequest,
SessionSpecRequest,
type TransferSpecInput,
TransferSpecRequest,
UsageLimitRequest,
} from "./types.js";

/**
* @extension ERC7702
*/
export type CreateSessionKeyOptions = {
/**
* The admin account that will perform the operation.
*/
account: Account;
/**
* The address to add as a session key.
*/
sessionKeyAddress: string;
/**
* How long the session key should be valid for, in seconds.
*/
durationInSeconds: number;
/**
* Whether to grant full execution permissions to the session key.
*/
grantFullPermissions?: boolean;
/**
* Smart contract interaction policies to apply to the session key, ignored if grantFullPermissions is true.
*/
callPolicies?: CallSpecInput[];
/**
* Value transfer policies to apply to the session key, ignored if grantFullPermissions is true.
*/
transferPolicies?: TransferSpecInput[];
};

/**
* Creates session key permissions for a specified address.
* @param options - The options for the createSessionKey function.
* @param {Contract} options.contract - The EIP-7702 smart EOA contract to create the session key from
* @returns The transaction object to be sent.
* @example
* ```ts
* import { createSessionKey } from 'thirdweb/extensions/7702';
* import { sendTransaction } from 'thirdweb';
*
* const transaction = createSessionKey({
* account: account,
* contract: accountContract,
* sessionKeyAddress: TEST_ACCOUNT_A.address,
* durationInSeconds: 86400, // 1 day
* grantFullPermissions: true
*})
*
* await sendTransaction({ transaction, account });
* ```
* @extension ERC7702
*/
export function createSessionKey(
options: BaseTransactionOptions<CreateSessionKeyOptions>,
) {
const {
contract,
account,
sessionKeyAddress,
durationInSeconds,
grantFullPermissions,
callPolicies,
transferPolicies,
} = options;

if (durationInSeconds <= 0) {
throw new Error("durationInSeconds must be positive");
}

Check warning on line 85 in packages/thirdweb/src/extensions/erc7702/account/createSessionKey.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/extensions/erc7702/account/createSessionKey.ts#L84-L85

Added lines #L84 - L85 were not covered by tests

return createSessionWithSig({
async asyncParams() {
const req = {
callPolicies: (callPolicies || []).map((policy) => ({
constraints: (policy.constraints || []).map((constraint) => ({
condition: Number(constraint.condition),
index: constraint.index || BigInt(0),
limit: constraint.limit
? {
limit: constraint.limit.limit,
limitType: Number(constraint.limit.limitType),
period: constraint.limit.period,
}

Check warning on line 99 in packages/thirdweb/src/extensions/erc7702/account/createSessionKey.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/extensions/erc7702/account/createSessionKey.ts#L95-L99

Added lines #L95 - L99 were not covered by tests
: {
limit: BigInt(0),
limitType: 0,
period: BigInt(0),
},
refValue: constraint.refValue || "0x",
})),
maxValuePerUse: policy.maxValuePerUse || BigInt(0),
selector: policy.selector,
target: policy.target,
valueLimit: policy.valueLimit
? {
limit: policy.valueLimit.limit,
limitType: Number(policy.valueLimit.limitType),
period: policy.valueLimit.period,
}
: {
limit: BigInt(0),
limitType: 0,
period: BigInt(0),
},

Check warning on line 120 in packages/thirdweb/src/extensions/erc7702/account/createSessionKey.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/extensions/erc7702/account/createSessionKey.ts#L116-L120

Added lines #L116 - L120 were not covered by tests
})),
expiresAt: BigInt(Math.floor(Date.now() / 1000) + durationInSeconds),
isWildcard: grantFullPermissions ?? true,
signer: sessionKeyAddress,
transferPolicies: (transferPolicies || []).map((policy) => ({
maxValuePerUse: policy.maxValuePerUse || BigInt(0),
target: policy.target,
valueLimit: policy.valueLimit
? {
limit: policy.valueLimit.limit,
limitType: Number(policy.valueLimit.limitType),
period: policy.valueLimit.period,
}
: {
limit: BigInt(0),
limitType: 0,
period: BigInt(0),
},

Check warning on line 138 in packages/thirdweb/src/extensions/erc7702/account/createSessionKey.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/extensions/erc7702/account/createSessionKey.ts#L134-L138

Added lines #L134 - L138 were not covered by tests
})),
uid: await randomBytesHex(),
};

const signature = await account.signTypedData({
domain: {
chainId: contract.chain.id,
name: "MinimalAccount",
verifyingContract: contract.address,
version: "1",
},
message: req,
primaryType: "SessionSpec",
types: {
CallSpec: CallSpecRequest,
Constraint: ConstraintRequest,
SessionSpec: SessionSpecRequest,
TransferSpec: TransferSpecRequest,
UsageLimit: UsageLimitRequest,
},
});

return { sessionSpec: req, signature };
},
contract,
});
}

/**
* Checks if the `isCreateSessionKeySupported` method is supported by the given contract.
* @param availableSelectors An array of 4byte function selectors of the contract. You can get this in various ways, such as using "whatsabi" or if you have the ABI of the contract available you can use it to generate the selectors.
* @returns A boolean indicating if the `isAddSessionKeySupported` method is supported.
* @extension ERC7702
* @example
* ```ts
* import { isCreateSessionKeySupported } from "thirdweb/extensions/erc7702";
*
* const supported = isCreateSessionKeySupported(["0x..."]);
* ```
*/
export function isCreateSessionKeySupported(availableSelectors: string[]) {
return isCreateSessionWithSigSupported(availableSelectors);
}

Check warning on line 181 in packages/thirdweb/src/extensions/erc7702/account/createSessionKey.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/extensions/erc7702/account/createSessionKey.ts#L180-L181

Added lines #L180 - L181 were not covered by tests
132 changes: 132 additions & 0 deletions packages/thirdweb/src/extensions/erc7702/account/sessionkey.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { defineChain } from "src/chains/utils.js";
import { prepareTransaction } from "src/transaction/prepare-transaction.js";
import { inAppWallet } from "src/wallets/in-app/web/in-app.js";
import type { Account } from "src/wallets/interfaces/wallet.js";
import { beforeAll, describe, expect, it } from "vitest";
import { TEST_CLIENT } from "../../../../test/src/test-clients.js";
import { TEST_ACCOUNT_A } from "../../../../test/src/test-wallets.js";
import { ZERO_ADDRESS } from "../../../constants/addresses.js";
import {
getContract,
type ThirdwebContract,
} from "../../../contract/contract.js";
import { parseEventLogs } from "../../../event/actions/parse-logs.js";
import { sendAndConfirmTransaction } from "../../../transaction/actions/send-and-confirm-transaction.js";
import { sessionCreatedEvent } from "../__generated__/MinimalAccount/events/SessionCreated.js";
import { createSessionKey } from "./createSessionKey.js";
import { Condition, LimitType } from "./types.js";

describe.runIf(process.env.TW_SECRET_KEY)(
"Session Key Behavior",
{
retry: 0,
timeout: 240_000,
},
() => {
const chainId = 11155111;
let account: Account;
let accountContract: ThirdwebContract;

beforeAll(async () => {
// Create 7702 Smart EOA
const wallet = inAppWallet({
executionMode: {
mode: "EIP7702",
sponsorGas: true,
},
});
account = await wallet.connect({
chain: defineChain(chainId),
client: TEST_CLIENT,
strategy: "guest",
});

// Send a null tx to trigger deploy/upgrade
await sendAndConfirmTransaction({
account: account,
transaction: prepareTransaction({
chain: defineChain(chainId),
client: TEST_CLIENT,
to: account.address,
value: 0n,
}),
});

// Will auto resolve abi since it's deployed
accountContract = getContract({
address: account.address,
chain: defineChain(chainId),
client: TEST_CLIENT,
});
}, 120_000);

it("should allow adding adminlike session keys", async () => {
const receipt = await sendAndConfirmTransaction({
account: account,
transaction: createSessionKey({
account: account,
contract: accountContract,
durationInSeconds: 86400,
grantFullPermissions: true, // 1 day
sessionKeyAddress: TEST_ACCOUNT_A.address,
}),
});
const logs = parseEventLogs({
events: [sessionCreatedEvent()],
logs: receipt.logs,
});
expect(logs[0]?.args.signer).toBe(TEST_ACCOUNT_A.address);
});

it("should allow adding granular session keys", async () => {
const receipt = await sendAndConfirmTransaction({
account: account,
transaction: createSessionKey({
account: account,
callPolicies: [
{
constraints: [
{
condition: Condition.Unconstrained,
index: 0n,
refValue:
"0x0000000000000000000000000000000000000000000000000000000000000000",
},
],
maxValuePerUse: 0n,
selector: "0x00000000",
target: ZERO_ADDRESS,
valueLimit: {
limit: 0n,
limitType: LimitType.Unlimited,
period: 0n,
},
},
],
contract: accountContract,
durationInSeconds: 86400, // 1 day
grantFullPermissions: false,
sessionKeyAddress: TEST_ACCOUNT_A.address,
transferPolicies: [
{
maxValuePerUse: 0n,
target: ZERO_ADDRESS,
valueLimit: {
limit: 0n,
limitType: 0,
period: 0n,
},
},
],
}),
});
const logs = parseEventLogs({
events: [sessionCreatedEvent()],
logs: receipt.logs,
});
expect(logs[0]?.args.signer).toBe(TEST_ACCOUNT_A.address);
expect(logs[0]?.args.sessionSpec.callPolicies).toHaveLength(1);
expect(logs[0]?.args.sessionSpec.transferPolicies).toHaveLength(1);
});
},
);
Loading
Loading