From e76c4b3c436852a7ea49fc6c2c3a8f8e7cd07c2c Mon Sep 17 00:00:00 2001 From: Dennis Won Date: Thu, 30 Nov 2023 16:10:32 -0800 Subject: [PATCH] feat: support one-off percentage overrides for user operations (#289) * feat: support one-off percentage overrides for user operations * feat: apply user op override or fee option utils in aa-core --- packages/alchemy/src/middleware/gas-fees.ts | 51 ++++----- .../alchemy/src/middleware/gas-manager.ts | 17 ++- packages/core/src/index.ts | 4 +- packages/core/src/provider/base.ts | 102 ++++++++---------- packages/core/src/provider/types.ts | 1 - packages/core/src/types.ts | 23 ++-- packages/core/src/utils/index.ts | 24 +---- packages/core/src/utils/userop.ts | 58 +++++++++- 8 files changed, 157 insertions(+), 123 deletions(-) diff --git a/packages/alchemy/src/middleware/gas-fees.ts b/packages/alchemy/src/middleware/gas-fees.ts index 316bba5a1d..92992dbd91 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 { applyUserOpOverrideOrFeeOption } from "@alchemy/aa-core"; import type { AlchemyProvider } from "../provider.js"; import type { ClientWithAlchemyMethods } from "./client.js"; @@ -6,34 +6,29 @@ 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 maxFeePerGas = - overrides?.maxFeePerGas != null - ? overrides?.maxFeePerGas - : await estimateMaxFeePerGas(maxPriorityFeePerGas); + const maxPriorityFeePerGas = applyUserOpOverrideOrFeeOption( + maxPriorityFeePerGasEstimate, + overrides?.maxPriorityFeePerGas, + feeOptions?.maxPriorityFeePerGas + ); + const maxFeePerGas = applyUserOpOverrideOrFeeOption( + baseFeePerGas + BigInt(maxPriorityFeePerGas), + overrides?.maxFeePerGas, + feeOptions?.maxFeePerGas + ); return { ...struct, diff --git a/packages/alchemy/src/middleware/gas-manager.ts b/packages/alchemy/src/middleware/gas-manager.ts index 9ac692e613..b1fc6eabb1 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..c6dada652f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -49,7 +49,9 @@ export type * from "./types.js"; export type * from "./utils/index.js"; export { ChainSchema, - applyFeeOption, + applyUserOpFeeOption, + applyUserOpOverride, + applyUserOpOverrideOrFeeOption, asyncPipe, bigIntMax, bigIntPercent, diff --git a/packages/core/src/provider/base.ts b/packages/core/src/provider/base.ts index 4465be42bf..b6117fcfe8 100644 --- a/packages/core/src/provider/base.ts +++ b/packages/core/src/provider/base.ts @@ -32,7 +32,7 @@ import { type UserOperationStruct, } from "../types.js"; import { - applyFeeOption, + applyUserOpOverrideOrFeeOption, asyncPipe, bigIntMax, bigIntPercent, @@ -521,41 +521,31 @@ export class SmartAccountProvider< overrides, feeOptions ) => { - let { callGasLimit, verificationGasLimit, preVerificationGas } = - overrides ?? {}; - - if ( - callGasLimit == null || - verificationGasLimit == null || - preVerificationGas == null - ) { - const request = deepHexlify(await resolveProperties(struct)); - const estimates = await this.rpcClient.estimateUserOperationGas( - request, - this.getEntryPointAddress() - ); + const request = deepHexlify(await resolveProperties(struct)); + const estimates = await this.rpcClient.estimateUserOperationGas( + request, + this.getEntryPointAddress() + ); - callGasLimit = - callGasLimit ?? - applyFeeOption(estimates.callGasLimit, feeOptions?.callGasLimit); - verificationGasLimit = - verificationGasLimit ?? - applyFeeOption( - estimates.verificationGasLimit, - feeOptions?.verificationGasLimit - ); - preVerificationGas = - preVerificationGas ?? - applyFeeOption( - estimates.preVerificationGas, - feeOptions?.preVerificationGas - ); - } + const callGasLimit = applyUserOpOverrideOrFeeOption( + estimates.callGasLimit, + overrides?.callGasLimit, + feeOptions?.callGasLimit + ); + const verificationGasLimit = applyUserOpOverrideOrFeeOption( + estimates.verificationGasLimit, + overrides?.verificationGasLimit, + feeOptions?.verificationGasLimit + ); + const preVerificationGas = applyUserOpOverrideOrFeeOption( + estimates.preVerificationGas, + overrides?.preVerificationGas, + feeOptions?.preVerificationGas + ); struct.callGasLimit = callGasLimit; struct.verificationGasLimit = verificationGasLimit; struct.preVerificationGas = preVerificationGas; - return struct; }; @@ -564,11 +554,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 // @@ -577,30 +562,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 + + const feeData = await this.rpcClient.estimateFeesPerGas(); + if (!feeData.maxFeePerGas || !feeData.maxPriorityFeePerGas) { + throw new Error( + "feeData is missing maxFeePerGas or maxPriorityFeePerGas" ); + } - return BigInt(baseFee) + BigInt(maxPriorityFeePerGas); - }; + let maxPriorityFeePerGas: BigNumberish = + await this.rpcClient.estimateMaxPriorityFeePerGas(); + maxPriorityFeePerGas = applyUserOpOverrideOrFeeOption( + maxPriorityFeePerGas, + overrides?.maxPriorityFeePerGas, + feeOptions?.maxPriorityFeePerGas + ); - struct.maxPriorityFeePerGas = - overrides?.maxPriorityFeePerGas != null - ? overrides?.maxPriorityFeePerGas - : await estimateMaxPriorityFeePerGas(); - struct.maxFeePerGas = - overrides?.maxFeePerGas != null - ? overrides?.maxFeePerGas - : await estimateMaxFeePerGas(struct.maxPriorityFeePerGas); + let maxFeePerGas: BigNumberish = + feeData.maxFeePerGas - + feeData.maxPriorityFeePerGas + + BigInt(maxPriorityFeePerGas); + + maxFeePerGas = applyUserOpOverrideOrFeeOption( + maxFeePerGas, + overrides?.maxFeePerGas, + feeOptions?.maxFeePerGas + ); + struct.maxFeePerGas = maxFeePerGas; + struct.maxPriorityFeePerGas = maxPriorityFeePerGas; return struct; }; diff --git a/packages/core/src/provider/types.ts b/packages/core/src/provider/types.ts index 1efdde77a6..a02f1b608a 100644 --- a/packages/core/src/provider/types.ts +++ b/packages/core/src/provider/types.ts @@ -330,7 +330,6 @@ export interface ISmartAccountProvider< * 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) => this; 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/index.ts b/packages/core/src/utils/index.ts index 89a2ceb1ae..75a835e7b2 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -5,10 +5,8 @@ import type { BigNumberish, Percentage, PromiseOrValue, - UserOperationFeeOptionsField, UserOperationRequest, } from "../types.js"; -import { bigIntClamp, bigIntPercent } from "./bigint.js"; import { BigNumberishSchema, PercentageSchema } from "./schema.js"; /** @@ -97,24 +95,6 @@ export function deepHexlify(obj: any): any { ); } -export function applyFeeOption( - value: BigNumberish | undefined, - feeOption?: UserOperationFeeOptionsField -): BigNumberish { - if (feeOption == null) { - return value ?? 0n; - } - return value - ? bigIntClamp( - feeOption.percentage - ? bigIntPercent(value, BigInt(100 + feeOption.percentage)) - : value, - feeOption.min, - feeOption.max - ) - : feeOption.min ?? 0n; -} - /** * Generates a hash for a UserOperation valid from entrypoint version 0.6 onwards * @@ -183,11 +163,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( diff --git a/packages/core/src/utils/userop.ts b/packages/core/src/utils/userop.ts index 21d3a9255d..34bbe729dc 100644 --- a/packages/core/src/utils/userop.ts +++ b/packages/core/src/utils/userop.ts @@ -1,4 +1,12 @@ -import type { UserOperationRequest, UserOperationStruct } from "../types"; +import type { + BigNumberish, + Percentage, + UserOperationFeeOptionsField, + UserOperationRequest, + UserOperationStruct, +} from "../types"; +import { bigIntClamp, bigIntPercent } from "./bigint.js"; +import { isBigNumberish } from "./index.js"; /** * Utility method for asserting a {@link UserOperationStruct} is a {@link UserOperationRequest} @@ -18,3 +26,51 @@ export function isValidRequest( !!request.verificationGasLimit ); } + +export function applyUserOpOverride( + 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 applyUserOpFeeOption( + value: BigNumberish | undefined, + feeOption?: UserOperationFeeOptionsField +): BigNumberish { + if (feeOption == null) { + return value ?? 0n; + } + return value != null + ? bigIntClamp( + feeOption.percentage + ? bigIntPercent(value, BigInt(100 + feeOption.percentage)) + : value, + feeOption.min, + feeOption.max + ) + : feeOption.min ?? 0n; +} + +export function applyUserOpOverrideOrFeeOption( + value: BigNumberish | undefined, + override?: BigNumberish | Percentage, + feeOption?: UserOperationFeeOptionsField +): BigNumberish { + return value != null && override != null + ? applyUserOpOverride(value, override)! + : applyUserOpFeeOption(value, feeOption); +}