Skip to content

Commit 6b2a90a

Browse files
[SDK] feat: Add session key update handling and expose utility (#6217)
1 parent cf53184 commit 6b2a90a

File tree

5 files changed

+157
-14
lines changed

5 files changed

+157
-14
lines changed

.changeset/good-carpets-dream.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+
Handle updating session keys with new params and expose `shouldUpdateSessionKey` from `extensions/erc4337`

packages/thirdweb/src/exports/extensions/erc4337.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export {
1616
type AddSessionKeyOptions,
1717
addSessionKey,
1818
isAddSessionKeySupported,
19+
shouldUpdateSessionKey,
1920
} from "../../extensions/erc4337/account/addSessionKey.js";
2021

2122
export {

packages/thirdweb/src/extensions/erc4337/account/addSessionKey.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
import { ZERO_ADDRESS } from "../../../constants/addresses.js";
2+
import type { ThirdwebContract } from "../../../contract/contract.js";
13
import type { BaseTransactionOptions } from "../../../transaction/types.js";
4+
import { isContractDeployed } from "../../../utils/bytecode/is-contract-deployed.js";
5+
import { toWei } from "../../../utils/units.js";
26
import type { Account } from "../../../wallets/interfaces/wallet.js";
7+
import { getPermissionsForSigner } from "../__generated__/IAccountPermissions/read/getPermissionsForSigner.js";
38
import {
49
isSetPermissionsForSignerSupported,
510
setPermissionsForSigner,
@@ -86,3 +91,78 @@ export function addSessionKey(
8691
export function isAddSessionKeySupported(availableSelectors: string[]) {
8792
return isSetPermissionsForSignerSupported(availableSelectors);
8893
}
94+
95+
/**
96+
* Checks if the session key should be updated.
97+
* @param currentPermissions - The current permissions of the session key.
98+
* @param newPermissions - The new permissions to set for the session key.
99+
* @returns A boolean indicating if the session key should be updated.
100+
*/
101+
export async function shouldUpdateSessionKey(args: {
102+
accountContract: ThirdwebContract;
103+
sessionKeyAddress: string;
104+
newPermissions: AccountPermissions;
105+
}): Promise<boolean> {
106+
const { accountContract, sessionKeyAddress, newPermissions } = args;
107+
108+
// check if account is deployed
109+
const accountDeployed = await isContractDeployed(accountContract);
110+
if (!accountDeployed) {
111+
return true;
112+
}
113+
114+
// get current permissions
115+
const currentPermissions = await getPermissionsForSigner({
116+
contract: accountContract,
117+
signer: sessionKeyAddress,
118+
});
119+
// check end time validity
120+
if (
121+
currentPermissions.endTimestamp &&
122+
currentPermissions.endTimestamp < Math.floor(new Date().getTime() / 1000)
123+
) {
124+
return true;
125+
}
126+
127+
// check targets
128+
if (
129+
!areSessionKeyContractTargetsEqual(
130+
currentPermissions.approvedTargets,
131+
newPermissions.approvedTargets,
132+
)
133+
) {
134+
return true;
135+
}
136+
137+
// check if the new native token limit is greater than the current one
138+
if (
139+
toWei(newPermissions.nativeTokenLimitPerTransaction?.toString() ?? "0") >
140+
currentPermissions.nativeTokenLimitPerTransaction
141+
) {
142+
return true;
143+
}
144+
145+
return false;
146+
}
147+
148+
function areSessionKeyContractTargetsEqual(
149+
currentTargets: readonly string[],
150+
newTargets: string[] | "*",
151+
): boolean {
152+
// Handle the case where approvedTargets is "*"
153+
if (
154+
newTargets === "*" &&
155+
currentTargets.length === 1 &&
156+
currentTargets[0] === ZERO_ADDRESS
157+
) {
158+
return true;
159+
}
160+
if (newTargets !== "*") {
161+
return newTargets
162+
.map((target) => target.toLowerCase())
163+
.every((target) =>
164+
currentTargets.map((t) => t.toLowerCase()).includes(target),
165+
);
166+
}
167+
return false;
168+
}

packages/thirdweb/src/extensions/erc4337/account/permissions.test.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { beforeAll, describe, expect, it } from "vitest";
22
import { ANVIL_CHAIN } from "../../../../test/src/chains.js";
33
import { TEST_CLIENT } from "../../../../test/src/test-clients.js";
4+
import { USDT_CONTRACT_ADDRESS } from "../../../../test/src/test-contracts.js";
45
import {
56
TEST_ACCOUNT_A,
67
TEST_ACCOUNT_B,
8+
TEST_ACCOUNT_C,
79
} from "../../../../test/src/test-wallets.js";
810
import { ZERO_ADDRESS } from "../../../constants/addresses.js";
911
import {
@@ -20,7 +22,7 @@ import { adminUpdatedEvent } from "../__generated__/IAccountPermissions/events/A
2022
import { signerPermissionsUpdatedEvent } from "../__generated__/IAccountPermissions/events/SignerPermissionsUpdated.js";
2123
import { getAllAdmins } from "../__generated__/IAccountPermissions/read/getAllAdmins.js";
2224
import { addAdmin } from "./addAdmin.js";
23-
import { addSessionKey } from "./addSessionKey.js";
25+
import { addSessionKey, shouldUpdateSessionKey } from "./addSessionKey.js";
2426
import { removeAdmin } from "./removeAdmin.js";
2527

2628
describe.runIf(process.env.TW_SECRET_KEY)("Account Permissions", () => {
@@ -129,5 +131,62 @@ describe.runIf(process.env.TW_SECRET_KEY)("Account Permissions", () => {
129131
expect(logs[0]?.args.permissions.approvedTargets).toStrictEqual([
130132
ZERO_ADDRESS,
131133
]);
134+
135+
expect(
136+
await shouldUpdateSessionKey({
137+
accountContract,
138+
sessionKeyAddress: TEST_ACCOUNT_A.address,
139+
newPermissions: {
140+
approvedTargets: "*",
141+
},
142+
}),
143+
).toBe(false);
144+
145+
expect(
146+
await shouldUpdateSessionKey({
147+
accountContract,
148+
sessionKeyAddress: TEST_ACCOUNT_A.address,
149+
newPermissions: {
150+
approvedTargets: "*",
151+
nativeTokenLimitPerTransaction: 0,
152+
},
153+
}),
154+
).toBe(false);
155+
156+
expect(
157+
await shouldUpdateSessionKey({
158+
accountContract,
159+
sessionKeyAddress: TEST_ACCOUNT_A.address,
160+
newPermissions: {
161+
approvedTargets: [USDT_CONTRACT_ADDRESS],
162+
},
163+
}),
164+
).toBe(true);
165+
166+
expect(
167+
await shouldUpdateSessionKey({
168+
accountContract,
169+
sessionKeyAddress: TEST_ACCOUNT_A.address,
170+
newPermissions: {
171+
approvedTargets: "*",
172+
nativeTokenLimitPerTransaction: 0.1,
173+
},
174+
}),
175+
).toBe(true);
176+
});
177+
178+
it("should update session key if account is not deployed", async () => {
179+
const shouldUpdate = await shouldUpdateSessionKey({
180+
accountContract: getContract({
181+
address: TEST_ACCOUNT_C.address,
182+
chain: ANVIL_CHAIN,
183+
client: TEST_CLIENT,
184+
}),
185+
sessionKeyAddress: TEST_ACCOUNT_A.address,
186+
newPermissions: {
187+
approvedTargets: "*",
188+
},
189+
});
190+
expect(shouldUpdate).toBe(true);
132191
});
133192
});

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

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ 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";
9+
import {
10+
addSessionKey,
11+
shouldUpdateSessionKey,
12+
} from "../../extensions/erc4337/account/addSessionKey.js";
1113
import { sendTransaction } from "../../transaction/actions/send-transaction.js";
1214
import { toSerializableTransaction } from "../../transaction/actions/to-serializable-transaction.js";
1315
import type { WaitForReceiptOptions } from "../../transaction/actions/wait-for-tx-receipt.js";
@@ -19,7 +21,6 @@ import type { PreparedTransaction } from "../../transaction/prepare-transaction.
1921
import { readContract } from "../../transaction/read-contract.js";
2022
import { getAddress } from "../../utils/address.js";
2123
import { isZkSyncChain } from "../../utils/any-evm/zksync/isZkSyncChain.js";
22-
import { isContractDeployed } from "../../utils/bytecode/is-contract-deployed.js";
2324
import type { Hex } from "../../utils/encoding/hex.js";
2425
import { resolvePromisedValue } from "../../utils/promise/resolve-promised-value.js";
2526
import { parseTypedData } from "../../utils/signatures/helpers/parse-typed-data.js";
@@ -169,16 +170,13 @@ export async function connectSmartAccount(
169170
smartAccountToAdminAccountMap.set(account, personalAccount);
170171

171172
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) {
173+
if (
174+
await shouldUpdateSessionKey({
175+
accountContract,
176+
sessionKeyAddress: options.sessionKey.address,
177+
newPermissions: options.sessionKey.permissions,
178+
})
179+
) {
182180
const transaction = addSessionKey({
183181
account: personalAccount,
184182
contract: accountContract,

0 commit comments

Comments
 (0)