-
Notifications
You must be signed in to change notification settings - Fork 171
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
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..." | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
); | ||
}, | ||
async getPaymasterData(parameters) { | ||
throw new Error("port over the paymaster data middleware"); | ||
}, | ||
}, | ||
userOperation: { | ||
// This doesn't have the ability to skip gas estimation AFAICT | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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< | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
"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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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_], | ||
}), | ||
}; | ||
}, | ||
}); | ||
} |
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, | ||
}); | ||
} |
There was a problem hiding this comment.
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
traitsOr, is it more worth it for the changing of the account type?