Skip to content

Commit

Permalink
feat: calculate L1 TVL using batch request
Browse files Browse the repository at this point in the history
  • Loading branch information
0xnigir1 committed Aug 2, 2024
1 parent 54562eb commit 539530f
Show file tree
Hide file tree
Showing 11 changed files with 262 additions and 13 deletions.
2 changes: 2 additions & 0 deletions libs/metrics/src/l1/bytecode/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const tokenBalancesBytecode =
"0x608060405234801561001057600080fd5b5060405161063538038061063583398181016040528101906100329190610332565b60008151905060006001826100479190610516565b67ffffffffffffffff811115610086577f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6040519080825280602002602001820160405280156100b45781602001602082028036833780820191505090505b50905060005b828110156101e75760008482815181106100fd577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b6020026020010151905060008173ffffffffffffffffffffffffffffffffffffffff166370a08231886040518263ffffffff1660e01b81526004016101429190610443565b60206040518083038186803b15801561015a57600080fd5b505afa15801561016e573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101929190610386565b9050808484815181106101ce577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b60200260200101818152505082600101925050506100ba565b8473ffffffffffffffffffffffffffffffffffffffff1631828281518110610238577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b602002602001018181525050600082604051602001610257919061045e565b60405160208183030381529060405290506020810180590381f35b6000610285610280846104b1565b610480565b905080838252602082019050828560208602820111156102a457600080fd5b60005b858110156102d457816102ba88826102de565b8452602084019350602083019250506001810190506102a7565b5050509392505050565b6000815190506102ed81610606565b92915050565b600082601f83011261030457600080fd5b8151610314848260208601610272565b91505092915050565b60008151905061032c8161061d565b92915050565b6000806040838503121561034557600080fd5b6000610353858286016102de565b925050602083015167ffffffffffffffff81111561037057600080fd5b61037c858286016102f3565b9150509250929050565b60006020828403121561039857600080fd5b60006103a68482850161031d565b91505092915050565b60006103bb8383610434565b60208301905092915050565b6103d08161056c565b82525050565b60006103e1826104ed565b6103eb8185610505565b93506103f6836104dd565b8060005b8381101561042757815161040e88826103af565b9750610419836104f8565b9250506001810190506103fa565b5085935050505092915050565b61043d8161059e565b82525050565b600060208201905061045860008301846103c7565b92915050565b6000602082019050818103600083015261047881846103d6565b905092915050565b6000604051905081810181811067ffffffffffffffff821117156104a7576104a66105d7565b5b8060405250919050565b600067ffffffffffffffff8211156104cc576104cb6105d7565b5b602082029050602081019050919050565b6000819050602082019050919050565b600081519050919050565b6000602082019050919050565b600082825260208201905092915050565b60006105218261059e565b915061052c8361059e565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03821115610561576105606105a8565b5b828201905092915050565b60006105778261057e565b9050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b61060f8161056c565b811461061a57600080fd5b50565b6106268161059e565b811461063157600080fd5b5056fe";
82 changes: 79 additions & 3 deletions libs/metrics/src/l1/l1MetricsService.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import assert from "assert";
import { Inject, Injectable, LoggerService } from "@nestjs/common";
import { WINSTON_MODULE_NEST_PROVIDER } from "nest-winston";
import { Address, ContractConstructorArgs, parseAbiParameters } from "viem";

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 { Tvl } 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 { tokens } from "@zkchainhub/shared/tokens/tokens";
import { parseUnits } from "@zkchainhub/shared/utils";

/**
* Acts as a wrapper around Viem library to provide methods to interact with an EVM-based blockchain.
Expand All @@ -27,10 +34,79 @@ export class L1MetricsService {
@Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService,
) {}

//TODO: Implement l1Tvl.
async l1Tvl(): Promise<{ [asset: string]: { amount: number; amountUsd: number } }> {
return { ETH: { amount: 1000000, amountUsd: 1000000 } };
/**
* Retrieves the Total Value Locked by token on L1 Shared Bridge contract
* @returns A Promise that resolves to an object representing the TVL.
*/
async l1Tvl(): Promise<Tvl> {
const addresses = tokens
.filter((token) => !!token.contractAddress)
.map((token) => token.contractAddress) as Address[];

const balances = await this.fetchTokenBalances(addresses);
const pricesRecord = await this.pricingService.getTokenPrices(
tokens.map((token) => token.coingeckoId),
);

assert(balances.length === addresses.length + 1, "Invalid balances length");
assert(Object.keys(pricesRecord).length === tokens.length, "Invalid prices length");

return this.calculateTvl(balances, addresses, pricesRecord);
}

