diff --git a/packages/alchemy/src/middleware/gas-fees.ts b/packages/alchemy/src/middleware/gas-fees.ts index 478ae4d56a..1ecb4994c5 100644 --- a/packages/alchemy/src/middleware/gas-fees.ts +++ b/packages/alchemy/src/middleware/gas-fees.ts @@ -1,4 +1,4 @@ -import { applyFeeOption, type BigNumberish } from "@alchemy/aa-core"; +import { applyFeeOption, applyUserOperationOverride } from "@alchemy/aa-core"; import type { AlchemyProvider } from "../provider.js"; import type { ClientWithAlchemyMethods } from "./client.js"; @@ -6,40 +6,43 @@ export const withAlchemyGasFeeEstimator = ( provider: AlchemyProvider ): AlchemyProvider => { provider.withFeeDataGetter(async (struct, overrides, feeOptions) => { - const maxPriorityFeePerGas = - overrides?.maxPriorityFeePerGas != null - ? overrides?.maxPriorityFeePerGas - : // it's a fair assumption that if someone is using this Alchemy Middleware, then they are using Alchemy RPC - applyFeeOption( - await (provider.rpcClient as ClientWithAlchemyMethods).request({ - method: "rundler_maxPriorityFeePerGas", - params: [], - }), - feeOptions?.maxPriorityFeePerGas - ); + let [block, maxPriorityFeePerGasEstimate] = await Promise.all([ + provider.rpcClient.getBlock({ blockTag: "latest" }), + // it's a fair assumption that if someone is using this Alchemy Middleware, then they are using Alchemy RPC + (provider.rpcClient as ClientWithAlchemyMethods).request({ + method: "rundler_maxPriorityFeePerGas", + params: [], + }), + ]); + const baseFeePerGas = block.baseFeePerGas; + if (baseFeePerGas == null) { + throw new Error("baseFeePerGas is null"); + } - const estimateMaxFeePerGas = async (priorityFeePerGas: BigNumberish) => { - const block = await provider.rpcClient.getBlock({ blockTag: "latest" }); - const baseFeePerGas = block.baseFeePerGas; - if (baseFeePerGas == null) { - throw new Error("baseFeePerGas is null"); - } - return applyFeeOption( - baseFeePerGas + BigInt(priorityFeePerGas), - feeOptions?.maxFeePerGas + const maxPriorityFeePerGas = + applyUserOperationOverride( + maxPriorityFeePerGasEstimate, + overrides?.maxPriorityFeePerGas + ) || + applyFeeOption( + maxPriorityFeePerGasEstimate, + feeOptions?.maxPriorityFeePerGas ); - }; - const maxFeePerGas = - overrides?.maxFeePerGas != null - ? overrides?.maxFeePerGas - : await estimateMaxFeePerGas(maxPriorityFeePerGas); + applyUserOperationOverride( + baseFeePerGas + BigInt(maxPriorityFeePerGas), + overrides?.maxPriorityFeePerGas + ) || + applyFeeOption( + baseFeePerGas + BigInt(maxPriorityFeePerGas), + feeOptions?.maxPriorityFeePerGas + ); return { ...struct, maxPriorityFeePerGas, maxFeePerGas, }; - }, provider.feeOptions); + }); return provider; }; diff --git a/packages/alchemy/src/middleware/gas-manager.ts b/packages/alchemy/src/middleware/gas-manager.ts index e301969c06..d9fcb07feb 100644 --- a/packages/alchemy/src/middleware/gas-manager.ts +++ b/packages/alchemy/src/middleware/gas-manager.ts @@ -1,6 +1,7 @@ import { deepHexlify, filterUndefined, + isBigNumberish, isPercentage, resolveProperties, type AccountMiddlewareFn, @@ -169,15 +170,25 @@ const withAlchemyGasAndPaymasterAndDataMiddleware =

( const overrideField = ( field: keyof UserOperationFeeOptions ): Hex | Percentage | undefined => { - // one-off absolute override if (overrides?.[field] != null) { - return deepHexlify(overrides[field]); + // one-off absolute override + if (isBigNumberish(overrides[field])) { + return deepHexlify(overrides[field]); + } + // one-off percentage overrides + else { + return { + percentage: + 100 + Number((overrides[field] as Percentage).percentage), + }; + } } // provider level fee options with percentage if (isPercentage(feeOptions?.[field])) { return { - percentage: 100 + Number(feeOptions?.[field]?.percentage), + percentage: + 100 + Number((feeOptions![field] as Percentage).percentage), }; } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a43fd77964..c1453bc03a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -50,6 +50,7 @@ export type * from "./utils/index.js"; export { ChainSchema, applyFeeOption, + applyUserOperationOverride, asyncPipe, bigIntMax, bigIntPercent, diff --git a/packages/core/src/provider/base.ts b/packages/core/src/provider/base.ts index 3717052260..791f1976bc 100644 --- a/packages/core/src/provider/base.ts +++ b/packages/core/src/provider/base.ts @@ -33,6 +33,7 @@ import { } from "../types.js"; import { applyFeeOption, + applyUserOperationOverride, asyncPipe, bigIntMax, bigIntPercent, @@ -50,10 +51,7 @@ import { createSmartAccountProviderConfigSchema } from "./schema.js"; import type { AccountMiddlewareFn, AccountMiddlewareOverrideFn, - FeeDataFeeOptions, FeeDataMiddleware, - FeeOptionsMiddleware, - GasEstimatorFeeOptions, GasEstimatorMiddleware, ISmartAccountProvider, PaymasterAndDataMiddleware, @@ -407,20 +405,15 @@ export class SmartAccountProvider< const { maxFeePerGas, maxPriorityFeePerGas } = await this._runMiddlewareStack(uoToSubmit, overrides); - const _maxPriorityFeePerGas = - bigIntMax( - BigInt(maxPriorityFeePerGas ?? 0n), - bigIntPercent(uoToDrop.maxPriorityFeePerGas, 110n) - ) ?? 0n; const _overrides: UserOperationOverrides = { maxFeePerGas: bigIntMax( BigInt(maxFeePerGas ?? 0n), - bigIntPercent( - BigInt(uoToDrop.maxFeePerGas) - _maxPriorityFeePerGas, - 110n - ) + _maxPriorityFeePerGas + bigIntPercent(uoToDrop.maxFeePerGas, 110n) + ), + maxPriorityFeePerGas: bigIntMax( + BigInt(maxPriorityFeePerGas ?? 0n), + bigIntPercent(uoToDrop.maxPriorityFeePerGas, 110n) ), - maxPriorityFeePerGas: _maxPriorityFeePerGas, paymasterAndData: uoToDrop.paymasterAndData, }; @@ -530,41 +523,39 @@ export class SmartAccountProvider< overrides, feeOptions ) => { - let { callGasLimit, verificationGasLimit, preVerificationGas } = - overrides ?? {}; + const request = deepHexlify(await resolveProperties(struct)); + const estimates = await this.rpcClient.estimateUserOperationGas( + request, + this.getEntryPointAddress() + ); - if ( - callGasLimit == null || - verificationGasLimit == null || - preVerificationGas == null - ) { - const request = deepHexlify(await resolveProperties(struct)); - const estimates = await this.rpcClient.estimateUserOperationGas( - request, - this.getEntryPointAddress() + const callGasLimit = + applyUserOperationOverride( + estimates.callGasLimit, + overrides?.callGasLimit + ) || applyFeeOption(estimates.callGasLimit, feeOptions?.callGasLimit); + const verificationGasLimit = + applyUserOperationOverride( + estimates.verificationGasLimit, + overrides?.verificationGasLimit + ) || + applyFeeOption( + estimates.verificationGasLimit, + feeOptions?.verificationGasLimit + ); + const preVerificationGas = + applyUserOperationOverride( + estimates.preVerificationGas, + overrides?.preVerificationGas + ) || + applyFeeOption( + estimates.preVerificationGas, + feeOptions?.preVerificationGas ); - - callGasLimit = - callGasLimit ?? - applyFeeOption(estimates.callGasLimit, feeOptions?.callGasLimit); - verificationGasLimit = - verificationGasLimit ?? - applyFeeOption( - estimates.verificationGasLimit, - feeOptions?.verificationGasLimit - ); - preVerificationGas = - preVerificationGas ?? - applyFeeOption( - estimates.preVerificationGas, - feeOptions?.preVerificationGas - ); - } struct.callGasLimit = callGasLimit; struct.verificationGasLimit = verificationGasLimit; struct.preVerificationGas = preVerificationGas; - return struct; }; @@ -573,11 +564,6 @@ export class SmartAccountProvider< overrides, feeOptions ) => { - const estimateMaxPriorityFeePerGas = async () => { - const estimate = await this.rpcClient.estimateMaxPriorityFeePerGas(); - return applyFeeOption(estimate, feeOptions?.maxPriorityFeePerGas); - }; - // maxFeePerGas must be at least the sum of maxPriorityFeePerGas and baseFee // so we need to accommodate for the fee option applied maxPriorityFeePerGas for the maxFeePerGas // @@ -586,59 +572,35 @@ export class SmartAccountProvider< // // Refer to https://docs.alchemy.com/docs/maxpriorityfeepergas-vs-maxfeepergas // for more information about maxFeePerGas and maxPriorityFeePerGas - const estimateMaxFeePerGas = async (maxPriorityFeePerGas: BigNumberish) => { - const feeData = await this.rpcClient.estimateFeesPerGas(); - if (!feeData.maxFeePerGas || !feeData.maxPriorityFeePerGas) { - throw new Error( - "feeData is missing maxFeePerGas or maxPriorityFeePerGas" - ); - } - const baseFee = applyFeeOption( - feeData.maxFeePerGas - feeData.maxPriorityFeePerGas, - feeOptions?.maxFeePerGas - ); - - return BigInt(baseFee) + BigInt(maxPriorityFeePerGas); - }; - struct.maxPriorityFeePerGas = - overrides?.maxPriorityFeePerGas != null - ? overrides?.maxPriorityFeePerGas - : await estimateMaxPriorityFeePerGas(); - struct.maxFeePerGas = - overrides?.maxFeePerGas != null - ? overrides?.maxFeePerGas - : await estimateMaxFeePerGas(struct.maxPriorityFeePerGas); - - return struct; - }; - - readonly feeOptionsMiddleware: AccountMiddlewareFn = async ( - struct, - overrides - ) => { - const resolved = await resolveProperties(struct); - - // max priority fee per gas to be added back after fee options applied - // maxFeePerGas fee option will be applied at the base fee level - resolved.maxFeePerGas = - BigInt(resolved.maxFeePerGas ?? 0n) - - BigInt(resolved.maxPriorityFeePerGas ?? 0n); - - Object.keys(this.feeOptions ?? {}).forEach((field) => { - if (overrides?.[field as keyof UserOperationOverrides] !== undefined) - return; - resolved[field as keyof UserOperationFeeOptions] = applyFeeOption( - resolved[field as keyof UserOperationFeeOptions], - this.feeOptions[field as keyof UserOperationFeeOptions] + const feeData = await this.rpcClient.estimateFeesPerGas(); + if (!feeData.maxFeePerGas || !feeData.maxPriorityFeePerGas) { + throw new Error( + "feeData is missing maxFeePerGas or maxPriorityFeePerGas" ); - }); - - resolved.maxFeePerGas = - BigInt(resolved.maxFeePerGas ?? 0n) + - BigInt(resolved.maxPriorityFeePerGas ?? 0n); + } - return resolved; + let maxPriorityFeePerGas: BigNumberish = + await this.rpcClient.estimateMaxPriorityFeePerGas(); + maxPriorityFeePerGas = + applyUserOperationOverride( + maxPriorityFeePerGas, + overrides?.maxPriorityFeePerGas + ) || + applyFeeOption(maxPriorityFeePerGas, feeOptions?.maxPriorityFeePerGas); + + let maxFeePerGas: BigNumberish = + feeData.maxFeePerGas - + feeData.maxPriorityFeePerGas + + BigInt(maxPriorityFeePerGas); + + maxFeePerGas = + applyUserOperationOverride(maxFeePerGas, overrides?.maxFeePerGas) || + applyFeeOption(maxFeePerGas, feeOptions?.maxFeePerGas); + + struct.maxFeePerGas = maxFeePerGas; + struct.maxPriorityFeePerGas = maxPriorityFeePerGas; + return struct; }; readonly customMiddleware: AccountMiddlewareFn = noOpMiddleware; @@ -662,16 +624,7 @@ export class SmartAccountProvider< return this; }; - withGasEstimator = ( - override: GasEstimatorMiddleware, - feeOptions?: GasEstimatorFeeOptions - ): this => { - // Note that this overrides the default gasEstimator middleware and - // also the gas estimator fee options set on the provider upon initialization - this.feeOptions.callGasLimit = feeOptions?.callGasLimit; - this.feeOptions.verificationGasLimit = feeOptions?.verificationGasLimit; - this.feeOptions.preVerificationGas = feeOptions?.preVerificationGas; - + withGasEstimator = (override: GasEstimatorMiddleware): this => { defineReadOnly( this, "gasEstimator", @@ -680,15 +633,7 @@ export class SmartAccountProvider< return this; }; - withFeeDataGetter = ( - override: FeeDataMiddleware, - feeOptions?: FeeDataFeeOptions - ): this => { - // Note that this overrides the default gasEstimator middleware and - // also the gas estimator fee options set on the provider upon initialization - this.feeOptions.maxFeePerGas = feeOptions?.maxFeePerGas; - this.feeOptions.maxPriorityFeePerGas = feeOptions?.maxPriorityFeePerGas; - + withFeeDataGetter = (override: FeeDataMiddleware): this => { defineReadOnly( this, "feeDataGetter", @@ -697,15 +642,6 @@ export class SmartAccountProvider< return this; }; - withFeeOptionsMiddleware = (override: FeeOptionsMiddleware): this => { - defineReadOnly( - this, - "feeOptionsMiddleware", - this.overrideMiddlewareFunction(override) - ); - return this; - }; - withCustomMiddleware = (override: AccountMiddlewareFn): this => { defineReadOnly(this, "customMiddleware", override); diff --git a/packages/core/src/provider/types.ts b/packages/core/src/provider/types.ts index b92ee681b7..a02f1b608a 100644 --- a/packages/core/src/provider/types.ts +++ b/packages/core/src/provider/types.ts @@ -82,28 +82,9 @@ export type PaymasterAndDataMiddleware = AccountMiddlewareOverrideFn< export type GasEstimatorMiddleware = AccountMiddlewareOverrideFn< "callGasLimit" | "preVerificationGas" | "verificationGasLimit" >; -export type GasEstimatorFeeOptions = Partial< - Pick< - UserOperationFeeOptions, - "callGasLimit" | "preVerificationGas" | "verificationGasLimit" - > ->; - export type FeeDataMiddleware = AccountMiddlewareOverrideFn< "maxFeePerGas" | "maxPriorityFeePerGas" >; -export type FeeDataFeeOptions = Partial< - Pick ->; - -export type FeeOptionsMiddleware = AccountMiddlewareOverrideFn< - never, - | "callGasLimit" - | "preVerificationGas" - | "verificationGasLimit" - | "maxFeePerGas" - | "maxPriorityFeePerGas" ->; export type SmartAccountProviderOpts = z.infer< typeof SmartAccountProviderOptsSchema @@ -126,11 +107,9 @@ export interface ISmartAccountProvider< readonly paymasterDataMiddleware: AccountMiddlewareFn; readonly gasEstimator: AccountMiddlewareFn; readonly feeDataGetter: AccountMiddlewareFn; - readonly feeOptionsMiddleware: AccountMiddlewareFn; readonly customMiddleware?: AccountMiddlewareFn; readonly account?: ISmartContractAccount; - readonly feeOptions: UserOperationFeeOptions; /** * Sends a user operation using the connected account. @@ -341,40 +320,19 @@ export interface ISmartAccountProvider< * Overrides the gasEstimator middleware which is used for setting the gasLimit fields on the UserOperation * prior to execution. * - * Note that when using your custom gas estimator with this override method, not only the default gasEstimator middleware, - * but also the gas estimator fee options set during initialization is overriden. - * Thus, when using your custom gas estimator, you need to set the fee options from this method instead. - * * @param override - a function for overriding the default gas estimator middleware - * @param feeOptions - optional GasEstimatorFeeOptions to set at the global level of the provider. * @returns */ - withGasEstimator: ( - override: GasEstimatorMiddleware, - feeOptions?: GasEstimatorFeeOptions - ) => this; + withGasEstimator: (override: GasEstimatorMiddleware) => this; /** * Overrides the feeDataGetter middleware which is used for setting the fee fields on the UserOperation * prior to execution. * * @param override - a function for overriding the default feeDataGetter middleware - * @param feeOptions - optional FeeDataFeeOptions to set at the global level of the provider. - * @returns - */ - withFeeDataGetter: ( - override: FeeDataMiddleware, - feeOptions?: FeeDataFeeOptions - ) => this; - - /** - * Overrides the feeOptions middleware which is used for applying the provider feeOptions - * on UserOperationFeeOptions fields on the UserOperation prior to execution. - * - * @param override - a function for overriding the default gas estimator middleware * @returns */ - withFeeOptionsMiddleware: (override: FeeOptionsMiddleware) => this; + withFeeDataGetter: (override: FeeDataMiddleware) => this; /** * Adds a function to the middleware call stack that runs before calling the paymaster middleware. diff --git a/packages/core/src/schema.ts b/packages/core/src/schema.ts deleted file mode 100644 index c452931779..0000000000 --- a/packages/core/src/schema.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { z } from "zod"; -import { BigNumberishRangeSchema, PercentageSchema } from "./utils/index.js"; - -export const UserOperationFeeOptionsFieldSchema = - BigNumberishRangeSchema.merge(PercentageSchema).partial(); - -export const UserOperationFeeOptionsSchema = z - .object({ - maxFeePerGas: UserOperationFeeOptionsFieldSchema, - maxPriorityFeePerGas: UserOperationFeeOptionsFieldSchema, - callGasLimit: UserOperationFeeOptionsFieldSchema, - verificationGasLimit: UserOperationFeeOptionsFieldSchema, - preVerificationGas: UserOperationFeeOptionsFieldSchema, - }) - .partial() - .strict(); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index d3a889aa91..c6af246ad1 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -43,17 +43,18 @@ export type UserOperationFeeOptions = z.infer< typeof UserOperationFeeOptionsSchema >; -export type UserOperationOverrides = Partial< - Pick< - UserOperationStruct, - | "callGasLimit" - | "maxFeePerGas" - | "maxPriorityFeePerGas" - | "paymasterAndData" - | "preVerificationGas" - | "verificationGasLimit" - > ->; +export type UserOperationOverrides = Partial<{ + callGasLimit: UserOperationStruct["callGasLimit"] | Percentage; + maxFeePerGas: UserOperationStruct["maxFeePerGas"] | Percentage; + maxPriorityFeePerGas: + | UserOperationStruct["maxPriorityFeePerGas"] + | Percentage; + preVerificationGas: UserOperationStruct["preVerificationGas"] | Percentage; + verificationGasLimit: + | UserOperationStruct["verificationGasLimit"] + | Percentage; + paymasterAndData: UserOperationStruct["paymasterAndData"]; +}>; // represents the request as it needs to be formatted for RPC requests export interface UserOperationRequest { diff --git a/packages/core/src/utils/bigint.ts b/packages/core/src/utils/bigint.ts index 1422fb8da0..5242abdd8e 100644 --- a/packages/core/src/utils/bigint.ts +++ b/packages/core/src/utils/bigint.ts @@ -8,7 +8,7 @@ import type { BigNumberish } from "../types"; */ export const bigIntMax = (...args: bigint[]): bigint => { if (!args.length) { - return undefined; + throw new Error("bigIntMax requires at least one argument"); } return args.reduce((m, c) => (m > c ? m : c)); diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 89a2ceb1ae..11d85fb223 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -97,6 +97,25 @@ export function deepHexlify(obj: any): any { ); } +export function applyUserOperationOverride( + value: BigNumberish | undefined, + override?: BigNumberish | Percentage +): BigNumberish | undefined { + if (override == null) { + return value; + } + + if (isBigNumberish(override)) { + return override; + } + // percentage override + else { + return value != null + ? bigIntPercent(value, BigInt(100 + override.percentage)) + : value; + } +} + export function applyFeeOption( value: BigNumberish | undefined, feeOption?: UserOperationFeeOptionsField @@ -104,7 +123,7 @@ export function applyFeeOption( if (feeOption == null) { return value ?? 0n; } - return value + return value != null ? bigIntClamp( feeOption.percentage ? bigIntPercent(value, BigInt(100 + feeOption.percentage)) @@ -183,11 +202,11 @@ export function defineReadOnly( } export function isBigNumberish(x: any): x is BigNumberish { - return BigNumberishSchema.safeParse(x).success; + return x != null && BigNumberishSchema.safeParse(x).success; } export function isPercentage(x: any): x is Percentage { - return PercentageSchema.safeParse(x).success; + return x != null && PercentageSchema.safeParse(x).success; } export function filterUndefined(