diff --git a/aa-sdk/core/src/errors/account.ts b/aa-sdk/core/src/errors/account.ts index 18ea7c63e7..3ce79488f2 100644 --- a/aa-sdk/core/src/errors/account.ts +++ b/aa-sdk/core/src/errors/account.ts @@ -17,6 +17,20 @@ export class AccountNotFoundError extends BaseError { } } +/** + * This error is thrown when an account is not a Modular Account V2 + */ + +export class NotAModularAccountV2Error extends BaseError { + override name = "NotAModularAccountV2Error"; + /** + * Constructor for initializing an error message indicating that the account is not a Modular Account V2. + */ + constructor() { + super("This is not a Modular Account V2 account."); + } +} + /** * Represents an error that is thrown when no default factory is defined for a specific account type on a given chain and entry point version. * This error suggests providing an override via the `factoryAddress` parameter when creating an account. diff --git a/aa-sdk/core/src/index.ts b/aa-sdk/core/src/index.ts index c141cd0a89..faa840bd74 100644 --- a/aa-sdk/core/src/index.ts +++ b/aa-sdk/core/src/index.ts @@ -57,6 +57,7 @@ export { export type * from "./entrypoint/types.js"; export { AccountNotFoundError, + NotAModularAccountV2Error, AccountRequiresOwnerError, BatchExecutionNotSupportedError, DefaultFactoryNotDefinedError, 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 24cb72cd88..6727c6ef04 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 @@ -1,38 +1,40 @@ import { createBundlerClient, getEntryPoint, + InvalidDeferredActionNonce, InvalidEntityIdError, InvalidNonceKeyError, - InvalidDeferredActionNonce, toSmartContractAccount, type AccountOp, type SmartAccountSigner, + type SmartContractAccount, type SmartContractAccountWithSigner, type ToSmartContractAccountParams, - type SmartContractAccount, } from "@aa-sdk/core"; -import { DEFAULT_OWNER_ENTITY_ID, parseDeferredAction } from "../../utils.js"; import { - type Hex, - type Address, - type Chain, - type Transport, + concatHex, encodeFunctionData, - maxUint32, - zeroAddress, getContract, - concatHex, maxUint152, + maxUint32, + zeroAddress, + type Address, + type Chain, + type Hex, + type Transport, } from "viem"; +import type { ToWebAuthnAccountParameters } from "viem/account-abstraction"; import { modularAccountAbi } from "../../abis/modularAccountAbi.js"; import { serializeModuleEntity } from "../../actions/common/utils.js"; -import { nativeSMASigner } from "../nativeSMASigner.js"; import { singleSignerMessageSigner } from "../../modules/single-signer-validation/signer.js"; -import type { ToWebAuthnAccountParameters } from "viem/account-abstraction"; import { webauthnSigningFunctions } from "../../modules/webauthn-validation/signingMethods.js"; +import { DEFAULT_OWNER_ENTITY_ID, parseDeferredAction } from "../../utils.js"; +import { nativeSMASigner } from "../nativeSMASigner.js"; export const executeUserOpSelector: Hex = "0x8DD7712F"; +export type ModularAccountsV2 = ModularAccountV2 | WebauthnModularAccountV2; + export type SignerEntity = { isGlobalValidation: boolean; entityId: number; @@ -358,3 +360,9 @@ export async function createMAv2Base< encodeCallData, } as ModularAccountV2; // TO DO: figure out when this breaks! we shouldn't have to cast } + +export function isModularAccountV2( + account: SmartContractAccount, +): account is ModularAccountV2 | WebauthnModularAccountV2 { + return account.source === "ModularAccountV2"; +} diff --git a/account-kit/smart-contracts/src/ma-v2/actions/install-validation/installValidation.ts b/account-kit/smart-contracts/src/ma-v2/actions/install-validation/installValidation.ts index 168cbe2799..f770b4732c 100644 --- a/account-kit/smart-contracts/src/ma-v2/actions/install-validation/installValidation.ts +++ b/account-kit/smart-contracts/src/ma-v2/actions/install-validation/installValidation.ts @@ -1,35 +1,44 @@ import { AccountNotFoundError, - IncompatibleClientError, - isSmartAccountClient, + NotAModularAccountV2Error, EntityIdOverrideError, + type GetAccountParameter, type GetEntryPointFromAccount, + IncompatibleClientError, type SendUserOperationResult, type UserOperationOverridesParameter, - type SmartAccountSigner, + isSmartAccountClient, + isSmartAccountWithSigner, } from "@aa-sdk/core"; import { type Address, + type Chain, + type Client, type Hex, - encodeFunctionData, + type Transport, concatHex, + encodeFunctionData, zeroAddress, } from "viem"; import { semiModularAccountBytecodeAbi } from "../../abis/semiModularAccountBytecodeAbi.js"; import type { HookConfig, ValidationConfig } from "../common/types.js"; import { - serializeValidationConfig, serializeHookConfig, serializeModuleEntity, + serializeValidationConfig, } from "../common/utils.js"; -import { type ModularAccountV2Client } from "../../client/client.js"; -import { type ModularAccountV2 } from "../../account/common/modularAccountV2Base.js"; +import { + type ModularAccountsV2, + isModularAccountV2, +} from "../../account/common/modularAccountV2Base.js"; import { DEFAULT_OWNER_ENTITY_ID } from "../../utils.js"; export type InstallValidationParams< - TSigner extends SmartAccountSigner = SmartAccountSigner, + TAccount extends ModularAccountsV2 | undefined = + | ModularAccountsV2 + | undefined, > = { validationConfig: ValidationConfig; selectors: Hex[]; @@ -38,38 +47,38 @@ export type InstallValidationParams< hookConfig: HookConfig; initData: Hex; }[]; - account?: ModularAccountV2 | undefined; -} & UserOperationOverridesParameter< - GetEntryPointFromAccount> ->; +} & UserOperationOverridesParameter> & + GetAccountParameter; export type UninstallValidationParams< - TSigner extends SmartAccountSigner = SmartAccountSigner, + TAccount extends ModularAccountsV2 | undefined = + | ModularAccountsV2 + | undefined, > = { moduleAddress: Address; entityId: number; uninstallData: Hex; hookUninstallDatas: Hex[]; - account?: ModularAccountV2 | undefined; -} & UserOperationOverridesParameter< - GetEntryPointFromAccount> ->; +} & UserOperationOverridesParameter> & + GetAccountParameter; export type InstallValidationActions< - TSigner extends SmartAccountSigner = SmartAccountSigner, + TAccount extends ModularAccountsV2 | undefined = + | ModularAccountsV2 + | undefined, > = { installValidation: ( - args: InstallValidationParams, + args: InstallValidationParams, ) => Promise; encodeInstallValidation: ( // TODO: omit the user op sending related parameters from this type - args: InstallValidationParams, + args: InstallValidationParams, ) => Promise; uninstallValidation: ( - args: UninstallValidationParams, + args: UninstallValidationParams, ) => Promise; encodeUninstallValidation: ( - args: UninstallValidationParams, + args: UninstallValidationParams, ) => Promise; }; @@ -117,23 +126,32 @@ export type InstallValidationActions< * @param {object} client - The client instance which provides account and sendUserOperation functionality. * @returns {object} - An object containing two methods, `installValidation` and `uninstallValidation`. */ -export const installValidationActions: < - TSigner extends SmartAccountSigner = SmartAccountSigner, +export function installValidationActions< + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined, + TAccount extends ModularAccountsV2 | undefined = + | ModularAccountsV2 + | undefined, >( - client: ModularAccountV2Client, -) => InstallValidationActions = (client) => { + client: Client, +): InstallValidationActions { const encodeInstallValidation = async ({ validationConfig, selectors, installData, hooks, account = client.account, - }: InstallValidationParams) => { + }: InstallValidationParams) => { if (!account) { throw new AccountNotFoundError(); } - if (!isSmartAccountClient(client)) { + if (!isModularAccountV2(account)) { + throw new NotAModularAccountV2Error(); + } + + if (isSmartAccountWithSigner(account) && !isSmartAccountClient(client)) { + // if we don't differentiate between WebauthnModularAccountV2Client and ModularAccountV2Client, passing client to isSmartAccountClient complains throw new IncompatibleClientError( "SmartAccountClient", "installValidation", @@ -171,12 +189,17 @@ export const installValidationActions: < uninstallData, hookUninstallDatas, account = client.account, - }: UninstallValidationParams) => { + }: UninstallValidationParams) => { if (!account) { throw new AccountNotFoundError(); } - if (!isSmartAccountClient(client)) { + if (!isModularAccountV2(account)) { + throw new NotAModularAccountV2Error(); + } + + if (isSmartAccountWithSigner(account) && !isSmartAccountClient(client)) { + // if we don't differentiate between WebauthnModularAccountV2Client and ModularAccountV2Client, passing client to isSmartAccountClient complains throw new IncompatibleClientError( "SmartAccountClient", "uninstallValidation", @@ -210,7 +233,19 @@ export const installValidationActions: < hooks, account = client.account, overrides, - }) => { + }: InstallValidationParams) => { + if (!isSmartAccountClient(client)) { + throw new IncompatibleClientError( + "SmartAccountClient", + "installValidation", + client, + ); + } + + if (!account) { + throw new AccountNotFoundError(); + } + const callData = await encodeInstallValidation({ validationConfig, selectors, @@ -233,8 +268,20 @@ export const installValidationActions: < hookUninstallDatas, account = client.account, overrides, - }) => { - const callData = await encodeUninstallValidation({ + }: UninstallValidationParams) => { + if (!account) { + throw new AccountNotFoundError(); + } + + if (!isSmartAccountClient(client)) { + throw new IncompatibleClientError( + "SmartAccountClient", + "uninstallValidation", + client, + ); + } + + const callData: Hex = await encodeUninstallValidation({ moduleAddress, entityId, uninstallData, @@ -249,4 +296,4 @@ export const installValidationActions: < }); }, }; -}; +} 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 688d03b0d4..a0cbaca9f4 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 @@ -29,6 +29,7 @@ import { getDefaultPaymasterGuardModuleAddress, getDefaultSingleSignerValidationModuleAddress, getDefaultTimeRangeModuleAddress, + getDefaultWebauthnValidationModuleAddress, installValidationActions, NativeTokenLimitModule, PaymasterGuardModule, @@ -66,6 +67,7 @@ import { } from "viem/account-abstraction"; import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; import { setBalance } from "viem/actions"; +import { parsePublicKey } from "webauthn-p256"; import { local070Instance } from "~test/instances.js"; import { paymaster070 } from "~test/paymaster/paymaster070.js"; import { SoftWebauthnDevice } from "~test/webauthn.js"; @@ -74,6 +76,7 @@ import { packPaymasterData, } from "../../../../../aa-sdk/core/src/entrypoint/0.7.js"; import { HookType } from "../actions/common/types.js"; +import { WebAuthnValidationModule } from "../modules/webauthn-validation/module.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. @@ -167,6 +170,86 @@ describe("MA v2 Tests", async () => { ); }); + it("installs WebAuthnValidationModule, sends UO on behalf of owner with webauthn session key", async () => { + const provider = (await givenWebAuthnProvider()).provider.extend( + installValidationActions, + ); + + await setBalance(instance.getClient(), { + address: provider.getAddress(), + value: parseEther("2"), + }); + + // set up session key client + const webauthnDevice = new SoftWebauthnDevice(); + + const credential = await createWebAuthnCredential({ + rp: { id: "localhost", name: "localhost" }, + createFn: (opts) => webauthnDevice.create(opts, "localhost"), + user: { name: "test", displayName: "test" }, + }); + + const { x, y } = parsePublicKey(credential.publicKey); + + // install webauthn validation module + const result = await provider.installValidation({ + validationConfig: { + moduleAddress: getDefaultWebauthnValidationModuleAddress( + provider.chain, + ), + entityId: 1, + isGlobal: true, + isSignatureValidation: true, + isUserOpValidation: true, + }, + selectors: [], + installData: WebAuthnValidationModule.encodeOnInstallData({ + entityId: 1, + x, + y, + }), + hooks: [], + }); + + // wait for the UserOperation to be mined + await provider.waitForUserOperationTransaction(result).catch(async () => { + const dropAndReplaceResult = await provider.dropAndReplaceUserOperation({ + uoToDrop: result.request, + }); + await provider.waitForUserOperationTransaction(dropAndReplaceResult); + }); + + // create session key client + const sessionKeyClient = await givenConnectedWebauthnProvider({ + credential, + accountAddress: provider.getAddress(), + signerEntity: { entityId: 1, isGlobalValidation: true }, + getFn: (opts) => webauthnDevice.get(opts, "localhost"), + rpId: "localhost", + }); + + const sessionKeyResult = await sessionKeyClient.sendUserOperation({ + uo: { + target: target, + value: sendAmount, + data: "0x", + }, + }); + + // wait for the UserOperation to be mined + await sessionKeyClient + .waitForUserOperationTransaction(sessionKeyResult) + .catch(async () => { + const dropAndReplaceResult = + await sessionKeyClient.dropAndReplaceUserOperation({ + uoToDrop: sessionKeyResult.request, + }); + await sessionKeyClient.waitForUserOperationTransaction( + dropAndReplaceResult, + ); + }); + }); + it.fails( "successfully sign + validate a message, for WebAuthn account", async () => { diff --git a/docs/docs.yml b/docs/docs.yml index 1d16c569d7..8a960ac438 100644 --- a/docs/docs.yml +++ b/docs/docs.yml @@ -1023,6 +1023,8 @@ navigation: path: wallets/pages/reference/aa-sdk/core/classes/Logger/verbose.mdx - page: warn path: wallets/pages/reference/aa-sdk/core/classes/Logger/warn.mdx + - page: NotAModularAccountV2Error + path: wallets/pages/reference/aa-sdk/core/classes/NotAModularAccountV2Error/constructor.mdx - page: SignTransactionNotSupportedError path: wallets/pages/reference/aa-sdk/core/classes/SignTransactionNotSupportedError/constructor.mdx - page: SmartAccountWithSignerRequiredError diff --git a/docs/pages/reference/aa-sdk/core/classes/NotAModularAccountV2Error/constructor.mdx b/docs/pages/reference/aa-sdk/core/classes/NotAModularAccountV2Error/constructor.mdx new file mode 100644 index 0000000000..4c138223e2 --- /dev/null +++ b/docs/pages/reference/aa-sdk/core/classes/NotAModularAccountV2Error/constructor.mdx @@ -0,0 +1,19 @@ +--- +# This file is autogenerated +title: NotAModularAccountV2Error +description: Overview of the NotAModularAccountV2Error method +slug: wallets/reference/aa-sdk/core/classes/NotAModularAccountV2Error/constructor +--- + +Constructor for initializing an error message indicating that the account is not a Modular Account V2. + + + `NotAModularAccountV2Error` extends `BaseError`, see the docs for BaseError + for all supported methods. + + +## Import + +```ts +import { NotAModularAccountV2Error } from "@aa-sdk/core"; +``` diff --git a/examples/ui-demo/src/hooks/useModularAccountV2Client.ts b/examples/ui-demo/src/hooks/useModularAccountV2Client.ts index 42a5eafd74..d9f2b749ba 100644 --- a/examples/ui-demo/src/hooks/useModularAccountV2Client.ts +++ b/examples/ui-demo/src/hooks/useModularAccountV2Client.ts @@ -1,4 +1,4 @@ -import type { AlchemySigner } from "@account-kit/core"; +import type { AlchemySigner, ModularAccountV2 } from "@account-kit/core"; import { useSigner, useSignerStatus } from "@account-kit/react"; import { useState, useEffect } from "react"; import { @@ -26,7 +26,7 @@ type Client = ModularAccountV2Client< AlchemySigner | LocalAccountSigner > & InstallValidationActions< - AlchemySigner | LocalAccountSigner + ModularAccountV2> >; // Hook that creates an MAv2 client that can be used for things that