Skip to content

Commit

Permalink
feat: extend msca account with account loupe decorators (#302)
Browse files Browse the repository at this point in the history
* refactor: move required by types to aa-core utils types folder

* feat: extend msca account with account loupe decorators

* feat: move extend method on account from msca to base account

* Update packages/core/src/account/types.ts

Co-authored-by: Michael Moldoveanu <[email protected]>

* Update packages/core/src/account/types.ts

Co-authored-by: Michael Moldoveanu <[email protected]>

* Update packages/core/src/account/types.ts

Co-authored-by: Michael Moldoveanu <[email protected]>

* Update packages/accounts/src/msca/account-loupe/decorator.ts

Co-authored-by: Michael Moldoveanu <[email protected]>

* fix: fix lint and build

---------

Co-authored-by: Michael Moldoveanu <[email protected]>
  • Loading branch information
denniswon and moldy530 authored Dec 5, 2023
1 parent f65970e commit 1e6cf6c
Show file tree
Hide file tree
Showing 11 changed files with 314 additions and 28 deletions.
6 changes: 1 addition & 5 deletions packages/accounts/scripts/plugingen.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { RequiredBy } from "@alchemy/aa-core";
import { type Plugin } from "@wagmi/cli";
import { camelCase, pascalCase } from "change-case";
import dedent from "dedent";
Expand All @@ -11,11 +12,6 @@ import {
} from "viem";
import { IPluginAbi } from "../src/msca/abis/IPlugin.js";

type RequiredBy<TType, TKeys extends keyof TType> = Required<
Pick<TType, TKeys>
> &
Omit<TType, TKeys>;

export function plugingen({
chain,
rpcUrl,
Expand Down
142 changes: 142 additions & 0 deletions packages/accounts/src/msca/abis/IAccountLoupe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
export const IAccountLoupeAbi = [
{
inputs: [
{
internalType: "bytes4",
name: "selector",
type: "bytes4",
},
],
name: "getExecutionFunctionConfig",
outputs: [
{
components: [
{
internalType: "address",
name: "plugin",
type: "address",
},
{
internalType: "FunctionReference",
name: "userOpValidationFunction",
type: "bytes21",
},
{
internalType: "FunctionReference",
name: "runtimeValidationFunction",
type: "bytes21",
},
],
internalType: "struct IAccountLoupe.ExecutionFunctionConfig",
name: "",
type: "tuple",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
internalType: "bytes4",
name: "selector",
type: "bytes4",
},
],
name: "getExecutionHooks",
outputs: [
{
components: [
{
internalType: "FunctionReference",
name: "preExecHook",
type: "bytes21",
},
{
internalType: "FunctionReference",
name: "postExecHook",
type: "bytes21",
},
],
internalType: "struct IAccountLoupe.ExecutionHooks[]",
name: "",
type: "tuple[]",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "getInstalledPlugins",
outputs: [
{
internalType: "address[]",
name: "",
type: "address[]",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
internalType: "address",
name: "callingPlugin",
type: "address",
},
{
internalType: "bytes4",
name: "selector",
type: "bytes4",
},
],
name: "getPermittedCallHooks",
outputs: [
{
components: [
{
internalType: "FunctionReference",
name: "preExecHook",
type: "bytes21",
},
{
internalType: "FunctionReference",
name: "postExecHook",
type: "bytes21",
},
],
internalType: "struct IAccountLoupe.ExecutionHooks[]",
name: "",
type: "tuple[]",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
internalType: "bytes4",
name: "selector",
type: "bytes4",
},
],
name: "getPreValidationHooks",
outputs: [
{
internalType: "FunctionReference[]",
name: "preUserOpValidationHooks",
type: "bytes21[]",
},
{
internalType: "FunctionReference[]",
name: "preRuntimeValidationHooks",
type: "bytes21[]",
},
],
stateMutability: "view",
type: "function",
},
] as const;
48 changes: 48 additions & 0 deletions packages/accounts/src/msca/account-loupe/decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { Address } from "@alchemy/aa-core";
import type { Hash } from "viem";
import { IAccountLoupeAbi } from "../abis/IAccountLoupe.js";
import type { IMSCA } from "../builder.js";
import type { FunctionReference, IAccountLoupe } from "./types.js";

export const accountLoupeDecorators = (
account: IMSCA<any, any>
): IAccountLoupe => ({
getExecutionFunctionConfig: async (selector: FunctionReference) =>
account.rpcProvider.readContract({
address: await account.getAddress(),
abi: IAccountLoupeAbi,
functionName: "getExecutionFunctionConfig",
args: [selector],
}),

getExecutionHooks: async (selector: FunctionReference) =>
account.rpcProvider.readContract({
address: await account.getAddress(),
abi: IAccountLoupeAbi,
functionName: "getExecutionHooks",
args: [selector],
}),

getPermittedCallHooks: async (callingPlugin: Address, selector: Hash) =>
account.rpcProvider.readContract({
address: await account.getAddress(),
abi: IAccountLoupeAbi,
functionName: "getPermittedCallHooks",
args: [callingPlugin, selector],
}),

getPreValidationHooks: async (selector: Hash) =>
account.rpcProvider.readContract({
address: await account.getAddress(),
abi: IAccountLoupeAbi,
functionName: "getPreValidationHooks",
args: [selector],
}),

getInstalledPlugins: async () =>
account.rpcProvider.readContract({
address: await account.getAddress(),
abi: IAccountLoupeAbi,
functionName: "getInstalledPlugins",
}),
});
56 changes: 56 additions & 0 deletions packages/accounts/src/msca/account-loupe/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { Address, Hash, Hex } from "viem";

// Treats the first 20 bytes as an address, and the last byte as a identifier.
export type FunctionReference = Hex;

export type ExecutionFunctionConfig = {
plugin: Address;
userOpValidationFunction: FunctionReference;
runtimeValidationFunction: FunctionReference;
};

export type ExecutionHooks = {
preExecHook: FunctionReference;
postExecHook: FunctionReference;
};

export type PreValidationHooks = [
readonly FunctionReference[],
readonly FunctionReference[]
];

export interface IAccountLoupe {
/// @notice Gets the validation functions and plugin address for a selector
/// @dev If the selector is a native function, the plugin address will be the address of the account
/// @param selector The selector to get the configuration for
/// @return The configuration for this selector
getExecutionFunctionConfig(
selector: FunctionReference
): Promise<ExecutionFunctionConfig>;

/// @notice Gets the pre and post execution hooks for a selector
/// @param selector The selector to get the hooks for
/// @return The pre and post execution hooks for this selector
getExecutionHooks(
selector: FunctionReference
): Promise<ReadonlyArray<ExecutionHooks>>;

/// @notice Gets the pre and post permitted call hooks applied for a plugin calling this selector
/// @param callingPlugin The plugin that is calling the selector
/// @param selector The selector the plugin is calling
/// @return The pre and post permitted call hooks for this selector
getPermittedCallHooks(
callingPlugin: Address,
selector: Hash
): Promise<ReadonlyArray<ExecutionHooks>>;

/// @notice Gets the pre user op and runtime validation hooks associated with a selector
/// @param selector The selector to get the hooks for
/// @return preUserOpValidationHooks The pre user op validation hooks for this selector
/// @return preRuntimeValidationHooks The pre runtime validation hooks for this selector
getPreValidationHooks(selector: Hash): Promise<Readonly<PreValidationHooks>>;

/// @notice Gets an array of all installed plugins
/// @return The addresses of all installed plugins
getInstalledPlugins(): Promise<ReadonlyArray<Address>>;
}
4 changes: 3 additions & 1 deletion packages/accounts/src/msca/multi-owner-account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from "viem";
import { z } from "zod";
import { MultiOwnerMSCAFactoryAbi } from "./abis/MultiOwnerMSCAFactory.js";
import { accountLoupeDecorators } from "./account-loupe/decorator.js";
import { MSCABuilder, StandardExecutor } from "./builder.js";
import { MultiOwnerPlugin } from "./plugins/multi-owner.js";

Expand Down Expand Up @@ -122,7 +123,8 @@ export const createMultiOwnerMSCA = <

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

return account;
};
9 changes: 9 additions & 0 deletions packages/core/src/account/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,15 @@ export abstract class BaseSmartContractAccount<
return this.accountAddress;
}

extend = <R>(fn: (self: this) => R): this & R => {
const extended = fn(this) as any;
// this should make it so extensions can't overwrite the base methods
for (const key in this) {
delete extended[key];
}
return Object.assign(this, extended);
};

getOwner(): SmartAccountSigner | undefined {
return this.owner;
}
Expand Down
27 changes: 27 additions & 0 deletions packages/core/src/account/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,31 @@ export interface ISmartContractAccount<
* @returns the address of the entry point contract for the smart account
*/
getEntryPointAddress(): Address;

/**
* Allows you to add additional functionality and utility methods to this account
* via a decorator pattern.
*
* NOTE: this method does not allow you to override existing methods on the account.
*
* @example
* ```ts
* const account = new BaseSmartCobntractAccount(...).extend((account) => ({
* readAccountState: async (...args) => {
* return this.rpcProvider.readContract({
* address: await this.getAddress(),
* abi: ThisContractsAbi
* args: args
* });
* }
* }));
*
* account.debugSendUserOperation(...);
* ```
*
* @param extendFn -- this function gives you access to the created account instance and returns an object
* with the extension methods
* @returns -- the account with the extension methods added
*/
extend: <R>(extendFn: (self: this) => R) => this & R;
}
3 changes: 1 addition & 2 deletions packages/core/src/provider/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,8 @@ import {
isValidRequest,
resolveProperties,
type Deferrable,
type IsUndefined,
type NoUndefined,
} from "../utils/index.js";
import { type IsUndefined, type NoUndefined } from "../utils/types.js";
import { createSmartAccountProviderConfigSchema } from "./schema.js";
import type {
AccountMiddlewareFn,
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/provider/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import type {
UserOperationResponse,
UserOperationStruct,
} from "../types.js";
import type { Deferrable, IsUndefined, NoUndefined } from "../utils";
import type { Deferrable } from "../utils";
import type { IsUndefined, NoUndefined } from "../utils/types.js";
import type {
SmartAccountProviderOptsSchema,
createSmartAccountProviderConfigSchema,
Expand Down
20 changes: 1 addition & 19 deletions packages/core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,27 +186,9 @@ export function pick(obj: Record<string, unknown>, 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 | undefined>
* => string
*/
export type NoUndefined<T> = T extends undefined ? never : T;

// borrowed from viem
/**
* @description Checks if {@link T} is `undefined`
* @param T - Type to check
* @example
* type Result = IsUndefined<undefined>
* // ^? type Result = true
*/
export type IsUndefined<T> = [undefined] extends [T] ? true : false;

export * from "./bigint.js";
export * from "./defaults.js";
export * from "./schema.js";
export type * from "./types.js";
export * from "./userop.js";
Loading

0 comments on commit 1e6cf6c

Please sign in to comment.