UNPKG

viem

Version:

TypeScript Interface for Ethereum

335 lines 15.7 kB
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