UNPKG

@biconomy/abstractjs

Version:

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

163 lines 7.1 kB
import { erc20Abi, zeroAddress } from "viem"; import { batchInstructions } from "../../../account/utils/batchInstructions.js"; import { isPermitSupported } from "../../../modules/utils/Helpers.js"; import { greaterThanOrEqualTo, runtimeERC20AllowanceOf } from "../../../modules/utils/composabilityCalls.js"; import getMmDtkQuote, {} from "./getMmDtkQuote.js"; import getOnChainQuote, {} from "./getOnChainQuote.js"; import { getPaymentToken } from "./getPaymentToken.js"; import getPermitQuote, {} from "./getPermitQuote.js"; import { DEFAULT_GAS_LIMIT } from "./getQuote.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", * paymentToken: "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 custom call is provided, we use on-chain tx fusion mode if ("call" in parameters.trigger) { return getOnChainQuote(client, parameters); } const paymentTokenInfo = await getPaymentToken(client, parameters.trigger); let permitEnabled = false; if (paymentTokenInfo.paymentToken) { permitEnabled = paymentTokenInfo.paymentToken.permitEnabled || false; } else if (paymentTokenInfo.isArbitraryPaymentTokensSupported) { const modularSmartAccount = client.account.deploymentOn(parameters.trigger.chainId, true); permitEnabled = await isPermitSupported(modularSmartAccount.walletClient, parameters.trigger.tokenAddress); } else { throw new Error(`Payment token (${parameters.trigger.tokenAddress}) not supported for chain ${parameters.trigger.chainId}`); } return permitEnabled ? getPermitQuote(client, parameters) : getOnChainQuote(client, parameters); }; export const prepareInstructions = async (client, parameters) => { const { resolvedInstructions, trigger, sender, scaAddress, recipient, account } = parameters; let triggerAmount = 0n; if (trigger.useMaxAvailableFunds) { const { publicClient } = client.account.deploymentOn(trigger.chainId, true); if (trigger.tokenAddress === zeroAddress) { const [balance, gasPrice, gasLimit] = await Promise.all([ publicClient.getBalance({ address: sender }), publicClient.getGasPrice(), publicClient.estimateGas({ account: sender, to: sender, value: 1n }) // Dummy values ]); // 50% gas limit buffer to avoid failures const gasLimitWithBuffer = (gasLimit * 150n) / 100n; // 50% buffer for gas price fluctuations const gasBuffer = 1.5; 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: [sender] }); } } 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: sender, spender: scaAddress, 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) { const batchedInstructions = await batchInstructions({ account, instructions: resolvedInstructions }); return { triggerGasLimit, triggerAmount, batchedInstructions }; } const params = { type: "transferFrom", data: { tokenAddress: trigger.tokenAddress, chainId: trigger.chainId, amount: transferFromAmount, recipient, sender, gasLimit: triggerGasLimit } }; const triggerTransfer = await (isComposable ? account.buildComposable(params) : account.build(params)); const batchedInstructions = await batchInstructions({ account, instructions: [...triggerTransfer, ...resolvedInstructions] }); return { triggerGasLimit, triggerAmount, batchedInstructions }; }; export default getFusionQuote; //# sourceMappingURL=getFusionQuote.js.map