Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add switching for executeUserOp #1223

Merged
merged 1 commit into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 121 additions & 5 deletions account-kit/smart-contracts/src/ma-v2/account/semiModularAccountV2.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,78 @@
import type {
EntryPointDef,
SmartAccountSigner,
AccountOp,
SmartContractAccountWithSigner,
ToSmartContractAccountParams,
} from "@aa-sdk/core";
import {
createBundlerClient,
getEntryPoint,
toSmartContractAccount,
InvalidEntityIdError,
InvalidNonceKeyError,
} from "@aa-sdk/core";
import {
concatHex,
encodeFunctionData,
getContract,
maxUint32,
maxUint152,
zeroAddress,
type Address,
type Chain,
type Hex,
type Transport,
} from "viem";
import { accountFactoryAbi } from "../abis/accountFactoryAbi.js";
import { getDefaultMAV2FactoryAddress } from "../utils.js";
import { standardExecutor } from "../../msca/account/standardExecutor.js";
import {
getDefaultMAV2FactoryAddress,
DEFAULT_OWNER_ENTITY_ID,
} from "../utils.js";
import { singleSignerMessageSigner } from "../modules/single-signer-validation/signer.js";
import { InvalidEntityIdError, InvalidNonceKeyError } from "@aa-sdk/core";
import { modularAccountAbi } from "../abis/modularAccountAbi.js";
import { serializeModuleEntity } from "../actions/common/utils.js";

export const DEFAULT_OWNER_ENTITY_ID = 0;
const executeUserOpSelector: Hex = "0x8DD7712F";

export type SignerEntity = {
isGlobalValidation: boolean;
entityId: number;
};

export type ExecutionDataView = {
module: Address;
skipRuntimeValidation: boolean;
allowGlobalValidation: boolean;
executionHooks: readonly Hex[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there are reason some fields are in this type and the next are readonly and others are not?

As personal style, I don't use readonly on data structs. IMO it doesn't meaningfully prevent mutations if you rely on that for correctness, and just makes things inconvenient otherwise. For example, the following typechecks:

type Circle = { readonly r: number };

function useCircle(circle: Circle) {
  // Do something with circle that would break
  // if it later changes.
}

const circle = { r: 10 };
useCircle(circle);
circle.r = 11;

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

without the readonly I get a type error, it seems like the return data from the view call is readonly for all Hex types

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a way to remove the readonlys?

};

export type ValidationDataView = {
validationHooks: readonly Hex[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Especially, I think marking arrays as readonly isn't very helpful, because this doesn't stop the contents of the array from changing; it only stops the variable from being reassigned.

executionHooks: readonly Hex[];
selectors: readonly Hex[];
validationFlags: number;
};

export type ValidationDataParams =
| {
validationModuleAddress: Address;
entityId?: never;
}
| {
validationModuleAddress?: never;
entityId: number;
};

export type SMAV2Account<
TSigner extends SmartAccountSigner = SmartAccountSigner
> = SmartContractAccountWithSigner<"SMAV2Account", TSigner, "0.7.0"> & {
signerEntity: SignerEntity;
getExecutionData: (selector: Hex) => Promise<ExecutionDataView>;
getValidationData: (
args: ValidationDataParams
) => Promise<ValidationDataView>;
encodeCallData: (callData: Hex) => Promise<Hex>;
};

export type CreateSMAV2AccountParams<
Expand Down Expand Up @@ -110,14 +146,43 @@ export async function createSMAV2Account(
]);
};

const encodeExecute: (tx: AccountOp) => Promise<Hex> = async ({
target,
data,
value,
}) =>
await encodeCallData(
encodeFunctionData({
abi: modularAccountAbi,
functionName: "execute",
args: [target, value ?? 0n, data],
})
);

const encodeBatchExecute: (txs: AccountOp[]) => Promise<Hex> = async (txs) =>
await encodeCallData(
encodeFunctionData({
abi: modularAccountAbi,
functionName: "executeBatch",
args: [
txs.map((tx) => ({
target: tx.target,
data: tx.data,
value: tx.value ?? 0n,
})),
],
})
);

