@zerodev/sdk
Version:
A utility library for working with ERC-4337
414 lines (393 loc) • 14.4 kB
text/typescript
import { satisfies } from "semver"
import {
type Address,
type Client,
type Hex,
concat,
concatHex,
maxUint16,
maxUint192,
pad,
toHex,
zeroAddress
} from "viem"
import type { EntryPointVersion } from "viem/account-abstraction"
import { getChainId } from "viem/actions"
import { encodeModuleInstallCallData as encodeModuleInstallCallDataEpV06 } from "../../accounts/kernel/utils/account/ep0_6/encodeModuleInstallCallData.js"
import {
ONLY_ENTRYPOINT_HOOK_ADDRESS,
VALIDATOR_MODE,
VALIDATOR_TYPE
} from "../../constants.js"
import {
type KernelPluginManager,
type KernelPluginManagerParams,
ValidatorMode
} from "../../types/kernel.js"
import { getKernelV3Nonce } from "../kernel/utils/account/ep0_7/getKernelV3Nonce.js"
import { accountMetadata } from "../kernel/utils/common/accountMetadata.js"
import { getActionSelector } from "../kernel/utils/common/getActionSelector.js"
import { getEncodedPluginsData as getEncodedPluginsDataV1 } from "../kernel/utils/plugins/ep0_6/getEncodedPluginsData.js"
import { getPluginsEnableTypedData as getPluginsEnableTypedDataV1 } from "../kernel/utils/plugins/ep0_6/getPluginsEnableTypedData.js"
import { getEncodedPluginsData as getEncodedPluginsDataV2 } from "../kernel/utils/plugins/ep0_7/getEncodedPluginsData.js"
import { getPluginsEnableTypedData as getPluginsEnableTypedDataV2 } from "../kernel/utils/plugins/ep0_7/getPluginsEnableTypedData.js"
import { isPluginInitialized } from "../kernel/utils/plugins/ep0_7/isPluginInitialized.js"
export function isKernelPluginManager<
entryPointVersion extends EntryPointVersion
>(
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
plugin: any
): plugin is KernelPluginManager<entryPointVersion> {
return plugin?.getPluginEnableSignature !== undefined
}
export async function toKernelPluginManager<
entryPointVersion extends EntryPointVersion
>(
client: Client,
{
sudo,
regular,
hook,
pluginEnableSignature,
validatorInitData,
action,
validAfter = 0,
validUntil = 0,
entryPoint,
kernelVersion,
chainId,
isPreInstalled = false
}: KernelPluginManagerParams<entryPointVersion>
): Promise<KernelPluginManager<entryPointVersion>> {
if (
(sudo && !satisfies(kernelVersion, sudo?.supportedKernelVersions)) ||
(regular && !satisfies(kernelVersion, regular?.supportedKernelVersions))
) {
throw new Error(
"Either sudo or/and regular validator version mismatch. Update to latest plugin package and use the proper plugin version"
)
}
let pluginEnabled = isPreInstalled
const activeValidator = regular || sudo
if (!activeValidator) {
throw new Error("One of `sudo` or `regular` validator must be set")
}
action = {
selector: action?.selector ?? getActionSelector(entryPoint.version),
address: action?.address ?? zeroAddress
}
if (
entryPoint.version === "0.7" &&
(action.address.toLowerCase() !== zeroAddress.toLowerCase() ||
action.selector.toLowerCase() !==
getActionSelector(entryPoint.version).toLowerCase()) &&
kernelVersion === "0.3.0"
) {
action.hook = {
address: action.hook?.address ?? ONLY_ENTRYPOINT_HOOK_ADDRESS
}
}
if (!action) {
throw new Error("Action data must be set")
}
const getSignatureData = async (
accountAddress: Address,
selector: Hex,
userOpSignature: Hex = "0x"
): Promise<Hex> => {
if (!action) {
throw new Error("Action data must be set")
}
if (entryPoint.version === "0.6") {
if (regular) {
if (pluginEnabled) {
return ValidatorMode.plugin
}
if (await isPluginEnabled(accountAddress, selector)) {
return ValidatorMode.plugin
}
const enableSignature =
await getPluginEnableSignature(accountAddress)
if (!enableSignature) {
throw new Error("Enable signature not set")
}
return getEncodedPluginsDataV1({
accountAddress,
enableSignature,
action,
validator: regular,
validUntil,
validAfter
})
} else if (sudo) {
return ValidatorMode.sudo
} else {
throw new Error(
"One of `sudo` or `regular` validator must be set"
)
}
}
if (regular) {
if (pluginEnabled) {
return userOpSignature
}
if (await isPluginEnabled(accountAddress, action.selector)) {
return userOpSignature
}
const enableSignature =
await getPluginEnableSignature(accountAddress)
return getEncodedPluginsDataV2({
enableSignature,
userOpSignature,
action,
enableData: await regular.getEnableData(accountAddress),
hook
})
} else if (sudo) {
return userOpSignature
} else {
throw new Error("One of `sudo` or `regular` validator must be set")
}
}
const isPluginEnabled = async (accountAddress: Address, selector: Hex) => {
if (isPreInstalled) return true
if (!action) {
throw new Error("Action data must be set")
}
if (!regular) throw new Error("regular validator not set")
if (entryPoint.version === "0.6") {
return regular.isEnabled(accountAddress, selector)
}
const isEnabled =
(await regular.isEnabled(accountAddress, action.selector)) ||
(await isPluginInitialized(client, accountAddress, regular.address))
if (isEnabled) {
pluginEnabled = true
}
return isEnabled
}
const getPluginEnableSignature = async (accountAddress: Address) => {
if (!action) {
throw new Error("Action data must be set")
}
if (pluginEnableSignature) return pluginEnableSignature
if (!sudo)
throw new Error(
"sudo validator not set -- need it to enable the validator"
)
if (!regular) throw new Error("regular validator not set")
const { version } = await accountMetadata(
client,
accountAddress,
kernelVersion
)
if (!chainId) {
chainId = client.chain?.id ?? (await getChainId(client))
}
let ownerSig: Hex
if (entryPoint.version === "0.6") {
const typeData = await getPluginsEnableTypedDataV1({
accountAddress,
chainId,
kernelVersion: version ?? kernelVersion,
action,
validator: regular,
validUntil,
validAfter
})
ownerSig = await sudo.signTypedData(typeData)
pluginEnableSignature = ownerSig
return ownerSig
}
const validatorNonce = await getKernelV3Nonce(client, accountAddress)
const typedData = await getPluginsEnableTypedDataV2({
accountAddress,
chainId,
kernelVersion: version,
action,
hook,
validator: regular,
validatorNonce
})
ownerSig = await sudo.signTypedData(typedData)
pluginEnableSignature = ownerSig
return ownerSig
}
const getIdentifier = (isSudo = false) => {
const validator = (isSudo ? sudo : regular) ?? activeValidator
return concat([
VALIDATOR_TYPE[validator.validatorType],
validator.getIdentifier()
])
}
const getPluginsEnableTypedData = async (accountAddress: Address) => {
if (!action) {
throw new Error("Action data must be set")
}
if (!sudo)
throw new Error(
"sudo validator not set -- need it to enable the validator"
)
if (!regular) throw new Error("regular validator not set")
const { version } = await accountMetadata(
client,
accountAddress,
kernelVersion
)
const validatorNonce = await getKernelV3Nonce(client, accountAddress)
if (!chainId) {
chainId = client.chain?.id ?? (await getChainId(client))
}
const typedData = await getPluginsEnableTypedDataV2({
accountAddress,
chainId,
kernelVersion: version,
action,
validator: regular,
validatorNonce
})
return typedData
}
return {
sudoValidator: sudo,
regularValidator: regular,
activeValidatorMode: sudo && !regular ? "sudo" : "regular",
...activeValidator,
hook,
getIdentifier,
encodeModuleInstallCallData: async (accountAddress: Address) => {
if (!action) {
throw new Error("Action data must be set")
}
if (!regular) throw new Error("regular validator not set")
if (entryPoint.version === "0.6") {
return await encodeModuleInstallCallDataEpV06({
accountAddress,
selector: action.selector,
executor: action.address,
validator: regular?.address,
validUntil,
validAfter,
enableData: await regular.getEnableData(accountAddress)
})
}
throw new Error("EntryPoint v0.7 not supported yet")
},
signUserOperation: async (userOperation) => {
const userOpSig =
await activeValidator.signUserOperation(userOperation)
if (entryPoint.version === "0.6") {
return concatHex([
await getSignatureData(
userOperation.sender,
userOperation.callData.toString().slice(0, 10) as Hex
),
userOpSig
])
}
return await getSignatureData(
userOperation.sender,
userOperation.callData.toString().slice(0, 10) as Hex,
userOpSig
)
},
getAction: () => {
if (!action) {
throw new Error("Action data must be set")
}
return action
},
getValidityData: () => ({
validAfter,
validUntil
}),
getStubSignature: async (userOperation) => {
const userOpSig =
await activeValidator.getStubSignature(userOperation)
if (entryPoint.version === "0.6") {
return concatHex([
await getSignatureData(
userOperation.sender,
userOperation.callData.toString().slice(0, 10) as Hex
),
userOpSig
])
}
return await getSignatureData(
userOperation.sender,
userOperation.callData.toString().slice(0, 10) as Hex,
userOpSig
)
},
getNonceKey: async (
accountAddress = zeroAddress,
customNonceKey = 0n
) => {
if (!action) {
throw new Error("Action data must be set")
}
if (entryPoint.version === "0.6") {
if (customNonceKey > maxUint192) {
throw new Error(
"Custom nonce key must be equal or less than maxUint192 for 0.6"
)
}
return await activeValidator.getNonceKey(
accountAddress,
customNonceKey
)
}
if (customNonceKey > maxUint16)
throw new Error(
"Custom nonce key must be equal or less than 2 bytes(maxUint16) for v0.7"
)
const validatorMode =
!regular ||
(await isPluginEnabled(accountAddress, action.selector))
? VALIDATOR_MODE.DEFAULT
: VALIDATOR_MODE.ENABLE
const validatorType = regular
? VALIDATOR_TYPE[regular.validatorType]
: VALIDATOR_TYPE.SUDO
const encoding = pad(
concatHex([
validatorMode, // 1 byte
validatorType, // 1 byte
pad(activeValidator.getIdentifier(), {
size: 20,
dir: "right"
}), // 20 bytes
pad(
toHex(
await activeValidator.getNonceKey(
accountAddress,
customNonceKey
)
),
{
size: 2
}
) // 2 byte
]),
{ size: 24 }
)
const encodedNonceKey = BigInt(encoding)
return encodedNonceKey
},
getPluginEnableSignature,
getPluginsEnableTypedData,
getValidatorInitData: async () => {
if (validatorInitData) return validatorInitData
return {
validatorAddress: sudo?.address ?? activeValidator.address,
enableData:
(await sudo?.getEnableData()) ??
(await activeValidator.getEnableData()),
identifier: pad(getIdentifier(true), { size: 21, dir: "right" })
}
},
signUserOperationWithActiveValidator: async (userOperation) => {
return activeValidator.signUserOperation(userOperation)
}
}
}