diff --git a/packages/alchemy/src/middleware/gas-fees.ts b/packages/alchemy/src/middleware/gas-fees.ts
index 9047d832cc..f8d8d921d8 100644
--- a/packages/alchemy/src/middleware/gas-fees.ts
+++ b/packages/alchemy/src/middleware/gas-fees.ts
@@ -29,6 +29,6 @@ export const withAlchemyGasFeeEstimator =
(
maxFeePerGas: baseFeeIncrease + prioFeeIncrease,
maxPriorityFeePerGas: prioFeeIncrease,
};
- });
+ }, provider.feeOptions);
return provider;
};
diff --git a/packages/alchemy/src/middleware/gas-manager.ts b/packages/alchemy/src/middleware/gas-manager.ts
index 413df2f5a5..c73d90fb15 100644
--- a/packages/alchemy/src/middleware/gas-manager.ts
+++ b/packages/alchemy/src/middleware/gas-manager.ts
@@ -13,7 +13,7 @@ export interface AlchemyGasManagerConfig {
/**
* This middleware wraps the Alchemy Gas Manager APIs to provide more flexible UserOperation gas sponsorship.
*
- * If `estimateGas` is true, it will use `alchemy_requestGasAndPaymasterAndData` to get all of the gas estimates + paymaster data
+ * If `delegateGasEstimation` is true, it will use `alchemy_requestGasAndPaymasterAndData` to get all of the gas estimates + paymaster data
* in one RPC call.
*
* Otherwise, it will use `alchemy_requestPaymasterAndData` to get only paymaster data, allowing you
@@ -21,27 +21,57 @@ export interface AlchemyGasManagerConfig {
*
* @param provider - the smart account provider to override to use the alchemy gas manager
* @param config - the alchemy gas manager configuration
- * @param estimateGas - if true, this will use `alchemy_requestGasAndPaymasterAndData` else will use `alchemy_requestPaymasterAndData`
+ * @param delegateGasEstimation - if true, this will use `alchemy_requestGasAndPaymasterAndData` else will use `alchemy_requestPaymasterAndData`
* @returns the provider augmented to use the alchemy gas manager
*/
export const withAlchemyGasManager =
(
provider: P,
config: AlchemyGasManagerConfig,
- estimateGas: boolean = true
+ delegateGasEstimation: boolean = true
): P => {
- return estimateGas
+ const fallbackGasEstimator = provider.gasEstimator;
+ const fallbackFeeDataGetter = provider.feeDataGetter;
+
+ return delegateGasEstimation
? provider
// no-op gas estimator
- .withGasEstimator(async () => ({
- callGasLimit: 0n,
- preVerificationGas: 0n,
- verificationGasLimit: 0n,
- }))
+ .withGasEstimator(async (struct, overrides) => {
+ // but if user is bypassing paymaster to fallback to having the account to pay the gas (one-off override),
+ // we cannot delegate gas estimation to the bundler because paymaster middleware will not be called
+ if (overrides?.paymasterAndData !== undefined) {
+ const result = await fallbackGasEstimator(struct, overrides);
+ return {
+ callGasLimit: (await result.callGasLimit) ?? 0n,
+ preVerificationGas: (await result.preVerificationGas) ?? 0n,
+ verificationGasLimit: (await result.verificationGasLimit) ?? 0n,
+ };
+ } else {
+ return {
+ callGasLimit: 0n,
+ preVerificationGas: 0n,
+ verificationGasLimit: 0n,
+ };
+ }
+ })
// no-op fee because the alchemy api will do it
- .withFeeDataGetter(async (struct) => ({
- maxFeePerGas: (await struct.maxFeePerGas) ?? 0n,
- maxPriorityFeePerGas: (await struct.maxPriorityFeePerGas) ?? 0n,
- }))
+ .withFeeDataGetter(async (struct, overrides) => {
+ let maxFeePerGas = (await struct.maxFeePerGas) ?? 0n;
+ let maxPriorityFeePerGas = (await struct.maxPriorityFeePerGas) ?? 0n;
+
+ // but if user is bypassing paymaster to fallback to having the account to pay the gas (one-off override),
+ // we cannot delegate gas estimation to the bundler because paymaster middleware will not be called
+ if (overrides?.paymasterAndData !== undefined) {
+ const result = await fallbackFeeDataGetter(struct, overrides);
+ maxFeePerGas = (await result.maxFeePerGas) ?? maxFeePerGas;
+ maxPriorityFeePerGas =
+ (await result.maxPriorityFeePerGas) ?? maxPriorityFeePerGas;
+ }
+
+ return {
+ maxFeePerGas,
+ maxPriorityFeePerGas,
+ };
+ })
.withPaymasterMiddleware(
withAlchemyGasAndPaymasterAndDataMiddleware(provider, config)
)
diff --git a/packages/core/e2e-tests/simple-account.test.ts b/packages/core/e2e-tests/simple-account.test.ts
index 302d521599..2e38894846 100644
--- a/packages/core/e2e-tests/simple-account.test.ts
+++ b/packages/core/e2e-tests/simple-account.test.ts
@@ -1,10 +1,18 @@
-import { isAddress, type Address, type Chain, type Hash } from "viem";
+import {
+ fromHex,
+ isAddress,
+ type Address,
+ type Chain,
+ type Hash,
+ type Hex,
+} from "viem";
import { generatePrivateKey } from "viem/accounts";
import { polygonMumbai } from "viem/chains";
import { SimpleSmartContractAccount } from "../src/account/simple.js";
import {
getDefaultSimpleAccountFactoryAddress,
type SmartAccountSigner,
+ type UserOperationFeeOptions,
} from "../src/index.js";
import { SmartAccountProvider } from "../src/provider/base.js";
import { LocalAccountSigner } from "../src/signer/local-account.js";
@@ -58,20 +66,86 @@ describe("Simple Account Tests", () => {
await expect(address).resolves.not.toThrowError();
expect(isAddress(await address)).toBe(true);
});
+
+ it("should correctly handle provider feeOptions set during init", async () => {
+ const signer = givenConnectedProvider({
+ owner,
+ chain,
+ });
+
+ const structPromise = signer.buildUserOperation({
+ target: await signer.getAddress(),
+ data: "0x",
+ });
+ await expect(structPromise).resolves.not.toThrowError();
+
+ const signerWithFeeOptions = givenConnectedProvider({
+ owner,
+ chain,
+ feeOptions: {
+ preVerificationGas: { percentage: 100 },
+ },
+ });
+
+ const structWithFeeOptionsPromise = signerWithFeeOptions.buildUserOperation(
+ {
+ target: await signer.getAddress(),
+ data: "0x",
+ }
+ );
+ await expect(structWithFeeOptionsPromise).resolves.not.toThrowError();
+
+ const [struct, structWithFeeOptions] = await Promise.all([
+ structPromise,
+ structWithFeeOptionsPromise,
+ ]);
+
+ const preVerificationGas =
+ typeof struct.preVerificationGas === "string"
+ ? fromHex(struct.preVerificationGas as Hex, "bigint")
+ : struct.preVerificationGas;
+ const preVerificationGasWithFeeOptions =
+ typeof structWithFeeOptions.preVerificationGas === "string"
+ ? fromHex(structWithFeeOptions.preVerificationGas as Hex, "bigint")
+ : structWithFeeOptions.preVerificationGas;
+
+ expect(preVerificationGasWithFeeOptions).toBeGreaterThan(
+ preVerificationGas!
+ );
+ }, 60000);
+
+ it("should correctly handle percentage overrides for sendUserOperation", async () => {
+ const signer = givenConnectedProvider({
+ owner,
+ chain,
+ feeOptions: {
+ preVerificationGas: { percentage: 100 },
+ },
+ });
+
+ const struct = signer.sendUserOperation({
+ target: await signer.getAddress(),
+ data: "0x",
+ });
+ await expect(struct).resolves.not.toThrowError();
+ }, 60000);
});
const givenConnectedProvider = ({
owner,
chain,
accountAddress,
+ feeOptions,
}: {
owner: SmartAccountSigner;
chain: Chain;
accountAddress?: Address;
+ feeOptions?: UserOperationFeeOptions;
}) => {
const provider = new SmartAccountProvider({
rpcProvider: `${chain.rpcUrls.alchemy.http[0]}/${API_KEY}`,
chain,
+ opts: { feeOptions },
});
const feeDataGetter = async () => ({
maxFeePerGas: 100_000_000_000n,
diff --git a/packages/core/src/account/base.ts b/packages/core/src/account/base.ts
index b95cedc38a..aacc58d471 100644
--- a/packages/core/src/account/base.ts
+++ b/packages/core/src/account/base.ts
@@ -256,12 +256,12 @@ export abstract class BaseSmartContractAccount<
try {
await this.entryPoint.simulate.getSenderAddress([initCode]);
} catch (err: any) {
- Logger.debug(
- "[BaseSmartContractAccount](getAddress) entrypoint.getSenderAddress result: ",
- err
- );
if (err.cause?.data?.errorName === "SenderAddressResult") {
this.accountAddress = err.cause.data.args[0] as Address;
+ Logger.debug(
+ "[BaseSmartContractAccount](getAddress) entrypoint.getSenderAddress result:",
+ this.accountAddress
+ );
return this.accountAddress;
}
}
diff --git a/packages/core/src/provider/base.ts b/packages/core/src/provider/base.ts
index aa8b11abf5..2e7939224d 100644
--- a/packages/core/src/provider/base.ts
+++ b/packages/core/src/provider/base.ts
@@ -10,7 +10,6 @@ import {
type Transaction,
type Transport,
} from "viem";
-import { arbitrum, arbitrumGoerli, arbitrumSepolia } from "viem/chains";
import type {
ISmartContractAccount,
SignTypedDataParams,
@@ -20,9 +19,12 @@ import type {
PublicErc4337Client,
SupportedTransports,
} from "../client/types.js";
+import { Logger } from "../logger.js";
import {
type BatchUserOperationCallData,
+ type BigNumberish,
type UserOperationCallData,
+ type UserOperationFeeOptions,
type UserOperationOverrides,
type UserOperationReceipt,
type UserOperationRequest,
@@ -30,12 +32,15 @@ import {
type UserOperationStruct,
} from "../types.js";
import {
+ applyFeeOption,
asyncPipe,
bigIntMax,
bigIntPercent,
deepHexlify,
defineReadOnly,
+ filterUndefined,
getDefaultEntryPointAddress,
+ getDefaultUserOperationFeeOptions,
getUserOperationHash,
isValidRequest,
resolveProperties,
@@ -45,7 +50,10 @@ import { createSmartAccountProviderConfigSchema } from "./schema.js";
import type {
AccountMiddlewareFn,
AccountMiddlewareOverrideFn,
+ FeeDataFeeOptions,
FeeDataMiddleware,
+ FeeOptionsMiddleware,
+ GasEstimatorFeeOptions,
GasEstimatorMiddleware,
ISmartAccountProvider,
PaymasterAndDataMiddleware,
@@ -55,15 +63,10 @@ import type {
} from "./types.js";
export const noOpMiddleware: AccountMiddlewareFn = async (
- struct: Deferrable
+ struct: Deferrable,
+ _overrides?: UserOperationOverrides
) => struct;
-const minPriorityFeePerBidDefaults = new Map([
- [arbitrum.id, 10_000_000n],
- [arbitrumGoerli.id, 10_000_000n],
- [arbitrumSepolia.id, 10_000_000n],
-]);
-
export class SmartAccountProvider<
TTransport extends SupportedTransports = Transport
>
@@ -74,10 +77,8 @@ export class SmartAccountProvider<
private txRetryIntervalMs: number;
private txRetryMulitplier: number;
- private minPriorityFeePerBid: bigint;
- private maxPriorityFeePerGasEstimateBuffer: number;
-
readonly account?: ISmartContractAccount;
+ readonly feeOptions: UserOperationFeeOptions;
protected entryPointAddress?: Address;
protected chain: Chain;
@@ -100,13 +101,8 @@ export class SmartAccountProvider<
this.txRetryMulitplier = opts?.txRetryMulitplier ?? 1.5;
this.entryPointAddress = entryPointAddress;
- this.minPriorityFeePerBid =
- opts?.minPriorityFeePerBid ??
- minPriorityFeePerBidDefaults.get(chain.id) ??
- 100_000_000n;
-
- this.maxPriorityFeePerGasEstimateBuffer =
- opts?.maxPriorityFeePerGasEstimateBuffer ?? 33;
+ this.feeOptions =
+ opts?.feeOptions ?? getDefaultUserOperationFeeOptions(chain);
this.rpcClient =
typeof rpcProvider === "string"
@@ -221,15 +217,19 @@ export class SmartAccountProvider<
throw new Error("transaction is missing to address");
}
- const _overrides: UserOperationOverrides = {};
- if (overrides?.maxFeePerGas || request.maxFeePerGas) {
- _overrides.maxFeePerGas = overrides?.maxFeePerGas ?? request.maxFeePerGas;
- }
- if (overrides?.maxPriorityFeePerGas || request.maxPriorityFeePerGas) {
- _overrides.maxPriorityFeePerGas =
- overrides?.maxPriorityFeePerGas ?? request.maxPriorityFeePerGas;
- }
- _overrides.paymasterAndData = overrides?.paymasterAndData;
+ const _overrides: UserOperationOverrides = {
+ maxFeePerGas:
+ overrides?.maxFeePerGas ??
+ (request.maxFeePerGas
+ ? fromHex(request.maxFeePerGas, "bigint")
+ : undefined),
+ maxPriorityFeePerGas:
+ overrides?.maxPriorityFeePerGas ??
+ (request.maxPriorityFeePerGas
+ ? fromHex(request.maxPriorityFeePerGas, "bigint")
+ : undefined),
+ };
+ filterUndefined(_overrides);
return this.buildUserOperation(
{
@@ -259,26 +259,26 @@ export class SmartAccountProvider<
};
});
- const maxFeePerGas = bigIntMax(
- ...requests
- .filter((x) => x.maxFeePerGas != null)
- .map((x) => fromHex(x.maxFeePerGas!, "bigint"))
- );
-
- const maxPriorityFeePerGas = bigIntMax(
- ...requests
- .filter((x) => x.maxPriorityFeePerGas != null)
- .map((x) => fromHex(x.maxPriorityFeePerGas!, "bigint"))
- );
- const _overrides: UserOperationOverrides = {};
- if (overrides?.maxFeePerGas || maxFeePerGas != null) {
- _overrides.maxFeePerGas = overrides?.maxFeePerGas ?? maxFeePerGas;
- }
+ const maxFeePerGas =
+ overrides?.maxFeePerGas ??
+ bigIntMax(
+ ...requests
+ .filter((x) => x.maxFeePerGas != null)
+ .map((x) => fromHex(x.maxFeePerGas!, "bigint"))
+ );
+ const maxPriorityFeePerGas =
+ overrides?.maxPriorityFeePerGas ??
+ bigIntMax(
+ ...requests
+ .filter((x) => x.maxPriorityFeePerGas != null)
+ .map((x) => fromHex(x.maxPriorityFeePerGas!, "bigint"))
+ );
- if (overrides?.maxPriorityFeePerGas || maxPriorityFeePerGas != null) {
- _overrides.maxPriorityFeePerGas =
- overrides?.maxPriorityFeePerGas ?? maxPriorityFeePerGas;
- }
+ const _overrides: UserOperationOverrides = {
+ maxFeePerGas,
+ maxPriorityFeePerGas,
+ };
+ filterUndefined(_overrides);
return {
batch,
@@ -309,9 +309,13 @@ export class SmartAccountProvider<
await new Promise((resolve) =>
setTimeout(resolve, txRetryIntervalWithJitterMs)
);
- const receipt = await this.getUserOperationReceipt(hash as `0x${string}`)
- // TODO: should maybe log the error?
- .catch(() => null);
+ const receipt = await this.getUserOperationReceipt(
+ hash as `0x${string}`
+ ).catch((e) => {
+ Logger.debug(
+ `[SmartAccountProvider] waitForUserOperationTransaction error fetching receipt for ${hash}: ${e}`
+ );
+ });
if (receipt) {
return this.getTransaction(receipt.receipt.transactionHash).then(
(x) => x.hash
@@ -405,10 +409,10 @@ export class SmartAccountProvider<
BigInt(maxPriorityFeePerGas ?? 0n),
bigIntPercent(uoToDrop.maxPriorityFeePerGas, 110n)
),
- paymasterAndData: uoToDrop.paymasterAndData,
};
const uoToSend = await this._runMiddlewareStack(uoToSubmit, _overrides);
+
return this._sendUserOperation(uoToSend);
};
@@ -433,14 +437,13 @@ export class SmartAccountProvider<
this.dummyPaymasterDataMiddleware,
this.feeDataGetter,
this.gasEstimator,
- // run this before paymaster middleware
- async (struct) => ({ ...struct, ...overrides }),
- this.customMiddleware,
- overrides?.paymasterAndData
- ? noOpMiddleware
+ this.customMiddleware ?? noOpMiddleware,
+ this.feeOptionsMiddleware,
+ overrides?.paymasterAndData != null
+ ? this.overridePaymasterDataMiddleware
: this.paymasterDataMiddleware,
this.simulateUOMiddleware
- )(uo);
+ )(uo, overrides);
return resolveProperties(result);
};
@@ -483,18 +486,30 @@ export class SmartAccountProvider<
// You should implement your own middleware to override these
// or extend this class and provider your own implemenation
readonly dummyPaymasterDataMiddleware: AccountMiddlewareFn = async (
- struct
+ struct,
+ _overrides
) => {
struct.paymasterAndData = "0x";
return struct;
};
- readonly paymasterDataMiddleware: AccountMiddlewareFn = async (struct) => {
+ readonly overridePaymasterDataMiddleware: AccountMiddlewareFn = async (
+ struct,
+ overrides
+ ) => {
+ struct.paymasterAndData = overrides?.paymasterAndData ?? "0x";
+ return struct;
+ };
+
+ readonly paymasterDataMiddleware: AccountMiddlewareFn = async (
+ struct,
+ _overrides
+ ) => {
struct.paymasterAndData = "0x";
return struct;
};
- readonly gasEstimator: AccountMiddlewareFn = async (struct) => {
+ readonly gasEstimator: AccountMiddlewareFn = async (struct, _overrides) => {
const request = deepHexlify(await resolveProperties(struct));
const estimates = await this.rpcClient.estimateUserOperationGas(
request,
@@ -508,36 +523,66 @@ export class SmartAccountProvider<
return struct;
};
- readonly feeDataGetter: AccountMiddlewareFn = async (struct) => {
- const [maxPriorityFeePerGas, feeData] = await Promise.all([
- this.rpcClient.estimateMaxPriorityFeePerGas(),
- this.rpcClient.estimateFeesPerGas(),
- ]);
- if (!feeData.maxFeePerGas || !feeData.maxPriorityFeePerGas) {
- throw new Error(
- "feeData is missing maxFeePerGas or maxPriorityFeePerGas"
+ readonly feeDataGetter: AccountMiddlewareFn = async (struct, overrides) => {
+ // 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
+ //
+ // Note that if maxFeePerGas is not at least the sum of maxPriorityFeePerGas and required baseFee
+ // after applying the fee options, then the transaction will fail
+ //
+ // 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"
+ );
+ }
+
+ return (
+ BigInt(feeData.maxFeePerGas) -
+ BigInt(feeData.maxPriorityFeePerGas) +
+ BigInt(maxPriorityFeePerGas)
);
- }
+ };
- // set maxPriorityFeePerGasBid to the max between 33% added priority fee estimate and
- // the min priority fee per gas set for the provider
- const maxPriorityFeePerGasBid = bigIntMax(
- bigIntPercent(
- maxPriorityFeePerGas,
- BigInt(100 + this.maxPriorityFeePerGasEstimateBuffer)
- ),
- this.minPriorityFeePerBid
- );
+ struct.maxPriorityFeePerGas =
+ overrides?.maxPriorityFeePerGas ??
+ (await this.rpcClient.estimateMaxPriorityFeePerGas());
+ struct.maxFeePerGas =
+ overrides?.maxFeePerGas ??
+ (await estimateMaxFeePerGas(struct.maxPriorityFeePerGas));
+
+ return struct;
+ };
- const maxFeePerGasBid =
- BigInt(feeData.maxFeePerGas) -
- BigInt(feeData.maxPriorityFeePerGas) +
- maxPriorityFeePerGasBid;
+ 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]
+ );
+ });
- struct.maxFeePerGas = maxFeePerGasBid;
- struct.maxPriorityFeePerGas = maxPriorityFeePerGasBid;
+ resolved.maxFeePerGas =
+ BigInt(resolved.maxFeePerGas ?? 0n) +
+ BigInt(resolved.maxPriorityFeePerGas ?? 0n);
- return struct;
+ return resolved;
};
readonly customMiddleware: AccountMiddlewareFn = noOpMiddleware;
@@ -561,7 +606,16 @@ export class SmartAccountProvider<
return this;
};
- withGasEstimator = (override: GasEstimatorMiddleware): 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;
+
defineReadOnly(
this,
"gasEstimator",
@@ -570,7 +624,15 @@ export class SmartAccountProvider<
return this;
};
- withFeeDataGetter = (override: FeeDataMiddleware): 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;
+
defineReadOnly(
this,
"feeDataGetter",
@@ -579,6 +641,15 @@ 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);
@@ -688,10 +759,10 @@ export class SmartAccountProvider<
private overrideMiddlewareFunction = (
override: AccountMiddlewareOverrideFn
): AccountMiddlewareFn => {
- return async (struct) => {
+ return async (struct, overrides) => {
return {
...struct,
- ...(await override(struct)),
+ ...(await override(struct, overrides)),
};
};
};
diff --git a/packages/core/src/provider/schema.ts b/packages/core/src/provider/schema.ts
index 5dfa7f7ec0..e96cae28cf 100644
--- a/packages/core/src/provider/schema.ts
+++ b/packages/core/src/provider/schema.ts
@@ -3,35 +3,32 @@ import type { Transport } from "viem";
import z from "zod";
import { createPublicErc4337ClientSchema } from "../client/schema.js";
import type { SupportedTransports } from "../client/types";
+import { UserOperationFeeOptionsSchema } from "../schema.js";
import { ChainSchema } from "../utils/index.js";
-export const SmartAccountProviderOptsSchema = z.object({
- /**
- * The maximum number of times to try fetching a transaction receipt before giving up (default: 5)
- */
- txMaxRetries: z.number().min(0).optional(),
-
- /**
- * The interval in milliseconds to wait between retries while waiting for tx receipts (default: 2_000)
- */
- txRetryIntervalMs: z.number().min(0).optional(),
+export const SmartAccountProviderOptsSchema = z
+ .object({
+ /**
+ * The maximum number of times to try fetching a transaction receipt before giving up (default: 5)
+ */
+ txMaxRetries: z.number().min(0).optional(),
- /**
- * The mulitplier on interval length to wait between retries while waiting for tx receipts (default: 1.5)
- */
- txRetryMulitplier: z.number().min(0).optional(),
+ /**
+ * The interval in milliseconds to wait between retries while waiting for tx receipts (default: 2_000)
+ */
+ txRetryIntervalMs: z.number().min(0).optional(),
- /**
- * Used when computing the fees for a user operation (default: 100_000_000n)
- */
- minPriorityFeePerBid: z.bigint().min(0n).optional(),
+ /**
+ * The mulitplier on interval length to wait between retries while waiting for tx receipts (default: 1.5)
+ */
+ txRetryMulitplier: z.number().min(0).optional(),
- /**
- * Percent value for maxPriorityFeePerGas estimate added buffer. maxPriorityFeePerGasBid is set to the max
- * between the buffer "added" priority fee estimate and the minPriorityFeePerBid (default: 33)
- */
- maxPriorityFeePerGasEstimateBuffer: z.number().min(0).optional(),
-});
+ /**
+ * Optional user operation fee options to be set globally at the provider level
+ */
+ feeOptions: UserOperationFeeOptionsSchema.optional(),
+ })
+ .strict();
export const createSmartAccountProviderConfigSchema = <
TTransport extends SupportedTransports = Transport
diff --git a/packages/core/src/provider/types.ts b/packages/core/src/provider/types.ts
index 1ed87c1f41..05cde55e15 100644
--- a/packages/core/src/provider/types.ts
+++ b/packages/core/src/provider/types.ts
@@ -19,6 +19,7 @@ import type {
import type {
BatchUserOperationCallData,
UserOperationCallData,
+ UserOperationFeeOptions,
UserOperationOverrides,
UserOperationReceipt,
UserOperationRequest,
@@ -53,14 +54,16 @@ export type SendUserOperationResult = {
};
export type AccountMiddlewareFn = (
- struct: Deferrable
+ struct: Deferrable,
+ overrides?: UserOperationOverrides
) => Promise>;
export type AccountMiddlewareOverrideFn<
Req extends keyof UserOperationStruct = never,
Opt extends keyof UserOperationStruct = never
> = (
- struct: Deferrable
+ struct: Deferrable,
+ overrides?: UserOperationOverrides
) => Promise<
WithRequired &
WithOptional
@@ -78,9 +81,28 @@ 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
@@ -103,9 +125,11 @@ 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.
@@ -316,19 +340,44 @@ 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) => this;
+ withGasEstimator: (
+ override: GasEstimatorMiddleware,
+ feeOptions?: GasEstimatorFeeOptions
+ ) => 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
+ * Note that when using your custom fee data getter with this override method, not only the default feeDataGetter middleware,
+ * but also the fee data getter fee options set during initialization is overriden.
+ * Thus, when using your custom fee data getter, you need to set the fee options from this method instead.
+ *
+ * @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
*/
- withFeeDataGetter: (override: FeeDataMiddleware) => this;
+ withFeeOptionsMiddleware: (override: FeeOptionsMiddleware) => 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
new file mode 100644
index 0000000000..c452931779
--- /dev/null
+++ b/packages/core/src/schema.ts
@@ -0,0 +1,16 @@
+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 8babbe411a..0db876be0d 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -1,12 +1,26 @@
import type { Address, Hash } from "viem";
+import type { z } from "zod";
+import type {
+ UserOperationFeeOptionsFieldSchema,
+ UserOperationFeeOptionsSchema,
+} from "./schema";
+import type {
+ BigNumberishRangeSchema,
+ BigNumberishSchema,
+ HexSchema,
+ PercentageSchema,
+} from "./utils";
-export type Hex = `0x${string}`;
+export type Hex = z.infer;
export type EmptyHex = `0x`;
// based on @account-abstraction/common
export type PromiseOrValue = T | Promise;
-export type BigNumberish = string | bigint | number;
export type BytesLike = Uint8Array | string;
+export type Percentage = z.infer;
+
+export type BigNumberish = z.infer;
+export type BigNumberishRange = z.infer;
export type UserOperationCallData =
| {
@@ -21,6 +35,14 @@ export type UserOperationCallData =
export type BatchUserOperationCallData = Exclude[];
+export type UserOperationFeeOptionsField = z.infer<
+ typeof UserOperationFeeOptionsFieldSchema
+>;
+
+export type UserOperationFeeOptions = z.infer<
+ typeof UserOperationFeeOptionsSchema
+>;
+
export type UserOperationOverrides = Partial<
Pick<
UserOperationStruct,
diff --git a/packages/core/src/utils/bigint.ts b/packages/core/src/utils/bigint.ts
index 7aecaae565..b07252073a 100644
--- a/packages/core/src/utils/bigint.ts
+++ b/packages/core/src/utils/bigint.ts
@@ -8,12 +8,58 @@ import type { BigNumberish } from "../types";
*/
export const bigIntMax = (...args: bigint[]) => {
if (!args.length) {
- throw new Error("bigIntMax requires at least one argument");
+ return undefined;
}
return args.reduce((m, c) => (m > c ? m : c));
};
+/**
+ * Returns the min bigint in a list of bigints
+ *
+ * @param args a list of bigints to get the max of
+ * @returns the min bigint in the list
+ */
+export const bigIntMin = (...args: bigint[]) => {
+ if (!args.length) {
+ return undefined;
+ }
+
+ return args.reduce((m, c) => (m < c ? m : c));
+};
+
+/**
+ * Given a bigint and a min-max range, returns the min-max clamped bigint value
+ *
+ * @param value a bigint value to clamp
+ * @param lower lower bound min max tuple value
+ * @param upper upper bound min max tuple value
+ * @returns the clamped bigint value per given range
+ */
+export const bigIntClamp = (
+ value: BigNumberish,
+ lower?: BigNumberish,
+ upper?: BigNumberish
+) => {
+ lower = lower ? BigInt(lower) : undefined;
+ upper = upper ? BigInt(upper) : undefined;
+
+ if (upper !== undefined && lower !== undefined && upper < lower) {
+ throw new Error(
+ `invalid range: upper bound ${upper} is less than lower bound ${lower}`
+ );
+ }
+
+ let ret = BigInt(value);
+ if (lower !== undefined && lower > ret) {
+ ret = lower;
+ }
+ if (upper !== undefined && upper < ret) {
+ ret = upper;
+ }
+ return ret;
+};
+
/**
* Useful if you want to increment a bigint by N% or decrement by N%
*
diff --git a/packages/core/src/utils/defaults.ts b/packages/core/src/utils/defaults.ts
index 8b490f0e2e..349697a829 100644
--- a/packages/core/src/utils/defaults.ts
+++ b/packages/core/src/utils/defaults.ts
@@ -15,6 +15,7 @@ import {
polygonMumbai,
sepolia,
} from "viem/chains";
+import type { UserOperationFeeOptions } from "../types";
/**
* Utility method returning the entry point contrafct address given a {@link Chain} object
@@ -79,3 +80,20 @@ export const getDefaultSimpleAccountFactoryAddress = (
`no default simple account factory contract exists for ${chain.name}`
);
};
+
+export const minPriorityFeePerBidDefaults = new Map([
+ [arbitrum.id, 10_000_000n],
+ [arbitrumGoerli.id, 10_000_000n],
+ [arbitrumSepolia.id, 10_000_000n],
+]);
+
+export const getDefaultUserOperationFeeOptions = (
+ chain: Chain
+): UserOperationFeeOptions => {
+ return {
+ maxPriorityFeePerGas: {
+ min: minPriorityFeePerBidDefaults.get(chain.id) ?? 100_000_000n,
+ percentage: 33,
+ },
+ };
+};
diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts
index 3b6c49269e..271223cf15 100644
--- a/packages/core/src/utils/index.ts
+++ b/packages/core/src/utils/index.ts
@@ -1,7 +1,15 @@
import type { Address, Hash, Hex } from "viem";
import { encodeAbiParameters, hexToBigInt, keccak256, toHex } from "viem";
import * as chains from "viem/chains";
-import type { PromiseOrValue, UserOperationRequest } from "../types.js";
+import type {
+ BigNumberish,
+ Percentage,
+ PromiseOrValue,
+ UserOperationFeeOptionsField,
+ UserOperationRequest,
+} from "../types.js";
+import { bigIntClamp, bigIntPercent } from "./bigint.js";
+import { BigNumberishSchema, PercentageSchema } from "./schema.js";
/**
* Utility method for converting a chainId to a {@link chains.Chain} object
@@ -26,11 +34,11 @@ export const getChain = (chainId: number): chains.Chain => {
* @returns result of the pipe
*/
export const asyncPipe =
- (...fns: ((x: T) => Promise)[]) =>
- async (x: T) => {
+ (...fns: ((x: T, o?: O) => Promise)[]) =>
+ async (x: T, o?: O) => {
let result = x;
for (const fn of fns) {
- result = await fn(result);
+ result = await fn(result, o);
}
return result;
};
@@ -89,6 +97,24 @@ export function deepHexlify(obj: any): any {
);
}
+export function applyFeeOption(
+ value: BigNumberish | undefined,
+ feeOption: UserOperationFeeOptionsField | undefined
+): BigNumberish {
+ if (feeOption === undefined) {
+ 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
*
@@ -156,6 +182,31 @@ export function defineReadOnly(
});
}
+export function isBigNumberish(x: any): x is BigNumberish {
+ return BigNumberishSchema.safeParse(x).success;
+}
+
+export function isPercentage(x: any): x is Percentage {
+ return PercentageSchema.safeParse(x).success;
+}
+
+export function filterUndefined(
+ obj: Record
+): Record {
+ Object.keys(obj).forEach((key) => {
+ if (obj[key] === undefined) {
+ delete obj[key];
+ }
+ });
+ return obj;
+}
+
+export function pick(obj: Record, keys: string | string[]) {
+ return Object.keys(obj)
+ .filter((k) => keys.includes(k))
+ .reduce((res, k) => Object.assign(res, { [k]: obj[k] }), {});
+}
+
export * from "./bigint.js";
export * from "./defaults.js";
export * from "./schema.js";
diff --git a/packages/core/src/utils/schema.ts b/packages/core/src/utils/schema.ts
index 140dd4f42b..f7428efcc2 100644
--- a/packages/core/src/utils/schema.ts
+++ b/packages/core/src/utils/schema.ts
@@ -1,4 +1,4 @@
-import type { Chain } from "viem";
+import { isHex, type Chain } from "viem";
import { z } from "zod";
import { getChain } from "./index.js";
@@ -18,3 +18,25 @@ export const ChainSchema = z.custom((chain) => {
return false;
}
});
+
+export const HexSchema = z.custom<`0x${string}` | "0x">((val) => {
+ return isHex(val) || val === "0x";
+});
+
+export const BigNumberishSchema = z.union([HexSchema, z.number(), z.bigint()]);
+
+export const BigNumberishRangeSchema = z
+ .object({
+ min: BigNumberishSchema.optional(),
+ max: BigNumberishSchema.optional(),
+ })
+ .strict();
+
+export const PercentageSchema = z
+ .object({
+ /**
+ * Percent value between 1 and 1000 inclusive
+ */
+ percentage: z.number().min(1).max(1000),
+ })
+ .strict();
diff --git a/site/packages/aa-alchemy/middleware/withAlchemyGasManager.md b/site/packages/aa-alchemy/middleware/withAlchemyGasManager.md
index 1a0ec1e4e4..21c9de5fd7 100644
--- a/site/packages/aa-alchemy/middleware/withAlchemyGasManager.md
+++ b/site/packages/aa-alchemy/middleware/withAlchemyGasManager.md
@@ -53,4 +53,4 @@ A new instance of an `AlchemyProvider` with the same attributes as the input, no
- `policyId: string` -- the Alchemy Gas Manager policy ID
-### `estimateGas: boolean` -- a flag to additionally estimate gas as part of
+### `delegateGasEstimation: boolean` -- a flag to additionally estimate gas as part of