Skip to content

Commit

Permalink
Proper Gas Estimation for Unsponsored UserOps (#106)
Browse files Browse the repository at this point in the history
  • Loading branch information
bh2smith authored Nov 12, 2024
1 parent 325368a commit 3037d6d
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 31 deletions.
13 changes: 9 additions & 4 deletions examples/send-tx.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import dotenv from "dotenv";
import { ethers } from "ethers";
import { isAddress } from "viem";
import { formatEther, isAddress } from "viem";

import { loadArgs, loadEnv } from "./cli";
import { DEFAULT_SAFE_SALT_NONCE, NearSafe, Network } from "../src";
import {
DEFAULT_SAFE_SALT_NONCE,
NearSafe,
Network,
userOpTransactionCost,
} from "../src";

dotenv.config();

Expand Down Expand Up @@ -48,8 +53,8 @@ async function main(): Promise<void> {
const safeOpHash = await txManager.opHash(chainId, unsignedUserOp);
console.log("Safe Op Hash", safeOpHash);

// TODO: Evaluate gas cost (in ETH)
const gasCost = ethers.parseEther("0.01");
const gasCost = userOpTransactionCost(unsignedUserOp);
console.log("Estimated gas cost", formatEther(gasCost));
// Whenever not using paymaster, or on value transfer, the Safe must be funded.
const sufficientFunded = await txManager.sufficientlyFunded(
chainId,
Expand Down
29 changes: 15 additions & 14 deletions src/lib/bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
http,
PublicClient,
rpcSchema,
toHex,
Transport,
RpcError,
HttpRequestError,
Expand All @@ -18,6 +17,7 @@ import {
SponsorshipPolicyData,
UnsignedUserOperation,
UserOperation,
UserOperationGas,
UserOperationReceipt,
} from "../types";
import { PLACEHOLDER_SIG } from "../util";
Expand All @@ -36,6 +36,11 @@ type BundlerRpcSchema = [
Parameters: [UnsignedUserOperation, Address, SponsorshipPolicy];
ReturnType: PaymasterData;
},
{
Method: "eth_estimateUserOperationGas";
Parameters: [UnsignedUserOperation, Address];
ReturnType: UserOperationGas;
},
{
Method: "eth_sendUserOperation";
Parameters: [UserOperation, Address];
Expand Down Expand Up @@ -71,24 +76,29 @@ export class Erc4337Bundler {

async getPaymasterData(
rawUserOp: UnsignedUserOperation,
safeNotDeployed: boolean,
sponsorshipPolicy?: string
): Promise<PaymasterData> {
// TODO: Keep this option out of the bundler
const userOp = { ...rawUserOp, signature: PLACEHOLDER_SIG };
if (sponsorshipPolicy) {
console.log("Requesting paymaster data...");
return handleRequest<PaymasterData>(() =>
this.client.request({
method: "pm_sponsorUserOperation",
params: [
{ ...rawUserOp, signature: PLACEHOLDER_SIG },
userOp,
this.entryPointAddress,
{ sponsorshipPolicyId: sponsorshipPolicy },
],
})
);
}
return defaultPaymasterData(safeNotDeployed);
console.log("Estimating user operation gas...");
return handleRequest<UserOperationGas>(() =>
this.client.request({
method: "eth_estimateUserOperationGas",
params: [userOp, this.entryPointAddress],
})
);
}

async sendUserOperation(userOp: UserOperation): Promise<Hash> {
Expand Down Expand Up @@ -172,15 +182,6 @@ async function handleRequest<T>(clientMethod: () => Promise<T>): Promise<T> {
}
}

// TODO(bh2smith) Should probably get reasonable estimates here:
const defaultPaymasterData = (safeNotDeployed: boolean): PaymasterData => {
return {
verificationGasLimit: toHex(safeNotDeployed ? 500000 : 100000),
callGasLimit: toHex(100000),
preVerificationGas: toHex(100000),
};
};

export function stripApiKey(error: unknown): string {
const message = error instanceof Error ? error.message : String(error);
return message.replace(/(apikey=)[^\s&]+/, "$1***");
Expand Down
1 change: 0 additions & 1 deletion src/near-safe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,6 @@ export class NearSafe {

const paymasterData = await bundler.getPaymasterData(
rawUserOp,
!safeDeployed,
sponsorshipPolicy
);

Expand Down
15 changes: 5 additions & 10 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,27 +55,22 @@ export interface UnsignedUserOperation {
/**
* Supported representation of a user operation for EntryPoint version 0.7, including gas limits and signature.
*/
export interface UserOperation extends UnsignedUserOperation {
/** The gas limit for verification of the operation. */
verificationGasLimit: Hex;
/** The gas limit for the execution of the operation call. */
callGasLimit: Hex;
/** The gas used before verification begins. */
preVerificationGas: Hex;
export interface UserOperation extends UnsignedUserOperation, PaymasterData {
/** Optional signature for the user operation. */
signature?: Hex;
}
// TODO(bh2smith): deduplicate the GasData fields between UserOperation and PaymasterData
// {verificationGasLimit, callGasLimit, preVerificationGas}

/**
* Represents additional paymaster-related data for a user operation.
*/
export interface PaymasterData {
export interface PaymasterData extends UserOperationGas {
/** Optional paymaster address responsible for covering gas costs. */
paymaster?: Address;
/** Optional additional data required by the paymaster. */
paymasterData?: Hex;
}

export interface UserOperationGas {
/** The gas limit for paymaster verification. */
paymasterVerificationGasLimit?: Hex;
/** The gas limit for paymaster post-operation execution. */
Expand Down
34 changes: 32 additions & 2 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
} from "viem";

import { DEFAULT_SETUP_RPC } from "./constants";
import { PaymasterData, MetaTransaction } from "./types";
import { PaymasterData, MetaTransaction, UserOperation } from "./types";

export const PLACEHOLDER_SIG = encodePacked(["uint48", "uint48"], [0, 0]);

Expand Down Expand Up @@ -68,7 +68,7 @@ export async function isContract(
}

export function getClient(chainId: number): PublicClient {
// TODO(bh2smith)
// TODO(bh2smith): Update defailt client URL in viem for sepolia.
if (chainId === 11155111) {
return createPublicClient({ transport: http(DEFAULT_SETUP_RPC) });
}
Expand Down Expand Up @@ -201,3 +201,33 @@ export function assertUnique<T>(
throw new Error(errorMessage);
}
}

export function userOpTransactionCost(userOp: UserOperation): bigint {
// Convert values from hex to decimal
const preVerificationGas = BigInt(userOp.preVerificationGas);
const verificationGasLimit = BigInt(userOp.verificationGasLimit);
const callGasLimit = BigInt(userOp.callGasLimit);
const paymasterVerificationGasLimit = BigInt(
userOp.paymasterVerificationGasLimit || "0x0"
);
const paymasterPostOpGasLimit = BigInt(
userOp.paymasterPostOpGasLimit || "0x0"
);

// Sum total gas
const totalGasUsed =
preVerificationGas +
verificationGasLimit +
callGasLimit +
paymasterVerificationGasLimit +
paymasterPostOpGasLimit;

// Convert maxFeePerGas from hex to decimal
const maxFeePerGas = BigInt(userOp.maxFeePerGas);

// Calculate total cost in wei
const totalCostInWei = totalGasUsed * maxFeePerGas;

// Convert to Ether for a human-readable value
return totalCostInWei;
}

0 comments on commit 3037d6d

Please sign in to comment.