Skip to content

Commit

Permalink
feat: price changes (#1735)
Browse files Browse the repository at this point in the history
* feat: tokenRates number => object with price, market cap and 24h change

* feat: asset price component

* feat: price colors

* feat: asset prices in popup assets table

* feat: change24h on balances

* feat: adjust price colors

* feat: asset price change in header of popup asset details

* wip: chart

* wip: chart

* wip: asset price chart

* wip: chart

* fix: merge

* feat: hover value management

* chore: changeset

* feat: chart adjustments

* feat: asset details pages adjustments

* feat: 1 block per token on asset details pages

* fix: formatPrice

* fix: dont render table header if no balance

* chore: fix file name

* fix: adjustments

* chore: cleanup

* fix: 25% margin at chart bottom to prevent overlap with buttons

* fix: db migration for token rates

* fix: comment

* chore: delete unused file
  • Loading branch information
0xKheops authored Dec 16, 2024
1 parent 0ee3c9c commit 84dd6ac
Show file tree
Hide file tree
Showing 32 changed files with 2,487 additions and 520 deletions.
5 changes: 5 additions & 0 deletions .changeset/hip-windows-whisper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@talismn/balances": minor
---

update to new tokenRates shape
5 changes: 5 additions & 0 deletions .changeset/old-gorillas-compete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@talismn/token-rates": major
---

BREAKING - add market cap and 24h change
5 changes: 5 additions & 0 deletions .changeset/thirty-elephants-breathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@talismn/util": minor
---

formatPrice utility
1 change: 1 addition & 0 deletions apps/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"bignumber.js": "^9.1.2",
"blueimp-md5": "2.19.0",
"buffer": "^6.0.3",
"chart.js": "^4.4.7",
"check-password-strength": "^2.0.10",
"date-fns": "^4.1.0",
"dcent-web-connector": "^0.16.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { Token } from "@talismn/chaindata-provider"
import { Token, TokenId } from "@talismn/chaindata-provider"
import { SendIcon } from "@talismn/icons"
import { t } from "i18next"
import { uniq } from "lodash"
import { FC, useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom"
import { Tooltip, TooltipContent, TooltipTrigger } from "talisman-ui"

import { Balances } from "@extension/core"
import { Breadcrumb } from "@talisman/components/Breadcrumb"
import { NavigateWithQuery } from "@talisman/components/NavigateWithQuery"
import { Fiat } from "@ui/domains/Asset/Fiat"
import { TokenLogo } from "@ui/domains/Asset/TokenLogo"
import { AssetPriceChart } from "@ui/domains/Asset/AssetPriceChart"
import { DashboardAssetDetails } from "@ui/domains/Portfolio/AssetDetails"
import { DashboardPortfolioHeader } from "@ui/domains/Portfolio/DashboardPortfolioHeader"
import { PortfolioToolbarButton } from "@ui/domains/Portfolio/PortfolioToolbarButton"
import { Statistics } from "@ui/domains/Portfolio/Statistics"
import { useDisplayBalances } from "@ui/domains/Portfolio/useDisplayBalances"
Expand All @@ -23,7 +23,6 @@ import {
import { useAnalytics } from "@ui/hooks/useAnalytics"
import { useNavigateWithQuery } from "@ui/hooks/useNavigateWithQuery"
import { useSendFundsPopup } from "@ui/hooks/useSendFundsPopup"
import { useUniswapV2LpTokenTotalValueLocked } from "@ui/hooks/useUniswapV2LpTokenTotalValueLocked"
import { usePortfolio, useSetting } from "@ui/state"

const HeaderRow: FC<{
Expand All @@ -33,6 +32,8 @@ const HeaderRow: FC<{
const { t } = useTranslation()
const canHaveLockedState = Boolean(token?.chain?.id)

if (summary.totalTokens.isZero()) return null

return (
<div className="text-body-secondary bg-grey-850 rounded p-8 text-left text-base">
<div className="grid grid-cols-[40%_30%_30%]">
Expand Down Expand Up @@ -103,43 +104,24 @@ const SendFundsButton: FC<{ symbol: string }> = ({ symbol }) => {
}

const TokenBreadcrumb: FC<{
balances: Balances
symbol: string
token: Token | undefined
rate: number | null | undefined
}> = ({ balances, symbol, token, rate }) => {
}> = ({ symbol }) => {
const { t } = useTranslation()

const navigate = useNavigateWithQuery()

const isUniswapV2LpToken = token?.type === "evm-uniswapv2"
const tvl = useUniswapV2LpTokenTotalValueLocked(token, rate, balances)

const items = useMemo(() => {
return [
{
label: t("All Tokens"),
onClick: () => navigate("/portfolio/tokens"),
},
{
label: (
<div className="flex items-center gap-2">
<TokenLogo tokenId={token?.id} className="text-md" />
<div className="text-body font-bold">{token?.symbol ?? symbol}</div>
{isUniswapV2LpToken && typeof tvl === "number" && (
<div className="text-body-secondary whitespace-nowrap">
<Fiat amount={tvl} /> <span className="text-tiny">TVL</span>
</div>
)}
{!isUniswapV2LpToken && typeof rate === "number" && (
<Fiat amount={rate} className="text-body-secondary" />
)}
</div>
),
label: <div className="text-body font-bold">{symbol}</div>,
onClick: undefined,
},
]
}, [t, token?.id, token?.symbol, symbol, isUniswapV2LpToken, tvl, rate, navigate])
}, [t, symbol, navigate])

return (
<div className="flex h-20 items-center justify-between">
Expand All @@ -149,10 +131,9 @@ const TokenBreadcrumb: FC<{
)
}

export const PortfolioAsset = () => {
const usePortfolioAsset = () => {
const { symbol } = useParams()
const { allBalances } = usePortfolio()
const { pageOpenEvent } = useAnalytics()
const [isTestnet] = useSetting("useTestnets")

const balances = useMemo(
Expand All @@ -165,6 +146,13 @@ export const PortfolioAsset = () => {
const { token, rate, summary } = useTokenBalancesSummary(balances)
const balancesToDisplay = useDisplayBalances(balances)

return { symbol, token, rate, balances, balancesToDisplay, summary }
}

export const PortfolioAsset = () => {
const { symbol, token, balancesToDisplay, summary } = usePortfolioAsset()
const { pageOpenEvent } = useAnalytics()

useEffect(() => {
pageOpenEvent("portfolio asset", { symbol })
}, [pageOpenEvent, symbol])
Expand All @@ -173,9 +161,25 @@ export const PortfolioAsset = () => {

return (
<>
<TokenBreadcrumb token={token} rate={rate} balances={balances} symbol={symbol} />
<TokenBreadcrumb symbol={symbol} />
<HeaderRow token={token} summary={summary} />
<DashboardAssetDetails balances={balancesToDisplay} symbol={symbol} />
</>
)
}

export const PortfolioAssetHeader = () => {
const { balances } = usePortfolioAsset()

// all tokenIds that match the symbol and have a coingeckoId
const tokenIds = useMemo(() => {
return uniq(balances.each.filter((b) => !!b.token?.coingeckoId).map((b) => b.token?.id)).filter(
Boolean,
) as TokenId[]
}, [balances])

// no chart to display, use default header
if (!tokenIds.length) return <DashboardPortfolioHeader />

return <AssetPriceChart tokenIds={tokenIds} variant="large" />
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,15 @@ const PortfolioAccountCheck: FC<PropsWithChildren> = ({ children }) => {
return <>{children}</>
}

export const PortfolioLayout: FC<PropsWithChildren & { toolbar?: ReactNode }> = ({
toolbar,
children,
}) => {
export const PortfolioLayout: FC<
PropsWithChildren & { toolbar?: ReactNode; header?: ReactNode }
> = ({ header, toolbar, children }) => {
return (
<div className="relative flex w-full flex-col gap-6 pb-12">
<Suspense
fallback={<SuspenseTracker name="DashboardPortfolioLayout.PortfolioAccountCheck" />}
>
<DashboardPortfolioHeader />
{header ?? <DashboardPortfolioHeader />}
<PortfolioAccountCheck>
<div className="flex h-16 w-full items-center justify-between gap-8 overflow-hidden">
<PortfolioTabs className="text-md my-0 h-14 w-auto font-bold" />
Expand Down
12 changes: 10 additions & 2 deletions apps/extension/src/ui/apps/dashboard/routes/Portfolio/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { Route, Routes, useSearchParams } from "react-router-dom"

import { NavigateWithQuery } from "@talisman/components/NavigateWithQuery"
import { useBuyTokensModal } from "@ui/domains/Asset/Buy/useBuyTokensModal"
import { DashboardPortfolioHeader } from "@ui/domains/Portfolio/DashboardPortfolioHeader"
import { PortfolioContainer } from "@ui/domains/Portfolio/PortfolioContainer"
import { PortfolioToolbarNfts } from "@ui/domains/Portfolio/PortfolioToolbarNfts"
import { PortfolioToolbarTokens } from "@ui/domains/Portfolio/PortfolioToolbarTokens"

import { DashboardLayout } from "../../layout"
import { PortfolioAsset } from "./PortfolioAsset"
import { PortfolioAsset, PortfolioAssetHeader } from "./PortfolioAsset"
import { PortfolioAssets } from "./PortfolioAssets"
import { PortfolioNftCollection } from "./PortfolioNftCollection"
import { PortfolioNfts } from "./PortfolioNfts"
Expand Down Expand Up @@ -36,7 +37,7 @@ export const PortfolioRoutes = () => (
<BuyTokensOpener />

{/* share layout to prevent tabs flickering */}
<PortfolioLayout toolbar={<PortfolioToolbar />}>
<PortfolioLayout toolbar={<PortfolioToolbar />} header={<PortfolioHeader />}>
<Routes>
<Route path="tokens/:symbol" element={<PortfolioAsset />} />
<Route path="nfts/:collectionId" element={<PortfolioNftCollection />} />
Expand All @@ -55,3 +56,10 @@ const PortfolioToolbar = () => (
<Route path="nfts" element={<PortfolioToolbarNfts />} />
</Routes>
)

const PortfolioHeader = () => (
<Routes>
<Route path="tokens/:symbol" element={<PortfolioAssetHeader />} />
<Route path="*" element={<DashboardPortfolioHeader />} />
</Routes>
)
44 changes: 16 additions & 28 deletions apps/extension/src/ui/apps/popup/pages/Portfolio/PortfolioAsset.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
import { TokenId } from "@talismn/chaindata-provider"
import { ChevronLeftIcon } from "@talismn/icons"
import { uniq } from "lodash"
import { useCallback, useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { Navigate, useNavigate, useParams } from "react-router-dom"
import { IconButton } from "talisman-ui"

import { Balances } from "@extension/core"
import { AssetPriceChart } from "@ui/domains/Asset/AssetPriceChart"
import { Fiat } from "@ui/domains/Asset/Fiat"
import { TokenLogo } from "@ui/domains/Asset/TokenLogo"
import { PopupAssetDetails } from "@ui/domains/Portfolio/AssetDetails"
import { useDisplayBalances } from "@ui/domains/Portfolio/useDisplayBalances"
import { usePortfolioNavigation } from "@ui/domains/Portfolio/usePortfolioNavigation"
import { useTokenBalancesSummary } from "@ui/domains/Portfolio/useTokenBalancesSummary"
import { useAnalytics } from "@ui/hooks/useAnalytics"
import { useUniswapV2LpTokenTotalValueLocked } from "@ui/hooks/useUniswapV2LpTokenTotalValueLocked"
import { useBalances, usePortfolio, useSelectedCurrency, useSetting } from "@ui/state"

const PageContent = ({ balances, symbol }: { balances: Balances; symbol: string }) => {
const navigate = useNavigate()
const balancesToDisplay = useDisplayBalances(balances)
const currency = useSelectedCurrency()
const { token, rate } = useTokenBalancesSummary(balancesToDisplay)

const handleBackBtnClick = useCallback(() => navigate(-1), [navigate])

Expand All @@ -28,39 +27,28 @@ const PageContent = ({ balances, symbol }: { balances: Balances; symbol: string
[balancesToDisplay.sum, currency],
)

const { t } = useTranslation()
const tokenIds = useMemo(
() => uniq(balancesToDisplay.each.map((b) => b.token?.id)).filter(Boolean) as TokenId[],
[balancesToDisplay],
)

const isUniswapV2LpToken = token?.type === "evm-uniswapv2"
const tvl = useUniswapV2LpTokenTotalValueLocked(token, rate, balances)
const { t } = useTranslation()

return (
<>
<div className="flex w-full items-center gap-4">
<div className="text-body flex h-12 w-full items-center gap-4 text-base font-bold">
<IconButton onClick={handleBackBtnClick}>
<ChevronLeftIcon />
</IconButton>
<div className="shrink-0 text-2xl">
<TokenLogo tokenId={token?.id} />
</div>
<div className="flex grow flex-col gap-1 overflow-hidden pl-2 text-sm">
<div className="text-body-secondary flex justify-between">
<div>{symbol}</div>
<div>{t("Total")}</div>
</div>
<div className="text-md flex justify-between font-bold">
{isUniswapV2LpToken && typeof tvl === "number" && (
<Fiat className="overflow-hidden text-ellipsis whitespace-nowrap" amount={tvl} />
)}
{!isUniswapV2LpToken && typeof rate === "number" && (
<Fiat className="overflow-hidden text-ellipsis whitespace-nowrap" amount={rate} />
)}
<div>
<Fiat amount={total} isBalance />
</div>
</div>
<div className="shrink-0">{symbol}</div>
<div className="flex grow items-center justify-end gap-3">
<div className="text-body-secondary text-sm">{t("Total")}</div>
<Fiat amount={total} isBalance />
</div>
</div>
<div className="py-12">

<div className="py-4">
<AssetPriceChart tokenIds={tokenIds} variant="small" className="mb-8" />
<PopupAssetDetails balances={balancesToDisplay} symbol={symbol} />
</div>
</>
Expand Down
86 changes: 86 additions & 0 deletions apps/extension/src/ui/domains/Asset/AssetPrice.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { bind } from "@react-rxjs/core"
import { TokenId } from "@talismn/chaindata-provider"
import { classNames, formatPrice } from "@talismn/util"
import { FC } from "react"
import { combineLatest, map } from "rxjs"
import { Tooltip, TooltipContent, TooltipTrigger } from "talisman-ui"

import { getTokenRates$, selectedCurrency$ } from "@ui/state"

const [useDisplayAssetPrice] = bind((tokenId: TokenId | null | undefined) =>
combineLatest([getTokenRates$(tokenId), selectedCurrency$]).pipe(
map(([rates, currency]) => {
const rate = rates?.[currency]
if (!rate) return null

const compact = formatPrice(rate.price, currency, true)

const full = formatPrice(rate.price, currency, false)

const rawChange24h = rate.change24h
? new Intl.NumberFormat(undefined, {
minimumFractionDigits: 1,
style: "percent",
signDisplay: "always",
}).format(rate.change24h / 100)
: undefined

// we dont want a sign (which is used for color check) if change displays as +0.0% or -0.0%
const change24h = rawChange24h?.length
? rawChange24h.slice(1) === "0.0%"
? "0.0%"
: rawChange24h
: undefined

return {
compact,
full,
change24h,
}
}),
),
)

export const AssetPrice: FC<{
tokenId: TokenId | null | undefined
as?: "div" | "span"
className?: string
priceClassName?: string
changeClassName?: string
noTooltip?: boolean
noChange?: boolean
}> = ({
as: Container = "div",
tokenId,
noTooltip,
noChange,
className,
priceClassName,
changeClassName,
}) => {
const price = useDisplayAssetPrice(tokenId)

if (!price) return null

return (
<Tooltip placement="bottom-start">
<TooltipTrigger asChild>
<Container className={classNames("whitespace-nowrap", className)}>
<span className={priceClassName}>{price.compact} </span>
{!noChange && price.change24h ? (
<span
className={classNames(
price.change24h.startsWith("+") && "text-price-up",
price.change24h.startsWith("-") && "text-price-down",
changeClassName,
)}
>
{price.change24h}
</span>
) : null}
</Container>
</TooltipTrigger>
{!noTooltip && <TooltipContent>{price.full}</TooltipContent>}
</Tooltip>
)
}
Loading

0 comments on commit 84dd6ac

Please sign in to comment.