Skip to content
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

tBTC stats on overview page #490

Merged
merged 16 commits into from
Apr 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/components/OutlineListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { FC } from "react"
import { ListItem, ListItemProps } from "@threshold-network/components"

export const OutlineListItem: FC<ListItemProps> = ({ ...props }) => {
return (
<ListItem
display="flex"
justifyContent="space-between"
alignItems="center"
borderColor="gray.100"
borderWidth="1px"
borderStyle="solid"
borderRadius="6px"
py="4"
px="6"
{...props}
/>
)
}
2 changes: 1 addition & 1 deletion src/components/ViewInBlockExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { supportedChainId } from "../utils/getEnvVariable"

export type Chain = "bitcoin" | "ethereum"

const createLinkToBlockExplorerForChain: Record<
export const createLinkToBlockExplorerForChain: Record<
Chain,
(id: string, type: ExplorerDataType) => string
> = {
Expand Down
82 changes: 82 additions & 0 deletions src/components/tBTC/RecentDeposits.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { FC } from "react"
import {
BodySm,
List,
HStack,
ListProps,
LinkBox,
LinkOverlay,
Link,
} from "@threshold-network/components"
import shortenAddress from "../../utils/shortenAddress"
import Identicon from "../Identicon"
import { OutlineListItem } from "../OutlineListItem"
import { InlineTokenBalance } from "../TokenBalance"
import { getRelativeTime } from "../../utils/date"
import { RecentDeposit } from "../../hooks/tbtc/useFetchRecentDeposits"
import ViewInBlockExplorer, {
createLinkToBlockExplorerForChain,
} from "../ViewInBlockExplorer"
import { ExplorerDataType } from "../../utils/createEtherscanLink"

export type RecentDepositsProps = {
deposits: RecentDeposit[]
} & ListProps

export const RecentDeposits: FC<RecentDepositsProps> = ({
deposits,
...restProps
}) => {
return (
<List spacing="1" position="relative" {...restProps}>
{deposits.map(renderRecentDeposit)}
</List>
)
}

const RecentDepositItem: FC<RecentDeposit> = ({
amount,
address,
date,
txHash,
}) => {
return (
<LinkBox as={OutlineListItem}>
<LinkOverlay
// We can't use `ViewInBlockExplorer` or our custom `Link` component
// because `LinkOverlay` component from chakra doesn't pass the
// `isExternal` prop forward so `ViewInBlockExplorer` or `Link`
// component sees this link as an internal so the link will open in the
// same tab and the TS compiler throws an error that `to` prop is
// missing because of conditional props of `Link` component.
as={Link}
textDecoration="none"
_hover={{ textDecoration: "none" }}
color="inherit"
isExternal
href={createLinkToBlockExplorerForChain.ethereum(
txHash,
ExplorerDataType.TRANSACTION
)}
>
<BodySm>
<InlineTokenBalance
tokenAmount={amount}
withSymbol
tokenSymbol="tBTC"
color="brand.500"
/>
</BodySm>
</LinkOverlay>
<HStack spacing="2">
<Identicon address={address} />
<BodySm textStyle="chain-identifier">{shortenAddress(address)}</BodySm>
</HStack>
<BodySm>{getRelativeTime(date)}</BodySm>
</LinkBox>
)
}

const renderRecentDeposit = (item: RecentDeposit) => (
<RecentDepositItem key={item.txHash} {...item} />
)
62 changes: 62 additions & 0 deletions src/components/tBTC/Stats.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { ComponentProps, FC } from "react"
import { BodyMd, Box, BoxProps, H1, H5 } from "@threshold-network/components"
import { InlineTokenBalance } from "../TokenBalance"
import { formatFiatCurrencyAmount } from "../../utils/formatAmount"
import { RecentDeposits, RecentDepositsProps } from "./RecentDeposits"
import { ExternalHref } from "../../enums"
import { RecentDeposit } from "../../hooks/tbtc"
import Link from "../Link"

export type TVLProps = {
tvl: string
tvlInUSD: string
}

export const TVL: FC<TVLProps> = ({ tvl, tvlInUSD }) => {
return (
<>
<BodyMd mb="1.5">TVL</BodyMd>
<H1 textAlign="center">
<InlineTokenBalance tokenAmount={tvl} /> <H5 as="span">tBTC</H5>
</H1>
<BodyMd textAlign="center">
{formatFiatCurrencyAmount(tvlInUSD, "0,00.00")} USD
</BodyMd>
</>
)
}

export type ProtocolHistoryProps = {
deposits: RecentDeposit[]
}

export const ProtocolHistoryTitle: FC<ComponentProps<typeof BodyMd>> = (
props
) => (
<BodyMd mb="3" {...props}>
Protocol History
</BodyMd>
)

export const ProtocolHistoryViewMoreLink: FC<BoxProps> = (props) => (
<Box as="p" mt="3.5" textAlign="center" {...props}>
<Link isExternal href={ExternalHref.tBTCDuneDashboard}>
View on Dune Analytics
</Link>
</Box>
)

export const ProtocolHistoryRecentDeposits: FC<RecentDepositsProps> = ({
deposits,
...restProps
}) => <RecentDeposits deposits={deposits} {...restProps} />

export const ProtocolHistory: FC<ProtocolHistoryProps> = ({ deposits }) => {
return (
<>
<ProtocolHistoryTitle />
<ProtocolHistoryRecentDeposits deposits={deposits} />
<ProtocolHistoryViewMoreLink />
</>
)
}
1 change: 1 addition & 0 deletions src/components/tBTC/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./TakeNoteList"
export * from "./Links"
export * from "./Stats"
1 change: 1 addition & 0 deletions src/enums/externalHref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ export enum ExternalHref {
tBTCRecoveryGuide = "https://github.com/keep-network/tbtc-v2/blob/main/typescript/scripts/README.adoc",
btcConfirmations = "https://en.bitcoin.it/wiki/Confirmation",
mintersAndGuardiansDocs = "https://blog.threshold.network/minters-guardians-and-a-strong-tbtc/",
tBTCDuneDashboard = "https://dune.com/threshold/tbtc",
}
85 changes: 56 additions & 29 deletions src/hooks/__tests__/useFetchTvl.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useFetchTvl } from "../useFetchTvl"
import * as useTokenModule from "../useToken"
import { TokenContext } from "../../contexts/TokenContext"
import * as usdUtils from "../../utils/getUsdBalance"
import { useTBTCv2TokenContract } from "../../web3/hooks/useTBTCv2TokenContract"