const baseAccount = await toSmartContractAccount({
transport,
chain,
entryPoint,
accountAddress,
source: `SMAV2Account`,
encodeExecute,
encodeBatchExecute,
getAccountInitCode,
...standardExecutor,
...singleSignerMessageSigner(signer),
});

Expand All @@ -144,10 +209,61 @@ export async function createSMAV2Account(
]) as Promise<bigint>;
};

const accountContract = getContract({
address: baseAccount.address,
abi: modularAccountAbi,
client,
});

const getExecutionData = async (selector: Hex) => {
if (!(await baseAccount.isAccountDeployed())) {
return {
module: zeroAddress,
skipRuntimeValidation: false,
allowGlobalValidation: false,
executionHooks: [],
};
}

return await accountContract.read.getExecutionData([selector]);
};

const getValidationData = async (args: ValidationDataParams) => {
if (!(await baseAccount.isAccountDeployed())) {
return {
validationHooks: [],
executionHooks: [],
selectors: [],
validationFlags: 0,
};
}

const { validationModuleAddress, entityId } = args;
return await accountContract.read.getValidationData([
serializeModuleEntity({
moduleAddress: validationModuleAddress ?? zeroAddress,
entityId: entityId ?? Number(maxUint32),
}),
]);
};

const encodeCallData = async (callData: Hex): Promise<Hex> => {
const validationData = await getValidationData({
entityId: Number(entityId),
});

return validationData.executionHooks.length
? concatHex([executeUserOpSelector, callData])
: callData;
};

return {
...baseAccount,
getAccountNonce,
getSigner: () => signer,
signerEntity,
getExecutionData,
getValidationData,
encodeCallData,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@moldy530 info about drilling into actions

};
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,12 @@ import {
IncompatibleClientError,
isSmartAccountClient,
EntityIdOverrideError,
type GetAccountParameter,
type GetEntryPointFromAccount,
type SendUserOperationResult,
type SmartContractAccount,
type SmartAccountClient,
type UserOperationOverridesParameter,
type SmartAccountSigner,
} from "@aa-sdk/core";
import {
type Address,
type Hex,
type Chain,
type Transport,
encodeFunctionData,
concatHex,
} from "viem";
import { type Address, type Hex, encodeFunctionData, concatHex } from "viem";

import { semiModularAccountBytecodeAbi } from "../../abis/semiModularAccountBytecodeAbi.js";
import type { HookConfig, ValidationConfig } from "../common/types.js";
Expand All @@ -27,10 +18,12 @@ import {
serializeModuleEntity,
} from "../common/utils.js";

import { type SMAV2AccountClient } from "../../client/client.js";
import { type SMAV2Account } from "../../account/semiModularAccountV2.js";
import { DEFAULT_OWNER_ENTITY_ID } from "../../utils.js";

export type InstallValidationParams<
TAccount extends SmartContractAccount | undefined =
| SmartContractAccount
| undefined
TSigner extends SmartAccountSigner = SmartAccountSigner
> = {
validationConfig: ValidationConfig;
selectors: Hex[];
Expand All @@ -39,41 +32,39 @@ export type InstallValidationParams<
hookConfig: HookConfig;
initData: Hex;
}[];
} & UserOperationOverridesParameter<GetEntryPointFromAccount<TAccount>> &
GetAccountParameter<TAccount>;
account?: SMAV2Account<TSigner> | undefined;
} & UserOperationOverridesParameter<
GetEntryPointFromAccount<SMAV2Account<TSigner>>
>;

export type UninstallValidationParams<
TAccount extends SmartContractAccount | undefined =
| SmartContractAccount
| undefined
TSigner extends SmartAccountSigner = SmartAccountSigner
> = {
moduleAddress: Address;
entityId: number;
uninstallData: Hex;
hookUninstallDatas: Hex[];
} & UserOperationOverridesParameter<GetEntryPointFromAccount<TAccount>> &
GetAccountParameter<TAccount>;
account?: SMAV2Account<TSigner> | undefined;
} & UserOperationOverridesParameter<
GetEntryPointFromAccount<SMAV2Account<TSigner>>
>;

