permissionless
Version:
A utility library for working with ERC-4337
805 lines (742 loc) • 25.3 kB
text/typescript
import { Base64, Hex, PublicKey } from "ox"
import type {
Account,
Assign,
Chain,
JsonRpcAccount,
OneOf,
PrivateKeyAccount,
Transport,
WalletClient
} from "viem"
import {
type Address,
type Client,
type LocalAccount,
type TypedDataDefinition,
concatHex,
encodeAbiParameters,
encodeFunctionData,
keccak256,
toHex,
zeroAddress
} from "viem"
import {
type SmartAccount,
type SmartAccountImplementation,
type UserOperation,
type WebAuthnAccount,
entryPoint06Abi,
entryPoint07Abi,
entryPoint07Address,
getUserOperationHash,
toSmartAccount
} from "viem/account-abstraction"
import { getChainId } from "viem/actions"
import { getAction } from "viem/utils"
import { getAccountNonce } from "../../actions/public/getAccountNonce.js"
import { getSenderAddress } from "../../actions/public/getSenderAddress.js"
import { type EthereumProvider, toOwner } from "../../utils/toOwner.js"
import { KernelInitAbi } from "./abi/KernelAccountAbi.js"
import {
KernelV3InitAbi,
KernelV3_1AccountAbi
} from "./abi/KernelV3AccountAbi.js"
import { KernelV3FactoryAbi } from "./abi/KernelV3FactoryAbi.js"
import { KernelV3MetaFactoryDeployWithFactoryAbi } from "./abi/KernelV3MetaFactoryAbi.js"
import {
DUMMY_ECDSA_SIGNATURE,
ROOT_MODE_KERNEL_V2,
VALIDATOR_TYPE
} from "./constants.js"
import { decodeCallData } from "./utils/decodeCallData.js"
import { encodeCallData } from "./utils/encodeCallData.js"
import { getNonceKeyWithEncoding } from "./utils/getNonceKey.js"
import { isKernelV2 } from "./utils/isKernelV2.js"
import { isWebAuthnAccount } from "./utils/isWebAuthnAccount.js"
import { signMessage } from "./utils/signMessage.js"
import { signTypedData } from "./utils/signTypedData.js"
type EntryPointVersion = "0.6" | "0.7"
/**
* The account creation ABI for a kernel smart account (from the KernelFactory)
*/
const createAccountAbi = [
{
inputs: [
{
internalType: "address",
name: "_implementation",
type: "address"
},
{
internalType: "bytes",
name: "_data",
type: "bytes"
},
{
internalType: "uint256",
name: "_index",
type: "uint256"
}
],
name: "createAccount",
outputs: [
{
internalType: "address",
name: "proxy",
type: "address"
}
],
stateMutability: "payable",
type: "function"
}
] as const
export type KernelVersion<entryPointVersion extends EntryPointVersion> =
entryPointVersion extends "0.6"
? "0.2.1" | "0.2.2" | "0.2.3" | "0.2.4"
: "0.3.0-beta" | "0.3.1" | "0.3.2" | "0.3.3"
/**
* Default addresses map for different kernel smart account versions
*/
export const KERNEL_VERSION_TO_ADDRESSES_MAP: {
[key in KernelVersion<EntryPointVersion>]: {
ECDSA_VALIDATOR: Address
WEB_AUTHN_VALIDATOR?: Address
ACCOUNT_LOGIC: Address
FACTORY_ADDRESS: Address
META_FACTORY_ADDRESS?: Address
}
} = {
"0.2.1": {
ECDSA_VALIDATOR: "0xd9AB5096a832b9ce79914329DAEE236f8Eea0390",
ACCOUNT_LOGIC: "0xf048AD83CB2dfd6037A43902a2A5Be04e53cd2Eb",
FACTORY_ADDRESS: "0x5de4839a76cf55d0c90e2061ef4386d962E15ae3"
},
"0.2.2": {
ECDSA_VALIDATOR: "0xd9AB5096a832b9ce79914329DAEE236f8Eea0390",
ACCOUNT_LOGIC: "0x0DA6a956B9488eD4dd761E59f52FDc6c8068E6B5",
FACTORY_ADDRESS: "0x5de4839a76cf55d0c90e2061ef4386d962E15ae3"
},
"0.2.3": {
ECDSA_VALIDATOR: "0xd9AB5096a832b9ce79914329DAEE236f8Eea0390",
ACCOUNT_LOGIC: "0xD3F582F6B4814E989Ee8E96bc3175320B5A540ab",
FACTORY_ADDRESS: "0x5de4839a76cf55d0c90e2061ef4386d962E15ae3"
},
"0.2.4": {
ECDSA_VALIDATOR: "0xd9AB5096a832b9ce79914329DAEE236f8Eea0390",
ACCOUNT_LOGIC: "0xd3082872F8B06073A021b4602e022d5A070d7cfC",
FACTORY_ADDRESS: "0x5de4839a76cf55d0c90e2061ef4386d962E15ae3"
},
"0.3.0-beta": {
ECDSA_VALIDATOR: "0x8104e3Ad430EA6d354d013A6789fDFc71E671c43",
ACCOUNT_LOGIC: "0x94F097E1ebEB4ecA3AAE54cabb08905B239A7D27",
FACTORY_ADDRESS: "0x6723b44Abeec4E71eBE3232BD5B455805baDD22f",
META_FACTORY_ADDRESS: "0xd703aaE79538628d27099B8c4f621bE4CCd142d5",
WEB_AUTHN_VALIDATOR: "0xbA45a2BFb8De3D24cA9D7F1B551E14dFF5d690Fd"
},
"0.3.1": {
ECDSA_VALIDATOR: "0x845ADb2C711129d4f3966735eD98a9F09fC4cE57",
ACCOUNT_LOGIC: "0xBAC849bB641841b44E965fB01A4Bf5F074f84b4D",
FACTORY_ADDRESS: "0xaac5D4240AF87249B3f71BC8E4A2cae074A3E419",
META_FACTORY_ADDRESS: "0xd703aaE79538628d27099B8c4f621bE4CCd142d5",
WEB_AUTHN_VALIDATOR: "0xbA45a2BFb8De3D24cA9D7F1B551E14dFF5d690Fd"
},
"0.3.2": {
ECDSA_VALIDATOR: "0x845ADb2C711129d4f3966735eD98a9F09fC4cE57",
ACCOUNT_LOGIC: "0xD830D15D3dc0C269F3dBAa0F3e8626d33CFdaBe1",
FACTORY_ADDRESS: "0x7a1dBAB750f12a90EB1B60D2Ae3aD17D4D81EfFe",
META_FACTORY_ADDRESS: "0xd703aaE79538628d27099B8c4f621bE4CCd142d5"
},
"0.3.3": {
ECDSA_VALIDATOR: "0x845ADb2C711129d4f3966735eD98a9F09fC4cE57",
ACCOUNT_LOGIC: "0xd6CEDDe84be40893d153Be9d467CD6aD37875b28",
FACTORY_ADDRESS: "0x2577507b78c2008Ff367261CB6285d44ba5eF2E9",
META_FACTORY_ADDRESS: "0xd703aaE79538628d27099B8c4f621bE4CCd142d5"
}
}
/**
* Get supported Kernel Smart Account version based on entryPoint
* @param entryPoint
*/
const getDefaultKernelVersion = <TEntryPointVersion extends EntryPointVersion>(
entryPointVersion: TEntryPointVersion,
version?: KernelVersion<TEntryPointVersion>,
eip7702?: boolean
): KernelVersion<TEntryPointVersion> => {
if (eip7702) {
return "0.3.3" as KernelVersion<TEntryPointVersion>
}
if (version) {
return version
}
return (
entryPointVersion === "0.6" ? "0.2.2" : "0.3.0-beta"
) as KernelVersion<TEntryPointVersion>
}
type KERNEL_ADDRESSES = {
validatorAddress?: Address
accountLogicAddress: Address
factoryAddress: Address
metaFactoryAddress: Address
}
/**
* Get default addresses for Kernel Smart Account based on entryPoint or user input
* @param entryPointAddress
* @param ecdsaValidatorAddress
* @param accountLogicAddress
* @param factoryAddress
* @param metaFactoryAddress
*/
const getDefaultAddresses = ({
validatorAddress: _validatorAddress,
accountLogicAddress: _accountLogicAddress,
factoryAddress: _factoryAddress,
metaFactoryAddress: _metaFactoryAddress,
kernelVersion,
isWebAuthn
}: Partial<KERNEL_ADDRESSES> & {
kernelVersion: KernelVersion<EntryPointVersion>
isWebAuthn: boolean
}): KERNEL_ADDRESSES => {
const addresses = KERNEL_VERSION_TO_ADDRESSES_MAP[kernelVersion]
const validatorAddress =
_validatorAddress ??
(isWebAuthn ? addresses.WEB_AUTHN_VALIDATOR : addresses.ECDSA_VALIDATOR)
const accountLogicAddress = _accountLogicAddress ?? addresses.ACCOUNT_LOGIC
const factoryAddress = _factoryAddress ?? addresses.FACTORY_ADDRESS
const metaFactoryAddress =
_metaFactoryAddress ?? addresses?.META_FACTORY_ADDRESS ?? zeroAddress // Meta Factory doesn't exists for Kernel v2.2
return {
validatorAddress,
accountLogicAddress,
factoryAddress,
metaFactoryAddress
}
}
export const getEcdsaRootIdentifierForKernelV3 = (
validatorAddress: Address
) => {
return concatHex([VALIDATOR_TYPE.VALIDATOR, validatorAddress])
}
/**
* Get the initialization data for a kernel smart account
* @param entryPoint
* @param owner
* @param ecdsaValidatorAddress
*/
const getInitializationData = <entryPointVersion extends EntryPointVersion>({
entryPoint: { version: entryPointVersion },
kernelVersion,
validatorData,
validatorAddress
}: {
kernelVersion: KernelVersion<entryPointVersion>
entryPoint: {
version: entryPointVersion
}
validatorData: Hex.Hex
validatorAddress: Address
}) => {
if (entryPointVersion === "0.6") {
return encodeFunctionData({
abi: KernelInitAbi,
functionName: "initialize",
args: [validatorAddress, validatorData]
})
}
if (kernelVersion === "0.3.0-beta") {
return encodeFunctionData({
abi: KernelV3InitAbi,
functionName: "initialize",
args: [
getEcdsaRootIdentifierForKernelV3(validatorAddress),
zeroAddress,
validatorData,
"0x"
]
})
}
return encodeFunctionData({
abi: KernelV3_1AccountAbi,
functionName: "initialize",
args: [
getEcdsaRootIdentifierForKernelV3(validatorAddress),
zeroAddress,
validatorData,
"0x",
[]
]
})
}
const getValidatorData = async (owner: WebAuthnAccount | LocalAccount) => {
if (owner.type === "local") {
return owner.address
}
if (isWebAuthnAccount(owner)) {
const parsedPublicKey = PublicKey.fromHex(owner.publicKey)
const authenticatorIdHash = keccak256(
Hex.fromBytes(Base64.toBytes(owner.id))
)
return encodeAbiParameters(
[
{
components: [
{ name: "x", type: "uint256" },
{ name: "y", type: "uint256" }
],
name: "webAuthnData",
type: "tuple"
},
{
name: "authenticatorIdHash",
type: "bytes32"
}
],
[
{
x: parsedPublicKey.x,
y: parsedPublicKey.y
},
authenticatorIdHash
]
)
}
throw new Error("Invalid owner type")
}
/**
* Get the account initialization code for a kernel smart account
* @param entryPoint
* @param owner
* @param index
* @param factoryAddress
* @param accountLogicAddress
* @param ecdsaValidatorAddress
*/
const getAccountInitCode = async <entryPointVersion extends EntryPointVersion>({
entryPointVersion,
kernelVersion,
validatorData,
index,
factoryAddress,
accountLogicAddress,
validatorAddress,
useMetaFactory
}: {
kernelVersion: KernelVersion<entryPointVersion>
entryPointVersion: entryPointVersion
validatorData: Hex.Hex
index: bigint
factoryAddress: Address
accountLogicAddress: Address
validatorAddress: Address
useMetaFactory: boolean
}): Promise<Hex.Hex> => {
// Build the account initialization data
const initializationData = getInitializationData({
entryPoint: { version: entryPointVersion },
kernelVersion,
validatorAddress,
validatorData
})
// Build the account init code
if (entryPointVersion === "0.6") {
return encodeFunctionData({
abi: createAccountAbi,
functionName: "createAccount",
args: [accountLogicAddress, initializationData, index]
})
}
if (!useMetaFactory) {
return encodeFunctionData({
abi: KernelV3FactoryAbi,
functionName: "createAccount",
args: [initializationData, toHex(index, { size: 32 })]
})
}
return encodeFunctionData({
abi: KernelV3MetaFactoryDeployWithFactoryAbi,
functionName: "deployWithFactory",
args: [factoryAddress, initializationData, toHex(index, { size: 32 })]
})
}
export type ToKernelSmartAccountParameters<
entryPointVersion extends EntryPointVersion,
kernelVersion extends KernelVersion<entryPointVersion>,
owner extends OneOf<
| EthereumProvider
| WalletClient<Transport, Chain | undefined, Account>
| LocalAccount
| WebAuthnAccount
>,
eip7702 extends boolean = false
> = {
client: Client<
Transport,
Chain | undefined,
JsonRpcAccount | LocalAccount | undefined
>
version?: kernelVersion
eip7702?: eip7702
} & (eip7702 extends true
? {
owner: OneOf<
| EthereumProvider
| WalletClient<Transport, Chain | undefined, Account>
| LocalAccount
>
entryPoint?: {
address: Address
version: "0.7"
}
address?: never
index?: never
factoryAddress?: never
metaFactoryAddress?: never
accountLogicAddress?: never
validatorAddress?: never
nonceKey?: never
useMetaFactory?: never
}
: {
entryPoint?: {
address: Address
version: entryPointVersion
}
owners: [owner]
address?: Address
index?: bigint
factoryAddress?: Address
metaFactoryAddress?: Address
accountLogicAddress?: Address
validatorAddress?: Address
nonceKey?: bigint
useMetaFactory?: boolean | "optional"
})
export type KernelSmartAccountImplementation<
entryPointVersion extends EntryPointVersion = "0.7",
eip7702 extends boolean = false
> = Assign<
SmartAccountImplementation<
entryPointVersion extends "0.6"
? typeof entryPoint06Abi
: typeof entryPoint07Abi,
entryPointVersion,
eip7702 extends true
? {
implementation: Address
}
: object,
eip7702
// {
// // entryPoint === ENTRYPOINT_ADDRESS_V06 ? "0.2.2" : "0.3.0-beta"
// abi: entryPointVersion extends "0.6" ? typeof BiconomyAbi
// factory: { abi: typeof FactoryAbi; address: Address }
// }
>,
{ sign: NonNullable<SmartAccountImplementation["sign"]> }
>
export type ToKernelSmartAccountReturnType<
entryPointVersion extends EntryPointVersion = "0.7",
eip7702 extends boolean = false
> = eip7702 extends true
? SmartAccount<KernelSmartAccountImplementation<entryPointVersion, true>>
: SmartAccount<KernelSmartAccountImplementation<entryPointVersion, false>>
/**
* Build a kernel smart account from a private key, that use the ECDSA or passkeys signer behind the scene
* @param client
* @param privateKey
* @param entryPoint
* @param index
* @param factoryAddress
* @param accountLogicAddress
* @param validatorAddress
*/
export async function toKernelSmartAccount<
entryPointVersion extends EntryPointVersion,
kernelVersion extends KernelVersion<entryPointVersion>,
owner extends OneOf<
| EthereumProvider
| WalletClient<Transport, Chain | undefined, Account>
| LocalAccount
| WebAuthnAccount
>,
eip7702 extends boolean = false
>(
parameters: ToKernelSmartAccountParameters<
entryPointVersion,
kernelVersion,
owner,
eip7702
>
): Promise<ToKernelSmartAccountReturnType<entryPointVersion, eip7702>> {
const {
client,
address,
index = 0n,
version,
validatorAddress: _validatorAddress,
factoryAddress: _factoryAddress,
metaFactoryAddress: _metaFactoryAddress,
accountLogicAddress: _accountLogicAddress,
useMetaFactory = true,
eip7702 = false
} = parameters
const owners = (() => {
if (eip7702 && "owner" in parameters) {
return [parameters.owner]
}
if ("owners" in parameters) {
return parameters.owners
}
throw new Error("Invalid parameters")
})()
const isWebAuthn = owners[0].type === "webAuthn"
const owner = await (() => {
if (isWebAuthn) {
return owners[0] as WebAuthnAccount
}
return toOwner({
owner: owners[0] as OneOf<
| EthereumProvider
| WalletClient<Transport, Chain | undefined, Account>
| LocalAccount
>
})
})()
const entryPoint = (() => {
const address = parameters.entryPoint?.address ?? entryPoint07Address
const version = parameters.entryPoint?.version ?? "0.7"
let abi: typeof entryPoint06Abi | typeof entryPoint07Abi =
entryPoint07Abi
if (version === "0.6") {
abi = entryPoint06Abi
}
return {
address,
abi,
version
} as const
})()
const kernelVersion = getDefaultKernelVersion(
entryPoint.version,
version,
eip7702
)
const {
accountLogicAddress,
validatorAddress,
factoryAddress,
metaFactoryAddress
} = getDefaultAddresses({
validatorAddress: _validatorAddress,
accountLogicAddress: _accountLogicAddress,
factoryAddress: _factoryAddress,
metaFactoryAddress: _metaFactoryAddress,
kernelVersion,
isWebAuthn
})
if (!validatorAddress) {
throw new Error("Validator address is required")
}
// Helper to generate the init code for the smart account
const generateInitCode = async (_useMetaFactory: boolean) =>
getAccountInitCode({
entryPointVersion: entryPoint.version,
kernelVersion,
validatorData: await getValidatorData(owner),
index,
factoryAddress,
accountLogicAddress,
validatorAddress,
useMetaFactory: _useMetaFactory
})
let chainId: number
const getMemoizedChainId = async () => {
if (chainId) return chainId
chainId = client.chain
? client.chain.id
: await getAction(client, getChainId, "getChainId")({})
return chainId
}
const getFactoryArgsFunc = (_useMetaFactory: boolean) => async () => {
return {
factory:
entryPoint.version === "0.6" || _useMetaFactory === false
? factoryAddress
: metaFactoryAddress,
factoryData: await generateInitCode(_useMetaFactory)
}
}
const { accountAddress, getFactoryArgs } = await (async () => {
if (eip7702) {
return {
accountAddress: (owner as LocalAccount).address,
getFactoryArgs: async () => {
return {
factory: undefined,
factoryData: undefined
}
}
}
}
let getFactoryArgs = getFactoryArgsFunc(
useMetaFactory === "optional" ? true : useMetaFactory
)
if (address && useMetaFactory !== "optional") {
return { accountAddress: address, getFactoryArgs }
}
const { factory, factoryData } = await getFactoryArgs()
let accountAddress = await getSenderAddress(client, {
factory,
factoryData,
entryPointAddress: entryPoint.address
})
if (address === accountAddress) {
return { accountAddress, getFactoryArgs }
}
if (useMetaFactory === "optional" && accountAddress === zeroAddress) {
getFactoryArgs = getFactoryArgsFunc(false)
const { factory, factoryData } = await getFactoryArgs()
accountAddress = await getSenderAddress(client, {
factory,
factoryData,
entryPointAddress: entryPoint.address
})
}
return { accountAddress, getFactoryArgs }
})()
return toSmartAccount({
client,
entryPoint,
getFactoryArgs,
extend: eip7702
? {
implementation: accountLogicAddress
}
: undefined,
authorization: eip7702
? {
address: accountLogicAddress,
account: owner as PrivateKeyAccount
}
: undefined,
async getAddress() {
return accountAddress
},
async encodeCalls(calls) {
return encodeCallData({ calls, kernelVersion })
},
async decodeCalls(callData) {
return decodeCallData({ callData, kernelVersion })
},
async getNonce(_args) {
return getAccountNonce(client, {
address: await this.getAddress(),
entryPointAddress: entryPoint.address,
key: getNonceKeyWithEncoding(
kernelVersion,
validatorAddress,
/*args?.key ?? */ parameters.nonceKey ?? 0n
)
})
},
async getStubSignature() {
if (isKernelV2(kernelVersion)) {
return concatHex([ROOT_MODE_KERNEL_V2, DUMMY_ECDSA_SIGNATURE])
}
if (isWebAuthnAccount(owner)) {
return encodeAbiParameters(
[
{ name: "authenticatorData", type: "bytes" },
{ name: "clientDataJSON", type: "string" },
{ name: "responseTypeLocation", type: "uint256" },
{ name: "r", type: "uint256" },
{ name: "s", type: "uint256" },
{ name: "usePrecompiled", type: "bool" }
],
[
"0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97631d00000000",
'{"type":"webauthn.get","challenge":"tbxXNFS9X_4Byr1cMwqKrIGB-_30a0QhZ6y7ucM0BOE","origin":"http://localhost:3000","crossOrigin":false, "other_keys_can_be_added_here":"do not compare clientDataJSON against a template. See https://goo.gl/yabPex"}',
1n,
44941127272049826721201904734628716258498742255959991581049806490182030242267n,
9910254599581058084911561569808925251374718953855182016200087235935345969636n,
false
]
)
}
return DUMMY_ECDSA_SIGNATURE
},
async sign({ hash }) {
return this.signMessage({ message: hash })
},
async signMessage({ message }) {
if (eip7702) {
throw new Error("Kernel with EIP-7702 isn't 1271 compliant")
}
const signature = await signMessage({
owner,
message,
accountAddress: await this.getAddress(),
kernelVersion: kernelVersion,
chainId: await getMemoizedChainId()
})
if (isKernelV2(kernelVersion)) {
return signature
}
return concatHex([
getEcdsaRootIdentifierForKernelV3(validatorAddress),
signature
])
},
async signTypedData(typedData) {
if (eip7702) {
throw new Error("Kernel with EIP-7702 isn't 1271 compliant")
}
const signature = await signTypedData({
owner: owner,
chainId: await getMemoizedChainId(),
...(typedData as TypedDataDefinition),
accountAddress: await this.getAddress(),
kernelVersion: kernelVersion
})
if (isKernelV2(kernelVersion)) {
return signature
}
return concatHex([
getEcdsaRootIdentifierForKernelV3(validatorAddress),
signature
])
},
// Sign a user operation
async signUserOperation(parameters) {
const { chainId = await getMemoizedChainId(), ...userOperation } =
parameters
const hash = getUserOperationHash({
userOperation: {
...userOperation,
sender: userOperation.sender ?? (await this.getAddress()),
signature: "0x"
} as UserOperation<entryPointVersion>,
entryPointAddress: entryPoint.address,
entryPointVersion: entryPoint.version,
chainId: chainId
})
const signature = isWebAuthnAccount(owner)
? await signMessage({
owner,
message: { raw: hash },
chainId,
accountAddress: await this.getAddress(),
kernelVersion
})
: await owner.signMessage({
message: { raw: hash }
})
// Always use the sudo mode, since we will use external paymaster
if (isKernelV2(kernelVersion)) {
return concatHex(["0x00000000", signature])
}
return signature
}
}) as Promise<ToKernelSmartAccountReturnType<entryPointVersion, eip7702>>
}