From fa0630606016cb93b28eca13d815af74c92b90de Mon Sep 17 00:00:00 2001 From: Martin CAYUELAS <112866305+mcayuelas-ledger@users.noreply.github.com> Date: Fri, 31 May 2024 16:21:13 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20[SUPPORT]=20:=20Rework=20M?= =?UTF-8?q?arket=20with=20ReactQuery=20in=20LLM=20+=20CVS=20v3=20on=20LL?= =?UTF-8?q?=20(#6428)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * WIP * WIP 2 * Add tool in settings * Remove MarketDataProvider from LLD * WIP 3 * Clean code 🧹 * fix some lint checks * Fix Search and LiveCompatible filter * 🧹 * fix some bugs * isLiveSupported wasn't used in codebase in both LLM/LLD * Reorg files * Reorg files * Fix BreadCrump * fix loader * Enrich market store * fix type in store * Reorg hook's content * Fix search and stared api call + Countervalue/range in MarketCoin + fix e2E tests * Add FF refreshTime rate for LLD * test(lld): update screenshots (ubuntu-latest) lld, test, screenshot * test(lld): update screenshots (ubuntu-latest) lld, test, screenshot * Fix some reviews comments * Mask graphs * remove darwin * test(lld): update screenshots (ubuntu-latest) lld, test, screenshot * Add Mask EthStacking * fix marketCoin screenshot * test(lld): update screenshots (ubuntu-latest) lld, test, screenshot * Add peerDependency * Add changeset * Only 1 call for range * Mask items + fix screenshots * test(lld): update screenshots (ubuntu-latest) lld, test, screenshot * Mask itemson ethStacking test * test(lld): update screenshots (ubuntu-latest) lld, test, screenshot * Mask items on Coin detail page * Refresh on Scroll or on position page every x time * Add some test on utils * test(lld): update screenshots (ubuntu-latest) lld, test, screenshot * Fix coming back to page and don't refetch everything * test(lld): update screenshots (ubuntu-latest) lld, test, screenshot * test(lld): update screenshots (ubuntu-latest) lld, test, screenshot * Fix refetch * Fix double fetch * Fix color in chart * test(lld): update screenshots (ubuntu-latest) lld, test, screenshot * WIP * Rework Store for Market * WIP * Add FF refresh in LLM * Fix Market + improve fetch * WIP ON detail Coin * Add useMArket hook * WIP * WIP * Update hooks * Remove unused dependency * Add refresh on scroll and evry X time * revert changes on this file * Reorg files * Fix typecheck * Remove unused stuff * Fix typecheck * Fix Starred coins * Fix double loading component + Starred Empty State * WIP FIX e2e * fix props * Fix E2e * fix e2e * Fix types * Fix some UI details in MarketDetail page when loading * fix Merge * Fix CounterValu in Stats * (chore): [LL] CVS v3 migration on Market (#6850) * WIP for LLD * WIP LLM * WIP LLM 2 * Fix color in graph llm * fix lint + remove unesed stuff + handle starred/liveCoin * add ids fields * Add order in LLD * Order in LLM + fix some UI bugs * Order in LLD * Add icons * remove log * Improve stuffs * Chnagesets * Fix unimported * Fix Tests LLD * test(lld): update screenshots (ubuntu-latest) lld, test, screenshot * fix Market integration test * Fix page fetched * Fix reviews * Fix lint --------- Co-authored-by: live-github-bot[bot] <105061298+live-github-bot[bot]@users.noreply.github.com> * Fix price null issue + clean some code * FIX LIVE-11870 * Fix lint * test(lld): update screenshots (ubuntu-latest) lld, test, screenshot --------- Co-authored-by: live-github-bot[bot] <105061298+live-github-bot[bot]@users.noreply.github.com> --- .changeset/empty-bags-worry.md | 5 + .changeset/few-radios-eat.md | 5 + .changeset/moody-cherries-float.md | 5 + .changeset/odd-bees-matter.md | 5 + .changeset/red-clouds-care.md | 5 + .changeset/shiny-fireants-chew.md | 5 + .../src/renderer/actions/market.ts | 2 +- .../src/renderer/reducers/market.ts | 6 +- .../useMarketPerformanceWidget.test.ts | 2 +- .../components/WidgetList.tsx | 2 +- .../MarketPerformanceWidget/types.ts | 2 +- .../useMarketPerformanceWidget.ts | 2 +- .../MarketPerformanceWidget/utils/index.ts | 2 +- .../MarketCoin/components/MarketCoinChart.tsx | 8 +- .../MarketCoin/components/MarketInfo.tsx | 11 +- .../screens/market/MarketCoin/index.tsx | 15 +- .../MarketList/components/MarketItemChart.tsx | 2 +- .../MarketList/components/MarketRowItem.tsx | 23 +- .../components/NoCryptoPlaceholder.tsx | 2 +- .../screens/market/MarketList/index.tsx | 23 +- .../market/components/SideDrawerFilter.tsx | 2 +- .../market/components/SortTableCell.tsx | 10 +- .../screens/market/hooks/useMarket.ts | 55 +- .../screens/market/hooks/useMarketActions.ts | 2 +- .../screens/market/hooks/useMarketCoin.ts | 22 +- .../renderer/screens/market/utils/index.ts | 8 + .../market-btc-page-linux.png | Bin 62149 -> 62722 bytes .../market-page-search-bitcoin-linux.png | Bin 75135 -> 73033 bytes .../__mocks__/api/market/markets.json | 1276 ++++++++++------- .../__tests__/handlers/market.ts | 7 +- .../__tests__/test-renderer.tsx | 2 + .../e2e/models/market/marketPage.ts | 2 +- apps/ledger-live-mobile/src/AppProviders.tsx | 3 +- apps/ledger-live-mobile/src/actions/market.ts | 20 + .../src/actions/settings.ts | 26 +- apps/ledger-live-mobile/src/actions/types.ts | 40 +- .../WalletTab/CollapsibleHeaderFlatList.tsx | 2 +- apps/ledger-live-mobile/src/db.ts | 1 + apps/ledger-live-mobile/src/index.tsx | 1 - .../src/locales/en/common.json | 15 +- .../changeCurrency.integration.test.tsx | 4 +- .../setFavorites.integration.test.tsx | 16 +- .../Market/__integrations__/shared.tsx | 8 +- .../MarketDataProviderWrapper/index.tsx | 60 - .../Market/components/MarketRowItem/index.tsx | 18 +- .../features/Market/hooks/useMarket.ts | 40 + .../Market/hooks/useMarketCoinData.ts | 38 + .../useMarketCurrencySelectViewModel.ts | 17 +- .../components/MarketGraph/index.tsx | 41 +- .../components/MarketStats/index.tsx | 155 +- .../Market/screens/MarketDetail/index.tsx | 47 +- .../MarketDetail/useMarketDetailViewModel.tsx | 87 +- .../components/BottomSection/index.tsx | 123 +- .../useBottomSectionViewModel.ts | 67 +- .../MarketList/components/ListEmpty/index.tsx | 6 +- .../MarketList/components/ListRow/index.tsx | 8 +- .../components/SearchHeader/index.tsx | 5 +- .../MarketList/components/SortBadge/index.tsx | 13 +- .../Market/screens/MarketList/index.tsx | 70 +- .../MarketList/useMarketListViewModel.ts | 148 +- .../newArch/features/Market/utils/index.ts | 21 +- apps/ledger-live-mobile/src/reducers/index.ts | 2 + .../ledger-live-mobile/src/reducers/market.ts | 59 + .../src/reducers/settings.ts | 68 +- apps/ledger-live-mobile/src/reducers/types.ts | 15 +- .../WalletCentricAsset/AssetMarketSection.tsx | 21 +- .../WalletCentricSections/MarketPrice.tsx | 18 +- libs/ledger-live-common/.unimportedrc.json | 16 +- .../src/featureFlags/defaultFeatures.ts | 6 + .../src/market/MarketDataProvider.tsx | 403 ------ .../src/market/api/api.mock.ts | 403 ------ libs/ledger-live-common/src/market/api/api.ts | 310 ---- .../src/market/api/index.ts | 138 ++ .../{v2 => hooks}/useMarketDataProvider.ts | 67 +- .../{v2 => hooks}/useMarketPerformers.ts | 8 +- .../src/market/utils/currencyFormatter.ts | 60 +- .../src/market/utils/index.ts | 34 + .../src/market/{v2 => utils}/queryKeys.ts | 0 .../src/market/utils/rangeFormatter.ts | 15 - .../src/market/{v2 => utils}/timers.ts | 0 .../src/market/{ => utils}/types.ts | 86 +- .../packages/types-live/src/feature.ts | 4 + libs/ui/packages/icons/src/svg/graph-asc.svg | 3 + libs/ui/packages/icons/src/svg/graph-desc.svg | 3 + 84 files changed, 1926 insertions(+), 2431 deletions(-) create mode 100644 .changeset/empty-bags-worry.md create mode 100644 .changeset/few-radios-eat.md create mode 100644 .changeset/moody-cherries-float.md create mode 100644 .changeset/odd-bees-matter.md create mode 100644 .changeset/red-clouds-care.md create mode 100644 .changeset/shiny-fireants-chew.md create mode 100644 apps/ledger-live-mobile/src/actions/market.ts delete mode 100644 apps/ledger-live-mobile/src/newArch/features/Market/components/MarketDataProviderWrapper/index.tsx create mode 100644 apps/ledger-live-mobile/src/newArch/features/Market/hooks/useMarket.ts create mode 100644 apps/ledger-live-mobile/src/newArch/features/Market/hooks/useMarketCoinData.ts create mode 100644 apps/ledger-live-mobile/src/reducers/market.ts delete mode 100644 libs/ledger-live-common/src/market/MarketDataProvider.tsx delete mode 100644 libs/ledger-live-common/src/market/api/api.mock.ts delete mode 100644 libs/ledger-live-common/src/market/api/api.ts create mode 100644 libs/ledger-live-common/src/market/api/index.ts rename libs/ledger-live-common/src/market/{v2 => hooks}/useMarketDataProvider.ts (71%) rename libs/ledger-live-common/src/market/{v2 => hooks}/useMarketPerformers.ts (81%) create mode 100644 libs/ledger-live-common/src/market/utils/index.ts rename libs/ledger-live-common/src/market/{v2 => utils}/queryKeys.ts (100%) delete mode 100644 libs/ledger-live-common/src/market/utils/rangeFormatter.ts rename libs/ledger-live-common/src/market/{v2 => utils}/timers.ts (100%) rename libs/ledger-live-common/src/market/{ => utils}/types.ts (83%) create mode 100644 libs/ui/packages/icons/src/svg/graph-asc.svg create mode 100644 libs/ui/packages/icons/src/svg/graph-desc.svg diff --git a/.changeset/empty-bags-worry.md b/.changeset/empty-bags-worry.md new file mode 100644 index 000000000000..fea365a89110 --- /dev/null +++ b/.changeset/empty-bags-worry.md @@ -0,0 +1,5 @@ +--- +"live-mobile": patch +--- + +Replace old MarketDataProvider with new implem and TanStackQuery diff --git a/.changeset/few-radios-eat.md b/.changeset/few-radios-eat.md new file mode 100644 index 000000000000..3c30dc4c9f64 --- /dev/null +++ b/.changeset/few-radios-eat.md @@ -0,0 +1,5 @@ +--- +"live-mobile": patch +--- + +New MArket Api usage diff --git a/.changeset/moody-cherries-float.md b/.changeset/moody-cherries-float.md new file mode 100644 index 000000000000..5614df9830a1 --- /dev/null +++ b/.changeset/moody-cherries-float.md @@ -0,0 +1,5 @@ +--- +"ledger-live-desktop": patch +--- + +Replace old import with new paths diff --git a/.changeset/odd-bees-matter.md b/.changeset/odd-bees-matter.md new file mode 100644 index 000000000000..a40a575d1fe8 --- /dev/null +++ b/.changeset/odd-bees-matter.md @@ -0,0 +1,5 @@ +--- +"ledger-live-desktop": patch +--- + +New MArket API usage diff --git a/.changeset/red-clouds-care.md b/.changeset/red-clouds-care.md new file mode 100644 index 000000000000..3ec77a320bbf --- /dev/null +++ b/.changeset/red-clouds-care.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/live-common": patch +--- + +Reorg files diff --git a/.changeset/shiny-fireants-chew.md b/.changeset/shiny-fireants-chew.md new file mode 100644 index 000000000000..b61377e59f2b --- /dev/null +++ b/.changeset/shiny-fireants-chew.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/live-common": minor +--- + +Usage of CVS v3 instead of V2 on Market part diff --git a/apps/ledger-live-desktop/src/renderer/actions/market.ts b/apps/ledger-live-desktop/src/renderer/actions/market.ts index e027c41e169b..733993889dff 100644 --- a/apps/ledger-live-desktop/src/renderer/actions/market.ts +++ b/apps/ledger-live-desktop/src/renderer/actions/market.ts @@ -1,4 +1,4 @@ -import { MarketListRequestParams } from "@ledgerhq/live-common/market/types"; +import { MarketListRequestParams } from "@ledgerhq/live-common/market/utils/types"; export const setMarketOptions = (payload: MarketListRequestParams) => ({ type: "MARKET_SET_VALUES", diff --git a/apps/ledger-live-desktop/src/renderer/reducers/market.ts b/apps/ledger-live-desktop/src/renderer/reducers/market.ts index 543d6385ea3d..38cef3c991f8 100644 --- a/apps/ledger-live-desktop/src/renderer/reducers/market.ts +++ b/apps/ledger-live-desktop/src/renderer/reducers/market.ts @@ -1,6 +1,6 @@ import { handleActions } from "redux-actions"; import { Handlers } from "./types"; -import { MarketListRequestParams } from "@ledgerhq/live-common/market/types"; +import { MarketListRequestParams, Order } from "@ledgerhq/live-common/market/utils/types"; export type MarketState = { marketParams: MarketListRequestParams; @@ -11,10 +11,8 @@ const initialState: MarketState = { marketParams: { range: "24h", limit: 50, - ids: [], starred: [], - orderBy: "market_cap", - order: "desc", + order: Order.MarketCapDesc, search: "", liveCompatible: false, page: 1, diff --git a/apps/ledger-live-desktop/src/renderer/screens/dashboard/MarketPerformanceWidget/__tests__/useMarketPerformanceWidget.test.ts b/apps/ledger-live-desktop/src/renderer/screens/dashboard/MarketPerformanceWidget/__tests__/useMarketPerformanceWidget.test.ts index 2a75b10bf95f..2e41ab179666 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/dashboard/MarketPerformanceWidget/__tests__/useMarketPerformanceWidget.test.ts +++ b/apps/ledger-live-desktop/src/renderer/screens/dashboard/MarketPerformanceWidget/__tests__/useMarketPerformanceWidget.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it } from "@jest/globals"; import { Order } from "../types"; -import { MarketItemPerformer } from "@ledgerhq/live-common/market/types"; +import { MarketItemPerformer } from "@ledgerhq/live-common/market/utils/types"; import { getChangePercentage, getSlicedList } from "../utils"; const createElem = (change: number): MarketItemPerformer => ({ diff --git a/apps/ledger-live-desktop/src/renderer/screens/dashboard/MarketPerformanceWidget/components/WidgetList.tsx b/apps/ledger-live-desktop/src/renderer/screens/dashboard/MarketPerformanceWidget/components/WidgetList.tsx index 29454d4851be..30ffb0240374 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/dashboard/MarketPerformanceWidget/components/WidgetList.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/dashboard/MarketPerformanceWidget/components/WidgetList.tsx @@ -86,7 +86,7 @@ function WidgetRow({ data, index, isFirst, range }: PropsBodyElem) { {!data.price ? "-" : counterValueFormatter({ - value: Number(parseFloat(String(data.price)).toFixed(data.price > 1 ? 2 : 8)), + value: Number(parseFloat(String(data.price)).toFixed(data.price > 1 ? 2 : 6)), currency: counterValueCurrency.ticker, locale, })} diff --git a/apps/ledger-live-desktop/src/renderer/screens/dashboard/MarketPerformanceWidget/types.ts b/apps/ledger-live-desktop/src/renderer/screens/dashboard/MarketPerformanceWidget/types.ts index c30e16236fef..21a009dfd188 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/dashboard/MarketPerformanceWidget/types.ts +++ b/apps/ledger-live-desktop/src/renderer/screens/dashboard/MarketPerformanceWidget/types.ts @@ -1,4 +1,4 @@ -import { MarketItemPerformer } from "@ledgerhq/live-common/market/types"; +import { MarketItemPerformer } from "@ledgerhq/live-common/market/utils/types"; import { ABTestingVariants, PortfolioRange } from "@ledgerhq/types-live"; import { Dispatch, SetStateAction } from "react"; diff --git a/apps/ledger-live-desktop/src/renderer/screens/dashboard/MarketPerformanceWidget/useMarketPerformanceWidget.ts b/apps/ledger-live-desktop/src/renderer/screens/dashboard/MarketPerformanceWidget/useMarketPerformanceWidget.ts index cb1a007d14a0..50349e9b9504 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/dashboard/MarketPerformanceWidget/useMarketPerformanceWidget.ts +++ b/apps/ledger-live-desktop/src/renderer/screens/dashboard/MarketPerformanceWidget/useMarketPerformanceWidget.ts @@ -6,7 +6,7 @@ import { import { useState } from "react"; import { Order } from "./types"; -import { useMarketPerformers } from "@ledgerhq/live-common/market/v2/useMarketPerformers"; +import { useMarketPerformers } from "@ledgerhq/live-common/market/hooks/useMarketPerformers"; import { getSlicedList } from "./utils"; import { useMarketPerformanceFeatureFlag } from "~/renderer/actions/marketperformance"; diff --git a/apps/ledger-live-desktop/src/renderer/screens/dashboard/MarketPerformanceWidget/utils/index.ts b/apps/ledger-live-desktop/src/renderer/screens/dashboard/MarketPerformanceWidget/utils/index.ts index 63e6e4875bc1..a321e3976871 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/dashboard/MarketPerformanceWidget/utils/index.ts +++ b/apps/ledger-live-desktop/src/renderer/screens/dashboard/MarketPerformanceWidget/utils/index.ts @@ -1,4 +1,4 @@ -import { MarketItemPerformer } from "@ledgerhq/live-common/market/types"; +import { MarketItemPerformer } from "@ledgerhq/live-common/market/utils/types"; import { PortfolioRange } from "@ledgerhq/types-live"; import { Order } from "~/renderer/screens/dashboard/MarketPerformanceWidget/types"; diff --git a/apps/ledger-live-desktop/src/renderer/screens/market/MarketCoin/components/MarketCoinChart.tsx b/apps/ledger-live-desktop/src/renderer/screens/market/MarketCoin/components/MarketCoinChart.tsx index 69d02593c2f4..c39622bbd567 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/market/MarketCoin/components/MarketCoinChart.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/market/MarketCoin/components/MarketCoinChart.tsx @@ -10,6 +10,8 @@ import { dayFormat, hourFormat, useDateFormatter } from "~/renderer/hooks/useDat import ChartPlaceholder from "../../assets/ChartPlaceholder"; import CountervalueSelect from "../../components/CountervalueSelect"; import { useTranslation } from "react-i18next"; +import { MarketCoinDataChart } from "@ledgerhq/live-common/market/utils/types"; +import { formatPercentage, formatPrice } from "../../utils"; const Title = styled(Text).attrs({ variant: "h3", color: "neutral.c100", mt: 1, mb: 5 })` font-size: 28px; @@ -72,7 +74,7 @@ function Tooltip({ data, counterCurrency, locale, formatDay, formatHour }: Toolt type Props = { price?: number; priceChangePercentage?: number; - chartData?: Record; + chartData?: MarketCoinDataChart; range: string; counterCurrency: string; refreshChart: (range: string) => void; @@ -149,7 +151,7 @@ function MarkeCoinChartComponent({ {counterValueFormatter({ currency: counterCurrency, - value: price, + value: formatPrice(price ?? 0), locale, })} @@ -158,7 +160,7 @@ function MarkeCoinChartComponent({ diff --git a/apps/ledger-live-desktop/src/renderer/screens/market/MarketCoin/components/MarketInfo.tsx b/apps/ledger-live-desktop/src/renderer/screens/market/MarketCoin/components/MarketInfo.tsx index 949eaad3bed1..88dd21e97222 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/market/MarketCoin/components/MarketInfo.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/market/MarketCoin/components/MarketInfo.tsx @@ -6,6 +6,7 @@ import FormattedVal from "~/renderer/components/FormattedVal"; import LoadingPlaceholder from "~/renderer/components/LoadingPlaceholder"; import counterValueFormatter from "@ledgerhq/live-common/market/utils/countervalueFormatter"; import { dayAndHourFormat, useDateFormatted } from "~/renderer/hooks/useDateFormatter"; +import { KeysPriceChange } from "@ledgerhq/live-common/market/utils/types"; const Title = styled(Text).attrs({ variant: "h5", color: "neutral.c100", mb: 2 })` font-size: 20px; @@ -82,7 +83,7 @@ type Props = { high24h?: number; low24h?: number; price?: number; - priceChangePercentage?: number; + priceChangePercentage?: Record; marketCapChangePercentage24h?: number; circulatingSupply?: number; totalSupply?: number; @@ -94,6 +95,7 @@ type Props = { counterCurrency: string; loading: boolean; locale: string; + range: string; }; function MarketInfo({ @@ -115,6 +117,7 @@ function MarketInfo({ counterCurrency, loading, locale, + range, }: Props) { const { t } = useTranslation(); @@ -123,6 +126,8 @@ function MarketInfo({ const athText = useDateFormatted(athDateD, dayAndHourFormat); const atlText = useDateFormatted(atlDateD, dayAndHourFormat); + const currentPriceChangePercentage = priceChangePercentage?.[range as KeysPriceChange]; + return ( @@ -134,11 +139,11 @@ function MarketInfo({ {counterValueFormatter({ value: price, currency: counterCurrency, locale })} - {priceChangePercentage ? ( + {currentPriceChangePercentage ? ( diff --git a/apps/ledger-live-desktop/src/renderer/screens/market/MarketCoin/index.tsx b/apps/ledger-live-desktop/src/renderer/screens/market/MarketCoin/index.tsx index 73a333e44220..50b0b5d41f39 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/market/MarketCoin/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/market/MarketCoin/index.tsx @@ -10,6 +10,7 @@ import { Button } from ".."; import MarketCoinChart from "./components/MarketCoinChart"; import MarketInfo from "./components/MarketInfo"; import { useMarketCoin } from "~/renderer/screens/market/hooks/useMarketCoin"; +import { KeysPriceChange } from "@ledgerhq/live-common/market/utils/types"; const CryptoCurrencyIconWrapper = styled.div` height: 56px; @@ -52,8 +53,6 @@ export default function MarketCoinScreen() { availableOnSwap, color, dataChart, - dataCurrency, - isLoadingData, isLoadingDataChart, isLoadingCurrency, range, @@ -68,9 +67,10 @@ export default function MarketCoinScreen() { changeCounterCurrency, } = useMarketCoin(); - const { price, priceChangePercentage } = dataCurrency || {}; + const { name, ticker, image, internalCurrency, price } = currency || {}; + + const currentPriceChangePercentage = currency?.priceChangePercentage[range as KeysPriceChange]; - const { name, ticker, image, internalCurrency } = currency || {}; return ( ); diff --git a/apps/ledger-live-desktop/src/renderer/screens/market/MarketList/components/MarketItemChart.tsx b/apps/ledger-live-desktop/src/renderer/screens/market/MarketList/components/MarketItemChart.tsx index 39a2d95523c5..71b77efaa04c 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/market/MarketList/components/MarketItemChart.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/market/MarketList/components/MarketItemChart.tsx @@ -1,6 +1,6 @@ import React, { memo } from "react"; import { useTheme } from "styled-components"; -import { SparklineSvgData } from "@ledgerhq/live-common/market/types"; +import { SparklineSvgData } from "@ledgerhq/live-common/market/utils/types"; type Props = { sparklineIn7d: SparklineSvgData; diff --git a/apps/ledger-live-desktop/src/renderer/screens/market/MarketList/components/MarketRowItem.tsx b/apps/ledger-live-desktop/src/renderer/screens/market/MarketList/components/MarketRowItem.tsx index 25493a23f175..f958dc64163d 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/market/MarketList/components/MarketRowItem.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/market/MarketList/components/MarketRowItem.tsx @@ -7,11 +7,12 @@ import { setTrackingSource } from "~/renderer/analytics/TrackPage"; import counterValueFormatter from "@ledgerhq/live-common/market/utils/countervalueFormatter"; import CryptoCurrencyIcon from "~/renderer/components/CryptoCurrencyIcon"; import { SmallMarketItemChart } from "./MarketItemChart"; -import { CurrencyData } from "@ledgerhq/live-common/market/types"; +import { CurrencyData, KeysPriceChange } from "@ledgerhq/live-common/market/utils/types"; import { Button } from "../.."; import { useTranslation } from "react-i18next"; import { TableRow, TableCell } from "../../components/Table"; import { Page, useMarketActions } from "../../hooks/useMarketActions"; +import { formatPercentage, formatPrice } from "../../utils"; const CryptoCurrencyIconWrapper = styled.div` height: 32px; @@ -39,6 +40,7 @@ type Props = { locale: string; isStarred: boolean; toggleStar: () => void; + range?: string; }; export const MarketRow = memo(function MarketRowItem({ @@ -49,13 +51,14 @@ export const MarketRow = memo(function MarketRowItem({ loading, isStarred, toggleStar, + range, }: Props) { + const history = useHistory(); + const { t } = useTranslation(); const { onBuy, onStake, onSwap, availableOnBuy, availableOnSwap, availableOnStake } = useMarketActions({ currency, page: Page.Market }); - const history = useHistory(); - const onCurrencyClick = useCallback(() => { if (currency) { setTrackingSource("Page Market"); @@ -78,6 +81,8 @@ export const MarketRow = memo(function MarketRowItem({ const hasActions = currency?.internalCurrency && (availableOnBuy || availableOnSwap || availableOnStake); + const currentPriceChangePercentage = currency?.priceChangePercentage[range as KeysPriceChange]; + return (
{loading || !currency ? ( @@ -161,15 +166,19 @@ export const MarketRow = memo(function MarketRowItem({ - {counterValueFormatter({ value: currency.price, currency: counterCurrency, locale })} + {counterValueFormatter({ + value: formatPrice(currency.price ?? 0), + currency: counterCurrency, + locale, + })} - {currency.priceChangePercentage ? ( + {currentPriceChangePercentage ? ( @@ -225,6 +234,7 @@ export const CurrencyRow = memo(function CurrencyRowItem({ starredMarketCoins, locale, style, + range, }: CurrencyRowProps) { const currency = data ? data[index] : null; const isStarred = currency && starredMarketCoins.includes(currency.id); @@ -239,6 +249,7 @@ export const CurrencyRow = memo(function CurrencyRowItem({ key={index} locale={locale} style={{ ...style }} + range={range} /> ); }); diff --git a/apps/ledger-live-desktop/src/renderer/screens/market/MarketList/components/NoCryptoPlaceholder.tsx b/apps/ledger-live-desktop/src/renderer/screens/market/MarketList/components/NoCryptoPlaceholder.tsx index 96b7c53a7c0b..44abf8699860 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/market/MarketList/components/NoCryptoPlaceholder.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/market/MarketList/components/NoCryptoPlaceholder.tsx @@ -1,4 +1,4 @@ -import { MarketListRequestParams } from "@ledgerhq/live-common/market/types"; +import { MarketListRequestParams } from "@ledgerhq/live-common/market/utils/types"; import { Flex, Text } from "@ledgerhq/react-ui"; import { TFunction } from "i18next"; import React from "react"; diff --git a/apps/ledger-live-desktop/src/renderer/screens/market/MarketList/index.tsx b/apps/ledger-live-desktop/src/renderer/screens/market/MarketList/index.tsx index e56e565dfb39..f6b9c711efb4 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/market/MarketList/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/market/MarketList/index.tsx @@ -4,10 +4,7 @@ import { TFunction } from "i18next"; import { FixedSizeList as List } from "react-window"; import InfiniteLoader from "react-window-infinite-loader"; import AutoSizer from "react-virtualized-auto-sizer"; -import { - MarketListRequestParams, - MarketListRequestResult, -} from "@ledgerhq/live-common/market/types"; +import { CurrencyData, MarketListRequestParams } from "@ledgerhq/live-common/market/utils/types"; import TrackPage from "~/renderer/analytics/TrackPage"; import { SortTableCell } from "../components/SortTableCell"; import { TableCell, TableRow, listItemHeight } from "../components/Table"; @@ -23,10 +20,10 @@ type MarketListProps = { itemCount: number; locale: string; fromCurrencies?: string[]; - marketResult: MarketListRequestResult; + marketData: CurrencyData[]; resetSearch: () => void; toggleFilterByStarredAccounts: () => void; - toggleSortBy: (newOrderBy: string) => void; + toggleSortBy: () => void; toggleStar: (id: string, isStarred: boolean) => void; t: TFunction; isItemLoaded: (index: number) => boolean; @@ -43,7 +40,7 @@ function MarketList({ currenciesLength, locale, fromCurrencies, - marketResult, + marketData, resetSearch, isItemLoaded, toggleFilterByStarredAccounts, @@ -53,7 +50,7 @@ function MarketList({ checkIfDataIsStaleAndRefetch, t, }: MarketListProps) { - const { order, orderBy, search, starred, range, counterCurrency } = marketParams; + const { order, search, starred, range, counterCurrency } = marketParams; return ( @@ -63,13 +60,7 @@ function MarketList({ <> {search && currenciesLength > 0 && } - + # {t("market.marketList.crypto")} @@ -132,7 +123,7 @@ function MarketList({ itemCount={itemCount} onItemsRendered={onItemsRendered} itemSize={listItemHeight} - itemData={marketResult.data} + itemData={marketData} style={{ overflowX: "hidden" }} ref={ref} overscanCount={10} diff --git a/apps/ledger-live-desktop/src/renderer/screens/market/components/SideDrawerFilter.tsx b/apps/ledger-live-desktop/src/renderer/screens/market/components/SideDrawerFilter.tsx index f879a1677491..1cb8a6ee4210 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/market/components/SideDrawerFilter.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/market/components/SideDrawerFilter.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from "react"; import { TFunction } from "i18next"; import Dropdown from "./DropDown"; -import { MarketListRequestParams } from "@ledgerhq/live-common/market/types"; +import { MarketListRequestParams } from "@ledgerhq/live-common/market/utils/types"; export default function SideDrawerFilter({ refresh, diff --git a/apps/ledger-live-desktop/src/renderer/screens/market/components/SortTableCell.tsx b/apps/ledger-live-desktop/src/renderer/screens/market/components/SortTableCell.tsx index 32a0cda0fb08..86fc9f3f1a11 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/market/components/SortTableCell.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/market/components/SortTableCell.tsx @@ -5,22 +5,18 @@ import { TableCellBase } from "./Table"; export const SortTableCell = ({ onClick, - orderByKey, - orderBy, order, children, ...props }: { loading?: boolean; - onClick?: (key: string) => void; - orderByKey: string; - orderBy?: string; + onClick?: () => void; order?: string; children?: React.ReactNode; }) => ( - !!onClick && onClick(orderByKey)} {...props}> + !!onClick && onClick()} {...props}> {children} - + diff --git a/apps/ledger-live-desktop/src/renderer/screens/market/hooks/useMarket.ts b/apps/ledger-live-desktop/src/renderer/screens/market/hooks/useMarket.ts index 90e241340a4d..b942e79e337d 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/market/hooks/useMarket.ts +++ b/apps/ledger-live-desktop/src/renderer/screens/market/hooks/useMarket.ts @@ -1,11 +1,11 @@ import { useFetchCurrencyFrom } from "@ledgerhq/live-common/exchange/swap/hooks/index"; import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; -import { MarketListRequestParams } from "@ledgerhq/live-common/market/types"; +import { MarketListRequestParams, Order } from "@ledgerhq/live-common/market/utils/types"; import { rangeDataTable } from "@ledgerhq/live-common/market/utils/rangeDataTable"; import { useMarketDataProvider, useMarketData as useMarketDataHook, -} from "@ledgerhq/live-common/market/v2/useMarketDataProvider"; +} from "@ledgerhq/live-common/market/hooks/useMarketDataProvider"; import { useCallback, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; @@ -25,28 +25,26 @@ export function useMarket() { const starredMarketCoins: string[] = useSelector(starredMarketCoinsSelector); const locale = useSelector(localeSelector); + const REFRESH_RATE = + Number(lldRefreshMarketDataFeature?.params?.refreshTime) > 0 + ? REFETCH_TIME_ONE_MINUTE * Number(lldRefreshMarketDataFeature?.params?.refreshTime) + : REFETCH_TIME_ONE_MINUTE * BASIC_REFETCH; + + const { range, starred = [], liveCompatible, order, search = "" } = marketParams; + + const starFilterOn = starred.length > 0; + useInitSupportedCounterValues(); const { data: fromCurrencies } = useFetchCurrencyFrom(); - const { supportedCurrencies, liveCoinsList, supportedCounterCurrencies } = - useMarketDataProvider(); + const { liveCoinsList, supportedCounterCurrencies } = useMarketDataProvider(); const marketResult = useMarketDataHook({ ...marketParams, - liveCoinsList, - supportedCoinsList: supportedCurrencies, + liveCoinsList: liveCompatible ? liveCoinsList : [], }); - const REFRESH_RATE = - Number(lldRefreshMarketDataFeature?.params?.refreshTime) > 0 - ? REFETCH_TIME_ONE_MINUTE * Number(lldRefreshMarketDataFeature?.params?.refreshTime) - : REFETCH_TIME_ONE_MINUTE * BASIC_REFETCH; - - const { range, starred = [], liveCompatible, orderBy, order, search = "" } = marketParams; - - const starFilterOn = starred.length > 0; - const timeRanges = useMemo( () => Object.keys(rangeDataTable) @@ -115,7 +113,7 @@ export function useMarket() { const toggleFilterByStarredAccounts = useCallback(() => { if (starredMarketCoins.length > 0 || starFilterOn) { const starred = starFilterOn ? [] : starredMarketCoins; - refresh({ starred, search: "" }); + refresh({ starred, search: "", page: 1 }); } }, [refresh, starFilterOn, starredMarketCoins]); @@ -134,20 +132,11 @@ export function useMarket() { [dispatch], ); - const toggleSortBy = useCallback( - (newOrderBy: string) => { - const isFreshSort = newOrderBy !== orderBy; - refresh( - isFreshSort - ? { orderBy: newOrderBy, order: "desc" } - : { - orderBy: newOrderBy, - order: order === "asc" ? "desc" : "asc", - }, - ); - }, - [order, orderBy, refresh], - ); + const toggleSortBy = useCallback(() => { + refresh({ + order: order === Order.MarketCapAsc ? Order.MarketCapDesc : Order.MarketCapAsc, + }); + }, [order, refresh]); const isItemLoaded = useCallback( (index: number) => !!marketResult.data[index], @@ -161,8 +150,8 @@ export function useMarket() { const refetchData = useCallback( (pageToRefetch: number) => { - const elem = marketResult.cachedMetadataMap.get(String(pageToRefetch ?? 1)); - + const page = pageToRefetch - 1 || 0; + const elem = marketResult.cachedMetadataMap.get(String(page)); if (elem && isDataStale(elem.updatedAt, REFRESH_RATE)) { elem.refetch(); } @@ -208,7 +197,7 @@ export function useMarket() { t, liveCompatible, starFilterOn, - marketResult, + marketData: marketResult.data, starredMarketCoins, timeRanges, marketParams, diff --git a/apps/ledger-live-desktop/src/renderer/screens/market/hooks/useMarketActions.ts b/apps/ledger-live-desktop/src/renderer/screens/market/hooks/useMarketActions.ts index 078890a791d3..4063dcd270b0 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/market/hooks/useMarketActions.ts +++ b/apps/ledger-live-desktop/src/renderer/screens/market/hooks/useMarketActions.ts @@ -1,4 +1,4 @@ -import { CurrencyData } from "@ledgerhq/live-common/market/types"; +import { CurrencyData } from "@ledgerhq/live-common/market/utils/types"; import { useCallback } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useHistory } from "react-router-dom"; diff --git a/apps/ledger-live-desktop/src/renderer/screens/market/hooks/useMarketCoin.ts b/apps/ledger-live-desktop/src/renderer/screens/market/hooks/useMarketCoin.ts index a2453bbad0ea..04cf11ee1549 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/market/hooks/useMarketCoin.ts +++ b/apps/ledger-live-desktop/src/renderer/screens/market/hooks/useMarketCoin.ts @@ -1,18 +1,18 @@ import { useSelector, useDispatch } from "react-redux"; -import { useParams } from "react-router-dom"; -import { localeSelector, starredMarketCoinsSelector } from "~/renderer/reducers/settings"; -import { useCallback, useMemo } from "react"; import { useTheme } from "styled-components"; import { getCurrencyColor } from "~/renderer/getCurrencyColor"; import { useCurrencyChartData, useCurrencyData, useMarketDataProvider, -} from "@ledgerhq/live-common/market/v2/useMarketDataProvider"; +} from "@ledgerhq/live-common/market/hooks/useMarketDataProvider"; import { Page, useMarketActions } from "./useMarketActions"; +import { useCallback } from "react"; +import { useParams } from "react-router"; import { setMarketOptions } from "~/renderer/actions/market"; -import { marketParamsSelector } from "~/renderer/reducers/market"; import { removeStarredMarketCoins, addStarredMarketCoins } from "~/renderer/actions/settings"; +import { marketParamsSelector } from "~/renderer/reducers/market"; +import { starredMarketCoinsSelector, localeSelector } from "~/renderer/reducers/settings"; export const useMarketCoin = () => { const marketParams = useSelector(marketParamsSelector); @@ -34,20 +34,16 @@ export const useMarketCoin = () => { range, }); - const { currencyData, currencyInfo } = useCurrencyData({ + const { data: currency, isLoading } = useCurrencyData({ counterCurrency, id: currencyId, - range, }); - const currency = useMemo(() => currencyInfo?.data, [currencyInfo]); - const isLoadingCurrency = useMemo(() => currencyInfo?.isLoading, [currencyInfo]); - const { id, internalCurrency } = currency || {}; const { onBuy, onStake, onSwap, availableOnBuy, availableOnStake, availableOnSwap } = useMarketActions({ - currency, + currency: currency, page: Page.MarketCoin, }); @@ -94,11 +90,9 @@ export const useMarketCoin = () => { onSwap, toggleStar, color, - dataCurrency: currencyData.data, dataChart: resCurrencyChartData.data, isLoadingDataChart: resCurrencyChartData.isLoading, - isLoadingData: currencyData.isLoading, - isLoadingCurrency, + isLoadingCurrency: isLoading, changeRange, range, counterCurrency, diff --git a/apps/ledger-live-desktop/src/renderer/screens/market/utils/index.ts b/apps/ledger-live-desktop/src/renderer/screens/market/utils/index.ts index e537e630741b..a6b8e3d829fa 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/market/utils/index.ts +++ b/apps/ledger-live-desktop/src/renderer/screens/market/utils/index.ts @@ -16,3 +16,11 @@ export function getCurrentPage(scrollPosition: number, pageSize: number): number const size = listItemHeight * pageSize; return Math.floor(scrollPosition / size) + 1; } + +export function formatPrice(price: number): number { + return parseFloat(price.toFixed(price >= 1 ? 2 : 6)); +} + +export function formatPercentage(percentage: number, decimals = 2): number { + return parseFloat(percentage.toFixed(decimals)); +} diff --git a/apps/ledger-live-desktop/tests/specs/market/market.spec.ts-snapshots/market-btc-page-linux.png b/apps/ledger-live-desktop/tests/specs/market/market.spec.ts-snapshots/market-btc-page-linux.png index a0f0f762e4b8819f2b37be4db56b2fc5948f2636..1fce52792810a64d4cbabc6596f2a6aaae7f31fe 100644 GIT binary patch delta 44433 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 delta 43897 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

5Vg>v-kr?ruvAcbH`OHY7spkCGR`U-NKd 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�zdnz~DVFLl+?!@___Fk3FVyw!3a+|_ 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

( return { ...otherProperties, access: false, - sort: `${requestParams.orderBy}_${requestParams.order}`, + ...(requestParams.order && + requestParams.range && { sort: getSortParam(requestParams.order, requestParams.range) }), "%change": requestParams.range, countervalue: requestParams.counterCurrency, view: requestParams.liveCompatible ? "Only Live Supported" : "All coins", }; } + +export const isDataStale = (lastUpdate: number, refreshRate: number) => { + const currentTime = new Date(); + const updatedAt = new Date(lastUpdate); + const elapsedTime = currentTime.getTime() - updatedAt.getTime(); + + return elapsedTime > refreshRate; +}; + +export function getCurrentPage(indexPosition: number, pageSize: number): number { + return Math.floor(indexPosition / pageSize) + 1; +} diff --git a/apps/ledger-live-mobile/src/reducers/index.ts b/apps/ledger-live-mobile/src/reducers/index.ts index 3b4a0c17064b..43547f175eac 100644 --- a/apps/ledger-live-mobile/src/reducers/index.ts +++ b/apps/ledger-live-mobile/src/reducers/index.ts @@ -12,6 +12,7 @@ import dynamicContent from "./dynamicContent"; import walletconnect from "./walletconnect"; import protect from "./protect"; import nft from "./nft"; +import market from "./market"; import wallet from "./wallet"; import { State } from "./types"; import { ActionsPayload } from "../actions/types"; @@ -33,6 +34,7 @@ const appReducer = combineReducers({ protect, nft, wallet, + market, }); // TODO: EXPORT ALL POSSIBLE ACTION TYPES AND USE ACTION diff --git a/apps/ledger-live-mobile/src/reducers/market.ts b/apps/ledger-live-mobile/src/reducers/market.ts new file mode 100644 index 000000000000..c05a3836bb12 --- /dev/null +++ b/apps/ledger-live-mobile/src/reducers/market.ts @@ -0,0 +1,59 @@ +import { Action, ReducerMap, handleActions } from "redux-actions"; +import { MarketState, State } from "./types"; +import { + MarketSetCurrentPagePayload, + MarketSetMarketFilterByStarredCurrenciesPayload, + MarketSetMarketRequestParamsPayload, + MarketStateActionTypes, + MarketStatePayload, +} from "~/actions/types"; +import { Order } from "@ledgerhq/live-common/market/utils/types"; + +export const LIMIT = 20; + +export const INITIAL_STATE: MarketState = { + marketParams: { + range: "24h", + limit: LIMIT, + starred: [], + order: Order.MarketCapDesc, + search: "", + liveCompatible: false, + page: 1, + counterCurrency: "usd", + }, + marketFilterByStarredCurrencies: false, + marketCurrentPage: 1, +}; + +const handlers: ReducerMap = { + [MarketStateActionTypes.SET_MARKET_REQUEST_PARAMS]: (state, action) => ({ + ...state, + marketParams: { + ...state.marketParams, + ...(action as Action).payload, + }, + }), + [MarketStateActionTypes.SET_MARKET_FILTER_BY_STARRED_CURRENCIES]: (state, action) => ({ + ...state, + marketFilterByStarredCurrencies: ( + action as Action + ).payload, + }), + + [MarketStateActionTypes.MARKET_SET_CURRENT_PAGE]: (state, action) => ({ + ...state, + marketCurrentPage: (action as Action).payload, + }), +}; + +// Selectors + +export const marketParamsSelector = (state: State) => state.market.marketParams; +export const marketFilterByStarredCurrenciesSelector = (state: State) => + state.market.marketFilterByStarredCurrencies; +export const marketCurrentPageSelector = (state: State) => state.market.marketCurrentPage; + +// Exporting reducer + +export default handleActions(handlers, INITIAL_STATE); diff --git a/apps/ledger-live-mobile/src/reducers/settings.ts b/apps/ledger-live-mobile/src/reducers/settings.ts index 07e04faca963..b6598724be9f 100644 --- a/apps/ledger-live-mobile/src/reducers/settings.ts +++ b/apps/ledger-live-mobile/src/reducers/settings.ts @@ -16,7 +16,6 @@ import { currencySettingsDefaults } from "../helpers/CurrencySettingsDefaults"; import { getDefaultLanguageLocale, getDefaultLocale } from "../languages"; import type { SettingsAcceptSwapProviderPayload, - SettingsAddStarredMarketcoinsPayload, SettingsBlacklistTokenPayload, SettingsDismissBannerPayload, SettingsHideEmptyTokenAccountsPayload, @@ -27,7 +26,6 @@ import type { SettingsSetHasInstalledAnyAppPayload, SettingsLastSeenDeviceInfoPayload, SettingsPayload, - SettingsRemoveStarredMarketcoinsPayload, SettingsSetAnalyticsPayload, SettingsSetPersonalizedRecommendationsPayload, SettingsSetAvailableUpdatePayload, @@ -40,8 +38,6 @@ import type { SettingsSetMarketCounterCurrencyPayload, SettingsSetCustomImageBackupPayload, SettingsSetLastSeenCustomImagePayload, - SettingsSetMarketFilterByStarredAccountsPayload, - SettingsSetMarketRequestParamsPayload, SettingsSetNotificationsPayload, SettingsSetNeverClickedOnAllowNotificationsButton, SettingsSetOrderAccountsPayload, @@ -81,6 +77,8 @@ import type { SettingsSetHasSeenAnalyticsOptInPrompt, SettingsSetDismissedContentCardsPayload, SettingsClearDismissedContentCardsPayload, + SettingsAddStarredMarketcoinsPayload, + SettingsRemoveStarredMarketcoinsPayload, } from "../actions/types"; import { SettingsActionTypes, @@ -149,18 +147,8 @@ export const INITIAL_STATE: SettingsState = { europa: false, }, hasSeenStaxEnabledNftsPopup: false, - starredMarketCoins: [], lastConnectedDevice: null, - marketRequestParams: { - range: "24h", - orderBy: "market_cap", - order: "desc", - liveCompatible: false, - sparkline: false, - top100: false, - }, marketCounterCurrency: null, - marketFilterByStarredAccounts: false, sensitiveAnalytics: false, onboardingHasDevice: null, notifications: { @@ -185,6 +173,7 @@ export const INITIAL_STATE: SettingsState = { supportedCounterValues: [], hasSeenAnalyticsOptInPrompt: false, dismissedContentCards: {}, + starredMarketCoins: [], }; const pairHash = (from: { ticker: string }, to: { ticker: string }) => @@ -496,21 +485,6 @@ const handlers: ReducerMap = { }; }, - [SettingsActionTypes.ADD_STARRED_MARKET_COINS]: (state, action) => ({ - ...state, - starredMarketCoins: [ - ...state.starredMarketCoins, - (action as Action).payload, - ], - }), - - [SettingsActionTypes.REMOVE_STARRED_MARKET_COINS]: (state, action) => ({ - ...state, - starredMarketCoins: state.starredMarketCoins.filter( - id => id !== (action as Action).payload, - ), - }), - [SettingsActionTypes.SET_CUSTOM_IMAGE_BACKUP]: (state, action) => ({ ...state, customLockScreenBackup: (action as Action).payload, @@ -530,26 +504,11 @@ const handlers: ReducerMap = { hasOrderedNano: (action as Action).payload, }), - [SettingsActionTypes.SET_MARKET_REQUEST_PARAMS]: (state, action) => ({ - ...state, - marketRequestParams: { - ...state.marketRequestParams, - ...(action as Action).payload, - }, - }), - [SettingsActionTypes.SET_MARKET_COUNTER_CURRENCY]: (state, action) => ({ ...state, marketCounterCurrency: (action as Action).payload, }), - [SettingsActionTypes.SET_MARKET_FILTER_BY_STARRED_ACCOUNTS]: (state, action) => ({ - ...state, - marketFilterByStarredAccounts: ( - action as Action - ).payload, - }), - [SettingsActionTypes.SET_SENSITIVE_ANALYTICS]: (state, action) => ({ ...state, sensitiveAnalytics: (action as Action).payload, @@ -678,6 +637,21 @@ const handlers: ReducerMap = { dismissedContentCards, }; }, + + [SettingsActionTypes.ADD_STARRED_MARKET_COINS]: (state, action) => ({ + ...state, + starredMarketCoins: [ + ...state.starredMarketCoins, + (action as Action).payload, + ], + }), + + [SettingsActionTypes.REMOVE_STARRED_MARKET_COINS]: (state, action) => ({ + ...state, + starredMarketCoins: state.starredMarketCoins.filter( + id => id !== (action as Action).payload, + ), + }), }; export default handleActions(handlers, INITIAL_STATE); @@ -854,7 +828,6 @@ export const knownDeviceModelIdsSelector = (state: State) => state.settings.know export const hasSeenStaxEnabledNftsPopupSelector = (state: State) => state.settings.hasSeenStaxEnabledNftsPopup; export const customImageTypeSelector = (state: State) => state.settings.customLockScreenType; -export const starredMarketCoinsSelector = (state: State) => state.settings.starredMarketCoins; export const lastSeenDeviceSelector = (state: State) => { const { lastSeenDevice } = state.settings; @@ -871,10 +844,7 @@ export const lastConnectedDeviceSelector = (state: State) => { }; export const hasOrderedNanoSelector = (state: State) => state.settings.hasOrderedNano; -export const marketRequestParamsSelector = (state: State) => state.settings.marketRequestParams; export const marketCounterCurrencySelector = (state: State) => state.settings.marketCounterCurrency; -export const marketFilterByStarredAccountsSelector = (state: State) => - state.settings.marketFilterByStarredAccounts; export const customImageBackupSelector = (state: State) => state.settings.customLockScreenBackup; export const sensitiveAnalyticsSelector = (state: State) => state.settings.sensitiveAnalytics; export const onboardingHasDeviceSelector = (state: State) => state.settings.onboardingHasDevice; @@ -904,3 +874,5 @@ export const supportedCounterValuesSelector = (state: State) => export const hasSeenAnalyticsOptInPromptSelector = (state: State) => state.settings.hasSeenAnalyticsOptInPrompt; export const dismissedContentCardsSelector = (state: State) => state.settings.dismissedContentCards; + +export const starredMarketCoinsSelector = (state: State) => state.settings.starredMarketCoins; diff --git a/apps/ledger-live-mobile/src/reducers/types.ts b/apps/ledger-live-mobile/src/reducers/types.ts index 239b2eae615b..b48cdcba59bb 100644 --- a/apps/ledger-live-mobile/src/reducers/types.ts +++ b/apps/ledger-live-mobile/src/reducers/types.ts @@ -9,7 +9,7 @@ import type { import type { Device } from "@ledgerhq/live-common/hw/actions/types"; import type { DeviceModelId } from "@ledgerhq/devices"; import type { CryptoCurrencyId, Currency, Unit } from "@ledgerhq/types-cryptoassets"; -import { MarketListRequestParams } from "@ledgerhq/live-common/market/types"; +import { MarketListRequestParams } from "@ledgerhq/live-common/market/utils/types"; import { PostOnboardingState } from "@ledgerhq/types-live"; import { AvailableProviderV3, ExchangeRate } from "@ledgerhq/live-common/exchange/swap/types"; import { Transaction } from "@ledgerhq/live-common/generated/types"; @@ -227,11 +227,8 @@ export type SettingsState = { lastSeenDevice: DeviceModelInfo | null; knownDeviceModelIds: Record; hasSeenStaxEnabledNftsPopup: boolean; - starredMarketCoins: string[]; lastConnectedDevice: Device | null; - marketRequestParams: MarketListRequestParams; marketCounterCurrency: string | null | undefined; - marketFilterByStarredAccounts: boolean; sensitiveAnalytics: boolean; onboardingHasDevice: boolean | null; onboardingType: OnboardingType | null; @@ -263,6 +260,7 @@ export type SettingsState = { supportedCounterValues: supportedCountervaluesData[]; hasSeenAnalyticsOptInPrompt: boolean; dismissedContentCards: { [id: string]: number }; + starredMarketCoins: string[]; }; export type NotificationsSettings = { @@ -333,6 +331,14 @@ export type NftGalleryChainFiltersState = Pick< "polygon" | "ethereum" >; +// === MARKET STATE === + +export type MarketState = { + marketParams: MarketListRequestParams; + marketFilterByStarredCurrencies: boolean; + marketCurrentPage: number; +}; + // === ROOT STATE === export type State = { @@ -349,5 +355,6 @@ export type State = { postOnboarding: PostOnboardingState; protect: ProtectState; nft: NftState; + market: MarketState; wallet: WalletState; }; diff --git a/apps/ledger-live-mobile/src/screens/WalletCentricAsset/AssetMarketSection.tsx b/apps/ledger-live-mobile/src/screens/WalletCentricAsset/AssetMarketSection.tsx index 01f3eebe3b57..401d2be72b42 100644 --- a/apps/ledger-live-mobile/src/screens/WalletCentricAsset/AssetMarketSection.tsx +++ b/apps/ledger-live-mobile/src/screens/WalletCentricAsset/AssetMarketSection.tsx @@ -1,11 +1,11 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Flex } from "@ledgerhq/native-ui"; import { CryptoOrTokenCurrency } from "@ledgerhq/types-cryptoassets"; -import { useSingleCoinMarketData } from "@ledgerhq/live-common/market/MarketDataProvider"; import SectionContainer from "../WalletCentricSections/SectionContainer"; import SectionTitle from "../WalletCentricSections/SectionTitle"; import MarketPriceSection from "../WalletCentricSections/MarketPrice"; +import { useMarketCoinData } from "~/newArch/features/Market/hooks/useMarketCoinData"; // @FIXME workaround for main tokens const tokenIDToMarketID = { @@ -15,18 +15,19 @@ const tokenIDToMarketID = { const AssetMarketSection = ({ currency }: { currency: CryptoOrTokenCurrency }) => { const { t } = useTranslation(); - const { selectedCoinData, selectCurrency, counterCurrency } = useSingleCoinMarketData(); + const [selectedCurrency, setSelectedCurrency] = useState(currency.id); + const { currency: fetchedCurrency, counterCurrency } = useMarketCoinData({ + currencyId: selectedCurrency, + }); useEffect(() => { - selectCurrency( + setSelectedCurrency( tokenIDToMarketID[currency.id as keyof typeof tokenIDToMarketID] || currency.id, - undefined, - "24h", ); - return () => selectCurrency(); - }, [currency, selectCurrency]); + }, [currency]); + + if (!fetchedCurrency?.price) return null; - if (!selectedCoinData?.price) return null; return ( diff --git a/apps/ledger-live-mobile/src/screens/WalletCentricSections/MarketPrice.tsx b/apps/ledger-live-mobile/src/screens/WalletCentricSections/MarketPrice.tsx index c85cdd7fe204..1af7e73398f5 100644 --- a/apps/ledger-live-mobile/src/screens/WalletCentricSections/MarketPrice.tsx +++ b/apps/ledger-live-mobile/src/screens/WalletCentricSections/MarketPrice.tsx @@ -4,16 +4,18 @@ import { useTranslation } from "react-i18next"; import { useNavigation } from "@react-navigation/native"; import counterValueFormatter from "@ledgerhq/live-common/market/utils/countervalueFormatter"; import { CryptoOrTokenCurrency } from "@ledgerhq/types-cryptoassets"; -import { SingleCoinProviderData } from "@ledgerhq/live-common/market/MarketDataProvider"; import { withDiscreetMode } from "~/context/DiscreetModeContext"; import { ScreenName } from "~/const"; import DeltaVariation from "LLM/features/Market/components/DeltaVariation"; import Touchable from "~/components/Touchable"; import { useSettings } from "~/hooks"; +import { CurrencyData, KeysPriceChange } from "@ledgerhq/live-common/market/utils/types"; +import { useTimeRange } from "~/actions/settings"; +import { PortfolioRange } from "@ledgerhq/types-live"; type Props = { currency: CryptoOrTokenCurrency; - selectedCoinData: SingleCoinProviderData["selectedCoinData"]; + selectedCoinData: CurrencyData; counterCurrency: string | undefined; }; @@ -22,12 +24,20 @@ const MarketPrice = ({ currency, selectedCoinData, counterCurrency }: Props) => const { locale } = useSettings(); const navigation = useNavigation(); + const [range] = useTimeRange(); + const goToMarketPage = useCallback(() => { navigation.navigate(ScreenName.MarketDetail, { currencyId: currency.id, }); }, [currency, navigation]); + const getPrice = (selectedCoinData: CurrencyData, range: PortfolioRange) => { + const key: KeysPriceChange = range === "all" ? KeysPriceChange.year : KeysPriceChange[range]; + return selectedCoinData.priceChangePercentage[key]; + }; + + const priceChange = getPrice(selectedCoinData, range); return ( - {t("portfolio.marketPriceSection.currencyPriceChange")} + {t(`portfolio.marketPriceSection.currencyPriceChange.${range}`)} - + diff --git a/libs/ledger-live-common/.unimportedrc.json b/libs/ledger-live-common/.unimportedrc.json index aa01cfc1b22d..8fd965fddd96 100644 --- a/libs/ledger-live-common/.unimportedrc.json +++ b/libs/ledger-live-common/.unimportedrc.json @@ -217,9 +217,6 @@ "src/manager/index.ts", "src/manager/localization.ts", "src/manager/provider.ts", - "src/market/api/api.mock.ts", - "src/market/MarketDataProvider.tsx", - "src/market/types.ts", "src/market/utils/countervalueFormatter.ts", "src/market/utils/rangeDataTable.ts", "src/mock/account.ts", @@ -2688,11 +2685,14 @@ "src/index.ts", "src/jest.d.ts", "src/manager/index.test.ts", - "src/market/v2", - "src/market/v2/queryKeys.ts", - "src/market/v2/timers.ts", - "src/market/v2/useMarketDataProvider.ts", - "src/market/v2/useMarketPerformers.ts", + "src/market/api/index.ts", + "src/market/utils/types.ts", + "src/market/utils/index.ts", + "src/market/utils/queryKeys.ts", + "src/market/utils/timers.ts", + "src/market/utils/currencyFormatter.ts", + "src/market/hooks/useMarketDataProvider.ts", + "src/market/hooks/useMarketPerformers.ts", "src/mock/fixtures", "src/mock/fixtures/aDeviceInfo.ts", "src/mock/fixtures/aLatestFirmwareContext.ts", diff --git a/libs/ledger-live-common/src/featureFlags/defaultFeatures.ts b/libs/ledger-live-common/src/featureFlags/defaultFeatures.ts index b431ad0cb7a7..12d5ce537916 100644 --- a/libs/ledger-live-common/src/featureFlags/defaultFeatures.ts +++ b/libs/ledger-live-common/src/featureFlags/defaultFeatures.ts @@ -463,6 +463,12 @@ export const DEFAULT_FEATURES: Features = { refreshTime: 3, //nb minutes }, }, + llmRefreshMarketData: { + ...DEFAULT_FEATURE, + params: { + refreshTime: 3, //nb minutes + }, + }, spamReportNfts: DEFAULT_FEATURE, lldWalletSync: DEFAULT_FEATURE, lldNftsGalleryNewArch: DEFAULT_FEATURE, diff --git a/libs/ledger-live-common/src/market/MarketDataProvider.tsx b/libs/ledger-live-common/src/market/MarketDataProvider.tsx deleted file mode 100644 index 6de165f70b0a..000000000000 --- a/libs/ledger-live-common/src/market/MarketDataProvider.tsx +++ /dev/null @@ -1,403 +0,0 @@ -/* eslint-disable @typescript-eslint/no-empty-function */ -// @flow -import React, { - createContext, - useCallback, - useContext, - ReactElement, - useReducer, - useEffect, -} from "react"; - -import { useDebounce } from "../hooks/useDebounce"; -import { - State, - MarketDataApi, - MarketListRequestParams, - MarketCurrencyChartDataRequestParams, - CurrencyData, - SingleCoinState, - SupportedCoins, -} from "./types"; -import defaultFetchApi from "./api/api"; -import type { Currency } from "@ledgerhq/types-cryptoassets"; -type Props = { - children: React.ReactNode; - fetchApi?: MarketDataApi; - countervalue?: Currency; - initState?: Partial; -}; -type API = { - refresh: (param?: MarketListRequestParams) => void; - refreshChart: (param?: MarketCurrencyChartDataRequestParams) => void; - selectCurrency: (id?: string, data?: CurrencyData, range?: string) => void; - loadNextPage: (startIndex?: number, stopIndex?: number) => void | Promise; - setCounterCurrency: (counterCurrency: string) => void; -}; - -export type MarketDataContextType = State & API; - -const initialState: State = { - ready: false, - marketData: [], - selectedCurrency: undefined, - requestParams: { - range: "24h", - limit: 50, - ids: [], - starred: [], - orderBy: "market_cap", - order: "desc", - search: "", - liveCompatible: false, - }, - page: 1, - chartRequestParams: { - range: "24h", - }, - loading: false, - loadingChart: false, - endOfList: false, - error: undefined, - totalCoinsAvailable: 0, - supportedCounterCurrencies: [], - selectedCoinData: undefined, - counterCurrency: undefined, -}; - -const MarketDataContext = createContext({ - ...initialState, - refresh: () => {}, - refreshChart: () => {}, - selectCurrency: () => {}, - loadNextPage: () => Promise.resolve(), - setCounterCurrency: () => {}, -}); - -const ACTIONS = { - IS_READY: "IS_READY", - - UPDATE_MARKET_DATA: "UPDATE_MARKET_DATA", - UPDATE_SINGLE_MARKET_DATA: "UPDATE_SINGLE_MARKET_DATA", - UPDATE_SINGLE_CHART_DATA: "UPDATE_SINGLE_CHART_DATA", - - REFRESH_MARKET_DATA: "REFRESH_MARKET_DATA", - REFRESH_CHART_DATA: "REFRESH_CHART_DATA", - - SET_LOADING: "SET_LOADING", - SET_LOADING_CHART: "SET_LOADING_CHART", - SET_ERROR: "SET_ERROR", - - SELECT_CURRENCY: "SELECT_CURRENCY", - UPDATE_COUNTERVALUE: "UPDATE_COUNTERVALUE", -}; - -function marketDataReducer(state, action) { - switch (action.type) { - case ACTIONS.IS_READY: - return { ...state, ...action.payload, isReady: true }; - case ACTIONS.UPDATE_MARKET_DATA: { - const newData = action.payload.marketData; - const page = action.payload.page || state.requestParams.page; - const marketData = [...state.marketData.map(data => ({ ...data }))]; - if (!newData.length || marketData.some(({ id }) => id === newData[0].id)) - return { ...state, loading: false }; - - return { - ...state, - marketData: marketData.concat(newData), - endOfList: newData.length < state.requestParams.limit, - loading: false, - page, - }; - } - case ACTIONS.UPDATE_SINGLE_MARKET_DATA: { - return { - ...state, - selectedCoinData: { ...state.selectedCoinData, ...action.payload }, - loading: false, - }; - } - case ACTIONS.UPDATE_SINGLE_CHART_DATA: { - const chartRequestParams = { - ...state.chartRequestParams, - loadingChart: false, - }; - const selectedCoinData = { ...state.selectedCoinData }; - - selectedCoinData.chartData = { - ...(selectedCoinData?.chartData ?? {}), - ...(action?.payload?.chartData ?? {}), - }; - - return { ...state, selectedCoinData, chartRequestParams }; - } - - case ACTIONS.REFRESH_MARKET_DATA: { - const requestParams = { - ...state.requestParams, - ...action.payload, - lastRequestTime: Date.now(), - }; - return { - ...state, - marketData: [], - requestParams, - loading: true, - page: 1, - }; - } - case ACTIONS.REFRESH_CHART_DATA: { - const chartRequestParams = { - ...state.chartRequestParams, - ...action.payload, - loadingChart: true, - lastRequestTime: Date.now(), - }; - return { ...state, chartRequestParams }; - } - - case ACTIONS.UPDATE_COUNTERVALUE: { - const requestParams = { - ...state.requestParams, - lastRequestTime: Date.now(), - page: 1, - counterCurrency: action.payload, - }; - const chartRequestParams = { - ...state.chartRequestParams, - lastRequestTime: Date.now(), - counterCurrency: action.payload, - }; - return { - ...state, - counterCurrency: action.payload, - marketData: [], - requestParams, - chartRequestParams, - loading: true, - }; - } - - case ACTIONS.SET_LOADING: - return { ...state, loading: action.payload }; - case ACTIONS.SET_LOADING_CHART: - return { ...state, loadingChart: action.payload }; - case ACTIONS.SET_ERROR: - return { ...state, error: action.payload, loading: false }; - case ACTIONS.SELECT_CURRENCY: { - const chartRequestParams = { - ...state.chartRequestParams, - counterCurrency: state.requestParams.counterCurrency, - range: action.payload.range || state.requestParams.range, - id: action.payload.id, - loadingChart: !!action.payload.id, - lastRequestTime: Date.now(), - }; - return { - ...state, - selectedCurrency: action.payload.id, - selectedCoinData: action.payload.data, - loading: !!action.payload.id, - chartRequestParams, - }; - } - default: - return state; - } -} - -export const MarketDataProvider = ({ - children, - fetchApi, - countervalue, - initState = {}, -}: Props): ReactElement => { - const [state, dispatch] = useReducer(marketDataReducer, { - ...initialState, - ...initState, - }); - const api = fetchApi || defaultFetchApi; - const { requestParams, chartRequestParams, loading, loadingChart, page, selectedCoinData } = - useDebounce(state, 300); - - const handleError = useCallback((payload: Error) => { - dispatch({ type: ACTIONS.SET_ERROR, payload }); - }, []); - - useEffect(() => { - if (countervalue) { - const ticker = countervalue.ticker.toLowerCase(); - api.supportedCounterCurrencies().then( - supportedCounterCurrencies => - api.setSupportedCoinsList().then((coins: SupportedCoins) => { - dispatch({ - type: ACTIONS.IS_READY, - payload: { - totalCoinsAvailable: coins.length, - supportedCounterCurrencies, - }, - }); - dispatch({ - type: ACTIONS.UPDATE_COUNTERVALUE, - payload: supportedCounterCurrencies.includes(ticker) ? ticker : "usd", - }); - }, handleError), - handleError, - ); - } - }, [api, countervalue, handleError]); - - useEffect(() => { - if (chartRequestParams?.id && chartRequestParams?.counterCurrency && !loadingChart) { - const range = chartRequestParams.range; - - if (selectedCoinData && !selectedCoinData?.chartData?.[range]) { - api.currencyChartData(chartRequestParams).then( - chartData => - dispatch({ - type: ACTIONS.UPDATE_SINGLE_CHART_DATA, - payload: { id: chartRequestParams.id, chartData }, - }), - handleError, - ); - } else - dispatch({ - type: ACTIONS.SET_LOADING_CHART, - payload: false, - }); - } - }, [chartRequestParams, selectedCoinData, api, handleError, loadingChart]); - - useEffect(() => { - if (chartRequestParams?.id && chartRequestParams?.counterCurrency) { - dispatch({ type: ACTIONS.SET_LOADING, payload: true }); - api - .listPaginated({ - ...chartRequestParams, - search: "", - starred: [], - liveCompatible: false, - ids: [chartRequestParams.id], - limit: 1, - page: 1, - }) - .then(([marketData]) => { - if (marketData) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { chartData, ...rest } = marketData; - dispatch({ - type: ACTIONS.UPDATE_SINGLE_MARKET_DATA, - payload: rest, - }); - } - }, handleError); - } - }, [api, chartRequestParams, handleError, loading]); - - useEffect(() => { - if (requestParams?.counterCurrency) { - api.listPaginated(requestParams).then( - marketData => - dispatch({ - type: ACTIONS.UPDATE_MARKET_DATA, - payload: { marketData }, - }), - handleError, - ); - } - }, [api, handleError, requestParams]); - - const refresh = useCallback((payload = {}) => { - dispatch({ type: ACTIONS.REFRESH_MARKET_DATA, payload }); - }, []); - - const refreshChart = useCallback((payload = {}) => { - dispatch({ type: ACTIONS.REFRESH_CHART_DATA, payload }); - }, []); - - const selectCurrency = useCallback((id, data, range) => { - dispatch({ type: ACTIONS.SELECT_CURRENCY, payload: { id, data, range } }); - }, []); - - const loadNextPage = useCallback( - () => - new Promise((resolve, reject) => { - if (loading) { - reject(new Error()); - } else { - const newPage = page + 1; - api.listPaginated({ ...requestParams, page: newPage }).then( - marketData => { - dispatch({ - type: ACTIONS.UPDATE_MARKET_DATA, - payload: { marketData, page: newPage }, - }); - resolve(true); - }, - err => { - handleError(err); - reject(new Error(err)); - }, - ); - } - }), - [loading, page, api, requestParams, handleError], - ); - - const setCounterCurrency = useCallback( - payload => dispatch({ type: ACTIONS.UPDATE_COUNTERVALUE, payload }), - [dispatch], - ); - - const value = { - ...state, - refresh, - refreshChart, - selectCurrency, - loadNextPage, - setCounterCurrency, - }; - - return {children}; -}; - -export function useMarketData(): MarketDataContextType { - return useContext(MarketDataContext); -} - -export type SingleCoinProviderData = SingleCoinState & { - selectCurrency: (id?: string, data?: CurrencyData, range?: string) => void; - refreshChart: (param?: MarketCurrencyChartDataRequestParams) => void; - setCounterCurrency: (counterCurrency: string) => void; -}; - -export function useSingleCoinMarketData(): SingleCoinProviderData { - const { - selectedCurrency, - selectedCoinData, - selectCurrency, - chartRequestParams, - loading, - loadingChart, - error, - counterCurrency, - refreshChart, - setCounterCurrency, - supportedCounterCurrencies, - } = useContext(MarketDataContext); - - return { - selectedCurrency, - selectedCoinData, - selectCurrency, - chartRequestParams, - loading, - loadingChart, - error, - counterCurrency, - refreshChart, - setCounterCurrency, - supportedCounterCurrencies, - }; -} diff --git a/libs/ledger-live-common/src/market/api/api.mock.ts b/libs/ledger-live-common/src/market/api/api.mock.ts deleted file mode 100644 index 16b76ccb79fb..000000000000 --- a/libs/ledger-live-common/src/market/api/api.mock.ts +++ /dev/null @@ -1,403 +0,0 @@ -import { CurrencyData, MarketCoin, MarketListRequestParams, SupportedCoins } from "../types"; -import { listCryptoCurrencies, listTokens } from "../../currencies"; - -const cryptoCurrenciesList = [...listCryptoCurrencies(), ...listTokens()]; - -async function setSupportedCoinsList(): Promise { - const response = await Promise.resolve([ - { id: "bitcoin", symbol: "btc", name: "Bitcoin" }, - { id: "ethereum", symbol: "eth", name: "Ethereum" }, - { id: "ethereum-apex", symbol: "eapex", name: "Ethereum Apex" }, - { id: "ethereum-cash", symbol: "ecash", name: "Ethereum Cash" }, - ]); - - return response; -} - -const matchSearch = - (search: string) => - (currency: MarketCoin): boolean => { - if (!search) return false; - const match = `${currency.symbol}|${currency.name}`; - return match.toLowerCase().includes(search.toLowerCase()); - }; - -const paginatedData = [ - { - id: "bitcoin", - symbol: "btc", - name: "Bitcoin", - image: "https://assets.coingecko.com/coins/images/1/large/bitcoin.png?1547033579", - current_price: 46978, - market_cap: 888084713113, - market_cap_rank: 1, - fully_diluted_valuation: 986540621536, - total_volume: 27166733732, - high_24h: 47466, - low_24h: 45653, - price_change_24h: -448.517772161074, - price_change_percentage_24h: -0.94571, - market_cap_change_24h: -6925208740.524048, - market_cap_change_percentage_24h: -0.77376, - circulating_supply: 18904218, - total_supply: 21000000, - max_supply: 21000000, - ath: 69045, - ath_change_percentage: -32.0311, - ath_date: "2021-11-10T14:24:11.849Z", - atl: 67.81, - atl_change_percentage: 69107.58322, - atl_date: "2013-07-06T00:00:00.000Z", - roi: null, - last_updated: "2021-12-18T16:31:02.628Z", - price_change_percentage_24h_in_currency: -0.9457084615407927, - }, - { - id: "ethereum", - symbol: "eth", - name: "Ethereum", - image: "https://assets.coingecko.com/coins/images/279/large/ethereum.png?1595348880", - current_price: 3956.23, - market_cap: 469967313961, - market_cap_rank: 2, - fully_diluted_valuation: null, - total_volume: 22896047898, - high_24h: 3998.6, - low_24h: 3790.1, - price_change_24h: 31.34, - price_change_percentage_24h: 0.79852, - market_cap_change_24h: 4376247843, - market_cap_change_percentage_24h: 0.93993, - circulating_supply: 118791776.124, - total_supply: null, - max_supply: null, - ath: 4878.26, - ath_change_percentage: -19.07897, - ath_date: "2021-11-10T14:24:19.604Z", - atl: 0.432979, - atl_change_percentage: 911616.29321, - atl_date: "2015-10-20T00:00:00.000Z", - roi: { - times: 111.85400578727416, - currency: "btc", - percentage: 11185.400578727416, - }, - last_updated: "2021-12-18T16:30:24.123Z", - price_change_percentage_24h_in_currency: 0.7985206175407733, - }, - { - id: "binancecoin", - symbol: "bnb", - name: "Binance Coin", - image: "https://assets.coingecko.com/coins/images/825/large/binance-coin-logo.png?1547034615", - current_price: 530.99, - market_cap: 89278760378, - market_cap_rank: 3, - fully_diluted_valuation: 89278760378, - total_volume: 601931681, - high_24h: 536.2, - low_24h: 519.35, - price_change_24h: -1.431695982805, - price_change_percentage_24h: -0.2689, - market_cap_change_24h: -217891640.56773376, - market_cap_change_percentage_24h: -0.24346, - circulating_supply: 168137035.9, - total_supply: 168137035.9, - max_supply: 168137035.9, - ath: 686.31, - ath_change_percentage: -22.80102, - ath_date: "2021-05-10T07:24:17.097Z", - atl: 0.0398177, - atl_change_percentage: 1330518.574, - atl_date: "2017-10-19T00:00:00.000Z", - roi: null, - last_updated: "2021-12-18T16:30:40.065Z", - price_change_percentage_24h_in_currency: -0.26890361163187976, - }, - { - id: "tether", - symbol: "usdt", - name: "Tether", - image: "https://assets.coingecko.com/coins/images/325/large/Tether-logo.png?1598003707", - current_price: 1, - market_cap: 77439200796, - market_cap_rank: 4, - fully_diluted_valuation: null, - total_volume: 62554588893, - high_24h: 1.01, - low_24h: 0.987325, - price_change_24h: -0.011415856662, - price_change_percentage_24h: -1.1271, - market_cap_change_24h: -259872290.71488953, - market_cap_change_percentage_24h: -0.33446, - circulating_supply: 77328369536.52, - total_supply: 77328369536.52, - max_supply: null, - ath: 1.32, - ath_change_percentage: -24.41245, - ath_date: "2018-07-24T00:00:00.000Z", - atl: 0.572521, - atl_change_percentage: 74.68274, - atl_date: "2015-03-02T00:00:00.000Z", - roi: null, - last_updated: "2021-12-18T16:27:59.344Z", - price_change_percentage_24h_in_currency: -1.1271033891153508, - }, - { - id: "solana", - symbol: "sol", - name: "Solana", - image: "https://assets.coingecko.com/coins/images/4128/large/Solana.jpg?1635329178", - current_price: 182.14, - market_cap: 55791855479, - market_cap_rank: 5, - fully_diluted_valuation: null, - total_volume: 1864966986, - high_24h: 182.76, - low_24h: 171.62, - price_change_24h: 1.27, - price_change_percentage_24h: 0.70456, - market_cap_change_24h: 1062110605, - market_cap_change_percentage_24h: 1.94065, - circulating_supply: 307978914.997809, - total_supply: 508180963.57, - max_supply: null, - ath: 259.96, - ath_change_percentage: -30.21122, - ath_date: "2021-11-06T21:54:35.825Z", - atl: 0.500801, - atl_change_percentage: 36126.43086, - atl_date: "2020-05-11T19:35:23.449Z", - roi: null, - last_updated: "2021-12-18T16:31:38.005Z", - price_change_percentage_24h_in_currency: 0.704558493197451, - }, - { - id: "usd-coin", - symbol: "usdc", - name: "USD Coin", - image: "https://assets.coingecko.com/coins/images/6319/large/USD_Coin_icon.png?1547042389", - current_price: 0.999438, - market_cap: 41947712099, - market_cap_rank: 6, - fully_diluted_valuation: null, - total_volume: 3610141997, - high_24h: 1.01, - low_24h: 0.992322, - price_change_24h: -0.008300533333, - price_change_percentage_24h: -0.82368, - market_cap_change_24h: -15466850.668174744, - market_cap_change_percentage_24h: -0.03686, - circulating_supply: 41988882706.8525, - total_supply: 41974529053.5122, - max_supply: null, - ath: 1.17, - ath_change_percentage: -14.81075, - ath_date: "2019-05-08T00:40:28.300Z", - atl: 0.891848, - atl_change_percentage: 12.0168, - atl_date: "2021-05-19T13:14:05.611Z", - roi: null, - last_updated: "2021-12-18T16:31:29.480Z", - price_change_percentage_24h_in_currency: -0.823678944021661, - }, - { - id: "cardano", - symbol: "ada", - name: "Cardano", - image: "https://assets.coingecko.com/coins/images/975/large/cardano.png?1547034860", - current_price: 1.25, - market_cap: 40061737912, - market_cap_rank: 7, - fully_diluted_valuation: 56220178462, - total_volume: 1039172214, - high_24h: 1.27, - low_24h: 1.2, - price_change_24h: -0.00949416665, - price_change_percentage_24h: -0.7542, - market_cap_change_24h: -35615245.100372314, - market_cap_change_percentage_24h: -0.08882, - circulating_supply: 32066390668.4135, - total_supply: 45000000000, - max_supply: 45000000000, - ath: 3.09, - ath_change_percentage: -59.59586, - ath_date: "2021-09-02T06:00:10.474Z", - atl: 0.01925275, - atl_change_percentage: 6378.24434, - atl_date: "2020-03-13T02:22:55.044Z", - roi: null, - last_updated: "2021-12-18T16:31:09.050Z", - price_change_percentage_24h_in_currency: -0.754204745255466, - }, -]; -// fetches currencies data for selected currencies ids -async function listPaginated({ - search = "", - starred = [], - order = "desc", - range = "24h", - ids = [], -}: MarketListRequestParams): Promise { - const response = await Promise.resolve(paginatedData); - - let filteredResponse = response; - - if (order !== "desc") { - filteredResponse = filteredResponse.sort((x, y) => y.market_cap_rank - x.market_cap_rank); - } - - if (search) { - filteredResponse = filteredResponse.filter(matchSearch(search)); - } - - if (starred.length > 0) { - filteredResponse = filteredResponse.filter(currency => starred.includes(currency.id)); - } - - if (ids.length > 0) { - filteredResponse = filteredResponse.filter(currency => ids.includes(currency.id)); - } - - // @ts-expect-error issue in typing - return filteredResponse.map((currency: any) => ({ - id: currency.id, - name: currency.name, - image: currency.image, - internalCurrency: cryptoCurrenciesList.find( - ({ ticker }) => ticker.toLowerCase() === currency.symbol, - ), - marketcap: currency.market_cap, - marketcapRank: currency.market_cap_rank, - totalVolume: currency.total_volume, - high24h: currency.high_24h, - low24h: currency.low_24h, - ticker: currency.symbol, - price: currency.current_price, - priceChangePercentage: - range !== "24h" - ? currency.price_change_percentage_24h_in_currency * 7 - : currency.price_change_percentage_24h_in_currency, - marketCapChangePercentage24h: currency.market_cap_change_percentage_24h, - circulatingSupply: currency.circulating_supply, - totalSupply: currency.total_supply, - maxSupply: currency.max_supply, - ath: currency.ath, - athDate: currency.ath_date, - atl: currency.atl, - atlDate: currency.atl_date, - sparklineIn7d: null, - chartData: [], - })); -} - -// Fetches list of supported counterCurrencies -async function supportedCounterCurrencies(): Promise { - return await Promise.resolve([ - "btc", - "eth", - "ltc", - "bch", - "bnb", - "eos", - "xrp", - "xlm", - "link", - "dot", - "yfi", - "usd", - "aed", - "ars", - "aud", - "bdt", - "bhd", - "bmd", - "brl", - "cad", - "chf", - "clp", - "cny", - "czk", - "dkk", - "eur", - "gbp", - "hkd", - "huf", - "idr", - "ils", - "inr", - "jpy", - "krw", - "kwd", - "lkr", - "mmk", - "mxn", - "myr", - "ngn", - "nok", - "nzd", - "php", - "pkr", - "pln", - "rub", - "sar", - "sek", - "sgd", - "thb", - "try", - "twd", - "uah", - "vef", - "vnd", - "zar", - "xdr", - "xag", - "xau", - "bits", - "sats", - ]); -} - -// Fetches list of supported currencies -async function currencyChartData(): Promise<{ - [range: string]: number[][]; -}> { - const response = await Promise.resolve({ - prices: [ - [1639760425566, 47035.7730170554], - [1639764200520, 46779.28084436036], - [1639767793752, 46899.81670459505], - [1639771405996, 46806.89615030911], - [1639774854719, 46372.35410149477], - [1639778429112, 47091.66371795489], - [1639782342710, 46507.965042542106], - [1639785697940, 46328.6963654447], - [1639789279103, 46415.52050647275], - [1639793084532, 45826.54707719198], - [1639796561743, 45886.69785964224], - [1639800325023, 46535.85544547407], - [1639803752264, 46630.66364205827], - [1639807349369, 46796.65748955589], - [1639810831681, 46404.12378055947], - [1639814720824, 46429.86171498202], - [1639818226026, 46208.467967110075], - [1639821761024, 46794.55365462657], - [1639825359138, 47337.122321658026], - [1639829106656, 47070.8445217621], - [1639832543681, 47113.48747087277], - [1639836121826, 46857.76684001326], - [1639839860258, 47145.895183572036], - [1639843597758, 46876.27784614691], - [1639845270000, 47035.775042793095], - ], - }); - - return { "24h": response.prices }; -} - -export default { - setSupportedCoinsList, - listPaginated, - supportedCounterCurrencies, - currencyChartData, -}; diff --git a/libs/ledger-live-common/src/market/api/api.ts b/libs/ledger-live-common/src/market/api/api.ts deleted file mode 100644 index b822851e5fad..000000000000 --- a/libs/ledger-live-common/src/market/api/api.ts +++ /dev/null @@ -1,310 +0,0 @@ -import network from "@ledgerhq/live-network/network"; -import { CryptoCurrency } from "@ledgerhq/types-cryptoassets"; -import { listCryptoCurrencies, listSupportedCurrencies, listTokens } from "../../currencies"; -import { getEnv } from "@ledgerhq/live-env"; -import { - CurrencyData, - MarketCoin, - MarketCurrencyChartDataRequestParams, - MarketListRequestParams, - RawCurrencyData, - MarketPerformersParams, - MarketItemResponse, - SupportedCoins, - MarketCurrencyRequestParams, -} from "../types"; -import { rangeDataTable } from "../utils/rangeDataTable"; -import { currencyFormatter } from "../utils/currencyFormatter"; -import URL from "url"; -import { getRange } from "../utils/rangeFormatter"; - -const cryptoCurrenciesList = [...listCryptoCurrencies(), ...listTokens()]; - -const supportedCurrencies = listSupportedCurrencies(); - -const liveCompatibleIds: string[] = supportedCurrencies - .map(({ id }: CryptoCurrency) => id) - .filter(Boolean); - -let LIVE_COINS_LIST: string[] = []; - -const baseURL = () => getEnv("LEDGER_COUNTERVALUES_API"); -const ROOT_PATH = getEnv("MARKET_API_URL"); - -let SUPPORTED_COINS_LIST: SupportedCoins = []; - -export async function setSupportedCoinsList(): Promise { - const url = `${ROOT_PATH}/coins/list`; - const { data } = await network({ method: "GET", url }); - - SUPPORTED_COINS_LIST = data; - - LIVE_COINS_LIST = SUPPORTED_COINS_LIST.filter(({ id }) => liveCompatibleIds.includes(id)).map( - ({ id }) => id, - ); - - return SUPPORTED_COINS_LIST; -} - -export async function getSupportedCoinsList(): Promise { - const url = `${ROOT_PATH}/coins/list`; - const { data } = await network({ method: "GET", url }); - return data; -} - -const matchSearch = - (search: string) => - (currency: MarketCoin): boolean => { - if (!search) return false; - const match = `${currency.symbol}|${currency.name}`; - return match.toLowerCase().includes(search.toLowerCase()); - }; - -// fetches currencies data for selected currencies ids -async function listPaginated({ - counterCurrency, - range = "24h", - limit = 50, - page = 1, - ids: _ids = [], - starred = [], - orderBy = "market_cap", - order = "desc", - search = "", - sparkline = true, - liveCompatible = false, - top100 = false, -}: MarketListRequestParams): Promise { - let ids = _ids; - - if (top100) { - limit = 100; - } else { - if (search) { - ids = SUPPORTED_COINS_LIST.filter(matchSearch(search)).map(({ id }) => id); - if (!ids.length) { - return []; - } - } - - if (liveCompatible) { - if (ids.length > 0) { - ids = LIVE_COINS_LIST.filter(id => ids.includes(id)); - } else { - ids = ids.concat(LIVE_COINS_LIST); - } - } - - if (starred.length > 0) { - if (ids.length > 0) { - ids = starred.filter(id => ids.includes(id)); - } else { - ids = ids.concat(starred); - } - } - } - - ids = ids.slice((page - 1) * limit, limit * page); - - const url = - `${ROOT_PATH}/coins/markets?vs_currency=${counterCurrency}&order=${orderBy}_${order}&per_page=${limit}` + - `&sparkline=${sparkline ? "true" : "false"}&price_change_percentage=${range}` + - `${ids.length > 0 ? `&page=1&&ids=${ids.toString()}` : `&page=${page}`}`; - - let { data } = await network({ - method: "GET", - url, - }); - - if (top100) { - // Perform a search by the user's input and order the result by change in percentage - data = data - .filter(currency => { - if (!search) return true; - const match = `${currency.symbol}|${currency.name}`; - return match.toLowerCase().includes(search.toLowerCase()); - }) - .sort( - (a, b) => - b[`price_change_percentage_${range}_in_currency`] - - a[`price_change_percentage_${range}_in_currency`], - ); - } - - return currencyFormatter(data, range, cryptoCurrenciesList); -} - -// fetches currencies data for selected currencies ids -export async function fetchList({ - counterCurrency, - range = "24h", - limit = 50, - page = 1, - ids: _ids = [], - starred = [], - orderBy = "market_cap", - order = "desc", - search = "", - sparkline = true, - liveCompatible = false, - top100 = false, - supportedCoinsList = [], - liveCoinsList = [], -}: MarketListRequestParams): Promise { - let ids = _ids; - - if (top100) { - limit = 100; - } else { - if (search) { - ids = supportedCoinsList.filter(matchSearch(search)).map(({ id }) => id); - if (!ids.length) { - return []; - } - } - - if (liveCompatible) { - if (ids.length > 0) { - ids = liveCoinsList.filter(id => ids.includes(id)); - } else { - ids = ids.concat(liveCoinsList); - } - } - - if (starred.length > 0) { - if (ids.length > 0) { - ids = starred.filter(id => ids.includes(id)); - } else { - ids = ids.concat(starred); - } - } - } - - ids = ids.slice((page - 1) * limit, limit * page); - - const url = - `${ROOT_PATH}/coins/markets?vs_currency=${counterCurrency}&order=${orderBy}_${order}&per_page=${limit}` + - `&sparkline=${sparkline ? "true" : "false"}&price_change_percentage=${range}` + - `${ids.length > 0 ? `&page=1&&ids=${ids.toString()}` : `&page=${page}`}`; - - if ((starred.length > 0 || search.length > 0) && ids.length === 0) return []; - - let { data } = await network({ - method: "GET", - url, - }); - - if (top100) { - // Perform a search by the user's input and order the result by change in percentage - data = data - .filter(currency => { - if (!search) return true; - const match = `${currency.symbol}|${currency.name}`; - return match.toLowerCase().includes(search.toLowerCase()); - }) - .sort( - (a, b) => - b[`price_change_percentage_${range}_in_currency`] - - a[`price_change_percentage_${range}_in_currency`], - ); - } - - return data; -} - -// Fetches list of supported counterCurrencies -export async function supportedCounterCurrencies(): Promise { - const url = `${ROOT_PATH}/simple/supported_vs_currencies`; - - const { data } = await network({ - method: "GET", - url, - }); - - return data; -} - -export async function fetchCurrencyChartData({ - id, - counterCurrency, - range = "24h", -}: MarketCurrencyChartDataRequestParams): Promise> { - const { days, interval } = rangeDataTable[range]; - - const url = `${ROOT_PATH}/coins/${id}/market_chart?vs_currency=${counterCurrency}&days=${days}&interval=${interval}`; - - const { data } = await network({ - method: "GET", - url, - }); - - return { [range]: data.prices }; -} - -export async function fetchCurrencyData({ - counterCurrency, - range = "24h", - id, -}: MarketCurrencyRequestParams): Promise { - const url = URL.format({ - pathname: `${ROOT_PATH}/coins/markets`, - query: { - vs_currency: counterCurrency, - sparkline: true, - price_change_percentage: range, - page: 1, - ids: id, - }, - }); - - const { data } = await network({ - method: "GET", - url, - }); - - return data[0]; -} - -export async function fetchCurrency({ id }: MarketCurrencyRequestParams): Promise { - const url = `${ROOT_PATH}/coins/${id}`; - - const { data } = await network({ - method: "GET", - url, - }); - - return data; -} - -export async function fetchMarketPerformers({ - counterCurrency, - range, - limit = 5, - top = 50, - sort, - supported, -}: MarketPerformersParams): Promise { - const sortParam = `${sort === "asc" ? "positive" : "negative"}-price-change-${getRange(range)}`; - - const url = URL.format({ - pathname: `${baseURL()}/v3/markets`, - query: { - to: counterCurrency, - limit, - top, - sort: sortParam, - supported, - }, - }); - - const { data } = await network({ method: "GET", url }); - - return data; -} - -export default { - setSupportedCoinsList, - listPaginated, - supportedCounterCurrencies, - currencyChartData: fetchCurrencyChartData, -}; diff --git a/libs/ledger-live-common/src/market/api/index.ts b/libs/ledger-live-common/src/market/api/index.ts new file mode 100644 index 000000000000..b9d771c492cd --- /dev/null +++ b/libs/ledger-live-common/src/market/api/index.ts @@ -0,0 +1,138 @@ +import network from "@ledgerhq/live-network/network"; +import { getEnv } from "@ledgerhq/live-env"; +import { + MarketCurrencyChartDataRequestParams, + MarketListRequestParams, + MarketPerformersParams, + MarketItemResponse, + SupportedCoins, + MarketCurrencyRequestParams, + MarketCoinDataChart, + Order, +} from "../utils/types"; +import { rangeDataTable } from "../utils/rangeDataTable"; +import URL from "url"; +import { getRange, getSortParam } from "../utils"; + +const baseURL = () => getEnv("LEDGER_COUNTERVALUES_API"); +const ROOT_PATH = getEnv("MARKET_API_URL"); + +export async function getSupportedCoinsList(): Promise { + const url = `${ROOT_PATH}/coins/list`; + const { data } = await network({ method: "GET", url }); + return data; +} + +// fetches currencies data for selected currencies ids +export async function fetchList({ + counterCurrency, + limit = 50, + page = 1, + order = Order.MarketCapDesc, + search = "", + liveCoinsList = [], + starred = [], + range = "24", +}: MarketListRequestParams): Promise { + const url = URL.format({ + pathname: `${baseURL()}/v3/markets`, + query: { + page: page, + pageSize: limit, + to: counterCurrency, + sort: getSortParam(order, range), + ...(search.length >= 1 && { filter: search }), + ...(starred.length > 0 && { ids: starred.join(",") }), + ...(liveCoinsList.length > 1 && { ids: liveCoinsList.join(",") }), + ...([Order.topLosers, Order.topGainers].includes(order) && { top: 100 }), + }, + }); + + const { data } = await network({ + method: "GET", + url, + }); + + return data; +} + +// Fetches list of supported counterCurrencies +export async function supportedCounterCurrencies(): Promise { + const url = `${ROOT_PATH}/simple/supported_vs_currencies`; + + const { data } = await network({ + method: "GET", + url, + }); + + return data; +} + +export async function fetchCurrencyChartData({ + id, + counterCurrency, + range = "24h", +}: MarketCurrencyChartDataRequestParams): Promise { + const { days, interval } = rangeDataTable[range]; + + const url = URL.format({ + pathname: `${ROOT_PATH}/coins/${id}/market_chart`, + query: { + vs_currency: counterCurrency, + days, + interval, + }, + }); + + const { data } = await network({ + method: "GET", + url, + }); + + return { [range]: data.prices }; +} + +export async function fetchCurrency({ + counterCurrency, + id, +}: MarketCurrencyRequestParams): Promise { + const url = URL.format({ + pathname: `${baseURL()}/v3/markets`, + query: { + to: counterCurrency, + ids: id, + pageSize: 1, + limit: 1, + }, + }); + + const { data } = await network({ method: "GET", url }); + + return data[0]; +} + +export async function fetchMarketPerformers({ + counterCurrency, + range, + limit = 5, + top = 50, + sort, + supported, +}: MarketPerformersParams): Promise { + const sortParam = `${sort === "asc" ? "positive" : "negative"}-price-change-${getRange(range)}`; + + const url = URL.format({ + pathname: `${baseURL()}/v3/markets`, + query: { + to: counterCurrency, + limit, + top, + sort: sortParam, + supported, + }, + }); + + const { data } = await network({ method: "GET", url }); + + return data; +} diff --git a/libs/ledger-live-common/src/market/v2/useMarketDataProvider.ts b/libs/ledger-live-common/src/market/hooks/useMarketDataProvider.ts similarity index 71% rename from libs/ledger-live-common/src/market/v2/useMarketDataProvider.ts rename to libs/ledger-live-common/src/market/hooks/useMarketDataProvider.ts index 3a4ee90d2042..dcc8054a724d 100644 --- a/libs/ledger-live-common/src/market/v2/useMarketDataProvider.ts +++ b/libs/ledger-live-common/src/market/hooks/useMarketDataProvider.ts @@ -2,26 +2,28 @@ import { UseQueryResult, useQueries, useQuery } from "@tanstack/react-query"; import { fetchCurrency, fetchCurrencyChartData, - fetchCurrencyData, fetchList, getSupportedCoinsList, supportedCounterCurrencies, -} from "../api/api"; +} from "../api"; +import { listCryptoCurrencies } from "@ledgerhq/cryptoassets/currencies"; +import { listTokens } from "@ledgerhq/cryptoassets/tokens"; +import { CryptoCurrency } from "@ledgerhq/types-cryptoassets"; + +import { useMemo } from "react"; +import { listSupportedCurrencies } from "../../currencies"; +import { currencyFormatter, format } from "../utils/currencyFormatter"; +import { QUERY_KEY } from "../utils/queryKeys"; +import { REFETCH_TIME_ONE_MINUTE, BASIC_REFETCH, ONE_DAY } from "../utils/timers"; import { - CurrencyData, - HashMapBody, MarketCurrencyRequestParams, MarketListRequestParams, + CurrencyData, + HashMapBody, + MarketItemResponse, MarketListRequestResult, - RawCurrencyData, -} from "../types"; -import { QUERY_KEY } from "./queryKeys"; -import { listCryptoCurrencies, listSupportedCurrencies, listTokens } from "../../currencies"; -import { CryptoCurrency } from "@ledgerhq/types-cryptoassets"; -import { useMemo } from "react"; - -import { currencyFormatter, format } from "../utils/currencyFormatter"; -import { BASIC_REFETCH, ONE_DAY, REFETCH_TIME_ONE_MINUTE } from "./timers"; + Order, +} from "../utils/types"; const cryptoCurrenciesList = [...listCryptoCurrencies(), ...listTokens()]; @@ -57,26 +59,15 @@ export const useCurrencyChartData = ({ id, counterCurrency, range }: MarketCurre staleTime: REFETCH_TIME_ONE_MINUTE * BASIC_REFETCH, }); -export function useCurrencyData({ id, counterCurrency, range }: MarketCurrencyRequestParams) { - const resultCurrencyData = useQuery({ - queryKey: [QUERY_KEY.CurrencyData, id, counterCurrency, range], - queryFn: () => fetchCurrencyData({ counterCurrency, range, id }), - refetchInterval: REFETCH_TIME_ONE_MINUTE * BASIC_REFETCH, - staleTime: REFETCH_TIME_ONE_MINUTE * BASIC_REFETCH, - select: (data: RawCurrencyData) => format(data, range ?? "24h", cryptoCurrenciesList), - }); - - const resultCurrency = useQuery({ - queryKey: [QUERY_KEY.CurrencyDataRaw, id], - queryFn: () => fetchCurrency({ id }), +export const useCurrencyData = ({ id, counterCurrency }: MarketCurrencyRequestParams) => + useQuery({ + queryKey: [QUERY_KEY.CurrencyDataRaw, id, counterCurrency], + queryFn: () => fetchCurrency({ id, counterCurrency }), refetchInterval: REFETCH_TIME_ONE_MINUTE * BASIC_REFETCH, staleTime: REFETCH_TIME_ONE_MINUTE * BASIC_REFETCH, - select: data => format(data, "24h", cryptoCurrenciesList), + select: data => format(data, cryptoCurrenciesList), }); - return { currencyData: resultCurrencyData, currencyInfo: resultCurrency }; -} - export const useSupportedCounterCurrencies = () => useQuery({ queryKey: [QUERY_KEY.SupportedCounterCurrencies], @@ -95,14 +86,24 @@ export const useSupportedCurrencies = () => export function useMarketData(props: MarketListRequestParams): MarketListRequestResult { return useQueries({ - queries: Array.from({ length: props.page ?? 1 }, (_, i) => i + 1).map(page => ({ + queries: Array.from({ length: props.page ?? 1 }, (_, i) => i).map(page => ({ queryKey: [ QUERY_KEY.MarketData, - { ...props, page, liveCoinsList: [], supportedCoinsList: [] }, + page, + props.order, + { + counterCurrency: props.counterCurrency, + ...(props.search && props.search?.length >= 1 && { search: props.search }), + ...(props.starred && props.starred?.length >= 1 && { starred: props.starred }), + ...(props.liveCoinsList && + props.liveCoinsList?.length >= 1 && { liveCoinsList: props.liveCoinsList }), + ...(props.order && + [Order.topLosers, Order.topGainers].includes(props.order) && { range: props.range }), + }, ], queryFn: () => fetchList({ ...props, page }), - select: (data: RawCurrencyData[]) => ({ - formattedData: currencyFormatter(data, props.range ?? "24h", cryptoCurrenciesList), + select: (data: MarketItemResponse[]) => ({ + formattedData: currencyFormatter(data, cryptoCurrenciesList), page, }), refetchOnMount: false, diff --git a/libs/ledger-live-common/src/market/v2/useMarketPerformers.ts b/libs/ledger-live-common/src/market/hooks/useMarketPerformers.ts similarity index 81% rename from libs/ledger-live-common/src/market/v2/useMarketPerformers.ts rename to libs/ledger-live-common/src/market/hooks/useMarketPerformers.ts index 909c8e3df30a..b3ea8171d602 100644 --- a/libs/ledger-live-common/src/market/v2/useMarketPerformers.ts +++ b/libs/ledger-live-common/src/market/hooks/useMarketPerformers.ts @@ -1,9 +1,9 @@ -import { fetchMarketPerformers } from "../api/api"; -import { MarketItemPerformer, MarketItemResponse, MarketPerformersParams } from "../types"; -import { QUERY_KEY } from "./queryKeys"; +import { fetchMarketPerformers } from "../api"; +import { MarketItemPerformer, MarketItemResponse, MarketPerformersParams } from "../utils/types"; +import { QUERY_KEY } from "../utils/queryKeys"; import { UseQueryResult, useQuery } from "@tanstack/react-query"; import { formatPerformer } from "../utils/currencyFormatter"; -import { REFETCH_TIME_ONE_MINUTE } from "./timers"; +import { REFETCH_TIME_ONE_MINUTE } from "../utils/timers"; export const useMarketPerformers = ({ counterCurrency, diff --git a/libs/ledger-live-common/src/market/utils/currencyFormatter.ts b/libs/ledger-live-common/src/market/utils/currencyFormatter.ts index ce53f37d3585..5e927f66aa7a 100644 --- a/libs/ledger-live-common/src/market/utils/currencyFormatter.ts +++ b/libs/ledger-live-common/src/market/utils/currencyFormatter.ts @@ -1,11 +1,11 @@ import { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets"; import { CurrencyData, + KeysPriceChange, MarketItemPerformer, MarketItemResponse, - RawCurrencyData, SparklineSvgData, -} from "../types"; +} from "./types"; function distributedCopy(items: number[], n: number): number[] { if (!items) return []; @@ -46,42 +46,46 @@ function sparklineAsSvgData(points: number[]): SparklineSvgData { } export function currencyFormatter( - data: RawCurrencyData[], - range: string, + data: MarketItemResponse[], cryptoCurrenciesList: (CryptoCurrency | TokenCurrency)[], ): CurrencyData[] { - return data.map((currency: RawCurrencyData) => format(currency, range, cryptoCurrenciesList)); + return data.map((currency: MarketItemResponse) => format(currency, cryptoCurrenciesList)); } export const format = ( - currency: RawCurrencyData, - range: string, + currency: MarketItemResponse, cryptoCurrenciesList: (CryptoCurrency | TokenCurrency)[], -) => ({ +): CurrencyData => ({ id: currency.id, name: currency.name, - image: typeof currency.image === "string" ? currency.image : currency.image?.thumb, + image: currency.image, internalCurrency: cryptoCurrenciesList.find( - ({ ticker }) => ticker.toLowerCase() === currency.symbol, + ({ ticker }) => ticker.toLowerCase() === currency.ticker, ), - marketcap: currency.market_cap, - marketcapRank: currency.market_cap_rank, - totalVolume: currency.total_volume, - high24h: currency.high_24h, - low24h: currency.low_24h, - ticker: currency.symbol, - price: currency.current_price, - priceChangePercentage: currency[`price_change_percentage_${range}_in_currency`], - marketCapChangePercentage24h: currency.market_cap_change_percentage_24h, - circulatingSupply: currency.circulating_supply, - totalSupply: currency.total_supply, - maxSupply: currency.max_supply, - ath: currency.ath, - athDate: currency.ath_date, - atl: currency.atl, - atlDate: currency.atl_date, - sparklineIn7d: currency?.sparkline_in_7d?.price - ? sparklineAsSvgData(distributedCopy(currency.sparkline_in_7d.price, 6 * 7)) // keep 6 points per day + marketcap: currency.marketCap, + marketcapRank: currency.marketCapRank, + totalVolume: currency.totalVolume, + high24h: currency.high24h, + low24h: currency.low24h, + ticker: currency.ticker, + price: currency.price, + priceChangePercentage: { + [KeysPriceChange.hour]: currency.priceChangePercentage1h, + [KeysPriceChange.day]: currency.priceChangePercentage24h, + [KeysPriceChange.week]: currency.priceChangePercentage7d, + [KeysPriceChange.month]: currency.priceChangePercentage30d, + [KeysPriceChange.year]: currency.priceChangePercentage1y, + }, + marketCapChangePercentage24h: currency.marketCapChangePercentage24h, + circulatingSupply: currency.circulatingSupply, + totalSupply: currency.totalSupply, + maxSupply: currency.maxSupply, + ath: currency.allTimeHigh, + athDate: new Date(currency.allTimeHighDate), + atl: currency.allTimeLow, + atlDate: new Date(currency.allTimeLowDate), + sparklineIn7d: currency.sparkline + ? sparklineAsSvgData(distributedCopy(currency.sparkline, 6 * 7)) // keep 6 points per day : undefined, chartData: {}, }); diff --git a/libs/ledger-live-common/src/market/utils/index.ts b/libs/ledger-live-common/src/market/utils/index.ts new file mode 100644 index 000000000000..10d683785c61 --- /dev/null +++ b/libs/ledger-live-common/src/market/utils/index.ts @@ -0,0 +1,34 @@ +import { PortfolioRange } from "@ledgerhq/types-live"; +import { Order } from "./types"; + +export function getRange(range: PortfolioRange | string) { + switch (range) { + case "day": + case "24h": + return "1d"; + case "7d": + case "week": + return "1w"; + case "30d": + case "month": + return "1m"; + case "1y": + case "year": + case "all": + return "1y"; + } +} + +export const getSortParam = (order: Order, range: PortfolioRange | string) => { + switch (order) { + default: + case Order.MarketCapDesc: + return "market-cap-rank"; + case Order.MarketCapAsc: + return "market-cap-rank-desc"; + case Order.topLosers: + return `negative-price-change-${getRange(range)}`; + case Order.topGainers: + return `positive-price-change-${getRange(range)}`; + } +}; diff --git a/libs/ledger-live-common/src/market/v2/queryKeys.ts b/libs/ledger-live-common/src/market/utils/queryKeys.ts similarity index 100% rename from libs/ledger-live-common/src/market/v2/queryKeys.ts rename to libs/ledger-live-common/src/market/utils/queryKeys.ts diff --git a/libs/ledger-live-common/src/market/utils/rangeFormatter.ts b/libs/ledger-live-common/src/market/utils/rangeFormatter.ts deleted file mode 100644 index 2a7ecb112c6b..000000000000 --- a/libs/ledger-live-common/src/market/utils/rangeFormatter.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { PortfolioRange } from "@ledgerhq/types-live"; - -export function getRange(range: PortfolioRange) { - switch (range) { - case "day": - return "1d"; - case "week": - return "1w"; - case "month": - return "1m"; - case "year": - case "all": - return "1y"; - } -} diff --git a/libs/ledger-live-common/src/market/v2/timers.ts b/libs/ledger-live-common/src/market/utils/timers.ts similarity index 100% rename from libs/ledger-live-common/src/market/v2/timers.ts rename to libs/ledger-live-common/src/market/utils/timers.ts diff --git a/libs/ledger-live-common/src/market/types.ts b/libs/ledger-live-common/src/market/utils/types.ts similarity index 83% rename from libs/ledger-live-common/src/market/types.ts rename to libs/ledger-live-common/src/market/utils/types.ts index 69909fdea3c3..3d03f13b551b 100644 --- a/libs/ledger-live-common/src/market/types.ts +++ b/libs/ledger-live-common/src/market/utils/types.ts @@ -8,23 +8,28 @@ export type MarketCoin = { symbol: string; }; +export type ChartDataPoint = [number, number]; +export type MarketCoinDataChart = Record>; + +export enum Order { + MarketCapDesc = "desc", + MarketCapAsc = "asc", + topLosers = "topLosers", + topGainers = "topGainers", +} + export type SupportedCoins = MarketCoin[]; export type MarketListRequestParams = { counterCurrency?: string; - ids?: string[]; starred?: string[]; page?: number; limit?: number; range?: string; - orderBy?: string; - order?: string; + order?: Order; search?: string; lastRequestTime?: Date; - sparkline?: boolean; liveCompatible?: boolean; - top100?: boolean; - supportedCoinsList?: SupportedCoins; liveCoinsList?: string[]; }; @@ -68,6 +73,14 @@ export type SparklineSvgData = { isPositive: boolean; }; +export enum KeysPriceChange { + hour = "1h", + day = "24h", + week = "7d", + month = "30d", + year = "1y", +} + export type CurrencyData = { id: string; name: string; @@ -80,7 +93,7 @@ export type CurrencyData = { low24h: number; ticker: string; price: number; - priceChangePercentage: number; + priceChangePercentage: Record; marketCapChangePercentage24h: number; circulatingSupply: number; totalSupply: number; @@ -90,30 +103,7 @@ export type CurrencyData = { atl: number; atlDate: Date; sparklineIn7d?: SparklineSvgData; - chartData: Record; -}; - -export type RawCurrencyData = { - [x: string]: any; - id: string; - name: string; - image?: string | { thumb: string; small: string; large: string }; - ["market_cap"]: number; - ["market_cap_rank"]: number; - ["total_volume"]: number; - ["high_24h"]: number; - ["low_24h"]: number; - symbol: string; - ["current_price"]: number; - ["market_cap_change_percentage_24h"]: number; - ["circulating_supply"]: number; - ["total_supply"]: number; - ["max_supply"]: number; - ath: number; - ["ath_date"]: Date; - atl: number; - ["atl_date"]: Date; - ["sparkline_in_7d"]: { price: any }; + chartData: MarketCoinDataChart; }; export type SingleCoinState = { @@ -147,34 +137,34 @@ export type MarketPerformersParams = { }; export type MarketItemResponse = { + allTimeHigh: number; + allTimeHighDate: string; + allTimeLow: number; + allTimeLowDate: string; + circulatingSupply: number; + fullyDilutedValuation: number; + high24h: number; id: string; - ledgerIds: string[]; - ticker: string; - name: string; image: string; + ledgerIds: string[]; + low24h: number; marketCap: number; + marketCapChange24h: number; + marketCapChangePercentage24h: number; marketCapRank: number; - fullyDilutedValuation: number; - totalVolume: number; - high24h: number; - low24h: number; + maxSupply: number; + name: string; price: number; priceChange24h: number; priceChangePercentage1h: number; priceChangePercentage24h: number; - priceChangePercentage7d: number; priceChangePercentage30d: number; + priceChangePercentage7d: number; priceChangePercentage1y: number; - marketCapChange24h: number; - marketCapChangePercentage24h: number; - circulatingSupply: number; - totalSupply: number; - maxSupply: number; - allTimeHigh: number; - allTimeLow: number; - allTimeHighDate: string; - allTimeLowDate: string; sparkline: number[]; + ticker: string; + totalSupply: number; + totalVolume: number; updatedAt: string; }; export type MarketItemPerformer = { diff --git a/libs/ledgerjs/packages/types-live/src/feature.ts b/libs/ledgerjs/packages/types-live/src/feature.ts index 6de67b2169af..499780785642 100644 --- a/libs/ledgerjs/packages/types-live/src/feature.ts +++ b/libs/ledgerjs/packages/types-live/src/feature.ts @@ -175,6 +175,7 @@ export type Features = CurrencyFeatures & { supportDeviceStax: Feature_SupportDeviceStax; supportDeviceEuropa: Feature_SupportDeviceEuropa; lldRefreshMarketData: Feature_LldRefreshMarketData; + llmRefreshMarketData: Feature_LlmRefreshMarketData; spamReportNfts: Feature_SpamReportNfts; lldWalletSync: Feature_LldWalletSync; lldNftsGalleryNewArch: DefaultFeature; @@ -462,6 +463,9 @@ export type Feature_NftsFromSimpleHash = Feature<{ export type Feature_LldRefreshMarketData = Feature<{ refreshTime: number; }>; +export type Feature_LlmRefreshMarketData = Feature<{ + refreshTime: number; +}>; export type Feature_BuySellUiManifest = Feature<{ manifestId: string; // id of the app to use for the Buy/Sell UI, e.g. "multibuy-v2" diff --git a/libs/ui/packages/icons/src/svg/graph-asc.svg b/libs/ui/packages/icons/src/svg/graph-asc.svg new file mode 100644 index 000000000000..5793549b160a --- /dev/null +++ b/libs/ui/packages/icons/src/svg/graph-asc.svg @@ -0,0 +1,3 @@ + + + diff --git a/libs/ui/packages/icons/src/svg/graph-desc.svg b/libs/ui/packages/icons/src/svg/graph-desc.svg new file mode 100644 index 000000000000..0abe2de064a2 --- /dev/null +++ b/libs/ui/packages/icons/src/svg/graph-desc.svg @@ -0,0 +1,3 @@ + + +

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 CqCSZL delta 58273 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 BtZV=P diff --git a/apps/ledger-live-mobile/__mocks__/api/market/markets.json b/apps/ledger-live-mobile/__mocks__/api/market/markets.json index 62cb7a84a9d5..b8112f350991 100644 --- a/apps/ledger-live-mobile/__mocks__/api/market/markets.json +++ b/apps/ledger-live-mobile/__mocks__/api/market/markets.json @@ -1,582 +1,806 @@ [ { "id": "bitcoin", - "symbol": "btc", + "ledgerIds": ["bitcoin"], + "ticker": "btc", "name": "Bitcoin", - "image": "https://assets.coingecko.com/coins/images/1/large/bitcoin.png?1696501400", - "current_price": 205609, - "market_cap": 4004278275821, - "market_cap_rank": 1, - "fully_diluted_valuation": 4297151012318, - "total_volume": 97875468097, - "high_24h": 207182, - "low_24h": 202169, - "price_change_24h": -784.959269785526, - "price_change_percentage_24h": -0.38032, - "market_cap_change_24h": -44654880099.0332, - "market_cap_change_percentage_24h": -1.10288, - "circulating_supply": 19568743.0, - "total_supply": 21000000.0, - "max_supply": 21000000.0, - "ath": 380542, - "ath_change_percentage": -46.30963, - "ath_date": "2021-11-09T04:09:45.771Z", - "atl": 149.66, - "atl_change_percentage": 136420.41563, - "atl_date": "2013-07-05T00:00:00.000Z", - "roi": null, - "last_updated": "2023-12-13T13:16:53.096Z", - "price_change_percentage_1y_in_currency": 122.7037621644335 + "image": "https://proxycgassets.api.live.ledger.com/coins/images/1/large/bitcoin.png", + "marketCap": 1267793184097, + "marketCapRank": 1, + "fullyDilutedValuation": 1351547281973, + "totalVolume": 29144313260, + "high24h": 64496, + "low24h": 61109, + "price": 64340, + "priceChange24h": 2750.07, + "priceChangePercentage1h": -0.0509643193795179, + "priceChangePercentage24h": 4.4651507102015, + "priceChangePercentage7d": 2.96752304779716, + "priceChangePercentage30d": -2.28897955960788, + "priceChangePercentage1y": 134.795529255386, + "marketCapChange24h": 50634775104, + "marketCapChangePercentage24h": 4.16008, + "circulatingSupply": 19698650, + "totalSupply": 21000000, + "maxSupply": 21000000, + "allTimeHigh": 73738, + "allTimeLow": 67.81, + "allTimeHighDate": "2024-03-14T07:10:36.635Z", + "allTimeLowDate": "2013-07-06T00:00:00Z", + "sparkline": [ + 62657.195, 62470.03, 61191.613, 61645.637, 61652.16, 60901.527, 61360.547, 62194.473, + 63284.332, 62892.17, 63166.668, 62982.42, 61168.273, 60728.492, 60832.766, 60870.69, 60954.93, + 60779.45, 60833.8, 61201.348, 60900.914, 60918.93, 60943.727, 61129.688, 61137.1, 61359.223, + 61289.84, 61045.938, 61523.223, 62696.383, 62747.48, 62719.96, 62796.348, 62530.746, + 62014.664, 61715.742, 61587.24, 61386.57, 61678.824, 61745.58, 61964.195, 62698.41 + ], + "updatedAt": "2024-05-15T14:48:15Z" }, { "id": "ethereum", - "symbol": "eth", + "ledgerIds": ["base", "arbitrum", "ethereum", "boba", "optimism", "linea"], + "ticker": "eth", "name": "Ethereum", - "image": "https://assets.coingecko.com/coins/images/279/large/ethereum.png?1696501628", - "current_price": 10888.91, - "market_cap": 1306993999856, - "market_cap_rank": 2, - "fully_diluted_valuation": 1306993999856, - "total_volume": 63272423335, - "high_24h": 11013.7, - "low_24h": 10700.54, - "price_change_24h": -66.10423198596618, - "price_change_percentage_24h": -0.60342, - "market_cap_change_24h": -15850362873.552246, - "market_cap_change_percentage_24h": -1.1982, - "circulating_supply": 120211761.826141, - "total_supply": 120211761.826141, - "max_supply": null, - "ath": 26931, - "ath_change_percentage": -59.81207, - "ath_date": "2021-11-09T04:03:16.303Z", - "atl": 1.69, - "atl_change_percentage": 639750.49177, - "atl_date": "2015-10-20T00:00:00.000Z", - "roi": { "times": 69.83868173394059, "currency": "btc", "percentage": 6983.868173394058 }, - "last_updated": "2023-12-13T13:16:46.411Z", - "price_change_percentage_1y_in_currency": 59.38295044504124 + "image": "https://proxycgassets.api.live.ledger.com/coins/images/279/large/ethereum.png", + "marketCap": 356824544811, + "marketCapRank": 2, + "fullyDilutedValuation": 356824544811, + "totalVolume": 12162636256, + "high24h": 2981.24, + "low24h": 2866.02, + "price": 2966.26, + "priceChange24h": 78.86, + "priceChangePercentage1h": -0.134101485686711, + "priceChangePercentage24h": 2.73123077324749, + "priceChangePercentage7d": -1.89505825839966, + "priceChangePercentage30d": -7.07201445813865, + "priceChangePercentage1y": 62.5162195610494, + "marketCapChange24h": 8177616478, + "marketCapChangePercentage24h": 2.34553, + "circulatingSupply": 120116737.016333, + "totalSupply": 120116737.016333, + "maxSupply": null, + "allTimeHigh": 4878.26, + "allTimeLow": 0.432979, + "allTimeHighDate": "2021-11-10T14:24:19.604Z", + "allTimeLowDate": "2015-10-20T00:00:00Z", + "sparkline": [ + 3019.3953, 3008.4812, 2965.0803, 2998.3618, 3002.7224, 2954.849, 2986.9253, 3014.6978, + 3054.3909, 3026.052, 3052.062, 3028.0156, 2937.0725, 2907.646, 2904.2188, 2917.9622, + 2920.3308, 2906.0405, 2905.786, 2932.144, 2915.898, 2918.8271, 2918.5112, 2929.6787, + 2928.3936, 2928.5955, 2930.5498, 2885.8953, 2922.4243, 2959.4775, 2949.5981, 2946.8352, + 2941.6138, 2947.8, 2916.3062, 2905.4883, 2897.6135, 2882.347, 2888.1667, 2886.7322, 2905.3943, + 2908.4165 + ], + "updatedAt": "2024-05-15T14:48:22Z" }, { "id": "tether", - "symbol": "usdt", + "ledgerIds": [ + "ethereum/erc20/usd_tether__erc20_", + "solana/spl/es9vmfrzacermjfrf4h2fyd4kconky11mcce8benwnyb", + "algorand/asa/312769", + "avalanche_c_chain/erc20/tethertoken", + "tron/trc20/tr7nhqjekqxgtci8q8zy4pl8otszgjlj6t" + ], + "ticker": "usdt", "name": "Tether", - "image": "https://assets.coingecko.com/coins/images/325/large/Tether.png?1696501661", - "current_price": 4.98, - "market_cap": 450075133006, - "market_cap_rank": 3, - "fully_diluted_valuation": 450075133006, - "total_volume": 208230681615, - "high_24h": 4.97, - "low_24h": 4.93, - "price_change_24h": 0.04136296, - "price_change_percentage_24h": 0.83833, - "market_cap_change_24h": 2638170804, - "market_cap_change_percentage_24h": 0.58962, - "circulating_supply": 90653779562.3468, - "total_supply": 90653779562.3468, - "max_supply": null, - "ath": 5.96, - "ath_change_percentage": -16.85163, - "ath_date": "2020-05-14T14:04:32.298Z", - "atl": 1.72, - "atl_change_percentage": 188.49109, - "atl_date": "2015-03-02T00:00:00.000Z", - "roi": null, - "last_updated": "2023-12-13T13:15:04.700Z", - "price_change_percentage_1y_in_currency": -5.962750391765175 + "image": "https://proxycgassets.api.live.ledger.com/coins/images/325/large/Tether.png", + "marketCap": 110972239135, + "marketCapRank": 3, + "fullyDilutedValuation": 110972239135, + "totalVolume": 44471547952, + "high24h": 1.003, + "low24h": 0.997865, + "price": 1, + "priceChange24h": 0.00206102, + "priceChangePercentage1h": -0.0144281115582721, + "priceChangePercentage24h": 0.206446063624742, + "priceChangePercentage7d": -0.0865659087990033, + "priceChangePercentage30d": -0.112870442749288, + "priceChangePercentage1y": -0.0465085734020258, + "marketCapChange24h": 43957579, + "marketCapChangePercentage24h": 0.03963, + "circulatingSupply": 110952273350.244, + "totalSupply": 110952273350.244, + "maxSupply": null, + "allTimeHigh": 1.32, + "allTimeLow": 0.572521, + "allTimeHighDate": "2018-07-24T00:00:00Z", + "allTimeLowDate": "2015-03-02T00:00:00Z", + "sparkline": [ + 0.9997615, 0.9997809, 0.9996473, 0.99941814, 1.0000937, 0.9974203, 0.99952626, 0.9997282, + 0.9999109, 1.0000697, 1.0000215, 0.9998755, 0.99563307, 1.0005046, 0.99974746, 0.99928164, + 0.99969465, 0.99971604, 0.99972934, 0.9995428, 0.99975425, 0.9998999, 0.9995791, 0.9996272, + 0.9995591, 0.99959487, 0.9994083, 0.99961823, 0.99987376, 1.0000235, 0.9993343, 0.99995553, + 0.99998915, 0.9995554, 0.99975014, 0.9994067, 1.002085, 0.9992855, 0.9995147, 0.9995943, + 0.9998037, 0.9996699 + ], + "updatedAt": "2024-05-15T14:45:17Z" }, { "id": "binancecoin", - "symbol": "bnb", + "ledgerIds": ["bsc", "binance_beacon_chain", "ethereum/erc20/bnb"], + "ticker": "bnb", "name": "BNB", - "image": "https://assets.coingecko.com/coins/images/825/large/bnb-icon2_2x.png?1696501970", - "current_price": 1247.58, - "market_cap": 191006568360, - "market_cap_rank": 4, - "fully_diluted_valuation": 191006568360, - "total_volume": 8565952447, - "high_24h": 1273.91, - "low_24h": 1215.57, - "price_change_24h": 8.44, - "price_change_percentage_24h": 0.68093, - "market_cap_change_24h": -196696085.7067566, - "market_cap_change_percentage_24h": -0.10287, - "circulating_supply": 153856150.0, - "total_supply": 153856150.0, - "max_supply": 200000000.0, - "ath": 3720.06, - "ath_change_percentage": -66.66874, - "ath_date": "2021-11-07T10:13:53.906Z", - "atl": 0.126083, - "atl_change_percentage": 983337.16139, - "atl_date": "2017-10-19T00:00:00.000Z", - "roi": null, - "last_updated": "2023-12-13T13:16:49.481Z", - "price_change_percentage_1y_in_currency": -11.607938511530975 - }, - { - "id": "ripple", - "symbol": "xrp", - "name": "XRP", - "image": "https://assets.coingecko.com/coins/images/44/large/xrp-symbol-white-128.png?1696501442", - "current_price": 3.02, - "market_cap": 162907702875, - "market_cap_rank": 5, - "fully_diluted_valuation": 301883057200, - "total_volume": 6842120612, - "high_24h": 3.08, - "low_24h": 2.97, - "price_change_24h": -0.041739604358680626, - "price_change_percentage_24h": -1.36162, - "market_cap_change_24h": -3080276856.0314026, - "market_cap_change_percentage_24h": -1.85572, - "circulating_supply": 53957460767.0, - "total_supply": 99988170772.0, - "max_supply": 100000000000.0, - "ath": 11.16, - "ath_change_percentage": -73.10702, - "ath_date": "2021-04-14T05:40:00.104Z", - "atl": 0.00605732, - "atl_change_percentage": 49462.03668, - "atl_date": "2013-08-16T00:00:00.000Z", - "roi": null, - "last_updated": "2023-12-13T13:16:51.707Z", - "price_change_percentage_1y_in_currency": 47.608929868142205 + "image": "https://proxycgassets.api.live.ledger.com/coins/images/825/large/bnb-icon2_2x.png", + "marketCap": 89208309844, + "marketCapRank": 4, + "fullyDilutedValuation": 89208309844, + "totalVolume": 1173132516, + "high24h": 581.83, + "low24h": 561.91, + "price": 579, + "priceChange24h": 11.03, + "priceChangePercentage1h": -0.352378663421871, + "priceChangePercentage24h": 1.94119895841773, + "priceChangePercentage7d": -1.18753602549864, + "priceChangePercentage30d": 1.41458732358996, + "priceChangePercentage1y": 83.4138114579877, + "marketCapChange24h": 1233437534, + "marketCapChangePercentage24h": 1.40203, + "circulatingSupply": 153856150, + "totalSupply": 153856150, + "maxSupply": 200000000, + "allTimeHigh": 686.31, + "allTimeLow": 0.0398177, + "allTimeHighDate": "2021-05-10T07:24:17.097Z", + "allTimeLowDate": "2017-10-19T00:00:00Z", + "sparkline": [ + 585.9531, 585.954, 587.1107, 598.35944, 599.4558, 589.50916, 597.43506, 594.9008, 599.6289, + 591.21844, 595.581, 593.9234, 586.5735, 587.1595, 585.7367, 583.55383, 586.01196, 587.6292, + 590.2012, 592.51984, 592.4529, 590.94806, 589.9293, 591.5477, 592.1661, 597.5357, 595.1515, + 588.0582, 593.3402, 595.2053, 593.1983, 592.7321, 592.2753, 587.9918, 587.25726, 586.0821, + 566.8174, 567.013, 565.43365, 566.4182, 569.42053, 567.91486 + ], + "updatedAt": "2024-05-15T14:48:58Z" }, { "id": "solana", - "symbol": "sol", + "ledgerIds": ["solana"], + "ticker": "sol", "name": "Solana", - "image": "https://assets.coingecko.com/coins/images/4128/large/solana.png?1696504756", - "current_price": 332.06, - "market_cap": 141290069493, - "market_cap_rank": 6, - "fully_diluted_valuation": 187115292902, - "total_volume": 11824096132, - "high_24h": 353.03, - "low_24h": 319.57, - "price_change_24h": -17.15469973101318, - "price_change_percentage_24h": -4.91238, - "market_cap_change_24h": -8131895200.720123, - "market_cap_change_percentage_24h": -5.44224, - "circulating_supply": 426405621.658286, - "total_supply": 564703613.481902, - "max_supply": null, - "ath": 1441.03, - "ath_change_percentage": -77.22114, - "ath_date": "2021-11-06T21:54:35.825Z", - "atl": 2.71, - "atl_change_percentage": 12005.19023, - "atl_date": "2020-04-21T11:37:15.012Z", - "roi": null, - "last_updated": "2023-12-13T13:16:52.221Z", - "price_change_percentage_1y_in_currency": 370.60794420571693 + "image": "https://proxycgassets.api.live.ledger.com/coins/images/4128/large/solana.png", + "marketCap": 68433744759, + "marketCapRank": 5, + "fullyDilutedValuation": 87858939690, + "totalVolume": 2758105035, + "high24h": 152.91, + "low24h": 141.55, + "price": 152.29, + "priceChange24h": 8.51, + "priceChangePercentage1h": -0.0321207568024643, + "priceChangePercentage24h": 5.91662067520528, + "priceChangePercentage7d": 3.1629065717832, + "priceChangePercentage30d": 1.79317210684906, + "priceChangePercentage1y": 611.924424885099, + "marketCapChange24h": 3783090644, + "marketCapChangePercentage24h": 5.85159, + "circulatingSupply": 448657840.741922, + "totalSupply": 576011181.470747, + "maxSupply": null, + "allTimeHigh": 259.96, + "allTimeLow": 0.500801, + "allTimeHighDate": "2021-11-06T21:54:35.825Z", + "allTimeLowDate": "2020-05-11T19:35:23.449Z", + "sparkline": [ + 147.60242, 147.23459, 141.0417, 144.24721, 144.88745, 140.83401, 144.66696, 147.48576, + 152.89209, 153.4424, 155.38895, 153.92587, 147.22972, 148.06512, 145.81874, 144.44762, + 145.51242, 144.07492, 144.83005, 146.10063, 145.64526, 146.59106, 146.51698, 145.41992, + 144.33853, 145.05293, 143.15129, 140.44177, 141.43762, 145.21169, 145.75146, 147.32007, + 146.9828, 146.6148, 146.10094, 146.08061, 143.63188, 143.69243, 142.84915, 142.8639, + 143.54529, 144.6397 + ], + "updatedAt": "2024-05-15T14:48:52Z" }, { "id": "usd-coin", - "symbol": "usdc", + "ledgerIds": [ + "optimism/erc20/usd_coin", + "elrond/esdt/555344432d633736663166", + "polygon/erc20/usd_coin", + "algorand/asa/31566704", + "arbitrum/erc20/usd_coin", + "ethereum/erc20/usd__coin", + "stellar/asset/usdc:ga5zsejyb37jrc5avcia5mop4rhtm335x2kgx3ihojapp5re34k4kzvn", + "moonbeam/erc20/usd_coin", + "base/erc20/usd_coin", + "tron/trc20/tekxitehnzsmse2xqrbj4w32run966rdz8", + "solana/spl/epjfwdd5aufqssqem2qn1xzybapc8g4weggkzwytdt1v", + "avalanche_c_chain/erc20/usd_coin" + ], + "ticker": "usdc", "name": "USDC", - "image": "https://assets.coingecko.com/coins/images/6319/large/usdc.png?1696506694", - "current_price": 4.98, - "market_cap": 119903185809, - "market_cap_rank": 7, - "fully_diluted_valuation": 119842521458, - "total_volume": 32061396757, - "high_24h": 4.98, - "low_24h": 4.93, - "price_change_24h": 0.04482732, - "price_change_percentage_24h": 0.90876, - "market_cap_change_24h": 462605294, - "market_cap_change_percentage_24h": 0.38731, - "circulating_supply": 24089302072.4732, - "total_supply": 24077114223.9167, - "max_supply": null, - "ath": 5.97, - "ath_change_percentage": -16.89973, - "ath_date": "2020-05-14T14:07:45.849Z", - "atl": 3.66, - "atl_change_percentage": 35.56688, - "atl_date": "2019-01-10T00:00:00.000Z", - "roi": null, - "last_updated": "2023-12-13T13:16:53.444Z", - "price_change_percentage_1y_in_currency": -6.0887894596327055 + "image": "https://proxycgassets.api.live.ledger.com/coins/images/6319/large/usdc.png", + "marketCap": 32966082232, + "marketCapRank": 6, + "fullyDilutedValuation": 32973154386, + "totalVolume": 5999012444, + "high24h": 1.006, + "low24h": 0.998171, + "price": 1.001, + "priceChange24h": 0.00232284, + "priceChangePercentage1h": 0.0279345839187226, + "priceChangePercentage24h": 0.23265841855982, + "priceChangePercentage7d": 0.0534421709488871, + "priceChangePercentage30d": 0.204957736705401, + "priceChangePercentage1y": 0.0378593637830182, + "marketCapChange24h": -93504461.0456963, + "marketCapChangePercentage24h": -0.28284, + "circulatingSupply": 32927436423.6292, + "totalSupply": 32934500287.1743, + "maxSupply": null, + "allTimeHigh": 1.17, + "allTimeLow": 0.877647, + "allTimeHighDate": "2019-05-08T00:40:28.300Z", + "allTimeLowDate": "2023-03-11T08:02:13.981Z", + "sparkline": [ + 0.9999258, 0.99969923, 0.9999768, 0.99998045, 0.99993086, 0.99930614, 1.000704, 1.000056, + 0.9995984, 1.0000048, 0.9990393, 1.0009893, 0.9996846, 1.000096, 0.9992139, 0.9998493, + 1.0003057, 0.99991375, 1.0004326, 1.0003217, 1.000017, 0.9998975, 0.9999778, 1.0003474, + 0.99995124, 1.0000898, 0.9997798, 1.0005147, 1.0021964, 1.0002855, 1.0002387, 1.0000032, + 0.9999825, 0.9999229, 1.0003145, 0.99975836, 0.9999368, 0.99981743, 1.000167, 1.0001553, + 0.9998695, 1.0006797 + ], + "updatedAt": "2024-05-15T14:48:42Z" }, { - "id": "cardano", - "symbol": "ada", - "name": "Cardano", - "image": "https://assets.coingecko.com/coins/images/975/large/cardano.png?1696502090", - "current_price": 2.9, - "market_cap": 100837089661, - "market_cap_rank": 8, - "fully_diluted_valuation": 129595943165, - "total_volume": 5276931054, - "high_24h": 2.97, - "low_24h": 2.72, - "price_change_24h": -0.037978120400622206, - "price_change_percentage_24h": -1.29088, - "market_cap_change_24h": -2485215109.93013, - "market_cap_change_percentage_24h": -2.4053, - "circulating_supply": 35013974387.9469, - "total_supply": 45000000000.0, - "max_supply": 45000000000.0, - "ath": 16.01, - "ath_change_percentage": -82.03663, - "ath_date": "2021-09-02T06:00:10.474Z", - "atl": 0.069678, - "atl_change_percentage": 4027.62534, - "atl_date": "2017-11-02T00:00:00.000Z", - "roi": null, - "last_updated": "2023-12-13T13:16:51.669Z", - "price_change_percentage_1y_in_currency": 78.30338410541216 + "id": "ripple", + "ledgerIds": ["ripple"], + "ticker": "xrp", + "name": "XRP", + "image": "https://proxycgassets.api.live.ledger.com/coins/images/44/large/xrp-symbol-white-128.png", + "marketCap": 28260991502, + "marketCapRank": 7, + "fullyDilutedValuation": 51043925286, + "totalVolume": 876028921, + "high24h": 0.510662, + "low24h": 0.497653, + "price": 0.510722, + "priceChange24h": 0.00941303, + "priceChangePercentage1h": 0.326468466742202, + "priceChangePercentage24h": 1.8776917634215, + "priceChangePercentage7d": -2.87310460464971, + "priceChangePercentage30d": 0.69008243089786, + "priceChangePercentage1y": 19.7716981250522, + "marketCapChange24h": 445904902, + "marketCapChangePercentage24h": 1.6031, + "circulatingSupply": 55359176420, + "totalSupply": 99987633657, + "maxSupply": 100000000000, + "allTimeHigh": 3.4, + "allTimeLow": 0.00268621, + "allTimeHighDate": "2018-01-07T00:00:00Z", + "allTimeLowDate": "2014-05-22T00:00:00Z", + "sparkline": [ + 0.5266193, 0.5270535, 0.5166041, 0.52149796, 0.52046347, 0.5099221, 0.51273763, 0.51828605, + 0.52115995, 0.5180987, 0.5177485, 0.5145132, 0.49867815, 0.50575167, 0.50144553, 0.50398844, + 0.50553405, 0.5023539, 0.50526136, 0.5064213, 0.5057429, 0.5071312, 0.50609237, 0.5050932, + 0.50330263, 0.5017274, 0.4992411, 0.49195737, 0.49866146, 0.50776076, 0.50627255, 0.5060036, + 0.50416404, 0.505221, 0.50382847, 0.5078337, 0.50306684, 0.50471085, 0.50036794, 0.50033724, + 0.49892935, 0.5016387 + ], + "updatedAt": "2024-05-15T14:48:54Z" }, { "id": "staked-ether", - "symbol": "steth", + "ledgerIds": ["ethereum/erc20/steth"], + "ticker": "steth", "name": "Lido Staked Ether", - "image": "https://assets.coingecko.com/coins/images/13442/large/steth_logo.png?1696513206", - "current_price": 10876.84, - "market_cap": 100123220735, - "market_cap_rank": 9, - "fully_diluted_valuation": 100123220735, - "total_volume": 286984904, - "high_24h": 10998.22, - "low_24h": 10689.12, - "price_change_24h": -76.63386356281626, - "price_change_percentage_24h": -0.69963, - "market_cap_change_24h": -1123979190.428009, - "market_cap_change_percentage_24h": -1.11013, - "circulating_supply": 9214708.39586071, - "total_supply": 9214708.39586071, - "max_supply": 9214708.39586071, - "ath": 26759, - "ath_change_percentage": -59.60261, - "ath_date": "2021-11-09T05:59:27.697Z", - "atl": 2474.06, - "atl_change_percentage": 336.92827, - "atl_date": "2020-12-22T04:08:21.854Z", - "roi": null, - "last_updated": "2023-12-13T13:16:33.439Z", - "price_change_percentage_1y_in_currency": 60.33924834029486 + "image": "https://proxycgassets.api.live.ledger.com/coins/images/13442/large/steth_logo.png", + "marketCap": 27761757681, + "marketCapRank": 8, + "fullyDilutedValuation": 27761757681, + "totalVolume": 72682472, + "high24h": 2980.8, + "low24h": 2865.01, + "price": 2963.13, + "priceChange24h": 75.63, + "priceChangePercentage1h": 0.0585949043565619, + "priceChangePercentage24h": 2.61915166126093, + "priceChangePercentage7d": -2.16489190403567, + "priceChangePercentage30d": -7.06159367316768, + "priceChangePercentage1y": 62.4341580659837, + "marketCapChange24h": 602760505, + "marketCapChangePercentage24h": 2.21938, + "circulatingSupply": 9362863.68264761, + "totalSupply": 9362911.32427392, + "maxSupply": null, + "allTimeHigh": 4829.57, + "allTimeLow": 482.9, + "allTimeHighDate": "2021-11-10T14:40:47.256Z", + "allTimeLowDate": "2020-12-22T04:08:21.854Z", + "sparkline": [ + 3016.6682, 3010.6316, 2962.3262, 2996.9583, 2998.4885, 2955.8462, 2980.8438, 3014.3232, + 3049.5745, 3028.8167, 3044.8994, 3026.6282, 2932.0835, 2905.7847, 2906.095, 2917.1833, + 2918.532, 2905.445, 2902.1538, 2930.7507, 2914.2334, 2916.155, 2916.5803, 2926.9321, 2926.609, + 2927.9692, 2929.5059, 2881.3572, 2914.2417, 2960.7954, 2950.6438, 2940.7524, 2939.2048, + 2948.4666, 2913.261, 2901.9316, 2889.5906, 2882.3435, 2886.3472, 2884.4995, 2902.221, 2904.396 + ], + "updatedAt": "2024-05-15T14:48:28Z" }, { - "id": "avalanche-2", - "symbol": "avax", - "name": "Avalanche", - "image": "https://assets.coingecko.com/coins/images/12559/large/Avalanche_Circle_RedWhite_Trans.png?1696512369", - "current_price": 188.27, - "market_cap": 68264026555, - "market_cap_rank": 10, - "fully_diluted_valuation": 80961321673, - "total_volume": 10150358504, - "high_24h": 198.86, - "low_24h": 170.37, - "price_change_24h": -8.3460117907604, - "price_change_percentage_24h": -4.24493, - "market_cap_change_24h": -4244545761.6068954, - "market_cap_change_percentage_24h": -5.85385, - "circulating_supply": 365762970.094816, - "total_supply": 433795880.094816, - "max_supply": 720000000.0, - "ath": 813.77, - "ath_change_percentage": -77.2898, - "ath_date": "2021-11-21T14:18:56.538Z", - "atl": 14.39, - "atl_change_percentage": 1183.87076, - "atl_date": "2020-12-09T08:34:53.550Z", - "roi": null, - "last_updated": "2023-12-13T13:16:53.245Z", - "price_change_percentage_1y_in_currency": 172.5837888877856 + "id": "the-open-network", + "ledgerIds": ["ton", "bsc/bep20/wrapped_ton_coin", "ethereum/erc20/wrapped_ton_coin"], + "ticker": "ton", + "name": "Toncoin", + "image": "https://proxycgassets.api.live.ledger.com/coins/images/17980/large/ton_symbol.png", + "marketCap": 24288585699, + "marketCapRank": 9, + "fullyDilutedValuation": 35701636251, + "totalVolume": 462192039, + "high24h": 7.05, + "low24h": 6.59, + "price": 6.99, + "priceChange24h": 0.337376, + "priceChangePercentage1h": -0.052040368831294, + "priceChangePercentage24h": 5.07290863694363, + "priceChangePercentage7d": 19.7521422432585, + "priceChangePercentage30d": 0.825830209175098, + "priceChangePercentage1y": 251.845864043224, + "marketCapChange24h": 1072258170, + "marketCapChangePercentage24h": 4.61855, + "circulatingSupply": 3474153822.66886, + "totalSupply": 5106636409.14248, + "maxSupply": null, + "allTimeHigh": 7.63, + "allTimeLow": 0.519364, + "allTimeHighDate": "2024-04-11T05:55:53.682Z", + "allTimeLowDate": "2021-09-21T00:33:11.092Z", + "sparkline": [ + 5.854216, 5.8081875, 5.7762685, 5.892911, 6.0360875, 5.937141, 6.025485, 6.344385, 6.3830214, + 6.3800955, 6.7669806, 6.9685383, 6.6657205, 6.7165, 6.705566, 6.8060784, 6.8768916, 6.7040715, + 6.7848415, 6.895431, 6.850175, 6.870988, 6.9030204, 6.9636674, 7.034313, 6.9712424, 6.9274926, + 6.8823295, 7.163029, 7.356615, 7.260922, 7.2986007, 7.0486293, 6.967607, 7.047161, 6.91046, + 6.637433, 6.995799, 6.883593, 6.9363832, 7.0104523, 6.8899474 + ], + "updatedAt": "2024-05-15T14:48:57Z" }, { "id": "dogecoin", - "symbol": "doge", + "ledgerIds": ["dogecoin"], + "ticker": "doge", "name": "Dogecoin", - "image": "https://assets.coingecko.com/coins/images/5/large/dogecoin.png?1696501409", - "current_price": 0.463277, - "market_cap": 65736432382, - "market_cap_rank": 11, - "fully_diluted_valuation": 65736367670, - "total_volume": 5735210207, - "high_24h": 0.477346, - "low_24h": 0.449248, - "price_change_24h": -0.009901449265711739, - "price_change_percentage_24h": -2.09254, - "market_cap_change_24h": -1924381357.8200684, - "market_cap_change_percentage_24h": -2.84416, - "circulating_supply": 142216776383.705, - "total_supply": 142216636383.705, - "max_supply": null, - "ath": 3.83, - "ath_change_percentage": -87.99194, - "ath_date": "2021-05-08T05:08:23.458Z", - "atl": 0.00024014, - "atl_change_percentage": 191419.03185, - "atl_date": "2014-08-18T00:00:00.000Z", - "roi": null, - "last_updated": "2023-12-13T13:16:50.572Z", - "price_change_percentage_1y_in_currency": -2.809059787098743 - }, - { - "id": "tron", - "symbol": "trx", - "name": "TRON", - "image": "https://assets.coingecko.com/coins/images/1094/large/tron-logo.png?1696502193", - "current_price": 0.513844, - "market_cap": 45464941485, - "market_cap_rank": 12, - "fully_diluted_valuation": 45465002837, - "total_volume": 2075136060, - "high_24h": 0.517011, - "low_24h": 0.506653, - "price_change_24h": -0.00104586707512977, - "price_change_percentage_24h": -0.20312, - "market_cap_change_24h": -197753802.0641632, - "market_cap_change_percentage_24h": -0.43308, - "circulating_supply": 88440140513.4899, - "total_supply": 88440259856.7903, - "max_supply": null, - "ath": 0.994777, - "ath_change_percentage": -48.54559, - "ath_date": "2021-04-17T03:43:18.701Z", - "atl": 0.00591448, - "atl_change_percentage": 8554.28757, - "atl_date": "2017-11-12T00:00:00.000Z", - "roi": { "times": 53.385767571022, "currency": "usd", "percentage": 5338.5767571022 }, - "last_updated": "2023-12-13T13:16:53.415Z", - "price_change_percentage_1y_in_currency": 82.06397995824528 + "image": "https://proxycgassets.api.live.ledger.com/coins/images/5/large/dogecoin.png", + "marketCap": 21881106867, + "marketCapRank": 10, + "fullyDilutedValuation": 21881311575, + "totalVolume": 1280013670, + "high24h": 0.15344, + "low24h": 0.144533, + "price": 0.152031, + "priceChange24h": 0.00277867, + "priceChangePercentage1h": -0.390992310587998, + "priceChangePercentage24h": 1.86173082598688, + "priceChangePercentage7d": 1.4266088327776, + "priceChangePercentage30d": -4.93976516623158, + "priceChangePercentage1y": 109.022043359089, + "marketCapChange24h": 263945334, + "marketCapChangePercentage24h": 1.221, + "circulatingSupply": 144300556383.705, + "totalSupply": 144301906383.705, + "maxSupply": null, + "allTimeHigh": 0.731578, + "allTimeLow": 0.0000869, + "allTimeHighDate": "2021-05-08T05:08:23.458Z", + "allTimeLowDate": "2015-05-06T00:00:00Z", + "sparkline": [ + 0.14976273, 0.14859755, 0.14237858, 0.14708596, 0.14748488, 0.14416611, 0.14786883, + 0.14942943, 0.1535368, 0.15023115, 0.15231374, 0.15150443, 0.14458291, 0.1446889, 0.14340827, + 0.1445574, 0.14453529, 0.14326382, 0.14362857, 0.14474642, 0.1434494, 0.14370988, 0.14329967, + 0.14318709, 0.14259319, 0.14114523, 0.14091884, 0.13712521, 0.14045705, 0.14287186, + 0.15028833, 0.14899188, 0.14800581, 0.14895535, 0.15033984, 0.14839932, 0.14946638, + 0.14620373, 0.14545225, 0.14622784, 0.1466549, 0.15016145 + ], + "updatedAt": "2024-05-15T14:48:27Z" }, { - "id": "polkadot", - "symbol": "dot", - "name": "Polkadot", - "image": "https://assets.coingecko.com/coins/images/12171/large/polkadot.png?1696512008", - "current_price": 34.12, - "market_cap": 44452607237, - "market_cap_rank": 13, - "fully_diluted_valuation": 47205546149, - "total_volume": 2322473911, - "high_24h": 36.03, - "low_24h": 33.05, - "price_change_24h": -1.535384167760668, - "price_change_percentage_24h": -4.30647, - "market_cap_change_24h": -2322956834.6354218, - "market_cap_change_percentage_24h": -4.96618, - "circulating_supply": 1307093558.97153, - "total_supply": 1388041538.03351, - "max_supply": null, - "ath": 307.72, - "ath_change_percentage": -89.02334, - "ath_date": "2021-11-04T14:10:09.301Z", - "atl": 14.81, - "atl_change_percentage": 128.02819, - "atl_date": "2020-08-19T03:44:11.556Z", - "roi": null, - "last_updated": "2023-12-13T13:16:52.037Z", - "price_change_percentage_1y_in_currency": 25.008138552432975 + "id": "cardano", + "ledgerIds": ["cardano"], + "ticker": "ada", + "name": "Cardano", + "image": "https://proxycgassets.api.live.ledger.com/coins/images/975/large/cardano.png", + "marketCap": 15737941344, + "marketCapRank": 11, + "fullyDilutedValuation": 20035350952, + "totalVolume": 263217352, + "high24h": 0.447276, + "low24h": 0.426531, + "price": 0.444963, + "priceChange24h": 0.01385242, + "priceChangePercentage1h": 0.0191756845920904, + "priceChangePercentage24h": 3.21319067062242, + "priceChangePercentage7d": -0.017761118306903, + "priceChangePercentage30d": -7.48778209951361, + "priceChangePercentage1y": 20.2755056191615, + "marketCapChange24h": 462996514, + "marketCapChangePercentage24h": 3.03108, + "circulatingSupply": 35347888947.3203, + "totalSupply": 45000000000, + "maxSupply": 45000000000, + "allTimeHigh": 3.09, + "allTimeLow": 0.01925275, + "allTimeHighDate": "2021-09-02T06:00:10.474Z", + "allTimeLowDate": "2020-03-13T02:22:55.044Z", + "sparkline": [ + 0.4584815, 0.4634913, 0.45286816, 0.4628828, 0.4580508, 0.45075914, 0.45218703, 0.45877424, + 0.46350923, 0.4648622, 0.4665452, 0.46315864, 0.45098487, 0.44812873, 0.44646174, 0.44788554, + 0.4467873, 0.44237947, 0.44262335, 0.4413735, 0.43944308, 0.44128522, 0.43997085, 0.44046968, + 0.44171676, 0.43815804, 0.4366339, 0.42845997, 0.43853173, 0.44765437, 0.4433328, 0.4402367, + 0.43673578, 0.43470168, 0.4338322, 0.43495002, 0.43214953, 0.43176976, 0.42704347, 0.4290453, + 0.42842388, 0.43311864 + ], + "updatedAt": "2024-05-15T14:48:58Z" }, { - "id": "chainlink", - "symbol": "link", - "name": "Chainlink", - "image": "https://assets.coingecko.com/coins/images/877/large/chainlink-new-logo.png?1696502009", - "current_price": 71.2, - "market_cap": 39423869457, - "market_cap_rank": 14, - "fully_diluted_valuation": 70798009327, - "total_volume": 3676839187, - "high_24h": 74.28, - "low_24h": 69.34, - "price_change_24h": -2.5374862922000148, - "price_change_percentage_24h": -3.44104, - "market_cap_change_24h": -1789731724.6812973, - "market_cap_change_percentage_24h": -4.34258, - "circulating_supply": 556849971.2305644, - "total_supply": 1000000000.0, - "max_supply": 1000000000.0, - "ath": 276.85, - "ath_change_percentage": -74.49309, - "ath_date": "2021-05-05T07:29:57.467Z", - "atl": 0.481059, - "atl_change_percentage": 14579.17478, - "atl_date": "2017-11-29T00:00:00.000Z", - "roi": null, - "last_updated": "2023-12-13T13:16:49.623Z", - "price_change_percentage_1y_in_currency": 100.23312736322798 + "id": "shiba-inu", + "ledgerIds": ["linea/erc20/shiba_inu", "bsc/bep20/shiba_inu", "ethereum/erc20/shiba_inu"], + "ticker": "shib", + "name": "Shiba Inu", + "image": "https://proxycgassets.api.live.ledger.com/coins/images/11939/large/shiba.png", + "marketCap": 14188664977, + "marketCapRank": 12, + "fullyDilutedValuation": 24078237074, + "totalVolume": 674579840, + "high24h": 0.00002428, + "low24h": 0.00002291, + "price": 0.00002408, + "priceChange24h": 6.76836e-7, + "priceChangePercentage1h": 0.147929548037171, + "priceChangePercentage24h": 2.8914737832155, + "priceChangePercentage7d": 5.20385867582226, + "priceChangePercentage30d": 4.7866399015205, + "priceChangePercentage1y": 171.096585275724, + "marketCapChange24h": 429195604, + "marketCapChangePercentage24h": 3.11927, + "circulatingSupply": 589263020389618, + "totalSupply": 999982361071363, + "maxSupply": null, + "allTimeHigh": 0.00008616, + "allTimeLow": 5.6366e-11, + "allTimeHighDate": "2021-10-28T03:54:55.568Z", + "allTimeLowDate": "2020-11-28T11:26:25.838Z", + "sparkline": [ + 0.0000230553, 0.000023022205, 0.000022473681, 0.00002284627, 0.000022971262, 0.00002245292, + 0.000022636044, 0.000023193998, 0.000023682025, 0.000023385624, 0.000023533134, + 0.000023388944, 0.000022421262, 0.000022466502, 0.000022345841, 0.000022619279, 0.00002254475, + 0.000022495838, 0.000022604387, 0.000022725751, 0.000022538425, 0.000022615912, + 0.000022557173, 0.000022542741, 0.000022513143, 0.00002239812, 0.000022318556, 0.000021750271, + 0.000022001304, 0.00002330186, 0.000023794977, 0.000023598512, 0.000023217648, 0.000023194294, + 0.000023572169, 0.000023554625, 0.000023402274, 0.000023265542, 0.00002300576, 0.00002302593, + 0.000023213357, 0.000023907405 + ], + "updatedAt": "2024-05-15T14:49:01Z" }, { - "id": "matic-network", - "symbol": "matic", - "name": "Polygon", - "image": "https://assets.coingecko.com/coins/images/4713/large/polygon.png?1698233745", - "current_price": 4.22, - "market_cap": 39076455551, - "market_cap_rank": 15, - "fully_diluted_valuation": 42094897241, - "total_volume": 3668922090, - "high_24h": 4.44, - "low_24h": 4.14, - "price_change_24h": -0.14402545543624434, - "price_change_percentage_24h": -3.29821, - "market_cap_change_24h": -1590144273.9434052, - "market_cap_change_percentage_24h": -3.9102, - "circulating_supply": 9282943566.203985, - "total_supply": 10000000000.0, - "max_supply": 10000000000.0, - "ath": 16.54, - "ath_change_percentage": -74.70374, - "ath_date": "2021-12-27T02:08:34.307Z", - "atl": 0.01240808, - "atl_change_percentage": 33625.10596, - "atl_date": "2019-05-10T00:00:00.000Z", - "roi": { "times": 321.8844218316348, "currency": "usd", "percentage": 32188.442183163483 }, - "last_updated": "2023-12-13T13:16:52.250Z", - "price_change_percentage_1y_in_currency": -11.519730791724017 + "id": "avalanche-2", + "ledgerIds": ["avalanche_c_chain"], + "ticker": "avax", + "name": "Avalanche", + "image": "https://proxycgassets.api.live.ledger.com/coins/images/12559/large/Avalanche_Circle_RedWhite_Trans.png", + "marketCap": 13159720883, + "marketCapRank": 13, + "fullyDilutedValuation": 15181619707, + "totalVolume": 429187118, + "high24h": 34.46, + "low24h": 31.42, + "price": 34.35, + "priceChange24h": 2.38, + "priceChangePercentage1h": 0.00736625237759751, + "priceChangePercentage24h": 7.44683444383987, + "priceChangePercentage7d": -1.11134465589259, + "priceChangePercentage30d": -9.43557253523738, + "priceChangePercentage1y": 125.387244860953, + "marketCapChange24h": 898502792, + "marketCapChangePercentage24h": 7.32801, + "circulatingSupply": 381987043.452672, + "totalSupply": 440676673.79611, + "maxSupply": 720000000, + "allTimeHigh": 144.96, + "allTimeLow": 2.8, + "allTimeHighDate": "2021-11-21T14:18:56.538Z", + "allTimeLowDate": "2020-12-31T13:15:21.540Z", + "sparkline": [ + 34.784294, 34.609623, 33.80005, 34.35657, 34.552494, 33.806793, 34.0954, 34.42683, 35.262913, + 35.464718, 35.78605, 35.621937, 34.375103, 34.239697, 33.2958, 33.63421, 33.617767, 33.39566, + 33.477787, 33.799294, 33.544006, 33.61532, 33.570015, 33.727917, 33.56361, 33.47636, 33.18843, + 32.21995, 32.737877, 33.44616, 32.965733, 32.617302, 32.413086, 32.157257, 32.33291, 32.86291, + 31.922855, 32.005623, 31.617971, 31.912663, 32.497448, 32.94441 + ], + "updatedAt": "2024-05-15T14:48:29Z" }, { - "id": "the-open-network", - "symbol": "ton", - "name": "Toncoin", - "image": "https://assets.coingecko.com/coins/images/17980/large/ton_symbol.png?1696517498", - "current_price": 10.2, - "market_cap": 35190911083, - "market_cap_rank": 16, - "fully_diluted_valuation": 51942448836, - "total_volume": 187190459, - "high_24h": 10.62, - "low_24h": 9.89, - "price_change_24h": -0.14169901716467947, - "price_change_percentage_24h": -1.36972, - "market_cap_change_24h": -568346534.461525, - "market_cap_change_percentage_24h": -1.58937, - "circulating_supply": 3454898330.72461, - "total_supply": 5099495132.46762, - "max_supply": null, - "ath": 28.57, - "ath_change_percentage": -64.47806, - "ath_date": "2021-11-12T06:50:02.476Z", - "atl": 2.77, - "atl_change_percentage": 266.89292, - "atl_date": "2021-09-21T00:33:11.092Z", - "roi": null, - "last_updated": "2023-12-13T13:16:38.623Z", - "price_change_percentage_1y_in_currency": -16.123463350261687 + "id": "tron", + "ledgerIds": ["tron"], + "ticker": "trx", + "name": "TRON", + "image": "https://proxycgassets.api.live.ledger.com/coins/images/1094/large/tron-logo.png", + "marketCap": 11023243767, + "marketCapRank": 14, + "fullyDilutedValuation": 11023246755, + "totalVolume": 251842518, + "high24h": 0.126445, + "low24h": 0.124711, + "price": 0.12602, + "priceChange24h": 0.00130835, + "priceChangePercentage1h": -0.0735658155100665, + "priceChangePercentage24h": 1.04910149016337, + "priceChangePercentage7d": 2.70504110294169, + "priceChangePercentage30d": 10.7962328874592, + "priceChangePercentage1y": 79.450606830917, + "marketCapChange24h": 109113807, + "marketCapChangePercentage24h": 0.99975, + "circulatingSupply": 87463840458.2667, + "totalSupply": 87463864160.3922, + "maxSupply": null, + "allTimeHigh": 0.231673, + "allTimeLow": 0.00180434, + "allTimeHighDate": "2018-01-05T00:00:00Z", + "allTimeLowDate": "2017-11-12T00:00:00Z", + "sparkline": [ + 0.12258068, 0.123549506, 0.12281498, 0.12370612, 0.124208696, 0.1245937, 0.12703244, + 0.12668729, 0.12618169, 0.12616628, 0.12684278, 0.124840595, 0.12562087, 0.12717699, + 0.12673779, 0.1267699, 0.12694426, 0.1268106, 0.12623182, 0.12633023, 0.12661718, 0.1269238, + 0.12691177, 0.1264906, 0.12663198, 0.12693585, 0.12711936, 0.12668999, 0.12674987, 0.12599795, + 0.12585458, 0.12595612, 0.12583506, 0.12535277, 0.12497228, 0.124879174, 0.12514152, + 0.12527645, 0.12541293, 0.12544909, 0.12535642, 0.1254923 + ], + "updatedAt": "2024-05-15T14:48:31Z" }, { "id": "wrapped-bitcoin", - "symbol": "wbtc", + "ledgerIds": [ + "polygon/erc20/(pos)_wrapped_btc", + "telos_evm/erc20/wrapped_bitcoin", + "linea/erc20/wrapped_btc", + "optimism/erc20/wrapped_btc", + "avalanche_c_chain/erc20/wrapped_btc_(bridged)", + "ethereum/erc20/wrapped_bitcoin", + "astar/erc20/wrapped_btc", + "neon_evm/erc20/wrapped_bitcoin", + "arbitrum/erc20/wrapped_btc", + "syscoin/erc20/wrapped_bitcoin_(multichain)", + "fantom/erc20/bitcoin", + "cronos/erc20/wrapped_btc", + "polygon_zk_evm/erc20/wrapped_btc" + ], + "ticker": "wbtc", "name": "Wrapped Bitcoin", - "image": "https://assets.coingecko.com/coins/images/7598/large/wrapped_bitcoin_wbtc.png?1696507857", - "current_price": 205442, - "market_cap": 31761351877, - "market_cap_rank": 17, - "fully_diluted_valuation": 31761351877, - "total_volume": 874405537, - "high_24h": 207143, - "low_24h": 201491, - "price_change_24h": -848.6460119594703, - "price_change_percentage_24h": -0.41138, - "market_cap_change_24h": -268757584.6622925, - "market_cap_change_percentage_24h": -0.83908, - "circulating_supply": 154744.26789025, - "total_supply": 154744.26789025, - "max_supply": 154744.26789025, - "ath": 384950, - "ath_change_percentage": -46.9414, - "ath_date": "2021-11-10T14:40:19.650Z", - "atl": 12091.78, - "atl_change_percentage": 1589.15633, - "atl_date": "2019-04-02T00:00:00.000Z", - "roi": null, - "last_updated": "2023-12-13T13:16:52.049Z", - "price_change_percentage_1y_in_currency": 122.9757907875177 + "image": "https://proxycgassets.api.live.ledger.com/coins/images/7598/large/wrapped_bitcoin_wbtc.png", + "marketCap": 9992791741, + "marketCapRank": 15, + "fullyDilutedValuation": 9992791741, + "totalVolume": 260669491, + "high24h": 64330, + "low24h": 61194, + "price": 64303, + "priceChange24h": 2750.51, + "priceChangePercentage1h": 0.0747657671741505, + "priceChangePercentage24h": 4.46858233308011, + "priceChangePercentage7d": 3.19977633828359, + "priceChangePercentage30d": -2.50490314626866, + "priceChangePercentage1y": 134.540268253676, + "marketCapChange24h": 400228844, + "marketCapChangePercentage24h": 4.17228, + "circulatingSupply": 155304.87472143, + "totalSupply": 155304.87472143, + "maxSupply": 155304.87472143, + "allTimeHigh": 73505, + "allTimeLow": 3139.17, + "allTimeHighDate": "2024-03-14T07:10:23.403Z", + "allTimeLowDate": "2019-04-02T00:00:00Z", + "sparkline": [ + 62467.496, 62491.43, 61160.383, 61739.19, 61554.07, 60775.074, 61616.98, 62327.06, 63179.426, + 62819.723, 63186.176, 62951.64, 61227.418, 60740.984, 60817.156, 60859.773, 60912.676, + 60683.168, 60885.22, 61101.066, 60934.734, 60852.566, 60957.99, 61169.812, 61181.613, + 61298.91, 61309.39, 60833.54, 61636.117, 62511.47, 62646.594, 62791.01, 62729.027, 62495.18, + 62067.543, 61801.03, 61641.934, 61430.113, 61607.445, 61805.688, 61958.297, 62684.336 + ], + "updatedAt": "2024-05-15T14:48:57Z" }, { - "id": "shiba-inu", - "symbol": "shib", - "name": "Shiba Inu", - "image": "https://assets.coingecko.com/coins/images/11939/large/shiba.png?1696511800", - "current_price": 4.695e-5, - "market_cap": 27635338530, - "market_cap_rank": 18, - "fully_diluted_valuation": 46892974642, - "total_volume": 1058762420, - "high_24h": 4.776e-5, - "low_24h": 4.566e-5, - "price_change_24h": -3.78842633583e-7, - "price_change_percentage_24h": -0.80043, - "market_cap_change_24h": -340491172.0766411, - "market_cap_change_percentage_24h": -1.21709, - "circulating_supply": 589317528983273.2, - "total_supply": 999982392571662.0, - "max_supply": null, - "ath": 0.00047701, - "ath_change_percentage": -90.22295, - "ath_date": "2021-10-28T03:54:55.568Z", - "atl": 2.86944e-10, - "atl_change_percentage": 16253052.84171, - "atl_date": "2020-12-11T05:57:22.476Z", - "roi": null, - "last_updated": "2023-12-13T13:16:54.123Z", - "price_change_percentage_1y_in_currency": -2.536699358617614 + "id": "polkadot", + "ledgerIds": ["polkadot"], + "ticker": "dot", + "name": "Polkadot", + "image": "https://proxycgassets.api.live.ledger.com/coins/images/12171/large/polkadot.png", + "marketCap": 9387497633, + "marketCapRank": 16, + "fullyDilutedValuation": 9944551507, + "totalVolume": 192369097, + "high24h": 6.92, + "low24h": 6.47, + "price": 6.89, + "priceChange24h": 0.281748, + "priceChangePercentage1h": -0.450501959656659, + "priceChangePercentage24h": 4.26669440683288, + "priceChangePercentage7d": -2.47705390182749, + "priceChangePercentage30d": -2.29806572154937, + "priceChangePercentage1y": 28.0100291798888, + "marketCapChange24h": 356054752, + "marketCapChangePercentage24h": 3.94239, + "circulatingSupply": 1364158529.92179, + "totalSupply": 1445107662.82179, + "maxSupply": null, + "allTimeHigh": 54.98, + "allTimeLow": 2.7, + "allTimeHighDate": "2021-11-04T14:10:09.301Z", + "allTimeLowDate": "2020-08-20T05:48:11.359Z", + "sparkline": [ + 7.146806, 7.0923047, 6.9858136, 7.007853, 6.9929857, 6.788605, 6.90348, 7.0047355, 7.0666695, + 7.074595, 7.0987244, 7.0782356, 6.85487, 6.829148, 6.7338724, 6.7446537, 6.76722, 6.6693707, + 6.7202005, 6.7247276, 6.6856637, 6.6914563, 6.698286, 6.719442, 6.7255535, 6.6847715, + 6.6195474, 6.5121765, 6.584909, 6.742404, 6.7130146, 6.6714516, 6.6642838, 6.643025, + 6.5860977, 6.6079116, 6.5997944, 6.557529, 6.4943366, 6.519899, 6.4902596, 6.6441603 + ], + "updatedAt": "2024-05-15T14:48:37Z" }, { - "id": "litecoin", - "symbol": "ltc", - "name": "Litecoin", - "image": "https://assets.coingecko.com/coins/images/2/large/litecoin.png?1696501400", - "current_price": 357.03, - "market_cap": 26436046404, - "market_cap_rank": 19, - "fully_diluted_valuation": 30017689896, - "total_volume": 1702664545, - "high_24h": 361.05, - "low_24h": 351.0, - "price_change_24h": -1.4985690071131899, - "price_change_percentage_24h": -0.41797, - "market_cap_change_24h": -187137241.22453308, - "market_cap_change_percentage_24h": -0.70291, - "circulating_supply": 73977308.2334713, - "total_supply": 84000000.0, - "max_supply": 84000000.0, - "ath": 2148.55, - "ath_change_percentage": -83.45505, - "ath_date": "2021-05-10T03:13:07.904Z", - "atl": 3.02, - "atl_change_percentage": 11681.60947, - "atl_date": "2015-01-14T00:00:00.000Z", - "roi": null, - "last_updated": "2023-12-13T13:16:49.249Z", - "price_change_percentage_1y_in_currency": -11.804501633759713 + "id": "bitcoin-cash", + "ledgerIds": ["bitcoin_cash"], + "ticker": "bch", + "name": "Bitcoin Cash", + "image": "https://proxycgassets.api.live.ledger.com/coins/images/780/large/bitcoin-cash-circle.png", + "marketCap": 8754025935, + "marketCapRank": 17, + "fullyDilutedValuation": 9328911844, + "totalVolume": 240547582, + "high24h": 455.73, + "low24h": 424.82, + "price": 443.49, + "priceChange24h": 9.85, + "priceChangePercentage1h": -1.5501825019966, + "priceChangePercentage24h": 2.27246744961323, + "priceChangePercentage7d": -4.38919939735473, + "priceChangePercentage30d": -16.554636748717, + "priceChangePercentage1y": 277.308856899426, + "marketCapChange24h": 189550516, + "marketCapChangePercentage24h": 2.21322, + "circulatingSupply": 19705893.6466508, + "totalSupply": 21000000, + "maxSupply": 21000000, + "allTimeHigh": 3785.82, + "allTimeLow": 76.93, + "allTimeHighDate": "2017-12-20T00:00:00Z", + "allTimeLowDate": "2018-12-16T00:00:00Z", + "sparkline": [ + 463.5754, 460.12888, 445.894, 455.58884, 451.4487, 445.86404, 446.8979, 447.555, 454.75186, + 451.72427, 455.84668, 453.20068, 433.4107, 427.29486, 425.9203, 430.7456, 429.33304, + 427.37115, 431.0011, 432.00452, 430.30698, 430.67712, 432.51508, 437.51672, 432.23755, + 430.7697, 433.14297, 424.362, 434.0329, 441.22513, 441.16437, 441.26947, 437.622, 433.84973, + 433.0171, 435.68076, 435.86185, 431.66513, 429.1921, 428.6377, 429.43994, 429.1287 + ], + "updatedAt": "2024-05-15T14:48:29Z" }, { - "id": "dai", - "symbol": "dai", - "name": "Dai", - "image": "https://assets.coingecko.com/coins/images/9956/large/Badge_Dai.png?1696509996", - "current_price": 4.97, - "market_cap": 26231930324, - "market_cap_rank": 20, - "fully_diluted_valuation": 26231930324, - "total_volume": 1494989198, - "high_24h": 4.97, - "low_24h": 4.92, - "price_change_24h": 0.04746639, - "price_change_percentage_24h": 0.96434, - "market_cap_change_24h": -100895363.19909286, - "market_cap_change_percentage_24h": -0.38315, - "circulating_supply": 5282558251.89807, - "total_supply": 5282558251.89807, - "max_supply": null, - "ath": 6.01, - "ath_change_percentage": -17.50987, - "ath_date": "2020-05-14T14:09:14.858Z", - "atl": 3.79, - "atl_change_percentage": 30.75587, - "atl_date": "2019-11-25T00:04:18.137Z", - "roi": null, - "last_updated": "2023-12-13T13:15:16.921Z", - "price_change_percentage_1y_in_currency": -6.291841659229985 + "id": "near", + "ledgerIds": ["near"], + "ticker": "near", + "name": "NEAR Protocol", + "image": "https://proxycgassets.api.live.ledger.com/coins/images/10365/large/near.jpg", + "marketCap": 8300340263, + "marketCapRank": 18, + "fullyDilutedValuation": 9126230760, + "totalVolume": 488522237, + "high24h": 7.73, + "low24h": 6.91, + "price": 7.75, + "priceChange24h": 0.663033, + "priceChangePercentage1h": 1.939411408338, + "priceChangePercentage24h": 9.35171016952677, + "priceChangePercentage7d": 8.17314768922293, + "priceChangePercentage30d": 39.6011269853427, + "priceChangePercentage1y": 363.59687439976, + "marketCapChange24h": 718692472, + "marketCapChangePercentage24h": 9.47937, + "circulatingSupply": 1076166720.91241, + "totalSupply": 1183246170.6779, + "maxSupply": null, + "allTimeHigh": 20.44, + "allTimeLow": 0.526762, + "allTimeHighDate": "2022-01-16T22:09:45.873Z", + "allTimeLowDate": "2020-11-04T16:09:15.137Z", + "sparkline": [ + 7.1171484, 7.005506, 6.8221536, 7.0155625, 7.242098, 6.96699, 7.2568116, 7.1692495, 7.4857078, + 7.555961, 7.5444703, 7.5104475, 7.3398886, 7.2963104, 7.192645, 7.2312255, 7.173561, + 7.1674075, 7.0487328, 7.1037083, 7.044606, 7.024968, 6.969458, 7.008387, 6.974627, 6.902136, + 6.865781, 6.7292852, 6.8305564, 7.0599904, 7.253971, 7.1597867, 7.2632213, 7.159971, + 7.2187934, 7.1860104, 7.089032, 7.001808, 7.030193, 6.961305, 6.972693, 7.190066 + ], + "updatedAt": "2024-05-15T14:48:37Z" + }, + { + "id": "chainlink", + "ledgerIds": [ + "telos_evm/erc20/chainlink_token", + "ethereum/erc20/link_chainlink", + "linea/erc20/chainlink_token", + "polygon/erc20/chainlink_token", + "fantom/erc20/chainlink", + "polygon_zk_evm/erc20/chainlink_token", + "bsc/bep20/binance-peg_chainlink_token", + "arbitrum/erc20/chainlink_token", + "cronos/erc20/chainlink_token", + "optimism/erc20/chainlink_token", + "avalanche_c_chain/erc20/chainlink_token_(bridged)" + ], + "ticker": "link", + "name": "Chainlink", + "image": "https://proxycgassets.api.live.ledger.com/coins/images/877/large/chainlink-new-logo.png", + "marketCap": 7969279610, + "marketCapRank": 19, + "fullyDilutedValuation": 13573973769, + "totalVolume": 358158059, + "high24h": 13.63, + "low24h": 12.84, + "price": 13.56, + "priceChange24h": 0.381302, + "priceChangePercentage1h": -0.251717995556605, + "priceChangePercentage24h": 2.89377817834872, + "priceChangePercentage7d": -3.12126529927258, + "priceChangePercentage30d": -6.0156204102492, + "priceChangePercentage1y": 104.0525709586, + "marketCapChange24h": 199190900, + "marketCapChangePercentage24h": 2.56356, + "circulatingSupply": 587099971.308341, + "totalSupply": 1000000000, + "maxSupply": 1000000000, + "allTimeHigh": 52.7, + "allTimeLow": 0.148183, + "allTimeHighDate": "2021-05-10T00:13:57.214Z", + "allTimeLowDate": "2017-11-29T00:00:00Z", + "sparkline": [ + 14.061462, 14.007142, 13.865917, 14.021732, 14.030632, 13.995071, 13.902331, 14.216178, + 14.341153, 14.280597, 14.318934, 14.289838, 13.821878, 13.679843, 13.528457, 13.667454, + 13.599427, 13.507352, 13.484882, 13.46367, 13.358525, 13.4093, 13.394172, 13.434845, + 13.5096655, 13.45807, 13.557987, 13.282213, 13.35756, 13.447565, 13.478754, 13.358292, + 13.380402, 13.461963, 13.3122225, 13.375667, 13.173788, 13.084595, 12.9830065, 12.942201, + 13.004883, 13.127375 + ], + "updatedAt": "2024-05-15T14:48:31Z" + }, + { + "id": "matic-network", + "ledgerIds": [ + "bsc/bep20/matic_token", + "polygon/erc20/matic_token", + "polygon", + "moonbeam/erc20/matic", + "ethereum/erc20/matic" + ], + "ticker": "matic", + "name": "Polygon", + "image": "https://proxycgassets.api.live.ledger.com/coins/images/4713/large/polygon.png", + "marketCap": 6274749518, + "marketCapRank": 20, + "fullyDilutedValuation": 6759439475, + "totalVolume": 277875402, + "high24h": 0.676284, + "low24h": 0.645395, + "price": 0.675435, + "priceChange24h": 0.02198214, + "priceChangePercentage1h": 0.339274489807851, + "priceChangePercentage24h": 3.36399926556075, + "priceChangePercentage7d": -1.73638527317055, + "priceChangePercentage30d": -8.13065660761741, + "priceChangePercentage1y": -22.1104428924695, + "marketCapChange24h": 186299377, + "marketCapChangePercentage24h": 3.05988, + "circulatingSupply": 9282943566.20399, + "totalSupply": 10000000000, + "maxSupply": 10000000000, + "allTimeHigh": 2.92, + "allTimeLow": 0.00314376, + "allTimeHighDate": "2021-12-27T02:08:34.307Z", + "allTimeLowDate": "2019-05-10T00:00:00Z", + "sparkline": [ + 0.6885055, 0.68996376, 0.68130785, 0.68870735, 0.68711394, 0.67408085, 0.6816523, 0.68900865, + 0.6961657, 0.69442683, 0.7003287, 0.698874, 0.67339766, 0.67286164, 0.6693012, 0.6853905, + 0.6813321, 0.68212163, 0.6834285, 0.68382174, 0.6794348, 0.67896974, 0.6772547, 0.6787339, + 0.6792699, 0.6759835, 0.6714718, 0.6530839, 0.66719157, 0.67193013, 0.6677646, 0.66395426, + 0.6630664, 0.6594885, 0.6576574, 0.6598124, 0.6542934, 0.6557534, 0.64937675, 0.6522604, + 0.65056443, 0.6598375 + ], + "updatedAt": "2024-05-15T14:48:55Z" } ] diff --git a/apps/ledger-live-mobile/__tests__/handlers/market.ts b/apps/ledger-live-mobile/__tests__/handlers/market.ts index 44dee561c395..e44ef0ab5604 100644 --- a/apps/ledger-live-mobile/__tests__/handlers/market.ts +++ b/apps/ledger-live-mobile/__tests__/handlers/market.ts @@ -4,9 +4,14 @@ import supportedVsCurrenciesMock from "@mocks/api/market/supportedVsCurrencies.j import coinsListMock from "@mocks/api/market/coinsList.json"; const handlers = [ - http.get("https://proxycg.api.live.ledger.com/api/v3/coins/markets", ({ request }) => { + http.get("https://countervalues.live.ledger.com/v3/markets", ({ request }) => { const searchParams = new URLSearchParams(request.url); // When we perform a search + if (searchParams.get("filter")) { + const coins = searchParams.get("filter")?.toLowerCase().split(",") || []; + return HttpResponse.json(marketsMock.filter(({ ticker }) => coins.includes(ticker))); + } + // When we perform starred if (searchParams.get("ids")) { const coins = searchParams.get("ids")?.split(",") || []; return HttpResponse.json(marketsMock.filter(({ id }) => coins.includes(id))); diff --git a/apps/ledger-live-mobile/__tests__/test-renderer.tsx b/apps/ledger-live-mobile/__tests__/test-renderer.tsx index ac61604b0fd5..8cdcc42230c4 100644 --- a/apps/ledger-live-mobile/__tests__/test-renderer.tsx +++ b/apps/ledger-live-mobile/__tests__/test-renderer.tsx @@ -25,6 +25,7 @@ import { INITIAL_STATE as DYNAMIC_CONTENT_INITIAL_STATE } from "~/reducers/dynam import { INITIAL_STATE as WALLET_CONNECT_INITIAL_STATE } from "~/reducers/walletconnect"; import { INITIAL_STATE as PROTECT_INITIAL_STATE } from "~/reducers/protect"; import { INITIAL_STATE as NFT_INITIAL_STATE } from "~/reducers/nft"; +import { INITIAL_STATE as MARKET_INITIAL_STATE } from "~/reducers/market"; import { initialState as WALLET_INITIAL_STATE } from "@ledgerhq/live-wallet/store"; const initialState = { @@ -41,6 +42,7 @@ const initialState = { postOnboarding: POST_ONBOARDING_INITIAL_STATE, protect: PROTECT_INITIAL_STATE, nft: NFT_INITIAL_STATE, + market: MARKET_INITIAL_STATE, wallet: WALLET_INITIAL_STATE, }; diff --git a/apps/ledger-live-mobile/e2e/models/market/marketPage.ts b/apps/ledger-live-mobile/e2e/models/market/marketPage.ts index ad25e03b695b..8abc59c8c7c6 100644 --- a/apps/ledger-live-mobile/e2e/models/market/marketPage.ts +++ b/apps/ledger-live-mobile/e2e/models/market/marketPage.ts @@ -4,7 +4,7 @@ export default class MarketPage { searchBar = () => getElementById("search-box"); starButton = () => getElementById("star-asset"); assetCardBackBtn = () => getElementById("market-back-btn"); - starMarketListButton = () => getElementById("starred"); + starMarketListButton = () => getElementById("toggle-starred-currencies"); buyAssetButton = () => getElementById("market-buy-btn"); searchAsset(asset: string) { diff --git a/apps/ledger-live-mobile/src/AppProviders.tsx b/apps/ledger-live-mobile/src/AppProviders.tsx index aef39f38e20c..ae66d4220483 100644 --- a/apps/ledger-live-mobile/src/AppProviders.tsx +++ b/apps/ledger-live-mobile/src/AppProviders.tsx @@ -9,7 +9,6 @@ import { OnboardingContextProvider } from "~/screens/Onboarding/onboardingContex import CounterValuesProvider from "~/components/CounterValuesProvider"; import NotificationsProvider from "~/screens/NotificationCenter/NotificationsProvider"; import SnackbarContainer from "~/screens/NotificationCenter/Snackbar/SnackbarContainer"; -import MarketDataProvider from "LLM/features/Market/components//MarketDataProviderWrapper"; import PostOnboardingProviderWrapped from "~/logic/postOnboarding/PostOnboardingProviderWrapped"; import { CounterValuesStateRaw } from "@ledgerhq/live-countervalues/types"; import { CountervaluesMarketcap } from "@ledgerhq/live-countervalues-react/index"; @@ -34,7 +33,7 @@ function AppProviders({ initialCountervalues, children }: AppProvidersProps) { - {children} + {children} diff --git a/apps/ledger-live-mobile/src/actions/market.ts b/apps/ledger-live-mobile/src/actions/market.ts new file mode 100644 index 000000000000..588084a0d0c7 --- /dev/null +++ b/apps/ledger-live-mobile/src/actions/market.ts @@ -0,0 +1,20 @@ +import { createAction } from "redux-actions"; +import { + MarketSetMarketRequestParamsPayload, + MarketStateActionTypes, + MarketSetCurrentPagePayload, + MarketSetMarketFilterByStarredCurrenciesPayload, +} from "./types"; + +export const setMarketRequestParams = createAction( + MarketStateActionTypes.SET_MARKET_REQUEST_PARAMS, +); + +export const setMarketFilterByStarredCurrencies = + createAction( + MarketStateActionTypes.SET_MARKET_FILTER_BY_STARRED_CURRENCIES, + ); + +export const setMarketCurrentPage = createAction( + MarketStateActionTypes.MARKET_SET_CURRENT_PAGE, +); diff --git a/apps/ledger-live-mobile/src/actions/settings.ts b/apps/ledger-live-mobile/src/actions/settings.ts index aab9c40e51e4..6d59a73a1536 100755 --- a/apps/ledger-live-mobile/src/actions/settings.ts +++ b/apps/ledger-live-mobile/src/actions/settings.ts @@ -6,7 +6,6 @@ import type { PortfolioRange } from "@ledgerhq/types-live"; import { selectedTimeRangeSelector } from "../reducers/settings"; import { SettingsAcceptSwapProviderPayload, - SettingsAddStarredMarketcoinsPayload, SettingsBlacklistTokenPayload, DangerouslyOverrideStatePayload, SettingsDismissBannerPayload, @@ -16,7 +15,6 @@ import { SettingsImportPayload, SettingsSetHasInstalledAnyAppPayload, SettingsLastSeenDeviceInfoPayload, - SettingsRemoveStarredMarketcoinsPayload, SettingsSetAnalyticsPayload, SettingsSetPersonalizedRecommendationsPayload, SettingsSetAvailableUpdatePayload, @@ -30,8 +28,6 @@ import { SettingsSetLocalePayload, SettingsSetCustomImageBackupPayload, SettingsSetMarketCounterCurrencyPayload, - SettingsSetMarketFilterByStarredAccountsPayload, - SettingsSetMarketRequestParamsPayload, SettingsSetNotificationsPayload, SettingsSetNeverClickedOnAllowNotificationsButton, SettingsSetOrderAccountsPayload, @@ -71,6 +67,8 @@ import { SettingsSetHasSeenAnalyticsOptInPrompt, SettingsSetDismissedContentCardsPayload, SettingsClearDismissedContentCardsPayload, + SettingsAddStarredMarketcoinsPayload, + SettingsRemoveStarredMarketcoinsPayload, } from "./types"; import { ImageType } from "~/components/CustomImage/types"; @@ -197,12 +195,6 @@ const setHasSeenStaxEnabledNftsPopupAction = ); export const setHasSeenStaxEnabledNftsPopup = (hasSeenStaxEnabledNftsPopup: boolean) => setHasSeenStaxEnabledNftsPopupAction({ hasSeenStaxEnabledNftsPopup }); -export const addStarredMarketCoins = createAction( - SettingsActionTypes.ADD_STARRED_MARKET_COINS, -); -export const removeStarredMarketCoins = createAction( - SettingsActionTypes.REMOVE_STARRED_MARKET_COINS, -); export const setLastConnectedDevice = createAction( SettingsActionTypes.SET_LAST_CONNECTED_DEVICE, ); @@ -217,16 +209,9 @@ export const setCustomImageType = (imageType: ImageType) => export const setHasOrderedNano = createAction( SettingsActionTypes.SET_HAS_ORDERED_NANO, ); -export const setMarketRequestParams = createAction( - SettingsActionTypes.SET_MARKET_REQUEST_PARAMS, -); export const setMarketCounterCurrency = createAction( SettingsActionTypes.SET_MARKET_COUNTER_CURRENCY, ); -export const setMarketFilterByStarredAccounts = - createAction( - SettingsActionTypes.SET_MARKET_FILTER_BY_STARRED_ACCOUNTS, - ); export const setSensitiveAnalytics = createAction( SettingsActionTypes.SET_SENSITIVE_ANALYTICS, ); @@ -300,6 +285,13 @@ export const clearDismissedContentCards = createAction( + SettingsActionTypes.ADD_STARRED_MARKET_COINS, +); +export const removeStarredMarketCoins = createAction( + SettingsActionTypes.REMOVE_STARRED_MARKET_COINS, +); + type PortfolioRangeOption = { key: PortfolioRange; value: string; diff --git a/apps/ledger-live-mobile/src/actions/types.ts b/apps/ledger-live-mobile/src/actions/types.ts index 1d05e05364f7..f731977bfbbd 100644 --- a/apps/ledger-live-mobile/src/actions/types.ts +++ b/apps/ledger-live-mobile/src/actions/types.ts @@ -31,6 +31,7 @@ import type { DynamicContentState, ProtectState, NftState, + MarketState, } from "../reducers/types"; import type { Unpacked } from "../types/helpers"; import { HandlersPayloads } from "@ledgerhq/live-wallet/store"; @@ -252,13 +253,10 @@ export enum SettingsActionTypes { SET_KNOWN_DEVICE_MODEL_IDS = "SET_KNOWN_DEVICE_MODEL_IDS", SET_HAS_SEEN_STAX_ENABLED_NFTS_POPUP = "SET_HAS_SEEN_STAX_ENABLED_NFTS_POPUP", SET_LAST_SEEN_CUSTOM_IMAGE = "SET_LAST_SEEN_CUSTOM_IMAGE", - ADD_STARRED_MARKET_COINS = "ADD_STARRED_MARKET_COINS", - REMOVE_STARRED_MARKET_COINS = "REMOVE_STARRED_MARKET_COINS", SET_LAST_CONNECTED_DEVICE = "SET_LAST_CONNECTED_DEVICE", SET_CUSTOM_IMAGE_TYPE = "SET_CUSTOM_IMAGE_TYPE", SET_CUSTOM_IMAGE_BACKUP = "SET_CUSTOM_IMAGE_BACKUP", SET_HAS_ORDERED_NANO = "SET_HAS_ORDERED_NANO", - SET_MARKET_REQUEST_PARAMS = "SET_MARKET_REQUEST_PARAMS", SET_MARKET_COUNTER_CURRENCY = "SET_MARKET_COUNTER_CURRENCY", SET_MARKET_FILTER_BY_STARRED_ACCOUNTS = "SET_MARKET_FILTER_BY_STARRED_ACCOUNTS", SET_SENSITIVE_ANALYTICS = "SET_SENSITIVE_ANALYTICS", @@ -281,6 +279,9 @@ export enum SettingsActionTypes { SET_HAS_SEEN_ANALYTICS_OPT_IN_PROMPT = "SET_HAS_SEEN_ANALYTICS_OPT_IN_PROMPT", SET_DISMISSED_CONTENT_CARD = "SET_DISMISSED_CONTENT_CARD", CLEAR_DISMISSED_CONTENT_CARDS = "CLEAR_DISMISSED_CONTENT_CARDS", + + ADD_STARRED_MARKET_COINS = "ADD_STARRED_MARKET_COINS", + REMOVE_STARRED_MARKET_COINS = "REMOVE_STARRED_MARKET_COINS", } export type SettingsImportPayload = Partial; @@ -334,8 +335,6 @@ export type SettingsLastSeenDevicePayload = NonNullable< export type SettingsLastSeenDeviceInfoPayload = DeviceModelInfo; export type SettingsLastSeenDeviceLanguagePayload = DeviceInfo["languageId"]; export type SettingsSetKnownDeviceModelIdsPayload = { [key in DeviceModelId]?: boolean }; -export type SettingsAddStarredMarketcoinsPayload = Unpacked; -export type SettingsRemoveStarredMarketcoinsPayload = Unpacked; export type SettingsSetLastConnectedDevicePayload = Device; export type SettingsSetHasSeenStaxEnabledNftsPopupPayload = Pick< SettingsState, @@ -348,10 +347,7 @@ export type SettingsSetCustomImageBackupPayload = { } | null; export type SettingsSetCustomImageTypePayload = Pick; export type SettingsSetHasOrderedNanoPayload = SettingsState["hasOrderedNano"]; -export type SettingsSetMarketRequestParamsPayload = SettingsState["marketRequestParams"]; export type SettingsSetMarketCounterCurrencyPayload = SettingsState["marketCounterCurrency"]; -export type SettingsSetMarketFilterByStarredAccountsPayload = - SettingsState["marketFilterByStarredAccounts"]; export type SettingsSetSensitiveAnalyticsPayload = SettingsState["sensitiveAnalytics"]; export type SettingsSetOnboardingHasDevicePayload = SettingsState["onboardingHasDevice"]; @@ -388,6 +384,9 @@ export type SettingsSetHasSeenAnalyticsOptInPrompt = SettingsState["hasSeenAnaly export type SettingsSetDismissedContentCardsPayload = SettingsState["dismissedContentCards"]; export type SettingsClearDismissedContentCardsPayload = string[]; +export type SettingsAddStarredMarketcoinsPayload = Unpacked; +export type SettingsRemoveStarredMarketcoinsPayload = Unpacked; + export type SettingsPayload = | SettingsImportPayload | SettingsImportDesktopPayload @@ -421,13 +420,9 @@ export type SettingsPayload = | SettingsLastSeenDeviceLanguagePayload | SettingsLastSeenDeviceInfoPayload | SettingsSetLastSeenCustomImagePayload - | SettingsAddStarredMarketcoinsPayload - | SettingsRemoveStarredMarketcoinsPayload | SettingsSetLastConnectedDevicePayload | SettingsSetHasOrderedNanoPayload - | SettingsSetMarketRequestParamsPayload | SettingsSetMarketCounterCurrencyPayload - | SettingsSetMarketFilterByStarredAccountsPayload | SettingsSetSensitiveAnalyticsPayload | SettingsSetOnboardingHasDevicePayload | SettingsSetNotificationsPayload @@ -446,7 +441,9 @@ export type SettingsPayload = | SettingsSetSupportedCounterValues | SettingsSetHasSeenAnalyticsOptInPrompt | SettingsSetDismissedContentCardsPayload - | SettingsClearDismissedContentCardsPayload; + | SettingsClearDismissedContentCardsPayload + | SettingsAddStarredMarketcoinsPayload + | SettingsRemoveStarredMarketcoinsPayload; // === WALLET CONNECT ACTIONS === export enum WalletConnectActionTypes { @@ -517,3 +514,20 @@ export type NftStateGalleryFilterDrawerVisiblePayload = NftState["filterDrawerVi export type NftStatePayload = | NftStateGalleryChainFiltersPayload | NftStateGalleryFilterDrawerVisiblePayload; + +// === MARKET ACTIONS === +export enum MarketStateActionTypes { + SET_MARKET_REQUEST_PARAMS = "SET_MARKET_REQUEST_PARAMS", + SET_MARKET_FILTER_BY_STARRED_CURRENCIES = "SET_MARKET_FILTER_BY_STARRED_CURRENCIES", + MARKET_SET_CURRENT_PAGE = "MARKET_SET_CURRENT_PAGE", +} + +export type MarketSetMarketFilterByStarredCurrenciesPayload = + MarketState["marketFilterByStarredCurrencies"]; +export type MarketSetCurrentPagePayload = MarketState["marketCurrentPage"]; +export type MarketSetMarketRequestParamsPayload = MarketState["marketParams"]; + +export type MarketStatePayload = + | MarketSetMarketFilterByStarredCurrenciesPayload + | MarketSetMarketRequestParamsPayload + | MarketSetCurrentPagePayload; diff --git a/apps/ledger-live-mobile/src/components/WalletTab/CollapsibleHeaderFlatList.tsx b/apps/ledger-live-mobile/src/components/WalletTab/CollapsibleHeaderFlatList.tsx index 45d1eb2f2d7b..0577d9b4de3c 100644 --- a/apps/ledger-live-mobile/src/components/WalletTab/CollapsibleHeaderFlatList.tsx +++ b/apps/ledger-live-mobile/src/components/WalletTab/CollapsibleHeaderFlatList.tsx @@ -24,6 +24,7 @@ function CollapsibleHeaderFlatList({ return ( + {...otherProps} scrollToOverflowEnabled={true} ref={(ref: FlatList) => onGetRef({ key: route.name, value: ref })} scrollEventThrottle={16} @@ -46,7 +47,6 @@ function CollapsibleHeaderFlatList({ ]} showsHorizontalScrollIndicator={false} showsVerticalScrollIndicator={false} - {...otherProps} > {children} diff --git a/apps/ledger-live-mobile/src/db.ts b/apps/ledger-live-mobile/src/db.ts index 5c1ed29785d0..95f105eb83ed 100644 --- a/apps/ledger-live-mobile/src/db.ts +++ b/apps/ledger-live-mobile/src/db.ts @@ -45,6 +45,7 @@ export async function getSettings(): Promise> { export async function saveSettings(obj: Partial): Promise { await store.save("settings", obj); } + export async function getWCSession(): Promise { const wcsession = await store.get("wcsession"); return wcsession; diff --git a/apps/ledger-live-mobile/src/index.tsx b/apps/ledger-live-mobile/src/index.tsx index 777ba377f0bd..0e5f1166ed96 100644 --- a/apps/ledger-live-mobile/src/index.tsx +++ b/apps/ledger-live-mobile/src/index.tsx @@ -300,7 +300,6 @@ export default class Root extends Component { - diff --git a/apps/ledger-live-mobile/src/locales/en/common.json b/apps/ledger-live-mobile/src/locales/en/common.json index aaf636c4667d..13dbc6c06e73 100644 --- a/apps/ledger-live-mobile/src/locales/en/common.json +++ b/apps/ledger-live-mobile/src/locales/en/common.json @@ -2245,7 +2245,13 @@ "marketPriceSection": { "title": "{{currencyTicker}} market price", "currencyPrice": "1 {{currencyTicker}} price", - "currencyPriceChange": "Last 24h change" + "currencyPriceChange": { + "day": "Last 24h change", + "week": "Last 7 days change", + "month": "Last 30 days change", + "year": "Last year change", + "all": "Last year change" + } }, "quickActions": { "buy": "Buy", @@ -6238,9 +6244,10 @@ }, "order": { "topGainers": "Top gainers", - "market_cap": "Rank", - "market_cap_asc": "Rank (Market cap) asc.", - "market_cap_desc": "Rank (Market cap) desc." + "topLosers": "Top losers", + "marketCap": "Rank", + "asc": "Rank (Market cap) asc.", + "desc": "Rank (Market cap) desc." }, "currency": "Currency", "time": "Time", diff --git a/apps/ledger-live-mobile/src/newArch/features/Market/__integrations__/changeCurrency.integration.test.tsx b/apps/ledger-live-mobile/src/newArch/features/Market/__integrations__/changeCurrency.integration.test.tsx index e33ac4582134..52be5d1bb979 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Market/__integrations__/changeCurrency.integration.test.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Market/__integrations__/changeCurrency.integration.test.tsx @@ -38,11 +38,11 @@ describe("Market integration test", () => { }); expect(await screen.findByText("Bitcoin (BTC)")).toBeOnTheScreen(); - expect(await screen.findByText("$4.004 tn")).toBeOnTheScreen(); + expect(await screen.findByText("$1.267 tn")).toBeOnTheScreen(); await user.press(screen.getByText("Currency")); expect(await screen.findByText("Euro - EUR")).toBeOnTheScreen(); await user.press(screen.getByText("Euro - EUR")); - expect(await screen.findByText("€4.004 tn")).toBeOnTheScreen(); + expect(await screen.findByText("€1.267 tn")).toBeOnTheScreen(); }); }); diff --git a/apps/ledger-live-mobile/src/newArch/features/Market/__integrations__/setFavorites.integration.test.tsx b/apps/ledger-live-mobile/src/newArch/features/Market/__integrations__/setFavorites.integration.test.tsx index e47a25915fde..05725b0c022f 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Market/__integrations__/setFavorites.integration.test.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Market/__integrations__/setFavorites.integration.test.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { screen } from "@testing-library/react-native"; +import { screen, waitFor } from "@testing-library/react-native"; import { render } from "@tests/test-renderer"; import { MarketPages } from "./shared"; @@ -9,26 +9,28 @@ describe("Market integration test", () => { //Set BTC as favorite expect(await screen.findByText("Bitcoin (BTC)")).toBeOnTheScreen(); - await user.press(screen.getByText("Bitcoin (BTC)")); await user.press(await screen.findByTestId("star-asset")); await user.press(screen.getByTestId("market-back-btn")); - const ethRow = await screen.findByText("Ethereum (ETH)"); - - await user.press(await screen.findByTestId("starred")); + await waitFor(() => screen.findByTestId("toggle-starred-currencies")); + const ethRow = await screen.findByText("Ethereum (ETH)"); expect(await screen.findByText("Bitcoin (BTC)")).toBeOnTheScreen(); + + await user.press(await screen.findByTestId("toggle-starred-currencies")); expect(ethRow).not.toBeOnTheScreen(); //Set BNB as favorite - await user.press(await screen.findByTestId("starred")); + await user.press(await screen.findByTestId("toggle-starred-currencies")); await user.press(await screen.findByText("BNB (BNB)")); await user.press(await screen.findByTestId("star-asset")); await user.press(screen.getByTestId("market-back-btn")); + + await waitFor(() => screen.findByTestId("toggle-starred-currencies")); const ethRow2 = await screen.findByText("Ethereum (ETH)"); - await user.press(await screen.findByTestId("starred")); + await user.press(await screen.findByTestId("toggle-starred-currencies")); expect(await screen.findByText("Bitcoin (BTC)")).toBeOnTheScreen(); expect(await screen.findByText("BNB (BNB)")).toBeOnTheScreen(); diff --git a/apps/ledger-live-mobile/src/newArch/features/Market/__integrations__/shared.tsx b/apps/ledger-live-mobile/src/newArch/features/Market/__integrations__/shared.tsx index dec31861ff9f..992d1c0baf73 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Market/__integrations__/shared.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Market/__integrations__/shared.tsx @@ -1,25 +1,25 @@ -/* eslint-disable i18next/no-literal-string */ import * as React from "react"; import MarketNavigator, { MarketNavigatorStackParamList } from "LLM/features/Market/Navigator"; -import MarketDataProviderWrapper from "../components/MarketDataProviderWrapper"; + import WalletTabNavigatorScrollManager from "~/components/WalletTab/WalletTabNavigatorScrollManager"; import { createStackNavigator } from "@react-navigation/stack"; import { ScreenName } from "~/const"; import MarketList from "../screens/MarketList"; import { BaseNavigatorStackParamList } from "~/components/RootNavigator/types/BaseNavigator"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; const Stack = createStackNavigator(); const StackWalletTab = createStackNavigator(); export function MarketPages() { return ( - + {MarketNavigator({ Stack })} - + ); } diff --git a/apps/ledger-live-mobile/src/newArch/features/Market/components/MarketDataProviderWrapper/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Market/components/MarketDataProviderWrapper/index.tsx deleted file mode 100644 index c61c1f2e82a8..000000000000 --- a/apps/ledger-live-mobile/src/newArch/features/Market/components/MarketDataProviderWrapper/index.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React, { ReactElement } from "react"; -import { useSelector } from "react-redux"; -import apiMock from "@ledgerhq/live-common/market/api/api.mock"; -import Config from "react-native-config"; -import { MarketDataProvider } from "@ledgerhq/live-common/market/MarketDataProvider"; -import { useNetInfo } from "@react-native-community/netinfo"; -import { Currency } from "@ledgerhq/types-cryptoassets"; -import { - counterValueCurrencySelector, - marketCounterCurrencySelector, - marketFilterByStarredAccountsSelector, - marketRequestParamsSelector, - starredMarketCoinsSelector, -} from "~/reducers/settings"; - -type Props = { - children: React.ReactNode; -}; - -export default function MarketDataProviderWrapper({ children }: Props): ReactElement { - const counterValueCurrency = useSelector(counterValueCurrencySelector); - const marketRequestParams = useSelector(marketRequestParamsSelector); - const marketCounterCurrency = useSelector(marketCounterCurrencySelector); - const starredMarketCoins = useSelector(starredMarketCoinsSelector); - const filterByStarredAccount = useSelector(marketFilterByStarredAccountsSelector); - const { isConnected } = useNetInfo(); - - const counterCurrency = !isConnected - ? undefined // without coutervalues service is not initialized with cg data, this forces it to fetch it at least once the network is on - : marketCounterCurrency // If there is a stored market counter currency we use it, otherwise we use the setting countervalue currency - ? { ticker: marketCounterCurrency } - : counterValueCurrency - ? // @TODO move this toLowercase check on live-common - { ticker: counterValueCurrency.ticker?.toLowerCase() } - : counterValueCurrency; - - return ( - - {children} - - ); -} diff --git a/apps/ledger-live-mobile/src/newArch/features/Market/components/MarketRowItem/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Market/components/MarketRowItem/index.tsx index 73c16a42991f..886a766582f5 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Market/components/MarketRowItem/index.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Market/components/MarketRowItem/index.tsx @@ -1,6 +1,6 @@ import React from "react"; import { Flex, Text } from "@ledgerhq/native-ui"; -import { CurrencyData } from "@ledgerhq/live-common/market/types"; +import { CurrencyData, KeysPriceChange } from "@ledgerhq/live-common/market/utils/types"; import { Image } from "react-native"; import { useTranslation } from "react-i18next"; import CircleCurrencyIcon from "~/components/CircleCurrencyIcon"; @@ -13,21 +13,15 @@ type Props = { index: number; item: CurrencyData; counterCurrency?: string; + range?: string; }; -function MarketRowItem({ item, index, counterCurrency }: Props) { +function MarketRowItem({ item, index, counterCurrency, range }: Props) { const { locale } = useLocale(); const { t } = useTranslation(); - const { - internalCurrency, - image, - name, - marketcap, - marketcapRank, - price, - priceChangePercentage, - ticker, - } = item; + const { internalCurrency, image, name, marketcap, marketcapRank, price, ticker } = item; + + const priceChangePercentage = item?.priceChangePercentage[range as KeysPriceChange]; return ( { + dispatch(setMarketRequestParams(payload ?? {})); + }, + [dispatch], + ); + + return { + dispatch, + refresh, + starredMarketCoins, + filterByStarredCurrencies, + marketParams, + liveCoinsList, + supportedCurrencies, + supportedCounterCurrencies, + marketCurrentPage, + }; +} diff --git a/apps/ledger-live-mobile/src/newArch/features/Market/hooks/useMarketCoinData.ts b/apps/ledger-live-mobile/src/newArch/features/Market/hooks/useMarketCoinData.ts new file mode 100644 index 000000000000..a3a8bad5c8bd --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/features/Market/hooks/useMarketCoinData.ts @@ -0,0 +1,38 @@ +import { + useCurrencyChartData, + useCurrencyData, +} from "@ledgerhq/live-common/market/hooks/useMarketDataProvider"; + +import { useSelector } from "react-redux"; +import { marketParamsSelector } from "~/reducers/market"; + +type HookProps = { + currencyId: string; +}; + +export const useMarketCoinData = ({ currencyId }: HookProps) => { + const marketParams = useSelector(marketParamsSelector); + + const { counterCurrency = "usd", range = "24h" } = marketParams; + + const resCurrencyChartData = useCurrencyChartData({ + counterCurrency, + id: currencyId, + range, + }); + + const { data: currency, isLoading } = useCurrencyData({ + counterCurrency, + id: currencyId, + }); + + return { + counterCurrency, + range, + currency, + dataChart: resCurrencyChartData.data, + loadingChart: resCurrencyChartData.isLoading, + loading: isLoading, + marketParams, + }; +}; diff --git a/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketCurrencySelect/useMarketCurrencySelectViewModel.ts b/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketCurrencySelect/useMarketCurrencySelectViewModel.ts index fef80c2e2194..0e7d2822c6bd 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketCurrencySelect/useMarketCurrencySelectViewModel.ts +++ b/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketCurrencySelect/useMarketCurrencySelectViewModel.ts @@ -1,18 +1,20 @@ -import { useMarketData } from "@ledgerhq/live-common/market/MarketDataProvider"; import { useCallback } from "react"; import { useDispatch, useSelector } from "react-redux"; import { supportedCounterValuesSelector } from "~/reducers/settings"; -import { setMarketCounterCurrency } from "~/actions/settings"; import { useNavigation } from "@react-navigation/native"; +import { useMarket } from "../../hooks/useMarket"; +import { setMarketRequestParams } from "~/actions/market"; function useMarketCurrencySelectViewModel() { const navigation = useNavigation(); const dispatch = useDispatch(); const supportedCountervalues = useSelector(supportedCounterValuesSelector); - const { counterCurrency, supportedCounterCurrencies, setCounterCurrency } = useMarketData(); + + const { supportedCounterCurrencies, marketParams } = useMarket(); + const { counterCurrency } = marketParams; const items = supportedCountervalues - .filter(({ ticker }) => supportedCounterCurrencies.includes(ticker.toLowerCase())) + .filter(({ ticker }) => supportedCounterCurrencies?.includes(ticker.toLowerCase())) .map(cur => ({ value: cur.ticker.toLowerCase(), label: cur.label, @@ -20,12 +22,11 @@ function useMarketCurrencySelectViewModel() { .sort(a => (a.value === counterCurrency ? -1 : 0)); const onSelectCurrency = useCallback( - (value: string) => { - dispatch(setMarketCounterCurrency(value)); - setCounterCurrency(value); + (counterCurrency: string) => { + dispatch(setMarketRequestParams({ counterCurrency })); navigation.goBack(); }, - [dispatch, navigation, setCounterCurrency], + [dispatch, navigation], ); return { diff --git a/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketDetail/components/MarketGraph/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketDetail/components/MarketGraph/index.tsx index 0639f4955d62..79a6198a3913 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketDetail/components/MarketGraph/index.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketDetail/components/MarketGraph/index.tsx @@ -1,43 +1,39 @@ import React, { useMemo, useCallback, memo } from "react"; import { useTheme } from "styled-components/native"; -import { Flex, GraphTabs, InfiniteLoader, Transitions } from "@ledgerhq/native-ui"; -import { rangeDataTable } from "@ledgerhq/live-common/market/utils/rangeDataTable"; +import { Flex, GraphTabs, InfiniteLoader, Transitions, ensureContrast } from "@ledgerhq/native-ui"; import { useTranslation } from "react-i18next"; -import { SingleCoinProviderData } from "@ledgerhq/live-common/market/MarketDataProvider"; import Graph from "~/components/Graph"; import getWindowDimensions from "~/logic/getWindowDimensions"; import { Item } from "~/components/Graph/types"; +import { RANGES } from "LLM/features/Market/utils"; +import { MarketCoinDataChart } from "@ledgerhq/live-common/market/utils/types"; +import { getCurrencyColor } from "@ledgerhq/live-common/currencies/index"; +import { CryptoOrTokenCurrency } from "@ledgerhq/types-cryptoassets"; const { width } = getWindowDimensions(); function MarketGraph({ setHoverItem, - chartRequestParams, - loading, - loadingChart, + isLoading, refreshChart, chartData, + range, + currency, }: { setHoverItem: (_: Item | null | undefined) => void; - chartRequestParams: SingleCoinProviderData["chartRequestParams"]; - loading?: boolean; - loadingChart?: boolean; + isLoading?: boolean; refreshChart: (_: { range: string }) => void; - chartData: Record; + chartData?: MarketCoinDataChart; + range: string; + currency?: CryptoOrTokenCurrency; }) { const { t } = useTranslation(); const { colors } = useTheme(); - const ranges = Object.keys(rangeDataTable) - .filter(key => key !== "1h") - .map(r => ({ label: t(`market.range.${r}`), value: r })); + const ranges = RANGES.map(r => ({ label: t(`market.range.${r}`), value: r })); const rangesLabels = ranges.map(({ label }) => label); - const isLoading = loading || loadingChart; - - const { range } = chartRequestParams; - const activeRangeIndex = ranges.findIndex(r => r.value === range); const data = useMemo( () => @@ -61,6 +57,14 @@ function MarketGraph({ const mapGraphValue = useCallback((d: Item) => d?.value || 0, []); + const graphColor = useMemo( + () => + !currency + ? colors.primary.c80 + : ensureContrast(getCurrencyColor(currency), colors.background.main), + [colors.background.main, colors.primary.c80, currency], + ); + return ( @@ -70,7 +74,7 @@ function MarketGraph({ isInteractive height={100} width={width} - color={colors.primary.c80} + color={graphColor} data={data} mapValue={mapGraphValue} onItemHover={setHoverItem} @@ -81,6 +85,7 @@ function MarketGraph({ )} + + {isDefined && value ? ( + <> + {counterValueFormatter({ + currency: withCounterValue ? counterCurrency : undefined, + value: value, + locale, + ticker, + t, + })} + + ) : ( + + - + + )} + + ); + } + return ( {t("market.detailsPage.priceStatistics")} - - {counterValueFormatter({ - currency: counterCurrency, - value: price, - locale, - t, - })} - - {priceChangePercentage !== null && !isNaN(priceChangePercentage) ? ( + + + {!!currency && !!priceChangePercentage && !isNaN(priceChangePercentage) ? ( ) : ( @@ -85,52 +112,39 @@ export default function MarketStats({ )} - - {counterValueFormatter({ - currency: counterCurrency, - value: totalVolume, - locale, - t, - })} - + - - {counterValueFormatter({ - currency: counterCurrency, - value: low24h, - locale, - t, - })}{" "} - /{" "} - {counterValueFormatter({ - currency: counterCurrency, - value: high24h, - locale, - t, - })} - + {currency && low24h && high24h ? ( + + {counterValueFormatter({ + currency: counterCurrency, + value: low24h, + locale, + t, + })}{" "} + /{" "} + {counterValueFormatter({ + currency: counterCurrency, + value: high24h, + locale, + t, + })} + + ) : ( + + - + + )} - - {counterValueFormatter({ - currency: counterCurrency, - value: ath, - locale, - t, - })}{" "} - + + {athDate ? dateFormatter.format(athDate) : "-"} - - {counterValueFormatter({ - currency: counterCurrency, - value: atl, - locale, - t, - })}{" "} - + + {atlDate ? dateFormatter.format(atlDate) : "-"} @@ -155,34 +169,23 @@ export default function MarketStats({ {t("market.detailsPage.supply")} - - {counterValueFormatter({ - value: circulatingSupply, - locale, - ticker, - t, - })} - + - - {counterValueFormatter({ - value: totalSupply, - locale, - ticker, - t, - })} - + - - {counterValueFormatter({ - value: maxSupply, - locale, - ticker, - t, - })} - + ); diff --git a/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketDetail/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketDetail/index.tsx index 80e5912ae4e7..61b2f60ede2a 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketDetail/index.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketDetail/index.tsx @@ -21,8 +21,10 @@ import BackButton from "./components/BackButton"; import { Item } from "~/components/Graph/types"; import { CurrencyData, + MarketCoinDataChart, + KeysPriceChange, MarketCurrencyChartDataRequestParams, -} from "@ledgerhq/live-common/market/types"; +} from "@ledgerhq/live-common/market/utils/types"; import usePullToRefresh from "../../hooks/usePullToRefresh"; import useMarketDetailViewModel from "./useMarketDetailViewModel"; @@ -32,12 +34,13 @@ interface ViewProps { refresh: (param?: MarketCurrencyChartDataRequestParams) => void; defaultAccount?: AccountLike; toggleStar: () => void; - currency?: CurrencyData; isStarred: boolean; accounts: AccountLike[]; counterCurrency?: string; - chartRequestParams: MarketCurrencyChartDataRequestParams; allAccounts: AccountLike[]; + range: string; + dataChart?: MarketCoinDataChart; + currency?: CurrencyData; } function View({ @@ -47,14 +50,15 @@ function View({ defaultAccount, toggleStar, currency, + dataChart, isStarred, accounts, counterCurrency, - chartRequestParams, allAccounts, + range, }: ViewProps) { - const { range } = chartRequestParams; - const { name, image, price, priceChangePercentage, internalCurrency, chartData } = currency || {}; + const { name, image, internalCurrency, price } = currency || {}; + const { handlePullToRefresh, refreshControlVisible } = usePullToRefresh({ loading, refresh }); const [hoveredItem, setHoverItem] = useState(null); const { t } = useTranslation(); @@ -66,6 +70,8 @@ function View({ [locale, range], ); + const priceChangePercentage = currency?.priceChangePercentage[range as KeysPriceChange]; + return ( + {hoveredItem && hoveredItem.date ? ( @@ -129,6 +136,7 @@ function View({ )} + {internalCurrency ? ( } > - {chartData ? ( - - ) : null} + {accounts?.length > 0 ? ( @@ -185,9 +191,12 @@ function View({ /> ) : null} - {currency && counterCurrency && ( - - )} + + ); diff --git a/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketDetail/useMarketDetailViewModel.tsx b/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketDetail/useMarketDetailViewModel.tsx index b518cf3aa090..4e52973b635f 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketDetail/useMarketDetailViewModel.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketDetail/useMarketDetailViewModel.tsx @@ -1,8 +1,6 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { useSingleCoinMarketData } from "@ledgerhq/live-common/market/MarketDataProvider"; -import { readOnlyModeEnabledSelector, starredMarketCoinsSelector } from "~/reducers/settings"; -import { addStarredMarketCoins, removeStarredMarketCoins } from "~/actions/settings"; +import { readOnlyModeEnabledSelector } from "~/reducers/settings"; import { flattenAccountsByCryptoCurrencyScreenSelector } from "~/reducers/accounts"; import { screen, track } from "~/analytics"; import { ScreenName } from "~/const"; @@ -10,6 +8,10 @@ import useNotifications from "~/logic/notifications"; import { BaseComposite, StackNavigatorProps } from "~/components/RootNavigator/types/helpers"; import { MarketNavigatorStackParamList } from "LLM/features/Market/Navigator"; +import { useMarket } from "LLM/features/Market/hooks/useMarket"; +import { useMarketCoinData } from "LLM/features/Market/hooks/useMarketCoinData"; +import { addStarredMarketCoins, removeStarredMarketCoins } from "~/actions/settings"; + type NavigationProps = BaseComposite< StackNavigatorProps >; @@ -17,34 +19,38 @@ type NavigationProps = BaseComposite< function useMarketDetailViewModel({ navigation, route }: NavigationProps) { const { params } = route; const { currencyId, resetSearchOnUmount } = params; + + const { marketParams, dataChart, loadingChart, loading, currency } = useMarketCoinData({ + currencyId, + }); + const dispatch = useDispatch(); - const starredMarketCoins: string[] = useSelector(starredMarketCoinsSelector); - const isStarred = starredMarketCoins.includes(currencyId); const { triggerMarketPushNotificationModal } = useNotifications(); - const [hasRetried, setHasRetried] = useState(false); + const readOnlyModeEnabled = useSelector(readOnlyModeEnabledSelector); - const { - selectedCoinData: currency, - selectCurrency, - chartRequestParams, - loading, - loadingChart, - refreshChart, - counterCurrency, - } = useSingleCoinMarketData(); + const { starredMarketCoins, refresh } = useMarket(); + + const isStarred = starredMarketCoins.includes(currencyId); + + const { counterCurrency = "usd", range = "24h" } = marketParams; const { name, internalCurrency } = currency || {}; useEffect(() => { - if (!loading) { - if (currency === undefined && !hasRetried) { - selectCurrency(currencyId); - setHasRetried(true); - } else if (currency && hasRetried) { - setHasRetried(false); - } + if (name) { + track("Page Market Coin", { + currencyName: name, + starred: isStarred, + timeframe: range, + }); + } + }, [name, isStarred, range]); + + useEffect(() => { + if (readOnlyModeEnabled) { + screen("ReadOnly", "Market Coin"); } - }, [currency, selectCurrency, currencyId, hasRetried, loading]); + }, [readOnlyModeEnabled]); useEffect(() => { const resetState = () => { @@ -54,7 +60,7 @@ function useMarketDetailViewModel({ navigation, route }: NavigationProps) { return () => { sub(); }; - }, [selectCurrency, resetSearchOnUmount, navigation]); + }, [resetSearchOnUmount, navigation]); const allAccounts = useSelector(flattenAccountsByCryptoCurrencyScreenSelector(internalCurrency)); @@ -75,36 +81,19 @@ function useMarketDetailViewModel({ navigation, route }: NavigationProps) { if (!isStarred) triggerMarketPushNotificationModal(); }, [dispatch, isStarred, currencyId, triggerMarketPushNotificationModal]); - useEffect(() => { - if (name) { - track("Page Market Coin", { - currencyName: name, - starred: isStarred, - timeframe: chartRequestParams.range, - }); - } - }, [name, isStarred, chartRequestParams.range]); - - const readOnlyModeEnabled = useSelector(readOnlyModeEnabledSelector); - - useEffect(() => { - if (readOnlyModeEnabled) { - screen("ReadOnly", "Market Coin"); - } - }, [readOnlyModeEnabled]); - return { - refresh: refreshChart, - currency, - loading, - loadingChart, - toggleStar, - chartRequestParams, defaultAccount, isStarred, accounts: filteredAccounts, counterCurrency, allAccounts, + range, + currency, + dataChart, + loadingChart, + loading, + refresh, + toggleStar, }; } diff --git a/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketList/components/BottomSection/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketList/components/BottomSection/index.tsx index 5e670b036524..fa87897bd17b 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketList/components/BottomSection/index.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketList/components/BottomSection/index.tsx @@ -1,94 +1,96 @@ import React from "react"; -import { - Text, - ScrollContainerHeader, - Icon, - ScrollContainer, - IconsLegacy, -} from "@ledgerhq/native-ui"; +import { Text, ScrollContainerHeader, Icon, ScrollContainer, Icons } from "@ledgerhq/native-ui"; import { useNavigation } from "@react-navigation/native"; import { useTranslation } from "react-i18next"; -import { rangeDataTable } from "@ledgerhq/live-common/market/utils/rangeDataTable"; import { TouchableOpacity } from "react-native"; import SortBadge from "../SortBadge"; import { StyledBadge } from "../SortBadge/SortBadge.styled"; import { ScreenName } from "~/const"; -import { MarketListRequestParams } from "@ledgerhq/live-common/market/types"; +import { MarketListRequestParams, Order } from "@ledgerhq/live-common/market/utils/types"; import TrackScreen from "~/analytics/TrackScreen"; import useBottomSectionViewModel from "./useBottomSectionViewModel"; +import { RANGES } from "~/newArch/features/Market/utils"; +import { LIMIT } from "~/reducers/market"; const SORT_OPTIONS = { - top100: { + top100G: { requestParam: { - ids: [], starred: [], - orderBy: "market_cap", - order: "desc", + order: Order.topGainers, search: "", liveCompatible: false, - sparkline: false, - top100: true, }, - value: "top100", + value: "top100_gainers", + }, + top100L: { + requestParam: { + starred: [], + order: Order.topLosers, + search: "", + liveCompatible: false, + }, + value: "top100_losers", }, market_cap_asc: { requestParam: { - order: "asc", - orderBy: "market_cap", - top100: false, - limit: 20, + order: Order.MarketCapAsc, + limit: LIMIT, }, value: "market_cap_asc", }, market_cap_desc: { requestParam: { - order: "desc", - orderBy: "market_cap", - top100: false, - limit: 20, + order: Order.MarketCapDesc, + limit: LIMIT, }, value: "market_cap_desc", }, }; -const getIcon = (top100?: boolean, order?: string) => - top100 - ? IconsLegacy.GraphGrowMedium - : order === "asc" - ? IconsLegacy.ArrowTopMedium - : IconsLegacy.ArrowBottomMedium; +const getIcon = (order?: Order) => { + switch (order) { + case Order.topGainers: + return ; + case Order.topLosers: + return ; + case Order.MarketCapDesc: + return ; + case Order.MarketCapAsc: + default: + return ; + } +}; -const TIME_RANGES = Object.keys(rangeDataTable) - .filter(key => key !== "1h") - .map(value => ({ - requestParam: { range: value }, - value, - })); +const TIME_RANGES = RANGES.map(value => ({ + requestParam: { range: value }, + value, +})); interface ViewProps { - top100?: boolean; - orderBy?: string; - order?: string; + order?: Order; range?: string; counterCurrency?: string; onFilterChange: (_: MarketListRequestParams) => void; - toggleFilterByStarredAccounts: () => void; - filterByStarredAccount: boolean; + toggleFilterByStarredCurrencies: () => void; + filterByStarredCurrencies: boolean; } function View({ - top100, - orderBy, order, range, counterCurrency, onFilterChange, - toggleFilterByStarredAccounts, - filterByStarredAccount, + toggleFilterByStarredCurrencies, + filterByStarredCurrencies, }: ViewProps) { const navigation = useNavigation(); const { t } = useTranslation(); + const top100G = order === Order.topGainers; + const top100L = order === Order.topLosers; + + const top100 = top100G || top100L; + const timeRanges = TIME_RANGES.map(timeRange => ({ ...timeRange, label: t(`market.range.${timeRange.value}`), @@ -106,33 +108,42 @@ function View({ showsHorizontalScrollIndicator={false} > - - + + { - if (firstMount.current) { - // We don't want to refresh the market data directly on mount, the data is already refreshed with wanted parameters from MarketDataProviderWrapper - firstMount.current = false; - return; - } - if (filterByStarredAccount) { - refresh({ starred: starredMarketCoins }); - } else { - refresh({ starred: [], search: "" }); - } - }, [refresh, filterByStarredAccount, starredMarketCoins]); + const { range, order, counterCurrency } = marketParams; + + const resetMarketPage = useCallback(() => { + dispatch(setMarketCurrentPage(1)); + dispatch(setMarketRequestParams({ page: 1 })); + }, [dispatch]); - const toggleFilterByStarredAccounts = useCallback(() => { - if (!filterByStarredAccount) { + const toggleFilterByStarredCurrencies = useCallback(() => { + if (!filterByStarredCurrencies) { track( "Page Market Favourites", - getAnalyticsProperties(requestParams, { + getAnalyticsProperties(marketParams, { currencies: starredMarketCoins, }), ); } - dispatch(setMarketFilterByStarredAccounts(!filterByStarredAccount)); - }, [dispatch, filterByStarredAccount, requestParams, starredMarketCoins]); + + dispatch(setMarketFilterByStarredCurrencies(!filterByStarredCurrencies)); + resetMarketPage(); + }, [dispatch, filterByStarredCurrencies, marketParams, resetMarketPage, starredMarketCoins]); const onFilterChange = useCallback( (value: MarketListRequestParams) => { track( "Page Market", getAnalyticsProperties({ - ...requestParams, + ...marketParams, ...value, }), ); + dispatch(setMarketRequestParams(value)); - refresh(value); + resetMarketPage(); }, - [dispatch, refresh, requestParams], + [dispatch, marketParams, resetMarketPage], ); return { onFilterChange, - filterByStarredAccount, - toggleFilterByStarredAccounts, + filterByStarredCurrencies, + toggleFilterByStarredCurrencies, range, - orderBy, order, - top100, counterCurrency, }; } diff --git a/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketList/components/ListEmpty/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketList/components/ListEmpty/index.tsx index b8bf7d57a23e..964dd0906081 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketList/components/ListEmpty/index.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketList/components/ListEmpty/index.tsx @@ -1,6 +1,6 @@ import React from "react"; import { useNetInfo } from "@react-native-community/netinfo"; -import { Text, InfiniteLoader } from "@ledgerhq/native-ui"; +import { Text } from "@ledgerhq/native-ui"; import { Trans, useTranslation } from "react-i18next"; import EmptyState from "../EmptyState"; import EmptyStarredCoins from "../EmptyStarredCoins"; @@ -60,9 +60,9 @@ function ListEmpty({ } else if (hasEmptyStarredCoins) { // Empty starred coins return ; + } else { + return null; } - - return ; } export default ListEmpty; diff --git a/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketList/components/ListRow/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketList/components/ListRow/index.tsx index 53886479e523..dd23bca7beac 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketList/components/ListRow/index.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketList/components/ListRow/index.tsx @@ -3,27 +3,25 @@ import { TouchableOpacity } from "react-native"; import { useNavigation } from "@react-navigation/native"; import { ScreenName } from "~/const"; import MarketRowItem from "LLM/features/Market/components/MarketRowItem"; -import { CurrencyData } from "@ledgerhq/live-common/market/types"; +import { CurrencyData } from "@ledgerhq/live-common/market/utils/types"; interface ListRowProps { item: CurrencyData; index: number; counterCurrency?: string; range?: string; - selectCurrency: (id?: string, data?: CurrencyData, range?: string) => void; } -function ListRow({ item, index, counterCurrency, range, selectCurrency }: ListRowProps) { +function ListRow({ item, index, counterCurrency, range }: ListRowProps) { const navigation = useNavigation(); return ( { - selectCurrency(item.id, item, range); navigation.navigate(ScreenName.MarketDetail, { currencyId: item.id, }); }} > - + ); } diff --git a/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketList/components/SearchHeader/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketList/components/SearchHeader/index.tsx index e0a571e4b53b..8bbbef359b54 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketList/components/SearchHeader/index.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketList/components/SearchHeader/index.tsx @@ -2,8 +2,9 @@ import { SearchInput } from "@ledgerhq/native-ui"; import { useDebounce } from "@ledgerhq/live-common/hooks/useDebounce"; import React, { memo, useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { MarketListRequestParams } from "@ledgerhq/live-common/market/types"; +import { MarketListRequestParams } from "@ledgerhq/live-common/market/utils/types"; import { track } from "~/analytics"; +import { LIMIT } from "~/reducers/market"; type Props = { search?: string; @@ -23,7 +24,7 @@ function SearchHeader({ search, refresh }: Props) { search: debouncedSearch ? debouncedSearch.trim() : "", starred: [], liveCompatible: false, - limit: 20, + limit: LIMIT, }); }, [debouncedSearch, refresh]); diff --git a/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketList/components/SortBadge/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketList/components/SortBadge/index.tsx index 9a93b90c7cc3..9cb3937096d7 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketList/components/SortBadge/index.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketList/components/SortBadge/index.tsx @@ -1,9 +1,8 @@ -import React, { memo, useState, useCallback } from "react"; +import React, { memo, useState, useCallback, ReactNode } from "react"; import { TouchableOpacity } from "react-native"; import { Flex, Text, Icon as IconUI } from "@ledgerhq/native-ui"; -import { IconType } from "@ledgerhq/native-ui/components/Icon/type"; import QueuedDrawer from "~/components/QueuedDrawer"; -import { MarketListRequestParams } from "@ledgerhq/live-common/market/types"; +import { MarketListRequestParams } from "@ledgerhq/live-common/market/utils/types"; import { StyledBadge, StyledCheckIconContainer } from "./SortBadge.styled"; type Option = { @@ -16,7 +15,7 @@ type Props = { label: string; valueLabel: string; value: unknown; - Icon?: IconType; + Icon?: ReactNode; options: Option[]; disabled?: boolean; onChange: (_: MarketListRequestParams) => void; @@ -39,11 +38,7 @@ function SortBadge({ label, valueLabel, value, Icon, options, disabled, onChange {valueLabel} - {Icon ? ( - - - - ) : null} + {Icon ? {Icon} : null} diff --git a/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketList/index.tsx b/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketList/index.tsx index a0d376c4d24c..23a1af6c5a10 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketList/index.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketList/index.tsx @@ -1,8 +1,8 @@ -import React, { useCallback, useContext } from "react"; +import React, { MutableRefObject, useCallback, useContext, useEffect } from "react"; import { Flex } from "@ledgerhq/native-ui"; -import { Platform, RefreshControl } from "react-native"; +import { Platform, RefreshControl, ViewToken } from "react-native"; import { TAB_BAR_SAFE_HEIGHT } from "~/components/TabBar/TabBarSafeAreaView"; -import { CurrencyData, MarketListRequestParams } from "@ledgerhq/live-common/market/types"; +import { CurrencyData, MarketListRequestParams } from "@ledgerhq/live-common/market/utils/types"; import { useFocusEffect } from "@react-navigation/native"; import { AnalyticsContext } from "~/analytics/AnalyticsContext"; import CollapsibleHeaderFlatList from "~/components/WalletTab/CollapsibleHeaderFlatList"; @@ -16,6 +16,7 @@ import BottomSection from "./components/BottomSection"; import globalSyncRefreshControl from "~/components/globalSyncRefreshControl"; import usePullToRefresh from "../../hooks/usePullToRefresh"; import useMarketListViewModel from "./useMarketListViewModel"; +import { LIMIT } from "~/reducers/market"; const RefreshableCollapsibleHeaderFlatList = globalSyncRefreshControl( CollapsibleHeaderFlatList, @@ -28,29 +29,44 @@ const keyExtractor = (item: CurrencyData, index: number) => item.id + index; interface ViewProps { marketData?: CurrencyData[]; - filterByStarredAccount: boolean; + filterByStarredCurrencies: boolean; starredMarketCoins: string[]; search?: string; loading: boolean; refresh: (param?: MarketListRequestParams) => void; counterCurrency?: string; range?: string; - selectCurrency: (id?: string, data?: CurrencyData, range?: string) => void; - isLoading: boolean; - onEndReached: () => Promise | undefined; + onEndReached?: () => void; + refetchData: (pageToRefetch: number) => void; + resetMarketPageToInital: (page: number) => void; + refreshRate: number; + marketParams: MarketListRequestParams; + marketCurrentPage: number; + viewabilityConfigCallbackPairs: MutableRefObject< + { + onViewableItemsChanged: ({ viewableItems }: { viewableItems: ViewToken[] }) => void; + viewabilityConfig: { + viewAreaCoveragePercentThreshold: number; + }; + }[] + >; } function View({ marketData, - filterByStarredAccount, + filterByStarredCurrencies, starredMarketCoins, search, loading, refresh, counterCurrency, range, - selectCurrency, - isLoading, onEndReached, + refreshRate, + marketCurrentPage, + refetchData, + viewabilityConfigCallbackPairs, + resetMarketPageToInital, + marketParams, }: ViewProps) { const { colors } = useTheme(); const { handlePullToRefresh, refreshControlVisible } = usePullToRefresh({ loading, refresh }); @@ -61,8 +77,7 @@ function View({ search: "", starred: [], liveCompatible: false, - top100: false, - limit: 20, + limit: LIMIT, }), [refresh], ); @@ -79,6 +94,23 @@ function View({ }, [setScreen, setSource]), ); + /** + * Reset the page to 1 when the component mounts to only refetch first page + * */ + useEffect(() => { + resetMarketPageToInital(marketParams.page ?? 1); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + /** + * Try to Refetch data every REFRESH_RATE time + */ + useEffect(() => { + const intervalId = setInterval(() => refetchData(marketCurrentPage ?? 1), refreshRate); + + return () => clearInterval(intervalId); + }, [marketCurrentPage, refetchData, refreshRate]); + const listProps = { contentContainerStyle: { paddingHorizontal: 16, @@ -86,24 +118,20 @@ function View({ }, data: marketData, renderItem: ({ item, index }: { item: CurrencyData; index: number }) => ( - + ), onEndReached, + maxToRenderPerBatch: 50, onEndReachedThreshold: 0.5, scrollEventThrottle: 50, initialNumToRender: 50, keyExtractor, - ListFooterComponent: , + viewabilityConfigCallbackPairs: viewabilityConfigCallbackPairs.current, + ListFooterComponent: , ListEmptyComponent: ( diff --git a/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketList/useMarketListViewModel.ts b/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketList/useMarketListViewModel.ts index 77c0684fe6a1..025d4d76c9de 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketList/useMarketListViewModel.ts +++ b/apps/ledger-live-mobile/src/newArch/features/Market/screens/MarketList/useMarketListViewModel.ts @@ -1,98 +1,136 @@ -import { useCallback, useState, useEffect } from "react"; -import { useSelector } from "react-redux"; +import { useCallback, useEffect, useRef } from "react"; +import { useDispatch } from "react-redux"; import { BaseComposite, StackNavigatorProps } from "~/components/RootNavigator/types/helpers"; import { MarketNavigatorStackParamList } from "LLM/features/Market/Navigator"; import { ScreenName } from "~/const"; import { useRoute } from "@react-navigation/native"; -import { - marketFilterByStarredAccountsSelector, - starredMarketCoinsSelector, -} from "~/reducers/settings"; -import { useMarketData } from "@ledgerhq/live-common/market/MarketDataProvider"; - +import { useMarketData as useMarketDataHook } from "@ledgerhq/live-common/market/hooks/useMarketDataProvider"; +import { setMarketCurrentPage, setMarketRequestParams } from "~/actions/market"; +import useFeature from "@ledgerhq/live-common/featureFlags/useFeature"; +import { useMarket } from "../../hooks/useMarket"; +import { getCurrentPage, isDataStale } from "../../utils"; +import { ViewToken } from "react-native"; +import { Order } from "@ledgerhq/live-common/market/utils/types"; type NavigationProps = BaseComposite< StackNavigatorProps >; +export const REFETCH_TIME_ONE_MINUTE = 60 * 1000; + +export const BASIC_REFETCH = 3; // nb minutes + +export const viewabilityConfig = { + viewAreaCoveragePercentThreshold: 72, +}; + function useMarketListViewModel() { + const llmRefreshMarketDataFeature = useFeature("llmRefreshMarketData"); const { params } = useRoute(); - const [isLoading, setIsLoading] = useState(true); + + const REFRESH_RATE = + Number(llmRefreshMarketDataFeature?.params?.refreshTime) > 0 + ? REFETCH_TIME_ONE_MINUTE * Number(llmRefreshMarketDataFeature?.params?.refreshTime) + : REFETCH_TIME_ONE_MINUTE * BASIC_REFETCH; + const initialTop100 = params?.top100; - const starredMarketCoins: string[] = useSelector(starredMarketCoinsSelector); - const filterByStarredAccount: boolean = useSelector(marketFilterByStarredAccountsSelector); + + const dispatch = useDispatch(); const { - requestParams, + marketParams, + starredMarketCoins, + filterByStarredCurrencies, + marketCurrentPage, refresh, - counterCurrency, - marketData, - loadNextPage, - loading, - page, - selectCurrency, - } = useMarketData(); + } = useMarket(); - const { limit, search, range, top100 } = requestParams; + const { search, counterCurrency, range } = marketParams; - const marketDataFiltered = filterByStarredAccount - ? marketData?.filter(d => starredMarketCoins.includes(d.id)) ?? undefined - : marketData; + const marketResult = useMarketDataHook({ + ...marketParams, + starred: filterByStarredCurrencies ? starredMarketCoins : [], + }); + + const marketDataFiltered = filterByStarredCurrencies + ? marketResult.data?.filter(d => starredMarketCoins.includes(d.id)) ?? undefined + : marketResult.data; useEffect(() => { if (initialTop100) { refresh({ limit: 100, - ids: [], starred: [], - orderBy: "market_cap", - order: "desc", + order: Order.MarketCapDesc, search: "", liveCompatible: false, - sparkline: false, - top100: true, }); } }, [initialTop100, refresh]); - useEffect(() => { - if (filterByStarredAccount && starredMarketCoins.length > 0) { - refresh({ starred: starredMarketCoins }); + const onEndReached = useCallback(() => { + dispatch(setMarketRequestParams({ page: (marketParams?.page || 1) + 1 })); + }, [dispatch, marketParams?.page]); + + /** + * + * Refresh mechanism ---------------------------------------------- + */ + + const refetchData = useCallback( + (pageToRefetch: number) => { + const elem = marketResult.cachedMetadataMap.get(String(pageToRefetch - 1 ?? 0)); + if (elem && isDataStale(elem.updatedAt, REFRESH_RATE)) { + elem.refetch(); + } + }, + [marketResult.cachedMetadataMap, REFRESH_RATE], + ); + + const checkIfDataIsStaleAndRefetch = useCallback( + (indexPosition: number) => { + const newCurrentPage = getCurrentPage(indexPosition, marketParams.limit || 50); + + if (marketCurrentPage !== newCurrentPage) { + dispatch(setMarketCurrentPage(newCurrentPage)); + } + + refetchData(newCurrentPage); + }, + [marketParams.limit, marketCurrentPage, refetchData, dispatch], + ); + + const onViewableItemsChanged = ({ viewableItems }: { viewableItems: ViewToken[] }) => { + const lastVisible = viewableItems.map((elem: ViewToken) => elem.index).at(-1); + if (lastVisible) { + checkIfDataIsStaleAndRefetch(Number(lastVisible) + 2 ?? 0); } - }, [refresh, starredMarketCoins, filterByStarredAccount]); + }; + const viewabilityConfigCallbackPairs = useRef([{ onViewableItemsChanged, viewabilityConfig }]); - const onEndReached = useCallback(() => { - if ( - !limit || - isNaN(limit) || - !marketData || - page * limit > marketData.length || - loading || - top100 - ) { - setIsLoading(false); - return Promise.resolve(); + const resetMarketPageToInital = (page: number) => { + if (page > 1) { + dispatch(setMarketRequestParams({ page: 1 })); + dispatch(setMarketCurrentPage(1)); } - setIsLoading(true); - const next = loadNextPage(); - return next - ?.then(() => { - // do nothing - }) - .finally(() => setIsLoading(false)); - }, [limit, marketData, page, loading, top100, loadNextPage]); + }; return { marketData: marketDataFiltered, - filterByStarredAccount, + filterByStarredCurrencies, starredMarketCoins, search, - loading, + loading: marketResult.isLoading, refresh, counterCurrency, range, - selectCurrency, - isLoading, onEndReached, + refetchData, + refreshRate: REFRESH_RATE, + marketCurrentPage, + checkIfDataIsStaleAndRefetch, + viewabilityConfigCallbackPairs, + marketParams, + resetMarketPageToInital, }; } diff --git a/apps/ledger-live-mobile/src/newArch/features/Market/utils/index.ts b/apps/ledger-live-mobile/src/newArch/features/Market/utils/index.ts index 757110a0cd30..8c561343370a 100644 --- a/apps/ledger-live-mobile/src/newArch/features/Market/utils/index.ts +++ b/apps/ledger-live-mobile/src/newArch/features/Market/utils/index.ts @@ -1,6 +1,10 @@ -import { MarketListRequestParams } from "@ledgerhq/live-common/market/types"; +import { MarketListRequestParams } from "@ledgerhq/live-common/market/utils/types"; +import { getSortParam } from "@ledgerhq/live-common/market/utils/index"; +import { rangeDataTable } from "@ledgerhq/live-common/market/utils/rangeDataTable"; import { TFunction } from "i18next"; +export const RANGES = Object.keys(rangeDataTable).filter(key => key !== "1h"); + const indexes: [string, number][] = [ ["d", 1], ["K", 1000], @@ -114,9 +118,22 @@ export function getAnalyticsProperties