diff --git a/examples/aa-simple-dapp/src/hooks/useAlchemyProvider.ts b/examples/aa-simple-dapp/src/hooks/useAlchemyProvider.ts index 9c5b50467e..860b68de8d 100644 --- a/examples/aa-simple-dapp/src/hooks/useAlchemyProvider.ts +++ b/examples/aa-simple-dapp/src/hooks/useAlchemyProvider.ts @@ -1,7 +1,7 @@ import { chain, gasManagerPolicyId } from "@/config/client"; import { getRpcUrl } from "@/config/rpc"; import { - LightSmartContractAccount, + createMultiOwnerMSCA, getDefaultLightAccountFactoryAddress, } from "@alchemy/aa-accounts"; import { AlchemyProvider } from "@alchemy/aa-alchemy"; @@ -24,7 +24,7 @@ export const useAlchemyProvider = () => { (signer: SmartAccountSigner, account?: Address) => { const connectedProvider = provider .connect((provider) => { - return new LightSmartContractAccount({ + return createMultiOwnerMSCA({ rpcClient: provider, owner: signer, chain, diff --git a/nx.json b/nx.json index 6239dea20d..442ffad898 100644 --- a/nx.json +++ b/nx.json @@ -9,11 +9,14 @@ }, "targetDefaults": { "build": { - "dependsOn": ["^build"], + "dependsOn": ["^build", "generate"], "outputs": ["{projectRoot}/dist"] }, "test": { "dependsOn": ["build"] + }, + "generate": { + "outputs": ["{projectRoot}/src/plugins"] } } } diff --git a/package.json b/package.json index 0f496292c2..ae3f6cb40e 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,10 @@ "site" ], "scripts": { - "generate": "lerna run generate && yarn lint:write", + "generate": "lerna run generate", + "postgenerate": "lint:write", "build": "lerna run build --ignore=alchemy-daapp --ignore=aa-simple-dapp", + "postbuild": "yarn lint:write", "build:examples": "lerna run build", "clean": "lerna run clean", "test": "lerna run test:run", diff --git a/packages/accounts/package.json b/packages/accounts/package.json index c03c7d16c1..ff3b25067b 100644 --- a/packages/accounts/package.json +++ b/packages/accounts/package.json @@ -30,7 +30,6 @@ }, "scripts": { "generate": "npx wagmi generate", - "prebuild": "yarn generate", "build": "yarn clean && yarn build:cjs && yarn build:esm && yarn build:types", "build:cjs": "tsc --project tsconfig.build.json --module commonjs --outDir ./dist/cjs --removeComments --verbatimModuleSyntax false && echo > ./dist/cjs/package.json '{\"type\":\"commonjs\"}'", "build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir ./dist/esm --removeComments && echo > ./dist/esm/package.json '{\"type\":\"module\"}'", diff --git a/packages/accounts/scripts/plugingen.ts b/packages/accounts/scripts/plugingen.ts index c94a1f27fc..22a1c1e73e 100644 --- a/packages/accounts/scripts/plugingen.ts +++ b/packages/accounts/scripts/plugingen.ts @@ -1,5 +1,5 @@ import { type Plugin } from "@wagmi/cli"; -import { pascalCase } from "change-case"; +import { camelCase, pascalCase } from "change-case"; import dedent from "dedent"; import { createPublicClient, @@ -77,7 +77,7 @@ export function plugingen({ const executionAbiConst = `${contract.name}ExecutionFunctionAbi`; - const encodeFunctions = executionAbi.map((n) => { + const accountFunctions = executionAbi.map((n) => { const methodContent = []; const argsParamString = n.inputs.length > 0 @@ -112,20 +112,50 @@ export function plugingen({ return methodContent.join(",\n\n"); }); + const providerFunctions = executionAbi + .filter((n) => n.stateMutability !== "view") + .map((n) => { + const argsParamString = + n.inputs.length > 0 + ? `{ args }: GetFunctionArgs` + : ""; + const argsEncodeString = n.inputs.length > 0 ? "args," : ""; + + return dedent` + ${camelCase(n.name)}: (${argsParamString}) => { + const callData = encodeFunctionData({ + abi: ${executionAbiConst}, + functionName: "${n.name}", + ${argsEncodeString} + }); + + return provider.sendUserOperation(callData); + } + `; + }); + content.push(dedent` const ${contract.name}_ = { meta: { name: "${name}", version: "${version}", }, - accountDecorators: (account: BaseSmartContractAccount) => ({ ${encodeFunctions.join( + accountDecorators: (account: ISmartContractAccount) => ({ ${accountFunctions.join( ",\n\n" - )} }) + )} }), + providerDecorators: < + TTransport extends SupportedTransports, + P extends ISmartAccountProvider & { account: MSCA } + >( + provider: P + ) => ({ ${providerFunctions.join(",\n\n")} }), } export const ${contract.name}: Plugin> = ${contract.name}_; + }_["accountDecorators"]>, ReturnType> = ${contract.name}_; `); // add the abi at the end so it's easier to read the actual plugin code output @@ -139,7 +169,8 @@ export function plugingen({ const imports = dedent` import { type GetFunctionArgs, encodeFunctionData } from "viem"; import type { Plugin } from "./types"; - import type { BaseSmartContractAccount } from "@alchemy/aa-core"; + import type { MSCA } from "../builder"; + import type { ISmartContractAccount, ISmartAccountProvider, SupportedTransports } from "@alchemy/aa-core"; `; return { diff --git a/packages/accounts/src/index.ts b/packages/accounts/src/index.ts index f860dd2650..689e035613 100644 --- a/packages/accounts/src/index.ts +++ b/packages/accounts/src/index.ts @@ -35,7 +35,7 @@ export { StandardExecutor, type Executor, type Factory, - type MSCA, + type IMSCA as MSCA, type SignerMethods, } from "./msca/builder.js"; export { diff --git a/packages/accounts/src/msca/builder.ts b/packages/accounts/src/msca/builder.ts index dc6e5188af..950c703482 100644 --- a/packages/accounts/src/msca/builder.ts +++ b/packages/accounts/src/msca/builder.ts @@ -2,6 +2,7 @@ import { BaseSmartContractAccount, type BaseSmartAccountParams, type BatchUserOperationCallData, + type ISmartAccountProvider, type ISmartContractAccount, type SignTypedDataParams, type SupportedTransports, @@ -14,17 +15,34 @@ import { } from "viem"; import { z } from "zod"; import { IStandardExecutorAbi } from "./abis/IStandardExecutor.js"; +import { pluginManagerDecorator } from "./plugin-manager/decorator.js"; import type { Plugin } from "./plugins/types"; -export interface MSCA extends ISmartContractAccount { - extendWithPluginMethods: (plugin: Plugin) => this & D; +export interface IMSCA< + TTransport extends SupportedTransports = Transport, + TProviderDecorators = {} +> extends ISmartContractAccount { + providerDecorators: ( + p: ISmartAccountProvider + ) => TProviderDecorators; + + extendWithPluginMethods: ( + plugin: Plugin + ) => IMSCA & AD; + + addProviderDecorator: < + PD, + TProvider extends ISmartAccountProvider & { account: IMSCA } + >( + decorator: (p: TProvider) => PD + ) => IMSCA; } -export type Executor = ( +export type Executor = >( acct: A ) => Pick; -export type SignerMethods = ( +export type SignerMethods = >( acct: A ) => Pick< ISmartContractAccount, @@ -34,7 +52,7 @@ export type SignerMethods = ( | "getDummySignature" >; -export type Factory = (acct: A) => Promise; +export type Factory = >(acct: A) => Promise; // TODO: this can be moved out into its own file export const StandardExecutor: Executor = () => ({ @@ -92,11 +110,39 @@ export class MSCABuilder { build( params: BaseSmartAccountParams - ): MSCA { + ): IMSCA> { const builder = this; const { signer, executor, factory } = zCompleteBuilder.parse(builder); - return new (class extends BaseSmartContractAccount { + return new (class DynamicMSCA< + TProviderDecorators = ReturnType + > extends BaseSmartContractAccount { + providerDecorators_: (< + TProvider extends ISmartAccountProvider & { account: IMSCA } + >( + p: TProvider + ) => any)[] = [pluginManagerDecorator]; + + providerDecorators: ( + p: ISmartAccountProvider + ) => TProviderDecorators = (p) => { + if (!p.isConnected() && p.account !== this) { + throw new Error( + "provider should be connected if it is being decorated by the account" + ); + } + + return this.providerDecorators_.reduce( + (acc, decorator) => ({ + ...acc, + ...decorator( + p as ISmartAccountProvider & { account: IMSCA } + ), + }), + {} as TProviderDecorators + ); + }; + getDummySignature(): `0x${string}` { return signer(this).getDummySignature(); } @@ -131,9 +177,29 @@ export class MSCABuilder { return factory(this); } - extendWithPluginMethods = (plugin: Plugin): this & D => { + extendWithPluginMethods = ( + plugin: Plugin + ): DynamicMSCA & AD => { const methods = plugin.accountDecorators(this); - return Object.assign(this, methods); + const result = Object.assign(this, methods) as unknown as DynamicMSCA< + TProviderDecorators & PD + > & + AD; + result.providerDecorators_.push(plugin.providerDecorators); + + return result as unknown as DynamicMSCA & AD; + }; + + addProviderDecorator = < + PD, + TProvider extends ISmartAccountProvider & { account: IMSCA } + >( + decorator: (p: TProvider) => PD + ): DynamicMSCA => { + // @ts-expect-error this will be an error, but it's fine because we cast below + this.providerDecorators_.push(decorator); + + return this as unknown as DynamicMSCA; }; })(params); } diff --git a/packages/accounts/src/msca/multi-owner-account.ts b/packages/accounts/src/msca/multi-owner-account.ts index 3b12d5f4a9..62008ab0a8 100644 --- a/packages/accounts/src/msca/multi-owner-account.ts +++ b/packages/accounts/src/msca/multi-owner-account.ts @@ -120,5 +120,9 @@ export const createMultiOwnerMSCA = < const params = createMultiOwnerMSCASchema().parse(params_); const builder = createMultiOwnerMSCABuilder(params); - return builder.build(params).extendWithPluginMethods(MultiOwnerPlugin); + const account = builder + .build(params) + .extendWithPluginMethods(MultiOwnerPlugin); + + return account; }; diff --git a/packages/accounts/src/msca/plugin-manager/decorator.ts b/packages/accounts/src/msca/plugin-manager/decorator.ts index 1312cbf38c..0c8f267715 100644 --- a/packages/accounts/src/msca/plugin-manager/decorator.ts +++ b/packages/accounts/src/msca/plugin-manager/decorator.ts @@ -1,5 +1,5 @@ import type { ISmartAccountProvider } from "@alchemy/aa-core"; -import type { MSCA } from "../builder"; +import type { IMSCA } from "../builder"; import { installPlugin, type InstallPluginParams } from "./installPlugin.js"; import { uninstallPlugin, @@ -7,7 +7,7 @@ import { } from "./uninstallPlugin.js"; export const pluginManagerDecorator = < - P extends ISmartAccountProvider & { account: MSCA } + P extends ISmartAccountProvider & { account: IMSCA } >( provider: P ) => ({ diff --git a/packages/accounts/src/msca/plugin-manager/installPlugin.ts b/packages/accounts/src/msca/plugin-manager/installPlugin.ts index dc89340cad..f71736c973 100644 --- a/packages/accounts/src/msca/plugin-manager/installPlugin.ts +++ b/packages/accounts/src/msca/plugin-manager/installPlugin.ts @@ -8,7 +8,7 @@ import { } from "viem"; import { IPluginAbi } from "../abis/IPlugin.js"; import { IPluginManagerAbi } from "../abis/IPluginManager.js"; -import type { MSCA } from "../builder.js"; +import type { IMSCA } from "../builder.js"; import type { InjectedHook } from "./types"; export type InstallPluginParams = { @@ -20,7 +20,7 @@ export type InstallPluginParams = { }; export async function installPlugin< - P extends ISmartAccountProvider & { account: MSCA } + P extends ISmartAccountProvider & { account: IMSCA } >(provider: P, params: InstallPluginParams) { const pluginManifest = await provider.rpcClient.readContract({ abi: IPluginAbi, diff --git a/packages/accounts/src/msca/plugin-manager/uninstallPlugin.ts b/packages/accounts/src/msca/plugin-manager/uninstallPlugin.ts index 4a4b415f3b..139ff71ef5 100644 --- a/packages/accounts/src/msca/plugin-manager/uninstallPlugin.ts +++ b/packages/accounts/src/msca/plugin-manager/uninstallPlugin.ts @@ -1,7 +1,7 @@ import type { ISmartAccountProvider } from "@alchemy/aa-core"; import { encodeFunctionData, type Address, type Hash } from "viem"; import { IPluginManagerAbi } from "../abis/IPluginManager.js"; -import type { MSCA } from "../builder.js"; +import type { IMSCA } from "../builder.js"; export type UninstallPluginParams = { pluginAddress: Address; @@ -11,7 +11,7 @@ export type UninstallPluginParams = { }; export async function uninstallPlugin< - P extends ISmartAccountProvider & { account: MSCA } + P extends ISmartAccountProvider & { account: IMSCA } >(provider: P, params: UninstallPluginParams) { const callData = encodeFunctionData({ abi: IPluginManagerAbi, diff --git a/packages/accounts/src/msca/plugins/multi-owner.ts b/packages/accounts/src/msca/plugins/multi-owner.ts index 292ef2163a..4782486e63 100644 --- a/packages/accounts/src/msca/plugins/multi-owner.ts +++ b/packages/accounts/src/msca/plugins/multi-owner.ts @@ -1,6 +1,11 @@ -import { type GetFunctionArgs, encodeFunctionData } from "viem"; +import type { + ISmartAccountProvider, + ISmartContractAccount, + SupportedTransports, +} from "@alchemy/aa-core"; +import { encodeFunctionData, type GetFunctionArgs } from "viem"; +import type { IMSCA } from "../builder"; import type { Plugin } from "./types"; -import type { BaseSmartContractAccount } from "@alchemy/aa-core"; ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // ERC6900PluginGen: This file is auto-generated by plugingen @@ -11,7 +16,7 @@ const MultiOwnerPlugin_ = { name: "Multi Owner Plugin", version: "1.0.0", }, - accountDecorators: (account: BaseSmartContractAccount) => ({ + accountDecorators: (account: ISmartContractAccount) => ({ encodeUpdateOwnersData: ({ args, }: GetFunctionArgs< @@ -109,10 +114,32 @@ const MultiOwnerPlugin_ = { }); }, }), + providerDecorators: < + TTransport extends SupportedTransports, + P extends ISmartAccountProvider & { account: IMSCA } + >( + provider: P + ) => ({ + updateOwners: ({ + args, + }: GetFunctionArgs< + typeof MultiOwnerPluginExecutionFunctionAbi, + "updateOwners" + >) => { + const callData = encodeFunctionData({ + abi: MultiOwnerPluginExecutionFunctionAbi, + functionName: "updateOwners", + args, + }); + + return provider.sendUserOperation(callData); + }, + }), }; export const MultiOwnerPlugin: Plugin< - ReturnType<(typeof MultiOwnerPlugin_)["accountDecorators"]> + ReturnType<(typeof MultiOwnerPlugin_)["accountDecorators"]>, + ReturnType<(typeof MultiOwnerPlugin_)["providerDecorators"]> > = MultiOwnerPlugin_; export const MultiOwnerPluginExecutionFunctionAbi = [ diff --git a/packages/accounts/src/msca/plugins/types.ts b/packages/accounts/src/msca/plugins/types.ts index c48d636deb..bf71b93ea8 100644 --- a/packages/accounts/src/msca/plugins/types.ts +++ b/packages/accounts/src/msca/plugins/types.ts @@ -1,6 +1,17 @@ -import type { BaseSmartContractAccount } from "@alchemy/aa-core"; +import type { + ISmartAccountProvider, + ISmartContractAccount, + SupportedTransports, +} from "@alchemy/aa-core"; +import type { IMSCA } from "../builder"; -export interface Plugin { +export interface Plugin { meta: { name: string; version: string }; - accountDecorators: (a: BaseSmartContractAccount) => D; + accountDecorators: (a: ISmartContractAccount) => AD; + providerDecorators: < + TTransport extends SupportedTransports, + P extends ISmartAccountProvider & { account: IMSCA } + >( + p: P + ) => PD; } diff --git a/packages/core/src/account/base.ts b/packages/core/src/account/base.ts index 2ef721bf3f..7df57aaf2c 100644 --- a/packages/core/src/account/base.ts +++ b/packages/core/src/account/base.ts @@ -35,7 +35,7 @@ export enum DeploymentState { export abstract class BaseSmartContractAccount< TTransport extends SupportedTransports = Transport -> implements ISmartContractAccount +> implements ISmartContractAccount { protected factoryAddress: Address; protected deploymentState: DeploymentState = DeploymentState.UNDEFINED; diff --git a/packages/core/src/account/types.ts b/packages/core/src/account/types.ts index d47668d216..c39a1c9c8c 100644 --- a/packages/core/src/account/types.ts +++ b/packages/core/src/account/types.ts @@ -1,8 +1,9 @@ import type { Address } from "abitype"; -import type { Hash, Hex, Transport } from "viem"; +import type { Hash, Hex, HttpTransport, Transport } from "viem"; import type { SignTypedDataParameters } from "viem/accounts"; import type { z } from "zod"; -import type { SupportedTransports } from "../client/types"; +import type { PublicErc4337Client, SupportedTransports } from "../client/types"; +import type { ISmartAccountProvider } from "../provider/types"; import type { SmartAccountSigner } from "../signer/types"; import type { BatchUserOperationCallData } from "../types"; import type { @@ -20,7 +21,25 @@ export type SimpleSmartAccountParams< TTransport extends SupportedTransports = Transport > = z.infer>>; -export interface ISmartContractAccount { +export interface ISmartContractAccount< + TTransport extends SupportedTransports = Transport +> { + /** + * The RPC provider the account uses to make RPC calls + */ + readonly rpcProvider: + | PublicErc4337Client + | PublicErc4337Client; + + /** + * Optional property that will be used to augment the provider on connect with methods (leveraging the provider's extend method) + * + * @param provider - the provider being connected + * @returns an object with methods that will be added to the provider + */ + providerDecorators?:

>( + provider: P + ) => unknown; /** * @returns the init code for the account */ diff --git a/packages/core/src/provider/base.ts b/packages/core/src/provider/base.ts index b6117fcfe8..013668e506 100644 --- a/packages/core/src/provider/base.ts +++ b/packages/core/src/provider/base.ts @@ -45,6 +45,8 @@ import { isValidRequest, resolveProperties, type Deferrable, + type IsUndefined, + type NoUndefined, } from "../utils/index.js"; import { createSmartAccountProviderConfigSchema } from "./schema.js"; import type { @@ -651,7 +653,11 @@ export class SmartAccountProvider< | PublicErc4337Client | PublicErc4337Client ) => TAccount - ): this & { account: TAccount } => { + ): IsUndefined extends true + ? this & { account: TAccount } + : this & { account: TAccount } & ReturnType< + NoUndefined + > => { const account = fn(this.rpcClient); // sanity check. Note that this check is only performed if and only if the optional entryPointAddress is given upon initialization. @@ -698,7 +704,17 @@ export class SmartAccountProvider< .getAddress() .then((address) => this.emit("accountsChanged", [address])); - return this as unknown as this & { account: TAccount }; + if (account.providerDecorators) { + this.extend(account.providerDecorators); + } + + return this as unknown as IsUndefined< + TAccount["providerDecorators"] + > extends true + ? this & { account: TAccount } + : this & { account: TAccount } & ReturnType< + NoUndefined + >; }; disconnect = (): this & { account: undefined } => { diff --git a/packages/core/src/provider/types.ts b/packages/core/src/provider/types.ts index a02f1b608a..d15e7fa207 100644 --- a/packages/core/src/provider/types.ts +++ b/packages/core/src/provider/types.ts @@ -26,7 +26,7 @@ import type { UserOperationResponse, UserOperationStruct, } from "../types.js"; -import type { Deferrable } from "../utils"; +import type { Deferrable, IsUndefined, NoUndefined } from "../utils"; import type { SmartAccountProviderOptsSchema, createSmartAccountProviderConfigSchema, @@ -356,7 +356,11 @@ export interface ISmartAccountProvider< | PublicErc4337Client | PublicErc4337Client ) => TAccount - ): this & { account: TAccount }; + ): IsUndefined extends true + ? this & { account: TAccount } + : this & { account: TAccount } & ReturnType< + NoUndefined + >; /** * Allows for disconnecting the account from the provider so you can connect the provider to another account instance diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 75a835e7b2..f976e9753b 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -186,6 +186,25 @@ export function pick(obj: Record, keys: string | string[]) { .filter((k) => keys.includes(k)) .reduce((res, k) => Object.assign(res, { [k]: obj[k] }), {}); } +// borrowed from viem +/** + * @description Constructs a type by excluding `undefined` from `T`. + * + * @example + * NoUndefined + * => string + */ +export type NoUndefined = T extends undefined ? never : T; + +// borrowed from viem +/** + * @description Checks if {@link T} is `undefined` + * @param T - Type to check + * @example + * type Result = IsUndefined + * // ^? type Result = true + */ +export type IsUndefined = [undefined] extends [T] ? true : false; export * from "./bigint.js"; export * from "./defaults.js";