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

feat: price changes #1735

Merged
merged 29 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7f93726
feat: tokenRates number => object with price, market cap and 24h change
0xKheops Nov 28, 2024
b67f035
Merge branch 'dev' into feat/price-changes
0xKheops Dec 3, 2024
109bbfc
feat: asset price component
0xKheops Dec 4, 2024
1e07c00
feat: price colors
0xKheops Dec 4, 2024
6e3f423
feat: asset prices in popup assets table
0xKheops Dec 4, 2024
d967803
feat: change24h on balances
0xKheops Dec 5, 2024
6b267ed
feat: adjust price colors
0xKheops Dec 5, 2024
efc2c5e
feat: asset price change in header of popup asset details
0xKheops Dec 5, 2024
246669d
wip: chart
0xKheops Dec 5, 2024
97911b8
wip: chart
0xKheops Dec 6, 2024
e650a5c
wip: asset price chart
0xKheops Dec 9, 2024
5b25914
wip: chart
0xKheops Dec 10, 2024
6bc2496
Merge branch 'dev' into feat/price-changes
0xKheops Dec 10, 2024
0ed8311
fix: merge
0xKheops Dec 10, 2024
35cc91b
feat: hover value management
0xKheops Dec 10, 2024
8c35508
chore: changeset
0xKheops Dec 10, 2024
1900cb1
feat: chart adjustments
0xKheops Dec 10, 2024
23fa15f
feat: asset details pages adjustments
0xKheops Dec 10, 2024
ef09df8
feat: 1 block per token on asset details pages
0xKheops Dec 10, 2024
65242ba
fix: formatPrice
0xKheops Dec 10, 2024
8ba452e
fix: dont render table header if no balance
0xKheops Dec 11, 2024
5166508
chore: fix file name
0xKheops Dec 11, 2024
982abc5
fix: adjustments
0xKheops Dec 11, 2024
69c584a
chore: cleanup
0xKheops Dec 11, 2024
38c26cc
fix: 25% margin at chart bottom to prevent overlap with buttons
0xKheops Dec 12, 2024
275a4f6
fix: db migration for token rates
0xKheops Dec 16, 2024
fb65c6a
fix: comment
0xKheops Dec 16, 2024
3341c34
chore: delete unused file
0xKheops Dec 16, 2024
a895bfc
Merge branch 'dev' into feat/price-changes
0xKheops Dec 16, 2024
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
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 @@ -70,6 +70,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 All @@ -35,7 +36,7 @@ export const PortfolioRoutes = () => (
<BuyTokensOpener />
<PortfolioContainer>
{/* 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 @@ -54,3 +55,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