Skip to content

Commit acd5656

Browse files
authored
[SDK] EIP-7702 Session Keys (#7432)
1 parent 6d1d344 commit acd5656

File tree

7 files changed

+433
-1
lines changed

7 files changed

+433
-1
lines changed

.changeset/chatty-chairs-mate.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": patch
3+
---
4+
5+
Introduces Session Keys to EIP-7702-powered In-App Wallets via a new createSessionKey extension

packages/thirdweb/src/exports/wallets/in-app.native.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
// --- KEEEP IN SYNC with exports/wallets/in-app.ts ---
22

3+
//ACCOUNT
4+
export {
5+
type CreateSessionKeyOptions,
6+
createSessionKey,
7+
isCreateSessionKeySupported,
8+
} from "../../extensions/erc7702/account/createSessionKey.js";
9+
export type {
10+
Condition,
11+
LimitType,
12+
} from "../../extensions/erc7702/account/types.js";
313
export type {
414
GetAuthenticatedUserParams,
515
MultiStepAuthArgsType,

packages/thirdweb/src/exports/wallets/in-app.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
// --- KEEEP IN SYNC with exports/wallets/in-app.native.ts ---
22

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

0 commit comments

Comments
 (0)