Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(alchemy): use rundler_getLocalRequiredFees for fee estimate #71

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,6 @@ import {
metaForStepIdentifier,
} from "./OnboardingDataModels";

export enum GasFeeStrategy {
DEFAULT = "DEFAULT",
FIXED = "FIXED",
BASE_FEE_PERCENTAGE = "BASE_FEE_PERCENTAGE",
PRIORITY_FEE_PERCENTAGE = "PRIORITY_FEE_PERCENTAGE",
}

export interface GasFeeMode {
strategy: GasFeeStrategy;
value: bigint;
}

async function pollForLambdaForComplete(
lambda: () => Promise<boolean>,
txnMaxDurationSeconds: number = 20
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export const API_KEY = process.env.API_KEY!;
export const RPC_URL = process.env.RPC_URL;
export const API_KEY = process.env.API_KEY;
export const OWNER_MNEMONIC = process.env.OWNER_MNEMONIC!;
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ import {
import { KernelAccountProvider } from "../provider.js";
import type { KernelUserOperationCallData } from "../types.js";
import { KernelBaseValidator, ValidatorMode } from "../validator/base.js";
import { API_KEY, OWNER_MNEMONIC } from "./constants.js";
import { RPC_URL, API_KEY, OWNER_MNEMONIC } from "./constants.js";
import { MockSigner } from "./mocks/mock-signer.js";

describe("Kernel Account Tests", () => {
//any wallet should work
const config = {
chain: polygonMumbai,
rpcProvider: `${polygonMumbai.rpcUrls.alchemy.http[0]}/${API_KEY}`,
rpcProvider: RPC_URL !== undefined ? RPC_URL : `${polygonMumbai.rpcUrls.alchemy.http[0]}/${API_KEY}`,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the use case for this?

also nit:

Suggested change
rpcProvider: RPC_URL !== undefined ? RPC_URL : `${polygonMumbai.rpcUrls.alchemy.http[0]}/${API_KEY}`,
rpcProvider: RPC_URL != null ? RPC_URL : `${polygonMumbai.rpcUrls.alchemy.http[0]}/${API_KEY}`,

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be able to run the e2e tests against something other than Alchemy prod

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can put this in a separate PR, just needed it for my current testing.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all good, we can keep it here

validatorAddress: "0x180D6465F921C7E0DEA0040107D342c87455fFF5" as Address,
accountFactoryAddress:
"0x5D006d3880645ec6e254E18C1F879DAC9Dd71A39" as Address,
Expand Down
3 changes: 2 additions & 1 deletion packages/alchemy/e2e-tests/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const API_KEY = process.env.API_KEY!;
export const RPC_URL = process.env.RPC_URL;
export const API_KEY = process.env.API_KEY;
export const OWNER_MNEMONIC = process.env.OWNER_MNEMONIC!;
export const PAYMASTER_POLICY_ID = process.env.PAYMASTER_POLICY_ID!;
4 changes: 3 additions & 1 deletion packages/alchemy/e2e-tests/simple-account.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { toHex, type Hash } from "viem";
import { mnemonicToAccount } from "viem/accounts";
import { polygonMumbai } from "viem/chains";
import { AlchemyProvider } from "../src/provider.js";
import { API_KEY, OWNER_MNEMONIC, PAYMASTER_POLICY_ID } from "./constants.js";
import { RPC_URL, API_KEY, OWNER_MNEMONIC, PAYMASTER_POLICY_ID } from "./constants.js";

const ENTRYPOINT_ADDRESS = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789";
const SIMPLE_ACCOUNT_FACTORY_ADDRESS =
Expand All @@ -24,6 +24,7 @@ describe("Simple Account Tests", () => {
const chain = polygonMumbai;
const signer = new AlchemyProvider({
apiKey: API_KEY,
rpcUrl: RPC_URL,
chain,
entryPointAddress: ENTRYPOINT_ADDRESS,
}).connect(
Expand Down Expand Up @@ -57,6 +58,7 @@ describe("Simple Account Tests", () => {
const accountAddress = "0xc33AbD9621834CA7c6Fc9f9CC3c47b9c17B03f9F";
const newSigner = new AlchemyProvider({
apiKey: API_KEY,
rpcUrl: RPC_URL,
chain,
entryPointAddress: ENTRYPOINT_ADDRESS,
}).connect(
Expand Down
54 changes: 0 additions & 54 deletions packages/alchemy/src/chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
polygonMumbai,
sepolia,
} from "viem/chains";
import { GasFeeStrategy, type GasFeeMode } from "./middleware/gas-fees.js";

export const SupportedChains = new Map<number, Chain>([
[polygonMumbai.id, polygonMumbai],
Expand All @@ -23,56 +22,3 @@ export const SupportedChains = new Map<number, Chain>([
[optimism.id, optimism],
[optimismGoerli.id, optimismGoerli],
]);

const defineChainStrategy = (
chainId: number,
strategy: GasFeeStrategy,
value: GasFeeMode["value"]
): [number, GasFeeMode] => {
return [chainId, { strategy, value }];
};

export const ChainFeeStrategies: Map<number, GasFeeMode> = new Map<
number,
GasFeeMode
>([
// testnets
defineChainStrategy(
goerli.id,
GasFeeStrategy.PRIORITY_FEE_INCREASE_PERCENT,
0n
),
defineChainStrategy(
sepolia.id,
GasFeeStrategy.PRIORITY_FEE_INCREASE_PERCENT,
0n
),
defineChainStrategy(
polygonMumbai.id,
GasFeeStrategy.PRIORITY_FEE_INCREASE_PERCENT,
0n
),
defineChainStrategy(
optimismGoerli.id,
GasFeeStrategy.PERCENT_OF_BASE_FEE,
0n
),
defineChainStrategy(
arbitrumGoerli.id,
GasFeeStrategy.PERCENT_OF_BASE_FEE,
0n
),
// mainnets
defineChainStrategy(
mainnet.id,
GasFeeStrategy.PRIORITY_FEE_INCREASE_PERCENT,
57n
),
defineChainStrategy(
polygon.id,
GasFeeStrategy.PRIORITY_FEE_INCREASE_PERCENT,
25n
),
defineChainStrategy(optimism.id, GasFeeStrategy.PERCENT_OF_BASE_FEE, 5n),
defineChainStrategy(arbitrum.id, GasFeeStrategy.PERCENT_OF_BASE_FEE, 5n),
]);
6 changes: 1 addition & 5 deletions packages/alchemy/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
export {
GasFeeStrategy,
withAlchemyGasFeeEstimator,
} from "./middleware/gas-fees.js";
export type { GasFeeMode } from "./middleware/gas-fees.js";
export { withAlchemyGasFeeEstimator } from "./middleware/gas-fees.js";

export {
withAlchemyGasManager,
Expand Down
56 changes: 56 additions & 0 deletions packages/alchemy/src/middleware/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {
type PublicErc4337Client,
type UserOperationRequest,
} from "@alchemy/aa-core";
import type { Address, Hex } from "viem";

export type ClientWithAlchemyMethods = PublicErc4337Client & {
request: PublicErc4337Client["request"] &
{
request(args: {
method: "alchemy_requestPaymasterAndData";
params: [
{
policyId: string;
entryPoint: Address;
userOperation: UserOperationRequest;
}
];
}): Promise<{ paymasterAndData: Hex }>;

request(args: {
method: "alchemy_requestGasAndPaymasterAndData";
params: [
{
policyId: string;
entryPoint: Address;
userOperation: UserOperationRequest;
dummySignature: Hex;
}
];
}): Promise<{
paymasterAndData: Hex;
callGasLimit: Hex;
verificationGasLimit: Hex;
preVerificationGas: Hex;
maxFeePerGas: Hex;
maxPriorityFeePerGas: Hex;
}>;

request(args: {
method: "rundler_getLocalRequiredFees";
params: [];
}): Promise<{
blockNumber: Hex;
baseFeePerGas: Hex;
minimum: {
maxFeePerGas: Hex;
maxPriorityFeePerGas: Hex;
};
recommended: {
maxFeePerGas: Hex;
maxPriorityFeePerGas: Hex;
};
}>;
}["request"];
};
56 changes: 9 additions & 47 deletions packages/alchemy/src/middleware/gas-fees.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,17 @@
import type { AlchemyProvider } from "../provider.js";

export enum GasFeeStrategy {
DEFAULT = "DEFAULT",
PERCENT_OF_BASE_FEE = "PERCENT_OF_BASE_FEE",
PRIORITY_FEE_INCREASE_PERCENT = "PRIORITY_FEE_INCREASE_PERCENT",
}

export interface GasFeeMode {
strategy: GasFeeStrategy;
value: bigint;
}
import type { ClientWithAlchemyMethods } from "./client.js";

export const withAlchemyGasFeeEstimator = (
provider: AlchemyProvider,
feeMode: GasFeeMode,
maxPriorityFeeBufferPercent: bigint
provider: AlchemyProvider
): AlchemyProvider => {
if (feeMode.strategy === GasFeeStrategy.DEFAULT) {
return provider;
}

provider.withFeeDataGetter(async () => {
const block = await provider.rpcClient.getBlock({ blockTag: "latest" });
const baseFeePerGas = block.baseFeePerGas;
if (baseFeePerGas == null) {
throw new Error("baseFeePerGas is null");
}
// add a buffer here to account for potential spikes in priority fee
const maxPriorityFeePerGas =
(BigInt(await provider.rpcClient.getMaxPriorityFeePerGas()) *
(100n + maxPriorityFeeBufferPercent)) /
100n;
// add 25% overhead to ensure mine
const baseFeeScaled = (baseFeePerGas * 5n) / 4n;

const prioFee = ((): bigint => {
switch (feeMode.strategy) {
case GasFeeStrategy.PERCENT_OF_BASE_FEE:
return (baseFeeScaled * feeMode.value) / 100n;
case GasFeeStrategy.PRIORITY_FEE_INCREASE_PERCENT:
// add 10% to required priority fee to ensure mine
return (maxPriorityFeePerGas * (100n + feeMode.value)) / 100n;
default:
throw new Error("fee mode not supported");
}
})();

return {
maxPriorityFeePerGas: prioFee,
maxFeePerGas: baseFeeScaled + prioFee,
};
const result = await (
provider.rpcClient as ClientWithAlchemyMethods
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should update the typing on the AlchemyProvider to be this so we don't have to typecast

).request({
method: "rundler_getLocalRequiredFees",
params: [],
});
return result.recommended;
});
return provider;
};
38 changes: 2 additions & 36 deletions packages/alchemy/src/middleware/gas-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,9 @@ import {
resolveProperties,
type ConnectedSmartAccountProvider,
type PublicErc4337Client,
type UserOperationRequest,
} from "@alchemy/aa-core";
import type { Address, Hex, Transport } from "viem";

type ClientWithAlchemyMethods = PublicErc4337Client & {
request: PublicErc4337Client["request"] &
{
request(args: {
method: "alchemy_requestPaymasterAndData";
params: [
{
policyId: string;
entryPoint: Address;
userOperation: UserOperationRequest;
}
];
}): Promise<{ paymasterAndData: Hex }>;
request(args: {
method: "alchemy_requestGasAndPaymasterAndData";
params: [
{
policyId: string;
entryPoint: Address;
userOperation: UserOperationRequest;
dummySignature: Hex;
}
];
}): Promise<{
paymasterAndData: Hex;
callGasLimit: Hex;
verificationGasLimit: Hex;
preVerificationGas: Hex;
maxFeePerGas: Hex;
maxPriorityFeePerGas: Hex;
}>;
}["request"];
};
import type { Address, Transport } from "viem";
import type { ClientWithAlchemyMethods } from "./client.js";

export interface AlchemyGasManagerConfig {
policyId: string;
Expand Down
16 changes: 3 additions & 13 deletions packages/alchemy/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,12 @@ import {
optimism,
optimismGoerli,
} from "viem/chains";
import { ChainFeeStrategies, SupportedChains } from "./chains.js";
import {
GasFeeStrategy,
withAlchemyGasFeeEstimator,
} from "./middleware/gas-fees.js";
import { SupportedChains } from "./chains.js";
import {
withAlchemyGasManager,
type AlchemyGasManagerConfig,
} from "./middleware/gas-manager.js";
import { withAlchemyGasFeeEstimator } from "./middleware/gas-fees.js";

type ConnectionConfig =
| {
Expand Down Expand Up @@ -63,14 +60,7 @@ export class AlchemyProvider extends SmartAccountProvider<HttpTransport> {

super(rpcUrl, entryPointAddress, _chain, account, opts);

withAlchemyGasFeeEstimator(
this,
ChainFeeStrategies.get(_chain.id) ?? {
strategy: GasFeeStrategy.DEFAULT,
value: 0n,
},
feeOpts?.maxPriorityFeeBufferPercent ?? 5n
);
withAlchemyGasFeeEstimator(this);
}

gasEstimator: AccountMiddlewareFn = async (struct) => {
Expand Down
3 changes: 2 additions & 1 deletion packages/core/e2e-tests/constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export const API_KEY = process.env.API_KEY!;
export const RPC_URL = process.env.RPC_URL;
export const API_KEY = process.env.API_KEY;
export const OWNER_MNEMONIC = process.env.OWNER_MNEMONIC!;
8 changes: 4 additions & 4 deletions packages/core/e2e-tests/simple-account.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from "../src/account/simple.js";
import { SmartAccountProvider } from "../src/provider/base.js";
import { LocalAccountSigner } from "../src/signer/local-account.js";
import { API_KEY, OWNER_MNEMONIC } from "./constants.js";
import { RPC_URL, API_KEY, OWNER_MNEMONIC } from "./constants.js";

const ENTRYPOINT_ADDRESS = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789";
const SIMPLE_ACCOUNT_FACTORY_ADDRESS =
Expand All @@ -18,7 +18,7 @@ describe("Simple Account Tests", () => {
LocalAccountSigner.mnemonicToAccountSigner(OWNER_MNEMONIC);
const chain = polygonMumbai;
const signer = new SmartAccountProvider(
`${chain.rpcUrls.alchemy.http[0]}/${API_KEY}`,
RPC_URL !== undefined ? RPC_URL : `${chain.rpcUrls.alchemy.http[0]}/${API_KEY}`,
ENTRYPOINT_ADDRESS,
chain
).connect(
Expand Down Expand Up @@ -51,7 +51,7 @@ describe("Simple Account Tests", () => {
it("should fail to execute if account address is not deployed and not correct", async () => {
const accountAddress = "0xc33AbD9621834CA7c6Fc9f9CC3c47b9c17B03f9F";
const newSigner = new SmartAccountProvider(
`${chain.rpcUrls.alchemy.http[0]}/${API_KEY}`,
RPC_URL !== undefined ? RPC_URL : `${chain.rpcUrls.alchemy.http[0]}/${API_KEY}`,
ENTRYPOINT_ADDRESS,
chain
).connect(
Expand Down Expand Up @@ -79,7 +79,7 @@ describe("Simple Account Tests", () => {
generatePrivateKey()
);
const provider = new SmartAccountProvider(
`${chain.rpcUrls.alchemy.http[0]}/${API_KEY}`,
RPC_URL !== undefined ? RPC_URL : `${chain.rpcUrls.alchemy.http[0]}/${API_KEY}`,
ENTRYPOINT_ADDRESS,
chain
).connect(
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/provider/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,8 +293,8 @@ export class SmartAccountProvider<
const initCode = await this.account.getInitCode();
const uoStruct = await asyncPipe(
this.dummyPaymasterDataMiddleware,
this.gasEstimator,
this.feeDataGetter,
this.gasEstimator,
Comment on lines -296 to +297
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@moldy530 can you check my logic on this change. We want the fees to get estimated and applied before gas is estimated as gas estimation can sometimes rely on fees (e.g. as it will on Optimism). Does this have that intended impact?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup! pipe executes sequentially so this will do fees then gas

this.paymasterDataMiddleware,
this.customMiddleware ?? noOpMiddleware,
// This applies the overrides if they've been passed in
Expand Down
Loading
Loading