jest.mock("../../web3/hooks", () => ({
...(jest.requireActual("../../web3/hooks") as {}),
Expand All @@ -25,6 +26,11 @@ jest.mock("../../web3/hooks", () => ({
useTStakingContract: jest.fn(),
}))

jest.mock("../../web3/hooks/useTBTCv2TokenContract", () => ({
...(jest.requireActual("../../web3/hooks/useTBTCv2TokenContract") as {}),
useTBTCv2TokenContract: jest.fn(),
}))

Comment on lines +29 to +33
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you double-check if all the tests pass? For me they don't (on your branch) but on main all have passed

Copy link
Collaborator Author

@r-czajkowski r-czajkowski Apr 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once we merge main should be all good 🤞

jest.mock("../useETHData", () => ({
...(jest.requireActual("../useETHData") as {}),
useETHData: jest.fn(),
Expand All @@ -35,12 +41,7 @@ describe("Test `useFetchTvl` hook", () => {
contract: {} as any,
usdConversion: 1,
} as any
const tbtcContext = {
contract: {} as any,
usdConversion: 2,
} as any

const tbtcv2Context = {
const tbtcv1Context = {
contract: {} as any,
usdConversion: 2,
} as any
Expand All @@ -54,21 +55,25 @@ describe("Test `useFetchTvl` hook", () => {
contract: {} as any,
usdConversion: 4,
} as any

const tBTCContext = {
contract: {} as any,
usdConversion: 2,
} as any
const mockedKeepTokenStakingContract = { address: "0x1" }
const mockedKeepBondingContract = { address: "0x0" }
const mockedTStakingContract = { address: "0x2" }
const mockedMultiCallContract = { interface: {}, address: "0x3" }
const mockedKeepAssetPoolContract = { interface: {}, address: "0x4" }
const mockedtBTCTokenContract = { interface: {}, address: "0x5" }

const wrapper = ({ children }) => (
<TokenContext.Provider
value={{
[Token.Keep]: keepContext,
[Token.TBTC]: tbtcContext,
[Token.TBTCV2]: tbtcv2Context,
[Token.TBTC]: tbtcv1Context,
[Token.T]: tContext,
[Token.Nu]: nuContext,
[Token.TBTCV2]: tBTCContext,
Comment on lines +73 to +76
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking:

I think we should decide which naming convention we want to use for both tbtc tokens and stick to use. We can either use [tBTC and tBTCv2] or [tBTCv1 and tBTC]. Messing them up here and there might be problematic later.

We don't have to do it in this PR though. It's just something that will have to be resolved sooner or later and the code needs to be checked for those occurences.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed this here #300 as well and #300 covers this. We need to revisit this PR and finally merge.

}}
>
{children}
Expand All @@ -94,6 +99,9 @@ describe("Test `useFetchTvl` hook", () => {
;(useKeepTokenStakingContract as jest.Mock).mockReturnValue(
mockedKeepTokenStakingContract
)
;(useTBTCv2TokenContract as jest.Mock).mockReturnValue(
mockedtBTCTokenContract
)
})

test("should fetch tvl data correctly.", async () => {
Expand All @@ -103,13 +111,15 @@ describe("Test `useFetchTvl` hook", () => {
const coveragePoolTvl = { raw: "300000000000000000000", format: "300.0" }
const keepStaking = { raw: "500000000000000000000", format: "500.0" }
const tStaking = { raw: "600000000000000000000", format: "600.0" }
const tBTC = { raw: "700000000000000000000", format: "700.0" }

const multicallRequestResult = [
ethInKeepBonding.raw,
tbtcTokenTotalSupply.raw,
coveragePoolTvl.raw,
keepStaking.raw,
tStaking.raw,
tBTC.raw,
]

multicallRequest.mockResolvedValue(multicallRequestResult)
Expand All @@ -119,27 +129,30 @@ describe("Test `useFetchTvl` hook", () => {
const spyOnUseToken = jest.spyOn(useTokenModule, "useToken")

const _expectedResult = {
ecdsa: ethInKeepBonding.format * mockedETHData.usdPrice,
tbtc: tbtcTokenTotalSupply.format * tbtcContext.usdConversion,
keepCoveragePool: coveragePoolTvl.format * keepContext.usdConversion,
keepStaking: keepStaking.format * keepContext.usdConversion,
tStaking: tStaking.format * tContext.usdConversion,
ecdsa: +ethInKeepBonding.format * mockedETHData.usdPrice,
tbtc: +tbtcTokenTotalSupply.format * tbtcv1Context.usdConversion,
keepCoveragePool: +coveragePoolTvl.format * keepContext.usdConversion,
keepStaking: +keepStaking.format * keepContext.usdConversion,
tStaking: +tStaking.format * tContext.usdConversion,
tBTC: +tBTC.format * tBTCContext.usdConversion,
}

// `FixedNumber` from `@ethersproject/bignumber` adds trailing zero so we
// need to do the same here.
const expectedResult = {
ecdsa: `${_expectedResult.ecdsa.toString()}.0`,
tbtc: `${_expectedResult.tbtc.toString()}.0`,
tbtcv1: `${_expectedResult.tbtc.toString()}.0`,
keepCoveragePool: `${_expectedResult.keepCoveragePool.toString()}.0`,
keepStaking: `${_expectedResult.keepStaking.toString()}.0`,
tStaking: `${_expectedResult.tStaking.toString()}.0`,
tBTC: `${_expectedResult.tBTC.toString()}.0`,
total: `${
_expectedResult.ecdsa +
_expectedResult.tbtc +
_expectedResult.keepCoveragePool +
_expectedResult.keepStaking +
_expectedResult.tStaking
_expectedResult.tStaking +
_expectedResult.tBTC
}.0`,
}

Expand All @@ -153,6 +166,7 @@ describe("Test `useFetchTvl` hook", () => {
expect(spyOnUseToken).toHaveBeenCalledWith(Token.Keep)
expect(spyOnUseToken).toHaveBeenCalledWith(Token.TBTC)
expect(spyOnUseToken).toHaveBeenCalledWith(Token.T)
expect(spyOnUseToken).toHaveBeenCalledWith(Token.TBTCV2)
expect(useKeepBondingContract).toHaveBeenCalled()
expect(useMulticallContract).toHaveBeenCalled()
expect(useKeepAssetPoolContract).toHaveBeenCalled()
Expand All @@ -166,8 +180,8 @@ describe("Test `useFetchTvl` hook", () => {
args: [mockedKeepBondingContract.address],
},
{
interface: tbtcContext.contract.interface,
address: tbtcContext.contract.address,
interface: tbtcv1Context.contract.interface,
address: tbtcv1Context.contract.address,
method: "totalSupply",
},
{
Expand All @@ -187,43 +201,56 @@ describe("Test `useFetchTvl` hook", () => {
method: "balanceOf",
args: [mockedTStakingContract.address],
},
{
interface: tBTCContext.contract.interface,
address: tBTCContext.contract.address,
method: "totalSupply",
},
])

result.current[1]()

await waitForNextUpdate()

expect(multicallRequest).toHaveBeenCalled()
expect(spyOnFormatUnits).toHaveBeenCalledTimes(
multicallRequestResult.length
)
// The `toUsdBalance` function was called 2x times because it was called
// first on mount for every value and then after fetching on-chain data.

// The `toUsdBalance` and `spyOnFormatUnits` function were called 2x times
// because they were called called first on mount for every value and then
// after fetching on-chain data.
expect(spyOnToUsdBalance).toHaveBeenCalledTimes(
multicallRequestResult.length * 2
)
expect(spyOnFormatUnits).toHaveBeenCalledTimes(
multicallRequestResult.length * 2
)

expect(spyOnToUsdBalance).toHaveBeenNthCalledWith(
6,
7,
ethInKeepBonding.format,
mockedETHData.usdPrice
)
expect(spyOnToUsdBalance).toHaveBeenNthCalledWith(
7,
8,
tbtcTokenTotalSupply.format,
tbtcContext.usdConversion
tbtcv1Context.usdConversion
)
expect(spyOnToUsdBalance).toHaveBeenNthCalledWith(
8,
9,
tBTC.format,
tBTCContext.usdConversion
)
expect(spyOnToUsdBalance).toHaveBeenNthCalledWith(
10,
coveragePoolTvl.format,
keepContext.usdConversion
)
expect(spyOnToUsdBalance).toHaveBeenNthCalledWith(
9,
11,
keepStaking.format,
keepContext.usdConversion
)
expect(spyOnToUsdBalance).toHaveBeenNthCalledWith(
10,
12,
tStaking.format,
tContext.usdConversion
)
Expand Down
Loading