@biconomy/abstractjs
Version:
SDK for Biconomy integration with support for account abstraction, smart accounts, ERC-4337.
448 lines • 20.9 kB
JavaScript
import { zeroAddress } from "viem";
import { buildComposable } from "../../../account/decorators/index.js";
import { addressEquals } from "../../../account/utils/Utils.js";
import { LARGE_DEFAULT_GAS_LIMIT } from "../../../account/utils/getMultichainContract.js";
import { resolveInstructions } from "../../../account/utils/resolveInstructions.js";
import { SMART_SESSIONS_ADDRESS } from "../../../constants/index.js";
import { greaterThanOrEqualTo, runtimeERC20BalanceOf, 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, DEFAULT_STAGING_PATHFINDER_URL } 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) => {
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, delegate = false, authorization, moduleAddress, shortEncodingSuperTxn = false, sponsorship = false, sponsorshipOptions } = parameters;
const resolvedInstructions = await resolveInstructions(instructions);
// if feePayer is provided, we need to use the /quote-permit path
let pathToQuery = path;
if (feePayer) {
pathToQuery = "/quote-permit";
}
const validUserOps = resolvedInstructions.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: ${resolvedInstructions
.map((x) => x.chainId)
.join(", ")}`);
}
const hasProcessedInitData = [];
const paymentVerificationGasLimit = resolvePaymentUserOpVerificationGasLimit({
moduleAddress,
sponsorship
});
const { paymentInfo, isInitDataProcessed } = await preparePaymentInfo(client, {
...parameters,
paymentVerificationGasLimit
});
if (isInitDataProcessed)
hasProcessedInitData.push(paymentInfo.chainId);
const preparedUserOps = await prepareUserOps(account_, resolvedInstructions, false, moduleAddress);
// 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 cleanUpUserOps = await prepareCleanUpUserOps(account_, userOpsNonceInfo, cleanUps, moduleAddress);
preparedUserOps.push(...cleanUpUserOps);
}
// 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]) => {
let initDataOrUndefined = undefined;
if (!indexPerChainId.has(chainId)) {
indexPerChainId.set(chainId, 0);
}
const shouldContainInitData = !hasProcessedInitData.includes(chainId) && !isAccountDeployed;
if (shouldContainInitData) {
hasProcessedInitData.push(chainId);
initDataOrUndefined = delegate
? {
eip7702Auth: await nexusAccount.toDelegation({ authorization })
}
: { initCode };
}
const verificationGasLimit = resolveVerificationGasLimit({
moduleAddress,
sponsorship,
index: indexPerChainId.get(chainId),
paymentChainId: paymentInfo.chainId,
currentChainId: chainId
});
indexPerChainId.set(chainId, indexPerChainId.get(chainId) + 1);
return {
lowerBoundTimestamp: lowerBoundTimestamp_,
upperBoundTimestamp: isCleanUpUserOp
? upperBoundTimestamp_ +
CLEANUP_USEROP_EXTENDED_EXEC_WINDOW_DURATION
: upperBoundTimestamp_,
sender,
callData,
callGasLimit,
nonce: nonce.toString(),
chainId,
isCleanUpUserOp,
...initDataOrUndefined,
...verificationGasLimit,
shortEncoding: shortEncodingSuperTxn || shortEncoding
};
}));
const quoteRequest = { userOps, paymentInfo };
let quote = await client.request({
path: pathToQuery,
body: quoteRequest
});
if (sponsorship && sponsorshipOptions) {
const isSelfHostedSponsorship = ![
DEFAULT_PATHFINDER_URL,
DEFAULT_STAGING_PATHFINDER_URL
].includes(sponsorshipOptions.url);
if (isSelfHostedSponsorship) {
const selfHostedClient = createHttpClient(sponsorshipOptions.url);
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, delegate = false, gasLimit, verificationGasLimit, authorization, sponsorship, sponsorshipOptions, shortEncodingSuperTxn, moduleAddress = zeroAddress, paymentVerificationGasLimit } = parameters;
let paymentInfo = undefined;
let isInitDataProcessed = 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 sponsorshipClient = createHttpClient(sponsorshipUrl);
const { nonce } = await sponsorshipClient.request({
path: `sponsorship/nonce/${chainId}/${sender}`,
method: "GET",
...(sponsorshipOptions?.customHeaders
? { headers: sponsorshipOptions.customHeaders }
: {})
});
paymentInfo = {
sponsored: true,
sender,
token,
nonce,
callGasLimit: gasLimit || DEFAULT_GAS_LIMIT,
verificationGasLimit: verificationGasLimit || DEFAULT_VERIFICATION_GAS_LIMIT,
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 {
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}`);
}
// TODO: Check the correctness of this while testing. This is a old logic
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
const initData = isAccountDeployed
? undefined
: delegate
? {
eip7702Auth: await validPaymentAccount.toDelegation({
authorization
})
}
: { initCode };
paymentInfo = {
sponsored: false,
sender: validPaymentAccount.address,
token: feeToken.address,
nonce: nonce.nonce.toString(),
callGasLimit: gasLimit || DEFAULT_GAS_LIMIT,
verificationGasLimit: verificationGasLimit || DEFAULT_VERIFICATION_GAS_LIMIT,
chainId: feeToken.chainId.toString(),
...(eoaOrFeePayer ? { eoa: eoaOrFeePayer } : {}),
...initData,
shortEncoding: shortEncodingSuperTxn,
...paymentVerificationGasLimit
};
// 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;
}
if (!paymentInfo)
throw new Error("Failed to generate payment info");
return { paymentInfo, isInitDataProcessed };
};
const prepareUserOps = async (account, instructions, isCleanUpUserOps = false, moduleAddress) => {
return await Promise.all(instructions.map((userOp) => {
const deployment = account.deploymentOn(userOp.chainId, true);
const accountAddress = account.addressOn(userOp.chainId, true);
let callsPromise;
if (userOp.isComposable) {
callsPromise = deployment.encodeExecuteComposable(userOp.calls);
}
else {
callsPromise =
userOp.calls.length > 1
? deployment.encodeExecuteBatch(userOp.calls)
: deployment.encodeExecute(userOp.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
// For we just set it to false for now
const shortEncoding = false;
return Promise.all([
callsPromise,
deployment.getNonceWithKey(accountAddress, { moduleAddress }),
deployment.isDeployed(),
deployment.getInitCode(),
deployment.address,
userOp.calls
.map((uo) => uo?.gasLimit ?? LARGE_DEFAULT_GAS_LIMIT)
.reduce((curr, acc) => curr + acc, 0n)
.toString(),
userOp.chainId.toString(),
isCleanUpUserOps,
deployment,
shortEncoding
]);
}));
};
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 cleanUpInstructions = await Promise.all(cleanUps.map(async (cleanUp) => {
let amount = cleanUp.amount ?? 0n;
// If there is no amount specified ? Runtime amount will be cleaned up by default
if (amount === 0n) {
amount = runtimeERC20BalanceOf({
targetAddress: account.addressOn(cleanUp.chainId, true),
tokenAddress: cleanUp.tokenAddress,
constraints: [greaterThanOrEqualTo(1n)] // Cleanup will only happen if there is atleast 1 wei
});
}
const [cleanUpTransferInstruction] = await buildComposable({ account: account, currentInstructions: [] }, {
type: "transfer",
data: {
recipient: cleanUp.recipientAddress,
tokenAddress: cleanUp.tokenAddress,
amount,
chainId: cleanUp.chainId,
...(cleanUp.gasLimit ? { gasLimit: cleanUp.gasLimit } : {})
}
});
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);
}
}
else {
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);
}
const nonceDependencyInputParams = nonceDependencies.flatMap((dep) => dep.inputParams);
cleanUpTransferInstruction.calls = cleanUpTransferInstruction.calls.map((call) => {
call.inputParams.push(...nonceDependencyInputParams);
return call;
});
return cleanUpTransferInstruction;
}));
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, sponsorship, index, paymentChainId, currentChainId } = parameters;
if (currentChainId === paymentChainId) {
return resolveVerificationGasLimitForPaymentChain({
moduleAddress,
sponsorship,
index
});
}
return resolveVerificationGasLimitForNonPaymentChain({
moduleAddress,
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, sponsorship, index } = parameters;
// if module address is not provided, the default verification gas limit will be applied
if (!moduleAddress) {
return undefined;
}
if (addressEquals(moduleAddress, SMART_SESSIONS_ADDRESS)) {
if (sponsorship) {
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: 1000000n };
}
}
// return slighly increased verification gas limit
// for USE session userOps
return { verificationGasLimit: 250000n };
}
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, index } = parameters;
// if module address is not provided, the default verification gas limit will be applied
if (!moduleAddress) {
return undefined;
}
if (addressEquals(moduleAddress, SMART_SESSIONS_ADDRESS)) {
if (index === 0) {
// return increased verification gas limit for payment userOp
// in a non-sponsored superTxn
return { verificationGasLimit: 1000000n };
}
// for all other userOps, return USE session verification gas limit
return { verificationGasLimit: 250000n };
}
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 resolvePaymentUserOpVerificationGasLimit = (parameters) => {
const { moduleAddress, sponsorship } = parameters;
// if module address is not provided, the default verification gas limit will be applied
if (!moduleAddress) {
return undefined;
}
if (addressEquals(moduleAddress, SMART_SESSIONS_ADDRESS)) {
if (!sponsorship) {
// return increased verification gas limit for payment userOp
// in a non-sponsored superTxn
return { 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
}
return undefined;
};
// ====================================================
export default getQuote;
//# sourceMappingURL=getQuote.js.map