@charterlabs/permissionless
Version:
A utility library for working with ERC-4337
383 lines (361 loc) • 15 kB
text/typescript
import type {
Assign,
Prettify,
} from "viem"
import {
type Address,
type Client,
type Hex,
concat,
encodeFunctionData,
toHex,
} from "viem"
import {
type SmartAccount,
type SmartAccountImplementation,
entryPoint07Abi,
entryPoint07Address,
toSmartAccount
} from "viem/account-abstraction"
import { readContract } from "viem/actions"
import { getAccountNonce } from "../../actions/public/getAccountNonce"
import { decode7579Calls } from "../../utils/decode7579Calls"
import { encode7579Calls } from "../../utils/encode7579Calls"
/**
* The account creation ABI for Biconomy Smart Account (from the biconomy SmartAccountFactory)
*/
/**
* Default addresses for Biconomy Smart Account
*/
const BICONOMY_ADDRESSES: {
CHARTER_FACTORY_ADDRESS: Address
K1_VALIDATOR_ADDRESS: Address
} = {
CHARTER_FACTORY_ADDRESS: "0xF6044Df1977DebF1069dE44a3Bf002d8738E25D6",
K1_VALIDATOR_ADDRESS: "0x9Bf4eF98187990262579B0C98764683A824dDc5b"
}
export type ToNexusSmartAccountParameters = Prettify<{
client: Client
version: "1.0.0"
address?: Address | undefined
entryPoint?: {
address: Address
version: "0.7"
}
index?: bigint
factoryAddress?: Address
validatorAddress?: Address
attesters?: Address[]
threshold?: number
/**
* Validator data containing the public key X and Y coordinates
*/
validatorData?: {
pubKeyX: bigint
pubKeyY: bigint
}
/**
* Hash of the authenticator ID
*/
authenticatorIdHash?: Hex
/**
* Array of executor modules with their configuration data
*/
executors?: { module: Address; data: Hex }[]
/**
* Hook module configuration
*/
hook?: {
module: Address
data: Hex
}
/**
* Array of fallback modules with their configuration data
*/
fallbacks?: { module: Address; data: Hex }[]
/**
* Array of pre-validation hooks
*/
preValidationHooks?: { hookType: bigint; module: Address; data: Hex }[]
/**
* Address of the registry contract
*/
registryAddress?: Address
}>
export type NexusSmartAccountImplementation = Assign<
SmartAccountImplementation<typeof entryPoint07Abi, "0.7">,
{ sign: NonNullable<SmartAccountImplementation["sign"]> }
>
export type ToNexusSmartAccountReturnType = Prettify<
SmartAccount<NexusSmartAccountImplementation>
>
export async function toNexusSmartAccount(
parameters: ToNexusSmartAccountParameters
): Promise<ToNexusSmartAccountReturnType> {
const {
client,
index = 0n,
address,
factoryAddress = BICONOMY_ADDRESSES.CHARTER_FACTORY_ADDRESS,
validatorAddress = BICONOMY_ADDRESSES.K1_VALIDATOR_ADDRESS,
attesters = [],
threshold = 0
} = parameters
const entryPoint = {
address: parameters.entryPoint?.address ?? entryPoint07Address,
abi: entryPoint07Abi,
version: parameters.entryPoint?.version ?? "0.7"
}
let _accountAddress: Address | undefined = address
const getFactoryArgs = async () => {
// Carefully handle defaults only for undefined values
const validatorData = parameters.validatorData === undefined
? { pubKeyX: 0n, pubKeyY: 0n }
: parameters.validatorData
const authenticatorIdHash = parameters.authenticatorIdHash === undefined
? "0x0000000000000000000000000000000000000000000000000000000000000000" as Hex
: parameters.authenticatorIdHash
const executors = parameters.executors === undefined ? [] : parameters.executors
const hook = parameters.hook === undefined
? { module: "0x0000000000000000000000000000000000000000" as Address, data: "0x" as Hex }
: parameters.hook
const fallbacks = parameters.fallbacks === undefined ? [] : parameters.fallbacks
const preValidationHooks = parameters.preValidationHooks === undefined ? [] : parameters.preValidationHooks
const registryConfig = {
registry: parameters.registryAddress === undefined
? "0x0000000000000000000000000000000000000000" as Address
: parameters.registryAddress,
attesters: attesters.sort((left, right) =>
left.toLowerCase().localeCompare(right.toLowerCase())
) as readonly Address[],
threshold
}
return {
factory: factoryAddress,
factoryData: encodeFunctionData({
abi: [
{
name: "createAccount",
type: "function",
stateMutability: "payable",
inputs: [
{ type: "uint256", name: "index" },
{
type: "tuple",
name: "validatorData",
components: [
{ type: "uint256", name: "pubKeyX" },
{ type: "uint256", name: "pubKeyY" }
]
},
{ type: "bytes32", name: "authenticatorIdHash" },
{
type: "tuple[]",
name: "executors",
components: [
{ type: "address", name: "module" },
{ type: "bytes", name: "data" }
]
},
{
type: "tuple",
name: "hook",
components: [
{ type: "address", name: "module" },
{ type: "bytes", name: "data" }
]
},
{
type: "tuple[]",
name: "fallbacks",
components: [
{ type: "address", name: "module" },
{ type: "bytes", name: "data" }
]
},
{
type: "tuple[]",
name: "preValidationHooks",
components: [
{ type: "uint256", name: "hookType" },
{ type: "address", name: "module" },
{ type: "bytes", name: "data" }
]
},
{
type: "tuple",
name: "registryConfig",
components: [
{ type: "address", name: "registry" },
{ type: "address[]", name: "attesters" },
{ type: "uint8", name: "threshold" }
]
}
],
outputs: [{ type: "address", name: "" }]
}
],
functionName: "createAccount",
args: [
index,
validatorData,
authenticatorIdHash,
executors,
hook,
fallbacks,
preValidationHooks,
registryConfig
]
})
}
}
return toSmartAccount({
client,
entryPoint,
getFactoryArgs,
async getAddress() {
if (_accountAddress) return _accountAddress
// If we need to compute the address
const computedAddress = await readContract(client, {
address: factoryAddress,
abi: [
{
name: "computeAccountAddress",
type: "function",
stateMutability: "view",
inputs: [
{ type: "uint256", name: "index" },
{
type: "tuple",
name: "validatorData",
components: [
{ type: "uint256", name: "pubKeyX" },
{ type: "uint256", name: "pubKeyY" }
]
},
{ type: "bytes32", name: "authenticatorIdHash" },
{
type: "tuple[]",
name: "executors",
components: [
{ type: "address", name: "module" },
{ type: "bytes", name: "data" }
]
},
{
type: "tuple",
name: "hook",
components: [
{ type: "address", name: "module" },
{ type: "bytes", name: "data" }
]
},
{
type: "tuple[]",
name: "fallbacks",
components: [
{ type: "address", name: "module" },
{ type: "bytes", name: "data" }
]
},
{
type: "tuple[]",
name: "preValidationHooks",
components: [
{ type: "uint256", name: "hookType" },
{ type: "address", name: "module" },
{ type: "bytes", name: "data" }
]
},
{
type: "tuple",
name: "registryConfig",
components: [
{ type: "address", name: "registry" },
{ type: "address[]", name: "attesters" },
{ type: "uint8", name: "threshold" }
]
}
],
outputs: [{ type: "address", name: "expectedAddress" }]
}
],
functionName: "computeAccountAddress",
args: [
index,
parameters.validatorData === undefined
? { pubKeyX: 0n, pubKeyY: 0n }
: parameters.validatorData,
parameters.authenticatorIdHash === undefined
? "0x0000000000000000000000000000000000000000000000000000000000000000" as Hex
: parameters.authenticatorIdHash,
parameters.executors === undefined ? [] : parameters.executors,
parameters.hook === undefined
? { module: "0x0000000000000000000000000000000000000000" as Address, data: "0x" as Hex }
: parameters.hook,
parameters.fallbacks === undefined ? [] : parameters.fallbacks,
parameters.preValidationHooks === undefined ? [] : parameters.preValidationHooks,
{
registry: parameters.registryAddress === undefined
? "0x0000000000000000000000000000000000000000" as Address
: parameters.registryAddress,
attesters: attesters.sort((left, right) =>
left.toLowerCase().localeCompare(right.toLowerCase())
) as readonly Address[],
threshold
}
]
})
_accountAddress = computedAddress
return computedAddress
},
async getNonce(args) {
const TIMESTAMP_ADJUSTMENT = 16777215n // max value for size 3
const defaultedKey = (args?.key ?? 0n) % TIMESTAMP_ADJUSTMENT
const defaultedValidationMode = "0x00"
const key = concat([
toHex(defaultedKey, { size: 3 }),
defaultedValidationMode,
validatorAddress
])
const address = await this.getAddress()
return getAccountNonce(client, {
address,
entryPointAddress: entryPoint.address,
key: BigInt(key)
})
},
encodeCalls: async (calls) => {
return encode7579Calls({
mode: {
type: calls.length > 1 ? "batchcall" : "call",
revertOnError: false,
selector: "0x",
context: "0x"
},
callData: calls
})
},
async decodeCalls(callData) {
return decode7579Calls(callData).callData
},
async getStubSignature() {
const dynamicPart = validatorAddress.substring(2).padEnd(40, "0")
return `0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000${dynamicPart}000000000000000000000000000000000000000000000000000000000000004181d4b4981670cb18f99f0b4a66446df1bf5b204d24cfcb659bf38ba27a4359b5711649ec2423c5e1247245eba2964679b6a1dbb85c992ae40b9b00c6935b02ff1b00000000000000000000000000000000000000000000000000000000000000` as Hex
},
async sign({ hash }) {
return this.signMessage({ message: hash })
},
// README: Logic Moved to client side
async signMessage() {
throw new Error('Not implemented')
},
// README: Logic Moved to client side
async signTypedData() {
throw new Error('Not implemented')
},
async signUserOperation() {
throw new Error('Not implemented')
}
})
}