Skip to content
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/good-carpets-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"thirdweb": patch
---

Handle updating session keys with new params and expose `shouldUpdateSessionKey` from `extensions/erc4337`
1 change: 1 addition & 0 deletions packages/thirdweb/src/exports/extensions/erc4337.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export {
type AddSessionKeyOptions,
addSessionKey,
isAddSessionKeySupported,
shouldUpdateSessionKey,
} from "../../extensions/erc4337/account/addSessionKey.js";

export {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { ZERO_ADDRESS } from "../../../constants/addresses.js";
import type { ThirdwebContract } from "../../../contract/contract.js";
import type { BaseTransactionOptions } from "../../../transaction/types.js";
import { isContractDeployed } from "../../../utils/bytecode/is-contract-deployed.js";
import { toWei } from "../../../utils/units.js";
import type { Account } from "../../../wallets/interfaces/wallet.js";
import { getPermissionsForSigner } from "../__generated__/IAccountPermissions/read/getPermissionsForSigner.js";
import {
isSetPermissionsForSignerSupported,
setPermissionsForSigner,
Expand Down Expand Up @@ -86,3 +91,78 @@
export function isAddSessionKeySupported(availableSelectors: string[]) {
return isSetPermissionsForSignerSupported(availableSelectors);
}

/**
* Checks if the session key should be updated.
* @param currentPermissions - The current permissions of the session key.
* @param newPermissions - The new permissions to set for the session key.
* @returns A boolean indicating if the session key should be updated.
*/
export async function shouldUpdateSessionKey(args: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also check on startDate and isAdmin values in our current version of this logic, but not sure if that's important here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

start date is a bit weird, decided to leave it alone for this. And yeah admin should be separate i think?

accountContract: ThirdwebContract;
sessionKeyAddress: string;
newPermissions: AccountPermissions;
}): Promise<boolean> {
const { accountContract, sessionKeyAddress, newPermissions } = args;

// check if account is deployed
const accountDeployed = await isContractDeployed(accountContract);
if (!accountDeployed) {
return true;
}

// get current permissions
const currentPermissions = await getPermissionsForSigner({
contract: accountContract,
signer: sessionKeyAddress,
});
// check end time validity
if (
currentPermissions.endTimestamp &&
currentPermissions.endTimestamp < Math.floor(new Date().getTime() / 1000)
) {
return true;

Check warning on line 124 in packages/thirdweb/src/extensions/erc4337/account/addSessionKey.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L124 was not covered by tests
}

// check targets
if (
!areSessionKeyContractTargetsEqual(
currentPermissions.approvedTargets,
newPermissions.approvedTargets,
)
Comment on lines +128 to +132
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of checking for equality, it could check for subests to minimize the need for creating sessions (i.e., currentPermissions.approvedTargets includes at least all of newPermissions.approvedTargets)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! Looks great

) {
return true;
}

// check if the new native token limit is greater than the current one
if (
toWei(newPermissions.nativeTokenLimitPerTransaction?.toString() ?? "0") >
currentPermissions.nativeTokenLimitPerTransaction
) {
return true;
}

return false;
}

function areSessionKeyContractTargetsEqual(
currentTargets: readonly string[],
newTargets: string[] | "*",
): boolean {
// Handle the case where approvedTargets is "*"
if (
newTargets === "*" &&
currentTargets.length === 1 &&
currentTargets[0] === ZERO_ADDRESS
) {
return true;
}
if (newTargets !== "*") {
return newTargets
.map((target) => target.toLowerCase())
.every((target) =>
currentTargets.map((t) => t.toLowerCase()).includes(target),
);
}
return false;
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { beforeAll, describe, expect, it } from "vitest";
import { ANVIL_CHAIN } from "../../../../test/src/chains.js";
import { TEST_CLIENT } from "../../../../test/src/test-clients.js";
import { USDT_CONTRACT_ADDRESS } from "../../../../test/src/test-contracts.js";
import {
TEST_ACCOUNT_A,
TEST_ACCOUNT_B,
TEST_ACCOUNT_C,
} from "../../../../test/src/test-wallets.js";
import { ZERO_ADDRESS } from "../../../constants/addresses.js";
import {
Expand All @@ -20,7 +22,7 @@ import { adminUpdatedEvent } from "../__generated__/IAccountPermissions/events/A
import { signerPermissionsUpdatedEvent } from "../__generated__/IAccountPermissions/events/SignerPermissionsUpdated.js";
import { getAllAdmins } from "../__generated__/IAccountPermissions/read/getAllAdmins.js";
import { addAdmin } from "./addAdmin.js";
import { addSessionKey } from "./addSessionKey.js";
import { addSessionKey, shouldUpdateSessionKey } from "./addSessionKey.js";
import { removeAdmin } from "./removeAdmin.js";

describe.runIf(process.env.TW_SECRET_KEY)("Account Permissions", () => {
Expand Down Expand Up @@ -129,5 +131,62 @@ describe.runIf(process.env.TW_SECRET_KEY)("Account Permissions", () => {
expect(logs[0]?.args.permissions.approvedTargets).toStrictEqual([
ZERO_ADDRESS,
]);

expect(
await shouldUpdateSessionKey({
accountContract,
sessionKeyAddress: TEST_ACCOUNT_A.address,
newPermissions: {
approvedTargets: "*",
},
}),
).toBe(false);

expect(
await shouldUpdateSessionKey({
accountContract,
sessionKeyAddress: TEST_ACCOUNT_A.address,
newPermissions: {
approvedTargets: "*",
nativeTokenLimitPerTransaction: 0,
},
}),
).toBe(false);

expect(
await shouldUpdateSessionKey({
accountContract,
sessionKeyAddress: TEST_ACCOUNT_A.address,
newPermissions: {
approvedTargets: [USDT_CONTRACT_ADDRESS],
},
}),
).toBe(true);

expect(
await shouldUpdateSessionKey({
accountContract,
sessionKeyAddress: TEST_ACCOUNT_A.address,
newPermissions: {
approvedTargets: "*",
nativeTokenLimitPerTransaction: 0.1,
},
}),
).toBe(true);
});

it("should update session key if account is not deployed", async () => {
const shouldUpdate = await shouldUpdateSessionKey({
accountContract: getContract({
address: TEST_ACCOUNT_C.address,
chain: ANVIL_CHAIN,
client: TEST_CLIENT,
}),
sessionKeyAddress: TEST_ACCOUNT_A.address,
newPermissions: {
approvedTargets: "*",
},
});
expect(shouldUpdate).toBe(true);
});
});
24 changes: 11 additions & 13 deletions packages/thirdweb/src/wallets/smart/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import type { ThirdwebClient } from "../../client/client.js";
import { type ThirdwebContract, getContract } from "../../contract/contract.js";
import { allowance } from "../../extensions/erc20/__generated__/IERC20/read/allowance.js";
import { approve } from "../../extensions/erc20/write/approve.js";
import { isActiveSigner } from "../../extensions/erc4337/__generated__/IAccountPermissions/read/isActiveSigner.js";
import { addSessionKey } from "../../extensions/erc4337/account/addSessionKey.js";
import {
addSessionKey,
shouldUpdateSessionKey,
} from "../../extensions/erc4337/account/addSessionKey.js";
import { sendTransaction } from "../../transaction/actions/send-transaction.js";
import { toSerializableTransaction } from "../../transaction/actions/to-serializable-transaction.js";
import type { WaitForReceiptOptions } from "../../transaction/actions/wait-for-tx-receipt.js";
Expand All @@ -19,7 +21,6 @@ import type { PreparedTransaction } from "../../transaction/prepare-transaction.
import { readContract } from "../../transaction/read-contract.js";
import { getAddress } from "../../utils/address.js";
import { isZkSyncChain } from "../../utils/any-evm/zksync/isZkSyncChain.js";
import { isContractDeployed } from "../../utils/bytecode/is-contract-deployed.js";
import type { Hex } from "../../utils/encoding/hex.js";
import { resolvePromisedValue } from "../../utils/promise/resolve-promised-value.js";
import { parseTypedData } from "../../utils/signatures/helpers/parse-typed-data.js";
Expand Down Expand Up @@ -169,16 +170,13 @@ export async function connectSmartAccount(
smartAccountToAdminAccountMap.set(account, personalAccount);

if (options.sessionKey) {
let hasSessionKey = false;
// check if already added
const accountDeployed = await isContractDeployed(accountContract);
if (accountDeployed) {
hasSessionKey = await isActiveSigner({
contract: accountContract,
signer: options.sessionKey.address,
});
}
if (!hasSessionKey) {
if (
await shouldUpdateSessionKey({
accountContract,
sessionKeyAddress: options.sessionKey.address,
newPermissions: options.sessionKey.permissions,
})
) {
const transaction = addSessionKey({
account: personalAccount,
contract: accountContract,
Expand Down
Loading