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