Skip to content

Commit

Permalink
fix: make code cleaner & readable, use bigint for math
Browse files Browse the repository at this point in the history
  • Loading branch information
0xnigir1 committed Aug 6, 2024
1 parent 6747ea4 commit 0d51d95
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 69 deletions.
89 changes: 57 additions & 32 deletions libs/metrics/src/l1/l1MetricsService.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import assert from "assert";
import { Inject, Injectable, LoggerService } from "@nestjs/common";
import { WINSTON_MODULE_NEST_PROVIDER } from "nest-winston";
import { Address, ContractConstructorArgs, formatUnits, parseAbiParameters } from "viem";
import {
Address,
ContractConstructorArgs,
formatUnits,
parseAbiParameters,
parseUnits,
} 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 { AssetTvl } 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 { erc20Tokens, isNativeToken, tokens } from "@zkchainhub/shared/tokens/tokens";

/**
* Acts as a wrapper around Viem library to provide methods to interact with an EVM-based blockchain.
Expand All @@ -35,61 +41,78 @@ export class L1MetricsService {

/**
* Retrieves the Total Value Locked by token on L1 Shared Bridge contract
* @returns A Promise that resolves to an object representing the TVL.
* @returns A Promise that resolves to an array of AssetTvl objects representing the TVL for each asset.
*/
async l1Tvl(): Promise<Tvl> {
const addresses = tokens
.filter((token) => !!token.contractAddress)
.map((token) => token.contractAddress) as Address[];
async l1Tvl(): Promise<AssetTvl[]> {
const erc20Addresses = erc20Tokens.map((token) => token.contractAddress);

const balances = await this.fetchTokenBalances(addresses);
const balances = await this.fetchTokenBalances(erc20Addresses);
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);
return this.calculateTvl(balances, erc20Addresses, pricesRecord);
}

