diff --git a/.changeset/refactor-create-session-key-parameters.md b/.changeset/refactor-create-session-key-parameters.md new file mode 100644 index 00000000000..70786fe22c3 --- /dev/null +++ b/.changeset/refactor-create-session-key-parameters.md @@ -0,0 +1,11 @@ +--- +"thirdweb": patch +--- + +**Refactor createSessionKey function to remove contract parameter** + +- Remove `contract` input parameter from `createSessionKey` function in `thirdweb/wallets/in-app` +- Function now recreates contract internally using `account.address` with auto-resolved ABI +- Updated function signature to take `client` and `chain` directly instead of extending `BaseTransactionOptions` +- Updated related session key tests to match new function signature +- Simplified implementation while maintaining all existing functionality \ No newline at end of file diff --git a/packages/thirdweb/src/extensions/erc7702/__generated__/MinimalAccount/abi.ts b/packages/thirdweb/src/extensions/erc7702/__generated__/MinimalAccount/abi.ts new file mode 100644 index 00000000000..385a839d521 --- /dev/null +++ b/packages/thirdweb/src/extensions/erc7702/__generated__/MinimalAccount/abi.ts @@ -0,0 +1,434 @@ +/** + * Generated ABI for MinimalAccount contract + * @module MinimalAccount + */ +export const MINIMAL_ACCOUNT_ABI = [ + { + type: "error", + name: "AllowanceExceeded", + inputs: [ + { name: "allowanceUsage", type: "uint256" }, + { name: "limit", type: "uint256" }, + { name: "period", type: "uint64" }, + ], + }, + { + type: "error", + name: "CallPolicyViolated", + inputs: [ + { name: "target", type: "address" }, + { name: "selector", type: "bytes4" }, + ], + }, + { + type: "error", + name: "CallReverted", + inputs: [], + }, + { + type: "error", + name: "ConditionFailed", + inputs: [ + { name: "param", type: "bytes32" }, + { name: "refValue", type: "bytes32" }, + { name: "condition", type: "uint8" }, + ], + }, + { + type: "error", + name: "InvalidDataLength", + inputs: [ + { name: "actualLength", type: "uint256" }, + { name: "expectedLength", type: "uint256" }, + ], + }, + { + type: "error", + name: "InvalidSignature", + inputs: [ + { name: "msgSender", type: "address" }, + { name: "thisAddress", type: "address" }, + ], + }, + { + type: "error", + name: "LifetimeUsageExceeded", + inputs: [ + { name: "lifetimeUsage", type: "uint256" }, + { name: "limit", type: "uint256" }, + ], + }, + { + type: "error", + name: "MaxValueExceeded", + inputs: [ + { name: "value", type: "uint256" }, + { name: "maxValuePerUse", type: "uint256" }, + ], + }, + { + type: "error", + name: "NoCallsToExecute", + inputs: [], + }, + { + type: "error", + name: "SessionExpired", + inputs: [], + }, + { + type: "error", + name: "SessionExpiresTooSoon", + inputs: [], + }, + { + type: "error", + name: "SessionZeroSigner", + inputs: [], + }, + { + type: "error", + name: "TransferPolicyViolated", + inputs: [{ name: "target", type: "address" }], + }, + { + type: "error", + name: "UIDAlreadyProcessed", + inputs: [], + }, + { + type: "event", + name: "Executed", + inputs: [ + { name: "to", type: "address", indexed: true }, + { name: "value", type: "uint256" }, + { name: "data", type: "bytes" }, + ], + }, + { + type: "event", + name: "SessionCreated", + inputs: [ + { name: "signer", type: "address", indexed: true }, + { + name: "sessionSpec", + type: "tuple", + components: [ + { name: "signer", type: "address" }, + { name: "isWildcard", type: "bool" }, + { name: "expiresAt", type: "uint256" }, + { + name: "callPolicies", + type: "tuple[]", + components: [ + { name: "target", type: "address" }, + { name: "selector", type: "bytes4" }, + { name: "maxValuePerUse", type: "uint256" }, + { + name: "valueLimit", + type: "tuple", + components: [ + { name: "limitType", type: "uint8" }, + { name: "limit", type: "uint256" }, + { name: "period", type: "uint256" }, + ], + }, + { + name: "constraints", + type: "tuple[]", + components: [ + { name: "condition", type: "uint8" }, + { name: "index", type: "uint64" }, + { name: "refValue", type: "bytes32" }, + { + name: "limit", + type: "tuple", + components: [ + { name: "limitType", type: "uint8" }, + { name: "limit", type: "uint256" }, + { name: "period", type: "uint256" }, + ], + }, + ], + }, + ], + }, + { + name: "transferPolicies", + type: "tuple[]", + components: [ + { name: "target", type: "address" }, + { name: "maxValuePerUse", type: "uint256" }, + { + name: "valueLimit", + type: "tuple", + components: [ + { name: "limitType", type: "uint8" }, + { name: "limit", type: "uint256" }, + { name: "period", type: "uint256" }, + ], + }, + ], + }, + { name: "uid", type: "bytes32" }, + ], + }, + ], + }, + { + type: "function", + name: "createSessionWithSig", + inputs: [ + { + name: "sessionSpec", + type: "tuple", + components: [ + { name: "signer", type: "address" }, + { name: "isWildcard", type: "bool" }, + { name: "expiresAt", type: "uint256" }, + { + name: "callPolicies", + type: "tuple[]", + components: [ + { name: "target", type: "address" }, + { name: "selector", type: "bytes4" }, + { name: "maxValuePerUse", type: "uint256" }, + { + name: "valueLimit", + type: "tuple", + components: [ + { name: "limitType", type: "uint8" }, + { name: "limit", type: "uint256" }, + { name: "period", type: "uint256" }, + ], + }, + { + name: "constraints", + type: "tuple[]", + components: [ + { name: "condition", type: "uint8" }, + { name: "index", type: "uint64" }, + { name: "refValue", type: "bytes32" }, + { + name: "limit", + type: "tuple", + components: [ + { name: "limitType", type: "uint8" }, + { name: "limit", type: "uint256" }, + { name: "period", type: "uint256" }, + ], + }, + ], + }, + ], + }, + { + name: "transferPolicies", + type: "tuple[]", + components: [ + { name: "target", type: "address" }, + { name: "maxValuePerUse", type: "uint256" }, + { + name: "valueLimit", + type: "tuple", + components: [ + { name: "limitType", type: "uint8" }, + { name: "limit", type: "uint256" }, + { name: "period", type: "uint256" }, + ], + }, + ], + }, + { name: "uid", type: "bytes32" }, + ], + }, + { name: "signature", type: "bytes" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "eip712Domain", + inputs: [], + outputs: [ + { name: "fields", type: "bytes1" }, + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { name: "chainId", type: "uint256" }, + { name: "verifyingContract", type: "address" }, + { name: "salt", type: "bytes32" }, + { name: "extensions", type: "uint256[]" }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "execute", + inputs: [ + { + name: "calls", + type: "tuple[]", + components: [ + { name: "target", type: "address" }, + { name: "value", type: "uint256" }, + { name: "data", type: "bytes" }, + ], + }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "executeWithSig", + inputs: [ + { + name: "wrappedCalls", + type: "tuple", + components: [ + { + name: "calls", + type: "tuple[]", + components: [ + { name: "target", type: "address" }, + { name: "value", type: "uint256" }, + { name: "data", type: "bytes" }, + ], + }, + { name: "uid", type: "bytes32" }, + ], + }, + { name: "signature", type: "bytes" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + name: "getCallPoliciesForSigner", + inputs: [{ name: "signer", type: "address" }], + outputs: [ + { + name: "", + type: "tuple[]", + components: [ + { name: "target", type: "address" }, + { name: "selector", type: "bytes4" }, + { name: "maxValuePerUse", type: "uint256" }, + { + name: "valueLimit", + type: "tuple", + components: [ + { name: "limitType", type: "uint8" }, + { name: "limit", type: "uint256" }, + { name: "period", type: "uint256" }, + ], + }, + { + name: "constraints", + type: "tuple[]", + components: [ + { name: "condition", type: "uint8" }, + { name: "index", type: "uint64" }, + { name: "refValue", type: "bytes32" }, + { + name: "limit", + type: "tuple", + components: [ + { name: "limitType", type: "uint8" }, + { name: "limit", type: "uint256" }, + { name: "period", type: "uint256" }, + ], + }, + ], + }, + ], + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "getSessionExpirationForSigner", + inputs: [{ name: "signer", type: "address" }], + outputs: [{ name: "", type: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "getSessionStateForSigner", + inputs: [{ name: "signer", type: "address" }], + outputs: [ + { + name: "", + type: "tuple", + components: [ + { + name: "transferValue", + type: "tuple[]", + components: [ + { name: "remaining", type: "uint256" }, + { name: "target", type: "address" }, + { name: "selector", type: "bytes4" }, + { name: "index", type: "uint256" }, + ], + }, + { + name: "callValue", + type: "tuple[]", + components: [ + { name: "remaining", type: "uint256" }, + { name: "target", type: "address" }, + { name: "selector", type: "bytes4" }, + { name: "index", type: "uint256" }, + ], + }, + { + name: "callParams", + type: "tuple[]", + components: [ + { name: "remaining", type: "uint256" }, + { name: "target", type: "address" }, + { name: "selector", type: "bytes4" }, + { name: "index", type: "uint256" }, + ], + }, + ], + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "getTransferPoliciesForSigner", + inputs: [{ name: "signer", type: "address" }], + outputs: [ + { + name: "", + type: "tuple[]", + components: [ + { name: "target", type: "address" }, + { name: "maxValuePerUse", type: "uint256" }, + { + name: "valueLimit", + type: "tuple", + components: [ + { name: "limitType", type: "uint8" }, + { name: "limit", type: "uint256" }, + { name: "period", type: "uint256" }, + ], + }, + ], + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "isWildcardSigner", + inputs: [{ name: "signer", type: "address" }], + outputs: [{ name: "", type: "bool" }], + stateMutability: "view", + }, +] as const; diff --git a/packages/thirdweb/src/extensions/erc7702/account/createSessionKey.ts b/packages/thirdweb/src/extensions/erc7702/account/createSessionKey.ts index 0ee060d8273..34b3652be82 100644 --- a/packages/thirdweb/src/extensions/erc7702/account/createSessionKey.ts +++ b/packages/thirdweb/src/extensions/erc7702/account/createSessionKey.ts @@ -1,177 +1,124 @@ -import type { BaseTransactionOptions } from "../../../transaction/types.js"; +import type { Chain } from "../../../chains/types.js"; +import type { ThirdwebClient } from "../../../client/client.js"; +import { getContract } from "../../../contract/contract.js"; import { randomBytesHex } from "../../../utils/random.js"; import type { Account } from "../../../wallets/interfaces/wallet.js"; import { createSessionWithSig, isCreateSessionWithSigSupported, } from "../__generated__/MinimalAccount/write/createSessionWithSig.js"; -import { - type CallSpecInput, - CallSpecRequest, - ConstraintRequest, - SessionSpecRequest, - type TransferSpecInput, - TransferSpecRequest, - UsageLimitRequest, -} from "./types.js"; +import { MINIMAL_ACCOUNT_ABI } from "../__generated__/MinimalAccount/abi.js"; +import { type CallSpecInput, type TransferSpecInput } from "./types.js"; -/** - * @extension ERC7702 - */ export type CreateSessionKeyOptions = { - /** - * The admin account that will perform the operation. - */ + client: ThirdwebClient; + chain: Chain; account: Account; - /** - * The address to add as a session key. - */ sessionKeyAddress: string; - /** - * How long the session key should be valid for, in seconds. - */ - durationInSeconds: number; - /** - * Whether to grant full execution permissions to the session key. - */ + durationInSeconds?: number; grantFullPermissions?: boolean; - /** - * Smart contract interaction policies to apply to the session key, ignored if grantFullPermissions is true. - */ callPolicies?: CallSpecInput[]; - /** - * Value transfer policies to apply to the session key, ignored if grantFullPermissions is true. - */ transferPolicies?: TransferSpecInput[]; }; /** - * Creates session key permissions for a specified address. - * @param options - The options for the createSessionKey function. - * @param {Contract} options.contract - The EIP-7702 smart EOA contract to create the session key from - * @returns The transaction object to be sent. + * Creates a session key for the minimal account. + * This allows delegation of specific permissions to another address. + * + * @param options - The options for creating the session key + * @returns A prepared transaction to create the session key + * * @example * ```ts - * import { createSessionKey } from 'thirdweb/wallets/in-app'; - * import { sendTransaction } from 'thirdweb'; + * import { createSessionKey } from "thirdweb/extensions/erc7702/account"; + * import { defineChain } from "thirdweb/chains"; * * const transaction = createSessionKey({ - * account: account, - * contract: accountContract, - * sessionKeyAddress: TEST_ACCOUNT_A.address, - * durationInSeconds: 86400, // 1 day - * grantFullPermissions: true - *}) - * - * await sendTransaction({ transaction, account }); + * client, + * chain: defineChain(1), + * account, + * sessionKeyAddress: "0x...", + * durationInSeconds: 3600, // 1 hour + * grantFullPermissions: true, + * }); * ``` - * @extension ERC7702 */ -export function createSessionKey( - options: BaseTransactionOptions, -) { +export function createSessionKey(options: CreateSessionKeyOptions) { const { - contract, + client, + chain, account, sessionKeyAddress, - durationInSeconds, - grantFullPermissions, - callPolicies, - transferPolicies, + durationInSeconds = 86400, // 24 hours default + grantFullPermissions = false, + callPolicies = [], + transferPolicies = [], } = options; - if (durationInSeconds <= 0) { - throw new Error("durationInSeconds must be positive"); - } + // Create the contract using account.address with MinimalAccount ABI + const contract = getContract({ + address: account.address, + chain, + client, + abi: MINIMAL_ACCOUNT_ABI, + }); - return createSessionWithSig({ - async asyncParams() { - const req = { - callPolicies: (callPolicies || []).map((policy) => ({ + // Create the session spec object directly + const sessionSpec = { + signer: sessionKeyAddress, + isWildcard: grantFullPermissions, + expiresAt: BigInt(Math.floor(Date.now() / 1000) + durationInSeconds), + callPolicies: grantFullPermissions + ? [] + : callPolicies.map((policy) => ({ + target: policy.target, + selector: policy.selector, + maxValuePerUse: policy.maxValuePerUse || 0n, + valueLimit: policy.valueLimit || { + limitType: 0, + limit: 0n, + period: 0n, + }, constraints: (policy.constraints || []).map((constraint) => ({ - condition: Number(constraint.condition), - index: constraint.index || BigInt(0), - limit: constraint.limit - ? { - limit: constraint.limit.limit, - limitType: Number(constraint.limit.limitType), - period: constraint.limit.period, - } - : { - limit: BigInt(0), - limitType: 0, - period: BigInt(0), - }, - refValue: constraint.refValue || "0x", + condition: constraint.condition, + index: constraint.index, + refValue: constraint.refValue, + limit: constraint.limit || { + limitType: 0, + limit: 0n, + period: 0n, + }, })), - maxValuePerUse: policy.maxValuePerUse || BigInt(0), - selector: policy.selector, - target: policy.target, - valueLimit: policy.valueLimit - ? { - limit: policy.valueLimit.limit, - limitType: Number(policy.valueLimit.limitType), - period: policy.valueLimit.period, - } - : { - limit: BigInt(0), - limitType: 0, - period: BigInt(0), - }, })), - expiresAt: BigInt(Math.floor(Date.now() / 1000) + durationInSeconds), - isWildcard: grantFullPermissions ?? true, - signer: sessionKeyAddress, - transferPolicies: (transferPolicies || []).map((policy) => ({ - maxValuePerUse: policy.maxValuePerUse || BigInt(0), + transferPolicies: grantFullPermissions + ? [] + : transferPolicies.map((policy) => ({ target: policy.target, - valueLimit: policy.valueLimit - ? { - limit: policy.valueLimit.limit, - limitType: Number(policy.valueLimit.limitType), - period: policy.valueLimit.period, - } - : { - limit: BigInt(0), - limitType: 0, - period: BigInt(0), - }, + maxValuePerUse: policy.maxValuePerUse || 0n, + valueLimit: policy.valueLimit || { + limitType: 0, + limit: 0n, + period: 0n, + }, })), - uid: await randomBytesHex(), - }; + uid: randomBytesHex(32), + }; - const signature = await account.signTypedData({ - domain: { - chainId: contract.chain.id, - name: "MinimalAccount", - verifyingContract: contract.address, - version: "1", - }, - message: req, - primaryType: "SessionSpec", - types: { - CallSpec: CallSpecRequest, - Constraint: ConstraintRequest, - SessionSpec: SessionSpecRequest, - TransferSpec: TransferSpecRequest, - UsageLimit: UsageLimitRequest, - }, - }); - - return { sessionSpec: req, signature }; - }, + return createSessionWithSig({ contract, + sessionSpec, + signature: "0x", }); } /** - * Checks if the `isCreateSessionKeySupported` method is supported by the given contract. + * Checks if the `createSessionKey` method is supported by the given contract. * @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. - * @returns A boolean indicating if the `isAddSessionKeySupported` method is supported. + * @returns A boolean indicating if the `createSessionKey` method is supported. * @extension ERC7702 * @example * ```ts - * import { isCreateSessionKeySupported } from "thirdweb/extensions/erc7702"; + * import { isCreateSessionKeySupported } from "thirdweb/extensions/erc7702/account"; * * const supported = isCreateSessionKeySupported(["0x..."]); * ``` diff --git a/packages/thirdweb/src/extensions/erc7702/account/sessionkey.test.ts b/packages/thirdweb/src/extensions/erc7702/account/sessionkey.test.ts index 156fcc8ee6c..f6978a80e79 100644 --- a/packages/thirdweb/src/extensions/erc7702/account/sessionkey.test.ts +++ b/packages/thirdweb/src/extensions/erc7702/account/sessionkey.test.ts @@ -6,10 +6,6 @@ import { beforeAll, describe, expect, it } from "vitest"; import { TEST_CLIENT } from "../../../../test/src/test-clients.js"; import { TEST_ACCOUNT_A } from "../../../../test/src/test-wallets.js"; import { ZERO_ADDRESS } from "../../../constants/addresses.js"; -import { - getContract, - type ThirdwebContract, -} from "../../../contract/contract.js"; import { parseEventLogs } from "../../../event/actions/parse-logs.js"; import { sendAndConfirmTransaction } from "../../../transaction/actions/send-and-confirm-transaction.js"; import { sessionCreatedEvent } from "../__generated__/MinimalAccount/events/SessionCreated.js"; @@ -25,7 +21,6 @@ describe.runIf(process.env.TW_SECRET_KEY)( () => { const chainId = 11155111; let account: Account; - let accountContract: ThirdwebContract; beforeAll(async () => { // Create 7702 Smart EOA @@ -51,21 +46,15 @@ describe.runIf(process.env.TW_SECRET_KEY)( value: 0n, }), }); - - // Will auto resolve abi since it's deployed - accountContract = getContract({ - address: account.address, - chain: defineChain(chainId), - client: TEST_CLIENT, - }); }, 120_000); it("should allow adding adminlike session keys", async () => { const receipt = await sendAndConfirmTransaction({ account: account, transaction: createSessionKey({ + client: TEST_CLIENT, + chain: defineChain(chainId), account: account, - contract: accountContract, durationInSeconds: 86400, grantFullPermissions: true, // 1 day sessionKeyAddress: TEST_ACCOUNT_A.address, @@ -82,6 +71,8 @@ describe.runIf(process.env.TW_SECRET_KEY)( const receipt = await sendAndConfirmTransaction({ account: account, transaction: createSessionKey({ + client: TEST_CLIENT, + chain: defineChain(chainId), account: account, callPolicies: [ { @@ -103,7 +94,6 @@ describe.runIf(process.env.TW_SECRET_KEY)( }, }, ], - contract: accountContract, durationInSeconds: 86400, // 1 day grantFullPermissions: false, sessionKeyAddress: TEST_ACCOUNT_A.address,