Skip to content

feat(wip): implement viem compatible SCA impls for LA #1555

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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
33 changes: 33 additions & 0 deletions account-kit/infra/src/client/viem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { createBundlerClient } from "viem/account-abstraction";
import type { AlchemySmartAccountClientConfig } from "./smartAccountClient";

// Yea this isn't great or going to work well, we should just keep using our smart account client, but change the account type to match the viem one
Copy link
Contributor

Choose a reason for hiding this comment

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

💬 What about the idea of a transform then from the current client into this new client. Like rust's Into traits
Or, is it more worth it for the changing of the account type?

export const foo = (params: AlchemySmartAccountClientConfig) => {
const cl = createBundlerClient({
transport: params.transport,
chain: params.chain,
paymasterContext: { policyId: params.policyId },
paymaster: {
async getPaymasterStubData(parameters) {
if (!("policyId" in parameters.context)) {
throw new Error("policyId is required");
}

throw new Error(
"this is the biggest issue with viem's approach. we can't skip gas estimation during paymaster flows..."
Copy link
Contributor

Choose a reason for hiding this comment

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

💬 If we do some hacky stuff, like stub it out, it might be possible. But would is it worth the cost to our souls.
Is there a line of communication to the viem team on this at least?

);
},
async getPaymasterData(parameters) {
throw new Error("port over the paymaster data middleware");
},
},
userOperation: {
// This doesn't have the ability to skip gas estimation AFAICT
Copy link
Contributor

Choose a reason for hiding this comment

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

🙀

estimateFeesPerGas(parameters) {
throw new Error("port over the fee estimator middleware");
},
},
});

return cl;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { getEntryPoint, type SmartAccountSigner } from "@aa-sdk/core";
import {
createPublicClient,
encodeFunctionData,
slice,
type Address,
type Hex,
type Transport,
} from "viem";
import type { SmartAccount } from "viem/account-abstraction";
import { LightAccountAbi_v1 } from "../../abis/LightAccountAbi_v1.js";
import { LightAccountAbi_v2 } from "../../abis/LightAccountAbi_v2.js";
import { LightAccountFactoryAbi_v1 } from "../../abis/LightAccountFactoryAbi_v1.js";
import { LightAccountFactoryAbi_v2 } from "../../abis/LightAccountFactoryAbi_v2.js";
import type { LightAccountVersion } from "../../types";
import {
AccountVersionRegistry,
defaultLightAccountVersion,
getDefaultLightAccountFactoryAddress,
} from "../../utils.js";
import {
createBaseViemLightAccount,
type CreateBaseViemLightAccountParams,
} from "./base.js";

export type CreateViemLightAccountParams<
TTransport extends Transport = Transport,
TSigner extends SmartAccountSigner = SmartAccountSigner,
TLightAccountVersion extends LightAccountVersion<"LightAccount"> = "v2.0.0"
> = CreateBaseViemLightAccountParams<
Copy link
Contributor

Choose a reason for hiding this comment

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

💬 ❓ Wondering if it is worth it to do this extend style, maybe we should be flattening this, so just inline this https://github.com/alchemyplatform/aa-sdk/pull/1555/files#diff-477606e38764f257d171de4bf2d12f95adb50f0b6b9256aaf04ff292e20ff638R26

Was hoping that there was a keyword for the types, like satisfies.
That exists but not what I was thinking of.
I would hate that the best is something along the lines of interface implements
but that also doesn't exists. 😞
image

"LightAccount",
TLightAccountVersion,
TTransport,
TSigner
> & {
accountAddress?: Address;
factoryAddress?: Address;
salt?: bigint;
initCode?: Hex;
};

// TODO: define the implementation and return type for single owner light account

// TODO: the return type isn't quite correct here, we can do better following this as an example https://github.com/wevm/viem/blob/main/src/account-abstraction/accounts/implementations/toSoladySmartAccount.ts
Copy link
Contributor

Choose a reason for hiding this comment

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

❤️ Love these todo's

export async function createViemLightAccount<
TTransport extends Transport = Transport,
TSigner extends SmartAccountSigner = SmartAccountSigner,
TLightAccountVersion extends LightAccountVersion<"LightAccount"> = "v2.0.0"
>(
params: CreateViemLightAccountParams<
TTransport,
TSigner,
TLightAccountVersion
>
): Promise<SmartAccount>;

export async function createViemLightAccount(
params: CreateViemLightAccountParams
): Promise<SmartAccount> {
const {
chain,
transport,
signer,
version = defaultLightAccountVersion(),
entryPoint = getEntryPoint(chain, {
version: AccountVersionRegistry["LightAccount"][version]
.entryPointVersion as any,
}),
accountAddress,
factoryAddress = getDefaultLightAccountFactoryAddress(chain, version),
salt: salt_ = 0n,
initCode,
} = params;

const accountAbi =
version === "v2.0.0" ? LightAccountAbi_v2 : LightAccountAbi_v1;
const factoryAbi =
version === "v2.0.0"
? LightAccountFactoryAbi_v1
: LightAccountFactoryAbi_v2;

// TODO: port over the extended methods in the current impl (ie. encodeTransferOwnership and getOwnerAddress)
return createBaseViemLightAccount({
abi: accountAbi,
factoryAbi,
chain,
transport,
signer,
version,
type: "LightAccount",
entryPoint,
async getAddress() {
if (accountAddress) return accountAddress;

const client = createPublicClient({
transport,
chain,
});

return client.readContract({
abi: factoryAbi,
address: factoryAddress,
functionName: "getAddress",
args: [await signer.getAddress(), salt_],
});
},
async getFactoryArgs() {
if (initCode) {
return {
factory: slice(initCode, 0, 20),
factoryData: slice(initCode, 20),
};
}

return {
factory: factoryAddress,
factoryData: encodeFunctionData({
abi: factoryAbi,
functionName: "createAccount",
args: [await signer.getAddress(), salt_],
}),
};
},
});
}
191 changes: 191 additions & 0 deletions account-kit/smart-contracts/src/light-account/accounts/viem/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import {
deepHexlify,
type EntryPointDef,
type SmartAccountSigner,
} from "@aa-sdk/core";
import {
concat,
createPublicClient,
encodeFunctionData,
hashMessage,
hashTypedData,
type Abi,
type Address,
type Chain,
type Hex,
type Transport,
} from "viem";
import { toSmartAccount, type SmartAccount } from "viem/account-abstraction";
import { SignatureType } from "../../../ma-v2/modules/utils.js";
import type {
LightAccountEntryPointVersion,
LightAccountType,
LightAccountVersion,
} from "../../types";

export type CreateBaseViemLightAccountParams<
TLightAccountType extends LightAccountType,
TLightAccountVersion extends LightAccountVersion<TLightAccountType> = LightAccountVersion<TLightAccountType>,
TTransport extends Transport = Transport,
TSigner extends SmartAccountSigner = SmartAccountSigner
> = {
transport: TTransport;
chain: Chain;
signer: TSigner;
abi: Abi;
factoryAbi: Abi;
version: TLightAccountVersion;
type: TLightAccountType;
entryPoint: EntryPointDef<
LightAccountEntryPointVersion<TLightAccountType, TLightAccountVersion>,
Chain
>;
getAddress: () => Promise<Address>;
getFactoryArgs: () => Promise<{
factory: Address;
factoryData: Hex;
}>;
};
// TODO: define the implementation and return type for the base account

// TODO: the return type isn't quite correct here, we can do better following this as an example https://github.com/wevm/viem/blob/main/src/account-abstraction/accounts/implementations/toSoladySmartAccount.ts
export async function createBaseViemLightAccount<
TLightAccountType extends LightAccountType,
TLightAccountVersion extends LightAccountVersion<TLightAccountType> = LightAccountVersion<TLightAccountType>,
TTransport extends Transport = Transport,
TSigner extends SmartAccountSigner = SmartAccountSigner
>(
params: CreateBaseViemLightAccountParams<
TLightAccountType,
TLightAccountVersion,
TTransport,
TSigner
>
): Promise<SmartAccount> {
const {
version,
signer,
abi,
type,
entryPoint,
transport,
chain,
getAddress,
getFactoryArgs,
} = params;
const client = createPublicClient({
transport,
chain,
});

const signWith1271Wrapper = async (
hashedMessage: Hex,
version: string
): Promise<Hex> => {
return signer.signTypedData({
// EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)
// https://github.com/alchemyplatform/light-account/blob/main/src/LightAccount.sol#L236
domain: {
chainId: Number(client.chain.id),
name: type,
verifyingContract: await getAddress(),
version,
},
types: {
LightAccountMessage: [{ name: "message", type: "bytes" }],
},
message: {
message: hashedMessage,
},
primaryType: "LightAccountMessage",
});
};

return toSmartAccount({
client,
extend: {
abi,
},
entryPoint: {
abi: entryPoint.abi,
address: entryPoint.address,
version:
entryPoint.version === "0.6.0"
? "0.6"
: entryPoint.version === "0.7.0"
? "0.7"
: (() => {
throw new Error("Invalid entry point version");
})(),
},
async sign({ hash }) {
if (version === "v1.0.1" || version === "v1.0.2") {
throw new Error(`${type} ${String(version)} doesn't support 1271`);
}

switch (version as string) {
case "v1.1.0":
return await signWith1271Wrapper(hash, "1");
case "v2.0.0":
const signature = await signWith1271Wrapper(hash, "2");
// TODO: handle case where signer is an SCA.
return concat([SignatureType.EOA, signature]);
default:
throw new Error(`Unknown version ${type} of ${String(version)}`);
}
},
async signMessage({ message }) {
return this.sign!({ hash: hashMessage(message) });
},
async signTypedData(params) {
return this.sign!({ hash: hashTypedData(params) });
},
async getStubSignature() {
const signature =
"0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c";
switch (version as string) {
case "v1.0.1":
case "v1.0.2":
case "v1.1.0":
return signature;
case "v2.0.0":
return concat([SignatureType.EOA, signature]);
default:
throw new Error(`Unknown version ${type} of ${String(version)}`);
}
},
async signUserOperation(parameters) {
const hash = entryPoint.getUserOperationHash(deepHexlify(parameters));

return signer.signMessage({ raw: hash });
},
async encodeCalls(calls) {
if (calls.length === 1) {
const call = calls[0];
return encodeFunctionData({
abi,
functionName: "execute",
args: [call.to, call.value ?? 0n, call.data],
});
}

const [targets, values, datas] = calls.reduce(
(accum, curr) => {
accum[0].push(curr.to);
accum[1].push(curr.value ?? 0n);
accum[2].push(curr.data ?? "0x");

return accum;
},
[[], [], []] as [Address[], bigint[], Hex[]]
);
return encodeFunctionData({
abi,
functionName: "executeBatch",
args: [targets, values, datas],
});
},
getAddress,
getFactoryArgs,
});
}
Loading
Loading