import { type Account, type Address, type Assign, type Chain, type Client, type Hex, type JsonRpcAccount, type LocalAccount, type OneOf, type Prettify, type Transport, type TypedDataDefinition, type WalletClient, concat, concatHex, domainSeparator, encodeAbiParameters, encodeFunctionData, encodePacked, getTypesForEIP712Domain, hashMessage, hashTypedData, keccak256, stringToHex, toHex, validateTypedData } from "viem" import { type SmartAccount, type SmartAccountImplementation, entryPoint07Abi, entryPoint07Address, getUserOperationHash, toSmartAccount } from "viem/account-abstraction" import { getChainId, readContract } from "viem/actions" import { getAction } from "viem/utils" import { getAccountNonce } from "../../actions/public/getAccountNonce.js" import { decode7579Calls } from "../../utils/decode7579Calls.js" import { encode7579Calls } from "../../utils/encode7579Calls.js" import { type EthereumProvider, toOwner } from "../../utils/toOwner.js" const wrapMessageHash = ( messageHash: Hex, { accountAddress, version, chainId }: { accountAddress: Address version: "1.0.0" chainId: number } ) => { const _domainSeparator = domainSeparator({ domain: { name: "Nexus", version: version, chainId, verifyingContract: accountAddress } }) const parentStructHash = keccak256( encodeAbiParameters( [{ type: "bytes32" }, { type: "bytes32" }], [ keccak256(stringToHex("PersonalSign(bytes prefixed)")), messageHash ] ) ) return keccak256(concatHex(["0x1901", _domainSeparator, parentStructHash])) } /** * The account creation ABI for Biconomy Smart Account (from the biconomy SmartAccountFactory) */ /** * Default addresses for Biconomy Smart Account */ const BICONOMY_ADDRESSES: { K1_VALIDATOR_FACTORY_ADDRESS: Address K1_VALIDATOR_ADDRESS: Address } = { K1_VALIDATOR_FACTORY_ADDRESS: "0x00000bb19a3579F4D779215dEf97AFbd0e30DB55", K1_VALIDATOR_ADDRESS: "0x00000004171351c442B202678c48D8AB5B321E8f" } export type ToNexusSmartAccountParameters = Prettify<{ client: Client< Transport, Chain | undefined, JsonRpcAccount | LocalAccount | undefined > owners: [ OneOf< | EthereumProvider | WalletClient | LocalAccount > ] version: "1.0.0" address?: Address | undefined entryPoint?: { address: Address version: "0.7" } index?: bigint factoryAddress?: Address validatorAddress?: Address attesters?: Address[] threshold?: number }> export type NexusSmartAccountImplementation = Assign< SmartAccountImplementation, { sign: NonNullable } > export type ToNexusSmartAccountReturnType = Prettify< SmartAccount > export async function toNexusSmartAccount( parameters: ToNexusSmartAccountParameters ): Promise { const { owners, client, index = 0n, address, version, factoryAddress = BICONOMY_ADDRESSES.K1_VALIDATOR_FACTORY_ADDRESS, validatorAddress = BICONOMY_ADDRESSES.K1_VALIDATOR_ADDRESS, attesters = [], threshold = 0 } = parameters const localOwner = await toOwner({ owner: owners[0] }) const entryPoint = { address: parameters.entryPoint?.address ?? entryPoint07Address, abi: entryPoint07Abi, version: parameters.entryPoint?.version ?? "0.7" } let accountAddress: Address | undefined = address const getFactoryArgs = async () => { return { factory: factoryAddress, factoryData: encodeFunctionData({ abi: [ { name: "createAccount", type: "function", stateMutability: "nonpayable", inputs: [ { type: "address", name: "eoaOwner" }, { type: "uint256", name: "index" }, { type: "address[]", name: "attesters" }, { type: "uint8", name: "threshold" } ], outputs: [{ type: "address" }] } ], functionName: "createAccount", args: [ localOwner.address, index, attesters.sort((left, right) => left.toLowerCase().localeCompare(right.toLowerCase()) ), threshold ] }) } } let chainId: number const getMemoizedChainId = async () => { if (chainId) return chainId chainId = client.chain ? client.chain.id : await getAction(client, getChainId, "getChainId")({}) return chainId } return toSmartAccount({ client, entryPoint, getFactoryArgs, async getAddress() { if (accountAddress) return accountAddress accountAddress = await readContract(client, { address: factoryAddress, abi: [ { name: "computeAccountAddress", type: "function", stateMutability: "view", inputs: [ { type: "address", name: "eoaOwner" }, { type: "uint256", name: "index" }, { type: "address[]", name: "attesters" }, { type: "uint8", name: "threshold" } ], outputs: [{ type: "address" }] } ], functionName: "computeAccountAddress", args: [ localOwner.address, index, attesters.sort((left, right) => left.toLowerCase().localeCompare(right.toLowerCase()) ), threshold ] }) return accountAddress }, 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 }) }, async signMessage({ message }) { const wrappedMessageHash = wrapMessageHash(hashMessage(message), { version, accountAddress: await this.getAddress(), chainId: await getMemoizedChainId() }) const signature = await localOwner.signMessage({ message: { raw: wrappedMessageHash } }) return encodePacked( ["address", "bytes"], [validatorAddress, signature] ) }, async signTypedData(typedData) { const { message, primaryType, types: _types, domain } = typedData as TypedDataDefinition const types = { EIP712Domain: getTypesForEIP712Domain({ domain: domain }), ..._types } validateTypedData({ domain, message, primaryType, types }) const typedHash = hashTypedData({ message, primaryType, types, domain }) const wrappedMessageHash = wrapMessageHash(typedHash, { version, accountAddress: await this.getAddress(), chainId: await getMemoizedChainId() }) const signature = await localOwner.signMessage({ message: { raw: wrappedMessageHash } }) return encodePacked( ["address", "bytes"], [validatorAddress, signature] ) }, async signUserOperation(parameters) { const { chainId = await getMemoizedChainId(), ...userOperation } = parameters if (!chainId) throw new Error("Chain id not found") const hash = getUserOperationHash({ userOperation: { ...userOperation, sender: userOperation.sender ?? (await this.getAddress()), signature: "0x" }, entryPointAddress: entryPoint.address, entryPointVersion: entryPoint.version, chainId: chainId }) return await localOwner.signMessage({ message: { raw: hash as Hex } }) } }) }