@alchemy/aa-core
Version:
viem based SDK that enables interactions with ERC-4337 Smart Accounts. ABIs are based off the definitions generated in @account-abstraction/contracts
407 lines (360 loc) • 11.9 kB
text/typescript
import {
getContract,
hexToBytes,
trim,
type Address,
type Chain,
type CustomSource,
type Hex,
type LocalAccount,
type PublicClient,
type SignableMessage,
type Transport,
type TypedData,
type TypedDataDefinition,
} from "viem";
import { toAccount } from "viem/accounts";
import { createBundlerClient } from "../client/bundlerClient.js";
import type {
EntryPointDef,
EntryPointRegistryBase,
EntryPointVersion,
} from "../entrypoint/types.js";
import {
BatchExecutionNotSupportedError,
FailedToGetStorageSlotError,
GetCounterFactualAddressError,
SignTransactionNotSupportedError,
UpgradesNotSupportedError,
} from "../errors/account.js";
import { InvalidRpcUrlError } from "../errors/client.js";
import { InvalidEntryPointError } from "../errors/entrypoint.js";
import { Logger } from "../logger.js";
import type { SmartAccountSigner } from "../signer/types.js";
import { wrapSignatureWith6492 } from "../signer/utils.js";
import type { NullAddress } from "../types.js";
import type { IsUndefined } from "../utils/types.js";
import { DeploymentState } from "./base.js";
export type AccountOp = {
target: Address;
value?: bigint;
data: Hex | "0x";
};
export type GetEntryPointFromAccount<
TAccount extends SmartContractAccount | undefined,
TAccountOverride extends SmartContractAccount = SmartContractAccount
> = GetAccountParameter<
TAccount,
TAccountOverride
> extends SmartContractAccount<string, infer TEntryPointVersion>
? TEntryPointVersion
: EntryPointVersion;
export type GetAccountParameter<
TAccount extends SmartContractAccount | undefined =
| SmartContractAccount
| undefined,
TAccountOverride extends SmartContractAccount = SmartContractAccount
> = IsUndefined<TAccount> extends true
? { account: TAccountOverride }
: { account?: TAccountOverride };
export type UpgradeToAndCallParams = {
upgradeToAddress: Address;
upgradeToInitData: Hex;
};
export type SmartContractAccountWithSigner<
Name extends string = string,
TSigner extends SmartAccountSigner = SmartAccountSigner,
TEntryPointVersion extends EntryPointVersion = EntryPointVersion
> = SmartContractAccount<Name, TEntryPointVersion> & {
getSigner: () => TSigner;
};
export const isSmartAccountWithSigner = (
account: SmartContractAccount
): account is SmartContractAccountWithSigner => {
return "getSigner" in account;
};
// [!region SmartContractAccount]
export type SmartContractAccount<
Name extends string = string,
TEntryPointVersion extends EntryPointVersion = EntryPointVersion
> = LocalAccount<Name> & {
source: Name;
getDummySignature: () => Hex | Promise<Hex>;
encodeExecute: (tx: AccountOp) => Promise<Hex>;
encodeBatchExecute: (txs: AccountOp[]) => Promise<Hex>;
signUserOperationHash: (uoHash: Hex) => Promise<Hex>;
signMessageWith6492: (params: { message: SignableMessage }) => Promise<Hex>;
signTypedDataWith6492: <
const typedData extends TypedData | Record<string, unknown>,
primaryType extends keyof typedData | "EIP712Domain" = keyof typedData
>(
typedDataDefinition: TypedDataDefinition<typedData, primaryType>
) => Promise<Hex>;
encodeUpgradeToAndCall: (params: UpgradeToAndCallParams) => Promise<Hex>;
getNonce(nonceKey?: bigint): Promise<bigint>;
getInitCode: () => Promise<Hex>;
isAccountDeployed: () => Promise<boolean>;
getFactoryAddress: () => Promise<Address>;
getFactoryData: () => Promise<Hex>;
getEntryPoint: () => EntryPointDef<TEntryPointVersion>;
getImplementationAddress: () => Promise<NullAddress | Address>;
};
// [!endregion SmartContractAccount]
export interface AccountEntryPointRegistry<Name extends string = string>
extends EntryPointRegistryBase<
SmartContractAccount<Name, EntryPointVersion>
> {
"0.6.0": SmartContractAccount<Name, "0.6.0">;
"0.7.0": SmartContractAccount<Name, "0.7.0">;
}
// [!region ToSmartContractAccountParams]
export type ToSmartContractAccountParams<
Name extends string = string,
TTransport extends Transport = Transport,
TChain extends Chain = Chain,
TEntryPointVersion extends EntryPointVersion = EntryPointVersion
> = {
source: Name;
transport: TTransport;
chain: TChain;
entryPoint: EntryPointDef<TEntryPointVersion, TChain>;
accountAddress?: Address;
getAccountInitCode: () => Promise<Hex>;
getDummySignature: () => Hex | Promise<Hex>;
encodeExecute: (tx: AccountOp) => Promise<Hex>;
encodeBatchExecute?: (txs: AccountOp[]) => Promise<Hex>;
// if not provided, will default to just using signMessage over the Hex
signUserOperationHash?: (uoHash: Hex) => Promise<Hex>;
encodeUpgradeToAndCall?: (params: UpgradeToAndCallParams) => Promise<Hex>;
} & Omit<CustomSource, "signTransaction" | "address">;
// [!endregion ToSmartContractAccountParams]
export const parseFactoryAddressFromAccountInitCode = (
initCode: Hex
): [Address, Hex] => {
const factoryAddress: Address = `0x${initCode.substring(2, 42)}`;
const factoryCalldata: Hex = `0x${initCode.substring(42)}`;
return [factoryAddress, factoryCalldata];
};
export const getAccountAddress = async ({
client,
entryPoint,
accountAddress,
getAccountInitCode,
}: {
client: PublicClient;
entryPoint: EntryPointDef;
accountAddress?: Address;
getAccountInitCode: () => Promise<Hex>;
}) => {
if (accountAddress) return accountAddress;
const entryPointContract = getContract({
address: entryPoint.address,
abi: entryPoint.abi,
client,
});
const initCode = await getAccountInitCode();
Logger.verbose("[BaseSmartContractAccount](getAddress) initCode: ", initCode);
try {
await entryPointContract.simulate.getSenderAddress([initCode]);
} catch (err: any) {
Logger.verbose(
"[BaseSmartContractAccount](getAddress) getSenderAddress err: ",
err
);
if (err.cause?.data?.errorName === "SenderAddressResult") {
Logger.verbose(
"[BaseSmartContractAccount](getAddress) entryPoint.getSenderAddress result:",
err.cause.data.args[0]
);
return err.cause.data.args[0] as Address;
}
if (err.details === "Invalid URL") {
throw new InvalidRpcUrlError();
}
}
throw new GetCounterFactualAddressError();
};
// [!region toSmartContractAccount]
export async function toSmartContractAccount<
Name extends string = string,
TTransport extends Transport = Transport,
TChain extends Chain = Chain,
TEntryPointVersion extends EntryPointVersion = EntryPointVersion
>({
transport,
chain,
entryPoint,
source,
accountAddress,
getAccountInitCode,
signMessage,
signTypedData,
encodeBatchExecute,
encodeExecute,
getDummySignature,
signUserOperationHash,
encodeUpgradeToAndCall,
}: ToSmartContractAccountParams<
Name,
TTransport,
TChain,
TEntryPointVersion
>): Promise<SmartContractAccount<Name, TEntryPointVersion>>;
// [!endregion toSmartContractAccount]
export async function toSmartContractAccount({
transport,
chain,
entryPoint,
source,
accountAddress,
getAccountInitCode,
signMessage,
signTypedData,
encodeBatchExecute,
encodeExecute,
getDummySignature,
signUserOperationHash,
encodeUpgradeToAndCall,
}: ToSmartContractAccountParams): Promise<SmartContractAccount> {
const client = createBundlerClient({
// we set the retry count to 0 so that viem doesn't retry during
// getting the address. That call always reverts and without this
// viem will retry 3 times, making this call very slow
transport: (opts) => transport({ ...opts, chain, retryCount: 0 }),
chain,
});
const entryPointContract = getContract({
address: entryPoint.address,
abi: entryPoint.abi,
client,
});
const accountAddress_ = await getAccountAddress({
client,
entryPoint: entryPoint,
accountAddress,
getAccountInitCode,
});
let deploymentState = DeploymentState.UNDEFINED;
const getInitCode = async () => {
if (deploymentState === DeploymentState.DEPLOYED) {
return "0x";
}
const contractCode = await client.getBytecode({
address: accountAddress_,
});
if ((contractCode?.length ?? 0) > 2) {
deploymentState = DeploymentState.DEPLOYED;
return "0x";
} else {
deploymentState = DeploymentState.NOT_DEPLOYED;
}
return getAccountInitCode();
};
const signUserOperationHash_ =
signUserOperationHash ??
(async (uoHash: Hex) => {
return signMessage({ message: { raw: hexToBytes(uoHash) } });
});
const getFactoryAddress = async (): Promise<Address> =>
parseFactoryAddressFromAccountInitCode(await getAccountInitCode())[0];
const getFactoryData = async (): Promise<Hex> =>
parseFactoryAddressFromAccountInitCode(await getAccountInitCode())[1];
const encodeUpgradeToAndCall_ =
encodeUpgradeToAndCall ??
(() => {
throw new UpgradesNotSupportedError(source);
});
const isAccountDeployed = async () => {
const initCode = await getInitCode();
return initCode === "0x";
};
const getNonce = async (nonceKey = 0n): Promise<bigint> => {
if (!(await isAccountDeployed())) {
return 0n;
}
return entryPointContract.read.getNonce([
accountAddress_,
nonceKey,
]) as Promise<bigint>;
};
const account = toAccount({
address: accountAddress_,
signMessage,
signTypedData,
signTransaction: () => {
throw new SignTransactionNotSupportedError();
},
});
const create6492Signature = async (isDeployed: boolean, signature: Hex) => {
if (isDeployed) {
return signature;
}
const [factoryAddress, factoryCalldata] =
parseFactoryAddressFromAccountInitCode(await getAccountInitCode());
return wrapSignatureWith6492({
factoryAddress,
factoryCalldata,
signature,
});
};
const signMessageWith6492 = async (message: { message: SignableMessage }) => {
const [isDeployed, signature] = await Promise.all([
isAccountDeployed(),
account.signMessage(message),
]);
return create6492Signature(isDeployed, signature);
};
const signTypedDataWith6492 = async <
const typedData extends TypedData | Record<string, unknown>,
primaryType extends keyof typedData | "EIP712Domain" = keyof typedData
>(
typedDataDefinition: TypedDataDefinition<typedData, primaryType>
): Promise<Hex> => {
const [isDeployed, signature] = await Promise.all([
isAccountDeployed(),
account.signTypedData(typedDataDefinition),
]);
return create6492Signature(isDeployed, signature);
};
const getImplementationAddress = async (): Promise<NullAddress | Address> => {
const storage = await client.getStorageAt({
address: account.address,
// This is the default slot for the implementation address for Proxies
slot: "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
});
if (storage == null) {
throw new FailedToGetStorageSlotError(
"0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
"Proxy Implementation Address"
);
}
return trim(storage);
};
if (entryPoint.version !== "0.6.0" && entryPoint.version !== "0.7.0") {
throw new InvalidEntryPointError(chain, entryPoint.version);
}
return {
...account,
source,
// TODO: I think this should probably be signUserOperation instead
// and allow for generating the UO hash based on the EP version
signUserOperationHash: signUserOperationHash_,
getFactoryAddress,
getFactoryData,
encodeBatchExecute:
encodeBatchExecute ??
(() => {
throw new BatchExecutionNotSupportedError(source);
}),
encodeExecute,
getDummySignature,
getInitCode,
encodeUpgradeToAndCall: encodeUpgradeToAndCall_,
getEntryPoint: () => entryPoint,
isAccountDeployed,
getNonce,
signMessageWith6492,
signTypedDataWith6492,
getImplementationAddress,
};
}