UNPKG

viem

Version:

TypeScript Interface for Ethereum

648 lines (597 loc) 22.7 kB
import type { Address, Narrow } from 'abitype' import { type ParseAccountErrorType, parseAccount, } from '../../../accounts/utils/parseAccount.js' import { type EstimateFeesPerGasErrorType, estimateFeesPerGas, } from '../../../actions/public/estimateFeesPerGas.js' import { getChainId as getChainId_ } from '../../../actions/public/getChainId.js' import type { Client } from '../../../clients/createClient.js' import type { Transport } from '../../../clients/transports/createTransport.js' import { AccountNotFoundError } from '../../../errors/account.js' import type { ErrorType } from '../../../errors/utils.js' import type { Call, Calls } from '../../../types/calls.js' import type { Chain } from '../../../types/chain.js' import type { Hex } from '../../../types/misc.js' import type { StateOverride } from '../../../types/stateOverride.js' import type { Assign, OneOf, Prettify, UnionOmit, } from '../../../types/utils.js' import { type EncodeFunctionDataErrorType, encodeFunctionData, } from '../../../utils/abi/encodeFunctionData.js' import { type ConcatErrorType, concat } from '../../../utils/data/concat.js' import { getAction } from '../../../utils/getAction.js' import type { SmartAccount } from '../../accounts/types.js' import type { BundlerClient } from '../../clients/createBundlerClient.js' import type { PaymasterActions } from '../../clients/decorators/paymaster.js' import type { DeriveSmartAccount, GetSmartAccountParameter, } from '../../types/account.js' import type { DeriveEntryPointVersion, EntryPointVersion, } from '../../types/entryPointVersion.js' import type { UserOperation, UserOperationRequest, } from '../../types/userOperation.js' import { type GetPaymasterDataErrorType, getPaymasterData as getPaymasterData_, } from '../paymaster/getPaymasterData.js' import { type GetPaymasterStubDataErrorType, getPaymasterStubData as getPaymasterStubData_, } from '../paymaster/getPaymasterStubData.js' import { type EstimateUserOperationGasParameters, estimateUserOperationGas, } from './estimateUserOperationGas.js' const defaultParameters = [ 'factory', 'fees', 'gas', 'paymaster', 'nonce', 'signature', ] as const export type PrepareUserOperationParameterType = | 'factory' | 'fees' | 'gas' | 'paymaster' | 'nonce' | 'signature' type FactoryProperties< entryPointVersion extends EntryPointVersion = EntryPointVersion, > = | (entryPointVersion extends '0.7' ? { factory: UserOperation['factory'] factoryData: UserOperation['factoryData'] } : never) | (entryPointVersion extends '0.6' ? { initCode: UserOperation['initCode'] } : never) type GasProperties< entryPointVersion extends EntryPointVersion = EntryPointVersion, > = | (entryPointVersion extends '0.7' ? { callGasLimit: UserOperation['callGasLimit'] preVerificationGas: UserOperation['preVerificationGas'] verificationGasLimit: UserOperation['verificationGasLimit'] paymasterPostOpGasLimit: UserOperation['paymasterPostOpGasLimit'] paymasterVerificationGasLimit: UserOperation['paymasterVerificationGasLimit'] } : never) | (entryPointVersion extends '0.6' ? { callGasLimit: UserOperation['callGasLimit'] preVerificationGas: UserOperation['preVerificationGas'] verificationGasLimit: UserOperation['verificationGasLimit'] } : never) type FeeProperties = { maxFeePerGas: UserOperation['maxFeePerGas'] maxPriorityFeePerGas: UserOperation['maxPriorityFeePerGas'] } type NonceProperties = { nonce: UserOperation['nonce'] } type PaymasterProperties< entryPointVersion extends EntryPointVersion = EntryPointVersion, > = | (entryPointVersion extends '0.7' ? { paymaster: UserOperation['paymaster'] paymasterData: UserOperation['paymasterData'] paymasterPostOpGasLimit: UserOperation['paymasterPostOpGasLimit'] paymasterVerificationGasLimit: UserOperation['paymasterVerificationGasLimit'] } : never) | (entryPointVersion extends '0.6' ? { paymasterAndData: UserOperation['paymasterAndData'] } : never) type SignatureProperties = { signature: UserOperation['signature'] } export type PrepareUserOperationRequest< account extends SmartAccount | undefined = SmartAccount | undefined, accountOverride extends SmartAccount | undefined = SmartAccount | undefined, calls extends readonly unknown[] = readonly unknown[], // _derivedAccount extends SmartAccount | undefined = DeriveSmartAccount< account, accountOverride >, _derivedVersion extends EntryPointVersion = DeriveEntryPointVersion<_derivedAccount>, > = Assign< UserOperationRequest<_derivedVersion>, OneOf<{ calls: Calls<Narrow<calls>> } | { callData: Hex }> & { parameters?: readonly PrepareUserOperationParameterType[] | undefined paymaster?: | Address | true | { /** Retrieves paymaster-related User Operation properties to be used for sending the User Operation. */ getPaymasterData?: PaymasterActions['getPaymasterData'] | undefined /** Retrieves paymaster-related User Operation properties to be used for gas estimation. */ getPaymasterStubData?: | PaymasterActions['getPaymasterStubData'] | undefined } | undefined /** Paymaster context to pass to `getPaymasterData` and `getPaymasterStubData` calls. */ paymasterContext?: unknown | undefined /** State overrides for the User Operation call. */ stateOverride?: StateOverride | undefined } > export type PrepareUserOperationParameters< account extends SmartAccount | undefined = SmartAccount | undefined, accountOverride extends SmartAccount | undefined = SmartAccount | undefined, calls extends readonly unknown[] = readonly unknown[], request extends PrepareUserOperationRequest< account, accountOverride, calls > = PrepareUserOperationRequest<account, accountOverride, calls>, > = request & GetSmartAccountParameter<account, accountOverride> export type PrepareUserOperationReturnType< account extends SmartAccount | undefined = SmartAccount | undefined, accountOverride extends SmartAccount | undefined = SmartAccount | undefined, calls extends readonly unknown[] = readonly unknown[], request extends PrepareUserOperationRequest< account, accountOverride, calls > = PrepareUserOperationRequest<account, accountOverride, calls>, // _parameters extends PrepareUserOperationParameterType = request['parameters'] extends readonly PrepareUserOperationParameterType[] ? request['parameters'][number] : (typeof defaultParameters)[number], _derivedAccount extends SmartAccount | undefined = DeriveSmartAccount< account, accountOverride >, _derivedVersion extends EntryPointVersion = DeriveEntryPointVersion<_derivedAccount>, > = Prettify< UnionOmit<request, 'calls' | 'parameters'> & { callData: Hex paymasterAndData: _derivedVersion extends '0.6' ? Hex : undefined sender: UserOperation['sender'] } & (Extract<_parameters, 'factory'> extends never ? {} : FactoryProperties<_derivedVersion>) & (Extract<_parameters, 'nonce'> extends never ? {} : NonceProperties) & (Extract<_parameters, 'fees'> extends never ? {} : FeeProperties) & (Extract<_parameters, 'gas'> extends never ? {} : GasProperties<_derivedVersion>) & (Extract<_parameters, 'paymaster'> extends never ? {} : PaymasterProperties<_derivedVersion>) & (Extract<_parameters, 'signature'> extends never ? {} : SignatureProperties) > export type PrepareUserOperationErrorType = | ParseAccountErrorType | GetPaymasterStubDataErrorType | GetPaymasterDataErrorType | EncodeFunctionDataErrorType | ConcatErrorType | EstimateFeesPerGasErrorType | ErrorType /** * 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< account extends SmartAccount | undefined, const calls extends readonly unknown[], const request extends PrepareUserOperationRequest< account, accountOverride, calls >, accountOverride extends SmartAccount | undefined = undefined, >( client: Client<Transport, Chain | undefined, account>, parameters_: PrepareUserOperationParameters< account, accountOverride, calls, request >, ): Promise< PrepareUserOperationReturnType<account, accountOverride, calls, request> > { const parameters = parameters_ as PrepareUserOperationParameters 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 as unknown as BundlerClient //////////////////////////////////////////////////////////////////////////////// // 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: any) => getAction( bundlerClient, getPaymasterStubData_, 'getPaymasterStubData', )(parameters), getPaymasterData: (parameters: any) => 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) as typeof getPaymasterStubData, 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, } as PrepareUserOperationRequest //////////////////////////////////////////////////////////////////////////////// // 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_ as Call if (call.abi) return { data: encodeFunctionData(call), to: call.to, value: call.value, } as Call return call as 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 as UserOperation, }) 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 as any) } if (typeof fees !== 'undefined') request = { ...request, ...(fees as any) } 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 as UserOperation, ) } //////////////////////////////////////////////////////////////////////////////// // `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: number | undefined async function getChainId(): Promise<number> { 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 as UserOperation), }) isPaymasterPopulated = isFinal request = { ...request, ...paymasterArgs, } as PrepareUserOperationRequest } //////////////////////////////////////////////////////////////////////////////// // `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 as UserOperation, ) request = { ...request, ...gas, } as PrepareUserOperationRequest } // 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, } as EstimateUserOperationGasParameters) 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, } as PrepareUserOperationRequest } } //////////////////////////////////////////////////////////////////////////////// // 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 as UserOperation), }) request = { ...request, ...paymaster, } as PrepareUserOperationRequest } //////////////////////////////////////////////////////////////////////////////// // 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 as unknown as PrepareUserOperationReturnType< account, accountOverride, calls, request > }