From 84762361f60276e106ff3631d1a9ea33f7860c4f Mon Sep 17 00:00:00 2001 From: zer0dot Date: Thu, 27 Mar 2025 17:57:00 -0400 Subject: [PATCH 01/32] feat: permissions WIP --- .../src/ma-v2/client/client.test.ts | 103 ++- .../src/ma-v2/permissionBuilder.ts | 652 ++++++++++++++++++ 2 files changed, 730 insertions(+), 25 deletions(-) create mode 100644 account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts diff --git a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts index da163ee264..34da3e30ae 100644 --- a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts +++ b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts @@ -63,6 +63,7 @@ import { import { getMAV2UpgradeToData } from "@account-kit/smart-contracts"; import { deferralActions } from "../actions/deferralActions.js"; import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; +import { PermissionBuilder, PermissionType } from "../permissionBuilder.js"; // Note: These tests maintain a shared state to not break the local-running rundler by desyncing the chain. describe("MA v2 Tests", async () => { @@ -95,6 +96,62 @@ describe("MA v2 Tests", async () => { address: target, }); + it.only("Install validation builder", async () => { + const provider = await givenConnectedProvider({ signer }); + + await setBalance(client, { + address: provider.getAddress(), + value: parseEther("2"), + }); + + const hookConfig = { + address: zeroAddress, + entityId: 69, + hookType: HookType.VALIDATION, + hasPreHooks: true, + hasPostHooks: false, + }; + + const res = await new PermissionBuilder(provider) + .configure({ + validationConfig: { + moduleAddress: getDefaultSingleSignerValidationModuleAddress( + provider.chain + ), + entityId: 1, + isGlobal: true, + isSignatureValidation: true, + isUserOpValidation: true, + }, + installData: SingleSignerValidationModule.encodeOnInstallData({ + entityId: 1, + signer: await sessionKey.getAddress(), + }), + }) + .addPermission({ + permission: { + type: PermissionType.GAS_LIMIT, + data: { + limit: "0x1234", + }, + }, + }) + .addPermission({ + permission: { + type: PermissionType.NATIVE_TOKEN_TRANSFER, + data: { + allowance: "0x1234", + }, + }, + }) + .compile_deferred({ + deadline: 0, + uoValidationEntityId: 0, + uoValidationIsGlobal: true, + }); + console.log("\n\n FIRST:", res.typedData); + }); + it("sends a simple UO", async () => { const provider = await givenConnectedProvider({ signer }); @@ -477,32 +534,28 @@ describe("MA v2 Tests", async () => { const newSessionKeyEntityId = 2; const isGlobalValidation = true; - // Encode install data to defer - let encodedInstallData = await sessionKeyClient.encodeInstallValidation({ - validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress( - provider.chain - ), - entityId: newSessionKeyEntityId, - isGlobal: isGlobalValidation, - isSignatureValidation: true, - isUserOpValidation: true, - }, - selectors: [], - installData: SingleSignerValidationModule.encodeOnInstallData({ - entityId: newSessionKeyEntityId, - signer: await newSessionKey.getAddress(), - }), - hooks: [], - }); - - // Build the typed data we need for the deferred action (provider/client only used for account address & entrypoint) - const { typedData, nonceOverride } = - await provider.createDeferredActionTypedDataObject({ - callData: encodedInstallData, + // Encode install data to defer and get the deferred action typed data + const { typedData, nonceOverride } = await new PermissionBuilder(provider) + .configure({ + validationConfig: { + moduleAddress: getDefaultSingleSignerValidationModuleAddress( + provider.chain + ), + entityId: newSessionKeyEntityId, + isGlobal: isGlobalValidation, + isSignatureValidation: true, + isUserOpValidation: true, + }, + selectors: [], + installData: SingleSignerValidationModule.encodeOnInstallData({ + entityId: newSessionKeyEntityId, + signer: await newSessionKey.getAddress(), + }), + }) + .compile_deferred({ deadline: 0, - entityId: newSessionKeyEntityId, - isGlobalValidation: isGlobalValidation, + uoValidationEntityId: newSessionKeyEntityId, // UO signing session key + uoValidationIsGlobal: isGlobalValidation, // UO validation is global }); // Sign the typed data using the first session key diff --git a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts new file mode 100644 index 0000000000..fd1b6c0ab9 --- /dev/null +++ b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts @@ -0,0 +1,652 @@ +import { zeroAddress, type Address, type Hex } from "viem"; +import { + HookType, + type HookConfig, + type ValidationConfig, +} from "./actions/common/types.js"; +import { + installValidationActions, + type InstallValidationParams, +} from "./actions/install-validation/installValidation.js"; +import type { ModularAccountV2Client } from "./client/client.js"; +import { + deferralActions, + type DeferredActionReturnData, +} from "./actions/DeferralActions.js"; +import { NativeTokenLimitModule } from "./modules/native-token-limit-module/module.js"; +import { + getDefaultAllowlistModuleAddress, + getDefaultNativeTokenLimitModuleAddress, +} from "./modules/utils.js"; +import { AllowlistModule } from "./modules/allowlist-module/module.js"; + +export enum PermissionType { + NATIVE_TOKEN_TRANSFER = "native-token-transfer", + ERC20_TOKEN_TRANSFER = "erc20-token-transfer", + // ERC721_TOKEN_TRANSFER = "erc721-token-transfer", //Unimplemented + // ERC1155_TOKEN_TRANSFER = "erc1155-token-transfer", //Unimplemented + GAS_LIMIT = "gas-limit", + // CALL_LIMIT = "call-limit", //Unimplemented + // RATE_LIMIT = "rate-limit", //Unimplemented + // Custom MAv2 permissions + CONTRACT_ACCESS = "contract-access", + ACCOUNT_FUNCTIONS = "account-functions", + FUNCTIONS_ON_ALL_CONTRACTS = "functions-on-all-contracts", + FUNCTIONS_ON_CONTRACT = "functions-on-contract", + ROOT = "root", +} + +enum HookIdentifier { + NATIVE_TOKEN_TRANSFER, + ERC20_TOKEN_TRANSFER, + GAS_LIMIT, + PREVAL_ALLOWLIST, // aggregate of CONTRACT_ACCESS, ACCOUNT_FUNCTIONS, FUNCTIONS_ON_ALL_CONTRACTS, FUNCTIONS_ON_CONTRACT +} + +type PreExecutionHookConfig = { + address: Address; + entityId: number; + hookType: HookType.EXECUTION; + hasPreHooks: true; + hasPostHooks: false; +}; + +type PreValidationHookConfig = { + address: Address; + entityId: number; + hookType: HookType.VALIDATION; + hasPreHooks: true; + hasPostHooks: false; +}; + +type RawHooks = { + [HookIdentifier.NATIVE_TOKEN_TRANSFER]: + | { + hookConfig: PreExecutionHookConfig; + initData: { + entityId: number; + spendLimit: bigint; + }; + } + | undefined; + [HookIdentifier.ERC20_TOKEN_TRANSFER]: + | { + hookConfig: PreExecutionHookConfig; + initData: { + entityId: number; + inputs: Array<{ + target: Address; + hasSelectorAllowlist: boolean; + hasERC20SpendLimit: boolean; + erc20SpendLimit: bigint; + selectors: Array; + }>; + }; + } + | undefined; + [HookIdentifier.GAS_LIMIT]: + | { + hookConfig: PreValidationHookConfig; + initData: { + entityId: number; + spendLimit: bigint; + }; + } + | undefined; + [HookIdentifier.PREVAL_ALLOWLIST]: + | { + hookConfig: PreValidationHookConfig; + + initData: { + entityId: number; + inputs: Array<{ + target: Address; + hasSelectorAllowlist: boolean; + hasERC20SpendLimit: boolean; + erc20SpendLimit: bigint; + selectors: Array; + }>; + }; + } + | undefined; +}; + +type OneOf = T[number]; + +export type Permission = OneOf< + [ + { + // this permission allows transfer of native tokens from the account + type: PermissionType.NATIVE_TOKEN_TRANSFER; + data: { + allowance: Hex; + }; + }, + { + // this permission allows transfer or approval of erc20 tokens from the account + type: PermissionType.ERC20_TOKEN_TRANSFER; + data: { + address: Address; // erc20 token contract address + allowance: Hex; + }; + }, + { + // this permissions allows the key to spend gas for UOs + type: PermissionType.GAS_LIMIT; + data: { + limit: Hex; + }; + }, + { + // this permission grants access to all functions in a contract + type: PermissionType.CONTRACT_ACCESS; + data: { + address: Address; + }; + }, + { + // this permission grants access to functions in the account + type: PermissionType.ACCOUNT_FUNCTIONS; + data: { + functions: Hex[]; // function signatures + }; + }, + { + // this permission grants access to a function selector in any address or contract + type: PermissionType.FUNCTIONS_ON_ALL_CONTRACTS; + data: { + functions: Hex[]; // function signatures + }; + }, + { + // this permission grants access to specified functions on a specific contract + type: PermissionType.FUNCTIONS_ON_CONTRACT; + data: { + address: Address; + functions: Hex[]; + }; + }, + { + // this permission grants full access to everything + type: PermissionType.ROOT; + data: never; + } + ] +>; + +type Hook = { + hookConfig: HookConfig; + initData: Hex; +}; + +export class PermissionBuilder { + private client: ModularAccountV2Client; + private validationConfig: ValidationConfig = { + moduleAddress: zeroAddress, + entityId: 0, // uint32 + isGlobal: false, + isSignatureValidation: false, + isUserOpValidation: false, + }; + private selectors: Hex[] = []; + private installData: Hex = "0x"; + private permissions: Permission[] = []; + private hooks: Hook[] = []; + + constructor(client: ModularAccountV2Client) { + this.client = client; + } + + // Configures the builder + configure({ + validationConfig, + selectors, + installData, + hooks, + }: { + validationConfig: ValidationConfig; + selectors?: Hex[]; + installData: Hex; + hooks?: Hook[]; + }): this { + this.validationConfig = validationConfig; + if (selectors) this.selectors = selectors; + this.installData = installData; + if (hooks) this.hooks = hooks; + return this; + } + + // Adds a hook, probably should be removed or only marked as low-level, these won't be consolidated + // addHook({ hookConfig, initData }: { hookConfig: HookConfig; initData: Hex }): this { + // this.hooks.push({ hookConfig, initData }); + // return this; + // } + + addSelector({ selector }: { selector: Hex }): this { + this.selectors.push(selector); + return this; + } + + addPermission({ permission }: { permission: Permission }): this { + // Check 1: If we're adding root, we can't have any other permissions + if (permission.type === PermissionType.ROOT) { + if (this.permissions.length !== 0) { + throw new Error( + "PERMISSION: ROOT: Cannot add ROOT permission with other permissions" + ); + } + // Set isGlobal to true + this.validationConfig.isGlobal = true; + } + + // Check 2: If the permission is NOT ROOT, ensure there is no ROOT permission set + if (permission.type !== PermissionType.ROOT) { + // Will resolve to undefined if ROOT is not found + if (this.permissions.find((p) => p.type === PermissionType.ROOT)) { + throw new Error( + `PERMISSION: ${permission.type} => Cannot add permissions with ROOT enabled` + ); + } + } + + // Check 3: If the permission is either CONTRACT_ACCESS or FUNCTIONS_ON_CONTRACT, ensure it doesn't collide with another like it. + if ( + permission.type === PermissionType.CONTRACT_ACCESS || + permission.type === PermissionType.FUNCTIONS_ON_CONTRACT + ) { + // Check 3.1: address must not be the account address, or the user should use the ACCOUNT_FUNCTIONS permission + if (permission.data.address === this.client.account.address) { + throw new Error( + `PERMISSION: ${permission.type} => Account address as target, use ACCOUNT_FUNCTIONS for account address` + ); + } + + // Check 3.2: there must not be an existing permission with this address as a target + const targetAddress = permission.data.address; + const existingPermissionWithSameAddress = this.permissions.find( + (p) => + (p.type === PermissionType.CONTRACT_ACCESS && + "address" in p.data && + p.data.address === targetAddress) || + (p.type === PermissionType.FUNCTIONS_ON_CONTRACT && + "address" in p.data && + p.data.address === targetAddress) + ); + + if (existingPermissionWithSameAddress) { + throw new Error( + `PERMISSION: ${permission.type} => Address ${targetAddress} already has a permission. Cannot add multiple CONTRACT_ACCESS or FUNCTIONS_ON_CONTRACT permissions for the same target address.` + ); + } + } + + this.permissions.push(permission); + return this; + } + + addPermissions({ permissions }: { permissions: Permission[] }): this { + // We could validate each permission here, but for simplicity we'll just add them + // A better approach would be to call addPermission for each one + permissions.forEach((permission) => { + this.addPermission({ permission }); + }); + return this; + } + + // Use for building deferred action typed data to sign + async compile_deferred({ + deadline, + uoValidationEntityId, + uoValidationIsGlobal, + }: { + deadline: number; + uoValidationEntityId: number; + uoValidationIsGlobal: boolean; + }): Promise { + this.validateConfiguration(); + + // Maybe add checks, like zero address module addr + + // 0. Add time range module hook via expiry + + // 1. Translate all permissions into raw hooks if >0 + if (this.permissions.length > 0) { + const rawHooks = this.translatePermissions(uoValidationEntityId); + // Add the translated permissions as hooks + this.addHooks(rawHooks); + } + + console.log("TRANSLATED PERMISSIONS:"); + + // 3. Consolidate hooks to not have repeated hooks + console.log("THIS HOOKS:", this.hooks); + const installValidationCall = await this.compile_raw(); + + return await deferralActions( + this.client + ).createDeferredActionTypedDataObject({ + callData: installValidationCall, + deadline: deadline, + entityId: uoValidationEntityId, + isGlobalValidation: uoValidationIsGlobal, + }); + } + + // Use for direct `installValidation()` low-level calls (maybe useless) + async compile_raw(): Promise { + this.validateConfiguration(); + + return await installValidationActions(this.client).encodeInstallValidation({ + validationConfig: this.validationConfig, + selectors: this.selectors, + installData: this.installData, + hooks: this.hooks, + account: this.client.account, + }); + } + + // Use for compiling args to installValidation + async compile_installArgs(): Promise { + this.validateConfiguration(); + + return { + validationConfig: this.validationConfig, + selectors: this.selectors, + installData: this.installData, + hooks: this.hooks, + account: this.client.account, + }; + } + + private validateConfiguration(): void { + if ( + this.validationConfig.isGlobal === false && + this.selectors.length === 0 + ) { + throw new Error( + "Validation config unset, use permissionBuilder.configure(...)" + ); + } + } + + // Used to translate consolidated permissions into raw unencoded hooks + private translatePermissions(entityId: number): RawHooks { + const rawHooks: RawHooks = { + [HookIdentifier.NATIVE_TOKEN_TRANSFER]: undefined, + [HookIdentifier.ERC20_TOKEN_TRANSFER]: undefined, + [HookIdentifier.GAS_LIMIT]: undefined, + [HookIdentifier.PREVAL_ALLOWLIST]: undefined, + }; + + this.permissions.forEach((permission) => { + switch (permission.type) { + case PermissionType.NATIVE_TOKEN_TRANSFER: + // Should never be added twice, check is on addPermission(s) too + if (rawHooks[HookIdentifier.NATIVE_TOKEN_TRANSFER] !== undefined) { + throw new Error( + "PERMISSION: NATIVE_TOKEN_TRANSFER => Must have at most ONE native token transfer permission" + ); + } + rawHooks[HookIdentifier.NATIVE_TOKEN_TRANSFER] = { + hookConfig: { + address: getDefaultNativeTokenLimitModuleAddress( + this.client.chain + ), + entityId, + hookType: HookType.EXECUTION, + hasPreHooks: true, + hasPostHooks: false, + }, + initData: { + entityId, + spendLimit: BigInt(permission.data.allowance), + }, + }; + break; + case PermissionType.ERC20_TOKEN_TRANSFER: + if (permission.data.address === zeroAddress) { + throw new Error( + "PERMISSION: ERC20_TOKEN_TRANSFER => Zero address provided" + ); + } + rawHooks[HookIdentifier.ERC20_TOKEN_TRANSFER] = { + hookConfig: { + address: getDefaultAllowlistModuleAddress(this.client.chain), + entityId, + hookType: HookType.EXECUTION, + hasPreHooks: true, + hasPostHooks: false, + }, + initData: { + entityId, // remember entityIds will be const from an object passed + inputs: [ + // Add previous inputs if they exist + ...(rawHooks[HookIdentifier.ERC20_TOKEN_TRANSFER]?.initData + .inputs || []), + { + target: permission.data.address, + hasSelectorAllowlist: false, + hasERC20SpendLimit: true, + erc20SpendLimit: BigInt(permission.data.allowance), + selectors: [], + }, + ], + }, + }; + break; + case PermissionType.GAS_LIMIT: + // Should only ever be added once, check is also on addPermission(s) + if (rawHooks[HookIdentifier.GAS_LIMIT] !== undefined) { + throw new Error( + "PERMISSION: GAS_LIMIT => Must have at most ONE gas limit permission" + ); + } + rawHooks[HookIdentifier.GAS_LIMIT] = { + hookConfig: { + address: getDefaultNativeTokenLimitModuleAddress( + this.client.chain + ), + entityId, + hookType: HookType.VALIDATION, + hasPreHooks: true, + hasPostHooks: false, + }, + initData: { + entityId, + spendLimit: BigInt(permission.data.limit), + }, + }; + break; + case PermissionType.CONTRACT_ACCESS: + if (permission.data.address === zeroAddress) { + throw new Error( + "PERMISSION: CONTRACT_ACCESS => Zero address provided" + ); + } + // TODO (consider): Maybe ensure every address in every allowlist input is different? + rawHooks[HookIdentifier.PREVAL_ALLOWLIST] = { + hookConfig: { + address: getDefaultAllowlistModuleAddress(this.client.chain), + entityId, + hookType: HookType.VALIDATION, + hasPreHooks: true, + hasPostHooks: false, + }, + initData: { + entityId, + inputs: [ + // Add previous inputs if they exist + ...(rawHooks[HookIdentifier.PREVAL_ALLOWLIST]?.initData + .inputs || []), + { + target: permission.data.address, + hasSelectorAllowlist: false, + hasERC20SpendLimit: false, + erc20SpendLimit: 0n, + selectors: [], + }, + ], + }, + }; + break; + case PermissionType.ACCOUNT_FUNCTIONS: + if (permission.data.functions.length === 0) { + throw new Error( + "PERMISSION: ACCOUNT_FUNCTION => No functions provided" + ); // should be in add perm + } + rawHooks[HookIdentifier.PREVAL_ALLOWLIST] = { + hookConfig: { + address: getDefaultAllowlistModuleAddress(this.client.chain), + entityId, + hookType: HookType.VALIDATION, + hasPreHooks: true, + hasPostHooks: false, + }, + initData: { + entityId, + inputs: [ + // Add previous inputs if they exist + ...(rawHooks[HookIdentifier.PREVAL_ALLOWLIST]?.initData + .inputs || []), + { + target: this.client.account.address, + hasSelectorAllowlist: false, + hasERC20SpendLimit: false, + erc20SpendLimit: 0n, + selectors: permission.data.functions, + }, + ], + }, + }; + break; + case PermissionType.FUNCTIONS_ON_ALL_CONTRACTS: + if (permission.data.functions.length === 0) { + throw new Error( + "PERMISSION: FUNCTIONS_ON_ALL_CONTRACTS => No functions provided" + ); + } + rawHooks[HookIdentifier.PREVAL_ALLOWLIST] = { + hookConfig: { + address: getDefaultAllowlistModuleAddress(this.client.chain), + entityId, + hookType: HookType.VALIDATION, + hasPreHooks: true, + hasPostHooks: false, + }, + initData: { + entityId, + inputs: [ + // Add previous inputs if they exist + ...(rawHooks[HookIdentifier.PREVAL_ALLOWLIST]?.initData + .inputs || []), + { + target: zeroAddress, + hasSelectorAllowlist: false, + hasERC20SpendLimit: false, + erc20SpendLimit: 0n, + selectors: permission.data.functions, + }, + ], + }, + }; + break; + case PermissionType.FUNCTIONS_ON_CONTRACT: + if (permission.data.functions.length === 0) { + throw new Error( + "PERMISSION: FUNCTIONS_ON_CONTRACT => No functions provided" + ); + } + if (permission.data.address === zeroAddress) { + throw new Error( + "PERMISSION: FUNCTIONS_ON_CONTRACT => Zero address provided" + ); + } + rawHooks[HookIdentifier.PREVAL_ALLOWLIST] = { + hookConfig: { + address: getDefaultAllowlistModuleAddress(this.client.chain), + entityId, + hookType: HookType.VALIDATION, + hasPreHooks: true, + hasPostHooks: false, + }, + initData: { + entityId, + inputs: [ + // Add previous inputs if they exist + ...(rawHooks[HookIdentifier.PREVAL_ALLOWLIST]?.initData + .inputs || []), + { + target: permission.data.address, + hasSelectorAllowlist: true, + hasERC20SpendLimit: false, + erc20SpendLimit: 0n, + selectors: permission.data.functions, + }, + ], + }, + }; + break; + case PermissionType.ROOT: + // Root permission handled in addPermission + break; + default: + throw new Error( + `Unsupported permission type: ${(permission as any).type}` + ); + } + }); + + return rawHooks; + } + + private addHooks(rawHooks: RawHooks) { + if (rawHooks[HookIdentifier.NATIVE_TOKEN_TRANSFER]) { + this.hooks.push({ + hookConfig: rawHooks[HookIdentifier.NATIVE_TOKEN_TRANSFER].hookConfig, + initData: NativeTokenLimitModule.encodeOnInstallData( + rawHooks[HookIdentifier.NATIVE_TOKEN_TRANSFER].initData + ), + }); + } + + if (rawHooks[HookIdentifier.ERC20_TOKEN_TRANSFER]) { + this.hooks.push({ + hookConfig: rawHooks[HookIdentifier.ERC20_TOKEN_TRANSFER].hookConfig, + initData: AllowlistModule.encodeOnInstallData( + rawHooks[HookIdentifier.ERC20_TOKEN_TRANSFER].initData + ), + }); + } + + if (rawHooks[HookIdentifier.GAS_LIMIT]) { + this.hooks.push({ + hookConfig: rawHooks[HookIdentifier.GAS_LIMIT].hookConfig, + initData: NativeTokenLimitModule.encodeOnInstallData( + rawHooks[HookIdentifier.GAS_LIMIT].initData + ), + }); + } + + if (rawHooks[HookIdentifier.PREVAL_ALLOWLIST]) { + this.hooks.push({ + hookConfig: rawHooks[HookIdentifier.PREVAL_ALLOWLIST].hookConfig, + initData: AllowlistModule.encodeOnInstallData( + rawHooks[HookIdentifier.PREVAL_ALLOWLIST].initData + ), + }); + } + } +} + +// Factory function to create a permission builder +export function createPermissionBuilder( + client: ModularAccountV2Client +): PermissionBuilder { + return new PermissionBuilder(client); +} + +/** + * Illegal cases: + * Contract access + functions on contract SAME contract + */ From 4ce730e6d2a2dec187cd124313aff05b8197ea25 Mon Sep 17 00:00:00 2001 From: zer0dot Date: Thu, 27 Mar 2025 18:07:09 -0400 Subject: [PATCH 02/32] feat: (WIP) working on permissions --- .../src/ma-v2/client/client.test.ts | 344 ++++++------------ .../src/ma-v2/permissionBuilder.ts | 30 +- 2 files changed, 120 insertions(+), 254 deletions(-) diff --git a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts index 34da3e30ae..cd5e98d215 100644 --- a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts +++ b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts @@ -50,16 +50,9 @@ import { local070Instance } from "~test/instances.js"; import { setBalance } from "viem/actions"; import { accounts } from "~test/constants.js"; import { paymaster070 } from "~test/paymaster/paymaster070.js"; -import { - packAccountGasLimits, - packPaymasterData, -} from "../../../../../aa-sdk/core/src/entrypoint/0.7.js"; +import { packAccountGasLimits, packPaymasterData } from "../../../../../aa-sdk/core/src/entrypoint/0.7.js"; import { entryPoint07Abi } from "viem/account-abstraction"; -import { - alchemy, - arbitrumSepolia, - alchemyGasAndPaymasterAndDataMiddleware, -} from "@account-kit/infra"; +import { alchemy, arbitrumSepolia, alchemyGasAndPaymasterAndDataMiddleware } from "@account-kit/infra"; import { getMAV2UpgradeToData } from "@account-kit/smart-contracts"; import { deferralActions } from "../actions/deferralActions.js"; import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; @@ -70,9 +63,7 @@ describe("MA v2 Tests", async () => { const instance = local070Instance; const isValidSigSuccess = "0x1626ba7e"; - let client: ReturnType & - ReturnType & - TestActions; + let client: ReturnType & ReturnType & TestActions; beforeAll(async () => { client = instance .getClient() @@ -80,13 +71,9 @@ describe("MA v2 Tests", async () => { .extend(testActions({ mode: "anvil" })); }); - const signer: SmartAccountSigner = new LocalAccountSigner( - accounts.fundedAccountOwner - ); + const signer: SmartAccountSigner = new LocalAccountSigner(accounts.fundedAccountOwner); - const sessionKey: SmartAccountSigner = new LocalAccountSigner( - accounts.unfundedAccountOwner - ); + const sessionKey: SmartAccountSigner = new LocalAccountSigner(accounts.unfundedAccountOwner); const target = "0x000000000000000000000000000000000000dEaD"; const sendAmount = parseEther("1"); @@ -115,9 +102,7 @@ describe("MA v2 Tests", async () => { const res = await new PermissionBuilder(provider) .configure({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress( - provider.chain - ), + moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), entityId: 1, isGlobal: true, isSignatureValidation: true, @@ -172,15 +157,11 @@ describe("MA v2 Tests", async () => { await provider.waitForUserOperationTransaction(result); - await expect(getTargetBalance()).resolves.toEqual( - startingAddressBalance + sendAmount - ); + await expect(getTargetBalance()).resolves.toEqual(startingAddressBalance + sendAmount); }); it("successfully sign + validate a message, for native and single signer validation", async () => { - const provider = (await givenConnectedProvider({ signer })).extend( - installValidationActions - ); + const provider = (await givenConnectedProvider({ signer })).extend(installValidationActions); await setBalance(instance.getClient(), { address: provider.getAddress(), @@ -196,9 +177,7 @@ describe("MA v2 Tests", async () => { // UO deploys the account to test 1271 against const result = await provider.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress( - provider.chain - ), + moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), entityId: 1, isGlobal: true, isSignatureValidation: true, @@ -218,9 +197,9 @@ describe("MA v2 Tests", async () => { let signature = await provider.signMessage({ message }); - await expect( - accountContract.read.isValidSignature([hashMessage(message), signature]) - ).resolves.toEqual(isValidSigSuccess); + await expect(accountContract.read.isValidSignature([hashMessage(message), signature])).resolves.toEqual( + isValidSigSuccess + ); // connect session key let sessionKeyClient = await createModularAccountV2Client({ @@ -233,15 +212,13 @@ describe("MA v2 Tests", async () => { signature = await sessionKeyClient.signMessage({ message }); - await expect( - accountContract.read.isValidSignature([hashMessage(message), signature]) - ).resolves.toEqual(isValidSigSuccess); + await expect(accountContract.read.isValidSignature([hashMessage(message), signature])).resolves.toEqual( + isValidSigSuccess + ); }); it("successfully sign + validate typed data messages, for native and single signer validation", async () => { - const provider = (await givenConnectedProvider({ signer })).extend( - installValidationActions - ); + const provider = (await givenConnectedProvider({ signer })).extend(installValidationActions); await setBalance(instance.getClient(), { address: provider.getAddress(), @@ -257,9 +234,7 @@ describe("MA v2 Tests", async () => { // UO deploys the account to test 1271 against const result = await provider.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress( - provider.chain - ), + moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), entityId: 1, isGlobal: true, isSignatureValidation: true, @@ -310,9 +285,9 @@ describe("MA v2 Tests", async () => { const hashedMessageTypedData = hashTypedData(typedData); let signature = await provider.signTypedData({ typedData }); - await expect( - accountContract.read.isValidSignature([hashedMessageTypedData, signature]) - ).resolves.toEqual(isValidSigSuccess); + await expect(accountContract.read.isValidSignature([hashedMessageTypedData, signature])).resolves.toEqual( + isValidSigSuccess + ); // connect session key let sessionKeyClient = await createModularAccountV2Client({ @@ -325,15 +300,13 @@ describe("MA v2 Tests", async () => { signature = await sessionKeyClient.signTypedData({ typedData }); - await expect( - accountContract.read.isValidSignature([hashedMessageTypedData, signature]) - ).resolves.toEqual(isValidSigSuccess); + await expect(accountContract.read.isValidSignature([hashedMessageTypedData, signature])).resolves.toEqual( + isValidSigSuccess + ); }); it("adds a session key with no permissions", async () => { - let provider = (await givenConnectedProvider({ signer })).extend( - installValidationActions - ); + let provider = (await givenConnectedProvider({ signer })).extend(installValidationActions); await setBalance(client, { address: provider.getAddress(), @@ -342,9 +315,7 @@ describe("MA v2 Tests", async () => { let result = await provider.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress( - provider.chain - ), + moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), entityId: 1, isGlobal: true, isSignatureValidation: true, @@ -381,15 +352,11 @@ describe("MA v2 Tests", async () => { await sessionKeyClient.waitForUserOperationTransaction(result); - await expect(getTargetBalance()).resolves.toEqual( - startingAddressBalance + sendAmount - ); + await expect(getTargetBalance()).resolves.toEqual(startingAddressBalance + sendAmount); }); it("installs a session key via deferred action signed by the owner and has it sign a UO", async () => { - let provider = (await givenConnectedProvider({ signer })) - .extend(installValidationActions) - .extend(deferralActions); + let provider = (await givenConnectedProvider({ signer })).extend(installValidationActions).extend(deferralActions); await setBalance(client, { address: provider.getAddress(), @@ -403,9 +370,7 @@ describe("MA v2 Tests", async () => { // Encode install data to defer let encodedInstallData = await provider.encodeInstallValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress( - provider.chain - ), + moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), entityId: sessionKeyEntityId, isGlobal: isGlobalValidation, isSignatureValidation: true, @@ -420,18 +385,15 @@ describe("MA v2 Tests", async () => { }); // Build the typed data we need for the deferred action (provider/client only used for account address & entrypoint) - const { typedData, nonceOverride } = - await provider.createDeferredActionTypedDataObject({ - callData: encodedInstallData, - deadline: 0, - entityId: sessionKeyEntityId, - isGlobalValidation: isGlobalValidation, - }); + const { typedData, nonceOverride } = await provider.createDeferredActionTypedDataObject({ + callData: encodedInstallData, + deadline: 0, + entityId: sessionKeyEntityId, + isGlobalValidation: isGlobalValidation, + }); // Sign the typed data using the owner (fallback) validation, this must be done via the account to skip 6492 - const deferredValidationSig = await provider.account.signTypedData( - typedData - ); + const deferredValidationSig = await provider.account.signTypedData(typedData); // Build the full hex to prepend to the UO signature // This MUST be done with the *same* client that has signed the typed data @@ -468,18 +430,13 @@ describe("MA v2 Tests", async () => { uo.signature = concatHex([signaturePrepend, uo.signature as Hex]); // Send the raw UserOp - const result = await sessionKeyClient.sendRawUserOperation( - uo, - provider.account.getEntryPoint().address - ); + const result = await sessionKeyClient.sendRawUserOperation(uo, provider.account.getEntryPoint().address); await provider.waitForUserOperationTransaction({ hash: result }); }); it("installs a session key via deferred action signed by another session key and has it sign a UO", async () => { - let provider = (await givenConnectedProvider({ signer })) - .extend(installValidationActions) - .extend(deferralActions); + let provider = (await givenConnectedProvider({ signer })).extend(installValidationActions).extend(deferralActions); await setBalance(client, { address: provider.getAddress(), @@ -491,9 +448,7 @@ describe("MA v2 Tests", async () => { // First, install a session key let sessionKeyInstallResult = await provider.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress( - provider.chain - ), + moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), entityId: sessionKeyEntityId, isGlobal: true, isSignatureValidation: true, @@ -526,9 +481,7 @@ describe("MA v2 Tests", async () => { .extend(deferralActions); const randomWallet = privateKeyToAccount(generatePrivateKey()); - const newSessionKey: SmartAccountSigner = new LocalAccountSigner( - randomWallet - ); + const newSessionKey: SmartAccountSigner = new LocalAccountSigner(randomWallet); // Test variables const newSessionKeyEntityId = 2; @@ -538,9 +491,7 @@ describe("MA v2 Tests", async () => { const { typedData, nonceOverride } = await new PermissionBuilder(provider) .configure({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress( - provider.chain - ), + moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), entityId: newSessionKeyEntityId, isGlobal: isGlobalValidation, isSignatureValidation: true, @@ -559,9 +510,7 @@ describe("MA v2 Tests", async () => { }); // Sign the typed data using the first session key - const deferredValidationSig = await sessionKeyClient.account.signTypedData( - typedData - ); + const deferredValidationSig = await sessionKeyClient.account.signTypedData(typedData); // Build the full hex to prepend to the UO signature // This MUST be done with the *same* client that has signed the typed data @@ -598,18 +547,13 @@ describe("MA v2 Tests", async () => { uo.signature = concatHex([signaturePrepend, uo.signature as Hex]); // Send the raw UserOp (provider/client only used for account address & entrypoint) - const result = await provider.sendRawUserOperation( - uo, - provider.account.getEntryPoint().address - ); + const result = await provider.sendRawUserOperation(uo, provider.account.getEntryPoint().address); await provider.waitForUserOperationTransaction({ hash: result }); }); it("uninstalls a session key", async () => { - let provider = (await givenConnectedProvider({ signer })).extend( - installValidationActions - ); + let provider = (await givenConnectedProvider({ signer })).extend(installValidationActions); await setBalance(client, { address: provider.getAddress(), @@ -618,9 +562,7 @@ describe("MA v2 Tests", async () => { let result = await provider.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress( - provider.chain - ), + moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), entityId: 1, isGlobal: true, isSignatureValidation: true, @@ -637,9 +579,7 @@ describe("MA v2 Tests", async () => { await provider.waitForUserOperationTransaction(result); result = await provider.uninstallValidation({ - moduleAddress: getDefaultSingleSignerValidationModuleAddress( - provider.chain - ), + moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), entityId: 1, uninstallData: SingleSignerValidationModule.encodeOnUninstallData({ entityId: 1, @@ -691,9 +631,7 @@ describe("MA v2 Tests", async () => { const installResult = await provider.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress( - provider.chain - ), + moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), entityId: 1, isGlobal: true, isSignatureValidation: true, @@ -756,9 +694,7 @@ describe("MA v2 Tests", async () => { }); // verify uninstall - await expect( - provider.waitForUserOperationTransaction(uninstallResult) - ).resolves.not.toThrowError(); + await expect(provider.waitForUserOperationTransaction(uninstallResult)).resolves.not.toThrowError(); }); it("installs paymaster guard module, verifies use of invalid paymaster, then uninstalls module", async () => { @@ -783,9 +719,7 @@ describe("MA v2 Tests", async () => { const installResult = await provider.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress( - provider.chain - ), + moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), entityId: 1, isGlobal: true, isSignatureValidation: true, @@ -838,9 +772,7 @@ describe("MA v2 Tests", async () => { }); const uninstallResult = await provider.uninstallValidation({ - moduleAddress: getDefaultSingleSignerValidationModuleAddress( - provider.chain - ), + moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), entityId: 1, uninstallData: SingleSignerValidationModule.encodeOnUninstallData({ entityId: 1, @@ -849,15 +781,11 @@ describe("MA v2 Tests", async () => { }); // verify uninstall - await expect( - provider.waitForUserOperationTransaction(uninstallResult) - ).resolves.not.toThrowError(); + await expect(provider.waitForUserOperationTransaction(uninstallResult)).resolves.not.toThrowError(); }); it("installs allowlist module, uses, then uninstalls", async () => { - let provider = (await givenConnectedProvider({ signer })).extend( - installValidationActions - ); + let provider = (await givenConnectedProvider({ signer })).extend(installValidationActions); await setBalance(client, { address: provider.getAddress(), @@ -867,9 +795,7 @@ describe("MA v2 Tests", async () => { // install validation module const installResult = await provider.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress( - provider.chain - ), + moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), entityId: 1, isGlobal: true, isSignatureValidation: true, @@ -895,18 +821,15 @@ describe("MA v2 Tests", async () => { ).extend(installValidationActions); // verify we can call into the zero address before allow list hook is installed - const sendResultBeforeHookInstallation = - await sessionKeyProvider.sendUserOperation({ - uo: { - target: zeroAddress, - value: 0n, - data: "0x", - }, - }); + const sendResultBeforeHookInstallation = await sessionKeyProvider.sendUserOperation({ + uo: { + target: zeroAddress, + value: 0n, + data: "0x", + }, + }); - await provider.waitForUserOperationTransaction( - sendResultBeforeHookInstallation - ); + await provider.waitForUserOperationTransaction(sendResultBeforeHookInstallation); const hookInstallData = AllowlistModule.encodeOnInstallData({ entityId: 1, @@ -924,9 +847,7 @@ describe("MA v2 Tests", async () => { // install hook const installHookResult = await provider.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress( - provider.chain - ), + moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), entityId: 1, isGlobal: true, isSignatureValidation: true, @@ -987,9 +908,7 @@ describe("MA v2 Tests", async () => { }); const uninstallResult = await provider.uninstallValidation({ - moduleAddress: getDefaultSingleSignerValidationModuleAddress( - provider.chain - ), + moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), entityId: 1, uninstallData: SingleSignerValidationModule.encodeOnUninstallData({ entityId: 1, @@ -1001,9 +920,7 @@ describe("MA v2 Tests", async () => { }); it("installs native token limit module, uses, then uninstalls", async () => { - let provider = (await givenConnectedProvider({ signer })).extend( - installValidationActions - ); + let provider = (await givenConnectedProvider({ signer })).extend(installValidationActions); await setBalance(client, { address: provider.getAddress(), @@ -1014,9 +931,7 @@ describe("MA v2 Tests", async () => { const installResult = await provider.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress( - provider.chain - ), + moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), entityId: 1, isGlobal: true, isSignatureValidation: true, @@ -1049,9 +964,7 @@ describe("MA v2 Tests", async () => { data: "0x", }, }); - await provider.waitForUserOperationTransaction( - preHookInstallationSendResult - ); + await provider.waitForUserOperationTransaction(preHookInstallationSendResult); // Let's verify the module's limit is set correctly after installation const hookInstallData = NativeTokenLimitModule.encodeOnInstallData({ @@ -1061,9 +974,7 @@ describe("MA v2 Tests", async () => { const installHookResult = await provider.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress( - provider.chain - ), + moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), entityId: 1, isGlobal: true, isSignatureValidation: true, @@ -1123,9 +1034,7 @@ describe("MA v2 Tests", async () => { }); const uninstallResult = await provider.uninstallValidation({ - moduleAddress: getDefaultSingleSignerValidationModuleAddress( - provider.chain - ), + moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), entityId: 1, uninstallData: SingleSignerValidationModule.encodeOnUninstallData({ entityId: 1, @@ -1165,9 +1074,7 @@ describe("MA v2 Tests", async () => { const installResult = await provider.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress( - provider.chain - ), + moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), entityId: 1, isGlobal: true, isSignatureValidation: true, @@ -1228,9 +1135,7 @@ describe("MA v2 Tests", async () => { sender: sessionKeyProvider.account.address, nonce: fromHex(signedUO.nonce, "bigint"), initCode: - signedUO.factory && signedUO.factoryData - ? concat([signedUO.factory, signedUO.factoryData]) - : "0x", + signedUO.factory && signedUO.factoryData ? concat([signedUO.factory, signedUO.factoryData]) : "0x", callData: signedUO.callData, accountGasLimits: packAccountGasLimits({ verificationGasLimit: signedUO.verificationGasLimit, @@ -1245,8 +1150,7 @@ describe("MA v2 Tests", async () => { signedUO.paymaster && isAddress(signedUO.paymaster) ? packPaymasterData({ paymaster: signedUO.paymaster, - paymasterVerificationGasLimit: - signedUO.paymasterVerificationGasLimit, + paymasterVerificationGasLimit: signedUO.paymasterVerificationGasLimit, paymasterPostOpGasLimit: signedUO.paymasterPostOpGasLimit, paymasterData: signedUO.paymasterData, }) @@ -1292,9 +1196,7 @@ describe("MA v2 Tests", async () => { const installResult = await provider.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress( - provider.chain - ), + moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), entityId: 2, isGlobal: true, isSignatureValidation: true, @@ -1356,18 +1258,13 @@ describe("MA v2 Tests", async () => { sender: sessionKeyProvider.account.address, nonce: fromHex(signedUO.nonce, "bigint"), initCode: - signedUO.factory && signedUO.factoryData - ? concat([signedUO.factory, signedUO.factoryData]) - : "0x", + signedUO.factory && signedUO.factoryData ? concat([signedUO.factory, signedUO.factoryData]) : "0x", callData: signedUO.callData, accountGasLimits: packAccountGasLimits({ verificationGasLimit: signedUO.verificationGasLimit, callGasLimit: signedUO.callGasLimit, }), - preVerificationGas: fromHex( - signedUO.preVerificationGas, - "bigint" - ), + preVerificationGas: fromHex(signedUO.preVerificationGas, "bigint"), gasFees: packAccountGasLimits({ maxPriorityFeePerGas: signedUO.maxPriorityFeePerGas, maxFeePerGas: signedUO.maxFeePerGas, @@ -1376,8 +1273,7 @@ describe("MA v2 Tests", async () => { signedUO.paymaster && isAddress(signedUO.paymaster) ? packPaymasterData({ paymaster: signedUO.paymaster, - paymasterVerificationGasLimit: - signedUO.paymasterVerificationGasLimit, + paymasterVerificationGasLimit: signedUO.paymasterVerificationGasLimit, paymasterPostOpGasLimit: signedUO.paymasterPostOpGasLimit, paymasterData: signedUO.paymasterData, }) @@ -1391,11 +1287,7 @@ describe("MA v2 Tests", async () => { }); } catch (err: any) { // verify that simulation fails due to violation of time range restriction on session key - assert( - err.metaMessages.some((str: string) => - str.includes("AA22 expired or not due") - ) - ); + assert(err.metaMessages.some((str: string) => str.includes("AA22 expired or not due"))); } client.setAutomine(true); @@ -1447,9 +1339,7 @@ describe("MA v2 Tests", async () => { // deploy the account and install at entity id 1 with global validation const uo1 = await newClient.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress( - newClient.chain - ), + moduleAddress: getDefaultSingleSignerValidationModuleAddress(newClient.chain), entityId: 1, isGlobal: true, isSignatureValidation: false, @@ -1464,10 +1354,7 @@ describe("MA v2 Tests", async () => { }); await newClient.waitForUserOperationTransaction(uo1); - const fns: ContractFunctionName[] = [ - "execute", - "executeBatch", - ]; + const fns: ContractFunctionName[] = ["execute", "executeBatch"]; const selectors = fns.map( (s) => @@ -1480,9 +1367,7 @@ describe("MA v2 Tests", async () => { // deploy the account and install some entity ids with selector validation const uo2 = await newClient.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress( - newClient.chain - ), + moduleAddress: getDefaultSingleSignerValidationModuleAddress(newClient.chain), entityId: 2, isGlobal: false, isSignatureValidation: false, @@ -1537,10 +1422,9 @@ describe("MA v2 Tests", async () => { value: parseEther("2"), }); - const { createModularAccountV2FromExisting, ...upgradeToData } = - await getMAV2UpgradeToData(lightAccountClient, { - account: lightAccountClient.account, - }); + const { createModularAccountV2FromExisting, ...upgradeToData } = await getMAV2UpgradeToData(lightAccountClient, { + account: lightAccountClient.account, + }); await lightAccountClient.upgradeAccount({ upgradeTo: upgradeToData, @@ -1567,42 +1451,9 @@ describe("MA v2 Tests", async () => { await maV2Client.waitForUserOperationTransaction(result); - await expect(getTargetBalance()).resolves.toEqual( - startingAddressBalance + sendAmount - ); + await expect(getTargetBalance()).resolves.toEqual(startingAddressBalance + sendAmount); }); - const givenConnectedProvider = async ({ - signer, - signerEntity, - accountAddress, - paymasterMiddleware, - salt = 0n, - }: { - signer: SmartAccountSigner; - signerEntity?: SignerEntity; - accountAddress?: `0x${string}`; - paymasterMiddleware?: "alchemyGasAndPaymasterAndData" | "erc7677"; - salt?: bigint; - }) => - createModularAccountV2Client({ - chain: instance.chain, - signer, - accountAddress, - signerEntity, - transport: custom(instance.getClient()), - ...(paymasterMiddleware === "alchemyGasAndPaymasterAndData" - ? alchemyGasAndPaymasterAndDataMiddleware({ - policyId: "FAKE_POLICY_ID", - // @ts-ignore (expects an alchemy transport, but we're using a custom transport for mocking) - transport: custom(instance.getClient()), - }) - : paymasterMiddleware === "erc7677" - ? erc7677Middleware() - : {}), - salt, - }); - it("alchemy client calls the createAlchemySmartAccountClient", async () => { const alchemyClientSpy = vi .spyOn(AAInfraModule, "createAlchemySmartAccountClient") @@ -1641,4 +1492,35 @@ describe("MA v2 Tests", async () => { expect(alchemyClientSpy).not.toHaveBeenCalled(); expect(notAlchemyClientSpy).toHaveBeenCalled(); }); + + const givenConnectedProvider = async ({ + signer, + signerEntity, + accountAddress, + paymasterMiddleware, + salt = 0n, + }: { + signer: SmartAccountSigner; + signerEntity?: SignerEntity; + accountAddress?: `0x${string}`; + paymasterMiddleware?: "alchemyGasAndPaymasterAndData" | "erc7677"; + salt?: bigint; + }) => + createModularAccountV2Client({ + chain: instance.chain, + signer, + accountAddress, + signerEntity, + transport: custom(instance.getClient()), + ...(paymasterMiddleware === "alchemyGasAndPaymasterAndData" + ? alchemyGasAndPaymasterAndDataMiddleware({ + policyId: "FAKE_POLICY_ID", + // @ts-ignore (expects an alchemy transport, but we're using a custom transport for mocking) + transport: custom(instance.getClient()), + }) + : paymasterMiddleware === "erc7677" + ? erc7677Middleware() + : {}), + salt, + }); }); diff --git a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts index fd1b6c0ab9..b654c7c3e7 100644 --- a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts +++ b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts @@ -216,12 +216,6 @@ export class PermissionBuilder { return this; } - // Adds a hook, probably should be removed or only marked as low-level, these won't be consolidated - // addHook({ hookConfig, initData }: { hookConfig: HookConfig; initData: Hex }): this { - // this.hooks.push({ hookConfig, initData }); - // return this; - // } - addSelector({ selector }: { selector: Hex }): this { this.selectors.push(selector); return this; @@ -237,16 +231,15 @@ export class PermissionBuilder { } // Set isGlobal to true this.validationConfig.isGlobal = true; + return this; } - // Check 2: If the permission is NOT ROOT, ensure there is no ROOT permission set - if (permission.type !== PermissionType.ROOT) { - // Will resolve to undefined if ROOT is not found - if (this.permissions.find((p) => p.type === PermissionType.ROOT)) { - throw new Error( - `PERMISSION: ${permission.type} => Cannot add permissions with ROOT enabled` - ); - } + // Check 2: If the permission is NOT ROOT (guaranteed), ensure there is no ROOT permission set + // Will resolve to undefined if ROOT is not found + if (this.permissions.find((p) => p.type === PermissionType.ROOT)) { + throw new Error( + `PERMISSION: ${permission.type} => Cannot add permissions with ROOT enabled` + ); } // Check 3: If the permission is either CONTRACT_ACCESS or FUNCTIONS_ON_CONTRACT, ensure it doesn't collide with another like it. @@ -316,10 +309,6 @@ export class PermissionBuilder { this.addHooks(rawHooks); } - console.log("TRANSLATED PERMISSIONS:"); - - // 3. Consolidate hooks to not have repeated hooks - console.log("THIS HOOKS:", this.hooks); const installValidationCall = await this.compile_raw(); return await deferralActions( @@ -645,8 +634,3 @@ export function createPermissionBuilder( ): PermissionBuilder { return new PermissionBuilder(client); } - -/** - * Illegal cases: - * Contract access + functions on contract SAME contract - */ From c84654727dc4da0b541c38fadf05c7819c76d0e6 Mon Sep 17 00:00:00 2001 From: zer0dot Date: Fri, 28 Mar 2025 17:25:55 -0400 Subject: [PATCH 03/32] feat: (WIP) permission work --- .../src/ma-v2/actions/deferralActions.ts | 13 +- .../src/ma-v2/client/client.test.ts | 182 +++++++++++++++++- .../src/ma-v2/permissionBuilder.ts | 43 ++++- 3 files changed, 225 insertions(+), 13 deletions(-) diff --git a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.ts b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.ts index 8c0048e7b5..7a95ec5d71 100644 --- a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.ts +++ b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.ts @@ -177,21 +177,26 @@ export const deferralActions: ( (BigInt(signerEntity.entityId) << 8n) | (signerEntity.isGlobalValidation ? 1n : 0n); - let encodedData = encodePacked( + const encodedCallData = encodePacked( ["uint168", "uint48", "bytes"], [validationLocator, typedData.message.deadline, typedData.message.call] ); - const encodedDataLength = size(encodedData); + const encodedDataLength = size(encodedCallData); const sigLength = size(sig); - encodedData = concatHex([ + const encodedData = concatHex([ toHex(encodedDataLength, { size: 4 }), - encodedData, + encodedCallData, toHex(sigLength, { size: 4 }), sig, ]); + console.log("Encoded data len:", encodedDataLength); + console.log("Encoded call data:", encodedCallData); + console.log("Encoded data:", encodedData); + console.log("sig len:", sigLength); + return encodedData; }; diff --git a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts index cd5e98d215..a4783c0969 100644 --- a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts +++ b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts @@ -83,7 +83,7 @@ describe("MA v2 Tests", async () => { address: target, }); - it.only("Install validation builder", async () => { + it("Install validation builder", async () => { const provider = await givenConnectedProvider({ signer }); await setBalance(client, { @@ -130,7 +130,7 @@ describe("MA v2 Tests", async () => { }, }) .compile_deferred({ - deadline: 0, + deadline: Math.round(Date.now() / 1000 + 100), uoValidationEntityId: 0, uoValidationIsGlobal: true, }); @@ -355,6 +355,184 @@ describe("MA v2 Tests", async () => { await expect(getTargetBalance()).resolves.toEqual(startingAddressBalance + sendAmount); }); + it.only("installs a session key via deferred action using PermissionBuilder signed by the owner and has it sign a UO", async () => { + let provider = (await givenConnectedProvider({ signer })) + .extend(installValidationActions) + .extend(deferralActions); + + await setBalance(client, { + address: provider.getAddress(), + value: parseEther("2"), + }); + + // Test variables + const sessionKeyEntityId = 1; + const isGlobalValidation = true; + + // const encodedInstallCall = await new PermissionBuilder(provider) + // // const { typedData, nonceOverride } = await new PermissionBuilder(provider) + // .configure({ + // validationConfig: { + // moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), + // entityId: sessionKeyEntityId, + // isGlobal: isGlobalValidation, + // isSignatureValidation: true, + // isUserOpValidation: true, + // }, + // installData: SingleSignerValidationModule.encodeOnInstallData({ + // entityId: sessionKeyEntityId, + // signer: await sessionKey.getAddress(), + // }), + // }) + // .addPermission({ + // permission: { + // type: PermissionType.GAS_LIMIT, + // data: { + // limit: "0x1234512", + // }, + // }, + // }) + // .compile_raw(); + + // console.log("TYPED DATA:", typedData); + + // console.log("ENCODED INSTALL CALL FROM TEST:", encodedInstallCall); + + // const receipt = await provider.sendUserOperation({ + // uo: encodedInstallCall, + // }); + + // await provider.waitForUserOperationTransaction(receipt); + + // const { typedData, nonceOverride } = await new PermissionBuilder(provider) + // .configure({ + // validationConfig: { + // moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), + // entityId: sessionKeyEntityId, + // isGlobal: isGlobalValidation, + // isSignatureValidation: true, + // isUserOpValidation: true, + // }, + // installData: SingleSignerValidationModule.encodeOnInstallData({ + // entityId: sessionKeyEntityId, + // signer: await sessionKey.getAddress(), + // }), + // }) + // .addPermission({ + // permission: { + // type: PermissionType.GAS_LIMIT, + // data: { + // limit: "0x1234512", + // }, + // }, + // }) + // .compile_deferred({ + // deadline: 0, //Math.round(Date.now() / 1000 + 100), + // uoValidationEntityId: sessionKeyEntityId, + // uoValidationIsGlobal: isGlobalValidation, + // }); + const paymaster = paymaster070.getPaymasterDetails().address; + + // // Encode install data to defer + let encodedInstallData = await provider.encodeInstallValidation({ + validationConfig: { + moduleAddress: getDefaultSingleSignerValidationModuleAddress( + provider.chain + ), + entityId: sessionKeyEntityId, + isGlobal: isGlobalValidation, + isSignatureValidation: true, + isUserOpValidation: true, + }, + selectors: [], + installData: SingleSignerValidationModule.encodeOnInstallData({ + entityId: sessionKeyEntityId, + signer: await sessionKey.getAddress(), + }), + hooks: [ + { + hookConfig: { + address: getDefaultPaymasterGuardModuleAddress(provider.chain), + entityId: 1, + hookType: HookType.VALIDATION, + hasPreHooks: true, + hasPostHooks: false, + }, + initData: PaymasterGuardModule.encodeOnInstallData({ + entityId: 1, + paymaster: paymaster, + }), + }, + ], + }); + + console.log( + "EXPECTED HOOK INIT DATA:", + PaymasterGuardModule.encodeOnInstallData({ + entityId: 1, + paymaster: paymaster, + }) + ); + + console.log("ACTUAL EXPECTED CALL DATA:", encodedInstallData); + + // // Build the typed data we need for the deferred action (provider/client only used for account address & entrypoint) + const { typedData, nonceOverride } = + await provider.createDeferredActionTypedDataObject({ + callData: encodedInstallData, + deadline: 0, + entityId: sessionKeyEntityId, + isGlobalValidation: isGlobalValidation, + }); + + // Sign the typed data using the owner (fallback) validation, this must be done via the account to skip 6492 + const deferredValidationSig = await provider.account.signTypedData( + typedData + ); + + // Build the full hex to prepend to the UO signature + // This MUST be done with the *same* client that has signed the typed data + const signaturePrepend = provider.buildDeferredActionDigest({ + typedData: typedData, + sig: deferredValidationSig, + }); + + // Build the full UO with the deferred action signature prepend (provider/client only used for account address & entrypoint) + const unsignedUo = await provider.buildUserOperationWithDeferredAction({ + uo: { target, data: "0x" }, + signaturePrepend, + nonceOverride, + }); + + // Initialize the session key client corresponding to the session key we will install in the deferred action + let sessionKeyClient = await createModularAccountV2Client({ + chain: instance.chain, + signer: sessionKey, + transport: custom(instance.getClient()), + accountAddress: provider.getAddress(), + signerEntity: { + entityId: sessionKeyEntityId, + isGlobalValidation: isGlobalValidation, + }, + }); + + // Sign the UO with the session key + const uo = await sessionKeyClient.signUserOperation({ + uoStruct: unsignedUo, + }); + + // Prepend the full hex for the deferred action to the new, real signature + uo.signature = concatHex([signaturePrepend, uo.signature as Hex]); + + // Send the raw UserOp + const result = await sessionKeyClient.sendRawUserOperation( + uo, + provider.account.getEntryPoint().address + ); + + await provider.waitForUserOperationTransaction({ hash: result }); + }); + it("installs a session key via deferred action signed by the owner and has it sign a UO", async () => { let provider = (await givenConnectedProvider({ signer })).extend(installValidationActions).extend(deferralActions); diff --git a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts index b654c7c3e7..22164f103f 100644 --- a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts +++ b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts @@ -17,8 +17,10 @@ import { NativeTokenLimitModule } from "./modules/native-token-limit-module/modu import { getDefaultAllowlistModuleAddress, getDefaultNativeTokenLimitModuleAddress, + getDefaultTimeRangeModuleAddress, } from "./modules/utils.js"; import { AllowlistModule } from "./modules/allowlist-module/module.js"; +import { TimeRangeModule } from "./modules/time-range-module/module.js"; export enum PermissionType { NATIVE_TOKEN_TRANSFER = "native-token-transfer", @@ -274,6 +276,7 @@ export class PermissionBuilder { } this.permissions.push(permission); + return this; } @@ -300,17 +303,31 @@ export class PermissionBuilder { // Maybe add checks, like zero address module addr - // 0. Add time range module hook via expiry - - // 1. Translate all permissions into raw hooks if >0 - if (this.permissions.length > 0) { - const rawHooks = this.translatePermissions(uoValidationEntityId); - // Add the translated permissions as hooks - this.addHooks(rawHooks); + // Add time range module hook via expiry + if (deadline !== 0) { + if (deadline < Date.now() / 1000) { + throw new Error( + `PERMISSION: compile_deferred(): Deadline ${deadline} cannot be before now (${ + Date.now() / 1000 + })` + ); + } + this.hooks.push( + TimeRangeModule.buildHook( + { + entityId: 1, // will be timerange entityId + validUntil: deadline, + validAfter: 0, + }, + getDefaultTimeRangeModuleAddress(this.client.chain) + ) + ); } const installValidationCall = await this.compile_raw(); + console.log("Install validation call:", installValidationCall); + return await deferralActions( this.client ).createDeferredActionTypedDataObject({ @@ -325,6 +342,17 @@ export class PermissionBuilder { async compile_raw(): Promise { this.validateConfiguration(); + // 1. Translate all permissions into raw hooks if >0 + if (this.permissions.length > 0) { + const rawHooks = this.translatePermissions(1); + // Add the translated permissions as hooks + this.addHooks(rawHooks); + } + + console.log("\n\nHOOKS:", this.hooks); + + console.log("COMPILE_RAW: HOOKS:", this.hooks); + return await installValidationActions(this.client).encodeInstallValidation({ validationConfig: this.validationConfig, selectors: this.selectors, @@ -359,6 +387,7 @@ export class PermissionBuilder { } // Used to translate consolidated permissions into raw unencoded hooks + // Note entityId will be a member object later private translatePermissions(entityId: number): RawHooks { const rawHooks: RawHooks = { [HookIdentifier.NATIVE_TOKEN_TRANSFER]: undefined, From d7c44d9fd1130cd62d7017958c4164c4d183b3ae Mon Sep 17 00:00:00 2001 From: zer0dot Date: Tue, 1 Apr 2025 13:18:53 -0400 Subject: [PATCH 04/32] feat: (WIP) permission builder --- .../src/ma-v2/actions/deferralActions.ts | 18 +- .../src/ma-v2/client/client.test.ts | 172 +++++------------- .../src/ma-v2/permissionBuilder.ts | 24 +-- 3 files changed, 62 insertions(+), 152 deletions(-) diff --git a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.ts b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.ts index 7a95ec5d71..71aa269180 100644 --- a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.ts +++ b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.ts @@ -50,8 +50,8 @@ export type DeferredActionReturnData = { export type CreateDeferredActionTypedDataParams = { callData: Hex; deadline: number; - entityId: number; - isGlobalValidation: boolean; + uoValidationEntityId: number; + uoIsGlobalValidation: boolean; nonceKeyOverride?: bigint; }; @@ -97,8 +97,8 @@ export const deferralActions: ( const createDeferredActionTypedDataObject = async ({ callData, deadline, - entityId, - isGlobalValidation, + uoValidationEntityId, + uoIsGlobalValidation, nonceKeyOverride = 0n, }: CreateDeferredActionTypedDataParams): Promise => { if (!client.account) { @@ -124,8 +124,8 @@ export const deferralActions: ( // 1 = isGlobal validation flag 0b01 const fullNonceKey: bigint = buildFullNonceKey({ nonceKey: nonceKeyOverride, - entityId, - isGlobalValidation, + entityId: uoValidationEntityId, + isGlobalValidation: uoIsGlobalValidation, isDeferredAction: true, }); @@ -191,12 +191,6 @@ export const deferralActions: ( toHex(sigLength, { size: 4 }), sig, ]); - - console.log("Encoded data len:", encodedDataLength); - console.log("Encoded call data:", encodedCallData); - console.log("Encoded data:", encodedData); - console.log("sig len:", sigLength); - return encodedData; }; diff --git a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts index a4783c0969..3e8455a389 100644 --- a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts +++ b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts @@ -355,8 +355,13 @@ describe("MA v2 Tests", async () => { await expect(getTargetBalance()).resolves.toEqual(startingAddressBalance + sendAmount); }); - it.only("installs a session key via deferred action using PermissionBuilder signed by the owner and has it sign a UO", async () => { - let provider = (await givenConnectedProvider({ signer })) + it("installs a session key via deferred action using PermissionBuilder signed by the owner and has it sign a UO", async () => { + let provider = ( + await givenConnectedProvider({ + signer, + paymasterMiddleware: "erc7677", + }) + ) .extend(installValidationActions) .extend(deferralActions); @@ -369,120 +374,42 @@ describe("MA v2 Tests", async () => { const sessionKeyEntityId = 1; const isGlobalValidation = true; - // const encodedInstallCall = await new PermissionBuilder(provider) - // // const { typedData, nonceOverride } = await new PermissionBuilder(provider) - // .configure({ - // validationConfig: { - // moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), - // entityId: sessionKeyEntityId, - // isGlobal: isGlobalValidation, - // isSignatureValidation: true, - // isUserOpValidation: true, - // }, - // installData: SingleSignerValidationModule.encodeOnInstallData({ - // entityId: sessionKeyEntityId, - // signer: await sessionKey.getAddress(), - // }), - // }) - // .addPermission({ - // permission: { - // type: PermissionType.GAS_LIMIT, - // data: { - // limit: "0x1234512", - // }, - // }, - // }) - // .compile_raw(); - - // console.log("TYPED DATA:", typedData); - - // console.log("ENCODED INSTALL CALL FROM TEST:", encodedInstallCall); - - // const receipt = await provider.sendUserOperation({ - // uo: encodedInstallCall, - // }); - - // await provider.waitForUserOperationTransaction(receipt); - - // const { typedData, nonceOverride } = await new PermissionBuilder(provider) - // .configure({ - // validationConfig: { - // moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), - // entityId: sessionKeyEntityId, - // isGlobal: isGlobalValidation, - // isSignatureValidation: true, - // isUserOpValidation: true, - // }, - // installData: SingleSignerValidationModule.encodeOnInstallData({ - // entityId: sessionKeyEntityId, - // signer: await sessionKey.getAddress(), - // }), - // }) - // .addPermission({ - // permission: { - // type: PermissionType.GAS_LIMIT, - // data: { - // limit: "0x1234512", - // }, - // }, - // }) - // .compile_deferred({ - // deadline: 0, //Math.round(Date.now() / 1000 + 100), - // uoValidationEntityId: sessionKeyEntityId, - // uoValidationIsGlobal: isGlobalValidation, - // }); - const paymaster = paymaster070.getPaymasterDetails().address; - - // // Encode install data to defer - let encodedInstallData = await provider.encodeInstallValidation({ - validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress( - provider.chain - ), - entityId: sessionKeyEntityId, - isGlobal: isGlobalValidation, - isSignatureValidation: true, - isUserOpValidation: true, - }, - selectors: [], - installData: SingleSignerValidationModule.encodeOnInstallData({ - entityId: sessionKeyEntityId, - signer: await sessionKey.getAddress(), - }), - hooks: [ - { - hookConfig: { - address: getDefaultPaymasterGuardModuleAddress(provider.chain), - entityId: 1, - hookType: HookType.VALIDATION, - hasPreHooks: true, - hasPostHooks: false, + const { typedData, nonceOverride } = await new PermissionBuilder(provider) + .configure({ + validationConfig: { + moduleAddress: getDefaultSingleSignerValidationModuleAddress( + provider.chain + ), + entityId: sessionKeyEntityId, + isGlobal: isGlobalValidation, + isSignatureValidation: true, + isUserOpValidation: true, + }, + installData: SingleSignerValidationModule.encodeOnInstallData({ + entityId: sessionKeyEntityId, + signer: await sessionKey.getAddress(), + }), + }) + .addPermission({ + permission: { + type: PermissionType.GAS_LIMIT, + data: { + limit: "0x1234512", }, - initData: PaymasterGuardModule.encodeOnInstallData({ - entityId: 1, - paymaster: paymaster, - }), }, - ], - }); - - console.log( - "EXPECTED HOOK INIT DATA:", - PaymasterGuardModule.encodeOnInstallData({ - entityId: 1, - paymaster: paymaster, }) - ); - - console.log("ACTUAL EXPECTED CALL DATA:", encodedInstallData); - - // // Build the typed data we need for the deferred action (provider/client only used for account address & entrypoint) - const { typedData, nonceOverride } = - await provider.createDeferredActionTypedDataObject({ - callData: encodedInstallData, + // .addPermission({ + // permission: { + // type: PermissionType.NATIVE_TOKEN_TRANSFER, + // data: { + // allowance: "0x1234", + // }, + // }, + // }) + .compile_deferred({ deadline: 0, - entityId: sessionKeyEntityId, - isGlobalValidation: isGlobalValidation, + uoValidationEntityId: sessionKeyEntityId, + uoIsGlobalValidation: isGlobalValidation, }); // Sign the typed data using the owner (fallback) validation, this must be done via the account to skip 6492 @@ -517,9 +444,9 @@ describe("MA v2 Tests", async () => { }); // Sign the UO with the session key - const uo = await sessionKeyClient.signUserOperation({ + const uo = (await sessionKeyClient.signUserOperation({ uoStruct: unsignedUo, - }); + })) as UserOperationRequest_v7; // Prepend the full hex for the deferred action to the new, real signature uo.signature = concatHex([signaturePrepend, uo.signature as Hex]); @@ -563,12 +490,13 @@ describe("MA v2 Tests", async () => { }); // Build the typed data we need for the deferred action (provider/client only used for account address & entrypoint) - const { typedData, nonceOverride } = await provider.createDeferredActionTypedDataObject({ - callData: encodedInstallData, - deadline: 0, - entityId: sessionKeyEntityId, - isGlobalValidation: isGlobalValidation, - }); + const { typedData, nonceOverride } = + await provider.createDeferredActionTypedDataObject({ + callData: encodedInstallData, + deadline: 0, + uoValidationEntityId: sessionKeyEntityId, // must be the UO validating entity ID + uoIsGlobalValidation: isGlobalValidation, + }); // Sign the typed data using the owner (fallback) validation, this must be done via the account to skip 6492 const deferredValidationSig = await provider.account.signTypedData(typedData); @@ -684,7 +612,7 @@ describe("MA v2 Tests", async () => { .compile_deferred({ deadline: 0, uoValidationEntityId: newSessionKeyEntityId, // UO signing session key - uoValidationIsGlobal: isGlobalValidation, // UO validation is global + uoIsGlobalValidation: isGlobalValidation, // UO validation is global }); // Sign the typed data using the first session key @@ -834,7 +762,7 @@ describe("MA v2 Tests", async () => { ], }); - // verify hook installtion succeeded + // verify hook installation succeeded await provider.waitForUserOperationTransaction(installResult); // create session key client diff --git a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts index 22164f103f..0523e25409 100644 --- a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts +++ b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts @@ -238,6 +238,8 @@ export class PermissionBuilder { // Check 2: If the permission is NOT ROOT (guaranteed), ensure there is no ROOT permission set // Will resolve to undefined if ROOT is not found + // NOTE: Technically this could be replaced by checking permissions[0] since it should not be possible + // to have >1 permission with root among them if (this.permissions.find((p) => p.type === PermissionType.ROOT)) { throw new Error( `PERMISSION: ${permission.type} => Cannot add permissions with ROOT enabled` @@ -293,11 +295,11 @@ export class PermissionBuilder { async compile_deferred({ deadline, uoValidationEntityId, - uoValidationIsGlobal, + uoIsGlobalValidation, }: { deadline: number; uoValidationEntityId: number; - uoValidationIsGlobal: boolean; + uoIsGlobalValidation: boolean; }): Promise { this.validateConfiguration(); @@ -326,15 +328,13 @@ export class PermissionBuilder { const installValidationCall = await this.compile_raw(); - console.log("Install validation call:", installValidationCall); - return await deferralActions( this.client ).createDeferredActionTypedDataObject({ callData: installValidationCall, deadline: deadline, - entityId: uoValidationEntityId, - isGlobalValidation: uoValidationIsGlobal, + uoValidationEntityId: uoValidationEntityId, + uoIsGlobalValidation: uoIsGlobalValidation, }); } @@ -349,10 +349,6 @@ export class PermissionBuilder { this.addHooks(rawHooks); } - console.log("\n\nHOOKS:", this.hooks); - - console.log("COMPILE_RAW: HOOKS:", this.hooks); - return await installValidationActions(this.client).encodeInstallValidation({ validationConfig: this.validationConfig, selectors: this.selectors, @@ -481,7 +477,6 @@ export class PermissionBuilder { "PERMISSION: CONTRACT_ACCESS => Zero address provided" ); } - // TODO (consider): Maybe ensure every address in every allowlist input is different? rawHooks[HookIdentifier.PREVAL_ALLOWLIST] = { hookConfig: { address: getDefaultAllowlistModuleAddress(this.client.chain), @@ -656,10 +651,3 @@ export class PermissionBuilder { } } } - -// Factory function to create a permission builder -export function createPermissionBuilder( - client: ModularAccountV2Client -): PermissionBuilder { - return new PermissionBuilder(client); -} From 86570a42f7b7d1cc5f2378e35ea81e0b6db71c93 Mon Sep 17 00:00:00 2001 From: howydev <132113803+howydev@users.noreply.github.com> Date: Tue, 1 Apr 2025 14:44:40 -0400 Subject: [PATCH 05/32] feat: update permission builder to take in key --- .../src/ma-v2/client/client.test.ts | 19 ++++------ .../src/ma-v2/permissionBuilder.ts | 35 +++++++++++++++---- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts index 3e8455a389..2c4c5a7900 100644 --- a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts +++ b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts @@ -23,7 +23,7 @@ import { concatHex, type TestActions, type Hex, - type ContractFunctionName, + toHex, } from "viem"; import { HookType } from "../actions/common/types.js"; import { @@ -376,19 +376,12 @@ describe("MA v2 Tests", async () => { const { typedData, nonceOverride } = await new PermissionBuilder(provider) .configure({ - validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress( - provider.chain - ), - entityId: sessionKeyEntityId, - isGlobal: isGlobalValidation, - isSignatureValidation: true, - isUserOpValidation: true, + key: { + publicKey: await sessionKey.getAddress(), + type: "secp256k1", }, - installData: SingleSignerValidationModule.encodeOnInstallData({ - entityId: sessionKeyEntityId, - signer: await sessionKey.getAddress(), - }), + entityId: sessionKeyEntityId, + nonce: BigInt(sessionKeyEntityId) + (3n << 64n), }) .addPermission({ permission: { diff --git a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts index 0523e25409..4b69d28c9c 100644 --- a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts +++ b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts @@ -17,8 +17,10 @@ import { NativeTokenLimitModule } from "./modules/native-token-limit-module/modu import { getDefaultAllowlistModuleAddress, getDefaultNativeTokenLimitModuleAddress, + getDefaultSingleSignerValidationModuleAddress, getDefaultTimeRangeModuleAddress, } from "./modules/utils.js"; +import { SingleSignerValidationModule } from "./modules/single-signer-validation/module.js"; import { AllowlistModule } from "./modules/allowlist-module/module.js"; import { TimeRangeModule } from "./modules/time-range-module/module.js"; @@ -115,6 +117,11 @@ type RawHooks = { type OneOf = T[number]; +type Key = { + publicKey: Hex; + type: "secp256k1" | "contract"; +}; + export type Permission = OneOf< [ { @@ -194,6 +201,7 @@ export class PermissionBuilder { private installData: Hex = "0x"; private permissions: Permission[] = []; private hooks: Hook[] = []; + private nonce: bigint = 0; constructor(client: ModularAccountV2Client) { this.client = client; @@ -201,20 +209,34 @@ export class PermissionBuilder { // Configures the builder configure({ - validationConfig, + key, + entityId, + nonce, selectors, - installData, hooks, }: { - validationConfig: ValidationConfig; + key: Key; + entityId: number; + nonce: bigint; selectors?: Hex[]; - installData: Hex; hooks?: Hook[]; }): this { - this.validationConfig = validationConfig; + this.validationConfig = { + moduleAddress: getDefaultSingleSignerValidationModuleAddress( + this.client.chain + ), + entityId, + isUserOpValidation: true, + isGlobal: false, + isSignatureValidation: false, + }; + this.installData = SingleSignerValidationModule.encodeOnInstallData({ + entityId: entityId, + signer: key.publicKey, + }); if (selectors) this.selectors = selectors; - this.installData = installData; if (hooks) this.hooks = hooks; + this.nonce = nonce; return this; } @@ -335,6 +357,7 @@ export class PermissionBuilder { deadline: deadline, uoValidationEntityId: uoValidationEntityId, uoIsGlobalValidation: uoIsGlobalValidation, + nonceKeyOverride: this.nonce, }); } From 07fe4af9ebcffcb04015faa926e9105e98c157aa Mon Sep 17 00:00:00 2001 From: zer0dot Date: Wed, 2 Apr 2025 15:19:21 -0400 Subject: [PATCH 06/32] feat: (wip) permission builder --- .../src/ma-v2/client/client.test.ts | 43 ++++++++---- .../src/ma-v2/permissionBuilder.ts | 67 ++++++++++--------- 2 files changed, 67 insertions(+), 43 deletions(-) diff --git a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts index 2c4c5a7900..56f0254e91 100644 --- a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts +++ b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts @@ -381,7 +381,7 @@ describe("MA v2 Tests", async () => { type: "secp256k1", }, entityId: sessionKeyEntityId, - nonce: BigInt(sessionKeyEntityId) + (3n << 64n), + nonceKeyOverride: BigInt(sessionKeyEntityId) + (3n << 64n), }) .addPermission({ permission: { @@ -589,25 +589,44 @@ describe("MA v2 Tests", async () => { // Encode install data to defer and get the deferred action typed data const { typedData, nonceOverride } = await new PermissionBuilder(provider) .configure({ - validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), - entityId: newSessionKeyEntityId, - isGlobal: isGlobalValidation, - isSignatureValidation: true, - isUserOpValidation: true, + key: { + publicKey: await sessionKey.getAddress(), + type: "secp256k1", + }, + entityId: newSessionKeyEntityId, + nonceKeyOverride: 0n, //BigInt(newSessionKeyEntityId) + (3n << 64n) + }) + .addPermission({ + permission: { + type: PermissionType.ROOT, }, - selectors: [], - installData: SingleSignerValidationModule.encodeOnInstallData({ - entityId: newSessionKeyEntityId, - signer: await newSessionKey.getAddress(), - }), }) + // .addPermission({ + // permission: { + // type: PermissionType.GAS_LIMIT, + // data: { + // limit: "0x1234512", + // }, + // }, + // }) + // .addPermission({ + // permission: { + // type: PermissionType.ACCOUNT_FUNCTIONS, + // // note: Would be great if we could get the type of "data" to narrow down once the type is set above + // data: { + // functions: ["0x12345678"], + // }, + // }, + // }) .compile_deferred({ deadline: 0, uoValidationEntityId: newSessionKeyEntityId, // UO signing session key uoIsGlobalValidation: isGlobalValidation, // UO validation is global }); + console.log(typedData); + console.log("Deferred: %s, \n\n Nonce: %s", typedData, nonceOverride); + // Sign the typed data using the first session key const deferredValidationSig = await sessionKeyClient.account.signTypedData(typedData); diff --git a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts index 4b69d28c9c..9309bd7fa3 100644 --- a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts +++ b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts @@ -12,7 +12,7 @@ import type { ModularAccountV2Client } from "./client/client.js"; import { deferralActions, type DeferredActionReturnData, -} from "./actions/DeferralActions.js"; +} from "./actions/deferralActions.js"; import { NativeTokenLimitModule } from "./modules/native-token-limit-module/module.js"; import { getDefaultAllowlistModuleAddress, @@ -178,7 +178,7 @@ export type Permission = OneOf< { // this permission grants full access to everything type: PermissionType.ROOT; - data: never; + data?: never; } ] >; @@ -201,7 +201,7 @@ export class PermissionBuilder { private installData: Hex = "0x"; private permissions: Permission[] = []; private hooks: Hook[] = []; - private nonce: bigint = 0; + private nonceKeyOverride: bigint = 0n; constructor(client: ModularAccountV2Client) { this.client = client; @@ -211,13 +211,13 @@ export class PermissionBuilder { configure({ key, entityId, - nonce, + nonceKeyOverride, selectors, hooks, }: { key: Key; entityId: number; - nonce: bigint; + nonceKeyOverride: bigint; selectors?: Hex[]; hooks?: Hook[]; }): this { @@ -236,7 +236,7 @@ export class PermissionBuilder { }); if (selectors) this.selectors = selectors; if (hooks) this.hooks = hooks; - this.nonce = nonce; + this.nonceKeyOverride = nonceKeyOverride; return this; } @@ -299,6 +299,12 @@ export class PermissionBuilder { } } + // Check 4: If the permission is ACCOUNT_FUNCTIONS, add selectors + if (permission.type === PermissionType.ACCOUNT_FUNCTIONS) { + this.selectors = [...this.selectors, ...permission.data.functions]; + return this; + } + this.permissions.push(permission); return this; @@ -357,7 +363,7 @@ export class PermissionBuilder { deadline: deadline, uoValidationEntityId: uoValidationEntityId, uoIsGlobalValidation: uoIsGlobalValidation, - nonceKeyOverride: this.nonce, + nonceKeyOverride: this.nonceKeyOverride, }); } @@ -531,30 +537,29 @@ export class PermissionBuilder { "PERMISSION: ACCOUNT_FUNCTION => No functions provided" ); // should be in add perm } - rawHooks[HookIdentifier.PREVAL_ALLOWLIST] = { - hookConfig: { - address: getDefaultAllowlistModuleAddress(this.client.chain), - entityId, - hookType: HookType.VALIDATION, - hasPreHooks: true, - hasPostHooks: false, - }, - initData: { - entityId, - inputs: [ - // Add previous inputs if they exist - ...(rawHooks[HookIdentifier.PREVAL_ALLOWLIST]?.initData - .inputs || []), - { - target: this.client.account.address, - hasSelectorAllowlist: false, - hasERC20SpendLimit: false, - erc20SpendLimit: 0n, - selectors: permission.data.functions, - }, - ], - }, - }; + // rawHooks[HookIdentifier.PREVAL_ALLOWLIST] = { + // hookConfig: { + // address: getDefaultAllowlistModuleAddress(this.client.chain), + // entityId, + // hookType: HookType.VALIDATION, + // hasPreHooks: true, + // hasPostHooks: false, + // }, + // initData: { + // entityId, + // inputs: [ + // // Add previous inputs if they exist + // ...(rawHooks[HookIdentifier.PREVAL_ALLOWLIST]?.initData.inputs || []), + // { + // target: this.client.account.address, + // hasSelectorAllowlist: false, + // hasERC20SpendLimit: false, + // erc20SpendLimit: 0n, + // selectors: permission.data.functions, + // }, + // ], + // }, + // }; break; case PermissionType.FUNCTIONS_ON_ALL_CONTRACTS: if (permission.data.functions.length === 0) { From dc2dda3386c9fa10823c5316d4a071ce17950f52 Mon Sep 17 00:00:00 2001 From: zer0dot Date: Wed, 2 Apr 2025 18:12:48 -0400 Subject: [PATCH 07/32] feat: (WIP) add sensible defaults to permission builder --- .../src/ma-v2/client/client.test.ts | 2 +- .../src/ma-v2/permissionBuilder.ts | 82 ++++++++++++------- 2 files changed, 55 insertions(+), 29 deletions(-) diff --git a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts index 56f0254e91..3614660c82 100644 --- a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts +++ b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts @@ -590,7 +590,7 @@ describe("MA v2 Tests", async () => { const { typedData, nonceOverride } = await new PermissionBuilder(provider) .configure({ key: { - publicKey: await sessionKey.getAddress(), + publicKey: await newSessionKey.getAddress(), type: "secp256k1", }, entityId: newSessionKeyEntityId, diff --git a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts index 9309bd7fa3..ee0358f869 100644 --- a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts +++ b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts @@ -24,6 +24,13 @@ import { SingleSignerValidationModule } from "./modules/single-signer-validation import { AllowlistModule } from "./modules/allowlist-module/module.js"; import { TimeRangeModule } from "./modules/time-range-module/module.js"; +// We use this to offset the ERC20 spend limit entityId +const HALF_UINT32 = 2147483647; +const ERC20_APPROVE_SELECTOR = "0x095ea7b3"; +const ERC20_TRANSFER_SELECTOR = "0xa9059cbb"; +const ACCOUNT_EXECUTE_SELECTOR = "0xb61d27f6"; +const ACCOUNT_EXECUTEBATCH_SELECTOR = "0x34fcd5be"; + export enum PermissionType { NATIVE_TOKEN_TRANSFER = "native-token-transfer", ERC20_TOKEN_TRANSFER = "erc20-token-transfer", @@ -32,7 +39,6 @@ export enum PermissionType { GAS_LIMIT = "gas-limit", // CALL_LIMIT = "call-limit", //Unimplemented // RATE_LIMIT = "rate-limit", //Unimplemented - // Custom MAv2 permissions CONTRACT_ACCESS = "contract-access", ACCOUNT_FUNCTIONS = "account-functions", FUNCTIONS_ON_ALL_CONTRACTS = "functions-on-all-contracts", @@ -371,9 +377,11 @@ export class PermissionBuilder { async compile_raw(): Promise { this.validateConfiguration(); - // 1. Translate all permissions into raw hooks if >0 + // Translate all permissions into raw hooks if >0 if (this.permissions.length > 0) { - const rawHooks = this.translatePermissions(1); + const rawHooks = this.translatePermissions( + this.validationConfig.entityId + ); // Add the translated permissions as hooks this.addHooks(rawHooks); } @@ -455,13 +463,13 @@ export class PermissionBuilder { rawHooks[HookIdentifier.ERC20_TOKEN_TRANSFER] = { hookConfig: { address: getDefaultAllowlistModuleAddress(this.client.chain), - entityId, + entityId: entityId + HALF_UINT32, hookType: HookType.EXECUTION, hasPreHooks: true, hasPostHooks: false, }, initData: { - entityId, // remember entityIds will be const from an object passed + entityId: entityId + HALF_UINT32, inputs: [ // Add previous inputs if they exist ...(rawHooks[HookIdentifier.ERC20_TOKEN_TRANSFER]?.initData @@ -476,6 +484,31 @@ export class PermissionBuilder { ], }, }; + // Also allow `approve` and `transfer` for the erc20 + rawHooks[HookIdentifier.PREVAL_ALLOWLIST] = { + hookConfig: { + address: getDefaultAllowlistModuleAddress(this.client.chain), + entityId, + hookType: HookType.VALIDATION, + hasPreHooks: true, + hasPostHooks: false, + }, + initData: { + entityId, + inputs: [ + // Add previous inputs if they exist + ...(rawHooks[HookIdentifier.PREVAL_ALLOWLIST]?.initData + .inputs || []), + { + target: permission.data.address, + hasSelectorAllowlist: true, + hasERC20SpendLimit: false, + erc20SpendLimit: 0n, + selectors: [ERC20_APPROVE_SELECTOR, ERC20_TRANSFER_SELECTOR], // approve, transfer + }, + ], + }, + }; break; case PermissionType.GAS_LIMIT: // Should only ever be added once, check is also on addPermission(s) @@ -537,29 +570,6 @@ export class PermissionBuilder { "PERMISSION: ACCOUNT_FUNCTION => No functions provided" ); // should be in add perm } - // rawHooks[HookIdentifier.PREVAL_ALLOWLIST] = { - // hookConfig: { - // address: getDefaultAllowlistModuleAddress(this.client.chain), - // entityId, - // hookType: HookType.VALIDATION, - // hasPreHooks: true, - // hasPostHooks: false, - // }, - // initData: { - // entityId, - // inputs: [ - // // Add previous inputs if they exist - // ...(rawHooks[HookIdentifier.PREVAL_ALLOWLIST]?.initData.inputs || []), - // { - // target: this.client.account.address, - // hasSelectorAllowlist: false, - // hasERC20SpendLimit: false, - // erc20SpendLimit: 0n, - // selectors: permission.data.functions, - // }, - // ], - // }, - // }; break; case PermissionType.FUNCTIONS_ON_ALL_CONTRACTS: if (permission.data.functions.length === 0) { @@ -636,6 +646,22 @@ export class PermissionBuilder { `Unsupported permission type: ${(permission as any).type}` ); } + + // isGlobal guaranteed to be false since it's only set with root permissions, + // we must add access to execute & executeBatch if there's a preVal allowlist hook set. + if (rawHooks[HookIdentifier.PREVAL_ALLOWLIST] !== undefined) { + const selectorsToAdd: `0x${string}`[] = [ + ACCOUNT_EXECUTE_SELECTOR, + ACCOUNT_EXECUTEBATCH_SELECTOR, + ]; // execute, executeBatch + + // Only add the selectors if they aren't already in this.selectors + const newSelectors = selectorsToAdd.filter( + (selector) => !this.selectors.includes(selector) + ); + + this.selectors = [...this.selectors, ...newSelectors]; + } }); return rawHooks; From 6ffb43c7ffa3a1386d7f113c1005f32aa58b2b40 Mon Sep 17 00:00:00 2001 From: zer0dot Date: Thu, 3 Apr 2025 13:35:46 -0400 Subject: [PATCH 08/32] fix: fix missing add permission on root --- account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts index ee0358f869..c70d071687 100644 --- a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts +++ b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts @@ -259,6 +259,7 @@ export class PermissionBuilder { "PERMISSION: ROOT: Cannot add ROOT permission with other permissions" ); } + this.permissions.push(permission); // Set isGlobal to true this.validationConfig.isGlobal = true; return this; From 18e8466e58d3d6bd2aac2ac50974ffee52446ac1 Mon Sep 17 00:00:00 2001 From: zer0dot Date: Thu, 3 Apr 2025 13:39:35 -0400 Subject: [PATCH 09/32] test: (wip) updating tests with permission builder --- .../src/ma-v2/client/client.test.ts | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts index 3614660c82..34755f5269 100644 --- a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts +++ b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts @@ -355,11 +355,10 @@ describe("MA v2 Tests", async () => { await expect(getTargetBalance()).resolves.toEqual(startingAddressBalance + sendAmount); }); - it("installs a session key via deferred action using PermissionBuilder signed by the owner and has it sign a UO", async () => { + it.only("installs a session key via deferred action using PermissionBuilder signed by the owner and has it sign a UO", async () => { let provider = ( await givenConnectedProvider({ signer, - paymasterMiddleware: "erc7677", }) ) .extend(installValidationActions) @@ -372,7 +371,6 @@ describe("MA v2 Tests", async () => { // Test variables const sessionKeyEntityId = 1; - const isGlobalValidation = true; const { typedData, nonceOverride } = await new PermissionBuilder(provider) .configure({ @@ -381,28 +379,28 @@ describe("MA v2 Tests", async () => { type: "secp256k1", }, entityId: sessionKeyEntityId, - nonceKeyOverride: BigInt(sessionKeyEntityId) + (3n << 64n), + nonceKeyOverride: 0n, }) .addPermission({ permission: { type: PermissionType.GAS_LIMIT, data: { - limit: "0x1234512", + limit: "0x123451222123", + }, + }, + }) + .addPermission({ + permission: { + type: PermissionType.ACCOUNT_FUNCTIONS, + data: { + functions: ["0xb61d27f6"], // execute selector }, }, }) - // .addPermission({ - // permission: { - // type: PermissionType.NATIVE_TOKEN_TRANSFER, - // data: { - // allowance: "0x1234", - // }, - // }, - // }) .compile_deferred({ deadline: 0, uoValidationEntityId: sessionKeyEntityId, - uoIsGlobalValidation: isGlobalValidation, + uoIsGlobalValidation: false, }); // Sign the typed data using the owner (fallback) validation, this must be done via the account to skip 6492 @@ -432,7 +430,7 @@ describe("MA v2 Tests", async () => { accountAddress: provider.getAddress(), signerEntity: { entityId: sessionKeyEntityId, - isGlobalValidation: isGlobalValidation, + isGlobalValidation: false, }, }); @@ -454,7 +452,9 @@ describe("MA v2 Tests", async () => { }); it("installs a session key via deferred action signed by the owner and has it sign a UO", async () => { - let provider = (await givenConnectedProvider({ signer })).extend(installValidationActions).extend(deferralActions); + let provider = (await givenConnectedProvider({ signer })) + .extend(installValidationActions) + .extend(deferralActions); await setBalance(client, { address: provider.getAddress(), From 8c73fe4f600d495c2d95ccfc777d18fc9e079b80 Mon Sep 17 00:00:00 2001 From: zer0dot Date: Thu, 3 Apr 2025 14:27:04 -0400 Subject: [PATCH 10/32] test: remove obsolete test --- .../src/ma-v2/client/client.test.ts | 311 +++++++++++------- 1 file changed, 184 insertions(+), 127 deletions(-) diff --git a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts index 34755f5269..433e4d05f0 100644 --- a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts +++ b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts @@ -50,9 +50,16 @@ import { local070Instance } from "~test/instances.js"; import { setBalance } from "viem/actions"; import { accounts } from "~test/constants.js"; import { paymaster070 } from "~test/paymaster/paymaster070.js"; -import { packAccountGasLimits, packPaymasterData } from "../../../../../aa-sdk/core/src/entrypoint/0.7.js"; +import { + packAccountGasLimits, + packPaymasterData, +} from "../../../../../aa-sdk/core/src/entrypoint/0.7.js"; import { entryPoint07Abi } from "viem/account-abstraction"; -import { alchemy, arbitrumSepolia, alchemyGasAndPaymasterAndDataMiddleware } from "@account-kit/infra"; +import { + alchemy, + arbitrumSepolia, + alchemyGasAndPaymasterAndDataMiddleware, +} from "@account-kit/infra"; import { getMAV2UpgradeToData } from "@account-kit/smart-contracts"; import { deferralActions } from "../actions/deferralActions.js"; import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; @@ -63,7 +70,9 @@ describe("MA v2 Tests", async () => { const instance = local070Instance; const isValidSigSuccess = "0x1626ba7e"; - let client: ReturnType & ReturnType & TestActions; + let client: ReturnType & + ReturnType & + TestActions; beforeAll(async () => { client = instance .getClient() @@ -71,9 +80,13 @@ describe("MA v2 Tests", async () => { .extend(testActions({ mode: "anvil" })); }); - const signer: SmartAccountSigner = new LocalAccountSigner(accounts.fundedAccountOwner); + const signer: SmartAccountSigner = new LocalAccountSigner( + accounts.fundedAccountOwner + ); - const sessionKey: SmartAccountSigner = new LocalAccountSigner(accounts.unfundedAccountOwner); + const sessionKey: SmartAccountSigner = new LocalAccountSigner( + accounts.unfundedAccountOwner + ); const target = "0x000000000000000000000000000000000000dEaD"; const sendAmount = parseEther("1"); @@ -83,60 +96,6 @@ describe("MA v2 Tests", async () => { address: target, }); - it("Install validation builder", async () => { - const provider = await givenConnectedProvider({ signer }); - - await setBalance(client, { - address: provider.getAddress(), - value: parseEther("2"), - }); - - const hookConfig = { - address: zeroAddress, - entityId: 69, - hookType: HookType.VALIDATION, - hasPreHooks: true, - hasPostHooks: false, - }; - - const res = await new PermissionBuilder(provider) - .configure({ - validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), - entityId: 1, - isGlobal: true, - isSignatureValidation: true, - isUserOpValidation: true, - }, - installData: SingleSignerValidationModule.encodeOnInstallData({ - entityId: 1, - signer: await sessionKey.getAddress(), - }), - }) - .addPermission({ - permission: { - type: PermissionType.GAS_LIMIT, - data: { - limit: "0x1234", - }, - }, - }) - .addPermission({ - permission: { - type: PermissionType.NATIVE_TOKEN_TRANSFER, - data: { - allowance: "0x1234", - }, - }, - }) - .compile_deferred({ - deadline: Math.round(Date.now() / 1000 + 100), - uoValidationEntityId: 0, - uoValidationIsGlobal: true, - }); - console.log("\n\n FIRST:", res.typedData); - }); - it("sends a simple UO", async () => { const provider = await givenConnectedProvider({ signer }); @@ -157,11 +116,15 @@ describe("MA v2 Tests", async () => { await provider.waitForUserOperationTransaction(result); - await expect(getTargetBalance()).resolves.toEqual(startingAddressBalance + sendAmount); + await expect(getTargetBalance()).resolves.toEqual( + startingAddressBalance + sendAmount + ); }); it("successfully sign + validate a message, for native and single signer validation", async () => { - const provider = (await givenConnectedProvider({ signer })).extend(installValidationActions); + const provider = (await givenConnectedProvider({ signer })).extend( + installValidationActions + ); await setBalance(instance.getClient(), { address: provider.getAddress(), @@ -177,7 +140,9 @@ describe("MA v2 Tests", async () => { // UO deploys the account to test 1271 against const result = await provider.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), + moduleAddress: getDefaultSingleSignerValidationModuleAddress( + provider.chain + ), entityId: 1, isGlobal: true, isSignatureValidation: true, @@ -197,9 +162,9 @@ describe("MA v2 Tests", async () => { let signature = await provider.signMessage({ message }); - await expect(accountContract.read.isValidSignature([hashMessage(message), signature])).resolves.toEqual( - isValidSigSuccess - ); + await expect( + accountContract.read.isValidSignature([hashMessage(message), signature]) + ).resolves.toEqual(isValidSigSuccess); // connect session key let sessionKeyClient = await createModularAccountV2Client({ @@ -212,13 +177,15 @@ describe("MA v2 Tests", async () => { signature = await sessionKeyClient.signMessage({ message }); - await expect(accountContract.read.isValidSignature([hashMessage(message), signature])).resolves.toEqual( - isValidSigSuccess - ); + await expect( + accountContract.read.isValidSignature([hashMessage(message), signature]) + ).resolves.toEqual(isValidSigSuccess); }); it("successfully sign + validate typed data messages, for native and single signer validation", async () => { - const provider = (await givenConnectedProvider({ signer })).extend(installValidationActions); + const provider = (await givenConnectedProvider({ signer })).extend( + installValidationActions + ); await setBalance(instance.getClient(), { address: provider.getAddress(), @@ -234,7 +201,9 @@ describe("MA v2 Tests", async () => { // UO deploys the account to test 1271 against const result = await provider.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), + moduleAddress: getDefaultSingleSignerValidationModuleAddress( + provider.chain + ), entityId: 1, isGlobal: true, isSignatureValidation: true, @@ -285,9 +254,9 @@ describe("MA v2 Tests", async () => { const hashedMessageTypedData = hashTypedData(typedData); let signature = await provider.signTypedData({ typedData }); - await expect(accountContract.read.isValidSignature([hashedMessageTypedData, signature])).resolves.toEqual( - isValidSigSuccess - ); + await expect( + accountContract.read.isValidSignature([hashedMessageTypedData, signature]) + ).resolves.toEqual(isValidSigSuccess); // connect session key let sessionKeyClient = await createModularAccountV2Client({ @@ -300,13 +269,15 @@ describe("MA v2 Tests", async () => { signature = await sessionKeyClient.signTypedData({ typedData }); - await expect(accountContract.read.isValidSignature([hashedMessageTypedData, signature])).resolves.toEqual( - isValidSigSuccess - ); + await expect( + accountContract.read.isValidSignature([hashedMessageTypedData, signature]) + ).resolves.toEqual(isValidSigSuccess); }); it("adds a session key with no permissions", async () => { - let provider = (await givenConnectedProvider({ signer })).extend(installValidationActions); + let provider = (await givenConnectedProvider({ signer })).extend( + installValidationActions + ); await setBalance(client, { address: provider.getAddress(), @@ -315,7 +286,9 @@ describe("MA v2 Tests", async () => { let result = await provider.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), + moduleAddress: getDefaultSingleSignerValidationModuleAddress( + provider.chain + ), entityId: 1, isGlobal: true, isSignatureValidation: true, @@ -352,7 +325,9 @@ describe("MA v2 Tests", async () => { await sessionKeyClient.waitForUserOperationTransaction(result); - await expect(getTargetBalance()).resolves.toEqual(startingAddressBalance + sendAmount); + await expect(getTargetBalance()).resolves.toEqual( + startingAddressBalance + sendAmount + ); }); it.only("installs a session key via deferred action using PermissionBuilder signed by the owner and has it sign a UO", async () => { @@ -468,7 +443,9 @@ describe("MA v2 Tests", async () => { // Encode install data to defer let encodedInstallData = await provider.encodeInstallValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), + moduleAddress: getDefaultSingleSignerValidationModuleAddress( + provider.chain + ), entityId: sessionKeyEntityId, isGlobal: isGlobalValidation, isSignatureValidation: true, @@ -492,7 +469,9 @@ describe("MA v2 Tests", async () => { }); // Sign the typed data using the owner (fallback) validation, this must be done via the account to skip 6492 - const deferredValidationSig = await provider.account.signTypedData(typedData); + const deferredValidationSig = await provider.account.signTypedData( + typedData + ); // Build the full hex to prepend to the UO signature // This MUST be done with the *same* client that has signed the typed data @@ -529,13 +508,18 @@ describe("MA v2 Tests", async () => { uo.signature = concatHex([signaturePrepend, uo.signature as Hex]); // Send the raw UserOp - const result = await sessionKeyClient.sendRawUserOperation(uo, provider.account.getEntryPoint().address); + const result = await sessionKeyClient.sendRawUserOperation( + uo, + provider.account.getEntryPoint().address + ); await provider.waitForUserOperationTransaction({ hash: result }); }); it("installs a session key via deferred action signed by another session key and has it sign a UO", async () => { - let provider = (await givenConnectedProvider({ signer })).extend(installValidationActions).extend(deferralActions); + let provider = (await givenConnectedProvider({ signer })) + .extend(installValidationActions) + .extend(deferralActions); await setBalance(client, { address: provider.getAddress(), @@ -547,7 +531,9 @@ describe("MA v2 Tests", async () => { // First, install a session key let sessionKeyInstallResult = await provider.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), + moduleAddress: getDefaultSingleSignerValidationModuleAddress( + provider.chain + ), entityId: sessionKeyEntityId, isGlobal: true, isSignatureValidation: true, @@ -580,7 +566,9 @@ describe("MA v2 Tests", async () => { .extend(deferralActions); const randomWallet = privateKeyToAccount(generatePrivateKey()); - const newSessionKey: SmartAccountSigner = new LocalAccountSigner(randomWallet); + const newSessionKey: SmartAccountSigner = new LocalAccountSigner( + randomWallet + ); // Test variables const newSessionKeyEntityId = 2; @@ -628,7 +616,9 @@ describe("MA v2 Tests", async () => { console.log("Deferred: %s, \n\n Nonce: %s", typedData, nonceOverride); // Sign the typed data using the first session key - const deferredValidationSig = await sessionKeyClient.account.signTypedData(typedData); + const deferredValidationSig = await sessionKeyClient.account.signTypedData( + typedData + ); // Build the full hex to prepend to the UO signature // This MUST be done with the *same* client that has signed the typed data @@ -665,13 +655,18 @@ describe("MA v2 Tests", async () => { uo.signature = concatHex([signaturePrepend, uo.signature as Hex]); // Send the raw UserOp (provider/client only used for account address & entrypoint) - const result = await provider.sendRawUserOperation(uo, provider.account.getEntryPoint().address); + const result = await provider.sendRawUserOperation( + uo, + provider.account.getEntryPoint().address + ); await provider.waitForUserOperationTransaction({ hash: result }); }); it("uninstalls a session key", async () => { - let provider = (await givenConnectedProvider({ signer })).extend(installValidationActions); + let provider = (await givenConnectedProvider({ signer })).extend( + installValidationActions + ); await setBalance(client, { address: provider.getAddress(), @@ -680,7 +675,9 @@ describe("MA v2 Tests", async () => { let result = await provider.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), + moduleAddress: getDefaultSingleSignerValidationModuleAddress( + provider.chain + ), entityId: 1, isGlobal: true, isSignatureValidation: true, @@ -697,7 +694,9 @@ describe("MA v2 Tests", async () => { await provider.waitForUserOperationTransaction(result); result = await provider.uninstallValidation({ - moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), + moduleAddress: getDefaultSingleSignerValidationModuleAddress( + provider.chain + ), entityId: 1, uninstallData: SingleSignerValidationModule.encodeOnUninstallData({ entityId: 1, @@ -749,7 +748,9 @@ describe("MA v2 Tests", async () => { const installResult = await provider.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), + moduleAddress: getDefaultSingleSignerValidationModuleAddress( + provider.chain + ), entityId: 1, isGlobal: true, isSignatureValidation: true, @@ -812,7 +813,9 @@ describe("MA v2 Tests", async () => { }); // verify uninstall - await expect(provider.waitForUserOperationTransaction(uninstallResult)).resolves.not.toThrowError(); + await expect( + provider.waitForUserOperationTransaction(uninstallResult) + ).resolves.not.toThrowError(); }); it("installs paymaster guard module, verifies use of invalid paymaster, then uninstalls module", async () => { @@ -837,7 +840,9 @@ describe("MA v2 Tests", async () => { const installResult = await provider.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), + moduleAddress: getDefaultSingleSignerValidationModuleAddress( + provider.chain + ), entityId: 1, isGlobal: true, isSignatureValidation: true, @@ -890,7 +895,9 @@ describe("MA v2 Tests", async () => { }); const uninstallResult = await provider.uninstallValidation({ - moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), + moduleAddress: getDefaultSingleSignerValidationModuleAddress( + provider.chain + ), entityId: 1, uninstallData: SingleSignerValidationModule.encodeOnUninstallData({ entityId: 1, @@ -899,11 +906,15 @@ describe("MA v2 Tests", async () => { }); // verify uninstall - await expect(provider.waitForUserOperationTransaction(uninstallResult)).resolves.not.toThrowError(); + await expect( + provider.waitForUserOperationTransaction(uninstallResult) + ).resolves.not.toThrowError(); }); it("installs allowlist module, uses, then uninstalls", async () => { - let provider = (await givenConnectedProvider({ signer })).extend(installValidationActions); + let provider = (await givenConnectedProvider({ signer })).extend( + installValidationActions + ); await setBalance(client, { address: provider.getAddress(), @@ -913,7 +924,9 @@ describe("MA v2 Tests", async () => { // install validation module const installResult = await provider.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), + moduleAddress: getDefaultSingleSignerValidationModuleAddress( + provider.chain + ), entityId: 1, isGlobal: true, isSignatureValidation: true, @@ -939,15 +952,18 @@ describe("MA v2 Tests", async () => { ).extend(installValidationActions); // verify we can call into the zero address before allow list hook is installed - const sendResultBeforeHookInstallation = await sessionKeyProvider.sendUserOperation({ - uo: { - target: zeroAddress, - value: 0n, - data: "0x", - }, - }); + const sendResultBeforeHookInstallation = + await sessionKeyProvider.sendUserOperation({ + uo: { + target: zeroAddress, + value: 0n, + data: "0x", + }, + }); - await provider.waitForUserOperationTransaction(sendResultBeforeHookInstallation); + await provider.waitForUserOperationTransaction( + sendResultBeforeHookInstallation + ); const hookInstallData = AllowlistModule.encodeOnInstallData({ entityId: 1, @@ -965,7 +981,9 @@ describe("MA v2 Tests", async () => { // install hook const installHookResult = await provider.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), + moduleAddress: getDefaultSingleSignerValidationModuleAddress( + provider.chain + ), entityId: 1, isGlobal: true, isSignatureValidation: true, @@ -1026,7 +1044,9 @@ describe("MA v2 Tests", async () => { }); const uninstallResult = await provider.uninstallValidation({ - moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), + moduleAddress: getDefaultSingleSignerValidationModuleAddress( + provider.chain + ), entityId: 1, uninstallData: SingleSignerValidationModule.encodeOnUninstallData({ entityId: 1, @@ -1038,7 +1058,9 @@ describe("MA v2 Tests", async () => { }); it("installs native token limit module, uses, then uninstalls", async () => { - let provider = (await givenConnectedProvider({ signer })).extend(installValidationActions); + let provider = (await givenConnectedProvider({ signer })).extend( + installValidationActions + ); await setBalance(client, { address: provider.getAddress(), @@ -1049,7 +1071,9 @@ describe("MA v2 Tests", async () => { const installResult = await provider.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), + moduleAddress: getDefaultSingleSignerValidationModuleAddress( + provider.chain + ), entityId: 1, isGlobal: true, isSignatureValidation: true, @@ -1082,7 +1106,9 @@ describe("MA v2 Tests", async () => { data: "0x", }, }); - await provider.waitForUserOperationTransaction(preHookInstallationSendResult); + await provider.waitForUserOperationTransaction( + preHookInstallationSendResult + ); // Let's verify the module's limit is set correctly after installation const hookInstallData = NativeTokenLimitModule.encodeOnInstallData({ @@ -1092,7 +1118,9 @@ describe("MA v2 Tests", async () => { const installHookResult = await provider.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), + moduleAddress: getDefaultSingleSignerValidationModuleAddress( + provider.chain + ), entityId: 1, isGlobal: true, isSignatureValidation: true, @@ -1152,7 +1180,9 @@ describe("MA v2 Tests", async () => { }); const uninstallResult = await provider.uninstallValidation({ - moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), + moduleAddress: getDefaultSingleSignerValidationModuleAddress( + provider.chain + ), entityId: 1, uninstallData: SingleSignerValidationModule.encodeOnUninstallData({ entityId: 1, @@ -1192,7 +1222,9 @@ describe("MA v2 Tests", async () => { const installResult = await provider.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), + moduleAddress: getDefaultSingleSignerValidationModuleAddress( + provider.chain + ), entityId: 1, isGlobal: true, isSignatureValidation: true, @@ -1253,7 +1285,9 @@ describe("MA v2 Tests", async () => { sender: sessionKeyProvider.account.address, nonce: fromHex(signedUO.nonce, "bigint"), initCode: - signedUO.factory && signedUO.factoryData ? concat([signedUO.factory, signedUO.factoryData]) : "0x", + signedUO.factory && signedUO.factoryData + ? concat([signedUO.factory, signedUO.factoryData]) + : "0x", callData: signedUO.callData, accountGasLimits: packAccountGasLimits({ verificationGasLimit: signedUO.verificationGasLimit, @@ -1268,7 +1302,8 @@ describe("MA v2 Tests", async () => { signedUO.paymaster && isAddress(signedUO.paymaster) ? packPaymasterData({ paymaster: signedUO.paymaster, - paymasterVerificationGasLimit: signedUO.paymasterVerificationGasLimit, + paymasterVerificationGasLimit: + signedUO.paymasterVerificationGasLimit, paymasterPostOpGasLimit: signedUO.paymasterPostOpGasLimit, paymasterData: signedUO.paymasterData, }) @@ -1314,7 +1349,9 @@ describe("MA v2 Tests", async () => { const installResult = await provider.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress(provider.chain), + moduleAddress: getDefaultSingleSignerValidationModuleAddress( + provider.chain + ), entityId: 2, isGlobal: true, isSignatureValidation: true, @@ -1376,13 +1413,18 @@ describe("MA v2 Tests", async () => { sender: sessionKeyProvider.account.address, nonce: fromHex(signedUO.nonce, "bigint"), initCode: - signedUO.factory && signedUO.factoryData ? concat([signedUO.factory, signedUO.factoryData]) : "0x", + signedUO.factory && signedUO.factoryData + ? concat([signedUO.factory, signedUO.factoryData]) + : "0x", callData: signedUO.callData, accountGasLimits: packAccountGasLimits({ verificationGasLimit: signedUO.verificationGasLimit, callGasLimit: signedUO.callGasLimit, }), - preVerificationGas: fromHex(signedUO.preVerificationGas, "bigint"), + preVerificationGas: fromHex( + signedUO.preVerificationGas, + "bigint" + ), gasFees: packAccountGasLimits({ maxPriorityFeePerGas: signedUO.maxPriorityFeePerGas, maxFeePerGas: signedUO.maxFeePerGas, @@ -1391,7 +1433,8 @@ describe("MA v2 Tests", async () => { signedUO.paymaster && isAddress(signedUO.paymaster) ? packPaymasterData({ paymaster: signedUO.paymaster, - paymasterVerificationGasLimit: signedUO.paymasterVerificationGasLimit, + paymasterVerificationGasLimit: + signedUO.paymasterVerificationGasLimit, paymasterPostOpGasLimit: signedUO.paymasterPostOpGasLimit, paymasterData: signedUO.paymasterData, }) @@ -1405,7 +1448,11 @@ describe("MA v2 Tests", async () => { }); } catch (err: any) { // verify that simulation fails due to violation of time range restriction on session key - assert(err.metaMessages.some((str: string) => str.includes("AA22 expired or not due"))); + assert( + err.metaMessages.some((str: string) => + str.includes("AA22 expired or not due") + ) + ); } client.setAutomine(true); @@ -1457,7 +1504,9 @@ describe("MA v2 Tests", async () => { // deploy the account and install at entity id 1 with global validation const uo1 = await newClient.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress(newClient.chain), + moduleAddress: getDefaultSingleSignerValidationModuleAddress( + newClient.chain + ), entityId: 1, isGlobal: true, isSignatureValidation: false, @@ -1472,7 +1521,10 @@ describe("MA v2 Tests", async () => { }); await newClient.waitForUserOperationTransaction(uo1); - const fns: ContractFunctionName[] = ["execute", "executeBatch"]; + const fns: ContractFunctionName[] = [ + "execute", + "executeBatch", + ]; const selectors = fns.map( (s) => @@ -1485,7 +1537,9 @@ describe("MA v2 Tests", async () => { // deploy the account and install some entity ids with selector validation const uo2 = await newClient.installValidation({ validationConfig: { - moduleAddress: getDefaultSingleSignerValidationModuleAddress(newClient.chain), + moduleAddress: getDefaultSingleSignerValidationModuleAddress( + newClient.chain + ), entityId: 2, isGlobal: false, isSignatureValidation: false, @@ -1540,9 +1594,10 @@ describe("MA v2 Tests", async () => { value: parseEther("2"), }); - const { createModularAccountV2FromExisting, ...upgradeToData } = await getMAV2UpgradeToData(lightAccountClient, { - account: lightAccountClient.account, - }); + const { createModularAccountV2FromExisting, ...upgradeToData } = + await getMAV2UpgradeToData(lightAccountClient, { + account: lightAccountClient.account, + }); await lightAccountClient.upgradeAccount({ upgradeTo: upgradeToData, @@ -1569,7 +1624,9 @@ describe("MA v2 Tests", async () => { await maV2Client.waitForUserOperationTransaction(result); - await expect(getTargetBalance()).resolves.toEqual(startingAddressBalance + sendAmount); + await expect(getTargetBalance()).resolves.toEqual( + startingAddressBalance + sendAmount + ); }); it("alchemy client calls the createAlchemySmartAccountClient", async () => { From d242224bed9598475220a6a76bd5e070af68279e Mon Sep 17 00:00:00 2001 From: howydev <132113803+howydev@users.noreply.github.com> Date: Thu, 3 Apr 2025 14:40:43 -0400 Subject: [PATCH 11/32] fix: boom lfg --- account-kit/smart-contracts/src/ma-v2/client/client.test.ts | 2 +- account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts index 433e4d05f0..2ecead0d2d 100644 --- a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts +++ b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts @@ -360,7 +360,7 @@ describe("MA v2 Tests", async () => { permission: { type: PermissionType.GAS_LIMIT, data: { - limit: "0x123451222123", + limit: toHex(parseEther("1")), }, }, }) diff --git a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts index c70d071687..24d129c737 100644 --- a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts +++ b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts @@ -1,4 +1,4 @@ -import { zeroAddress, type Address, type Hex } from "viem"; +import { zeroAddress, type Address, type Hex, decodeFunctionData } from "viem"; import { HookType, type HookConfig, @@ -23,6 +23,7 @@ import { import { SingleSignerValidationModule } from "./modules/single-signer-validation/module.js"; import { AllowlistModule } from "./modules/allowlist-module/module.js"; import { TimeRangeModule } from "./modules/time-range-module/module.js"; +import { modularAccountAbi } from "./index.js"; // We use this to offset the ERC20 spend limit entityId const HALF_UINT32 = 2147483647; From 6d1d65c20540d7afaad9d0d0a783d8fafcfe379d Mon Sep 17 00:00:00 2001 From: howydev <132113803+howydev@users.noreply.github.com> Date: Thu, 3 Apr 2025 15:00:22 -0400 Subject: [PATCH 12/32] chore: lint --- .../aa-sdk/core/functions/tracingHeader.mdx | 43 +++++++++++++++++++ .../BaseAlchemySigner/toSolanaSigner.mdx | 41 ++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 site/pages/reference/aa-sdk/core/functions/tracingHeader.mdx create mode 100644 site/pages/reference/account-kit/signer/classes/BaseAlchemySigner/toSolanaSigner.mdx diff --git a/site/pages/reference/aa-sdk/core/functions/tracingHeader.mdx b/site/pages/reference/aa-sdk/core/functions/tracingHeader.mdx new file mode 100644 index 0000000000..5e0d1e34d3 --- /dev/null +++ b/site/pages/reference/aa-sdk/core/functions/tracingHeader.mdx @@ -0,0 +1,43 @@ +--- +# This file is autogenerated + +title: tracingHeader +description: Overview of the tracingHeader method +--- + +# tracingHeader + +We wanted a transport that is adding the add headers for the headers that we are sending to the server for tracing cross actions +and services. + +## Import + +```ts +import { tracingHeader } from "@aa-sdk/core"; +``` + +## Usage + +```ts +import { createPublicClient, http } from "viem"; +import { tracingHeader } from "@aa-sdk/core"; + +const clientWithTracing = createPublicClient({ + transport: tracingHeader({ + breadcrumb: "myMethodOrAction", + transport: http(OTHER_RPC_URL), + }), +}); +``` + +## Parameters + +### params + +`TracingHeadersParams` +tracing headers config to wrap around a transport to add in headers for tracing + +## Returns + +`CustomTransport` +a viem Transport that splits traffic diff --git a/site/pages/reference/account-kit/signer/classes/BaseAlchemySigner/toSolanaSigner.mdx b/site/pages/reference/account-kit/signer/classes/BaseAlchemySigner/toSolanaSigner.mdx new file mode 100644 index 0000000000..1c7420ca45 --- /dev/null +++ b/site/pages/reference/account-kit/signer/classes/BaseAlchemySigner/toSolanaSigner.mdx @@ -0,0 +1,41 @@ +--- +# This file is autogenerated + +title: toSolanaSigner +description: Overview of the toSolanaSigner method +--- + +# toSolanaSigner + +Creates a new instance of `SolanaSigner` using the provided inner value. +This requires the signer to be authenticated first + +## Import + +```ts +import { BaseAlchemySigner } from "@account-kit/signer"; +``` + +## Usage + +```ts +import { AlchemyWebSigner } from "@account-kit/signer"; + +const signer = new AlchemyWebSigner({ + client: { + connection: { + rpcUrl: "/api/rpc", + }, + iframeConfig: { + iframeContainerId: "alchemy-signer-iframe-container", + }, + }, +}); + +const solanaSigner = signer.toSolanaSigner(); +``` + +## Returns + +`SolanaSigner` +A new instance of `SolanaSigner` From 4509ea9ce71f0ee9e38131e3e16946c06f998f86 Mon Sep 17 00:00:00 2001 From: howydev <132113803+howydev@users.noreply.github.com> Date: Thu, 3 Apr 2025 15:28:34 -0400 Subject: [PATCH 13/32] fix: test --- .../src/ma-v2/client/client.test.ts | 27 ++++--------------- .../src/ma-v2/permissionBuilder.ts | 3 +-- 2 files changed, 6 insertions(+), 24 deletions(-) diff --git a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts index 2ecead0d2d..7f173b342d 100644 --- a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts +++ b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts @@ -330,7 +330,7 @@ describe("MA v2 Tests", async () => { ); }); - it.only("installs a session key via deferred action using PermissionBuilder signed by the owner and has it sign a UO", async () => { + it("installs a session key via deferred action using PermissionBuilder signed by the owner and has it sign a UO", async () => { let provider = ( await givenConnectedProvider({ signer, @@ -589,23 +589,6 @@ describe("MA v2 Tests", async () => { type: PermissionType.ROOT, }, }) - // .addPermission({ - // permission: { - // type: PermissionType.GAS_LIMIT, - // data: { - // limit: "0x1234512", - // }, - // }, - // }) - // .addPermission({ - // permission: { - // type: PermissionType.ACCOUNT_FUNCTIONS, - // // note: Would be great if we could get the type of "data" to narrow down once the type is set above - // data: { - // functions: ["0x12345678"], - // }, - // }, - // }) .compile_deferred({ deadline: 0, uoValidationEntityId: newSessionKeyEntityId, // UO signing session key @@ -1459,7 +1442,7 @@ describe("MA v2 Tests", async () => { }); it("tests entity id and nonce selection", async () => { - let newClient = (await givenConnectedProvider({ signer, salt: 1n })) + let newClient = (await givenConnectedProvider({ signer })) .extend(deferralActions) .extend(installValidationActions); @@ -1668,18 +1651,18 @@ describe("MA v2 Tests", async () => { expect(notAlchemyClientSpy).toHaveBeenCalled(); }); + let salt = 1n; + const givenConnectedProvider = async ({ signer, signerEntity, accountAddress, paymasterMiddleware, - salt = 0n, }: { signer: SmartAccountSigner; signerEntity?: SignerEntity; accountAddress?: `0x${string}`; paymasterMiddleware?: "alchemyGasAndPaymasterAndData" | "erc7677"; - salt?: bigint; }) => createModularAccountV2Client({ chain: instance.chain, @@ -1696,6 +1679,6 @@ describe("MA v2 Tests", async () => { : paymasterMiddleware === "erc7677" ? erc7677Middleware() : {}), - salt, + salt: salt++, }); }); diff --git a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts index 24d129c737..c70d071687 100644 --- a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts +++ b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts @@ -1,4 +1,4 @@ -import { zeroAddress, type Address, type Hex, decodeFunctionData } from "viem"; +import { zeroAddress, type Address, type Hex } from "viem"; import { HookType, type HookConfig, @@ -23,7 +23,6 @@ import { import { SingleSignerValidationModule } from "./modules/single-signer-validation/module.js"; import { AllowlistModule } from "./modules/allowlist-module/module.js"; import { TimeRangeModule } from "./modules/time-range-module/module.js"; -import { modularAccountAbi } from "./index.js"; // We use this to offset the ERC20 spend limit entityId const HALF_UINT32 = 2147483647; From bd94d6fca95badaa7ce28cb436299a3c8f711e0c Mon Sep 17 00:00:00 2001 From: howy <132113803+howydev@users.noreply.github.com> Date: Thu, 3 Apr 2025 20:39:04 -0400 Subject: [PATCH 14/32] test: deferred actions e2e (#1505) * feat: (WIP) working on permissions * feat: add mav2 account/client mode, via deferred action * chore: add todos * feat: change back to context hex input --------- Co-authored-by: zer0dot --- .../account/common/modularAccountV2Base.ts | 60 +++++-- .../src/ma-v2/account/modularAccountV2.ts | 3 + .../src/ma-v2/account/nativeSMASigner.ts | 34 ++-- .../src/ma-v2/actions/deferralActions.test.ts | 157 ++++++++++++++++++ .../src/ma-v2/actions/deferralActions.ts | 1 + .../src/ma-v2/client/client.test.ts | 3 - .../single-signer-validation/signer.ts | 32 ++-- .../src/ma-v2/permissionBuilder.ts | 19 ++- 8 files changed, 266 insertions(+), 43 deletions(-) create mode 100644 account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts diff --git a/account-kit/smart-contracts/src/ma-v2/account/common/modularAccountV2Base.ts b/account-kit/smart-contracts/src/ma-v2/account/common/modularAccountV2Base.ts index 407bf4e580..46fda0fdd8 100644 --- a/account-kit/smart-contracts/src/ma-v2/account/common/modularAccountV2Base.ts +++ b/account-kit/smart-contracts/src/ma-v2/account/common/modularAccountV2Base.ts @@ -85,6 +85,7 @@ export type CreateMAV2BaseParams< signer: TSigner; signerEntity?: SignerEntity; accountAddress: Address; + deferredAction?: Hex; }; export type CreateMAV2BaseReturnType< @@ -108,6 +109,7 @@ export async function createMAv2Base< entityId = DEFAULT_OWNER_ENTITY_ID, } = {}, accountAddress, + deferredAction, ...remainingToSmartContractAccountParams } = config; @@ -120,6 +122,39 @@ export async function createMAv2Base< chain, }); + const entryPointContract = getContract({ + address: entryPoint.address, + abi: entryPoint.abi, + client, + }); + + let useDeferredAction: boolean = false; + let nonce: bigint = 0n; + let deferredActionData: Hex = "0x"; + let hasAssociatedExecHooks: boolean = false; + + if (deferredAction) { + if (deferredAction.slice(3, 5) !== "00") { + throw new Error("Invalid deferred action version"); + } + nonce = BigInt(`0x${deferredAction.slice(6, 70)}`); + // Set these values if the deferred action has not been consumed. We check this with the EP + const nextNonceForDeferredActionNonce: bigint = + (await entryPointContract.read.getNonce([ + accountAddress, + nonce >> 64n, + ])) as bigint; + + // we only add the deferred action in if the nonce has not been consumed + if (nonce === nextNonceForDeferredActionNonce) { + useDeferredAction = true; + deferredActionData = `0x${deferredAction.slice(70)}`; + hasAssociatedExecHooks = deferredAction[6] === "1"; + } else if (nonce > nextNonceForDeferredActionNonce) { + throw new Error("Deferred action nonce invalid"); + } + } + const encodeExecute: (tx: AccountOp) => Promise = async ({ target, data, @@ -150,18 +185,16 @@ export async function createMAv2Base< const isAccountDeployed: () => Promise = async () => !!(await client.getCode({ address: accountAddress })); - // TODO: add deferred action flag + const getNonce = async (nonceKey: bigint = 0n): Promise => { + if (useDeferredAction && deferredAction) { + return nonce; + } + if (nonceKey > maxUint152) { throw new InvalidNonceKeyError(nonceKey); } - const entryPointContract = getContract({ - address: entryPoint.address, - abi: entryPoint.abi, - client, - }); - const fullNonceKey: bigint = (nonceKey << 40n) + (BigInt(entityId) << 8n) + @@ -216,7 +249,8 @@ export async function createMAv2Base< entityId: Number(entityId), }); - return validationData.executionHooks.length + return (useDeferredAction && hasAssociatedExecHooks) || + validationData.executionHooks.length ? concatHex([executeUserOpSelector, callData]) : callData; }; @@ -231,8 +265,14 @@ export async function createMAv2Base< encodeBatchExecute, getNonce, ...(entityId === DEFAULT_OWNER_ENTITY_ID - ? nativeSMASigner(signer, chain, accountAddress) - : singleSignerMessageSigner(signer, chain, accountAddress, entityId)), + ? nativeSMASigner(signer, chain, accountAddress, deferredActionData) + : singleSignerMessageSigner( + signer, + chain, + accountAddress, + entityId, + deferredActionData + )), }); return { diff --git a/account-kit/smart-contracts/src/ma-v2/account/modularAccountV2.ts b/account-kit/smart-contracts/src/ma-v2/account/modularAccountV2.ts index 044621f8b5..681fc8e78b 100644 --- a/account-kit/smart-contracts/src/ma-v2/account/modularAccountV2.ts +++ b/account-kit/smart-contracts/src/ma-v2/account/modularAccountV2.ts @@ -36,6 +36,7 @@ export type CreateModularAccountV2Params< > & { signer: TSigner; entryPoint?: EntryPointDef<"0.7.0", Chain>; + deferredAction?: Hex; signerEntity?: SignerEntity; }) & ( @@ -103,6 +104,7 @@ export async function createModularAccountV2( entityId: DEFAULT_OWNER_ENTITY_ID, }, signerEntity: { entityId = DEFAULT_OWNER_ENTITY_ID } = {}, + deferredAction, } = config; const client = createBundlerClient({ @@ -183,6 +185,7 @@ export async function createModularAccountV2( signer, entryPoint, signerEntity, + deferredAction, ...accountFunctions, }); } diff --git a/account-kit/smart-contracts/src/ma-v2/account/nativeSMASigner.ts b/account-kit/smart-contracts/src/ma-v2/account/nativeSMASigner.ts index 6ba64d4745..476e7fa174 100644 --- a/account-kit/smart-contracts/src/ma-v2/account/nativeSMASigner.ts +++ b/account-kit/smart-contracts/src/ma-v2/account/nativeSMASigner.ts @@ -9,6 +9,7 @@ import { type Chain, type Address, concat, + concatHex, } from "viem"; import { @@ -23,7 +24,6 @@ import { SignatureType } from "../modules/utils.js"; * @example * ```ts * import { nativeSMASigner } from "@account-kit/smart-contracts"; - * import { LocalAccountSigner } from "@aa-sdk/core"; * * const MNEMONIC = "...": @@ -38,31 +38,37 @@ import { SignatureType } from "../modules/utils.js"; * @param {SmartAccountSigner} signer Signer to use for signing operations * @param {Chain} chain Chain object for the signer * @param {Address} accountAddress address of the smart account using this signer + * @param {Hex} deferredActionData optional deferred action data to prepend to the uo signatures * @returns {object} an object with methods for signing operations and managing signatures */ export const nativeSMASigner = ( signer: SmartAccountSigner, chain: Chain, - accountAddress: Address + accountAddress: Address, + deferredActionData?: Hex ) => { return { getDummySignature: (): Hex => { - const dummyEcdsaSignature = - "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c"; - - return packUOSignature({ + const sig = packUOSignature({ // orderedHookData: [], - validationSignature: dummyEcdsaSignature, + validationSignature: + "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c", }); + + return deferredActionData ? concatHex([deferredActionData, sig]) : sig; }, - signUserOperationHash: (uoHash: Hex): Promise => { - return signer.signMessage({ raw: uoHash }).then((signature: Hex) => - packUOSignature({ - // orderedHookData: [], - validationSignature: signature, - }) - ); + signUserOperationHash: async (uoHash: Hex): Promise => { + const sig = await signer + .signMessage({ raw: uoHash }) + .then((signature: Hex) => + packUOSignature({ + // orderedHookData: [], + validationSignature: signature, + }) + ); + + return deferredActionData ? concatHex([deferredActionData, sig]) : sig; }, // we apply the expected 1271 packing here since the account contract will expect it diff --git a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts new file mode 100644 index 0000000000..1e31b50dc0 --- /dev/null +++ b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts @@ -0,0 +1,157 @@ +import { + erc7677Middleware, + LocalAccountSigner, + type SmartAccountSigner, +} from "@aa-sdk/core"; +import { + custom, + parseEther, + publicActions, + testActions, + toHex, + type TestActions, +} from "viem"; +import { installValidationActions } from "@account-kit/smart-contracts/experimental"; +import { + // createModularAccountV2Client, + type SignerEntity, +} from "@account-kit/smart-contracts"; +import { local070Instance } from "~test/instances.js"; +import { setBalance } from "viem/actions"; +import { accounts } from "~test/constants.js"; +import { alchemyGasAndPaymasterAndDataMiddleware } from "@account-kit/infra"; +import { deferralActions } from "./deferralActions.js"; +import { PermissionBuilder, PermissionType } from "../permissionBuilder.js"; +import { createModularAccountV2Client } from "../client/client.js"; + +// Note: These tests maintain a shared state to not break the local-running rundler by desyncing the chain. +describe("MA v2 deferral actions tests", async () => { + const instance = local070Instance; + + let client: ReturnType & + ReturnType & + TestActions; + + beforeAll(async () => { + client = instance + .getClient() + .extend(publicActions) + .extend(testActions({ mode: "anvil" })); + }); + + const signer: SmartAccountSigner = new LocalAccountSigner( + accounts.fundedAccountOwner + ); + + const target = "0x000000000000000000000000000000000000dEaD"; + const sendAmount = parseEther("1"); + + it("tests the full deferred actions flow", async () => { + const provider = (await givenConnectedProvider({ signer })) + .extend(deferralActions) + .extend(installValidationActions); + + await setBalance(instance.getClient(), { + address: provider.getAddress(), + value: parseEther("2"), + }); + + const sessionKeyEntityId = 1; + const sessionKey: SmartAccountSigner = new LocalAccountSigner( + accounts.unfundedAccountOwner + ); + + // these can be default values or from call arguments + const { entityId, nonce } = await provider.getEntityIdAndNonce({ + entityId: 0, + nonceKey: 0n, + isGlobalValidation: true, + }); + + // TODO: remove nonce override here + const { typedData, hasAssociatedExecHooks } = await new PermissionBuilder( + provider + ) + .configure({ + key: { + publicKey: await sessionKey.getAddress(), + type: "secp256k1", + }, + entityId, + nonceKeyOverride: 0n, // TODO: add nonce override here + }) + .addPermission({ + permission: { + type: PermissionType.ROOT, + }, + }) + .compile_deferred({ + deadline: 0, + uoValidationEntityId: entityId, + uoIsGlobalValidation: true, + }); + + const sig = await provider.account.signTypedData(typedData); + + const deferredActionDigest = await provider.buildDeferredActionDigest({ + typedData, + sig, + }); + + // TODO: need nonce, orig account address, orig account initcode + const sessionKeyClient = await createModularAccountV2Client({ + transport: custom(instance.getClient()), + chain: instance.chain, + accountAddress: provider.getAddress(), + signer: sessionKey, + initCode: provider.account.getInitCode(), + deferredAction: `0x00${hasAssociatedExecHooks ? "01" : "00"}${toHex( + nonce, + { + size: 32, + } + ).slice(2)}${deferredActionDigest.slice(2)}`, + }); + + const uoResult = await sessionKeyClient.sendUserOperation({ + uo: { + target: target, + value: sendAmount, + data: "0x", + }, + }); + + await provider.waitForUserOperationTransaction(uoResult); + }); + + const givenConnectedProvider = async ({ + signer, + signerEntity, + accountAddress, + paymasterMiddleware, + salt = 0n, + }: { + signer: SmartAccountSigner; + signerEntity?: SignerEntity; + accountAddress?: `0x${string}`; + paymasterMiddleware?: "alchemyGasAndPaymasterAndData" | "erc7677"; + salt?: bigint; + }) => + createModularAccountV2Client({ + chain: instance.chain, + signer, + accountAddress, + signerEntity, + transport: custom(instance.getClient()), + ...(paymasterMiddleware === "alchemyGasAndPaymasterAndData" + ? alchemyGasAndPaymasterAndDataMiddleware({ + policyId: "FAKE_POLICY_ID", + // @ts-ignore (expects an alchemy transport, but we're using a custom transport for mocking) + transport: custom(instance.getClient()), + }) + : paymasterMiddleware === "erc7677" + ? erc7677Middleware() + : {}), + salt, + }); +}); diff --git a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.ts b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.ts index 71aa269180..274e1ff419 100644 --- a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.ts +++ b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.ts @@ -182,6 +182,7 @@ export const deferralActions: ( [validationLocator, typedData.message.deadline, typedData.message.call] ); + // TODO: encoded call data as input const encodedDataLength = size(encodedCallData); const sigLength = size(sig); diff --git a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts index 7f173b342d..deb969cf92 100644 --- a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts +++ b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts @@ -595,9 +595,6 @@ describe("MA v2 Tests", async () => { uoIsGlobalValidation: isGlobalValidation, // UO validation is global }); - console.log(typedData); - console.log("Deferred: %s, \n\n Nonce: %s", typedData, nonceOverride); - // Sign the typed data using the first session key const deferredValidationSig = await sessionKeyClient.account.signTypedData( typedData diff --git a/account-kit/smart-contracts/src/ma-v2/modules/single-signer-validation/signer.ts b/account-kit/smart-contracts/src/ma-v2/modules/single-signer-validation/signer.ts index 5d29b27e90..f0cd764ecb 100644 --- a/account-kit/smart-contracts/src/ma-v2/modules/single-signer-validation/signer.ts +++ b/account-kit/smart-contracts/src/ma-v2/modules/single-signer-validation/signer.ts @@ -39,32 +39,38 @@ import { packUOSignature, pack1271Signature } from "../../utils.js"; * @param {Chain} chain Chain object for the signer * @param {Address} accountAddress address of the smart account using this signer * @param {number} entityId the entity id of the signing validation + * @param {Hex} deferredActionData optional deferred action data to prepend to the uo signatures * @returns {object} an object with methods for signing operations and managing signatures */ export const singleSignerMessageSigner = ( signer: SmartAccountSigner, chain: Chain, accountAddress: Address, - entityId: number + entityId: number, + deferredActionData?: Hex ) => { return { getDummySignature: (): Hex => { - const dummyEcdsaSignature = - "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c"; - - return packUOSignature({ + const sig = packUOSignature({ // orderedHookData: [], - validationSignature: dummyEcdsaSignature, + validationSignature: + "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c", }); + + return deferredActionData ? concatHex([deferredActionData, sig]) : sig; }, - signUserOperationHash: (uoHash: Hex): Promise => { - return signer.signMessage({ raw: uoHash }).then((signature: Hex) => - packUOSignature({ - // orderedHookData: [], - validationSignature: signature, - }) - ); + signUserOperationHash: async (uoHash: Hex): Promise => { + const sig = await signer + .signMessage({ raw: uoHash }) + .then((signature: Hex) => + packUOSignature({ + // orderedHookData: [], + validationSignature: signature, + }) + ); + + return deferredActionData ? concatHex([deferredActionData, sig]) : sig; }, // we apply the expected 1271 packing here since the account contract will expect it diff --git a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts index c70d071687..0639733eec 100644 --- a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts +++ b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts @@ -11,7 +11,7 @@ import { import type { ModularAccountV2Client } from "./client/client.js"; import { deferralActions, - type DeferredActionReturnData, + type DeferredActionTypedData, } from "./actions/deferralActions.js"; import { NativeTokenLimitModule } from "./modules/native-token-limit-module/module.js"; import { @@ -208,6 +208,7 @@ export class PermissionBuilder { private permissions: Permission[] = []; private hooks: Hook[] = []; private nonceKeyOverride: bigint = 0n; + private hasAssociatedExecHooks: boolean = false; constructor(client: ModularAccountV2Client) { this.client = client; @@ -335,7 +336,11 @@ export class PermissionBuilder { deadline: number; uoValidationEntityId: number; uoIsGlobalValidation: boolean; - }): Promise { + }): Promise<{ + typedData: DeferredActionTypedData; + hasAssociatedExecHooks: boolean; + nonceOverride: bigint; + }> { this.validateConfiguration(); // Maybe add checks, like zero address module addr @@ -363,7 +368,7 @@ export class PermissionBuilder { const installValidationCall = await this.compile_raw(); - return await deferralActions( + const { typedData, nonceOverride } = await deferralActions( this.client ).createDeferredActionTypedDataObject({ callData: installValidationCall, @@ -372,6 +377,12 @@ export class PermissionBuilder { uoIsGlobalValidation: uoIsGlobalValidation, nonceKeyOverride: this.nonceKeyOverride, }); + + return { + typedData, + nonceOverride, + hasAssociatedExecHooks: this.hasAssociatedExecHooks, + }; } // Use for direct `installValidation()` low-level calls (maybe useless) @@ -454,6 +465,7 @@ export class PermissionBuilder { spendLimit: BigInt(permission.data.allowance), }, }; + this.hasAssociatedExecHooks = true; break; case PermissionType.ERC20_TOKEN_TRANSFER: if (permission.data.address === zeroAddress) { @@ -485,6 +497,7 @@ export class PermissionBuilder { ], }, }; + this.hasAssociatedExecHooks = true; // Also allow `approve` and `transfer` for the erc20 rawHooks[HookIdentifier.PREVAL_ALLOWLIST] = { hookConfig: { From f3dea951eaed1fb08888c63782e6b373810d9f4e Mon Sep 17 00:00:00 2001 From: howy <132113803+howydev@users.noreply.github.com> Date: Fri, 4 Apr 2025 14:04:30 -0400 Subject: [PATCH 15/32] chore: add new errors (#1509) --- aa-sdk/core/src/errors/client.ts | 28 ++++++++++++ aa-sdk/core/src/index.ts | 2 + .../account/common/modularAccountV2Base.ts | 6 ++- .../InvalidDeferredActionMode/constructor.mdx | 18 ++++++++ .../constructor.mdx | 18 ++++++++ .../aa-sdk/core/functions/tracingHeader.mdx | 43 ------------------- 6 files changed, 70 insertions(+), 45 deletions(-) create mode 100644 site/pages/reference/aa-sdk/core/classes/InvalidDeferredActionMode/constructor.mdx create mode 100644 site/pages/reference/aa-sdk/core/classes/InvalidDeferredActionNonce/constructor.mdx delete mode 100644 site/pages/reference/aa-sdk/core/functions/tracingHeader.mdx diff --git a/aa-sdk/core/src/errors/client.ts b/aa-sdk/core/src/errors/client.ts index 4968eae236..5b83123cfe 100644 --- a/aa-sdk/core/src/errors/client.ts +++ b/aa-sdk/core/src/errors/client.ts @@ -117,3 +117,31 @@ export class InvalidModularAccountV2Mode extends BaseError { super(`The provided account mode is invalid for ModularAccount V2`); } } + +/** + * Error class denoting that the deferred action mode used is invalid. + */ +export class InvalidDeferredActionMode extends BaseError { + override name = "InvalidDeferredActionMode"; + + /** + * Initializes a new instance of the error message with a default message indicating that the provided ma v2 account mode is invalid. + */ + constructor() { + super(`The provided deferred action mode is invalid`); + } +} + +/** + * Error class denoting that the deferred action nonce used is invalid. + */ +export class InvalidDeferredActionNonce extends BaseError { + override name = "InvalidDeferredActionNonce"; + + /** + * Initializes a new instance of the error message with a default message indicating that the provided ma v2 account mode is invalid. + */ + constructor() { + super(`The provided deferred action nonce is invalid`); + } +} diff --git a/aa-sdk/core/src/index.ts b/aa-sdk/core/src/index.ts index 26dc751bc8..14f3c24234 100644 --- a/aa-sdk/core/src/index.ts +++ b/aa-sdk/core/src/index.ts @@ -77,6 +77,8 @@ export { InvalidNonceKeyError, EntityIdOverrideError, InvalidModularAccountV2Mode, + InvalidDeferredActionMode, + InvalidDeferredActionNonce, } from "./errors/client.js"; export { EntryPointNotFoundError, diff --git a/account-kit/smart-contracts/src/ma-v2/account/common/modularAccountV2Base.ts b/account-kit/smart-contracts/src/ma-v2/account/common/modularAccountV2Base.ts index 46fda0fdd8..7b08a78346 100644 --- a/account-kit/smart-contracts/src/ma-v2/account/common/modularAccountV2Base.ts +++ b/account-kit/smart-contracts/src/ma-v2/account/common/modularAccountV2Base.ts @@ -3,6 +3,8 @@ import { getEntryPoint, InvalidEntityIdError, InvalidNonceKeyError, + InvalidDeferredActionNonce, + InvalidDeferredActionMode, toSmartContractAccount, type AccountOp, type SmartAccountSigner, @@ -135,7 +137,7 @@ export async function createMAv2Base< if (deferredAction) { if (deferredAction.slice(3, 5) !== "00") { - throw new Error("Invalid deferred action version"); + throw new InvalidDeferredActionMode(); } nonce = BigInt(`0x${deferredAction.slice(6, 70)}`); // Set these values if the deferred action has not been consumed. We check this with the EP @@ -151,7 +153,7 @@ export async function createMAv2Base< deferredActionData = `0x${deferredAction.slice(70)}`; hasAssociatedExecHooks = deferredAction[6] === "1"; } else if (nonce > nextNonceForDeferredActionNonce) { - throw new Error("Deferred action nonce invalid"); + throw new InvalidDeferredActionNonce(); } } diff --git a/site/pages/reference/aa-sdk/core/classes/InvalidDeferredActionMode/constructor.mdx b/site/pages/reference/aa-sdk/core/classes/InvalidDeferredActionMode/constructor.mdx new file mode 100644 index 0000000000..591e20009e --- /dev/null +++ b/site/pages/reference/aa-sdk/core/classes/InvalidDeferredActionMode/constructor.mdx @@ -0,0 +1,18 @@ +--- +# This file is autogenerated +title: InvalidDeferredActionMode +description: Overview of the InvalidDeferredActionMode method +--- + +# InvalidDeferredActionMode + +Initializes a new instance of the error message with a default message indicating that the provided ma v2 account mode is invalid. +:::note +`InvalidDeferredActionMode` extends `BaseError`, see the docs for BaseError for all supported methods. +::: + +## Import + +```ts +import { InvalidDeferredActionMode } from "@aa-sdk/core"; +``` diff --git a/site/pages/reference/aa-sdk/core/classes/InvalidDeferredActionNonce/constructor.mdx b/site/pages/reference/aa-sdk/core/classes/InvalidDeferredActionNonce/constructor.mdx new file mode 100644 index 0000000000..f35dd89a8f --- /dev/null +++ b/site/pages/reference/aa-sdk/core/classes/InvalidDeferredActionNonce/constructor.mdx @@ -0,0 +1,18 @@ +--- +# This file is autogenerated +title: InvalidDeferredActionNonce +description: Overview of the InvalidDeferredActionNonce method +--- + +# InvalidDeferredActionNonce + +Initializes a new instance of the error message with a default message indicating that the provided ma v2 account mode is invalid. +:::note +`InvalidDeferredActionNonce` extends `BaseError`, see the docs for BaseError for all supported methods. +::: + +## Import + +```ts +import { InvalidDeferredActionNonce } from "@aa-sdk/core"; +``` diff --git a/site/pages/reference/aa-sdk/core/functions/tracingHeader.mdx b/site/pages/reference/aa-sdk/core/functions/tracingHeader.mdx deleted file mode 100644 index 5e0d1e34d3..0000000000 --- a/site/pages/reference/aa-sdk/core/functions/tracingHeader.mdx +++ /dev/null @@ -1,43 +0,0 @@ ---- -# This file is autogenerated - -title: tracingHeader -description: Overview of the tracingHeader method ---- - -# tracingHeader - -We wanted a transport that is adding the add headers for the headers that we are sending to the server for tracing cross actions -and services. - -## Import - -```ts -import { tracingHeader } from "@aa-sdk/core"; -``` - -## Usage - -```ts -import { createPublicClient, http } from "viem"; -import { tracingHeader } from "@aa-sdk/core"; - -const clientWithTracing = createPublicClient({ - transport: tracingHeader({ - breadcrumb: "myMethodOrAction", - transport: http(OTHER_RPC_URL), - }), -}); -``` - -## Parameters - -### params - -`TracingHeadersParams` -tracing headers config to wrap around a transport to add in headers for tracing - -## Returns - -`CustomTransport` -a viem Transport that splits traffic From 378c938807c963feb245ff04314dd08e7647e595 Mon Sep 17 00:00:00 2001 From: zer0dot Date: Fri, 4 Apr 2025 16:18:07 -0400 Subject: [PATCH 16/32] fix: (wip) small fix, adapt test --- .../account/common/modularAccountV2Base.ts | 2 +- .../src/ma-v2/actions/deferralActions.test.ts | 4 +- .../src/ma-v2/client/client.test.ts | 136 ++++++++++++++++++ .../src/ma-v2/permissionBuilder.ts | 11 +- .../smart-contracts/src/ma-v2/utils.ts | 9 ++ .../aa-sdk/core/functions/tracingHeader.mdx | 43 ++++++ 6 files changed, 195 insertions(+), 10 deletions(-) create mode 100644 site/pages/reference/aa-sdk/core/functions/tracingHeader.mdx diff --git a/account-kit/smart-contracts/src/ma-v2/account/common/modularAccountV2Base.ts b/account-kit/smart-contracts/src/ma-v2/account/common/modularAccountV2Base.ts index 7b08a78346..0c0483c40c 100644 --- a/account-kit/smart-contracts/src/ma-v2/account/common/modularAccountV2Base.ts +++ b/account-kit/smart-contracts/src/ma-v2/account/common/modularAccountV2Base.ts @@ -151,7 +151,7 @@ export async function createMAv2Base< if (nonce === nextNonceForDeferredActionNonce) { useDeferredAction = true; deferredActionData = `0x${deferredAction.slice(70)}`; - hasAssociatedExecHooks = deferredAction[6] === "1"; + hasAssociatedExecHooks = deferredAction[5] === "1"; } else if (nonce > nextNonceForDeferredActionNonce) { throw new InvalidDeferredActionNonce(); } diff --git a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts index 1e31b50dc0..51f87812a2 100644 --- a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts +++ b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts @@ -56,7 +56,6 @@ describe("MA v2 deferral actions tests", async () => { value: parseEther("2"), }); - const sessionKeyEntityId = 1; const sessionKey: SmartAccountSigner = new LocalAccountSigner( accounts.unfundedAccountOwner ); @@ -68,7 +67,6 @@ describe("MA v2 deferral actions tests", async () => { isGlobalValidation: true, }); - // TODO: remove nonce override here const { typedData, hasAssociatedExecHooks } = await new PermissionBuilder( provider ) @@ -93,7 +91,7 @@ describe("MA v2 deferral actions tests", async () => { const sig = await provider.account.signTypedData(typedData); - const deferredActionDigest = await provider.buildDeferredActionDigest({ + const deferredActionDigest = provider.buildDeferredActionDigest({ typedData, sig, }); diff --git a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts index deb969cf92..4e0c17d973 100644 --- a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts +++ b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts @@ -24,6 +24,10 @@ import { type TestActions, type Hex, toHex, + createWalletClient, + getContractAddress, + encodeFunctionData, + type ContractFunctionName, } from "viem"; import { HookType } from "../actions/common/types.js"; import { @@ -40,6 +44,7 @@ import { NativeTokenLimitModule, semiModularAccountBytecodeAbi, buildFullNonceKey, + modularAccountAbi, } from "@account-kit/smart-contracts/experimental"; import { createLightAccountClient, @@ -64,6 +69,7 @@ import { getMAV2UpgradeToData } from "@account-kit/smart-contracts"; import { deferralActions } from "../actions/deferralActions.js"; import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; import { PermissionBuilder, PermissionType } from "../permissionBuilder.js"; +import { mintableERC20Abi, mintableERC20Bytecode } from "../utils.js"; // Note: These tests maintain a shared state to not break the local-running rundler by desyncing the chain. describe("MA v2 Tests", async () => { @@ -426,6 +432,136 @@ describe("MA v2 Tests", async () => { await provider.waitForUserOperationTransaction({ hash: result }); }); + it.only("installs a session key via deferred action using PermissionBuilder with ERC20 permission signed by the owner and has it sign a UO", async () => { + let provider = ( + await givenConnectedProvider({ + signer, + }) + ) + .extend(installValidationActions) + .extend(deferralActions); + + await setBalance(client, { + address: provider.getAddress(), + value: parseEther("2"), + }); + + const walletClient = createWalletClient({ + chain: provider.chain, + transport: custom(instance.getClient()), + account: privateKeyToAccount(generatePrivateKey()), + }); + + await setBalance(client, { + address: walletClient.account.address, + value: parseEther("2"), + }); + + const mockErc20Address = getContractAddress({ + from: walletClient.account.address, + nonce: BigInt( + await client.getTransactionCount({ + address: walletClient.account.address, + }) + ), + }); + + // Deploy mock ERC20 + const ret = await walletClient.deployContract({ + abi: mintableERC20Abi, + account: walletClient.account, + bytecode: mintableERC20Bytecode, + }); + + // Mint some test tokens + await walletClient.writeContract({ + address: mockErc20Address, + abi: mintableERC20Abi, + functionName: "mint", + args: [provider.getAddress(), 1000n], + }); + + // Test variables + // const sessionKeyEntityId = 1; + // these can be default values or from call arguments + const { entityId, nonce } = await provider.getEntityIdAndNonce({ + entityId: 0, + nonceKey: 0n, + isGlobalValidation: false, + }); + + const { typedData, hasAssociatedExecHooks } = await new PermissionBuilder( + provider + ) + .configure({ + key: { + publicKey: await sessionKey.getAddress(), + type: "secp256k1", + }, + entityId: entityId, + nonceKeyOverride: 0n, + }) + .addPermission({ + permission: { + type: PermissionType.ERC20_TOKEN_TRANSFER, + data: { + address: mockErc20Address, + allowance: toHex(900), + }, + }, + }) + // .addPermission({ + // permission: { + // type: PermissionType.ROOT, + // }, + // }) + .compile_deferred({ + deadline: 0, + uoValidationEntityId: entityId, + uoIsGlobalValidation: false, + }); + + // Sign the typed data using the owner (fallback) validation, this must be done via the account to skip 6492 + const deferredValidationSig = await provider.account.signTypedData( + typedData + ); + // Build the full hex to prepend to the UO signature + // This MUST be done with the *same* client that has signed the typed data + const signaturePrepend = provider.buildDeferredActionDigest({ + typedData: typedData, + sig: deferredValidationSig, + }); + + // Initialize the session key client corresponding to the session key we will install in the deferred action + let sessionKeyClient = await createModularAccountV2Client({ + chain: instance.chain, + signer: sessionKey, + transport: custom(instance.getClient()), + accountAddress: provider.getAddress(), + initCode: provider.account.getInitCode(), + deferredAction: `0x00${hasAssociatedExecHooks ? "01" : "00"}${toHex( + nonce, + { + size: 32, + } + ).slice(2)}${signaturePrepend.slice(2)}`, + }); + + // Build the full UO with the deferred action signature prepend (must be session key client) + const hash = await sessionKeyClient.sendUserOperation({ + uo: { + target: mockErc20Address, + data: encodeFunctionData({ + abi: mintableERC20Abi, + functionName: "transfer", + args: [target, 900n], + }), + }, + }); + + await provider.waitForUserOperationTransaction({ hash: hash.hash }); + }); + it("installs a session key via deferred action signed by the owner and has it sign a UO", async () => { let provider = (await givenConnectedProvider({ signer })) .extend(installValidationActions) diff --git a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts index 0639733eec..4a913c67a0 100644 --- a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts +++ b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts @@ -341,9 +341,8 @@ export class PermissionBuilder { hasAssociatedExecHooks: boolean; nonceOverride: bigint; }> { - this.validateConfiguration(); - - // Maybe add checks, like zero address module addr + // Need to remove this because compile_raw may add selectors + // this.validateConfiguration(); // Add time range module hook via expiry if (deadline !== 0) { @@ -357,7 +356,7 @@ export class PermissionBuilder { this.hooks.push( TimeRangeModule.buildHook( { - entityId: 1, // will be timerange entityId + entityId: this.validationConfig.entityId, // will be timerange entityId validUntil: deadline, validAfter: 0, }, @@ -367,6 +366,7 @@ export class PermissionBuilder { } const installValidationCall = await this.compile_raw(); + console.log(installValidationCall); const { typedData, nonceOverride } = await deferralActions( this.client @@ -387,8 +387,6 @@ export class PermissionBuilder { // Use for direct `installValidation()` low-level calls (maybe useless) async compile_raw(): Promise { - this.validateConfiguration(); - // Translate all permissions into raw hooks if >0 if (this.permissions.length > 0) { const rawHooks = this.translatePermissions( @@ -397,6 +395,7 @@ export class PermissionBuilder { // Add the translated permissions as hooks this.addHooks(rawHooks); } + this.validateConfiguration(); return await installValidationActions(this.client).encodeInstallValidation({ validationConfig: this.validationConfig, diff --git a/account-kit/smart-contracts/src/ma-v2/utils.ts b/account-kit/smart-contracts/src/ma-v2/utils.ts index 5486d7f6b0..0a3ba9135a 100644 --- a/account-kit/smart-contracts/src/ma-v2/utils.ts +++ b/account-kit/smart-contracts/src/ma-v2/utils.ts @@ -7,6 +7,7 @@ import { type Chain, type Address, type Transport, + parseAbi, } from "viem"; import { arbitrum, @@ -232,6 +233,14 @@ export async function getMAV2UpgradeToData< export const entityIdAndNonceReaderBytecode = "0x608060405234801561001057600080fd5b506040516104f13803806104f183398101604081905261002f916101e5565b60006008826001600160c01b0316901c90506000808263ffffffff1611610057576001610059565b815b90506001600160a01b0385163b15610133575b60006001600160a01b03861663d31b575b6bffffffff0000000000000000604085901b166040516001600160e01b031960e084901b1681526001600160401b03199091166004820152602401600060405180830381865afa1580156100d5573d6000803e3d6000fd5b505050506040513d6000823e601f3d908101601f191682016040526100fd91908101906103c6565b805190915060ff161580156101155750606081015151155b156101205750610133565b8161012a816104a4565b9250505061006c565b604051631aab3f0d60e11b81526001600160a01b03868116600483015264ffffffff01600160c01b038516600884901b64ffffffff0016176024830152600091908616906335567e1a90604401602060405180830381865afa15801561019d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101c191906104d7565b90508060005260206000f35b6001600160a01b03811681146101e257600080fd5b50565b6000806000606084860312156101fa57600080fd5b8351610205816101cd565b6020850151909350610216816101cd565b60408501519092506001600160c01b038116811461023357600080fd5b809150509250925092565b634e487b7160e01b600052604160045260246000fd5b604051608081016001600160401b03811182821017156102765761027661023e565b60405290565b604051601f8201601f191681016001600160401b03811182821017156102a4576102a461023e565b604052919050565b60006001600160401b038211156102c5576102c561023e565b5060051b60200190565b600082601f8301126102e057600080fd5b81516102f36102ee826102ac565b61027c565b8082825260208201915060208360051b86010192508583111561031557600080fd5b602085015b8381101561034857805166ffffffffffffff198116811461033a57600080fd5b83526020928301920161031a565b5095945050505050565b600082601f83011261036357600080fd5b81516103716102ee826102ac565b8082825260208201915060208360051b86010192508583111561039357600080fd5b602085015b838110156103485780516001600160e01b0319811681146103b857600080fd5b835260209283019201610398565b6000602082840312156103d857600080fd5b81516001600160401b038111156103ee57600080fd5b82016080818503121561040057600080fd5b610408610254565b815160ff8116811461041957600080fd5b815260208201516001600160401b0381111561043457600080fd5b610440868285016102cf565b60208301525060408201516001600160401b0381111561045f57600080fd5b61046b868285016102cf565b60408301525060608201516001600160401b0381111561048a57600080fd5b61049686828501610352565b606083015250949350505050565b600063ffffffff821663ffffffff81036104ce57634e487b7160e01b600052601160045260246000fd5b60010192915050565b6000602082840312156104e957600080fd5b505191905056fe"; +export const mintableERC20Bytecode = + "0x608060405234801561000f575f80fd5b506040518060400160405280600d81526020016c26b4b73a30b13632aa37b5b2b760991b81525060405180604001604052806002815260200161135560f21b8152508160039081610060919061010d565b50600461006d828261010d565b5050506101c7565b634e487b7160e01b5f52604160045260245ffd5b600181811c9082168061009d57607f821691505b6020821081036100bb57634e487b7160e01b5f52602260045260245ffd5b50919050565b601f82111561010857805f5260205f20601f840160051c810160208510156100e65750805b601f840160051c820191505b81811015610105575f81556001016100f2565b50505b505050565b81516001600160401b0381111561012657610126610075565b61013a816101348454610089565b846100c1565b6020601f82116001811461016c575f83156101555750848201515b5f19600385901b1c1916600184901b178455610105565b5f84815260208120601f198516915b8281101561019b578785015182556020948501946001909201910161017b565b50848210156101b857868401515f19600387901b60f8161c191681555b50505050600190811b01905550565b610737806101d45f395ff3fe608060405234801561000f575f80fd5b506004361061008c575f3560e01c806306fdde0314610090578063095ea7b3146100ae57806318160ddd146100d157806323b872dd146100e3578063313ce567146100f657806340c10f191461010557806370a082311461011a57806395d89b4114610142578063a9059cbb1461014a578063dd62ed3e1461015d575b5f80fd5b610098610170565b6040516100a59190610572565b60405180910390f35b6100c16100bc3660046105c2565b610200565b60405190151581526020016100a5565b6002545b6040519081526020016100a5565b6100c16100f13660046105ea565b610219565b604051601281526020016100a5565b6101186101133660046105c2565b61023c565b005b6100d5610128366004610624565b6001600160a01b03165f9081526020819052604090205490565b61009861024a565b6100c16101583660046105c2565b610259565b6100d561016b366004610644565b610266565b60606003805461017f90610675565b80601f01602080910402602001604051908101604052809291908181526020018280546101ab90610675565b80156101f65780601f106101cd576101008083540402835291602001916101f6565b820191905f5260205f20905b8154815290600101906020018083116101d957829003601f168201915b5050505050905090565b5f3361020d818585610290565b60019150505b92915050565b5f336102268582856102a2565b6102318585856102fc565b506001949350505050565b6102468282610359565b5050565b60606004805461017f90610675565b5f3361020d8185856102fc565b6001600160a01b039182165f90815260016020908152604080832093909416825291909152205490565b61029d838383600161038d565b505050565b5f6102ad8484610266565b90505f198110156102f657818110156102e857828183604051637dc7a0d960e11b81526004016102df939291906106ad565b60405180910390fd5b6102f684848484035f61038d565b50505050565b6001600160a01b038316610325575f604051634b637e8f60e11b81526004016102df91906106ce565b6001600160a01b03821661034e575f60405163ec442f0560e01b81526004016102df91906106ce565b61029d83838361045f565b6001600160a01b038216610382575f60405163ec442f0560e01b81526004016102df91906106ce565b6102465f838361045f565b6001600160a01b0384166103b6575f60405163e602df0560e01b81526004016102df91906106ce565b6001600160a01b0383166103df575f604051634a1406b160e11b81526004016102df91906106ce565b6001600160a01b038085165f90815260016020908152604080832093871683529290522082905580156102f657826001600160a01b0316846001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258460405161045191815260200190565b60405180910390a350505050565b6001600160a01b038316610489578060025f82825461047e91906106e2565b909155506104e69050565b6001600160a01b0383165f90815260208190526040902054818110156104c85783818360405163391434e360e21b81526004016102df939291906106ad565b6001600160a01b0384165f9081526020819052604090209082900390555b6001600160a01b03821661050257600280548290039055610520565b6001600160a01b0382165f9081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8360405161056591815260200190565b60405180910390a3505050565b602081525f82518060208401528060208501604085015e5f604082850101526040601f19601f83011684010191505092915050565b80356001600160a01b03811681146105bd575f80fd5b919050565b5f80604083850312156105d3575f80fd5b6105dc836105a7565b946020939093013593505050565b5f805f606084860312156105fc575f80fd5b610605846105a7565b9250610613602085016105a7565b929592945050506040919091013590565b5f60208284031215610634575f80fd5b61063d826105a7565b9392505050565b5f8060408385031215610655575f80fd5b61065e836105a7565b915061066c602084016105a7565b90509250929050565b600181811c9082168061068957607f821691505b6020821081036106a757634e487b7160e01b5f52602260045260245ffd5b50919050565b6001600160a01b039390931683526020830191909152604082015260600190565b6001600160a01b0391909116815260200190565b8082018082111561021357634e487b7160e01b5f52601160045260245ffdfea2646970667358221220f9ae46a2e15270bfb77fe3d4d0ee0e45b749e3dde93805ee2cf795cb800244e664736f6c634300081a0033"; + +export const mintableERC20Abi = parseAbi([ + "function transfer(address to, uint256 amount) external", + "function mint(address to, uint256 amount) external", +]); + export type BuildNonceParams = { nonceKey?: bigint; entityId?: number; diff --git a/site/pages/reference/aa-sdk/core/functions/tracingHeader.mdx b/site/pages/reference/aa-sdk/core/functions/tracingHeader.mdx new file mode 100644 index 0000000000..5e0d1e34d3 --- /dev/null +++ b/site/pages/reference/aa-sdk/core/functions/tracingHeader.mdx @@ -0,0 +1,43 @@ +--- +# This file is autogenerated + +title: tracingHeader +description: Overview of the tracingHeader method +--- + +# tracingHeader + +We wanted a transport that is adding the add headers for the headers that we are sending to the server for tracing cross actions +and services. + +## Import + +```ts +import { tracingHeader } from "@aa-sdk/core"; +``` + +## Usage + +```ts +import { createPublicClient, http } from "viem"; +import { tracingHeader } from "@aa-sdk/core"; + +const clientWithTracing = createPublicClient({ + transport: tracingHeader({ + breadcrumb: "myMethodOrAction", + transport: http(OTHER_RPC_URL), + }), +}); +``` + +## Parameters + +### params + +`TracingHeadersParams` +tracing headers config to wrap around a transport to add in headers for tracing + +## Returns + +`CustomTransport` +a viem Transport that splits traffic From e9ee6800e652312acda7212b34cedb77c085eaef Mon Sep 17 00:00:00 2001 From: zer0dot Date: Fri, 4 Apr 2025 17:16:41 -0400 Subject: [PATCH 17/32] fix: (wip) minor fix and update --- .../src/ma-v2/account/common/modularAccountV2Base.ts | 2 +- .../smart-contracts/src/ma-v2/client/client.test.ts | 8 +++----- .../smart-contracts/src/ma-v2/permissionBuilder.ts | 1 - account-kit/smart-contracts/src/ma-v2/utils.ts | 1 + 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/account-kit/smart-contracts/src/ma-v2/account/common/modularAccountV2Base.ts b/account-kit/smart-contracts/src/ma-v2/account/common/modularAccountV2Base.ts index 0c0483c40c..00dd720a39 100644 --- a/account-kit/smart-contracts/src/ma-v2/account/common/modularAccountV2Base.ts +++ b/account-kit/smart-contracts/src/ma-v2/account/common/modularAccountV2Base.ts @@ -136,7 +136,7 @@ export async function createMAv2Base< let hasAssociatedExecHooks: boolean = false; if (deferredAction) { - if (deferredAction.slice(3, 5) !== "00") { + if (deferredAction.slice(2, 4) !== "00") { throw new InvalidDeferredActionMode(); } nonce = BigInt(`0x${deferredAction.slice(6, 70)}`); diff --git a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts index 4e0c17d973..42ca6d9901 100644 --- a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts +++ b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts @@ -510,11 +510,6 @@ describe("MA v2 Tests", async () => { }, }, }) - // .addPermission({ - // permission: { - // type: PermissionType.ROOT, - // }, - // }) .compile_deferred({ deadline: 0, uoValidationEntityId: entityId, @@ -560,6 +555,9 @@ describe("MA v2 Tests", async () => { }); await provider.waitForUserOperationTransaction({ hash: hash.hash }); + + // TODO: Validate the transfer + // await expect() }); it("installs a session key via deferred action signed by the owner and has it sign a UO", async () => { diff --git a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts index 4a913c67a0..80396df626 100644 --- a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts +++ b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts @@ -366,7 +366,6 @@ export class PermissionBuilder { } const installValidationCall = await this.compile_raw(); - console.log(installValidationCall); const { typedData, nonceOverride } = await deferralActions( this.client diff --git a/account-kit/smart-contracts/src/ma-v2/utils.ts b/account-kit/smart-contracts/src/ma-v2/utils.ts index 0a3ba9135a..8db63c71d3 100644 --- a/account-kit/smart-contracts/src/ma-v2/utils.ts +++ b/account-kit/smart-contracts/src/ma-v2/utils.ts @@ -239,6 +239,7 @@ export const mintableERC20Bytecode = export const mintableERC20Abi = parseAbi([ "function transfer(address to, uint256 amount) external", "function mint(address to, uint256 amount) external", + "function balanceOf(address target) external returns (uint256)", ]); export type BuildNonceParams = { From 7b27d2a999198ca53b90fa376fdc90a751ec6349 Mon Sep 17 00:00:00 2001 From: zer0dot Date: Fri, 4 Apr 2025 22:10:26 -0400 Subject: [PATCH 18/32] feat: adapt permission builder to new structure --- .../src/ma-v2/actions/deferralActions.test.ts | 50 ++- .../src/ma-v2/actions/deferralActions.ts | 71 ++-- .../src/ma-v2/client/client.test.ts | 350 +++++++++--------- .../src/ma-v2/permissionBuilder.ts | 35 +- 4 files changed, 248 insertions(+), 258 deletions(-) diff --git a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts index 51f87812a2..c21a56818e 100644 --- a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts +++ b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts @@ -67,32 +67,31 @@ describe("MA v2 deferral actions tests", async () => { isGlobalValidation: true, }); - const { typedData, hasAssociatedExecHooks } = await new PermissionBuilder( - provider - ) - .configure({ - key: { - publicKey: await sessionKey.getAddress(), - type: "secp256k1", - }, - entityId, - nonceKeyOverride: 0n, // TODO: add nonce override here - }) - .addPermission({ - permission: { - type: PermissionType.ROOT, - }, - }) - .compile_deferred({ - deadline: 0, - uoValidationEntityId: entityId, - uoIsGlobalValidation: true, - }); + const { typedData, fullPreSignatureDeferredActionDigest } = + await new PermissionBuilder(provider) + .configure({ + key: { + publicKey: await sessionKey.getAddress(), + type: "secp256k1", + }, + entityId, + nonce: nonce, + }) + .addPermission({ + permission: { + type: PermissionType.ROOT, + }, + }) + .compile_deferred({ + deadline: 0, + uoValidationEntityId: entityId, + uoIsGlobalValidation: true, + }); const sig = await provider.account.signTypedData(typedData); const deferredActionDigest = provider.buildDeferredActionDigest({ - typedData, + fullPreSignatureDeferredActionDigest, sig, }); @@ -103,12 +102,7 @@ describe("MA v2 deferral actions tests", async () => { accountAddress: provider.getAddress(), signer: sessionKey, initCode: provider.account.getInitCode(), - deferredAction: `0x00${hasAssociatedExecHooks ? "01" : "00"}${toHex( - nonce, - { - size: 32, - } - ).slice(2)}${deferredActionDigest.slice(2)}`, + deferredAction: deferredActionDigest, }); const uoResult = await sessionKeyClient.sendUserOperation({ diff --git a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.ts b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.ts index 274e1ff419..9b3c9d9a54 100644 --- a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.ts +++ b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.ts @@ -11,7 +11,6 @@ import { type Hex, concatHex, maxUint152, - getContract, encodePacked, size, toHex, @@ -44,22 +43,23 @@ export type DeferredActionTypedData = { export type DeferredActionReturnData = { typedData: DeferredActionTypedData; - nonceOverride: bigint; }; export type CreateDeferredActionTypedDataParams = { callData: Hex; deadline: number; - uoValidationEntityId: number; - uoIsGlobalValidation: boolean; - nonceKeyOverride?: bigint; + nonce: bigint; }; export type BuildDeferredActionDigestParams = { - typedData: DeferredActionTypedData; + fullPreSignatureDeferredActionDigest: Hex; sig: Hex; }; +export type BuildPreSignatureDeferredActionDigestParams = { + typedData: DeferredActionTypedData; +}; + export type BuildUserOperationWithDeferredActionParams = { uo: UserOperationCallData | BatchUserOperationCallData; signaturePrepend: Hex; @@ -77,6 +77,9 @@ export type DeferralActions = { args: CreateDeferredActionTypedDataParams ) => Promise; buildDeferredActionDigest: (args: BuildDeferredActionDigestParams) => Hex; + buildPreSignatureDeferredActionDigest: ( + args: BuildPreSignatureDeferredActionDigestParams + ) => Hex; buildUserOperationWithDeferredAction: ( args: BuildUserOperationWithDeferredActionParams ) => Promise; @@ -97,43 +100,17 @@ export const deferralActions: ( const createDeferredActionTypedDataObject = async ({ callData, deadline, - uoValidationEntityId, - uoIsGlobalValidation, - nonceKeyOverride = 0n, + nonce, }: CreateDeferredActionTypedDataParams): Promise => { if (!client.account) { throw new AccountNotFoundError(); } - if (nonceKeyOverride > maxUint152) { - throw new InvalidNonceKeyError(nonceKeyOverride); - } - const entryPoint = client.account.getEntryPoint(); if (entryPoint === undefined) { throw new EntryPointNotFoundError(client.chain, "0.7.0"); } - const entryPointContract = getContract({ - address: entryPoint.address, - abi: entryPoint.abi, - client: client, - }); - - // 2 = deferred action flags 0b10 - // 1 = isGlobal validation flag 0b01 - const fullNonceKey: bigint = buildFullNonceKey({ - nonceKey: nonceKeyOverride, - entityId: uoValidationEntityId, - isGlobalValidation: uoIsGlobalValidation, - isDeferredAction: true, - }); - - const nonceOverride = (await entryPointContract.read.getNonce([ - client.account.address, - fullNonceKey, - ])) as bigint; - return { typedData: { domain: { @@ -149,12 +126,11 @@ export const deferralActions: ( }, primaryType: "DeferredAction", message: { - nonce: nonceOverride, + nonce: nonce, deadline: deadline, call: callData, }, }, - nonceOverride: nonceOverride, }; }; @@ -164,14 +140,27 @@ export const deferralActions: ( * Assumption: The client this extends is used to sign the typed data. * * @param {object} args The argument object containing the following: - * @param {DeferredActionTypedData} args.typedData The typed data object for the deferred action + * @param {Hex} args.fullPreSignatureDeferredActionDigest The The data to append the signature and length to * @param {Hex} args.sig The signature to include in the digest * @returns {Hex} The encoded digest to be prepended to the userOp signature */ const buildDeferredActionDigest = ({ - typedData, + fullPreSignatureDeferredActionDigest, sig, }: BuildDeferredActionDigestParams): Hex => { + const sigLength = size(sig); + + const encodedData = concatHex([ + fullPreSignatureDeferredActionDigest, + toHex(sigLength, { size: 4 }), + sig, + ]); + return encodedData; + }; + + const buildPreSignatureDeferredActionDigest = ({ + typedData, + }: BuildPreSignatureDeferredActionDigestParams): Hex => { const signerEntity = client.account.signerEntity; const validationLocator = (BigInt(signerEntity.entityId) << 8n) | @@ -182,15 +171,12 @@ export const deferralActions: ( [validationLocator, typedData.message.deadline, typedData.message.call] ); - // TODO: encoded call data as input - const encodedDataLength = size(encodedCallData); - const sigLength = size(sig); + console.log(validationLocator); + const encodedDataLength = size(encodedCallData); const encodedData = concatHex([ toHex(encodedDataLength, { size: 4 }), encodedCallData, - toHex(sigLength, { size: 4 }), - sig, ]); return encodedData; }; @@ -285,6 +271,7 @@ export const deferralActions: ( return { createDeferredActionTypedDataObject, buildDeferredActionDigest, + buildPreSignatureDeferredActionDigest, buildUserOperationWithDeferredAction, getEntityIdAndNonce, }; diff --git a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts index 42ca6d9901..3cf0c97f3a 100644 --- a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts +++ b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts @@ -350,39 +350,45 @@ describe("MA v2 Tests", async () => { value: parseEther("2"), }); - // Test variables - const sessionKeyEntityId = 1; + const { entityId, nonce } = await provider.getEntityIdAndNonce({ + entityId: 0, + nonceKey: 0n, + isGlobalValidation: false, + }); - const { typedData, nonceOverride } = await new PermissionBuilder(provider) - .configure({ - key: { - publicKey: await sessionKey.getAddress(), - type: "secp256k1", - }, - entityId: sessionKeyEntityId, - nonceKeyOverride: 0n, - }) - .addPermission({ - permission: { - type: PermissionType.GAS_LIMIT, - data: { - limit: toHex(parseEther("1")), + // Must be built with the client that's going to sign the deferred action + // OR a client with the same set signer entity as the signing client (entityId + isGlobal) + const { typedData, fullPreSignatureDeferredActionDigest } = + await new PermissionBuilder(provider) + .configure({ + key: { + publicKey: await sessionKey.getAddress(), + type: "secp256k1", }, - }, - }) - .addPermission({ - permission: { - type: PermissionType.ACCOUNT_FUNCTIONS, - data: { - functions: ["0xb61d27f6"], // execute selector + entityId: entityId, + nonce: nonce, + }) + .addPermission({ + permission: { + type: PermissionType.GAS_LIMIT, + data: { + limit: toHex(parseEther("1")), + }, }, - }, - }) - .compile_deferred({ - deadline: 0, - uoValidationEntityId: sessionKeyEntityId, - uoIsGlobalValidation: false, - }); + }) + .addPermission({ + permission: { + type: PermissionType.ACCOUNT_FUNCTIONS, + data: { + functions: ["0xb61d27f6"], // execute selector + }, + }, + }) + .compile_deferred({ + deadline: 0, + uoValidationEntityId: entityId, + uoIsGlobalValidation: false, + }); // Sign the typed data using the owner (fallback) validation, this must be done via the account to skip 6492 const deferredValidationSig = await provider.account.signTypedData( @@ -390,49 +396,34 @@ describe("MA v2 Tests", async () => { ); // Build the full hex to prepend to the UO signature - // This MUST be done with the *same* client that has signed the typed data - const signaturePrepend = provider.buildDeferredActionDigest({ - typedData: typedData, + const deferredActionDigest = provider.buildDeferredActionDigest({ + fullPreSignatureDeferredActionDigest, sig: deferredValidationSig, }); - // Build the full UO with the deferred action signature prepend (provider/client only used for account address & entrypoint) - const unsignedUo = await provider.buildUserOperationWithDeferredAction({ - uo: { target, data: "0x" }, - signaturePrepend, - nonceOverride, - }); - // Initialize the session key client corresponding to the session key we will install in the deferred action let sessionKeyClient = await createModularAccountV2Client({ - chain: instance.chain, - signer: sessionKey, transport: custom(instance.getClient()), + chain: instance.chain, accountAddress: provider.getAddress(), - signerEntity: { - entityId: sessionKeyEntityId, - isGlobalValidation: false, - }, + signer: sessionKey, + initCode: provider.account.getInitCode(), // must be called with the owner provider! + deferredAction: deferredActionDigest, }); - // Sign the UO with the session key - const uo = (await sessionKeyClient.signUserOperation({ - uoStruct: unsignedUo, - })) as UserOperationRequest_v7; - - // Prepend the full hex for the deferred action to the new, real signature - uo.signature = concatHex([signaturePrepend, uo.signature as Hex]); - // Send the raw UserOp - const result = await sessionKeyClient.sendRawUserOperation( - uo, - provider.account.getEntryPoint().address - ); + const result = await sessionKeyClient.sendUserOperation({ + uo: { + target: target, + value: sendAmount, + data: "0x", + }, + }); - await provider.waitForUserOperationTransaction({ hash: result }); + await provider.waitForUserOperationTransaction(result); }); - it.only("installs a session key via deferred action using PermissionBuilder with ERC20 permission signed by the owner and has it sign a UO", async () => { + it("installs a session key via deferred action using PermissionBuilder with ERC20 permission signed by the owner and has it sign a UO", async () => { let provider = ( await givenConnectedProvider({ signer, @@ -490,40 +481,41 @@ describe("MA v2 Tests", async () => { isGlobalValidation: false, }); - const { typedData, hasAssociatedExecHooks } = await new PermissionBuilder( - provider - ) - .configure({ - key: { - publicKey: await sessionKey.getAddress(), - type: "secp256k1", - }, - entityId: entityId, - nonceKeyOverride: 0n, - }) - .addPermission({ - permission: { - type: PermissionType.ERC20_TOKEN_TRANSFER, - data: { - address: mockErc20Address, - allowance: toHex(900), + // Must be built with the client that's going to sign the deferred action + // OR a client with the same set signer entity as the signing client (entityId + isGlobal) + const { typedData, fullPreSignatureDeferredActionDigest } = + await new PermissionBuilder(provider) + .configure({ + key: { + publicKey: await sessionKey.getAddress(), + type: "secp256k1", }, - }, - }) - .compile_deferred({ - deadline: 0, - uoValidationEntityId: entityId, - uoIsGlobalValidation: false, - }); + entityId: entityId, + nonce: nonce, + }) + .addPermission({ + permission: { + type: PermissionType.ERC20_TOKEN_TRANSFER, + data: { + address: mockErc20Address, + allowance: toHex(900), + }, + }, + }) + .compile_deferred({ + deadline: 0, + uoValidationEntityId: entityId, + uoIsGlobalValidation: false, + }); // Sign the typed data using the owner (fallback) validation, this must be done via the account to skip 6492 const deferredValidationSig = await provider.account.signTypedData( typedData ); + // Build the full hex to prepend to the UO signature - // This MUST be done with the *same* client that has signed the typed data - const signaturePrepend = provider.buildDeferredActionDigest({ - typedData: typedData, + const deferredActionDigest = provider.buildDeferredActionDigest({ + fullPreSignatureDeferredActionDigest, sig: deferredValidationSig, }); @@ -534,14 +526,19 @@ describe("MA v2 Tests", async () => { transport: custom(instance.getClient()), accountAddress: provider.getAddress(), initCode: provider.account.getInitCode(), - deferredAction: `0x00${hasAssociatedExecHooks ? "01" : "00"}${toHex( - nonce, - { - size: 32, - } - ).slice(2)}${signaturePrepend.slice(2)}`, + deferredAction: deferredActionDigest, }); + // Validate the target's balance is zero before the transfer + expect( + await client.readContract({ + address: mockErc20Address, + abi: mintableERC20Abi, + functionName: "balanceOf", + args: [target], + }) + ).to.eq(0n); + // Build the full UO with the deferred action signature prepend (must be session key client) const hash = await sessionKeyClient.sendUserOperation({ uo: { @@ -556,11 +553,18 @@ describe("MA v2 Tests", async () => { await provider.waitForUserOperationTransaction({ hash: hash.hash }); - // TODO: Validate the transfer - // await expect() + // Validate the erc20 transfer happened + expect( + await client.readContract({ + address: mockErc20Address, + abi: mintableERC20Abi, + functionName: "balanceOf", + args: [target], + }) + ).to.eq(900n); }); - it("installs a session key via deferred action signed by the owner and has it sign a UO", async () => { + it("Low-level installs a session key via deferred action signed by the owner and has it sign a UO", async () => { let provider = (await givenConnectedProvider({ signer })) .extend(installValidationActions) .extend(deferralActions); @@ -571,8 +575,12 @@ describe("MA v2 Tests", async () => { }); // Test variables - const sessionKeyEntityId = 1; const isGlobalValidation = true; + const { entityId, nonce } = await provider.getEntityIdAndNonce({ + entityId: 0, + nonceKey: 0n, + isGlobalValidation: isGlobalValidation, + }); // Encode install data to defer let encodedInstallData = await provider.encodeInstallValidation({ @@ -580,56 +588,64 @@ describe("MA v2 Tests", async () => { moduleAddress: getDefaultSingleSignerValidationModuleAddress( provider.chain ), - entityId: sessionKeyEntityId, + entityId: entityId, isGlobal: isGlobalValidation, isSignatureValidation: true, isUserOpValidation: true, }, selectors: [], installData: SingleSignerValidationModule.encodeOnInstallData({ - entityId: sessionKeyEntityId, + entityId: entityId, signer: await sessionKey.getAddress(), }), hooks: [], }); // Build the typed data we need for the deferred action (provider/client only used for account address & entrypoint) - const { typedData, nonceOverride } = - await provider.createDeferredActionTypedDataObject({ - callData: encodedInstallData, - deadline: 0, - uoValidationEntityId: sessionKeyEntityId, // must be the UO validating entity ID - uoIsGlobalValidation: isGlobalValidation, - }); + const { typedData } = await provider.createDeferredActionTypedDataObject({ + callData: encodedInstallData, + deadline: 0, + nonce, + }); // Sign the typed data using the owner (fallback) validation, this must be done via the account to skip 6492 const deferredValidationSig = await provider.account.signTypedData( typedData ); + const fullPreSignatureDeferredActionDigest = + provider.buildPreSignatureDeferredActionDigest({ + typedData, + }); + // Build the full hex to prepend to the UO signature - // This MUST be done with the *same* client that has signed the typed data - const signaturePrepend = provider.buildDeferredActionDigest({ - typedData: typedData, + const deferredActionDigest = provider.buildDeferredActionDigest({ + fullPreSignatureDeferredActionDigest, sig: deferredValidationSig, }); - // Build the full UO with the deferred action signature prepend (provider/client only used for account address & entrypoint) - const unsignedUo = await provider.buildUserOperationWithDeferredAction({ - uo: { target, data: "0x" }, - signaturePrepend, - nonceOverride, - }); + // version 00, preExecHooks 00, nonce, deferredActionDigest + const fullDeferredAction = concatHex([ + "0x0000", + toHex(nonce, { size: 32 }), + deferredActionDigest, + ]); // Initialize the session key client corresponding to the session key we will install in the deferred action let sessionKeyClient = await createModularAccountV2Client({ - chain: instance.chain, - signer: sessionKey, transport: custom(instance.getClient()), + chain: instance.chain, accountAddress: provider.getAddress(), - signerEntity: { - entityId: sessionKeyEntityId, - isGlobalValidation: isGlobalValidation, + signer: sessionKey, + initCode: provider.account.getInitCode(), + deferredAction: fullDeferredAction, + }); + + // Build the full UO with the deferred action signature prepend (provider/client only used for account address & entrypoint) + const unsignedUo = await sessionKeyClient.buildUserOperation({ + uo: { target, data: "0x" }, + overrides: { + nonce: nonce, }, }); @@ -638,9 +654,6 @@ describe("MA v2 Tests", async () => { uoStruct: unsignedUo, }); - // Prepend the full hex for the deferred action to the new, real signature - uo.signature = concatHex([signaturePrepend, uo.signature as Hex]); - // Send the raw UserOp const result = await sessionKeyClient.sendRawUserOperation( uo, @@ -705,29 +718,36 @@ describe("MA v2 Tests", async () => { ); // Test variables - const newSessionKeyEntityId = 2; const isGlobalValidation = true; - // Encode install data to defer and get the deferred action typed data - const { typedData, nonceOverride } = await new PermissionBuilder(provider) - .configure({ - key: { - publicKey: await newSessionKey.getAddress(), - type: "secp256k1", - }, - entityId: newSessionKeyEntityId, - nonceKeyOverride: 0n, //BigInt(newSessionKeyEntityId) + (3n << 64n) - }) - .addPermission({ - permission: { - type: PermissionType.ROOT, - }, - }) - .compile_deferred({ - deadline: 0, - uoValidationEntityId: newSessionKeyEntityId, // UO signing session key - uoIsGlobalValidation: isGlobalValidation, // UO validation is global - }); + const { entityId, nonce } = await provider.getEntityIdAndNonce({ + entityId: 0, + nonceKey: 0n, + isGlobalValidation: true, + }); + + // Must be built with the client that's going to sign the deferred action + // OR a client with the same set signer entity as the signing client (entityId + isGlobal) + const { typedData, fullPreSignatureDeferredActionDigest } = + await new PermissionBuilder(sessionKeyClient) + .configure({ + key: { + publicKey: await newSessionKey.getAddress(), + type: "secp256k1", + }, + entityId: entityId, + nonce: nonce, + }) + .addPermission({ + permission: { + type: PermissionType.ROOT, + }, + }) + .compile_deferred({ + deadline: 0, + uoValidationEntityId: entityId, + uoIsGlobalValidation: isGlobalValidation, + }); // Sign the typed data using the first session key const deferredValidationSig = await sessionKeyClient.account.signTypedData( @@ -735,46 +755,30 @@ describe("MA v2 Tests", async () => { ); // Build the full hex to prepend to the UO signature - // This MUST be done with the *same* client that has signed the typed data - const signaturePrepend = sessionKeyClient.buildDeferredActionDigest({ - typedData: typedData, + const deferredActionDigest = sessionKeyClient.buildDeferredActionDigest({ + fullPreSignatureDeferredActionDigest, sig: deferredValidationSig, }); - // Build the full UO with the deferred action signature prepend (provider/client only used for account address & entrypoint) - const unsignedUo = await provider.buildUserOperationWithDeferredAction({ - uo: { target, data: "0x" }, - signaturePrepend, - nonceOverride, - }); - // Initialize the session key client corresponding to the session key we will install in the deferred action let newSessionKeyClient = await createModularAccountV2Client({ - chain: instance.chain, - signer: newSessionKey, transport: custom(instance.getClient()), + chain: instance.chain, accountAddress: provider.getAddress(), - signerEntity: { - entityId: newSessionKeyEntityId, - isGlobalValidation: isGlobalValidation, - }, + signer: newSessionKey, + initCode: provider.account.getInitCode(), + deferredAction: deferredActionDigest, }); - // Sign the UO with the newly installed session key - const uo = await newSessionKeyClient.signUserOperation({ - uoStruct: unsignedUo, + // Send the UserOp (provider/client only used for account address & entrypoint) + const result = await newSessionKeyClient.sendUserOperation({ + uo: { + target, + data: "0x", + }, }); - // Prepend the full hex for the deferred action to the new, real signature - uo.signature = concatHex([signaturePrepend, uo.signature as Hex]); - - // Send the raw UserOp (provider/client only used for account address & entrypoint) - const result = await provider.sendRawUserOperation( - uo, - provider.account.getEntryPoint().address - ); - - await provider.waitForUserOperationTransaction({ hash: result }); + await provider.waitForUserOperationTransaction(result); }); it("uninstalls a session key", async () => { diff --git a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts index 80396df626..22b7b5a426 100644 --- a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts +++ b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts @@ -1,4 +1,4 @@ -import { zeroAddress, type Address, type Hex } from "viem"; +import { toHex, zeroAddress, type Address, type Hex } from "viem"; import { HookType, type HookConfig, @@ -207,7 +207,7 @@ export class PermissionBuilder { private installData: Hex = "0x"; private permissions: Permission[] = []; private hooks: Hook[] = []; - private nonceKeyOverride: bigint = 0n; + private nonce: bigint = 0n; private hasAssociatedExecHooks: boolean = false; constructor(client: ModularAccountV2Client) { @@ -218,13 +218,13 @@ export class PermissionBuilder { configure({ key, entityId, - nonceKeyOverride, + nonce, selectors, hooks, }: { key: Key; entityId: number; - nonceKeyOverride: bigint; + nonce: bigint; selectors?: Hex[]; hooks?: Hook[]; }): this { @@ -243,7 +243,7 @@ export class PermissionBuilder { }); if (selectors) this.selectors = selectors; if (hooks) this.hooks = hooks; - this.nonceKeyOverride = nonceKeyOverride; + this.nonce = nonce; return this; } @@ -330,16 +330,13 @@ export class PermissionBuilder { // Use for building deferred action typed data to sign async compile_deferred({ deadline, - uoValidationEntityId, - uoIsGlobalValidation, }: { deadline: number; uoValidationEntityId: number; uoIsGlobalValidation: boolean; }): Promise<{ typedData: DeferredActionTypedData; - hasAssociatedExecHooks: boolean; - nonceOverride: bigint; + fullPreSignatureDeferredActionDigest: Hex; }> { // Need to remove this because compile_raw may add selectors // this.validateConfiguration(); @@ -367,20 +364,28 @@ export class PermissionBuilder { const installValidationCall = await this.compile_raw(); - const { typedData, nonceOverride } = await deferralActions( + const { typedData } = await deferralActions( this.client ).createDeferredActionTypedDataObject({ callData: installValidationCall, deadline: deadline, - uoValidationEntityId: uoValidationEntityId, - uoIsGlobalValidation: uoIsGlobalValidation, - nonceKeyOverride: this.nonceKeyOverride, + nonce: this.nonce, }); + const preSignatureDigest = deferralActions( + this.client + ).buildPreSignatureDeferredActionDigest({ typedData }); + + // Encode additional information to build the full pre-signature digest + const fullPreSignatureDeferredActionDigest: `0x${string}` = `0x00${ + this.hasAssociatedExecHooks ? "01" : "00" + }${toHex(this.nonce, { + size: 32, + }).slice(2)}${preSignatureDigest.slice(2)}`; + return { typedData, - nonceOverride, - hasAssociatedExecHooks: this.hasAssociatedExecHooks, + fullPreSignatureDeferredActionDigest, }; } From 0a2e072758a804aa9ab39e0a5f214ad80b64aec6 Mon Sep 17 00:00:00 2001 From: howydev <132113803+howydev@users.noreply.github.com> Date: Mon, 7 Apr 2025 14:52:16 -0400 Subject: [PATCH 19/32] chore: update --- .../account/common/modularAccountV2Base.ts | 10 +++++----- .../src/ma-v2/actions/deferralActions.test.ts | 10 +++------- .../src/ma-v2/actions/deferralActions.ts | 14 +++++++------- .../src/ma-v2/client/client.test.ts | 18 +++++------------- 4 files changed, 20 insertions(+), 32 deletions(-) diff --git a/account-kit/smart-contracts/src/ma-v2/account/common/modularAccountV2Base.ts b/account-kit/smart-contracts/src/ma-v2/account/common/modularAccountV2Base.ts index 00dd720a39..b621410982 100644 --- a/account-kit/smart-contracts/src/ma-v2/account/common/modularAccountV2Base.ts +++ b/account-kit/smart-contracts/src/ma-v2/account/common/modularAccountV2Base.ts @@ -139,20 +139,20 @@ export async function createMAv2Base< if (deferredAction.slice(2, 4) !== "00") { throw new InvalidDeferredActionMode(); } - nonce = BigInt(`0x${deferredAction.slice(6, 70)}`); // Set these values if the deferred action has not been consumed. We check this with the EP - const nextNonceForDeferredActionNonce: bigint = + const nextNonceForDeferredAction: bigint = (await entryPointContract.read.getNonce([ accountAddress, nonce >> 64n, ])) as bigint; // we only add the deferred action in if the nonce has not been consumed - if (nonce === nextNonceForDeferredActionNonce) { + if (nonce === nextNonceForDeferredAction) { + nonce = BigInt(`0x${deferredAction.slice(6, 70)}`); useDeferredAction = true; deferredActionData = `0x${deferredAction.slice(70)}`; hasAssociatedExecHooks = deferredAction[5] === "1"; - } else if (nonce > nextNonceForDeferredActionNonce) { + } else if (nonce > nextNonceForDeferredAction) { throw new InvalidDeferredActionNonce(); } } @@ -189,7 +189,7 @@ export async function createMAv2Base< !!(await client.getCode({ address: accountAddress })); const getNonce = async (nonceKey: bigint = 0n): Promise => { - if (useDeferredAction && deferredAction) { + if (useDeferredAction) { return nonce; } diff --git a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts index c21a56818e..d54c5a496e 100644 --- a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts +++ b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts @@ -8,12 +8,11 @@ import { parseEther, publicActions, testActions, - toHex, type TestActions, } from "viem"; import { installValidationActions } from "@account-kit/smart-contracts/experimental"; import { - // createModularAccountV2Client, + createModularAccountV2Client, type SignerEntity, } from "@account-kit/smart-contracts"; import { local070Instance } from "~test/instances.js"; @@ -22,7 +21,6 @@ import { accounts } from "~test/constants.js"; import { alchemyGasAndPaymasterAndDataMiddleware } from "@account-kit/infra"; import { deferralActions } from "./deferralActions.js"; import { PermissionBuilder, PermissionType } from "../permissionBuilder.js"; -import { createModularAccountV2Client } from "../client/client.js"; // Note: These tests maintain a shared state to not break the local-running rundler by desyncing the chain. describe("MA v2 deferral actions tests", async () => { @@ -62,8 +60,6 @@ describe("MA v2 deferral actions tests", async () => { // these can be default values or from call arguments const { entityId, nonce } = await provider.getEntityIdAndNonce({ - entityId: 0, - nonceKey: 0n, isGlobalValidation: true, }); @@ -90,7 +86,7 @@ describe("MA v2 deferral actions tests", async () => { const sig = await provider.account.signTypedData(typedData); - const deferredActionDigest = provider.buildDeferredActionDigest({ + const deferredActionDigest = await provider.buildDeferredActionDigest({ fullPreSignatureDeferredActionDigest, sig, }); @@ -101,7 +97,7 @@ describe("MA v2 deferral actions tests", async () => { chain: instance.chain, accountAddress: provider.getAddress(), signer: sessionKey, - initCode: provider.account.getInitCode(), + initCode: await provider.account.getInitCode(), deferredAction: deferredActionDigest, }); diff --git a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.ts b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.ts index 9b3c9d9a54..b1a035f614 100644 --- a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.ts +++ b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.ts @@ -67,9 +67,10 @@ export type BuildUserOperationWithDeferredActionParams = { }; export type EntityIdAndNonceParams = { - entityId: number; - nonceKey: bigint; + entityId?: number; + nonceKey?: bigint; isGlobalValidation: boolean; + isDeferredAction?: boolean; }; export type DeferralActions = { @@ -171,8 +172,6 @@ export const deferralActions: ( [validationLocator, typedData.message.deadline, typedData.message.call] ); - console.log(validationLocator); - const encodedDataLength = size(encodedCallData); const encodedData = concatHex([ toHex(encodedDataLength, { size: 4 }), @@ -225,9 +224,10 @@ export const deferralActions: ( }; const getEntityIdAndNonce = async ({ - entityId, - nonceKey, + entityId = 1, + nonceKey = 0n, isGlobalValidation, + isDeferredAction = true, }: EntityIdAndNonceParams) => { if (!client.account) { throw new AccountNotFoundError(); @@ -252,7 +252,7 @@ export const deferralActions: ( nonceKey, entityId, isGlobalValidation, - isDeferredAction: true, + isDeferredAction, }), ], }); diff --git a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts index 3cf0c97f3a..866a7f58f1 100644 --- a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts +++ b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts @@ -351,8 +351,6 @@ describe("MA v2 Tests", async () => { }); const { entityId, nonce } = await provider.getEntityIdAndNonce({ - entityId: 0, - nonceKey: 0n, isGlobalValidation: false, }); @@ -407,7 +405,7 @@ describe("MA v2 Tests", async () => { chain: instance.chain, accountAddress: provider.getAddress(), signer: sessionKey, - initCode: provider.account.getInitCode(), // must be called with the owner provider! + initCode: await provider.account.getInitCode(), // must be called with the owner provider! deferredAction: deferredActionDigest, }); @@ -458,7 +456,7 @@ describe("MA v2 Tests", async () => { }); // Deploy mock ERC20 - const ret = await walletClient.deployContract({ + await walletClient.deployContract({ abi: mintableERC20Abi, account: walletClient.account, bytecode: mintableERC20Bytecode, @@ -476,8 +474,6 @@ describe("MA v2 Tests", async () => { // const sessionKeyEntityId = 1; // these can be default values or from call arguments const { entityId, nonce } = await provider.getEntityIdAndNonce({ - entityId: 0, - nonceKey: 0n, isGlobalValidation: false, }); @@ -525,7 +521,7 @@ describe("MA v2 Tests", async () => { signer: sessionKey, transport: custom(instance.getClient()), accountAddress: provider.getAddress(), - initCode: provider.account.getInitCode(), + initCode: await provider.account.getInitCode(), deferredAction: deferredActionDigest, }); @@ -577,8 +573,6 @@ describe("MA v2 Tests", async () => { // Test variables const isGlobalValidation = true; const { entityId, nonce } = await provider.getEntityIdAndNonce({ - entityId: 0, - nonceKey: 0n, isGlobalValidation: isGlobalValidation, }); @@ -637,7 +631,7 @@ describe("MA v2 Tests", async () => { chain: instance.chain, accountAddress: provider.getAddress(), signer: sessionKey, - initCode: provider.account.getInitCode(), + initCode: await provider.account.getInitCode(), deferredAction: fullDeferredAction, }); @@ -721,8 +715,6 @@ describe("MA v2 Tests", async () => { const isGlobalValidation = true; const { entityId, nonce } = await provider.getEntityIdAndNonce({ - entityId: 0, - nonceKey: 0n, isGlobalValidation: true, }); @@ -766,7 +758,7 @@ describe("MA v2 Tests", async () => { chain: instance.chain, accountAddress: provider.getAddress(), signer: newSessionKey, - initCode: provider.account.getInitCode(), + initCode: await provider.account.getInitCode(), deferredAction: deferredActionDigest, }); From cf46a00b0a8b26a3bc263722b8dada641381ed71 Mon Sep 17 00:00:00 2001 From: howydev <132113803+howydev@users.noreply.github.com> Date: Mon, 7 Apr 2025 15:31:22 -0400 Subject: [PATCH 20/32] chore: remove unused action --- .../src/ma-v2/actions/deferralActions.test.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts index d54c5a496e..8e70cd0080 100644 --- a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts +++ b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts @@ -10,7 +10,6 @@ import { testActions, type TestActions, } from "viem"; -import { installValidationActions } from "@account-kit/smart-contracts/experimental"; import { createModularAccountV2Client, type SignerEntity, @@ -45,9 +44,9 @@ describe("MA v2 deferral actions tests", async () => { const sendAmount = parseEther("1"); it("tests the full deferred actions flow", async () => { - const provider = (await givenConnectedProvider({ signer })) - .extend(deferralActions) - .extend(installValidationActions); + const provider = (await givenConnectedProvider({ signer })).extend( + deferralActions + ); await setBalance(instance.getClient(), { address: provider.getAddress(), From 44467034e5a3a284c86276e67775bdaabe68aedd Mon Sep 17 00:00:00 2001 From: howydev <132113803+howydev@users.noreply.github.com> Date: Mon, 7 Apr 2025 15:44:28 -0400 Subject: [PATCH 21/32] chore: more fixes --- .../src/ma-v2/actions/deferralActions.test.ts | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts index 8e70cd0080..94d8ac5f5f 100644 --- a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts +++ b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts @@ -44,9 +44,16 @@ describe("MA v2 deferral actions tests", async () => { const sendAmount = parseEther("1"); it("tests the full deferred actions flow", async () => { - const provider = (await givenConnectedProvider({ signer })).extend( - deferralActions - ); + const provider = await givenConnectedProvider({ signer }); + + const serverClient = ( + await createModularAccountV2Client({ + chain: instance.chain, + accountAddress: provider.getAddress(), + signer: new LocalAccountSigner(accounts.fundedAccountOwner), + transport: custom(instance.getClient()), + }) + ).extend(deferralActions); await setBalance(instance.getClient(), { address: provider.getAddress(), @@ -58,12 +65,12 @@ describe("MA v2 deferral actions tests", async () => { ); // these can be default values or from call arguments - const { entityId, nonce } = await provider.getEntityIdAndNonce({ + const { entityId, nonce } = await serverClient.getEntityIdAndNonce({ isGlobalValidation: true, }); const { typedData, fullPreSignatureDeferredActionDigest } = - await new PermissionBuilder(provider) + await new PermissionBuilder(serverClient) .configure({ key: { publicKey: await sessionKey.getAddress(), @@ -85,12 +92,11 @@ describe("MA v2 deferral actions tests", async () => { const sig = await provider.account.signTypedData(typedData); - const deferredActionDigest = await provider.buildDeferredActionDigest({ + const deferredActionDigest = await serverClient.buildDeferredActionDigest({ fullPreSignatureDeferredActionDigest, sig, }); - // TODO: need nonce, orig account address, orig account initcode const sessionKeyClient = await createModularAccountV2Client({ transport: custom(instance.getClient()), chain: instance.chain, From 23fbf6e1379b8fb3440b88b0130b111952e4cd66 Mon Sep 17 00:00:00 2001 From: Zer0dot Date: Mon, 7 Apr 2025 15:54:44 -0400 Subject: [PATCH 22/32] chore: update viem peer dep --- account-kit/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account-kit/core/package.json b/account-kit/core/package.json index ecbef15aeb..856fd230b5 100644 --- a/account-kit/core/package.json +++ b/account-kit/core/package.json @@ -62,7 +62,7 @@ "zustand": "^5.0.0-rc.2" }, "peerDependencies": { - "viem": "^2.22.6", + "viem": "^2.26.0", "wagmi": "^2.12.7" }, "publishConfig": { From 63e440a53275446176fa13441d3b29bd6aead374 Mon Sep 17 00:00:00 2001 From: zer0dot Date: Mon, 7 Apr 2025 15:56:54 -0400 Subject: [PATCH 23/32] Revert "chore: update viem peer dep" This reverts commit 641c7f2235490723e97bcbe6908b53fdec4177f3. --- account-kit/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account-kit/core/package.json b/account-kit/core/package.json index 856fd230b5..ecbef15aeb 100644 --- a/account-kit/core/package.json +++ b/account-kit/core/package.json @@ -62,7 +62,7 @@ "zustand": "^5.0.0-rc.2" }, "peerDependencies": { - "viem": "^2.26.0", + "viem": "^2.22.6", "wagmi": "^2.12.7" }, "publishConfig": { From 9e0c6dd8ca9f745bc65288dac47067e684fb77e8 Mon Sep 17 00:00:00 2001 From: Zer0dot Date: Mon, 7 Apr 2025 16:07:20 -0400 Subject: [PATCH 24/32] Update aa-sdk/core/src/errors/client.ts Co-authored-by: jakehobbs --- aa-sdk/core/src/errors/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aa-sdk/core/src/errors/client.ts b/aa-sdk/core/src/errors/client.ts index 5b83123cfe..1c04dfab48 100644 --- a/aa-sdk/core/src/errors/client.ts +++ b/aa-sdk/core/src/errors/client.ts @@ -125,7 +125,7 @@ export class InvalidDeferredActionMode extends BaseError { override name = "InvalidDeferredActionMode"; /** - * Initializes a new instance of the error message with a default message indicating that the provided ma v2 account mode is invalid. + * Initializes a new instance of the error message with a default message indicating that the provided deferred action mode is invalid. */ constructor() { super(`The provided deferred action mode is invalid`); From 7787ee81db9771cee6620b497d5ddf521a637301 Mon Sep 17 00:00:00 2001 From: Zer0dot Date: Mon, 7 Apr 2025 16:07:26 -0400 Subject: [PATCH 25/32] Update aa-sdk/core/src/errors/client.ts Co-authored-by: jakehobbs --- aa-sdk/core/src/errors/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aa-sdk/core/src/errors/client.ts b/aa-sdk/core/src/errors/client.ts index 1c04dfab48..ec76004af1 100644 --- a/aa-sdk/core/src/errors/client.ts +++ b/aa-sdk/core/src/errors/client.ts @@ -139,7 +139,7 @@ export class InvalidDeferredActionNonce extends BaseError { override name = "InvalidDeferredActionNonce"; /** - * Initializes a new instance of the error message with a default message indicating that the provided ma v2 account mode is invalid. + * Initializes a new instance of the error message with a default message indicating that the provided deferred action nonce is invalid. */ constructor() { super(`The provided deferred action nonce is invalid`); From cb255ffe8622c9e6378e883a1db0784f288669be Mon Sep 17 00:00:00 2001 From: Zer0dot Date: Mon, 7 Apr 2025 16:08:40 -0400 Subject: [PATCH 26/32] Update account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts Co-authored-by: Michael Moldoveanu --- account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts index 22b7b5a426..e93b771c6f 100644 --- a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts +++ b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts @@ -328,7 +328,7 @@ export class PermissionBuilder { } // Use for building deferred action typed data to sign - async compile_deferred({ + async compileDeferred({ deadline, }: { deadline: number; From 5e8eee5bb8c86457bd87c941d1a0da70f8e673a0 Mon Sep 17 00:00:00 2001 From: Zer0dot Date: Mon, 7 Apr 2025 16:08:51 -0400 Subject: [PATCH 27/32] Update account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts Co-authored-by: Michael Moldoveanu --- account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts index e93b771c6f..ed797530a1 100644 --- a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts +++ b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts @@ -390,7 +390,7 @@ export class PermissionBuilder { } // Use for direct `installValidation()` low-level calls (maybe useless) - async compile_raw(): Promise { + async compileRaw(): Promise { // Translate all permissions into raw hooks if >0 if (this.permissions.length > 0) { const rawHooks = this.translatePermissions( From 849e1869fc47b9ccfe390955d84b083cd3fbe67e Mon Sep 17 00:00:00 2001 From: Zer0dot Date: Mon, 7 Apr 2025 16:12:02 -0400 Subject: [PATCH 28/32] Update account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts Co-authored-by: Michael Moldoveanu --- account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts index ed797530a1..0c8cadeb28 100644 --- a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts +++ b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts @@ -411,7 +411,7 @@ export class PermissionBuilder { } // Use for compiling args to installValidation - async compile_installArgs(): Promise { + async compileInstallArgs(): Promise { this.validateConfiguration(); return { From 9e9ed1c523f67047ff35be59e6b631843d098850 Mon Sep 17 00:00:00 2001 From: howydev <132113803+howydev@users.noreply.github.com> Date: Mon, 7 Apr 2025 16:19:58 -0400 Subject: [PATCH 29/32] chore: remove docs that don't seem to belong to changes in this branch --- .../aa-sdk/core/functions/tracingHeader.mdx | 43 ------------------- .../BaseAlchemySigner/toSolanaSigner.mdx | 41 ------------------ 2 files changed, 84 deletions(-) delete mode 100644 site/pages/reference/aa-sdk/core/functions/tracingHeader.mdx delete mode 100644 site/pages/reference/account-kit/signer/classes/BaseAlchemySigner/toSolanaSigner.mdx diff --git a/site/pages/reference/aa-sdk/core/functions/tracingHeader.mdx b/site/pages/reference/aa-sdk/core/functions/tracingHeader.mdx deleted file mode 100644 index 5e0d1e34d3..0000000000 --- a/site/pages/reference/aa-sdk/core/functions/tracingHeader.mdx +++ /dev/null @@ -1,43 +0,0 @@ ---- -# This file is autogenerated - -title: tracingHeader -description: Overview of the tracingHeader method ---- - -# tracingHeader - -We wanted a transport that is adding the add headers for the headers that we are sending to the server for tracing cross actions -and services. - -## Import - -```ts -import { tracingHeader } from "@aa-sdk/core"; -``` - -## Usage - -```ts -import { createPublicClient, http } from "viem"; -import { tracingHeader } from "@aa-sdk/core"; - -const clientWithTracing = createPublicClient({ - transport: tracingHeader({ - breadcrumb: "myMethodOrAction", - transport: http(OTHER_RPC_URL), - }), -}); -``` - -## Parameters - -### params - -`TracingHeadersParams` -tracing headers config to wrap around a transport to add in headers for tracing - -## Returns - -`CustomTransport` -a viem Transport that splits traffic diff --git a/site/pages/reference/account-kit/signer/classes/BaseAlchemySigner/toSolanaSigner.mdx b/site/pages/reference/account-kit/signer/classes/BaseAlchemySigner/toSolanaSigner.mdx deleted file mode 100644 index 1c7420ca45..0000000000 --- a/site/pages/reference/account-kit/signer/classes/BaseAlchemySigner/toSolanaSigner.mdx +++ /dev/null @@ -1,41 +0,0 @@ ---- -# This file is autogenerated - -title: toSolanaSigner -description: Overview of the toSolanaSigner method ---- - -# toSolanaSigner - -Creates a new instance of `SolanaSigner` using the provided inner value. -This requires the signer to be authenticated first - -## Import - -```ts -import { BaseAlchemySigner } from "@account-kit/signer"; -``` - -## Usage - -```ts -import { AlchemyWebSigner } from "@account-kit/signer"; - -const signer = new AlchemyWebSigner({ - client: { - connection: { - rpcUrl: "/api/rpc", - }, - iframeConfig: { - iframeContainerId: "alchemy-signer-iframe-container", - }, - }, -}); - -const solanaSigner = signer.toSolanaSigner(); -``` - -## Returns - -`SolanaSigner` -A new instance of `SolanaSigner` From d629ea1506d9bae4b545a87380fbf1d2828a23bc Mon Sep 17 00:00:00 2001 From: howydev <132113803+howydev@users.noreply.github.com> Date: Mon, 7 Apr 2025 16:26:32 -0400 Subject: [PATCH 30/32] fix: test fail --- account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts index 0c8cadeb28..728ea435f9 100644 --- a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts +++ b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts @@ -338,7 +338,7 @@ export class PermissionBuilder { typedData: DeferredActionTypedData; fullPreSignatureDeferredActionDigest: Hex; }> { - // Need to remove this because compile_raw may add selectors + // Need to remove this because compileRaw may add selectors // this.validateConfiguration(); // Add time range module hook via expiry @@ -362,7 +362,7 @@ export class PermissionBuilder { ); } - const installValidationCall = await this.compile_raw(); + const installValidationCall = await this.compileRaw(); const { typedData } = await deferralActions( this.client From b2cc4a52a5d6dd893ae24a91cf2d17be49fa631d Mon Sep 17 00:00:00 2001 From: howydev <132113803+howydev@users.noreply.github.com> Date: Mon, 7 Apr 2025 16:37:10 -0400 Subject: [PATCH 31/32] fix: more test failures --- .../src/ma-v2/actions/deferralActions.test.ts | 2 +- account-kit/smart-contracts/src/ma-v2/client/client.test.ts | 6 +++--- account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts | 2 +- .../core/classes/InvalidDeferredActionMode/constructor.mdx | 2 +- .../core/classes/InvalidDeferredActionNonce/constructor.mdx | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts index 94d8ac5f5f..fdc5a89a0f 100644 --- a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts +++ b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts @@ -84,7 +84,7 @@ describe("MA v2 deferral actions tests", async () => { type: PermissionType.ROOT, }, }) - .compile_deferred({ + .compileDeferred({ deadline: 0, uoValidationEntityId: entityId, uoIsGlobalValidation: true, diff --git a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts index 866a7f58f1..c7b5f2290e 100644 --- a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts +++ b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts @@ -382,7 +382,7 @@ describe("MA v2 Tests", async () => { }, }, }) - .compile_deferred({ + .compileDeferred({ deadline: 0, uoValidationEntityId: entityId, uoIsGlobalValidation: false, @@ -498,7 +498,7 @@ describe("MA v2 Tests", async () => { }, }, }) - .compile_deferred({ + .compileDeferred({ deadline: 0, uoValidationEntityId: entityId, uoIsGlobalValidation: false, @@ -735,7 +735,7 @@ describe("MA v2 Tests", async () => { type: PermissionType.ROOT, }, }) - .compile_deferred({ + .compileDeferred({ deadline: 0, uoValidationEntityId: entityId, uoIsGlobalValidation: isGlobalValidation, diff --git a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts index 728ea435f9..a7d4016a9c 100644 --- a/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts +++ b/account-kit/smart-contracts/src/ma-v2/permissionBuilder.ts @@ -345,7 +345,7 @@ export class PermissionBuilder { if (deadline !== 0) { if (deadline < Date.now() / 1000) { throw new Error( - `PERMISSION: compile_deferred(): Deadline ${deadline} cannot be before now (${ + `PERMISSION: compileDeferred(): Deadline ${deadline} cannot be before now (${ Date.now() / 1000 })` ); diff --git a/site/pages/reference/aa-sdk/core/classes/InvalidDeferredActionMode/constructor.mdx b/site/pages/reference/aa-sdk/core/classes/InvalidDeferredActionMode/constructor.mdx index 591e20009e..83fb5560e7 100644 --- a/site/pages/reference/aa-sdk/core/classes/InvalidDeferredActionMode/constructor.mdx +++ b/site/pages/reference/aa-sdk/core/classes/InvalidDeferredActionMode/constructor.mdx @@ -6,7 +6,7 @@ description: Overview of the InvalidDeferredActionMode method # InvalidDeferredActionMode -Initializes a new instance of the error message with a default message indicating that the provided ma v2 account mode is invalid. +Initializes a new instance of the error message with a default message indicating that the provided deferred action mode is invalid. :::note `InvalidDeferredActionMode` extends `BaseError`, see the docs for BaseError for all supported methods. ::: diff --git a/site/pages/reference/aa-sdk/core/classes/InvalidDeferredActionNonce/constructor.mdx b/site/pages/reference/aa-sdk/core/classes/InvalidDeferredActionNonce/constructor.mdx index f35dd89a8f..3690b5e9dc 100644 --- a/site/pages/reference/aa-sdk/core/classes/InvalidDeferredActionNonce/constructor.mdx +++ b/site/pages/reference/aa-sdk/core/classes/InvalidDeferredActionNonce/constructor.mdx @@ -6,7 +6,7 @@ description: Overview of the InvalidDeferredActionNonce method # InvalidDeferredActionNonce -Initializes a new instance of the error message with a default message indicating that the provided ma v2 account mode is invalid. +Initializes a new instance of the error message with a default message indicating that the provided deferred action nonce is invalid. :::note `InvalidDeferredActionNonce` extends `BaseError`, see the docs for BaseError for all supported methods. ::: From baaebf6b1883a524b824655cd075403f2334544b Mon Sep 17 00:00:00 2001 From: howydev <132113803+howydev@users.noreply.github.com> Date: Mon, 7 Apr 2025 17:35:44 -0400 Subject: [PATCH 32/32] chore: add additions to exports --- .../src/ma-v2/actions/deferralActions.test.ts | 7 +++++-- .../smart-contracts/src/ma-v2/client/client.test.ts | 9 ++++----- account-kit/smart-contracts/src/ma-v2/index.ts | 4 ++++ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts index fdc5a89a0f..968227b2fc 100644 --- a/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts +++ b/account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts @@ -14,12 +14,15 @@ import { createModularAccountV2Client, type SignerEntity, } from "@account-kit/smart-contracts"; +import { + deferralActions, + PermissionBuilder, + PermissionType, +} from "@account-kit/smart-contracts/experimental"; import { local070Instance } from "~test/instances.js"; import { setBalance } from "viem/actions"; import { accounts } from "~test/constants.js"; import { alchemyGasAndPaymasterAndDataMiddleware } from "@account-kit/infra"; -import { deferralActions } from "./deferralActions.js"; -import { PermissionBuilder, PermissionType } from "../permissionBuilder.js"; // Note: These tests maintain a shared state to not break the local-running rundler by desyncing the chain. describe("MA v2 deferral actions tests", async () => { diff --git a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts index c7b5f2290e..3352c11cb0 100644 --- a/account-kit/smart-contracts/src/ma-v2/client/client.test.ts +++ b/account-kit/smart-contracts/src/ma-v2/client/client.test.ts @@ -21,12 +21,11 @@ import { concat, testActions, concatHex, - type TestActions, - type Hex, toHex, createWalletClient, getContractAddress, encodeFunctionData, + type TestActions, type ContractFunctionName, } from "viem"; import { HookType } from "../actions/common/types.js"; @@ -44,7 +43,9 @@ import { NativeTokenLimitModule, semiModularAccountBytecodeAbi, buildFullNonceKey, - modularAccountAbi, + deferralActions, + PermissionBuilder, + PermissionType, } from "@account-kit/smart-contracts/experimental"; import { createLightAccountClient, @@ -66,9 +67,7 @@ import { alchemyGasAndPaymasterAndDataMiddleware, } from "@account-kit/infra"; import { getMAV2UpgradeToData } from "@account-kit/smart-contracts"; -import { deferralActions } from "../actions/deferralActions.js"; import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; -import { PermissionBuilder, PermissionType } from "../permissionBuilder.js"; import { mintableERC20Abi, mintableERC20Bytecode } from "../utils.js"; // Note: These tests maintain a shared state to not break the local-running rundler by desyncing the chain. diff --git a/account-kit/smart-contracts/src/ma-v2/index.ts b/account-kit/smart-contracts/src/ma-v2/index.ts index e70d15268a..979f13bc36 100644 --- a/account-kit/smart-contracts/src/ma-v2/index.ts +++ b/account-kit/smart-contracts/src/ma-v2/index.ts @@ -20,6 +20,10 @@ export { } from "./actions/common/utils.js"; export type * from "./actions/install-validation/installValidation.js"; export { installValidationActions } from "./actions/install-validation/installValidation.js"; +export type * from "./actions/deferralActions.js"; +export { deferralActions } from "./actions/deferralActions.js"; +export type * from "./permissionBuilder.js"; +export { PermissionBuilder, PermissionType } from "./permissionBuilder.js"; export { getDefaultAllowlistModuleAddress,