@biconomy/abstractjs
Version:
SDK for Biconomy integration with support for account abstraction, smart accounts, ERC-4337.
163 lines • 7.1 kB
JavaScript
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