Skip to content

Commit 9663079

Browse files
[SDK] feat: Add session keys to smart wallet options (#6194)
1 parent 1f6bb7c commit 9663079

File tree

4 files changed

+121
-0
lines changed

4 files changed

+121
-0
lines changed

.changeset/tender-dolls-tan.md

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
---
2+
"thirdweb": minor
3+
---
4+
5+
Added session keys to smart wallet options
6+
7+
You can now pass a `sessionKey` to the `smartWallet` options function to immediately add a session key to the smart wallet upon connection.
8+
9+
This is great in combination with an engine backend wallet! Let's you act on behalf of the user from your backend, making executing transactions as easy as a REST API call. Also unblocks automations, like renewing a subscription, or paying for a service.
10+
11+
```ts
12+
const wallet = smartWallet({
13+
sessionKey: {
14+
address: "0x...", // the session key address (ex: engine backend wallet)
15+
permissions: {
16+
approvedTargets: ["0x..."], // allowed contract addresses (or * for all)
17+
nativeTokenLimitPerTransaction: 0.1, // max spend per transaction in ETH
18+
permissionEndTimestamp: new Date(Date.now() + 1000 * 60 * 60), // expiration date
19+
},
20+
},
21+
});
22+
23+
// this will connect the user wallet and add the session key if not already added
24+
await wallet.connect({
25+
client: TEST_CLIENT,
26+
personalAccount,
27+
});
28+
```
29+
30+
You can also pass the `sessionKey` to the `ConnectButton`, `ConnectEmbed` components and `useConnect` hook.
31+
32+
```tsx
33+
<ConnectButton
34+
client={client}
35+
accountAbstraction={{
36+
chain,
37+
sponsorGas: true,
38+
sessionKey: {
39+
address: "0x...",
40+
permissions: {
41+
approvedTargets: "*",
42+
},
43+
},
44+
}}
45+
/>
46+
```
47+
48+
Also works for the `inAppWallet` `smartAccount` option!
49+
50+
```ts
51+
const wallet = inAppWallet({
52+
smartAccount: {
53+
chain,
54+
sponsorGas: true,
55+
sessionKey: {
56+
address: "0x...",
57+
permissions: {
58+
approvedTargets: "*",
59+
},
60+
},
61+
},
62+
});
63+
```

packages/thirdweb/src/wallets/smart/index.ts

+28
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import type { ThirdwebClient } from "../../client/client.js";
66
import { type ThirdwebContract, getContract } from "../../contract/contract.js";
77
import { allowance } from "../../extensions/erc20/__generated__/IERC20/read/allowance.js";
88
import { approve } from "../../extensions/erc20/write/approve.js";
9+
import { isActiveSigner } from "../../extensions/erc4337/__generated__/IAccountPermissions/read/isActiveSigner.js";
10+
import { addSessionKey } from "../../extensions/erc4337/account/addSessionKey.js";
11+
import { sendTransaction } from "../../transaction/actions/send-transaction.js";
912
import { toSerializableTransaction } from "../../transaction/actions/to-serializable-transaction.js";
1013
import type { WaitForReceiptOptions } from "../../transaction/actions/wait-for-tx-receipt.js";
1114
import {
@@ -16,6 +19,7 @@ import type { PreparedTransaction } from "../../transaction/prepare-transaction.
1619
import { readContract } from "../../transaction/read-contract.js";
1720
import { getAddress } from "../../utils/address.js";
1821
import { isZkSyncChain } from "../../utils/any-evm/zksync/isZkSyncChain.js";
22+
import { isContractDeployed } from "../../utils/bytecode/is-contract-deployed.js";
1923
import type { Hex } from "../../utils/encoding/hex.js";
2024
import { resolvePromisedValue } from "../../utils/promise/resolve-promised-value.js";
2125
import { parseTypedData } from "../../utils/signatures/helpers/parse-typed-data.js";
@@ -164,6 +168,30 @@ export async function connectSmartAccount(
164168
adminAccountToSmartAccountMap.set(personalAccount, account);
165169
smartAccountToAdminAccountMap.set(account, personalAccount);
166170

171+
if (options.sessionKey) {
172+
let hasSessionKey = false;
173+
// check if already added
174+
const accountDeployed = await isContractDeployed(accountContract);
175+
if (accountDeployed) {
176+
hasSessionKey = await isActiveSigner({
177+
contract: accountContract,
178+
signer: options.sessionKey.address,
179+
});
180+
}
181+
if (!hasSessionKey) {
182+
const transaction = addSessionKey({
183+
account: personalAccount,
184+
contract: accountContract,
185+
permissions: options.sessionKey.permissions,
186+
sessionKeyAddress: options.sessionKey.address,
187+
});
188+
await sendTransaction({
189+
account: account,
190+
transaction,
191+
});
192+
}
193+
}
194+
167195
return [account, chain] as const;
168196
}
169197

packages/thirdweb/src/wallets/smart/smart-wallet-integration.test.ts

+25
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from "../../exports/extensions/erc4337.js";
1616
import { balanceOf } from "../../extensions/erc1155/__generated__/IERC1155/read/balanceOf.js";
1717
import { claimTo } from "../../extensions/erc1155/drops/write/claimTo.js";
18+
import { isActiveSigner } from "../../extensions/erc4337/__generated__/IAccountPermissions/read/isActiveSigner.js";
1819
import { setContractURI } from "../../extensions/marketplace/__generated__/IMarketplace/write/setContractURI.js";
1920
import { estimateGasCost } from "../../transaction/actions/estimate-gas-cost.js";
2021
import { sendAndConfirmTransaction } from "../../transaction/actions/send-and-confirm-transaction.js";
@@ -458,5 +459,29 @@ describe.runIf(process.env.TW_SECRET_KEY).sequential(
458459
}),
459460
).rejects.toThrowError(/AA21 didn't pay prefund/);
460461
});
462+
463+
it("can use a session key right after connecting", async () => {
464+
const sessionKey = await generateAccount({ client });
465+
const wallet = smartWallet({
466+
chain,
467+
gasless: true,
468+
sessionKey: {
469+
address: sessionKey.address,
470+
permissions: {
471+
approvedTargets: "*",
472+
},
473+
},
474+
});
475+
await wallet.connect({
476+
client: TEST_CLIENT,
477+
personalAccount,
478+
});
479+
480+
const isSigner = await isActiveSigner({
481+
contract: accountContract,
482+
signer: sessionKey.address,
483+
});
484+
expect(isSigner).toEqual(true);
485+
});
461486
},
462487
);

packages/thirdweb/src/wallets/smart/types.ts

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type * as ox__TypedData from "ox/TypedData";
33
import type { Chain } from "../../chains/types.js";
44
import type { ThirdwebClient } from "../../client/client.js";
55
import type { ThirdwebContract } from "../../contract/contract.js";
6+
import type { AccountPermissions } from "../../extensions/erc4337/account/types.js";
67
import type { PreparedTransaction } from "../../transaction/prepare-transaction.js";
78
import type { TransactionReceipt } from "../../transaction/types.js";
89
import type { Hex } from "../../utils/encoding/hex.js";
@@ -21,6 +22,10 @@ export type SmartWalletOptions = Prettify<
2122
{
2223
chain: Chain; // TODO consider making default chain optional
2324
factoryAddress?: string;
25+
sessionKey?: {
26+
address: string;
27+
permissions: AccountPermissions;
28+
};
2429
overrides?: {
2530
bundlerUrl?: string;
2631
accountAddress?: string;

0 commit comments

Comments
 (0)