Skip to content

Commit

Permalink
feat: gas info (#38)
Browse files Browse the repository at this point in the history
  • Loading branch information
0xnigir1 authored Aug 7, 2024
1 parent 4ef73d9 commit 512da31
Show file tree
Hide file tree
Showing 10 changed files with 282 additions and 24 deletions.
13 changes: 13 additions & 0 deletions libs/metrics/src/exceptions/provider.exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export class ProviderException extends Error {
constructor(message: string) {
super(message);
this.name = "ProviderException";
}
}

export class L1ProviderException extends ProviderException {
constructor(message: string) {
super(message);
this.name = "L1ProviderException";
}
}
75 changes: 69 additions & 6 deletions libs/metrics/src/l1/l1MetricsService.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,36 @@
import assert from "assert";
import { isNativeError } from "util/types";
import { Inject, Injectable, LoggerService } from "@nestjs/common";
import { WINSTON_MODULE_NEST_PROVIDER } from "nest-winston";
import {
Address,
ContractConstructorArgs,
encodeFunctionData,
erc20Abi,
formatUnits,
parseAbiParameters,
parseEther,
parseUnits,
zeroAddress,
} from "viem";

import { L1ProviderException } from "@zkchainhub/metrics/exceptions/provider.exception";
import { bridgeHubAbi, sharedBridgeAbi } from "@zkchainhub/metrics/l1/abis";
import { tokenBalancesAbi } from "@zkchainhub/metrics/l1/abis/tokenBalances.abi";
import { tokenBalancesBytecode } from "@zkchainhub/metrics/l1/bytecode";
import { AssetTvl } from "@zkchainhub/metrics/types";
import { AssetTvl, GasInfo } from "@zkchainhub/metrics/types";
import { IPricingService, PRICING_PROVIDER } from "@zkchainhub/pricing";
import { EvmProviderService } from "@zkchainhub/providers";
import { AbiWithAddress, ChainId, L1_CONTRACTS } from "@zkchainhub/shared";
import { erc20Tokens, isNativeToken, tokens } from "@zkchainhub/shared/tokens/tokens";
import { AbiWithAddress, ChainId, L1_CONTRACTS, vitalikAddress } from "@zkchainhub/shared";
import {
erc20Tokens,
isNativeToken,
nativeToken,
tokens,
WETH,
} from "@zkchainhub/shared/tokens/tokens";

const ONE_ETHER = parseEther("1");

/**
* Acts as a wrapper around Viem library to provide methods to interact with an EVM-based blockchain.
Expand Down Expand Up @@ -147,10 +161,59 @@ export class L1MetricsService {
async chainType(_chainId: number): Promise<"validium" | "rollup"> {
return "rollup";
}
//TODO: Implement ethGasInfo.
async ethGasInfo(): Promise<{ gasPrice: number; ethTransfer: number; erc20Transfer: number }> {
return { gasPrice: 50, ethTransfer: 21000, erc20Transfer: 65000 };

/**
* Retrieves gas information for Ethereum transfers and ERC20 token transfers.
* @returns {GasInfo} A promise that resolves to an object containing gas-related information.
*/
async ethGasInfo(): Promise<GasInfo> {
try {
const [ethTransferGasCost, erc20TransferGasCost, gasPrice] = await Promise.all([
// Estimate gas for an ETH transfer.
this.evmProviderService.estimateGas({
account: vitalikAddress,
to: zeroAddress,
value: ONE_ETHER,
}),
// Estimate gas for an ERC20 transfer.
this.evmProviderService.estimateGas({
account: vitalikAddress,
to: WETH.contractAddress,
data: encodeFunctionData({
abi: erc20Abi,
functionName: "transfer",
args: [L1_CONTRACTS.SHARED_BRIDGE, ONE_ETHER],
}),
}),
// Get the current gas price.
this.evmProviderService.getGasPrice(),
]);

// Get the current price of ether.
let ethPriceInUsd: number | undefined = undefined;
try {
const priceResult = await this.pricingService.getTokenPrices([
nativeToken.coingeckoId,
]);
ethPriceInUsd = priceResult[nativeToken.coingeckoId];
} catch (e) {
this.logger.error("Failed to get the price of ether.");
}

return {
gasPrice,
ethPrice: ethPriceInUsd,
ethTransferGas: ethTransferGasCost,
erc20TransferGas: erc20TransferGasCost,
};
} catch (e: unknown) {
if (isNativeError(e)) {
this.logger.error(`Failed to get gas information: ${e.message}`);
}
throw new L1ProviderException("Failed to get gas information from L1.");
}
}

//TODO: Implement feeParams.
async feeParams(_chainId: number): Promise<{
batchOverheadL1Gas: number;
Expand Down
6 changes: 6 additions & 0 deletions libs/metrics/src/types/gasInfo.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type GasInfo = {
gasPrice: bigint; // wei
ethPrice?: number; // USD
ethTransferGas: bigint; // units of gas
erc20TransferGas: bigint; // units of gas
};
1 change: 1 addition & 0 deletions libs/metrics/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./gasInfo.type";
export * from "./tvl.type";
174 changes: 156 additions & 18 deletions libs/metrics/test/unit/l1/l1MetricsService.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,26 @@ import { createMock } from "@golevelup/ts-jest";
import { Logger } from "@nestjs/common";
import { Test, TestingModule } from "@nestjs/testing";
import { WINSTON_MODULE_PROVIDER } from "nest-winston";
import { encodeFunctionData, erc20Abi, parseEther, zeroAddress } from "viem";

import { L1ProviderException } from "@zkchainhub/metrics/exceptions/provider.exception";
import { L1MetricsService } from "@zkchainhub/metrics/l1/";
import { bridgeHubAbi, sharedBridgeAbi } from "@zkchainhub/metrics/l1/abis";
import { tokenBalancesAbi } from "@zkchainhub/metrics/l1/abis/tokenBalances.abi";
import { tokenBalancesBytecode } from "@zkchainhub/metrics/l1/bytecode";
import { IPricingService, PRICING_PROVIDER } from "@zkchainhub/pricing";
import { EvmProviderService } from "@zkchainhub/providers";
import { L1_CONTRACTS } from "@zkchainhub/shared";
import { L1_CONTRACTS, vitalikAddress } from "@zkchainhub/shared";
import { nativeToken, WETH } from "@zkchainhub/shared/tokens/tokens";

// Mock implementations of the dependencies
const mockEvmProviderService = createMock<EvmProviderService>();

const mockPricingService = createMock<IPricingService>();

const ONE_ETHER = parseEther("1");
jest.mock("@zkchainhub/shared/tokens/tokens", () => ({
...jest.requireActual("@zkchainhub/shared/tokens/tokens"),
get nativeToken() {
return {
name: "Ethereum",
symbol: "ETH",
contractAddress: null,
coingeckoId: "ethereum",
type: "native",
imageUrl:
"https://coin-images.coingecko.com/coins/images/279/large/ethereum.png?1696501628",
decimals: 18,
};
},
get erc20Tokens() {
return [
{
Expand Down Expand Up @@ -107,11 +99,15 @@ describe("L1MetricsService", () => {
{
provide: L1MetricsService,
useFactory: (
evmProviderService: EvmProviderService,
pricingService: IPricingService,
mockEvmProviderService: EvmProviderService,
mockPricingService: IPricingService,
logger: Logger,
) => {
return new L1MetricsService(evmProviderService, pricingService, logger);
return new L1MetricsService(
mockEvmProviderService,
mockPricingService,
logger,
);
},
inject: [EvmProviderService, PRICING_PROVIDER, WINSTON_MODULE_PROVIDER],
},
Expand Down Expand Up @@ -267,9 +263,151 @@ describe("L1MetricsService", () => {
});

describe("ethGasInfo", () => {
it("return ethGasInfo", async () => {
it("returns gas information from L1", async () => {
// Mock the necessary dependencies
const mockEstimateGas = jest.spyOn(mockEvmProviderService, "estimateGas");
mockEstimateGas.mockResolvedValueOnce(BigInt(21000)); // ethTransferGasCost
mockEstimateGas.mockResolvedValueOnce(BigInt(65000)); // erc20TransferGasCost'
const mockGetGasPrice = jest.spyOn(mockEvmProviderService, "getGasPrice");
mockGetGasPrice.mockResolvedValueOnce(BigInt(50000000000)); // gasPrice

const mockGetTokenPrices = jest.spyOn(mockPricingService, "getTokenPrices");
mockGetTokenPrices.mockResolvedValueOnce({ [nativeToken.coingeckoId]: 2000 }); // ethPriceInUsd

// Call the method
const result = await l1MetricsService.ethGasInfo();
expect(result).toEqual({ gasPrice: 50, ethTransfer: 21000, erc20Transfer: 65000 });

// Assertions
expect(mockEstimateGas).toHaveBeenCalledTimes(2);
expect(mockEstimateGas).toHaveBeenNthCalledWith(1, {
account: vitalikAddress,
to: zeroAddress,
value: ONE_ETHER,
});
expect(mockEstimateGas).toHaveBeenNthCalledWith(2, {
account: vitalikAddress,
to: WETH.contractAddress,
data: encodeFunctionData({
abi: erc20Abi,
functionName: "transfer",
args: [L1_CONTRACTS.SHARED_BRIDGE, ONE_ETHER],
}),
});

expect(mockGetGasPrice).toHaveBeenCalledTimes(1);

expect(mockGetTokenPrices).toHaveBeenCalledTimes(1);
expect(mockGetTokenPrices).toHaveBeenCalledWith([nativeToken.coingeckoId]);

expect(result).toEqual({
gasPrice: 50000000000n,
ethPrice: 2000,
ethTransferGas: 21000n,
erc20TransferGas: 65000n,
});
});

it("returns gas information from L1 without ether price", async () => {
const mockEstimateGas = jest.spyOn(mockEvmProviderService, "estimateGas");
mockEstimateGas.mockResolvedValueOnce(BigInt(21000)); // ethTransferGasCost
mockEstimateGas.mockResolvedValueOnce(BigInt(65000)); // erc20TransferGasCost

const mockGetGasPrice = jest.spyOn(mockEvmProviderService, "getGasPrice");
mockGetGasPrice.mockResolvedValueOnce(BigInt(50000000000)); // gasPrice

const mockGetTokenPrices = jest.spyOn(mockPricingService, "getTokenPrices");
mockGetTokenPrices.mockRejectedValueOnce(new Error("Failed to get token prices"));

const result = await l1MetricsService.ethGasInfo();

// Assertions
expect(result).toEqual({
gasPrice: 50000000000n,
ethPrice: undefined,
ethTransferGas: 21000n,
erc20TransferGas: 65000n,
});
expect(mockEstimateGas).toHaveBeenCalledTimes(2);
expect(mockEstimateGas).toHaveBeenNthCalledWith(1, {
account: vitalikAddress,
to: zeroAddress,
value: ONE_ETHER,
});
expect(mockEstimateGas).toHaveBeenNthCalledWith(2, {
account: vitalikAddress,
to: WETH.contractAddress,
data: encodeFunctionData({
abi: erc20Abi,
functionName: "transfer",
args: [L1_CONTRACTS.SHARED_BRIDGE, ONE_ETHER],
}),
});

expect(mockGetGasPrice).toHaveBeenCalledTimes(1);

expect(mockGetTokenPrices).toHaveBeenCalledTimes(1);
expect(mockGetTokenPrices).toHaveBeenCalledWith([nativeToken.coingeckoId]);
});

it("throws L1ProviderException when estimateGas fails", async () => {
// Mock the necessary dependencies
const mockEstimateGas = jest.spyOn(mockEvmProviderService, "estimateGas");
mockEstimateGas.mockRejectedValueOnce(new Error("Failed to estimate gas"));

const mockGetGasPrice = jest.spyOn(mockEvmProviderService, "getGasPrice");
mockGetGasPrice.mockResolvedValueOnce(BigInt(50000000000)); // gasPrice

const mockGetTokenPrices = jest.spyOn(mockPricingService, "getTokenPrices");
mockGetTokenPrices.mockResolvedValueOnce({ [nativeToken.coingeckoId]: 2000 }); // ethPriceInUsd

// Call the method and expect it to throw L1ProviderException
await expect(l1MetricsService.ethGasInfo()).rejects.toThrow(L1ProviderException);

// Assertions
expect(mockEstimateGas).toHaveBeenCalledWith({
account: vitalikAddress,
to: zeroAddress,
value: ONE_ETHER,
});

expect(mockGetTokenPrices).not.toHaveBeenCalled();
});

it("throws L1ProviderException when getGasPrice fails", async () => {
// Mock the necessary dependencies
const mockEstimateGas = jest.spyOn(mockEvmProviderService, "estimateGas");
mockEstimateGas.mockResolvedValueOnce(BigInt(21000)); // ethTransferGasCost
mockEstimateGas.mockResolvedValueOnce(BigInt(65000)); // erc20TransferGasCost

const mockGetGasPrice = jest.spyOn(mockEvmProviderService, "getGasPrice");
mockGetGasPrice.mockRejectedValueOnce(new Error("Failed to get gas price"));

const mockGetTokenPrices = jest.spyOn(mockPricingService, "getTokenPrices");
mockGetTokenPrices.mockResolvedValueOnce({ [nativeToken.coingeckoId]: 2000 }); // ethPriceInUsd

// Call the method and expect it to throw L1ProviderException
await expect(l1MetricsService.ethGasInfo()).rejects.toThrow(L1ProviderException);

// Assertions
expect(mockEstimateGas).toHaveBeenCalledTimes(2);
expect(mockEstimateGas).toHaveBeenNthCalledWith(1, {
account: vitalikAddress,
to: zeroAddress,
value: ONE_ETHER,
});
expect(mockEstimateGas).toHaveBeenNthCalledWith(2, {
account: vitalikAddress,
to: WETH.contractAddress,
data: encodeFunctionData({
abi: erc20Abi,
functionName: "transfer",
args: [L1_CONTRACTS.SHARED_BRIDGE, ONE_ETHER],
}),
});

expect(mockGetGasPrice).toHaveBeenCalledTimes(1);

expect(mockGetTokenPrices).not.toHaveBeenCalled();
});
});

Expand Down
5 changes: 5 additions & 0 deletions libs/providers/src/providers/evmProvider.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
decodeAbiParameters,
DecodeAbiParametersReturnType,
encodeDeployData,
EstimateGasParameters,
GetBlockReturnType,
Hex,
http,
Expand Down Expand Up @@ -74,6 +75,10 @@ export class EvmProviderService {
return this.client.getGasPrice();
}

async estimateGas(args: EstimateGasParameters<typeof this.chain>): Promise<bigint> {
return this.client.estimateGas(args);
}

/**
* Retrieves the value from a storage slot at a given address.
* @param {Address} address The address of the contract.
Expand Down
18 changes: 18 additions & 0 deletions libs/providers/test/unit/providers/evmProvider.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,24 @@ describe("EvmProviderService", () => {
});
});

describe("estimateGas", () => {
it("return the estimated gas for the given transaction", async () => {
const args = createMock<viem.EstimateGasParameters<typeof localhost>>({
account: "0xffff",
to: viem.zeroAddress,
value: 100n,
});

const expectedGas = 50000n;
jest.spyOn(mockClient, "estimateGas").mockResolvedValue(expectedGas);

const gas = await viemProvider.estimateGas(args);

expect(gas).toBe(expectedGas);
expect(mockClient.estimateGas).toHaveBeenCalledWith(args);
});
});

describe("getStorageAt", () => {
it("should return the value of the storage slot at the given address and slot number", async () => {
const address = "0x123456789";
Expand Down
3 changes: 3 additions & 0 deletions libs/shared/src/constants/addresses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Address } from "abitype";

export const vitalikAddress: Address = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
1 change: 1 addition & 0 deletions libs/shared/src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./l1";
export * from "./addresses";
export const TOKEN_CACHE_TTL_IN_SEC = 60;
export const BASE_CURRENCY = "usd";
Loading

0 comments on commit 512da31

Please sign in to comment.