-
Notifications
You must be signed in to change notification settings - Fork 0
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: calculate L1 TVL using batch request #37
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export const tokenBalancesBytecode = | ||
"0x608060405234801561001057600080fd5b5060405161063538038061063583398181016040528101906100329190610332565b60008151905060006001826100479190610516565b67ffffffffffffffff811115610086577f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6040519080825280602002602001820160405280156100b45781602001602082028036833780820191505090505b50905060005b828110156101e75760008482815181106100fd577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b6020026020010151905060008173ffffffffffffffffffffffffffffffffffffffff166370a08231886040518263ffffffff1660e01b81526004016101429190610443565b60206040518083038186803b15801561015a57600080fd5b505afa15801561016e573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101929190610386565b9050808484815181106101ce577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b60200260200101818152505082600101925050506100ba565b8473ffffffffffffffffffffffffffffffffffffffff1631828281518110610238577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b602002602001018181525050600082604051602001610257919061045e565b60405160208183030381529060405290506020810180590381f35b6000610285610280846104b1565b610480565b905080838252602082019050828560208602820111156102a457600080fd5b60005b858110156102d457816102ba88826102de565b8452602084019350602083019250506001810190506102a7565b5050509392505050565b6000815190506102ed81610606565b92915050565b600082601f83011261030457600080fd5b8151610314848260208601610272565b91505092915050565b60008151905061032c8161061d565b92915050565b6000806040838503121561034557600080fd5b6000610353858286016102de565b925050602083015167ffffffffffffffff81111561037057600080fd5b61037c858286016102f3565b9150509250929050565b60006020828403121561039857600080fd5b60006103a68482850161031d565b91505092915050565b60006103bb8383610434565b60208301905092915050565b6103d08161056c565b82525050565b60006103e1826104ed565b6103eb8185610505565b93506103f6836104dd565b8060005b8381101561042757815161040e88826103af565b9750610419836104f8565b9250506001810190506103fa565b5085935050505092915050565b61043d8161059e565b82525050565b600060208201905061045860008301846103c7565b92915050565b6000602082019050818103600083015261047881846103d6565b905092915050565b6000604051905081810181811067ffffffffffffffff821117156104a7576104a66105d7565b5b8060405250919050565b600067ffffffffffffffff8211156104cc576104cb6105d7565b5b602082029050602081019050919050565b6000819050602082019050919050565b600081519050919050565b6000602082019050919050565b600082825260208201905092915050565b60006105218261059e565b915061052c8361059e565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03821115610561576105606105a8565b5b828201905092915050565b60006105778261057e565b9050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b61060f8161056c565b811461061a57600080fd5b50565b6106268161059e565b811461063157600080fd5b5056fe"; |
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. | ||||||
|
@@ -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[]; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. reduce |
||||||
|
||||||
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"); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Out of curiosity, why is this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💯 |
||||||
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" | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we can add 2 typeguards |
||||||
? balances[addresses.length] | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this correct ? always returning the same value for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm I had the same doubt 🤔 but I think it's related with the structure of the return value of I'll write a suggestion at that method that might make things easier to understand here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the custom contract for batch requests, requests token balances + native balanceOf(address) which in L1 is eth balance as the last element of balances array, so we always return addresses + 1 (since native token doesn't have a contract itself) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmmmmmm, maybe using the batch contract just for tokens would be a better approach and would make the code more clear. then you will have something like
I would also rename Both |
||||||
: 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[]; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm suggesting this purely from a pure code perspective, I've got no idea about the bussiness logic behind this so feel free to tell me that I'm absolutely wrong lol Trying to make things a little bit more declarative, the
Suggested change
Something like that might make things easier to use: you can change the assertion I've commented on by dropping the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this approach looks quite better and its better than the one that i've pointed out in my prev comment. |
||||||
} | ||||||
|
||||||
//TODO: Implement getBatchesInfo. | ||||||
async getBatchesInfo( | ||||||
_chainId: number, | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./tvl.type"; |
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we add address here ? |
||
}; | ||
|
||
export type Tvl = { | ||
[asset: string]: TokenTvl; | ||
}; |
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(), | ||
|
@@ -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({ | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any special reason to have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is to now where the decimal part starts on the bigint 😅 , do you think this is misleading? |
||
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"); | ||
}); | ||
}); | ||
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lmk if suggestions is what we want |
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"; | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||
contractAddress: string | null; | ||||||||||||||||||||||
contractAddress: Address | null; | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||
decimals: number; | ||||||||||||||||||||||
imageUrl?: string; | ||||||||||||||||||||||
}; | ||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./parseUnits"; |
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 => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. https://viem.sh/docs/utilities/parseUnits#parseunits is this the same ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah you're right, i'll refactor to use: https://viem.sh/docs/utilities/formatUnits |
||
return BigNumber(value.toString()) | ||
.div(10 ** decimals) | ||
.toNumber(); | ||
}; |
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); | ||
}); | ||
}); |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we have a script to generate this? is it necessary to have it as variable? maybe just plain text is good enough, given that we are not getting any typing from this value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for now, i manually copied the bytecode generated on contracts compilation (same as with the abi). i think adding a script that runs after compilation that copies the bytecode into the .ts file is too much at this stage but can be a future enhancement
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
regarding plain text, i didn't follow you here, what are you suggesting? the bytecode as string is needed to be passed as argument of
batchRequest
on EvmProvider methodThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i want to automate this for the cases in which the smart contract changes, so we use the compiled output each time we want to run or deploy or even running the pipeline.
this is kind of hardcoded, i don't like it if we have the contracts that can be compiled on the repo, maybe using the
typechain
version of viem could be an option.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i would have to write a script or copy and move the json with compilation info but i think the json has too much extra info that is not needed. however i think this script should be written on a separate PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sounds good, i will add it to
tech debt