@biconomy/abstractjs
Version:
SDK for Biconomy integration with support for account abstraction, smart accounts, ERC-4337.
908 lines • 45.7 kB
JavaScript
import { pad, toHex } from "viem";
import { buildComposable, formatCallDataInputParamsWithVersion } from "../../../account/decorators/index.js";
import { addressEquals, calculateNonceStorageSlot, isBigInt, isNativeToken, validateConsistentMeeVersions } from "../../../account/utils/Utils.js";
import { batchInstructions } from "../../../account/utils/batchInstructions.js";
import { LARGE_DEFAULT_GAS_LIMIT } from "../../../account/utils/getMultichainContract.js";
import { resolveInstructions } from "../../../account/utils/resolveInstructions.js";
import { ComposabilityVersion, SMART_SESSIONS_ADDRESS, SmartSessionMode } from "../../../constants/index.js";
import { greaterThanOrEqualTo, runtimeERC20BalanceOf, runtimeNativeBalanceOf, 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, getDefaultMEENetworkUrl } 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, quoteType = "simple", trigger) => {
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, executionSimulationRetryDelay: executionSimulationRetryDelay_, delegate = false, authorizations = [], multichain7702Auth = false, moduleAddress, batch = true, simulation, verificationGasLimit, shortEncodingSuperTxn = false, sponsorship = false, sponsorshipOptions, feeToken, sessionDetails, smartSessionMode } = parameters;
const mode = smartSessionMode === "ENABLE_AND_USE"
? SmartSessionMode.UNSAFE_ENABLE
: SmartSessionMode.USE;
let resolvedInstructions = await resolveInstructions(instructions);
// If there is no metadata is configured by the SDK or developer ? Custom metadata will be added always
resolvedInstructions = resolvedInstructions.map((instruction) => {
if (!instruction.metadata || instruction.metadata.length === 0) {
return {
...instruction,
metadata: [
{
type: "CUSTOM",
description: "Custom on-chain action",
chainId: instruction.chainId
}
]
};
}
return instruction;
});
let finalInstructions = resolvedInstructions;
const meeVersions = getMeeVersionsForQuoteRequest(account_, resolvedInstructions, sponsorship, feeToken);
// By default, all the main instructions are batched
if (batch) {
finalInstructions = await batchInstructions({
accountAddress: account_.signer.address,
instructions: [...resolvedInstructions],
meeVersions // TODO: check if we can just pass empty array here because MeeVerions are not used when batching. Will it improve performance?
});
}
// if feePayer is provided, we need to use the /quote-permit path
let pathToQuery = path;
if (feePayer) {
pathToQuery = "/quote-permit";
}
const validUserOps = finalInstructions.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: ${finalInstructions
.map((x) => x.chainId)
.join(", ")}`);
}
const hasProcessedInitData = [];
const hasProcessedSessionDetails = new Set();
const initDataTypeByChainId = new Map();
const sprtxChainIdsSet = new Set([]);
// For non sponsored flow, fee token chainId needs to be included
if (feeToken)
sprtxChainIdsSet.add(feeToken.chainId);
// Chains IDS from instructions are considered
for (const inx of finalInstructions) {
sprtxChainIdsSet.add(inx.chainId);
}
const sprtxChainIds = [...sprtxChainIdsSet];
if (delegate) {
if (multichain7702Auth) {
// Check for all the nonces are same only if there is more than one chain is involved in the sprtx
if (sprtxChainIds.length > 1) {
const noncesAndChainIds = await Promise.all(sprtxChainIds.map(async (chainId) => {
const { publicClient, walletClient: { account: { address } } } = account_.deploymentOn(chainId, true);
return {
chainId,
nonce: await publicClient.getTransactionCount({ address })
};
}));
const nonceCountMap = noncesAndChainIds.reduce((map, { nonce }) => {
map.set(nonce, (map.get(nonce) || 0) + 1);
return map;
}, new Map());
// Chains with different nonces needs different authorizations
const noncesAndChainIdsWithUniqueNonces = noncesAndChainIds.filter((info) => nonceCountMap.get(info.nonce) === 1);
// Chains with same nonces can reuse the same authorization which is signed only once
const noncesAndChainIdsWithSameNonces = noncesAndChainIds.filter((info) => nonceCountMap.get(info.nonce) > 1);
// If custom authorizations are passed, a series of validation is conducted here
if (authorizations.length > 0) {
// If noncesAndChainIdsWithUniqueNonces length is zero ? It means all the nonces are same and can be used for multichain
// It is expected to pass only one auth from outside the SDK.
if (noncesAndChainIdsWithUniqueNonces.length === 0 &&
authorizations.length > 1) {
throw new Error("Invalid authorizations: The nonce for all the chains are same and only one multichain authorization is expected");
}
// If multichain nonce are not same and custom auth are passed ? The auth should be sufficient orelse error will be thrown
if (noncesAndChainIdsWithUniqueNonces.length > 0) {
const missingAuthsByChainId = [];
for (const { chainId } of noncesAndChainIdsWithUniqueNonces) {
const isAuthProvided = authorizations.some((auth) => {
return auth.chainId === chainId;
});
if (!isAuthProvided)
missingAuthsByChainId.push(chainId);
}
if (missingAuthsByChainId.length > 0) {
throw new Error(`Invalid authorizations: The nonce for all the chains are not same. You need to pass specific authorizations for the following chains: ${missingAuthsByChainId.join(", ")}`);
}
}
// For same multichain nonces ? Check for auth with zero id and throw error if it is not there
if (noncesAndChainIdsWithSameNonces.length > 0) {
const isAuthProvided = authorizations.some((auth) => {
return auth.chainId === 0;
});
if (!isAuthProvided) {
const chainIds = noncesAndChainIdsWithSameNonces.map((auth) => auth.chainId);
throw new Error(`Invalid authorizations: The nonce for some of the chains are same. Missing multichain authorization for the following chains: ${chainIds.join(", ")}`);
}
}
}
for (const chainId of sprtxChainIds) {
const [isMultichainAuth] = noncesAndChainIdsWithSameNonces.filter((info) => info.chainId === chainId);
initDataTypeByChainId.set(chainId, isMultichainAuth ? "MULTI_CHAIN_AUTH" : "SINGLE_CHAIN_AUTH");
}
}
else {
if (authorizations.length > 1) {
throw new Error("Invalid authorizations: The nonce for all the chains are same and only one multichain authorization is expected");
}
if (authorizations.length === 1 && authorizations[0].chainId !== 0) {
throw new Error("Invalid authorizations: Multichain authorization should be signed with chain ID zero");
}
// If only one chain is invloved. It can be directly treated as multichain
for (const chainId of sprtxChainIds) {
initDataTypeByChainId.set(chainId, "MULTI_CHAIN_AUTH");
}
}
}
else {
// If custom authorizations are passed, a series of validation is conducted here
if (authorizations.length > 0) {
const missingAuthsByChainId = [];
for (const chainId of sprtxChainIds) {
const isAuthProvided = authorizations.some((auth) => {
return auth.chainId === chainId;
});
if (!isAuthProvided)
missingAuthsByChainId.push(chainId);
}
if (missingAuthsByChainId.length > 0) {
throw new Error(`Authorizations are missing for the following chains: ${missingAuthsByChainId.join(", ")}`);
}
}
// All the auths will be treated as single chain auth without chain id zero
for (const chainId of sprtxChainIds) {
initDataTypeByChainId.set(chainId, "SINGLE_CHAIN_AUTH");
}
}
}
else {
// No auth at all. Init code will be added if account is not deployed
for (const chainId of sprtxChainIds) {
initDataTypeByChainId.set(chainId, "INIT_CODE");
}
}
const [{ paymentInfo, isInitDataProcessed, isSessionDetailsProcessed }, preparedUserOps] = await Promise.all([
preparePaymentInfo(client, {
...parameters,
initDataTypeByChainId
}),
prepareUserOps(account_, finalInstructions, false, moduleAddress)
]);
let multichainEIP7702Auth = undefined;
const paymentAuthType = initDataTypeByChainId.get(Number(paymentInfo.chainId));
// If payment info has eip7702 auth ? It is an non sponsored flow and auth is prepared
// If it is multichain auth and eip7702Auth prepared by either custom auth or SDK signed one
// It will be used for other userOp for delegation.
if (paymentInfo.eip7702Auth && paymentAuthType === "MULTI_CHAIN_AUTH") {
multichainEIP7702Auth = paymentInfo.eip7702Auth;
}
if (isInitDataProcessed)
hasProcessedInitData.push(Number(paymentInfo.chainId));
if (isSessionDetailsProcessed)
hasProcessedSessionDetails.add(paymentInfo.chainId);
// 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 result = await prepareCleanUpUserOps(account_, userOpsNonceInfo, cleanUps, moduleAddress);
preparedUserOps.push(...result);
}
// 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, metadata, simulationOverrides, lowerBoundTimestamp, upperBoundTimestamp, executionSimulationRetryDelay]) => {
let initDataOrUndefined = undefined;
if (!indexPerChainId.has(chainId)) {
indexPerChainId.set(chainId, 0);
}
// If account is not deployed, either initCode or eip7702Auth needs to be attached.
// If init code or 7702 auth is already added for the chain ? Skip this
if (!isAccountDeployed &&
!hasProcessedInitData.includes(Number(chainId))) {
// Mark as initData processed
hasProcessedInitData.push(Number(chainId));
const authType = initDataTypeByChainId.get(Number(chainId));
// If multichain EIP7702 auth is available ? It means, 7702 mode and no initCode is there.
if (authType === "MULTI_CHAIN_AUTH") {
// Apply existing multichain auth where the chain Ids were same. So no need multiple auths to be signed
if (multichainEIP7702Auth) {
initDataOrUndefined = {
eip7702Auth: multichainEIP7702Auth
};
}
else {
// This multichain auth will be used for the current chains and other chains which has the same nonce
multichainEIP7702Auth = await prepare7702Auth(nexusAccount, Number(chainId), initDataTypeByChainId, authorizations);
initDataOrUndefined = {
eip7702Auth: multichainEIP7702Auth
};
}
}
else if (authType === "SINGLE_CHAIN_AUTH") {
initDataOrUndefined = {
eip7702Auth: await prepare7702Auth(nexusAccount, Number(chainId), initDataTypeByChainId, authorizations)
};
}
else {
initDataOrUndefined = { initCode };
}
}
const resolvedVerificationGasLimit = resolveVerificationGasLimit({
moduleAddress,
verificationGasLimit,
sponsorship,
index: indexPerChainId.get(chainId),
paymentChainId: paymentInfo.chainId,
currentChainId: chainId
});
indexPerChainId.set(chainId, indexPerChainId.get(chainId) + 1);
let sessionDetail = undefined;
if (sessionDetails) {
// Find session details for this chain
const relevantIndex = sessionDetails.findIndex(({ enableSessionData }) => enableSessionData?.enableSession?.sessionToEnable?.chainId ===
BigInt(chainId));
if (relevantIndex === -1) {
throw new Error(`No session details found for chainId ${chainId}`);
}
const isFirstTimeForChain = !hasProcessedSessionDetails.has(chainId);
const dynamicMode = isFirstTimeForChain ? mode : SmartSessionMode.USE;
sessionDetail = {
...sessionDetails[relevantIndex],
mode: dynamicMode
};
hasProcessedSessionDetails.add(chainId);
}
return {
// Instruction level timebound will be considered first
lowerBoundTimestamp: lowerBoundTimestamp || lowerBoundTimestamp_,
upperBoundTimestamp: isCleanUpUserOp
? upperBoundTimestamp_ +
CLEANUP_USEROP_EXTENDED_EXEC_WINDOW_DURATION
: // Instruction level timebound will be considered first
upperBoundTimestamp || upperBoundTimestamp_,
// Instruction level execution simulation retry delay will be considered first and then global config
executionSimulationRetryDelay: executionSimulationRetryDelay || executionSimulationRetryDelay_,
sender,
callData,
callGasLimit,
nonce: nonce.toString(),
chainId,
isCleanUpUserOp,
...initDataOrUndefined,
...resolvedVerificationGasLimit,
shortEncoding: shortEncodingSuperTxn || shortEncoding,
sessionDetails: sessionDetail,
metadata,
simulationOverrides
};
}));
// in `simple` mode, we should validate the consistent MEE versions across all chains
// because simple mode signatures can be incompatible b/w some mee versions
if (quoteType === "simple") {
validateConsistentMeeVersions(meeVersions);
}
const quoteRequest = {
quoteType,
userOps,
paymentInfo,
meeVersions,
simulation,
trigger,
tags: parameters.tags
};
let quote = await client.request({
path: pathToQuery,
body: quoteRequest
});
if (sponsorship && sponsorshipOptions) {
// Both prod and staging network url is considered as biconomy hosted sponsorship service
const isSelfHostedSponsorship = ![
getDefaultMEENetworkUrl(false), // Prod
getDefaultMEENetworkUrl(true) // Staging
].includes(sponsorshipOptions.url);
if (isSelfHostedSponsorship) {
const selfHostedClient = createHttpClient(sponsorshipOptions.url, undefined, client.info.isDebugMode);
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, gasLimit, authorizations = [], sponsorship, sponsorshipOptions, shortEncodingSuperTxn, moduleAddress, sessionDetails, smartSessionMode = "USE", verificationGasLimit } = parameters;
let paymentInfo = undefined;
let isInitDataProcessed = false;
let isSessionDetailsProcessed = 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 isBiconomyHostedSponsorship = [
getDefaultMEENetworkUrl(false), // Prod
getDefaultMEENetworkUrl(true) // Staging
].includes(sponsorshipUrl);
// Biconomy hosted sponsorship will be considered as trusted sponsorship
const isTrustedSponsorship = sender.toLowerCase() ===
DEFAULT_MEE_SPONSORSHIP_PAYMASTER_ACCOUNT.toLowerCase() &&
isBiconomyHostedSponsorship;
let nonce = "0";
// If it is not an trusted sponsorship ? The nonce will be fetched from third party sponsorship backend
if (!isTrustedSponsorship) {
const sponsorshipClient = createHttpClient(sponsorshipUrl, undefined, client.info.isDebugMode);
const nonceInfo = await sponsorshipClient.request({
path: `sponsorship/nonce/${chainId}/${sender}`,
method: "GET",
...(sponsorshipOptions?.customHeaders
? { headers: sponsorshipOptions.customHeaders }
: {})
});
nonce = nonceInfo.nonce;
}
paymentInfo = {
sponsored: true,
sender,
token,
nonce,
callGasLimit: gasLimit || DEFAULT_GAS_LIMIT,
verificationGasLimit: DEFAULT_VERIFICATION_GAS_LIMIT, // when sponsored, this will be set by the node
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 {
// No sponsorship
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}`);
}
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
let initData = undefined;
if (!isAccountDeployed) {
const initDataType = parameters.initDataTypeByChainId.get(feeToken.chainId);
if (initDataType === "INIT_CODE") {
initData = { initCode };
}
else {
initData = {
eip7702Auth: await prepare7702Auth(validPaymentAccount, feeToken.chainId, parameters.initDataTypeByChainId, authorizations)
};
}
}
// for non-sponsored superTxn, the verification gas limit is resolved here
const paymentVerificationGasLimit = resolvePaymentUserOpVerificationGasLimitNonSponsored(moduleAddress, verificationGasLimit);
let sessionDetail = undefined;
// Adding session details on quote to enable node to detect smart session flow on quote phase + use this for simulations
if (sessionDetails) {
// Find session details for this chain
const relevantIndex = sessionDetails.findIndex(({ enableSessionData }) => enableSessionData?.enableSession?.sessionToEnable?.chainId ===
BigInt(feeToken.chainId));
if (relevantIndex === -1) {
throw new Error(`No session details found for chainId ${feeToken.chainId}`);
}
if (!smartSessionMode) {
throw new Error("smartSessionMode is required for smart sessions flow");
}
// Mostly use and enable mode for payment userOp
const mode = smartSessionMode === "ENABLE_AND_USE"
? SmartSessionMode.UNSAFE_ENABLE
: SmartSessionMode.USE;
sessionDetail = {
...sessionDetails[relevantIndex],
mode
};
}
paymentInfo = {
sponsored: false,
sender: validPaymentAccount.address,
token: feeToken.address,
nonce: nonce.nonce.toString(),
callGasLimit: gasLimit || DEFAULT_GAS_LIMIT,
verificationGasLimit: paymentVerificationGasLimit?.verificationGasLimit ||
DEFAULT_VERIFICATION_GAS_LIMIT,
chainId: feeToken.chainId.toString(),
...(feeToken.gasRefundAddress
? { gasRefundAddress: feeToken.gasRefundAddress }
: {}),
...(eoaOrFeePayer ? { eoa: eoaOrFeePayer } : {}),
...initData,
shortEncoding: shortEncodingSuperTxn,
sessionDetails: sessionDetail
};
// 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;
isSessionDetailsProcessed = true;
}
if (!paymentInfo)
throw new Error("Failed to generate payment info");
return { paymentInfo, isInitDataProcessed, isSessionDetailsProcessed };
};
const prepare7702Auth = async (smartAccount, chainId, initDataTypeByChainId, customAuthorizations = []) => {
let eip7702Auth;
const authType = initDataTypeByChainId.get(chainId);
if (authType === "MULTI_CHAIN_AUTH") {
// if it is multichain auth ? custom auth will be filtered with zero chain id
const [authorization] = customAuthorizations.filter((auth) => auth.chainId === 0);
eip7702Auth = await smartAccount.toDelegation(authorization ? { authorization } : { multiChain: true });
}
else if (authType === "SINGLE_CHAIN_AUTH") {
// if it is not multichain auth ? custom auth will be filtered for specific chain
const [authorization] = customAuthorizations.filter((auth) => {
return auth.chainId === Number(chainId);
});
eip7702Auth = await smartAccount.toDelegation(authorization ? { authorization } : { chainId });
}
else {
// This should never happen in theory
throw new Error("Invalid authorization type");
}
return eip7702Auth;
};
const prepareUserOps = async (account, instructions, isCleanUpUserOps = false, validatorAddress) => {
return await Promise.all(instructions.map((instruction) => {
const deployment = account.deploymentOn(instruction.chainId, true);
const accountAddress = account.addressOn(instruction.chainId, true);
let callsPromise;
if (instruction.isComposable) {
callsPromise = deployment.encodeExecuteComposable(instruction.calls);
}
else {
callsPromise =
instruction.calls.length > 1
? deployment.encodeExecuteBatch(instruction.calls)
: deployment.encodeExecute(instruction.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
// We just set it to `false` for now
const shortEncoding = false;
return Promise.all([
callsPromise,
deployment.getNonceWithKey(accountAddress, {
moduleAddress: validatorAddress
}),
deployment.isDeployed(),
deployment.getInitCode(),
deployment.address,
instruction.calls
.map((uo) => uo?.gasLimit ?? LARGE_DEFAULT_GAS_LIMIT)
.reduce((curr, acc) => curr + acc, 0n)
.toString(),
instruction.chainId.toString(),
isCleanUpUserOps,
deployment,
shortEncoding,
instruction.metadata,
instruction.simulationOverrides,
instruction.lowerBoundTimestamp,
instruction.upperBoundTimestamp,
instruction.executionSimulationRetryDelay
]);
}));
};
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 meeVersions = account.deployments.map(({ version, chain }) => ({
chainId: chain.id,
version
}));
const cleanUpInstructions = await Promise.all(cleanUps.map(async (cleanUp) => {
let cleanUpInstruction;
const { version, entryPoint } = account.deploymentOn(cleanUp.chainId, true);
const composabilityVersion = version.composabilityVersion;
// Initialize simulation overrides; Used for simulation overrides of cleanUp userOps
const tokenOverrides = [];
const customOverrides = [];
let simulationTokenOverrideAmount;
if (isNativeToken(cleanUp.tokenAddress)) {
if (!isBigInt(cleanUp.amount) || cleanUp.amount === 0n) {
if (composabilityVersion === ComposabilityVersion.V1_0_0) {
throw new Error("Native token cleanup with runtime-injected amount is not supported for Composability v1.0.0");
}
// If the amount is not a bigint, or is 0, then build a runtime injected cleanup
let amount;
if (cleanUp.amount === undefined || cleanUp.amount === 0n) {
// if it is not properly supplied as runtime value,
// then use the runtime native balance of the account
amount = runtimeNativeBalanceOf({
targetAddress: account.addressOn(cleanUp.chainId, true)
});
}
else {
// else just use provided runtime value
amount = cleanUp.amount;
}
const [cleanUpNativeTransferInstruction] = await buildComposable({
accountAddress: account.signer.address,
currentInstructions: [],
meeVersions
}, {
type: "nativeTokenTransfer",
data: {
to: cleanUp.recipientAddress,
value: amount,
chainId: cleanUp.chainId,
...(cleanUp.gasLimit ? { gasLimit: cleanUp.gasLimit } : {})
}
}, composabilityVersion);
cleanUpInstruction = cleanUpNativeTransferInstruction;
simulationTokenOverrideAmount = 1n; // default to 1 wei if runtime balance is used
}
else {
const amount = cleanUp.amount;
const [cleanUpNativeTransferInstruction] = await buildComposable({
accountAddress: account.signer.address,
currentInstructions: [],
meeVersions
}, {
type: "nativeTokenTransfer",
data: {
to: cleanUp.recipientAddress,
value: amount,
chainId: cleanUp.chainId,
...(cleanUp.gasLimit ? { gasLimit: cleanUp.gasLimit } : {})
}
}, composabilityVersion);
cleanUpInstruction = cleanUpNativeTransferInstruction;
simulationTokenOverrideAmount = amount;
}
}
else {
// Else ERC20 cleanup
let amount = cleanUp.amount ?? 0n;
// If the amount is a bigint and greater than 0, then use it for simulation, otherwise default to 1 wei
if (isBigInt(amount) && amount > 0n) {
simulationTokenOverrideAmount = amount;
}
else {
simulationTokenOverrideAmount = 1n; // default to 1 wei if runtime balance is used
}
// If there is no amount specified, runtime amount will be used for cleanup by default
if (amount === 0n) {
amount = runtimeERC20BalanceOf({
targetAddress: account.addressOn(cleanUp.chainId, true),
tokenAddress: cleanUp.tokenAddress
});
}
const [cleanUpERC20TransferInstruction] = await buildComposable({
accountAddress: account.signer.address,
currentInstructions: [],
meeVersions
}, {
type: "transfer",
data: {
recipient: cleanUp.recipientAddress,
tokenAddress: cleanUp.tokenAddress,
amount,
chainId: cleanUp.chainId,
...(cleanUp.gasLimit ? { gasLimit: cleanUp.gasLimit } : {})
}
}, composabilityVersion);
cleanUpInstruction = cleanUpERC20TransferInstruction;
}
tokenOverrides.push({
tokenAddress: cleanUp.tokenAddress,
accountAddress: account.addressOn(cleanUp.chainId, true),
balance: simulationTokenOverrideAmount,
chainId: cleanUp.chainId
});
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);
customOverrides.push({
chainId: cleanUp.chainId,
contractAddress: entryPoint.address,
storageSlot: calculateNonceStorageSlot(account.addressOn(cleanUp.chainId, true), nonceKey),
value: pad(toHex(nonce + 1n).slice(-16)) // taking only last 8 bytes (16 hex characters) - which is the nonce value for a given nonce key
});
}
}
else {
if (userOpsNonceInfo.length === 0) {
throw new Error("At least one instruction should be configured to use cleanups.");
}
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);
customOverrides.push({
chainId: cleanUp.chainId,
contractAddress: entryPoint.address,
storageSlot: calculateNonceStorageSlot(account.addressOn(cleanUp.chainId, true), nonceKey),
value: pad(toHex(nonce + 1n).slice(-16))
});
}
const nonceDependencyInputParams = nonceDependencies.flatMap((dep) => dep.inputParams);
const formattedNonceDependencyInputParams = formatCallDataInputParamsWithVersion(composabilityVersion, false, nonceDependencyInputParams);
cleanUpInstruction.calls = cleanUpInstruction.calls.map((call) => {
call.inputParams.push(...formattedNonceDependencyInputParams);
return call;
});
return {
...cleanUpInstruction,
simulationOverrides: {
tokenOverrides,
customOverrides
}
};
}));
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, verificationGasLimit, sponsorship, index, paymentChainId, currentChainId } = parameters;
if (currentChainId === paymentChainId) {
return resolveVerificationGasLimitForPaymentChain({
moduleAddress,
verificationGasLimit,
sponsorship,
index
});
}
return resolveVerificationGasLimitForNonPaymentChain({
moduleAddress,
verificationGasLimit,
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, verificationGasLimit, sponsorship, index } = parameters;
// if neither module address nor custom verification gas limit is provided,
// the default verification gas limit will be applied
if (!moduleAddress && !verificationGasLimit) {
return undefined;
}
if (!moduleAddress && verificationGasLimit) {
return { verificationGasLimit };
}
// at this stage moduleAddress is definitely provided
if (addressEquals(moduleAddress, SMART_SESSIONS_ADDRESS)) {
if (sponsorship) {
// handling this only for sponsored superTxn. (payment userOp
// is signed by the node and can not enable the permission) =>
// => the permission is enabled in the first meaningful userOp
// for non-sponsored superTxn, enabling the permission happens
// in the payment userOp see resolvePaymentUserOpVerificationGasLimit(...) below
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: verificationGasLimit || 1000000n
};
}
}
// return slighly increased verification gas limit
// for USE session userOps
return { verificationGasLimit: 250000n };
}
// if module is defined, however it is not SMART_SESSIONS_ADDRESS,
// return the custom verification gas limit
// more manual handling for other modules can be added here if needed
if (verificationGasLimit) {
return { verificationGasLimit };
}
// if module is provided but no custom verification gas limit is provided,
// return undefined == default verification gas limit
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, verificationGasLimit, index } = parameters;
// if neither module address nor custom verification gas limit is provided,
// the default verification gas limit will be applied
if (!moduleAddress && !verificationGasLimit) {
return undefined;
}
if (!moduleAddress && verificationGasLimit) {
return { verificationGasLimit };
}
// at this stage moduleAddress is definitely provided
if (addressEquals(moduleAddress, SMART_SESSIONS_ADDRESS)) {
// on the non-payment chain, the permission is always enabled in the first meaningful userOp
if (index === 0) {
// return increased verification gas limit for payment userOp
// in a non-sponsored superTxn
return { verificationGasLimit: verificationGasLimit || 1000000n };
}
// for all other userOps, return USE session verification gas limit
return { verificationGasLimit: 250000n };
}
if (verificationGasLimit) {
return { verificationGasLimit };
}
// if module is provided but no custom verification gas limit is provided, return undefined == default verification gas limit
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 resolvePaymentUserOpVerificationGasLimitNonSponsored = (moduleAddress, verificationGasLimit) => {
// if neither module address nor custom verification gas limit is provided,
// the default verification gas limit will be applied
if (!moduleAddress && !verificationGasLimit) {
return undefined;
}
if (!moduleAddress && verificationGasLimit) {
return { verificationGasLimit };
}
// at this stage moduleAddress is definitely provided
if (addressEquals(moduleAddress, SMART_SESSIONS_ADDRESS)) {
// return increased verification gas limit for payment userOp
// in a non-sponsored superTxn
return { verificationGasLimit: 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
// also payment userOp never utilizes USE mode of Smart Sessions Module
}
if (verificationGasLimit) {
return { verificationGasLimit };
}
// if module is provided but no custom verification gas limit is provided, return undefined == default verification gas limit
return undefined;
};
/**
* Returns the MEE versions of the orchestrator account
* on all the chains involved in the quote request.
*
* @param account - The multichain smart account
* @param instructions - The instructions to be executed
* @param sponsorship - Whether the quote is sponsored
* @param feeToken - The fee token
* @returns An array of chain IDs with their corresponding MEE versions as MeeVersionsWithChainId
* @example
* ```typescript
* const meeVersions = getMeeVersionsForQuote(account, instructions, sponsorship, feeToken)
* ```
*/
export function getMeeVersionsForQuoteRequest(account, instructions, sponsorship, feeToken) {
const usedChains = new Set();
for (const op of instructions) {
usedChains.add(Number(op.chainId));
}
// For sponsored flow, we can ignore payment chain because orchestrator account
// used there will be not user's orchestrator account but sponsorship account.
// For non-sponsored flow, the user's orchestrator account will be performing'
// the payment userOp, so we need to make sure its MEE version is consistent.
if (!sponsorship) {
// if sponsorship is false, the feeToken is defined: see GetQuoteParams type
usedChains.add(Number(feeToken.chainId));
}
return Array.from(usedChains, (chainId) => {
const deployment = account.deploymentOn(chainId, true);
return {
version: deployment.version,
chainId
};
});
}
// ====================================================
export default getQuote;
//# sourceMappingURL=getQuote.js.map