From 21aa58ea795119c2e17ac31dd338f6b79304b61b Mon Sep 17 00:00:00 2001 From: Dennis Won Date: Tue, 28 Nov 2023 13:01:39 -0800 Subject: [PATCH 1/2] feat: support one-off percentage overrides for user operations --- packages/alchemy/src/middleware/gas-fees.ts | 55 ++++----- .../alchemy/src/middleware/gas-manager.ts | 17 ++- packages/core/src/index.ts | 1 + packages/core/src/provider/base.ts | 110 +++++++++--------- packages/core/src/provider/types.ts | 1 - packages/core/src/types.ts | 23 ++-- packages/core/src/utils/index.ts | 25 +++- 7 files changed, 132 insertions(+), 100 deletions(-) diff --git a/packages/alchemy/src/middleware/gas-fees.ts b/packages/alchemy/src/middleware/gas-fees.ts index 316bba5a1..1ecb4994c 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,34 +6,37 @@ 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, diff --git a/packages/alchemy/src/middleware/gas-manager.ts b/packages/alchemy/src/middleware/gas-manager.ts index 9ac692e61..b1fc6eabb 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 a43fd7796..c1453bc03 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 4465be42b..8dfa4415e 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, @@ -521,41 +522,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; }; @@ -564,11 +563,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 +571,34 @@ 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); + const feeData = await this.rpcClient.estimateFeesPerGas(); + if (!feeData.maxFeePerGas || !feeData.maxPriorityFeePerGas) { + throw new Error( + "feeData is missing maxFeePerGas or maxPriorityFeePerGas" + ); + } + 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; }; diff --git a/packages/core/src/provider/types.ts b/packages/core/src/provider/types.ts index 1efdde77a..a02f1b608 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 d3a889aa9..c6af246ad 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 89a2ceb1a..11d85fb22 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( From d9ce6070998cceba4a7ea7a630b59b2ccb0b24d4 Mon Sep 17 00:00:00 2001 From: Dennis Won Date: Thu, 30 Nov 2023 15:41:55 -0800 Subject: [PATCH 2/2] feat: apply user op override or fee option utils in aa-core --- packages/alchemy/src/middleware/gas-fees.ts | 30 ++++------- packages/core/src/index.ts | 5 +- packages/core/src/provider/base.ts | 60 +++++++++------------ packages/core/src/utils/index.ts | 39 -------------- packages/core/src/utils/userop.ts | 58 +++++++++++++++++++- 5 files changed, 97 insertions(+), 95 deletions(-) diff --git a/packages/alchemy/src/middleware/gas-fees.ts b/packages/alchemy/src/middleware/gas-fees.ts index 1ecb4994c..92992dbd9 100644 --- a/packages/alchemy/src/middleware/gas-fees.ts +++ b/packages/alchemy/src/middleware/gas-fees.ts @@ -1,4 +1,4 @@ -import { applyFeeOption, applyUserOperationOverride } from "@alchemy/aa-core"; +import { applyUserOpOverrideOrFeeOption } from "@alchemy/aa-core"; import type { AlchemyProvider } from "../provider.js"; import type { ClientWithAlchemyMethods } from "./client.js"; @@ -19,24 +19,16 @@ export const withAlchemyGasFeeEstimator = ( throw new Error("baseFeePerGas is null"); } - const maxPriorityFeePerGas = - applyUserOperationOverride( - maxPriorityFeePerGasEstimate, - overrides?.maxPriorityFeePerGas - ) || - applyFeeOption( - maxPriorityFeePerGasEstimate, - feeOptions?.maxPriorityFeePerGas - ); - const maxFeePerGas = - applyUserOperationOverride( - baseFeePerGas + BigInt(maxPriorityFeePerGas), - overrides?.maxPriorityFeePerGas - ) || - applyFeeOption( - baseFeePerGas + BigInt(maxPriorityFeePerGas), - feeOptions?.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/core/src/index.ts b/packages/core/src/index.ts index c1453bc03..c6dada652 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -49,8 +49,9 @@ export type * from "./types.js"; export type * from "./utils/index.js"; export { ChainSchema, - applyFeeOption, - applyUserOperationOverride, + applyUserOpFeeOption, + applyUserOpOverride, + applyUserOpOverrideOrFeeOption, asyncPipe, bigIntMax, bigIntPercent, diff --git a/packages/core/src/provider/base.ts b/packages/core/src/provider/base.ts index 8dfa4415e..b6117fcfe 100644 --- a/packages/core/src/provider/base.ts +++ b/packages/core/src/provider/base.ts @@ -32,8 +32,7 @@ import { type UserOperationStruct, } from "../types.js"; import { - applyFeeOption, - applyUserOperationOverride, + applyUserOpOverrideOrFeeOption, asyncPipe, bigIntMax, bigIntPercent, @@ -528,29 +527,21 @@ export class SmartAccountProvider< 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 - ); + 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; @@ -581,21 +572,22 @@ export class SmartAccountProvider< let maxPriorityFeePerGas: BigNumberish = await this.rpcClient.estimateMaxPriorityFeePerGas(); - maxPriorityFeePerGas = - applyUserOperationOverride( - maxPriorityFeePerGas, - overrides?.maxPriorityFeePerGas - ) || - applyFeeOption(maxPriorityFeePerGas, feeOptions?.maxPriorityFeePerGas); + maxPriorityFeePerGas = applyUserOpOverrideOrFeeOption( + maxPriorityFeePerGas, + overrides?.maxPriorityFeePerGas, + feeOptions?.maxPriorityFeePerGas + ); let maxFeePerGas: BigNumberish = feeData.maxFeePerGas - feeData.maxPriorityFeePerGas + BigInt(maxPriorityFeePerGas); - maxFeePerGas = - applyUserOperationOverride(maxFeePerGas, overrides?.maxFeePerGas) || - applyFeeOption(maxFeePerGas, feeOptions?.maxFeePerGas); + maxFeePerGas = applyUserOpOverrideOrFeeOption( + maxFeePerGas, + overrides?.maxFeePerGas, + feeOptions?.maxFeePerGas + ); struct.maxFeePerGas = maxFeePerGas; struct.maxPriorityFeePerGas = maxPriorityFeePerGas; diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 11d85fb22..75a835e7b 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,43 +95,6 @@ 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 -): 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; -} - /** * Generates a hash for a UserOperation valid from entrypoint version 0.6 onwards * diff --git a/packages/core/src/utils/userop.ts b/packages/core/src/utils/userop.ts index 21d3a9255..34bbe729d 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); +}