UNPKG

@biconomy/abstractjs

Version:

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

206 lines 8.57 kB
import { encodeFunctionData, erc20Abi, zeroAddress } from "viem"; import { batchInstructions } from "../../../account/utils/batchInstructions.js"; import { ForwarderAbi } from "../../../constants/abi/ForwarderAbi.js"; import { greaterThanOrEqualTo, runtimeERC20AllowanceOf } from "../../../modules/utils/composabilityCalls.js"; import getMmDtkQuote, {} from "./getMmDtkQuote.js"; import getOnChainQuote, {} from "./getOnChainQuote.js"; import getPermitQuote, {} from "./getPermitQuote.js"; import { DEFAULT_GAS_LIMIT } from "./getQuote.js"; import { getQuoteType } from "./getQuoteType.js"; import getSafeQuote, {} from "./getSafeQuote.js"; /** * Gets a quote using either permit or standard on-chain transaction based on token capabilities. * This function automatically determines whether to use permit-based or standard transactions * by checking the payment token's permit support. * * @param client - The Mee client instance used for API interactions * @param parameters - Parameters for generating the quote * @param parameters.trigger - Transaction trigger information * @param parameters.instructions - Array of transaction instructions to be executed * @param parameters.chainId - Target blockchain chain ID * @param parameters.walletProvider - Wallet provider to use * @param [parameters.gasToken] - Optional token address to use for gas payment * @param [parameters.fusionMode] - Optional explicitly set fusion mode * * @returns Promise resolving to either a permit quote or on-chain quote payload * * @example * ```typescript * const quote = await getFusionQuote(client, { * chainId: "1", * walletProvider: "metamask", * trigger: { * chainId: "1", * tokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" // USDC * }, * instructions: [{ * to: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", * data: "0x...", * value: "0" * }] * }); * // Returns either GetPermitQuotePayload or GetOnChainQuotePayload * // depending on USDC's permit support * ``` * * @throws Will throw an error if: * - The payment token information cannot be retrieved * - The quote generation fails * - The API request fails */ export const getFusionQuote = async (client, parameters) => { // if delegator smart account is provided, we use mm-dtk fusion mode if (parameters.delegatorSmartAccount) { return getMmDtkQuote(client, parameters); } // if safe account is provided, we use safe-sa fusion mode if (parameters.safeAccount) { return getSafeQuote(client, parameters); } const signatureType = await getQuoteType(client, parameters); switch (signatureType) { case "permit": return getPermitQuote(client, parameters); case "onchain": return getOnChainQuote(client, parameters); default: throw new Error("Invalid quote type for fusion quote"); } }; export const prepareInstructions = async (client, parameters) => { const { resolvedInstructions, trigger, owner, spender, recipient, account, batch = true } = parameters; const meeVersions = client.account.deployments.map(({ version, chain }) => ({ chainId: chain.id, version })); let triggerAmount = 0n; if (trigger.useMaxAvailableFunds) { const { publicClient } = client.account.deploymentOn(trigger.chainId, true); if (trigger.tokenAddress === zeroAddress) { const { version } = account.deploymentOn(trigger.chainId, true); const forwardCalldata = encodeFunctionData({ abi: ForwarderAbi, functionName: "forward", args: [recipient] }); const [balance, gasPrice, gasLimit] = await Promise.all([ publicClient.getBalance({ address: owner }), publicClient.getGasPrice(), publicClient.estimateGas({ account: owner, to: version.ethForwarderAddress, data: forwardCalldata, value: 100n // Dummy amount }) ]); // 100% gas limit buffer to avoid failures const gasLimitWithBuffer = (gasLimit * 200n) / 100n; // 100% buffer for gas price fluctuations const gasBuffer = 2; const baseCost = gasLimitWithBuffer * gasPrice; const gasReserve = BigInt(Math.ceil(Number(baseCost) * gasBuffer)); if (balance <= gasReserve) { throw new Error("Not enough native token to transfer"); } // native token balance maximum available balance fetch triggerAmount = balance - gasReserve; } else { // EOA balance maximum available balance fetch triggerAmount = await publicClient.readContract({ address: trigger.tokenAddress, abi: erc20Abi, functionName: "balanceOf", args: [owner] }); } } else { if (!trigger.amount) throw new Error("Trigger amount field is required"); triggerAmount = trigger.amount; } let isComposable = resolvedInstructions.some(({ isComposable }) => isComposable); let transferFromAmount = 0n; // If max available funds ? the entire balance from EOA is added as transfer amount. But the fees will be taken from this // So we don't know a specific amount to be defined here before getting the quote. So we take runtimeBalance which will take // the remaining funds after the fee deduction. if (trigger.useMaxAvailableFunds && trigger.tokenAddress !== zeroAddress) { transferFromAmount = runtimeERC20AllowanceOf({ owner, spender, tokenAddress: trigger.tokenAddress, constraints: [greaterThanOrEqualTo(1n)] }); // If max funds is used, it will be always composable isComposable = true; } else { transferFromAmount = triggerAmount; } const triggerGasLimit = trigger.gasLimit ? trigger.gasLimit : DEFAULT_GAS_LIMIT; // If token address is zero address, we don't need to add transferFrom instruction if (trigger.tokenAddress === zeroAddress) { let batchedInstructions = []; if (batch) { batchedInstructions = await batchInstructions({ accountAddress: account.signer.address, meeVersions, instructions: resolvedInstructions }); } else { // If integrators explicitly want unbatched userOps batchedInstructions = resolvedInstructions; } return { triggerGasLimit, triggerAmount, batchedInstructions }; } const params = { type: "transferFrom", data: { tokenAddress: trigger.tokenAddress, chainId: trigger.chainId, amount: transferFromAmount, recipient, sender: owner, gasLimit: triggerGasLimit } }; const triggerTransfer = await (isComposable ? account.buildComposable(params) : account.build(params)); let batchedInstructions = []; if (batch) { batchedInstructions = await batchInstructions({ accountAddress: account.signer.address, meeVersions, instructions: [...triggerTransfer, ...resolvedInstructions] }); } else { let partiallyBatchedInstructions = []; if (resolvedInstructions.length > 0) { // Try to batch trigger transfer and first dev instruction together. const triggerAndFirstInstructionBatch = await batchInstructions({ accountAddress: account.signer.address, meeVersions, instructions: [...triggerTransfer, ...resolvedInstructions.slice(0, 1)] }); // Trigger + first inx batched and keep rest of the instructions unbatched. partiallyBatchedInstructions = [ ...triggerAndFirstInstructionBatch, ...resolvedInstructions.slice(1) ]; } else { partiallyBatchedInstructions = [...triggerTransfer]; } // If integrators explicitly want unbatched userOps batchedInstructions = partiallyBatchedInstructions; } return { triggerGasLimit, triggerAmount, batchedInstructions }; }; export default getFusionQuote; //# sourceMappingURL=getFusionQuote.js.map