UNPKG

@biconomy/abstractjs

Version:

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

908 lines 45.7 kB
import { pad, toHex } from "viem"; import { buildComposable, formatCallDataInputParamsWithVersion } from "../../../account/decorators/index.js"; import { addressEquals, calculateNonceStorageSlot, isBigInt, isNativeToken, validateConsistentMeeVersions } from "../../../account/utils/Utils.js"; import { batchInstructions } from "../../../account/utils/batchInstructions.js"; import { LARGE_DEFAULT_GAS_LIMIT } from "../../../account/utils/getMultichainContract.js"; import { resolveInstructions } from "../../../account/utils/resolveInstructions.js"; import { ComposabilityVersion, SMART_SESSIONS_ADDRESS, SmartSessionMode } from "../../../constants/index.js"; import { greaterThanOrEqualTo, runtimeERC20BalanceOf, runtimeNativeBalanceOf, runtimeNonceOf } from "../../../modules/utils/composabilityCalls.js"; import createHttpClient, {} from "../../createHttpClient.js"; import { DEFAULT_MEE_SPONSORSHIP_CHAIN_ID, DEFAULT_MEE_SPONSORSHIP_PAYMASTER_ACCOUNT, DEFAULT_MEE_SPONSORSHIP_TOKEN_ADDRESS, DEFAULT_PATHFINDER_URL, getDefaultMEENetworkUrl } from "../../createMeeClient.js"; export const USEROP_MIN_EXEC_WINDOW_DURATION = 180; export const CLEANUP_USEROP_EXTENDED_EXEC_WINDOW_DURATION = USEROP_MIN_EXEC_WINDOW_DURATION / 2; export const DEFAULT_GAS_LIMIT = 75000n; export const DEFAULT_VERIFICATION_GAS_LIMIT = 150000n; /** * Requests a quote from the MEE service for executing a set of instructions. * This function handles the complexity of creating a supertransaction quote * that can span multiple chains. * * @param client - MEE client instance used to make the request * @param parameters - Parameters for the quote request * @returns Promise resolving to a committed supertransaction quote * * @example * ```typescript * const quote = await getQuote(meeClient, { * instructions: [{ * calls: [{ * to: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", * data: "0x...", * value: 0n * }], * chainId: 1 // Ethereum Mainnet * }], * feeToken: { * address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC * chainId: 1 * } * }); * ``` * * @throws Will throw an error if: * - The account is not deployed on required chains * - The fee token is not supported * - The chain(s) are not supported by the node */ export const getQuote = async (client, parameters, quoteType = "simple", trigger) => { const { account: account_ = client.account, instructions, cleanUps, feePayer, path = "quote", lowerBoundTimestamp: lowerBoundTimestamp_ = Math.floor(Date.now() / 1000), upperBoundTimestamp: upperBoundTimestamp_ = lowerBoundTimestamp_ + USEROP_MIN_EXEC_WINDOW_DURATION, executionSimulationRetryDelay: executionSimulationRetryDelay_, delegate = false, authorizations = [], multichain7702Auth = false, moduleAddress, batch = true, simulation, verificationGasLimit, shortEncodingSuperTxn = false, sponsorship = false, sponsorshipOptions, feeToken, sessionDetails, smartSessionMode } = parameters; const mode = smartSessionMode === "ENABLE_AND_USE" ? SmartSessionMode.UNSAFE_ENABLE : SmartSessionMode.USE; let resolvedInstructions = await resolveInstructions(instructions); // If there is no metadata is configured by the SDK or developer ? Custom metadata will be added always resolvedInstructions = resolvedInstructions.map((instruction) => { if (!instruction.metadata || instruction.metadata.length === 0) { return { ...instruction, metadata: [ { type: "CUSTOM", description: "Custom on-chain action", chainId: instruction.chainId } ] }; } return instruction; }); let finalInstructions = resolvedInstructions; const meeVersions = getMeeVersionsForQuoteRequest(account_, resolvedInstructions, sponsorship, feeToken); // By default, all the main instructions are batched if (batch) { finalInstructions = await batchInstructions({ accountAddress: account_.signer.address, instructions: [...resolvedInstructions], meeVersions // TODO: check if we can just pass empty array here because MeeVerions are not used when batching. Will it improve performance? }); } // if feePayer is provided, we need to use the /quote-permit path let pathToQuery = path; if (feePayer) { pathToQuery = "/quote-permit"; } const validUserOps = finalInstructions.every((userOp) => account_.deploymentOn(userOp.chainId) && client.info.supportedChains .map(({ chainId }) => +chainId) .includes(userOp.chainId)); if (!validUserOps) { throw Error(`User operation chain(s) not supported by the node: ${finalInstructions .map((x) => x.chainId) .join(", ")}`); } const hasProcessedInitData = []; const hasProcessedSessionDetails = new Set(); const initDataTypeByChainId = new Map(); const sprtxChainIdsSet = new Set([]); // For non sponsored flow, fee token chainId needs to be included if (feeToken) sprtxChainIdsSet.add(feeToken.chainId); // Chains IDS from instructions are considered for (const inx of finalInstructions) { sprtxChainIdsSet.add(inx.chainId); } const sprtxChainIds = [...sprtxChainIdsSet]; if (delegate) { if (multichain7702Auth) { // Check for all the nonces are same only if there is more than one chain is involved in the sprtx if (sprtxChainIds.length > 1) { const noncesAndChainIds = await Promise.all(sprtxChainIds.map(async (chainId) => { const { publicClient, walletClient: { account: { address } } } = account_.deploymentOn(chainId, true); return { chainId, nonce: await publicClient.getTransactionCount({ address }) }; })); const nonceCountMap = noncesAndChainIds.reduce((map, { nonce }) => { map.set(nonce, (map.get(nonce) || 0) + 1); return map; }, new Map()); // Chains with different nonces needs different authorizations const noncesAndChainIdsWithUniqueNonces = noncesAndChainIds.filter((info) => nonceCountMap.get(info.nonce) === 1); // Chains with same nonces can reuse the same authorization which is signed only once const noncesAndChainIdsWithSameNonces = noncesAndChainIds.filter((info) => nonceCountMap.get(info.nonce) > 1); // If custom authorizations are passed, a series of validation is conducted here if (authorizations.length > 0) { // If noncesAndChainIdsWithUniqueNonces length is zero ? It means all the nonces are same and can be used for multichain // It is expected to pass only one auth from outside the SDK. if (noncesAndChainIdsWithUniqueNonces.length === 0 && authorizations.length > 1) { throw new Error("Invalid authorizations: The nonce for all the chains are same and only one multichain authorization is expected"); } // If multichain nonce are not same and custom auth are passed ? The auth should be sufficient orelse error will be thrown if (noncesAndChainIdsWithUniqueNonces.length > 0) { const missingAuthsByChainId = []; for (const { chainId } of noncesAndChainIdsWithUniqueNonces) { const isAuthProvided = authorizations.some((auth) => { return auth.chainId === chainId; }); if (!isAuthProvided) missingAuthsByChainId.push(chainId); } if (missingAuthsByChainId.length > 0) { throw new Error(`Invalid authorizations: The nonce for all the chains are not same. You need to pass specific authorizations for the following chains: ${missingAuthsByChainId.join(", ")}`); } } // For same multichain nonces ? Check for auth with zero id and throw error if it is not there if (noncesAndChainIdsWithSameNonces.length > 0) { const isAuthProvided = authorizations.some((auth) => { return auth.chainId === 0; }); if (!isAuthProvided) { const chainIds = noncesAndChainIdsWithSameNonces.map((auth) => auth.chainId); throw new Error(`Invalid authorizations: The nonce for some of the chains are same. Missing multichain authorization for the following chains: ${chainIds.join(", ")}`); } } } for (const chainId of sprtxChainIds) { const [isMultichainAuth] = noncesAndChainIdsWithSameNonces.filter((info) => info.chainId === chainId); initDataTypeByChainId.set(chainId, isMultichainAuth ? "MULTI_CHAIN_AUTH" : "SINGLE_CHAIN_AUTH"); } } else { if (authorizations.length > 1) { throw new Error("Invalid authorizations: The nonce for all the chains are same and only one multichain authorization is expected"); } if (authorizations.length === 1 && authorizations[0].chainId !== 0) { throw new Error("Invalid authorizations: Multichain authorization should be signed with chain ID zero"); } // If only one chain is invloved. It can be directly treated as multichain for (const chainId of sprtxChainIds) { initDataTypeByChainId.set(chainId, "MULTI_CHAIN_AUTH"); } } } else { // If custom authorizations are passed, a series of validation is conducted here if (authorizations.length > 0) { const missingAuthsByChainId = []; for (const chainId of sprtxChainIds) { const isAuthProvided = authorizations.some((auth) => { return auth.chainId === chainId; }); if (!isAuthProvided) missingAuthsByChainId.push(chainId); } if (missingAuthsByChainId.length > 0) { throw new Error(`Authorizations are missing for the following chains: ${missingAuthsByChainId.join(", ")}`); } } // All the auths will be treated as single chain auth without chain id zero for (const chainId of sprtxChainIds) { initDataTypeByChainId.set(chainId, "SINGLE_CHAIN_AUTH"); } } } else { // No auth at all. Init code will be added if account is not deployed for (const chainId of sprtxChainIds) { initDataTypeByChainId.set(chainId, "INIT_CODE"); } } const [{ paymentInfo, isInitDataProcessed, isSessionDetailsProcessed }, preparedUserOps] = await Promise.all([ preparePaymentInfo(client, { ...parameters, initDataTypeByChainId }), prepareUserOps(account_, finalInstructions, false, moduleAddress) ]); let multichainEIP7702Auth = undefined; const paymentAuthType = initDataTypeByChainId.get(Number(paymentInfo.chainId)); // If payment info has eip7702 auth ? It is an non sponsored flow and auth is prepared // If it is multichain auth and eip7702Auth prepared by either custom auth or SDK signed one // It will be used for other userOp for delegation. if (paymentInfo.eip7702Auth && paymentAuthType === "MULTI_CHAIN_AUTH") { multichainEIP7702Auth = paymentInfo.eip7702Auth; } if (isInitDataProcessed) hasProcessedInitData.push(Number(paymentInfo.chainId)); if (isSessionDetailsProcessed) hasProcessedSessionDetails.add(paymentInfo.chainId); // If cleanup is configured, the cleanup userops will be appended to the existing userops // Every cleanup is a separate user op and will be executed if certain conditions met if (cleanUps && cleanUps.length > 0) { const userOpsNonceInfo = preparedUserOps.map(([, { nonceKey, nonce }]) => ({ nonce, nonceKey })); const result = await prepareCleanUpUserOps(account_, userOpsNonceInfo, cleanUps, moduleAddress); preparedUserOps.push(...result); } // complete the userOps including cleanup ones const indexPerChainId = new Map(); const userOps = await Promise.all(preparedUserOps.map(async ([callData, { nonce }, isAccountDeployed, initCode, sender, callGasLimit, chainId, isCleanUpUserOp, nexusAccount, shortEncoding, metadata, simulationOverrides, lowerBoundTimestamp, upperBoundTimestamp, executionSimulationRetryDelay]) => { let initDataOrUndefined = undefined; if (!indexPerChainId.has(chainId)) { indexPerChainId.set(chainId, 0); } // If account is not deployed, either initCode or eip7702Auth needs to be attached. // If init code or 7702 auth is already added for the chain ? Skip this if (!isAccountDeployed && !hasProcessedInitData.includes(Number(chainId))) { // Mark as initData processed hasProcessedInitData.push(Number(chainId)); const authType = initDataTypeByChainId.get(Number(chainId)); // If multichain EIP7702 auth is available ? It means, 7702 mode and no initCode is there. if (authType === "MULTI_CHAIN_AUTH") { // Apply existing multichain auth where the chain Ids were same. So no need multiple auths to be signed if (multichainEIP7702Auth) { initDataOrUndefined = { eip7702Auth: multichainEIP7702Auth }; } else { // This multichain auth will be used for the current chains and other chains which has the same nonce multichainEIP7702Auth = await prepare7702Auth(nexusAccount, Number(chainId), initDataTypeByChainId, authorizations); initDataOrUndefined = { eip7702Auth: multichainEIP7702Auth }; } } else if (authType === "SINGLE_CHAIN_AUTH") { initDataOrUndefined = { eip7702Auth: await prepare7702Auth(nexusAccount, Number(chainId), initDataTypeByChainId, authorizations) }; } else { initDataOrUndefined = { initCode }; } } const resolvedVerificationGasLimit = resolveVerificationGasLimit({ moduleAddress, verificationGasLimit, sponsorship, index: indexPerChainId.get(chainId), paymentChainId: paymentInfo.chainId, currentChainId: chainId }); indexPerChainId.set(chainId, indexPerChainId.get(chainId) + 1); let sessionDetail = undefined; if (sessionDetails) { // Find session details for this chain const relevantIndex = sessionDetails.findIndex(({ enableSessionData }) => enableSessionData?.enableSession?.sessionToEnable?.chainId === BigInt(chainId)); if (relevantIndex === -1) { throw new Error(`No session details found for chainId ${chainId}`); } const isFirstTimeForChain = !hasProcessedSessionDetails.has(chainId); const dynamicMode = isFirstTimeForChain ? mode : SmartSessionMode.USE; sessionDetail = { ...sessionDetails[relevantIndex], mode: dynamicMode }; hasProcessedSessionDetails.add(chainId); } return { // Instruction level timebound will be considered first lowerBoundTimestamp: lowerBoundTimestamp || lowerBoundTimestamp_, upperBoundTimestamp: isCleanUpUserOp ? upperBoundTimestamp_ + CLEANUP_USEROP_EXTENDED_EXEC_WINDOW_DURATION : // Instruction level timebound will be considered first upperBoundTimestamp || upperBoundTimestamp_, // Instruction level execution simulation retry delay will be considered first and then global config executionSimulationRetryDelay: executionSimulationRetryDelay || executionSimulationRetryDelay_, sender, callData, callGasLimit, nonce: nonce.toString(), chainId, isCleanUpUserOp, ...initDataOrUndefined, ...resolvedVerificationGasLimit, shortEncoding: shortEncodingSuperTxn || shortEncoding, sessionDetails: sessionDetail, metadata, simulationOverrides }; })); // in `simple` mode, we should validate the consistent MEE versions across all chains // because simple mode signatures can be incompatible b/w some mee versions if (quoteType === "simple") { validateConsistentMeeVersions(meeVersions); } const quoteRequest = { quoteType, userOps, paymentInfo, meeVersions, simulation, trigger, tags: parameters.tags }; let quote = await client.request({ path: pathToQuery, body: quoteRequest }); if (sponsorship && sponsorshipOptions) { // Both prod and staging network url is considered as biconomy hosted sponsorship service const isSelfHostedSponsorship = ![ getDefaultMEENetworkUrl(false), // Prod getDefaultMEENetworkUrl(true) // Staging ].includes(sponsorshipOptions.url); if (isSelfHostedSponsorship) { const selfHostedClient = createHttpClient(sponsorshipOptions.url, undefined, client.info.isDebugMode); quote = await selfHostedClient.request({ path: `sponsorship/sign/${sponsorshipOptions.gasTank.chainId}/${sponsorshipOptions.gasTank.address}`, method: "POST", body: quote, ...(sponsorshipOptions.customHeaders ? { headers: sponsorshipOptions.customHeaders } : {}) }); } } return quote; }; const preparePaymentInfo = async (client, parameters) => { const { account: account_ = client.account, eoa, feeToken, feePayer, gasLimit, authorizations = [], sponsorship, sponsorshipOptions, shortEncodingSuperTxn, moduleAddress, sessionDetails, smartSessionMode = "USE", verificationGasLimit } = parameters; let paymentInfo = undefined; let isInitDataProcessed = false; let isSessionDetailsProcessed = false; const eoaOrFeePayer = feePayer || eoa; if (sponsorship) { // For sponsorship, the sender should be the sponsorship SCA which will bare the gas payment for developers let sender = DEFAULT_MEE_SPONSORSHIP_PAYMASTER_ACCOUNT; let token = DEFAULT_MEE_SPONSORSHIP_TOKEN_ADDRESS; let chainId = DEFAULT_MEE_SPONSORSHIP_CHAIN_ID; let sponsorshipUrl = DEFAULT_PATHFINDER_URL; if (sponsorshipOptions) { sender = sponsorshipOptions.gasTank.address; token = sponsorshipOptions.gasTank.token; chainId = sponsorshipOptions.gasTank.chainId; sponsorshipUrl = sponsorshipOptions.url; } const isBiconomyHostedSponsorship = [ getDefaultMEENetworkUrl(false), // Prod getDefaultMEENetworkUrl(true) // Staging ].includes(sponsorshipUrl); // Biconomy hosted sponsorship will be considered as trusted sponsorship const isTrustedSponsorship = sender.toLowerCase() === DEFAULT_MEE_SPONSORSHIP_PAYMASTER_ACCOUNT.toLowerCase() && isBiconomyHostedSponsorship; let nonce = "0"; // If it is not an trusted sponsorship ? The nonce will be fetched from third party sponsorship backend if (!isTrustedSponsorship) { const sponsorshipClient = createHttpClient(sponsorshipUrl, undefined, client.info.isDebugMode); const nonceInfo = await sponsorshipClient.request({ path: `sponsorship/nonce/${chainId}/${sender}`, method: "GET", ...(sponsorshipOptions?.customHeaders ? { headers: sponsorshipOptions.customHeaders } : {}) }); nonce = nonceInfo.nonce; } paymentInfo = { sponsored: true, sender, token, nonce, callGasLimit: gasLimit || DEFAULT_GAS_LIMIT, verificationGasLimit: DEFAULT_VERIFICATION_GAS_LIMIT, // when sponsored, this will be set by the node chainId: chainId.toString(), sponsorshipUrl, ...(eoaOrFeePayer ? { eoa: eoaOrFeePayer } : {}), // For sponsorship, the sponsorship paymaster EOA is always assumed to be deployed and funded already // So initCode will be always undefined initCode: undefined // no short encodings }; // Init code / authorization list will not be added to payment userOp in the case of sponsorship. It will be added in the // first developer defined userOp. To make this happen, this field should be false isInitDataProcessed = false; } else { // No sponsorship if (!feeToken) throw Error("Fee token should be configured"); const validPaymentAccount = account_.deploymentOn(feeToken.chainId); if (!validPaymentAccount) { throw Error(`Account is not deployed on necessary chain(s) ${feeToken.chainId}`); } const validFeeToken = validPaymentAccount && client.info.supportedGasTokens .map(({ chainId }) => +chainId) .includes(feeToken.chainId); if (!validFeeToken) { throw Error(`Fee token ${feeToken.address} is not supported on this chain: ${feeToken.chainId}`); } const [nonce, isAccountDeployed, initCode] = await Promise.all([ validPaymentAccount.getNonceWithKey(validPaymentAccount.address, { moduleAddress }), validPaymentAccount.isDeployed(), validPaymentAccount.getInitCode() ]); // Do authorization only if required as it requires signing let initData = undefined; if (!isAccountDeployed) { const initDataType = parameters.initDataTypeByChainId.get(feeToken.chainId); if (initDataType === "INIT_CODE") { initData = { initCode }; } else { initData = { eip7702Auth: await prepare7702Auth(validPaymentAccount, feeToken.chainId, parameters.initDataTypeByChainId, authorizations) }; } } // for non-sponsored superTxn, the verification gas limit is resolved here const paymentVerificationGasLimit = resolvePaymentUserOpVerificationGasLimitNonSponsored(moduleAddress, verificationGasLimit); let sessionDetail = undefined; // Adding session details on quote to enable node to detect smart session flow on quote phase + use this for simulations if (sessionDetails) { // Find session details for this chain const relevantIndex = sessionDetails.findIndex(({ enableSessionData }) => enableSessionData?.enableSession?.sessionToEnable?.chainId === BigInt(feeToken.chainId)); if (relevantIndex === -1) { throw new Error(`No session details found for chainId ${feeToken.chainId}`); } if (!smartSessionMode) { throw new Error("smartSessionMode is required for smart sessions flow"); } // Mostly use and enable mode for payment userOp const mode = smartSessionMode === "ENABLE_AND_USE" ? SmartSessionMode.UNSAFE_ENABLE : SmartSessionMode.USE; sessionDetail = { ...sessionDetails[relevantIndex], mode }; } paymentInfo = { sponsored: false, sender: validPaymentAccount.address, token: feeToken.address, nonce: nonce.nonce.toString(), callGasLimit: gasLimit || DEFAULT_GAS_LIMIT, verificationGasLimit: paymentVerificationGasLimit?.verificationGasLimit || DEFAULT_VERIFICATION_GAS_LIMIT, chainId: feeToken.chainId.toString(), ...(feeToken.gasRefundAddress ? { gasRefundAddress: feeToken.gasRefundAddress } : {}), ...(eoaOrFeePayer ? { eoa: eoaOrFeePayer } : {}), ...initData, shortEncoding: shortEncodingSuperTxn, sessionDetails: sessionDetail }; // Init code / authorization list will added to payment userOp. To prevent adding the init code / authList // in developer defined userOps, this field must be true isInitDataProcessed = true; isSessionDetailsProcessed = true; } if (!paymentInfo) throw new Error("Failed to generate payment info"); return { paymentInfo, isInitDataProcessed, isSessionDetailsProcessed }; }; const prepare7702Auth = async (smartAccount, chainId, initDataTypeByChainId, customAuthorizations = []) => { let eip7702Auth; const authType = initDataTypeByChainId.get(chainId); if (authType === "MULTI_CHAIN_AUTH") { // if it is multichain auth ? custom auth will be filtered with zero chain id const [authorization] = customAuthorizations.filter((auth) => auth.chainId === 0); eip7702Auth = await smartAccount.toDelegation(authorization ? { authorization } : { multiChain: true }); } else if (authType === "SINGLE_CHAIN_AUTH") { // if it is not multichain auth ? custom auth will be filtered for specific chain const [authorization] = customAuthorizations.filter((auth) => { return auth.chainId === Number(chainId); }); eip7702Auth = await smartAccount.toDelegation(authorization ? { authorization } : { chainId }); } else { // This should never happen in theory throw new Error("Invalid authorization type"); } return eip7702Auth; }; const prepareUserOps = async (account, instructions, isCleanUpUserOps = false, validatorAddress) => { return await Promise.all(instructions.map((instruction) => { const deployment = account.deploymentOn(instruction.chainId, true); const accountAddress = account.addressOn(instruction.chainId, true); let callsPromise; if (instruction.isComposable) { callsPromise = deployment.encodeExecuteComposable(instruction.calls); } else { callsPromise = instruction.calls.length > 1 ? deployment.encodeExecuteBatch(instruction.calls) : deployment.encodeExecute(instruction.calls[0]); } // This is the place to set the short encoding flag // It can be based on the module address or on the instruction type // Currently instructions are for the userOps only // That's why this function is called prepareUserOps // However it is possible that a superTxn consists not of the userOps only // So for example signed EIP-712 data structs or // ERC-7683 Cross-chain intents can be included in the superTxn // And this function will have to convert them out of instructions // Such 'off-chain' entities will have to be used with short encoding flag // We just set it to `false` for now const shortEncoding = false; return Promise.all([ callsPromise, deployment.getNonceWithKey(accountAddress, { moduleAddress: validatorAddress }), deployment.isDeployed(), deployment.getInitCode(), deployment.address, instruction.calls .map((uo) => uo?.gasLimit ?? LARGE_DEFAULT_GAS_LIMIT) .reduce((curr, acc) => curr + acc, 0n) .toString(), instruction.chainId.toString(), isCleanUpUserOps, deployment, shortEncoding, instruction.metadata, instruction.simulationOverrides, instruction.lowerBoundTimestamp, instruction.upperBoundTimestamp, instruction.executionSimulationRetryDelay ]); })); }; export const userOp = (userOpIndex) => { if (userOpIndex <= 0) throw new Error("UserOp index should be greater than zero"); // During the userop building, the payment user ops is not available. But the slot 1 is always reserved for payment userop // as standard practise. Hence, the userOp will indirectly implies this by -1 which yields the first userop defined by devs return userOpIndex - 1; }; const prepareCleanUpUserOps = async (account, userOpsNonceInfo, cleanUps, moduleAddress) => { const meeVersions = account.deployments.map(({ version, chain }) => ({ chainId: chain.id, version })); const cleanUpInstructions = await Promise.all(cleanUps.map(async (cleanUp) => { let cleanUpInstruction; const { version, entryPoint } = account.deploymentOn(cleanUp.chainId, true); const composabilityVersion = version.composabilityVersion; // Initialize simulation overrides; Used for simulation overrides of cleanUp userOps const tokenOverrides = []; const customOverrides = []; let simulationTokenOverrideAmount; if (isNativeToken(cleanUp.tokenAddress)) { if (!isBigInt(cleanUp.amount) || cleanUp.amount === 0n) { if (composabilityVersion === ComposabilityVersion.V1_0_0) { throw new Error("Native token cleanup with runtime-injected amount is not supported for Composability v1.0.0"); } // If the amount is not a bigint, or is 0, then build a runtime injected cleanup let amount; if (cleanUp.amount === undefined || cleanUp.amount === 0n) { // if it is not properly supplied as runtime value, // then use the runtime native balance of the account amount = runtimeNativeBalanceOf({ targetAddress: account.addressOn(cleanUp.chainId, true) }); } else { // else just use provided runtime value amount = cleanUp.amount; } const [cleanUpNativeTransferInstruction] = await buildComposable({ accountAddress: account.signer.address, currentInstructions: [], meeVersions }, { type: "nativeTokenTransfer", data: { to: cleanUp.recipientAddress, value: amount, chainId: cleanUp.chainId, ...(cleanUp.gasLimit ? { gasLimit: cleanUp.gasLimit } : {}) } }, composabilityVersion); cleanUpInstruction = cleanUpNativeTransferInstruction; simulationTokenOverrideAmount = 1n; // default to 1 wei if runtime balance is used } else { const amount = cleanUp.amount; const [cleanUpNativeTransferInstruction] = await buildComposable({ accountAddress: account.signer.address, currentInstructions: [], meeVersions }, { type: "nativeTokenTransfer", data: { to: cleanUp.recipientAddress, value: amount, chainId: cleanUp.chainId, ...(cleanUp.gasLimit ? { gasLimit: cleanUp.gasLimit } : {}) } }, composabilityVersion); cleanUpInstruction = cleanUpNativeTransferInstruction; simulationTokenOverrideAmount = amount; } } else { // Else ERC20 cleanup let amount = cleanUp.amount ?? 0n; // If the amount is a bigint and greater than 0, then use it for simulation, otherwise default to 1 wei if (isBigInt(amount) && amount > 0n) { simulationTokenOverrideAmount = amount; } else { simulationTokenOverrideAmount = 1n; // default to 1 wei if runtime balance is used } // If there is no amount specified, runtime amount will be used for cleanup by default if (amount === 0n) { amount = runtimeERC20BalanceOf({ targetAddress: account.addressOn(cleanUp.chainId, true), tokenAddress: cleanUp.tokenAddress }); } const [cleanUpERC20TransferInstruction] = await buildComposable({ accountAddress: account.signer.address, currentInstructions: [], meeVersions }, { type: "transfer", data: { recipient: cleanUp.recipientAddress, tokenAddress: cleanUp.tokenAddress, amount, chainId: cleanUp.chainId, ...(cleanUp.gasLimit ? { gasLimit: cleanUp.gasLimit } : {}) } }, composabilityVersion); cleanUpInstruction = cleanUpERC20TransferInstruction; } tokenOverrides.push({ tokenAddress: cleanUp.tokenAddress, accountAddress: account.addressOn(cleanUp.chainId, true), balance: simulationTokenOverrideAmount, chainId: cleanUp.chainId }); const nonceDependencies = []; if (cleanUp.dependsOn && cleanUp.dependsOn.length > 0) { for (const userOpIndex of cleanUp.dependsOn) { const userOpNonceInfo = userOpsNonceInfo[userOpIndex]; if (!userOpNonceInfo) throw new Error("Invalid UserOp dependency, please check the dependsOn configuration"); const { nonce, nonceKey } = userOpNonceInfo; const nonceOf = runtimeNonceOf({ smartAccountAddress: account.addressOn(cleanUp.chainId, true), nonceKey: nonceKey, constraints: [greaterThanOrEqualTo(nonce + 1n)] }); nonceDependencies.push(nonceOf); customOverrides.push({ chainId: cleanUp.chainId, contractAddress: entryPoint.address, storageSlot: calculateNonceStorageSlot(account.addressOn(cleanUp.chainId, true), nonceKey), value: pad(toHex(nonce + 1n).slice(-16)) // taking only last 8 bytes (16 hex characters) - which is the nonce value for a given nonce key }); } } else { if (userOpsNonceInfo.length === 0) { throw new Error("At least one instruction should be configured to use cleanups."); } const lastUserOp = userOpsNonceInfo[userOpsNonceInfo.length - 1]; const { nonce, nonceKey } = lastUserOp; const nonceOf = runtimeNonceOf({ smartAccountAddress: account.addressOn(cleanUp.chainId, true), nonceKey: nonceKey, constraints: [greaterThanOrEqualTo(nonce + 1n)] }); nonceDependencies.push(nonceOf); customOverrides.push({ chainId: cleanUp.chainId, contractAddress: entryPoint.address, storageSlot: calculateNonceStorageSlot(account.addressOn(cleanUp.chainId, true), nonceKey), value: pad(toHex(nonce + 1n).slice(-16)) }); } const nonceDependencyInputParams = nonceDependencies.flatMap((dep) => dep.inputParams); const formattedNonceDependencyInputParams = formatCallDataInputParamsWithVersion(composabilityVersion, false, nonceDependencyInputParams); cleanUpInstruction.calls = cleanUpInstruction.calls.map((call) => { call.inputParams.push(...formattedNonceDependencyInputParams); return call; }); return { ...cleanUpInstruction, simulationOverrides: { tokenOverrides, customOverrides } }; })); const cleanUpUserOps = await prepareUserOps(account, cleanUpInstructions, true, moduleAddress); return cleanUpUserOps; }; /** * Returns the verification gas limit for the userOp * @param parameters - The parameters for the resolveVerificationGasLimit function * @returns The verification gas limit for the userOp/paymentInfo * returns undefined if there's no special gas limit required for a given case * 'undefined' means the node will apply the default verification gas limit */ const resolveVerificationGasLimit = (parameters) => { const { moduleAddress, verificationGasLimit, sponsorship, index, paymentChainId, currentChainId } = parameters; if (currentChainId === paymentChainId) { return resolveVerificationGasLimitForPaymentChain({ moduleAddress, verificationGasLimit, sponsorship, index }); } return resolveVerificationGasLimitForNonPaymentChain({ moduleAddress, verificationGasLimit, index }); }; /** * Returns the verification gas limit for the userOp on the payment chain * @param parameters - The parameters for the resolveVerificationGasLimit function * @returns The verification gas limit for the userOp * returns undefined if there's no special gas limit required for a given case * 'undefined' means the node will apply the default verification gas limit */ const resolveVerificationGasLimitForPaymentChain = (parameters) => { const { moduleAddress, verificationGasLimit, sponsorship, index } = parameters; // if neither module address nor custom verification gas limit is provided, // the default verification gas limit will be applied if (!moduleAddress && !verificationGasLimit) { return undefined; } if (!moduleAddress && verificationGasLimit) { return { verificationGasLimit }; } // at this stage moduleAddress is definitely provided if (addressEquals(moduleAddress, SMART_SESSIONS_ADDRESS)) { if (sponsorship) { // handling this only for sponsored superTxn. (payment userOp // is signed by the node and can not enable the permission) => // => the permission is enabled in the first meaningful userOp // for non-sponsored superTxn, enabling the permission happens // in the payment userOp see resolvePaymentUserOpVerificationGasLimit(...) below if (index === 0) { // return increased verification gas limit for the first userOp // as it this userOp will be enabling the permission => requires more gas return { verificationGasLimit: verificationGasLimit || 1000000n }; } } // return slighly increased verification gas limit // for USE session userOps return { verificationGasLimit: 250000n }; } // if module is defined, however it is not SMART_SESSIONS_ADDRESS, // return the custom verification gas limit // more manual handling for other modules can be added here if needed if (verificationGasLimit) { return { verificationGasLimit }; } // if module is provided but no custom verification gas limit is provided, // return undefined == default verification gas limit return undefined; }; /** * Returns the verification gas limit for the userOp on a non-payment chain * @param parameters - The parameters for the resolveVerificationGasLimit function * @returns The verification gas limit for the userOp * returns undefined if there's no special gas limit required for a given case * 'undefined' means the node will apply the default verification gas limit */ const resolveVerificationGasLimitForNonPaymentChain = (parameters) => { const { moduleAddress, verificationGasLimit, index } = parameters; // if neither module address nor custom verification gas limit is provided, // the default verification gas limit will be applied if (!moduleAddress && !verificationGasLimit) { return undefined; } if (!moduleAddress && verificationGasLimit) { return { verificationGasLimit }; } // at this stage moduleAddress is definitely provided if (addressEquals(moduleAddress, SMART_SESSIONS_ADDRESS)) { // on the non-payment chain, the permission is always enabled in the first meaningful userOp if (index === 0) { // return increased verification gas limit for payment userOp // in a non-sponsored superTxn return { verificationGasLimit: verificationGasLimit || 1000000n }; } // for all other userOps, return USE session verification gas limit return { verificationGasLimit: 250000n }; } if (verificationGasLimit) { return { verificationGasLimit }; } // if module is provided but no custom verification gas limit is provided, return undefined == default verification gas limit return undefined; }; /** * Returns the verification gas limit for the payment userOp * @param parameters - The parameters for the resolveVerificationGasLimit function * @returns The verification gas limit for the payment userOp * returns undefined if there's no special gas limit required for a given case * 'undefined' means the node will apply the default verification gas limit */ const resolvePaymentUserOpVerificationGasLimitNonSponsored = (moduleAddress, verificationGasLimit) => { // if neither module address nor custom verification gas limit is provided, // the default verification gas limit will be applied if (!moduleAddress && !verificationGasLimit) { return undefined; } if (!moduleAddress && verificationGasLimit) { return { verificationGasLimit }; } // at this stage moduleAddress is definitely provided if (addressEquals(moduleAddress, SMART_SESSIONS_ADDRESS)) { // return increased verification gas limit for payment userOp // in a non-sponsored superTxn return { verificationGasLimit: verificationGasLimit || 1000000n }; // if it is sponsorship, the payment userOp won't even use Smart Sessions Module // so doesn't need any custom verification gas limit // also payment userOp never utilizes USE mode of Smart Sessions Module } if (verificationGasLimit) { return { verificationGasLimit }; } // if module is provided but no custom verification gas limit is provided, return undefined == default verification gas limit return undefined; }; /** * Returns the MEE versions of the orchestrator account * on all the chains involved in the quote request. * * @param account - The multichain smart account * @param instructions - The instructions to be executed * @param sponsorship - Whether the quote is sponsored * @param feeToken - The fee token * @returns An array of chain IDs with their corresponding MEE versions as MeeVersionsWithChainId * @example * ```typescript * const meeVersions = getMeeVersionsForQuote(account, instructions, sponsorship, feeToken) * ``` */ export function getMeeVersionsForQuoteRequest(account, instructions, sponsorship, feeToken) { const usedChains = new Set(); for (const op of instructions) { usedChains.add(Number(op.chainId)); } // For sponsored flow, we can ignore payment chain because orchestrator account // used there will be not user's orchestrator account but sponsorship account. // For non-sponsored flow, the user's orchestrator account will be performing' // the payment userOp, so we need to make sure its MEE version is consistent. if (!sponsorship) { // if sponsorship is false, the feeToken is defined: see GetQuoteParams type usedChains.add(Number(feeToken.chainId)); } return Array.from(usedChains, (chainId) => { const deployment = account.deploymentOn(chainId, true); return { version: deployment.version, chainId }; }); } // ==================================================== export default getQuote; //# sourceMappingURL=getQuote.js.map