private calculateTvl(
balances: bigint[],
addresses: Address[],
prices: Record<string, number>,
): Tvl {
const tvl: Tvl = {};

for (const token of tokens) {
const balance =
token.type === "native"
? balances[addresses.length]
: balances[addresses.indexOf(token.contractAddress as Address)];

assert(balance !== undefined, `Balance for ${token.symbol} not found`);

const price = prices[token.coingeckoId] as number;
const parsedBalance = parseUnits(balance, token.decimals);
const tvlValue = parsedBalance * price;

tvl[token.symbol] = {
amount: parsedBalance,
amountUsd: tvlValue,
name: token.name,
imageUrl: token.imageUrl,
};
}

return tvl;
}

/**
* Fetches the token balances of Shared Bridgefor the given addresses.
* Note: The last balance in the returned array is the ETH balance.
* @param addresses The addresses for which to fetch the token balances.
* @returns A promise that resolves to an array of token balances as bigints.
*/
private async fetchTokenBalances(addresses: Address[]): Promise<bigint[]> {
const returnAbiParams = parseAbiParameters("uint256[]");
const args: ContractConstructorArgs<typeof tokenBalancesAbi> = [
L1_CONTRACTS.SHARED_BRIDGE,
addresses,
];

const [balances] = await this.evmProviderService.batchRequest(
tokenBalancesAbi,
tokenBalancesBytecode,
args,
returnAbiParams,
);

return balances as bigint[];
}

//TODO: Implement getBatchesInfo.
async getBatchesInfo(
_chainId: number,
Expand Down
1 change: 1 addition & 0 deletions libs/metrics/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./tvl.type";
10 changes: 10 additions & 0 deletions libs/metrics/src/types/tvl.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type TokenTvl = {
amount: number;
amountUsd: number;
name: string;
imageUrl?: string;
};

export type Tvl = {
[asset: string]: TokenTvl;
};
124 changes: 116 additions & 8 deletions libs/metrics/test/unit/l1/l1MetricsService.spec.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,57 @@
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 { 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";

// Mock implementations of the dependencies
const mockEvmProviderService = {
// Mock methods and properties as needed
};
const mockEvmProviderService = createMock<EvmProviderService>();

const mockPricingService = {
// Mock methods and properties as needed
};
const mockPricingService = createMock<IPricingService>();

jest.mock("@zkchainhub/shared/tokens/tokens", () => ({
get tokens() {
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,
},
{
name: "USDC",
symbol: "USDC",
contractAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
coingeckoId: "usd-coin",
imageUrl:
"https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694",
type: "erc20",
decimals: 6,
},
{
name: "Wrapped BTC",
symbol: "WBTC",
contractAddress: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
coingeckoId: "wrapped-bitcoin",
imageUrl:
"https://coin-images.coingecko.com/coins/images/7598/large/wrapped_bitcoin_wbtc.png?1696507857",
type: "erc20",
decimals: 8,
},
];
},
}));

