Skip to content

Commit 4386865

Browse files
committed
[SDK] EIP-7702 Session Keys
1 parent bbff67d commit 4386865

File tree

4 files changed

+337
-1
lines changed

4 files changed

+337
-1
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { randomBytesHex } from "../../../utils/random.js";
2+
import type { BaseTransactionOptions } from "../../../transaction/types.js";
3+
import type { Account } from "../../../wallets/interfaces/wallet.js";
4+
import { CallSpecRequest, ConstraintRequest, SessionSpecRequest, TransferSpecRequest, UsageLimitRequest, type CallSpecInput, type TransferSpecInput } from "./types.js";
5+
import { isCreateSessionWithSigSupported, createSessionWithSig } from "../__generated__/MinimalAccount/write/createSessionWithSig.js";
6+
7+
/**
8+
* @extension ERC7702
9+
*/
10+
export type CreateSessionKeyOptions = {
11+
/**
12+
* The admin account that will perform the operation.
13+
*/
14+
account: Account;
15+
/**
16+
* The address to add as a session key.
17+
*/
18+
sessionKeyAddress: `0x${string}`;
19+
/**
20+
* How long the session key should be valid for, in seconds.
21+
*/
22+
durationInSeconds: number;
23+
/**
24+
* Whether to grant full execution permissions to the session key.
25+
*/
26+
grantFullPermissions?: boolean;
27+
/**
28+
* Smart contract interaction policies to apply to the session key, ignored if grantFullPermissions is true.
29+
*/
30+
callPolicies?: CallSpecInput[];
31+
/**
32+
* Value transfer policies to apply to the session key, ignored if grantFullPermissions is true.
33+
*/
34+
transferPolicies?: TransferSpecInput[];
35+
};
36+
37+
/**
38+
* Creates session key permissions for a specified address.
39+
* @param options - The options for the createSessionKey function.
40+
* @param {Contract} options.contract - The EIP-7702 smart EOA contract to create the session key from
41+
* @returns The transaction object to be sent.
42+
* @example
43+
* ```ts
44+
* import { createSessionKey } from 'thirdweb/extensions/7702';
45+
* import { sendTransaction } from 'thirdweb';
46+
*
47+
* const transaction = createSessionKey({
48+
* account: account,
49+
* contract: accountContract,
50+
* sessionKeyAddress: TEST_ACCOUNT_A.address,
51+
* durationInSeconds: 86400, // 1 day
52+
* grantFullPermissions: true
53+
*})
54+
*
55+
* await sendTransaction({ transaction, account });
56+
* ```
57+
* @extension ERC7702
58+
*/
59+
export function createSessionKey(
60+
options: BaseTransactionOptions<CreateSessionKeyOptions>,
61+
) {
62+
const { contract, account, sessionKeyAddress, durationInSeconds, grantFullPermissions, callPolicies, transferPolicies } = options;
63+
64+
return createSessionWithSig({
65+
async asyncParams() {
66+
const req = {
67+
signer: sessionKeyAddress,
68+
isWildcard: grantFullPermissions ?? true,
69+
expiresAt: BigInt(Math.floor(Date.now() / 1000) + durationInSeconds),
70+
callPolicies: (callPolicies || []).map(policy => ({
71+
target: policy.target,
72+
selector: policy.selector,
73+
maxValuePerUse: policy.maxValuePerUse || BigInt(0),
74+
valueLimit: policy.valueLimit || { limitType: 0, limit: BigInt(0), period: BigInt(0) },
75+
constraints: (policy.constraints || []).map(constraint => ({
76+
condition: constraint.condition,
77+
index: constraint.index || BigInt(0),
78+
refValue: constraint.refValue || "0x",
79+
limit: constraint.limit || { limitType: 0, limit: BigInt(0), period: BigInt(0) }
80+
}))
81+
})),
82+
transferPolicies: (transferPolicies || []).map(policy => ({
83+
target: policy.target,
84+
maxValuePerUse: policy.maxValuePerUse || BigInt(0),
85+
valueLimit: policy.valueLimit || { limitType: 0, limit: BigInt(0), period: BigInt(0) }
86+
})),
87+
uid: await randomBytesHex(),
88+
};
89+
90+
const signature = await account.signTypedData({
91+
domain: {
92+
chainId: contract.chain.id,
93+
name: "MinimalAccount",
94+
verifyingContract: contract.address,
95+
version: "1",
96+
},
97+
message: req,
98+
primaryType: "SessionSpec",
99+
types: {
100+
SessionSpec: SessionSpecRequest,
101+
CallSpec: CallSpecRequest,
102+
Constraint: ConstraintRequest,
103+
TransferSpec: TransferSpecRequest,
104+
UsageLimit: UsageLimitRequest,
105+
},
106+
});
107+
108+
return { sessionSpec: req, signature };
109+
},
110+
contract,
111+
});
112+
}
113+
114+
/**
115+
* Checks if the `isCreateSessionKeySupported` method is supported by the given contract.
116+
* @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.
117+
* @returns A boolean indicating if the `isAddSessionKeySupported` method is supported.
118+
* @extension ERC7702
119+
* @example
120+
* ```ts
121+
* import { isCreateSessionKeySupported } from "thirdweb/extensions/erc7702";
122+
*
123+
* const supported = isCreateSessionKeySupported(["0x..."]);
124+
* ```
125+
*/
126+
export function isCreateSessionKeySupported(availableSelectors: string[]) {
127+
return isCreateSessionWithSigSupported(availableSelectors);
128+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { beforeAll, describe, expect, it } from "vitest";
2+
import { TEST_CLIENT } from "../../../../test/src/test-clients.js";
3+
import {
4+
TEST_ACCOUNT_A,
5+
TEST_ACCOUNT_B,
6+
} from "../../../../test/src/test-wallets.js";
7+
import { ZERO_ADDRESS } from "../../../constants/addresses.js";
8+
import {
9+
getContract,
10+
type ThirdwebContract,
11+
} from "../../../contract/contract.js";
12+
import { parseEventLogs } from "../../../event/actions/parse-logs.js";
13+
import { sendAndConfirmTransaction } from "../../../transaction/actions/send-and-confirm-transaction.js";
14+
import { sessionCreatedEvent } from "../__generated__/MinimalAccount/events/SessionCreated.js";
15+
import { createSessionKey } from "./createSessionKey.js";
16+
import { inAppWallet } from "src/wallets/in-app/web/in-app.js";
17+
import { prepareTransaction } from "src/transaction/prepare-transaction.js";
18+
import type { Account } from "src/wallets/interfaces/wallet.js";
19+
import { defineChain } from "src/chains/utils.js";
20+
21+
describe.runIf(process.env.TW_SECRET_KEY)("Session Key Behavior", {
22+
retry: 0,
23+
timeout: 240_000,
24+
},() => {
25+
let chainId = 11155111;
26+
let account: Account;
27+
let accountContract: ThirdwebContract;
28+
29+
beforeAll(async () => {
30+
// Create 7702 Smart EOA
31+
const wallet = inAppWallet({
32+
executionMode: {
33+
mode: "EIP7702",
34+
sponsorGas: true,
35+
},
36+
});
37+
account = await wallet.connect({
38+
chain: defineChain(chainId),
39+
client: TEST_CLIENT,
40+
strategy: "guest",
41+
})
42+
43+
// Send a null tx to trigger deploy/upgrade
44+
await sendAndConfirmTransaction({
45+
account: account,
46+
transaction: prepareTransaction({
47+
to: account.address,
48+
chain: defineChain(chainId),
49+
client: TEST_CLIENT,
50+
value: 0n,
51+
}),
52+
});
53+
54+
// Will auto resolve abi since it's deployed
55+
accountContract = getContract({
56+
address: account.address,
57+
chain: defineChain(chainId),
58+
client: TEST_CLIENT,
59+
});
60+
}, 120_000);
61+
62+
it("should allow adding adminlike session keys", async () => {
63+
const receipt = await sendAndConfirmTransaction({
64+
account: account,
65+
transaction: createSessionKey({
66+
account: account,
67+
contract: accountContract,
68+
sessionKeyAddress: TEST_ACCOUNT_A.address,
69+
durationInSeconds: 86400, // 1 day
70+
grantFullPermissions: true
71+
}),
72+
});
73+
const logs = parseEventLogs({
74+
events: [sessionCreatedEvent()],
75+
logs: receipt.logs,
76+
});
77+
expect(logs[0]?.args.signer).toBe(TEST_ACCOUNT_A.address);
78+
});
79+
80+
it("should allow adding granular session keys", async () => {
81+
const receipt = await sendAndConfirmTransaction({
82+
account: account,
83+
transaction: createSessionKey({
84+
account: account,
85+
contract: accountContract,
86+
sessionKeyAddress: TEST_ACCOUNT_A.address,
87+
durationInSeconds: 86400, // 1 day
88+
grantFullPermissions: false,
89+
callPolicies: [
90+
{
91+
target: ZERO_ADDRESS,
92+
selector: "0x00000000",
93+
maxValuePerUse: 0n,
94+
valueLimit: {
95+
limitType: 0,
96+
limit: 0n,
97+
period: 0n,
98+
},
99+
constraints: [],
100+
},
101+
],
102+
transferPolicies: [
103+
{
104+
target: ZERO_ADDRESS,
105+
maxValuePerUse: 0n,
106+
valueLimit: {
107+
limitType: 0,
108+
limit: 0n,
109+
period: 0n,
110+
},
111+
},
112+
],
113+
}),
114+
});
115+
const logs = parseEventLogs({
116+
events: [sessionCreatedEvent()],
117+
logs: receipt.logs,
118+
});
119+
expect(logs[0]?.args.signer).toBe(TEST_ACCOUNT_A.address);
120+
expect(logs[0]?.args.sessionSpec.callPolicies).toHaveLength(1);
121+
expect(logs[0]?.args.sessionSpec.transferPolicies).toHaveLength(1);
122+
});
123+
});
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
2+
/* ────────────────────────────────
3+
Input types
4+
──────────────────────────────── */
5+
6+
/* ---------- UsageLimit ---------- */
7+
export interface UsageLimitInput {
8+
limitType: number;
9+
limit: bigint;
10+
period: bigint;
11+
}
12+
13+
/* ---------- Constraint ---------- */
14+
export interface ConstraintInput {
15+
condition: number;
16+
index: bigint;
17+
refValue: `0x${string}`;
18+
limit?: UsageLimitInput;
19+
}
20+
21+
/* ---------- CallSpec ---------- */
22+
export interface CallSpecInput {
23+
target: `0x${string}`;
24+
selector: `0x${string}`;
25+
maxValuePerUse?: bigint;
26+
valueLimit?: UsageLimitInput;
27+
constraints?: ConstraintInput[];
28+
}
29+
30+
/* ---------- TransferSpec ---------- */
31+
export interface TransferSpecInput {
32+
target: `0x${string}`;
33+
maxValuePerUse?: bigint;
34+
valueLimit?: UsageLimitInput;
35+
}
36+
37+
/* ---------- SessionSpec ---------- */
38+
export interface SessionSpecInput {
39+
signer: `0x${string}`;
40+
isWildcard?: boolean;
41+
expiresAt?: bigint;
42+
callPolicies?: CallSpecInput[];
43+
transferPolicies?: TransferSpecInput[];
44+
uid: `0x${string}`;
45+
}
46+
47+
/* ────────────────────────────────
48+
EIP-712 structs
49+
──────────────────────────────── */
50+
51+
export const UsageLimitRequest = [
52+
{ name: "limitType", type: "uint8" },
53+
{ name: "limit", type: "uint256" },
54+
{ name: "period", type: "uint256" },
55+
] as const;
56+
57+
export const ConstraintRequest = [
58+
{ name: "condition", type: "uint8" },
59+
{ name: "index", type: "uint64" },
60+
{ name: "refValue", type: "bytes32" },
61+
{ name: "limit", type: "UsageLimit" },
62+
] as const;
63+
64+
export const CallSpecRequest = [
65+
{ name: "target", type: "address" },
66+
{ name: "selector", type: "bytes4" },
67+
{ name: "maxValuePerUse", type: "uint256" },
68+
{ name: "valueLimit", type: "UsageLimit" },
69+
{ name: "constraints", type: "Constraint[]" },
70+
] as const;
71+
72+
export const TransferSpecRequest = [
73+
{ name: "target", type: "address" },
74+
{ name: "maxValuePerUse", type: "uint256" },
75+
{ name: "valueLimit", type: "UsageLimit" },
76+
] as const;
77+
78+
export const SessionSpecRequest = [
79+
{ name: "signer", type: "address" },
80+
{ name: "isWildcard", type: "bool" },
81+
{ name: "expiresAt", type: "uint256" },
82+
{ name: "callPolicies", type: "CallSpec[]" },
83+
{ name: "transferPolicies", type: "TransferSpec[]" },
84+
{ name: "uid", type: "bytes32" },
85+
] as const;

packages/thirdweb/src/wallets/in-app/core/eip7702/minimal-account.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
import type { BundlerOptions } from "../../../smart/types.js";
2525

2626
const MINIMAL_ACCOUNT_IMPLEMENTATION_ADDRESS =
27-
"0xbaC7e770af15d130Cd72838ff386f14FBF3e9a3D";
27+
"0xD6999651Fc0964B9c6B444307a0ab20534a66560";
2828

2929
export const create7702MinimalAccount = (args: {
3030
client: ThirdwebClient;

0 commit comments

Comments
 (0)