Skip to content

Commit

Permalink
feat: support one-off percentage overrides for user operations
Browse files Browse the repository at this point in the history
  • Loading branch information
denniswon committed Nov 30, 2023
1 parent eac55b4 commit 05b78a3
Show file tree
Hide file tree
Showing 9 changed files with 143 additions and 230 deletions.
57 changes: 30 additions & 27 deletions packages/alchemy/src/middleware/gas-fees.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,48 @@
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";

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;
};
17 changes: 14 additions & 3 deletions packages/alchemy/src/middleware/gas-manager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
deepHexlify,
filterUndefined,
isBigNumberish,
isPercentage,
resolveProperties,
type AccountMiddlewareFn,
Expand Down Expand Up @@ -169,15 +170,25 @@ const withAlchemyGasAndPaymasterAndDataMiddleware = <P extends AlchemyProvider>(
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),
};
}

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export type * from "./utils/index.js";
export {
ChainSchema,
applyFeeOption,
applyUserOperationOverride,
asyncPipe,
bigIntMax,
bigIntPercent,
Expand Down
186 changes: 61 additions & 125 deletions packages/core/src/provider/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
} from "../types.js";
import {
applyFeeOption,
applyUserOperationOverride,
asyncPipe,
bigIntMax,
bigIntPercent,
Expand All @@ -50,10 +51,7 @@ import { createSmartAccountProviderConfigSchema } from "./schema.js";
import type {
AccountMiddlewareFn,
AccountMiddlewareOverrideFn,
FeeDataFeeOptions,
FeeDataMiddleware,
FeeOptionsMiddleware,
GasEstimatorFeeOptions,
GasEstimatorMiddleware,
ISmartAccountProvider,
PaymasterAndDataMiddleware,
Expand Down Expand Up @@ -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,
};

Expand Down Expand Up @@ -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;
};

Expand All @@ -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
//
Expand All @@ -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<UserOperationStruct>(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;
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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);

Expand Down
Loading

0 comments on commit 05b78a3

Please sign in to comment.