export const mockLogger: Partial<Logger> = {
log: jest.fn(),
Expand Down Expand Up @@ -60,6 +96,10 @@ describe("L1MetricsService", () => {
l1MetricsService = module.get<L1MetricsService>(L1MetricsService);
});

afterEach(() => {
jest.clearAllMocks();
});

describe("constructor", () => {
it("initialize bridgeHub and sharedBridge", () => {
expect(l1MetricsService["bridgeHub"]).toEqual({
Expand All @@ -78,9 +118,77 @@ describe("L1MetricsService", () => {
});

describe("l1Tvl", () => {
it("return l1Tvl", async () => {
it("return the TVL on L1 Shared Bridge", async () => {
const mockBalances = [60_841_657_140641n, 135_63005559n, 12_3803_824374847279970609n]; // Mocked balances
const mockPrices = { "wrapped-bitcoin": 66_129, "usd-coin": 0.999, ethereum: 3_181.09 }; // Mocked prices

jest.spyOn(mockEvmProviderService, "batchRequest").mockResolvedValue([mockBalances]);
jest.spyOn(mockPricingService, "getTokenPrices").mockResolvedValue(mockPrices);

const result = await l1MetricsService.l1Tvl();
expect(result).toEqual({ ETH: { amount: 1000000, amountUsd: 1000000 } });

expect(result).toMatchObject({
ETH: {
amount: expect.closeTo(12_3803.824),
amountUsd: expect.closeTo(393_831_107.68),
name: "Ethereum",
imageUrl:
"https://coin-images.coingecko.com/coins/images/279/large/ethereum.png?1696501628",
},
WBTC: {
amount: expect.closeTo(135.631),
amountUsd: expect.closeTo(8_969_079.95),
name: "Wrapped BTC",
imageUrl:
"https://coin-images.coingecko.com/coins/images/7598/large/wrapped_bitcoin_wbtc.png?1696507857",
},
USDC: {
amount: expect.closeTo(60_841_657.141),
amountUsd: expect.closeTo(60_780_815.48),
name: "USDC",
imageUrl:
"https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694",
},
});
expect(mockEvmProviderService.batchRequest).toHaveBeenCalledWith(
tokenBalancesAbi,
tokenBalancesBytecode,
[
L1_CONTRACTS.SHARED_BRIDGE,
[
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
],
],
[
{
type: "uint256[]",
},
],
);
expect(mockPricingService.getTokenPrices).toHaveBeenCalledWith([
"ethereum",
"usd-coin",
"wrapped-bitcoin",
]);
});

it("throws an error if the balances length is invalid", async () => {
jest.spyOn(mockEvmProviderService, "batchRequest").mockResolvedValue([[]]);

await expect(l1MetricsService.l1Tvl()).rejects.toThrowError("Invalid balances length");
});

it("throws an error if the prices length is invalid", async () => {
jest.spyOn(mockEvmProviderService, "batchRequest").mockResolvedValue([
[60_841_657_140641n, 135_63005559n, 12_3803_824374847279970609n],
]);
jest.spyOn(mockPricingService, "getTokenPrices").mockResolvedValue({
ethereum: 3_181.09,
"usd-coin": 0.999,
});

await expect(l1MetricsService.l1Tvl()).rejects.toThrowError("Invalid prices length");
});
});

Expand Down
4 changes: 3 additions & 1 deletion libs/shared/src/tokens/tokens.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Address } from "abitype";

export type TokenType = {
name: string;
symbol: string;
coingeckoId: string;
type: "erc20" | "native";
contractAddress: string | null;
contractAddress: Address | null;
decimals: number;
imageUrl?: string;
};
Expand Down
1 change: 1 addition & 0 deletions libs/shared/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./parseUnits";
7 changes: 7 additions & 0 deletions libs/shared/src/utils/parseUnits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import BigNumber from "bignumber.js";

export const parseUnits = (value: bigint, decimals: number): number => {
return BigNumber(value.toString())
.div(10 ** decimals)
.toNumber();
};
33 changes: 33 additions & 0 deletions libs/shared/test/unit/utils/parseUnits.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { parseUnits } from "@zkchainhub/shared/utils";

describe("parseUnits", () => {
it("parse units correctly", () => {
const value = 1000000000000000000n;
const decimals = 18;
const expected = 1;

const result = parseUnits(value, decimals);

expect(result).toEqual(expected);
});

it("handle decimals correctly", () => {
const value = 123456789n;
const decimals = 9;
const expected = 0.123456789;

const result = parseUnits(value, decimals);

expect(result).toEqual(expected);
});

it("handles zero value correctly", () => {
const value = 0n;
const decimals = 18;
const expected = 0;

const result = parseUnits(value, decimals);

expect(result).toEqual(expected);
});
});
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@
"abitype": "1.0.5",
"axios": "1.7.2",
"axios-mock-adapter": "1.22.0",
"nest-winston": "1.9.7",
"bignumber.js": "9.1.2",
"cache-manager": "5.7.4",
"nest-winston": "1.9.7",
"reflect-metadata": "0.1.13",
"rxjs": "7.8.1",
"solhint-community": "4.0.0",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 539530f

Please sign in to comment.