UNPKG

@biconomy/sdk

Version:

SDK for Biconomy integration with support for account abstraction, smart accounts, ERC-4337.

356 lines 13.8 kB
import { concat, concatHex, createPublicClient, createWalletClient, domainSeparator, encodeAbiParameters, encodeFunctionData, encodePacked, getContract, keccak256, parseAbi, parseAbiParameters, publicActions, toBytes, toHex, validateTypedData, zeroAddress } from "viem"; import { entryPoint07Address, getUserOperationHash, toSmartAccount } from "viem/account-abstraction"; import { ENTRY_POINT_ADDRESS, MAINNET_ADDRESS_K1_VALIDATOR_ADDRESS, MAINNET_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS, MEE_VALIDATOR_ADDRESS, NEXUS_BOOTSTRAP_ADDRESS, REGISTRY_ADDRESS, RHINESTONE_ATTESTER_ADDRESS } from "../constants/index.js"; // Constants import { EntrypointAbi } from "../constants/abi/index.js"; import { getK1NexusAddress, getMeeNexusAddress } from "./decorators/getNexusAddress.js"; // Modules import { toK1Validator } from "../modules/k1Validator/toK1Validator.js"; import { getK1FactoryData } from "./decorators/getFactoryData.js"; import { getMeeFactoryData } from "./decorators/getFactoryData.js"; import { EXECUTE_BATCH, EXECUTE_SINGLE, MAGIC_BYTES, PARENT_TYPEHASH } from "./utils/Constants.js"; import { addressEquals, eip712WrapHash, getAccountDomainStructFields, getTypesForEIP712Domain, isNullOrUndefined, typeToString } from "./utils/Utils.js"; import { toSigner } from "./utils/toSigner.js"; /** * @description Create a Nexus Smart Account. * * @param parameters - {@link ToNexusSmartAccountParameters} * @returns Nexus Smart Account. {@link NexusAccount} * * @example * import { toNexusAccount } from '@biconomy/sdk' * import { createWalletClient, http } from 'viem' * import { mainnet } from 'viem/chains' * * const account = await toNexusAccount({ * chain: mainnet, * transport: http(), * signer: '0x...', * }) */ export const toNexusAccount = async (parameters) => { const { chain, transport, signer: _signer, index = 0n, module: module_, factoryAddress = MAINNET_ADDRESS_K1_VALIDATOR_FACTORY_ADDRESS, validatorAddress = MAINNET_ADDRESS_K1_VALIDATOR_ADDRESS, key = "nexus account", name = "Nexus Account", attesters: attesters_ = [RHINESTONE_ATTESTER_ADDRESS], attesterThreshold = 1, bootStrapAddress = NEXUS_BOOTSTRAP_ADDRESS, registryAddress = REGISTRY_ADDRESS, useTestBundler = false } = parameters; const useMeeAccount = addressEquals(validatorAddress, MEE_VALIDATOR_ADDRESS); // @ts-ignore const signer = await toSigner({ signer: _signer }); const walletClient = createWalletClient({ account: signer, chain, transport, key, name }).extend(publicActions); const signerAddress = walletClient.account.address; const publicClient = createPublicClient({ chain, transport }); const entryPointContract = getContract({ address: ENTRY_POINT_ADDRESS, abi: EntrypointAbi, client: { public: publicClient, wallet: walletClient } }); const factoryData = useMeeAccount ? await getMeeFactoryData({ signerAddress, index, publicClient, walletClient, bootStrapAddress, registryAddress, attesters: attesters_, attesterThreshold, validatorAddress }) : await getK1FactoryData({ signerAddress, index, attesters: attesters_, attesterThreshold }); /** * @description Gets the init code for the account * @returns The init code as a hexadecimal string */ const getInitCode = () => concatHex([factoryAddress, factoryData]); let _accountAddress = parameters.accountAddress; /** * @description Gets the counterfactual address of the account * @returns The counterfactual address * @throws {Error} If unable to get the counterfactual address */ const getCounterFactualAddress = async () => { if (!isNullOrUndefined(_accountAddress)) return _accountAddress; try { await entryPointContract.simulate.getSenderAddress([getInitCode()]); // biome-ignore lint/suspicious/noExplicitAny: <explanation> } catch (e) { if (e?.cause?.data?.errorName === "SenderAddressResult") { const accountAddressFromError = e?.cause.data.args[0]; if (!addressEquals(accountAddressFromError, zeroAddress)) { _accountAddress = accountAddressFromError; return accountAddressFromError; } } } const addressFromFactory = useMeeAccount ? await getMeeNexusAddress({ publicClient, signerAddress, index }) : await getK1NexusAddress({ publicClient, signerAddress, index, attesters: attesters_, threshold: attesterThreshold, factoryAddress }); if (!addressEquals(addressFromFactory, zeroAddress)) { _accountAddress = addressFromFactory; return addressFromFactory; } throw new Error("Failed to get counterfactual account address"); }; let module = module_ ?? toK1Validator({ address: validatorAddress, accountAddress: await getCounterFactualAddress(), initData: signerAddress, deInitData: "0x", signer }); /** * @description Checks if the account is deployed * @returns True if the account is deployed, false otherwise */ const isDeployed = async () => { const address = await getCounterFactualAddress(); const contractCode = await publicClient.getCode({ address }); return (contractCode?.length ?? 0) > 2; }; /** * @description Calculates the hash of a user operation * @param userOp - The user operation * @returns The hash of the user operation */ const getUserOpHash = (userOp) => getUserOperationHash({ chainId: chain.id, entryPointAddress: entryPoint07Address, entryPointVersion: "0.7", userOperation: userOp }); /** * @description Encodes a batch of calls for execution * @param calls - An array of calls to encode * @param mode - The execution mode * @returns The encoded calls */ const encodeExecuteBatch = async (calls, mode = EXECUTE_BATCH) => { const executionAbiParams = { type: "tuple[]", components: [ { name: "target", type: "address" }, { name: "value", type: "uint256" }, { name: "callData", type: "bytes" } ] }; const executions = calls.map((tx) => ({ target: tx.to, callData: tx.data ?? "0x", value: BigInt(tx.value ?? 0n) })); const executionCalldataPrep = encodeAbiParameters([executionAbiParams], [executions]); return encodeFunctionData({ abi: parseAbi([ "function execute(bytes32 mode, bytes calldata executionCalldata) external" ]), functionName: "execute", args: [mode, executionCalldataPrep] }); }; /** * @description Encodes a single call for execution * @param call - The call to encode * @param mode - The execution mode * @returns The encoded call */ const encodeExecute = async (call, mode = EXECUTE_SINGLE) => { const executionCalldata = encodePacked(["address", "uint256", "bytes"], [call.to, BigInt(call.value ?? 0n), (call.data ?? "0x")]); return encodeFunctionData({ abi: parseAbi([ "function execute(bytes32 mode, bytes calldata executionCalldata) external" ]), functionName: "execute", args: [mode, executionCalldata] }); }; /** * @description Gets the nonce for the account * @param parameters - Optional parameters for getting the nonce * @returns The nonce */ const getNonce = async (parameters) => { try { const TIMESTAMP_ADJUSTMENT = 16777215n; const defaultedKey = BigInt(parameters?.key ?? 0n) % TIMESTAMP_ADJUSTMENT; const defaultedValidationMode = parameters?.validationMode ?? "0x00"; const key = concat([ toHex(defaultedKey, { size: 3 }), defaultedValidationMode, parameters?.moduleAddress ?? module.address ]); const accountAddress = await getCounterFactualAddress(); return await entryPointContract.read.getNonce([ accountAddress, BigInt(key) ]); } catch (e) { return 0n; } }; /** * @description Changes the active module for the account * @param module - The new module to set as active * @returns void */ const setModule = (validationModule) => { module = validationModule; }; /** * @description Signs a message * @param params - The parameters for signing * @param params.message - The message to sign * @returns The signature */ const signMessage = async ({ message }) => { const tempSignature = await module.signMessage(message); const signature = encodePacked(["address", "bytes"], [module.address, tempSignature]); const erc6492Signature = concat([ encodeAbiParameters([ { type: "address", name: "create2Factory" }, { type: "bytes", name: "factoryCalldata" }, { type: "bytes", name: "originalERC1271Signature" } ], [factoryAddress, factoryData, signature]), MAGIC_BYTES ]); const accountIsDeployed = await isDeployed(); return accountIsDeployed ? signature : erc6492Signature; }; /** * @description Signs typed data * @param parameters - The typed data parameters * @returns The signature */ async function signTypedData(parameters) { const { message, primaryType, types: _types, domain } = parameters; if (!domain) throw new Error("Missing domain"); if (!message) throw new Error("Missing message"); const types = { EIP712Domain: getTypesForEIP712Domain({ domain }), ..._types }; // @ts-ignore: Comes from nexus parent typehash const messageStuff = message.stuff; // @ts-ignore validateTypedData({ domain, message, primaryType, types }); const appDomainSeparator = domainSeparator({ domain }); const accountDomainStructFields = await getAccountDomainStructFields(publicClient, await getCounterFactualAddress()); const parentStructHash = keccak256(encodePacked(["bytes", "bytes"], [ encodeAbiParameters(parseAbiParameters(["bytes32, bytes32"]), [ keccak256(toBytes(PARENT_TYPEHASH)), messageStuff ]), accountDomainStructFields ])); const wrappedTypedHash = eip712WrapHash(parentStructHash, appDomainSeparator); let signature = await module.signMessage({ raw: toBytes(wrappedTypedHash) }); const contentsType = toBytes(typeToString(types)[1]); const signatureData = concatHex([ signature, appDomainSeparator, messageStuff, toHex(contentsType), toHex(contentsType.length, { size: 2 }) ]); signature = encodePacked(["address", "bytes"], [module.address, signatureData]); return signature; } return toSmartAccount({ client: walletClient, entryPoint: { abi: EntrypointAbi, address: ENTRY_POINT_ADDRESS, version: "0.7" }, getAddress: getCounterFactualAddress, encodeCalls: (calls) => { return calls.length === 1 ? encodeExecute(calls[0]) : encodeExecuteBatch(calls); }, getFactoryArgs: async () => ({ factory: factoryAddress, factoryData }), getStubSignature: async () => module.getStubSignature(), signMessage, signTypedData, signUserOperation: async (parameters) => { const { chainId = publicClient.chain.id, ...userOpWithoutSender } = parameters; const address = await getCounterFactualAddress(); const userOperation = { ...userOpWithoutSender, sender: address }; const hash = getUserOperationHash({ chainId, entryPointAddress: entryPoint07Address, entryPointVersion: "0.7", userOperation }); return await module.signUserOpHash(hash); }, getNonce, extend: { entryPointAddress: entryPoint07Address, getCounterFactualAddress, isDeployed, getInitCode, encodeExecute, encodeExecuteBatch, getUserOpHash, setModule, getModule: () => module, factoryData, factoryAddress, validatorAddress, signer, walletClient, publicClient, attesters: attesters_, useTestBundler } }); }; //# sourceMappingURL=toNexusAccount.js.map