Skip to content

Commit

Permalink
feat: add a mechanism for adding provider decorators to accounts (#287)
Browse files Browse the repository at this point in the history
* feat: add a mechanism for adding provider decorators to accounts

* chore: update lerna and package json to lint generated files

* refactor: rename MSCA to IMSCA
  • Loading branch information
moldy530 committed Dec 1, 2023
1 parent 39690c8 commit eb454b6
Show file tree
Hide file tree
Showing 18 changed files with 244 additions and 43 deletions.
4 changes: 2 additions & 2 deletions examples/aa-simple-dapp/src/hooks/useAlchemyProvider.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion nx.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@
},
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"dependsOn": ["^build", "generate"],
"outputs": ["{projectRoot}/dist"]
},
"test": {
"dependsOn": ["build"]
},
"generate": {
"outputs": ["{projectRoot}/src/plugins"]
}
}
}
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 0 additions & 1 deletion packages/accounts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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\"}'",
Expand Down
43 changes: 37 additions & 6 deletions packages/accounts/scripts/plugingen.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<typeof ${executionAbiConst}, "${n.name}">`
: "";
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<TTransport> & { account: MSCA<TTransport> }
>(
provider: P
) => ({ ${providerFunctions.join(",\n\n")} }),
}
export const ${contract.name}: Plugin<ReturnType<typeof ${
contract.name
}_["accountDecorators"]>> = ${contract.name}_;
}_["accountDecorators"]>, ReturnType<typeof ${
contract.name
}_["providerDecorators"]>> = ${contract.name}_;
`);

// add the abi at the end so it's easier to read the actual plugin code output
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion packages/accounts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export {
StandardExecutor,
type Executor,
type Factory,
type MSCA,
type IMSCA as MSCA,
type SignerMethods,
} from "./msca/builder.js";
export {
Expand Down
84 changes: 75 additions & 9 deletions packages/accounts/src/msca/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
BaseSmartContractAccount,
type BaseSmartAccountParams,
type BatchUserOperationCallData,
type ISmartAccountProvider,
type ISmartContractAccount,
type SignTypedDataParams,
type SupportedTransports,
Expand All @@ -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: <D>(plugin: Plugin<D>) => this & D;
export interface IMSCA<
TTransport extends SupportedTransports = Transport,
TProviderDecorators = {}
> extends ISmartContractAccount {
providerDecorators: (
p: ISmartAccountProvider<TTransport>
) => TProviderDecorators;

extendWithPluginMethods: <AD, PD>(
plugin: Plugin<AD, PD>
) => IMSCA<TTransport, TProviderDecorators & PD> & AD;

addProviderDecorator: <
PD,
TProvider extends ISmartAccountProvider<TTransport> & { account: IMSCA }
>(
decorator: (p: TProvider) => PD
) => IMSCA<TTransport, TProviderDecorators & PD>;
}

export type Executor = <A extends MSCA>(
export type Executor = <A extends IMSCA<any, any>>(
acct: A
) => Pick<ISmartContractAccount, "encodeExecute" | "encodeBatchExecute">;

export type SignerMethods = <A extends MSCA>(
export type SignerMethods = <A extends IMSCA<any, any>>(
acct: A
) => Pick<
ISmartContractAccount,
Expand All @@ -34,7 +52,7 @@ export type SignerMethods = <A extends MSCA>(
| "getDummySignature"
>;

export type Factory = <A extends MSCA>(acct: A) => Promise<Hex>;
export type Factory = <A extends IMSCA<any, any>>(acct: A) => Promise<Hex>;

// TODO: this can be moved out into its own file
export const StandardExecutor: Executor = () => ({
Expand Down Expand Up @@ -92,11 +110,39 @@ export class MSCABuilder {

build<TTransport extends SupportedTransports = Transport>(
params: BaseSmartAccountParams
): MSCA {
): IMSCA<TTransport, ReturnType<typeof pluginManagerDecorator>> {
const builder = this;
const { signer, executor, factory } = zCompleteBuilder.parse(builder);

return new (class extends BaseSmartContractAccount<TTransport> {
return new (class DynamicMSCA<
TProviderDecorators = ReturnType<typeof pluginManagerDecorator>
> extends BaseSmartContractAccount<TTransport> {
providerDecorators_: (<
TProvider extends ISmartAccountProvider<TTransport> & { account: IMSCA }
>(
p: TProvider
) => any)[] = [pluginManagerDecorator];

providerDecorators: (
p: ISmartAccountProvider<TTransport>
) => 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<TTransport> & { account: IMSCA }
),
}),
{} as TProviderDecorators
);
};

getDummySignature(): `0x${string}` {
return signer(this).getDummySignature();
}
Expand Down Expand Up @@ -131,9 +177,29 @@ export class MSCABuilder {
return factory(this);
}

extendWithPluginMethods = <D>(plugin: Plugin<D>): this & D => {
extendWithPluginMethods = <AD, PD>(
plugin: Plugin<AD, PD>
): DynamicMSCA<TProviderDecorators & PD> & 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<TProviderDecorators & PD> & AD;
};

addProviderDecorator = <
PD,
TProvider extends ISmartAccountProvider<TTransport> & { account: IMSCA }
>(
decorator: (p: TProvider) => PD
): DynamicMSCA<TProviderDecorators & PD> => {
// @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<TProviderDecorators & PD>;
};
})(params);
}
Expand Down
6 changes: 5 additions & 1 deletion packages/accounts/src/msca/multi-owner-account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,5 +120,9 @@ export const createMultiOwnerMSCA = <
const params = createMultiOwnerMSCASchema<TTransport>().parse(params_);
const builder = createMultiOwnerMSCABuilder<TTransport>(params);

return builder.build(params).extendWithPluginMethods(MultiOwnerPlugin);
const account = builder
.build(params)
.extendWithPluginMethods(MultiOwnerPlugin);

return account;
};
4 changes: 2 additions & 2 deletions packages/accounts/src/msca/plugin-manager/decorator.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
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,
type UninstallPluginParams,
} from "./uninstallPlugin.js";

export const pluginManagerDecorator = <
P extends ISmartAccountProvider & { account: MSCA }
P extends ISmartAccountProvider & { account: IMSCA }
>(
provider: P
) => ({
Expand Down
4 changes: 2 additions & 2 deletions packages/accounts/src/msca/plugin-manager/installPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions packages/accounts/src/msca/plugin-manager/uninstallPlugin.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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,
Expand Down
35 changes: 31 additions & 4 deletions packages/accounts/src/msca/plugins/multi-owner.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -11,7 +16,7 @@ const MultiOwnerPlugin_ = {
name: "Multi Owner Plugin",
version: "1.0.0",
},
accountDecorators: (account: BaseSmartContractAccount) => ({
accountDecorators: (account: ISmartContractAccount) => ({
encodeUpdateOwnersData: ({
args,
}: GetFunctionArgs<
Expand Down Expand Up @@ -109,10 +114,32 @@ const MultiOwnerPlugin_ = {
});
},
}),
providerDecorators: <
TTransport extends SupportedTransports,
P extends ISmartAccountProvider<TTransport> & { account: IMSCA<TTransport> }
>(
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 = [
Expand Down
Loading

0 comments on commit eb454b6

Please sign in to comment.