viem
Version:
335 lines • 15.7 kB
JavaScript
import { parseAccount, } from '../../../accounts/utils/parseAccount.js';
import { estimateFeesPerGas, } from '../../../actions/public/estimateFeesPerGas.js';
import { getChainId as getChainId_ } from '../../../actions/public/getChainId.js';
import { AccountNotFoundError } from '../../../errors/account.js';
import { encodeFunctionData, } from '../../../utils/abi/encodeFunctionData.js';
import { concat } from '../../../utils/data/concat.js';
import { getAction } from '../../../utils/getAction.js';
import { getPaymasterData as getPaymasterData_, } from '../paymaster/getPaymasterData.js';
import { getPaymasterStubData as getPaymasterStubData_, } from '../paymaster/getPaymasterStubData.js';
import { estimateUserOperationGas, } from './estimateUserOperationGas.js';
const defaultParameters = [
'factory',
'fees',
'gas',
'paymaster',
'nonce',
'signature',
];
/**
* Prepares a User Operation and fills in missing properties.
*
* - Docs: https://viem.sh/actions/bundler/prepareUserOperation
*
* @param args - {@link PrepareUserOperationParameters}
* @returns The User Operation. {@link PrepareUserOperationReturnType}
*
* @example
* import { createBundlerClient, http } from 'viem'
* import { toSmartAccount } from 'viem/accounts'
* import { mainnet } from 'viem/chains'
* import { prepareUserOperation } from 'viem/actions'
*
* const account = await toSmartAccount({ ... })
*
* const client = createBundlerClient({
* chain: mainnet,
* transport: http(),
* })
*
* const request = await prepareUserOperation(client, {
* account,
* calls: [{ to: '0x...', value: parseEther('1') }],
* })
*/
export async function prepareUserOperation(client, parameters_) {
const parameters = parameters_;
const { account: account_ = client.account, parameters: properties = defaultParameters, stateOverride, } = parameters;
////////////////////////////////////////////////////////////////////////////////
// Assert that an Account is defined.
////////////////////////////////////////////////////////////////////////////////
if (!account_)
throw new AccountNotFoundError();
const account = parseAccount(account_);
////////////////////////////////////////////////////////////////////////////////
// Declare typed Bundler Client.
////////////////////////////////////////////////////////////////////////////////
const bundlerClient = client;
////////////////////////////////////////////////////////////////////////////////
// Declare Paymaster properties.
////////////////////////////////////////////////////////////////////////////////
const paymaster = parameters.paymaster ?? bundlerClient?.paymaster;
const paymasterAddress = typeof paymaster === 'string' ? paymaster : undefined;
const { getPaymasterStubData, getPaymasterData } = (() => {
// If `paymaster: true`, we will assume the Bundler Client supports Paymaster Actions.
if (paymaster === true)
return {
getPaymasterStubData: (parameters) => getAction(bundlerClient, getPaymasterStubData_, 'getPaymasterStubData')(parameters),
getPaymasterData: (parameters) => getAction(bundlerClient, getPaymasterData_, 'getPaymasterData')(parameters),
};
// If Actions are passed to `paymaster` (via Paymaster Client or directly), we will use them.
if (typeof paymaster === 'object') {
const { getPaymasterStubData, getPaymasterData } = paymaster;
return {
getPaymasterStubData: (getPaymasterData && getPaymasterStubData
? getPaymasterStubData
: getPaymasterData),
getPaymasterData: getPaymasterData && getPaymasterStubData
? getPaymasterData
: undefined,
};
}
// No Paymaster functions.
return {
getPaymasterStubData: undefined,
getPaymasterData: undefined,
};
})();
const paymasterContext = parameters.paymasterContext
? parameters.paymasterContext
: bundlerClient?.paymasterContext;
////////////////////////////////////////////////////////////////////////////////
// Set up the User Operation request.
////////////////////////////////////////////////////////////////////////////////
let request = {
...parameters,
paymaster: paymasterAddress,
sender: account.address,
};
////////////////////////////////////////////////////////////////////////////////
// Concurrently prepare properties required to fill the User Operation.
////////////////////////////////////////////////////////////////////////////////
const [callData, factory, fees, nonce] = await Promise.all([
(async () => {
if (parameters.calls)
return account.encodeCalls(parameters.calls.map((call_) => {
const call = call_;
if (call.abi)
return {
data: encodeFunctionData(call),
to: call.to,
value: call.value,
};
return call;
}));
return parameters.callData;
})(),
(async () => {
if (!properties.includes('factory'))
return undefined;
if (parameters.initCode)
return { initCode: parameters.initCode };
if (parameters.factory && parameters.factoryData) {
return {
factory: parameters.factory,
factoryData: parameters.factoryData,
};
}
const { factory, factoryData } = await account.getFactoryArgs();
if (account.entryPoint.version === '0.6')
return {
initCode: factory && factoryData ? concat([factory, factoryData]) : undefined,
};
return {
factory,
factoryData,
};
})(),
(async () => {
if (!properties.includes('fees'))
return undefined;
// If we have sufficient properties for fees, return them.
if (typeof parameters.maxFeePerGas === 'bigint' &&
typeof parameters.maxPriorityFeePerGas === 'bigint')
return request;
// If the Bundler Client has a `estimateFeesPerGas` hook, run it.
if (bundlerClient?.userOperation?.estimateFeesPerGas) {
const fees = await bundlerClient.userOperation.estimateFeesPerGas({
account,
bundlerClient,
userOperation: request,
});
return {
...request,
...fees,
};
}
// Otherwise, we will need to estimate the fees to fill the fee properties.
try {
const client_ = bundlerClient.client ?? client;
const fees = await getAction(client_, estimateFeesPerGas, 'estimateFeesPerGas')({
chain: client_.chain,
type: 'eip1559',
});
return {
maxFeePerGas: typeof parameters.maxFeePerGas === 'bigint'
? parameters.maxFeePerGas
: BigInt(
// Bundlers unfortunately have strict rules on fee prechecks – we will need to set a generous buffer.
2n * fees.maxFeePerGas),
maxPriorityFeePerGas: typeof parameters.maxPriorityFeePerGas === 'bigint'
? parameters.maxPriorityFeePerGas
: BigInt(
// Bundlers unfortunately have strict rules on fee prechecks – we will need to set a generous buffer.
2n * fees.maxPriorityFeePerGas),
};
}
catch {
return undefined;
}
})(),
(async () => {
if (!properties.includes('nonce'))
return undefined;
if (typeof parameters.nonce === 'bigint')
return parameters.nonce;
return account.getNonce();
})(),
]);
////////////////////////////////////////////////////////////////////////////////
// Fill User Operation with the prepared properties from above.
////////////////////////////////////////////////////////////////////////////////
if (typeof callData !== 'undefined')
request.callData = callData;
if (typeof factory !== 'undefined')
request = { ...request, ...factory };
if (typeof fees !== 'undefined')
request = { ...request, ...fees };
if (typeof nonce !== 'undefined')
request.nonce = nonce;
////////////////////////////////////////////////////////////////////////////////
// Fill User Operation with the `signature` property.
////////////////////////////////////////////////////////////////////////////////
if (properties.includes('signature')) {
if (typeof parameters.signature !== 'undefined')
request.signature = parameters.signature;
else
request.signature = await account.getStubSignature(request);
}
////////////////////////////////////////////////////////////////////////////////
// `initCode` is required to be filled with EntryPoint 0.6.
////////////////////////////////////////////////////////////////////////////////
// If no `initCode` is provided, we use an empty bytes string.
if (account.entryPoint.version === '0.6' && !request.initCode)
request.initCode = '0x';
////////////////////////////////////////////////////////////////////////////////
// Fill User Operation with paymaster-related properties for **gas estimation**.
////////////////////////////////////////////////////////////////////////////////
let chainId;
async function getChainId() {
if (chainId)
return chainId;
if (client.chain)
return client.chain.id;
const chainId_ = await getAction(client, getChainId_, 'getChainId')({});
chainId = chainId_;
return chainId;
}
// If the User Operation is intended to be sponsored, we will need to fill the paymaster-related
// User Operation properties required to estimate the User Operation gas.
let isPaymasterPopulated = false;
if (properties.includes('paymaster') &&
getPaymasterStubData &&
!paymasterAddress &&
!parameters.paymasterAndData) {
const { isFinal = false, sponsor, ...paymasterArgs } = await getPaymasterStubData({
chainId: await getChainId(),
entryPointAddress: account.entryPoint.address,
context: paymasterContext,
...request,
});
isPaymasterPopulated = isFinal;
request = {
...request,
...paymasterArgs,
};
}
////////////////////////////////////////////////////////////////////////////////
// `paymasterAndData` is required to be filled with EntryPoint 0.6.
////////////////////////////////////////////////////////////////////////////////
// If no `paymasterAndData` is provided, we use an empty bytes string.
if (account.entryPoint.version === '0.6' && !request.paymasterAndData)
request.paymasterAndData = '0x';
////////////////////////////////////////////////////////////////////////////////
// Fill User Operation with gas-related properties.
////////////////////////////////////////////////////////////////////////////////
if (properties.includes('gas')) {
// If the Account has opinionated gas estimation logic, run the `estimateGas` hook and
// fill the request with the prepared gas properties.
if (account.userOperation?.estimateGas) {
const gas = await account.userOperation.estimateGas(request);
request = {
...request,
...gas,
};
}
// If not all the gas properties are already populated, we will need to estimate the gas
// to fill the gas properties.
if (typeof request.callGasLimit === 'undefined' ||
typeof request.preVerificationGas === 'undefined' ||
typeof request.verificationGasLimit === 'undefined' ||
(request.paymaster &&
typeof request.paymasterPostOpGasLimit === 'undefined') ||
(request.paymaster &&
typeof request.paymasterVerificationGasLimit === 'undefined')) {
const gas = await getAction(bundlerClient, estimateUserOperationGas, 'estimateUserOperationGas')({
account,
// Some Bundlers fail if nullish gas values are provided for gas estimation :') –
// so we will need to set a default zeroish value.
callGasLimit: 0n,
preVerificationGas: 0n,
verificationGasLimit: 0n,
stateOverride,
...(request.paymaster
? {
paymasterPostOpGasLimit: 0n,
paymasterVerificationGasLimit: 0n,
}
: {}),
...request,
});
request = {
...request,
callGasLimit: request.callGasLimit ?? gas.callGasLimit,
preVerificationGas: request.preVerificationGas ?? gas.preVerificationGas,
verificationGasLimit: request.verificationGasLimit ?? gas.verificationGasLimit,
paymasterPostOpGasLimit: request.paymasterPostOpGasLimit ?? gas.paymasterPostOpGasLimit,
paymasterVerificationGasLimit: request.paymasterVerificationGasLimit ??
gas.paymasterVerificationGasLimit,
};
}
}
////////////////////////////////////////////////////////////////////////////////
// Fill User Operation with paymaster-related properties for **sending** the User Operation.
////////////////////////////////////////////////////////////////////////////////
// If the User Operation is intended to be sponsored, we will need to fill the paymaster-related
// User Operation properties required to send the User Operation.
if (properties.includes('paymaster') &&
getPaymasterData &&
!paymasterAddress &&
!parameters.paymasterAndData &&
!isPaymasterPopulated) {
// Retrieve paymaster-related User Operation properties to be used for **sending** the User Operation.
const paymaster = await getPaymasterData({
chainId: await getChainId(),
entryPointAddress: account.entryPoint.address,
context: paymasterContext,
...request,
});
request = {
...request,
...paymaster,
};
}
////////////////////////////////////////////////////////////////////////////////
// Remove redundant properties that do not conform to the User Operation schema.
////////////////////////////////////////////////////////////////////////////////
delete request.calls;
delete request.parameters;
delete request.paymasterContext;
if (typeof request.paymaster !== 'string')
delete request.paymaster;
////////////////////////////////////////////////////////////////////////////////
return request;
}
//# sourceMappingURL=prepareUserOperation.js.map