export type InstallValidationActions<
TAccount extends SmartContractAccount | undefined =
| SmartContractAccount
| undefined
TSigner extends SmartAccountSigner = SmartAccountSigner
> = {
installValidation: (
args: InstallValidationParams<TAccount>
args: InstallValidationParams<TSigner>
) => Promise<SendUserOperationResult>;
uninstallValidation: (
args: UninstallValidationParams<TAccount>
args: UninstallValidationParams<TSigner>
) => Promise<SendUserOperationResult>;
};

export const installValidationActions: <
TTransport extends Transport = Transport,
TChain extends Chain | undefined = Chain | undefined,
TAccount extends SmartContractAccount = SmartContractAccount
TSigner extends SmartAccountSigner = SmartAccountSigner
>(
client: SmartAccountClient<TTransport, TChain, TAccount>
) => InstallValidationActions<TAccount> = (client) => ({
client: SMAV2AccountClient<TSigner>
) => InstallValidationActions<TSigner> = (client) => ({
installValidation: async ({
validationConfig,
selectors,
Expand All @@ -94,22 +85,26 @@ export const installValidationActions: <
);
}

if (validationConfig.entityId === 0) {
if (validationConfig.entityId === DEFAULT_OWNER_ENTITY_ID) {
throw new EntityIdOverrideError();
}

const callData = encodeFunctionData({
abi: semiModularAccountBytecodeAbi,
functionName: "installValidation",
args: [
serializeValidationConfig(validationConfig),
selectors,
installData,
hooks.map((hook: { hookConfig: HookConfig; initData: Hex }) =>
concatHex([serializeHookConfig(hook.hookConfig), hook.initData])
),
],
});
const { encodeCallData } = account;

const callData = await encodeCallData(
encodeFunctionData({
abi: semiModularAccountBytecodeAbi,
functionName: "installValidation",
args: [
serializeValidationConfig(validationConfig),
selectors,
installData,
hooks.map((hook: { hookConfig: HookConfig; initData: Hex }) =>
concatHex([serializeHookConfig(hook.hookConfig), hook.initData])
),
],
})
);

return client.sendUserOperation({
uo: callData,
Expand Down Expand Up @@ -138,18 +133,22 @@ export const installValidationActions: <
);
}

const callData = encodeFunctionData({
abi: semiModularAccountBytecodeAbi,
functionName: "uninstallValidation",
args: [
serializeModuleEntity({
moduleAddress,
entityId,
}),
uninstallData,
hookUninstallDatas,
],
});
const { encodeCallData } = account;

const callData = await encodeCallData(
encodeFunctionData({
abi: semiModularAccountBytecodeAbi,
functionName: "uninstallValidation",
args: [
serializeModuleEntity({
moduleAddress,
entityId,
}),
uninstallData,
hookUninstallDatas,
],
})
);

return client.sendUserOperation({
uo: callData,
Expand Down
7 changes: 2 additions & 5 deletions account-kit/smart-contracts/src/ma-v2/client/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { custom, parseEther, publicActions } from "viem";

import { LocalAccountSigner, type SmartAccountSigner } from "@aa-sdk/core";

import { createSMAV2AccountClient } from "./client.js";

import { createSMAV2AccountClient, type SMAV2AccountClient } from "./client.js";

Check warning on line 3 in account-kit/smart-contracts/src/ma-v2/client/client.test.ts

View workflow job for this annotation

GitHub Actions / Lint

'SMAV2AccountClient' is defined but never used
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] <@typescript-eslint/no-unused-vars> reported by reviewdog 🐶
'SMAV2AccountClient' is defined but never used.

import { local070Instance } from "~test/instances.js";
import { setBalance } from "viem/actions";
import { accounts } from "~test/constants.js";
import { installValidationActions } from "../actions/install-validation/installValidation.js";
import { getDefaultSingleSignerValidationModuleAddress } from "../modules/utils.js";
import { SingleSignerValidationModule } from "../modules/single-signer-validation/module.js";
import { installValidationActions } from "../actions/install-validation/installValidation.js";

describe("MA v2 Tests", async () => {
const instance = local070Instance;
Expand Down
Loading
Loading