/**
* Calculates the Total Value Locked (TVL) for each token based on the provided balances, addresses, and prices.
* @param balances - The balances object containing the ETH balance and an array of erc20 token addresses balance.
* @param addresses - The array of erc20 addresses.
* @param prices - The object containing the prices of tokens.
* @returns An array of AssetTvl objects representing the TVL for each token in descending order.
*/
private calculateTvl(
balances: bigint[],
balances: { ethBalance: bigint; addressesBalance: bigint[] },
addresses: Address[],
prices: Record<string, number>,
): Tvl {
const tvl: Tvl = {};
): AssetTvl[] {
const tvl: AssetTvl[] = [];

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

assert(balance !== undefined, `Balance for ${token.symbol} not found`);
const balance = isNativeToken(token)
? balances.ethBalance
: balances.addressesBalance[
addresses.indexOf(tokenInfo.contractAddress as Address)
];

const price = prices[token.coingeckoId] as number;
const parsedBalance = Number(formatUnits(balance, token.decimals));
const tvlValue = parsedBalance * price;
assert(balance !== undefined, `Balance for ${tokenInfo.symbol} not found`);

tvl[token.symbol] = {
amount: parsedBalance,
const price = prices[coingeckoId] as number;
// math is done with bigints for better precision
const tvlValue = formatUnits(
balance * parseUnits(price.toString(), tokenInfo.decimals),
tokenInfo.decimals * 2,
);

const assetTvl: AssetTvl = {
amount: formatUnits(balance, tokenInfo.decimals),
amountUsd: tvlValue,
name: token.name,
imageUrl: token.imageUrl,
price: price.toString(),
...tokenInfo,
};

tvl.push(assetTvl);
}

// we assume the rounding error is negligible for sorting purposes
tvl.sort((a, b) => Number(b.amountUsd) - Number(a.amountUsd));

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.
* Fetches the token balances for the given addresses and ETH balance.
* Note: The last balance in the returned array is the ETH balance, so the fetch length should be addresses.length + 1.
* @param addresses - An array of addresses for which to fetch the token balances.
* @returns A promise that resolves to an object containing the ETH balance and an array of address balances.
*/
private async fetchTokenBalances(addresses: Address[]): Promise<bigint[]> {
private async fetchTokenBalances(
addresses: Address[],
): Promise<{ ethBalance: bigint; addressesBalance: bigint[] }> {
const returnAbiParams = parseAbiParameters("uint256[]");
const args: ContractConstructorArgs<typeof tokenBalancesAbi> = [
L1_CONTRACTS.SHARED_BRIDGE,
Expand All @@ -103,7 +126,9 @@ export class L1MetricsService {
returnAbiParams,
);

return balances as bigint[];
assert(balances.length === addresses.length + 1, "Invalid balances length");

return { ethBalance: balances[addresses.length]!, addressesBalance: balances.slice(0, -1) };
}

//TODO: Implement getBatchesInfo.
Expand Down
13 changes: 5 additions & 8 deletions libs/metrics/src/types/tvl.type.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
export type TokenTvl = {
amount: number;
amountUsd: number;
name: string;
imageUrl?: string;
};
import { TokenUnion } from "@zkchainhub/shared/tokens/tokens";

export type Tvl = {
[asset: string]: TokenTvl;
export type AssetTvl = Omit<TokenUnion, "coingeckoId"> & {
amount: string;
amountUsd: string;
price: string;
};
83 changes: 68 additions & 15 deletions libs/metrics/test/unit/l1/l1MetricsService.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,43 @@ const mockEvmProviderService = createMock<EvmProviderService>();
const mockPricingService = createMock<IPricingService>();

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 [
{
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,
},
];
},
get tokens() {
return [
{
Expand Down Expand Up @@ -127,29 +164,45 @@ describe("L1MetricsService", () => {

const result = await l1MetricsService.l1Tvl();

expect(result).toMatchObject({
ETH: {
amount: expect.closeTo(123_803.824),
amountUsd: expect.closeTo(393_831_107.68),
expect(result).toHaveLength(3);
expect(result).toEqual([
{
amount: "123803.824374847279970609",
amountUsd: expect.stringContaining("393831107.68"),
price: "3181.09",
name: "Ethereum",
symbol: "ETH",
contractAddress: null,
type: "native",
imageUrl:
"https://coin-images.coingecko.com/coins/images/279/large/ethereum.png?1696501628",
decimals: 18,
},
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),
{
amount: "60841657.140641",
amountUsd: expect.stringContaining("60780815.48"),
price: "0.999",
name: "USDC",
symbol: "USDC",
contractAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
imageUrl:
"https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694",
type: "erc20",
decimals: 6,
},
});
{
amount: "135.63005559",
amountUsd: expect.stringContaining("8969079.94"),
price: "66129",
name: "Wrapped BTC",
symbol: "WBTC",
contractAddress: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
imageUrl:
"https://coin-images.coingecko.com/coins/images/7598/large/wrapped_bitcoin_wbtc.png?1696507857",
type: "erc20",
decimals: 8,
},
]);
expect(mockEvmProviderService.batchRequest).toHaveBeenCalledWith(
tokenBalancesAbi,
tokenBalancesBytecode,
Expand Down
45 changes: 31 additions & 14 deletions libs/shared/src/tokens/tokens.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,38 @@
/**
* The token list in this file was manually crafted and represents the top 50
* tokens by market cap, held by L1 Shared Bridge contract and with data
* present in Coingecko.
* Last updated: 2024-08-03
*
* This list is not exhaustive and can be updated with more tokens as needed.
* Link to the token list: https://etherscan.io/tokenholdings?a=0xD7f9f54194C633F36CCD5F3da84ad4a1c38cB2cB
*/

import { Address } from "abitype";

export type TokenType = {
export type Token<TokenType extends "erc20" | "native"> = {
name: string;
symbol: string;
coingeckoId: string;
type: "erc20" | "native";
contractAddress: Address | null;
type: TokenType;
contractAddress: TokenType extends "erc20" ? Address : null;
decimals: number;
imageUrl?: string;
};

export const tokens: TokenType[] = [
{
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,
},
export type TokenUnion = Token<"erc20"> | Token<"native">;

export const nativeToken: Readonly<Token<"native">> = {
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,
};

export const erc20Tokens: Readonly<Token<"erc20">[]> = [
{
name: "USDC",
symbol: "USDC",
Expand Down Expand Up @@ -489,3 +501,8 @@ export const tokens: TokenType[] = [
decimals: 18,
},
];

export const tokens: Readonly<TokenUnion[]> = [nativeToken, ...erc20Tokens];

export const isNativeToken = (token: TokenUnion): token is Token<"native"> =>
token.type === "native";

0 comments on commit 0d51d95

Please sign in to comment.