UNPKG

@biconomy/abstractjs

Version:

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

527 lines 18.9 kB
import { concat, createPublicClient, decodeAbiParameters, decodeFunctionResult, encodeAbiParameters, encodeFunctionData, erc20Abi, hexToBytes, isAddress, isHex, keccak256, numberToHex, pad, padHex, parseAbi, parseAbiParameters, publicActions, stringToBytes, toBytes, toHex } from "viem"; import { BICONOMY_TOKEN_PAYMASTER, MOCK_MULTI_MODULE_ADDRESS, MODULE_ENABLE_MODE_TYPE_HASH, NEXUS_DOMAIN_NAME, NEXUS_DOMAIN_TYPEHASH, NEXUS_DOMAIN_VERSION } from "../../account/utils/Constants.js"; import { MEEVersion } from "../../constants/index.js"; import { EIP1271Abi } from "../../constants/abi/index.js"; import { moduleTypeIds } from "../../modules/utils/Types.js"; import { isVersionOlder, versionIsAtLeast } from "./getVersion.js"; /** * Type guard to check if a value is null or undefined. * * @param value - The value to check * @returns True if the value is null or undefined */ export const isNullOrUndefined = (value) => { return value === null || value === undefined; }; /** * Checks if the provided value can be safely converted to a BigInt. * * @param value - The value to check for BigInt compatibility. * @returns True if the value can be converted to BigInt, false otherwise. */ export const isBigInt = (value) => { try { // Attempt to convert the value to BigInt. BigInt(value); return true; } catch { // If conversion fails, value is not a valid BigInt. return false; } }; export const toBytes32 = (value) => { if (typeof value === "boolean") { return padHex(toHex(value ? 1n : 0n), { size: 32 }); } if (typeof value === "bigint") { return padHex(toHex(value), { size: 32 }); } if (isAddress(value)) { return padHex(value, { size: 32 }); } if (isHex(value)) { return padHex(value, { size: 32 }); } throw new Error("Invalid value: must be boolean, bigint, address, or hex string"); }; /** * Removes all HTTP/HTTPS URLs from the provided string. * * @param value - The string to sanitize. * @returns The sanitized string with URLs removed. */ export const sanitizeUrl = (value) => { // Replace all occurrences of http:// or https:// followed by non-whitespace characters with an empty string. return value.replace(/https?:\/\/[^\s]+/g, ""); }; /** * Validates if a string is a valid RPC URL. * * @param url - The URL to validate * @returns True if the URL is a valid RPC endpoint */ export const isValidRpcUrl = (url) => { const regex = /^(http:\/\/|wss:\/\/|https:\/\/).*/; return regex.test(url); }; /** * Compares two addresses for equality, case-insensitive. * * @param a - First address * @param b - Second address * @returns True if addresses are equal */ export const addressEquals = (a, b) => !!a && !!b && a?.toLowerCase() === b.toLowerCase(); export const isNativeToken = (tokenAddress) => addressEquals(tokenAddress, "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee") || addressEquals(tokenAddress, "0x0000000000000000000000000000000000000000"); /** * The EIP-6492 magic suffix used to identify wrapped signatures. */ export const ERC6492_MAGIC_BYTES = "0x6492649264926492649264926492649264926492649264926492649264926492"; /** * Wraps a signature according to EIP-6492 specification. * * @param params - Parameters including factory address, calldata, and signature * @returns The wrapped signature */ export const wrapSignatureWith6492 = ({ factoryAddress, factoryCalldata, signature }) => { // wrap the signature as follows: https://eips.ethereum.org/EIPS/eip-6492 // concat( // abi.encode( // (create2Factory, factoryCalldata, originalERC1271Signature), // (address, bytes, bytes)), // magicBytes // ) return concat([ encodeAbiParameters(parseAbiParameters("address, bytes, bytes"), [ factoryAddress, factoryCalldata, signature ]), ERC6492_MAGIC_BYTES ]); }; /** * Unwraps an EIP-6492 signature if it's wrapped, otherwise returns the original signature. * * EIP-6492 signatures are used to validate signatures for contracts that haven't been deployed yet. * They wrap the original signature with factory deployment information. * * @param wrappedSignature - The potentially wrapped signature to unwrap * @returns An object containing the unwrapped signature and deployment information if wrapped * * @example * ```ts * const result = unwrapSignature6492(signature) * if (result.isWrapped) { * console.log('Factory:', result.factoryAddress) * console.log('Original signature:', result.originalSignature) * } else { * console.log('Not wrapped, using signature as-is') * } * ``` */ export const unwrapSignature6492 = (wrappedSignature) => { // Check if signature ends with EIP-6492 magic bytes if (!wrappedSignature.endsWith(ERC6492_MAGIC_BYTES.slice(2))) { return { isWrapped: false, originalSignature: wrappedSignature }; } // Remove magic bytes (32 bytes = 64 hex chars) const signatureWithoutMagic = wrappedSignature.slice(0, -64); // Decode the ABI-encoded data: (address, bytes, bytes) const decoded = decodeAbiParameters(parseAbiParameters("address, bytes, bytes"), signatureWithoutMagic); return { isWrapped: true, factoryAddress: decoded[0], factoryCalldata: decoded[1], originalSignature: decoded[2] }; }; /** * Calculates the percentage of a partial value relative to a total value. * * @param partialValue - The partial value * @param totalValue - The total value * @returns The percentage as a number */ export function percentage(partialValue, totalValue) { return (100 * partialValue) / totalValue; } /** * Converts a percentage to a factor (e.g., 50% -> 1.5). * * @param percentage - The percentage value (1-100) * @returns The converted factor * @throws If percentage is outside valid range */ export function convertToFactor(percentage) { // Check if the input is within the valid range if (percentage) { if (percentage < 1 || percentage > 100) { throw new Error("The percentage value should be between 1 and 100."); } // Calculate the factor const factor = percentage / 100 + 1; return factor; } return 1; } /** * Generates installation data and hash for module installation. * * @param accountOwner - The account owner address * @param modules - Array of modules with their types and configurations * @param domainName - Optional domain name * @param domainVersion - Optional domain version * @returns Tuple of [installData, hash] */ export function makeInstallDataAndHash(accountOwner, modules, domainName = NEXUS_DOMAIN_NAME, domainVersion = NEXUS_DOMAIN_VERSION) { const types = modules.map((module) => BigInt(moduleTypeIds[module.type])); const initDatas = modules.map((module) => toHex(concat([toBytes(BigInt(moduleTypeIds[module.type])), module.config]))); const multiInstallData = encodeAbiParameters([{ type: "uint256[]" }, { type: "bytes[]" }], [types, initDatas]); const structHash = keccak256(encodeAbiParameters([{ type: "bytes32" }, { type: "address" }, { type: "bytes32" }], [ MODULE_ENABLE_MODE_TYPE_HASH, MOCK_MULTI_MODULE_ADDRESS, keccak256(multiInstallData) ])); const hashToSign = _hashTypedData(structHash, domainName, domainVersion, accountOwner); return [multiInstallData, hashToSign]; } export function _hashTypedData(structHash, name, version, verifyingContract) { const DOMAIN_SEPARATOR = keccak256(encodeAbiParameters([ { type: "bytes32" }, { type: "bytes32" }, { type: "bytes32" }, { type: "address" } ], [ keccak256(stringToBytes(NEXUS_DOMAIN_TYPEHASH)), keccak256(stringToBytes(name)), keccak256(stringToBytes(version)), verifyingContract ])); return keccak256(concat([ stringToBytes("\x19\x01"), hexToBytes(DOMAIN_SEPARATOR), hexToBytes(structHash) ])); } export function getTypesForEIP712Domain({ domain }) { return [ typeof domain?.name === "string" && { name: "name", type: "string" }, domain?.version && { name: "version", type: "string" }, typeof domain?.chainId === "number" && { name: "chainId", type: "uint256" }, domain?.verifyingContract && { name: "verifyingContract", type: "address" }, domain?.salt && { name: "salt", type: "bytes32" } ].filter(Boolean); } /** * Retrieves account metadata including name, version, and chain ID. * * @param client - The viem Client instance * @param accountAddress - The account address to query * @returns Promise resolving to account metadata */ export const getAccountMeta = async (client, accountAddress) => { try { const domain = await client.request({ method: "eth_call", params: [ { to: accountAddress, data: encodeFunctionData({ abi: EIP1271Abi, functionName: "eip712Domain" }) }, "latest" ] }); if (domain !== "0x") { const decoded = decodeFunctionResult({ abi: EIP1271Abi, functionName: "eip712Domain", data: domain }); return { name: decoded?.[1], version: decoded?.[2], chainId: decoded?.[3] }; } } catch (error) { } return { name: NEXUS_DOMAIN_NAME, version: NEXUS_DOMAIN_VERSION, chainId: client.chain ? BigInt(client.chain.id) : BigInt(await client.extend(publicActions).getChainId()) }; }; /** * Wraps a typed data hash with EIP-712 domain separator. * * @param typedHash - The hash to wrap * @param appDomainSeparator - The domain separator * @returns The wrapped hash */ export const eip712WrapHash = (typedHash, appDomainSeparator) => keccak256(concat(["0x1901", appDomainSeparator, typedHash])); export function typeToString(typeDef) { return Object.entries(typeDef).map(([key, fields]) => { const fieldStrings = (fields ?? []) .map((field) => `${field.type} ${field.name}`) .join(","); return `${key}(${fieldStrings})`; }); } /** @ignore */ export function bigIntReplacer(_key, value) { return typeof value === "bigint" ? value.toString() : value; } export function numberTo3Bytes(key) { // todo: check range const buffer = new Uint8Array(3); buffer[0] = Number((key >> 16n) & 0xffn); buffer[1] = Number((key >> 8n) & 0xffn); buffer[2] = Number(key & 0xffn); return buffer; } export function toHexString(byteArray) { return Array.from(byteArray) .map((byte) => byte.toString(16).padStart(2, "0")) // Convert each byte to hex and pad to 2 digits .join(""); // Join all hex values together into a single string } export const getAccountDomainStructFields = async (publicClient, accountAddress) => { const accountDomainStructFields = (await publicClient.readContract({ address: accountAddress, abi: parseAbi([ "function eip712Domain() public view returns (bytes1 fields, string memory name, string memory version, uint256 chainId, address verifyingContract, bytes32 salt, uint256[] memory extensions)" ]), functionName: "eip712Domain" })); const [, name, version, chainId, verifyingContract, salt] = accountDomainStructFields; const params = parseAbiParameters([ "bytes32", "bytes32", "uint256", "address", "bytes32" ]); return encodeAbiParameters(params, [ keccak256(toBytes(name)), keccak256(toBytes(version)), chainId, verifyingContract, salt ]); }; export const inProduction = () => { try { return process?.env?.environment === "production"; } catch (e) { return true; } }; export const playgroundTrue = () => { try { return process?.env?.RUN_PLAYGROUND === "true"; } catch (e) { return false; } }; export const getTenderlyDetails = () => { try { const accountSlug = process?.env?.TENDERLY_ACCOUNT_SLUG; const projectSlug = process?.env?.TENDERLY_PROJECT_SLUG; const apiKey = process?.env?.TENDERLY_API_KEY; if (!accountSlug || !projectSlug || !apiKey) { return null; } return { accountSlug, projectSlug, apiKey }; } catch (e) { return null; } }; /** * Safely multiplies a bigint by a number, rounding appropriately. * * @param bI - The bigint to multiply * @param multiplier - The multiplication factor * @returns The multiplied bigint */ export const safeMultiplier = (bI, multiplier) => BigInt(Math.round(Number(bI) * multiplier)); export const getAllowance = async (client, owner, tokenAddress, spender = BICONOMY_TOKEN_PAYMASTER) => { const approval = await client.readContract({ address: tokenAddress, abi: erc20Abi, functionName: "allowance", args: [owner, spender] }); return approval; }; export function parseRequestArguments(input) { const fieldsToOmit = [ "callGasLimit", "preVerificationGas", "maxFeePerGas", "maxPriorityFeePerGas", "paymasterAndData", "verificationGasLimit" ]; // Skip the first element which is just "Request Arguments:" const argsString = input.slice(1).join(""); // Split by newlines and filter out empty lines const lines = argsString.split("\n").filter((line) => line.trim()); // Create an object from the key-value pairs const result = lines.reduce((acc, line) => { // Remove extra spaces and split by ':' const [key, value] = line.split(":").map((s) => s.trim()); // Clean up the key (remove trailing spaces and colons) const cleanKey = key.trim(); // Clean up the value (remove 'gwei' and other units) const cleanValue = value.replace("gwei", "").trim(); if (fieldsToOmit.includes(cleanKey)) { return acc; } acc[cleanKey] = cleanValue; return acc; }, {}); return result; } /** * Checks if a chain supports Cancun. * * @param transport - The transport to use * @param chain - The chain to check * @returns True if the chain supports Cancun, false otherwise */ export async function supportsCancun({ transport, chain }) { const cancunSupportedChains = { "1": true, "11155111": true, "8453": true, "84532": true, "137": true, "80002": true, "42161": true, "421614": true, "10": true, "11155420": true, "56": true, "97": true, "146": true, "57054": true, "534352": true, "534351": true, "100": true, "10200": true, "43114": true, "43113": true, "33139": true, "33111": true, "999": true, "1116": true, "267": true, "1329": true, "1328": true, "130": true, "1301": true, "747474": true, "1135": true, "480": true, "4801": true, "20993": true, "10143": true, "143": true, "88882": false, "531050204": true, "5010405": true }; if (cancunSupportedChains[chain.id.toString()]) { return cancunSupportedChains[chain.id.toString()]; } const client = createPublicClient({ chain, transport }); // Fetch the latest block with full transactions const block = await client.getBlock({ blockTag: "latest" }); // Check for Cancun-specific block fields if (block.blobGasUsed !== undefined || block.excessBlobGas !== undefined) { return true; } return false; } /** * Calculate the storage slot for nonces in EntryPoint V7 * Following exact Solidity storage layout rules from docs */ export function calculateNonceStorageSlot(sender, key = 0n) { const BASE_SLOT = 1; // nonceSequenceNumber is at slot 1 // Step 1: Calculate slot for first mapping level // keccak256(abi.encode(address_key, uint256_slot)) // Both address and slot are padded to 32 bytes const firstLevelSlot = keccak256(concat([ pad(sender, { size: 32 }), // address padded to 32 bytes pad(numberToHex(BASE_SLOT), { size: 32 }) // slot padded to 32 bytes ])); // Step 2: Calculate slot for second mapping level // keccak256(abi.encode(uint192_key, bytes32_slot)) const finalSlot = keccak256(concat([ pad(numberToHex(key), { size: 32 }), // uint192 key padded to 32 bytes firstLevelSlot // already 32 bytes ])); return finalSlot; } /** * Checks if the account has consistent MEE versions across deployments for signing the quote. * * Version < 2.2.0 uses personal_sign, version >= 2.2.0 uses EIP-712 signing. * Mixing these two signing methods is not allowed as it would result in incompatible signatures. * * For EIP-712 versions (>= 2.2.0), all MEE versions must be the same to ensure consistent * signature validation across chains. * * @param meeVersions - Array of chain IDs with their corresponding MEE versions * @throws Error if versions are inconsistent or mixing signing methods */ export function validateConsistentMeeVersions(meeVersions) { let hasPersonalSignVersion = false; let hasEIP712Version = false; // Track version consistency while iterating (fail-fast approach) for (const { version: meeVersionConfig } of meeVersions) { const meeVersion = meeVersionConfig.version; // Check version type as we go const isCurrentVersionPersonalSign = isVersionOlder(meeVersion, MEEVersion.V2_2_1); const isCurrentVersionEIP712 = versionIsAtLeast(meeVersion, MEEVersion.V2_2_1); // Update flags hasPersonalSignVersion = hasPersonalSignVersion || isCurrentVersionPersonalSign; hasEIP712Version = hasEIP712Version || isCurrentVersionEIP712; // Early exit: Cannot mix personal_sign and EIP-712 signing methods if (hasPersonalSignVersion && hasEIP712Version) { throw new Error("MEE versions on all chains should be whether less than 2.2.0 or greater than or equal to 2.2.0. " + "Otherwise Multichain account won't be able to consume the same signature across all chains involved in the quote request."); } } } //# sourceMappingURL=Utils.js.map