From f51619ffd6bc4610a64601bd90c427370b055200 Mon Sep 17 00:00:00 2001 From: Kristof Csillag Date: Fri, 16 Feb 2024 02:16:56 +0100 Subject: [PATCH 1/4] Add support for using different tokens on ParaTimes Up to now, we assumed that the native token on each ParaTime is always the same as the native token on the network. Unfortunately this is not always true, so now we have to make it possible to use different tokens on paratimes. --- .changelog/1265.feature.md | 1 + .../RuntimeTransactionDetailPage/index.tsx | 5 +++-- .../pages/RuntimeTransactionsPage/index.tsx | 4 ++-- .../SearchResultsPage/SearchResultsList.tsx | 7 ++++--- .../__tests__/SearchResultsList.test.tsx | 9 ++++---- src/coin-gecko/api.ts | 15 +++++++------ src/config.ts | 21 ++++++++++++++++++- src/oasis-nexus/api.ts | 10 ++++----- 8 files changed, 49 insertions(+), 23 deletions(-) create mode 100644 .changelog/1265.feature.md diff --git a/.changelog/1265.feature.md b/.changelog/1265.feature.md new file mode 100644 index 000000000..35ef457a2 --- /dev/null +++ b/.changelog/1265.feature.md @@ -0,0 +1 @@ +Add support for using different tokens on ParaTimes diff --git a/src/app/pages/RuntimeTransactionDetailPage/index.tsx b/src/app/pages/RuntimeTransactionDetailPage/index.tsx index 29e820fd6..5687cd23c 100644 --- a/src/app/pages/RuntimeTransactionDetailPage/index.tsx +++ b/src/app/pages/RuntimeTransactionDetailPage/index.tsx @@ -26,7 +26,8 @@ import { TransactionLink } from '../../components/Transactions/TransactionLink' import { TransactionEvents } from '../../components/Transactions/TransactionEvents' import { useRequiredScopeParam } from '../../hooks/useScopeParam' import { DashboardLink } from '../ParatimeDashboardPage/DashboardLink' -import { getNameForTicker, getTickerForNetwork, Ticker } from '../../../types/ticker' +import { getNameForTicker, Ticker } from '../../../types/ticker' +import { getTickerForScope } from '../../../config' import { TokenPriceInfo, useTokenPrice } from '../../../coin-gecko/api' import { CurrentFiatValue } from './CurrentFiatValue' import { AddressSwitch, AddressSwitchOption } from '../../components/AddressSwitch' @@ -102,7 +103,7 @@ export const RuntimeTransactionDetailPage: FC = () => { data?.data, ) - const tokenPriceInfo = useTokenPrice(getTickerForNetwork(scope.network)) + const tokenPriceInfo = useTokenPrice(getTickerForScope(scope)) if (!transaction && !isLoading) { throw AppErrors.NotFoundTxHash diff --git a/src/app/pages/RuntimeTransactionsPage/index.tsx b/src/app/pages/RuntimeTransactionsPage/index.tsx index 4419d2f35..6b9ab1666 100644 --- a/src/app/pages/RuntimeTransactionsPage/index.tsx +++ b/src/app/pages/RuntimeTransactionsPage/index.tsx @@ -15,7 +15,7 @@ import { TableLayout, TableLayoutButton } from '../../components/TableLayoutButt import { RuntimeTransactionDetailView } from '../RuntimeTransactionDetailPage' import { useRequiredScopeParam } from '../../hooks/useScopeParam' import { useTokenPrice } from '../../../coin-gecko/api' -import { getTickerForNetwork } from '../../../types/ticker' +import { getTickerForScope } from '../../../config' import { VerticalList } from '../../components/VerticalList' const limit = NUMBER_OF_ITEMS_ON_SEPARATE_PAGE @@ -35,7 +35,7 @@ export const RuntimeTransactionsPage: FC = () => { // we should call useGetConsensusTransactions() } - const tokenPriceInfo = useTokenPrice(getTickerForNetwork(scope.network)) + const tokenPriceInfo = useTokenPrice(getTickerForScope(scope)) useEffect(() => { if (!isMobile) { diff --git a/src/app/pages/SearchResultsPage/SearchResultsList.tsx b/src/app/pages/SearchResultsPage/SearchResultsList.tsx index 4eb4945fd..2da0f092b 100644 --- a/src/app/pages/SearchResultsPage/SearchResultsList.tsx +++ b/src/app/pages/SearchResultsPage/SearchResultsList.tsx @@ -21,6 +21,7 @@ import { AllTokenPrices } from '../../../coin-gecko/api' import { ResultListFrame } from './ResultListFrame' import { TokenDetails } from '../../components/Tokens/TokenDetails' import { ProposalDetailView } from '../ProposalDetailsPage' +import { getTickerForScope } from '../../../config' /** * Component for displaying a list of search results @@ -70,7 +71,7 @@ export const SearchResultsList: FC<{ )} @@ -86,7 +87,7 @@ export const SearchResultsList: FC<{ isLoading={false} isError={false} account={item} - tokenPriceInfo={tokenPrices[item.network]} + tokenPriceInfo={tokenPrices[getTickerForScope(item)]} showLayer={true} /> )} @@ -102,7 +103,7 @@ export const SearchResultsList: FC<{ isLoading={false} isError={false} account={item} - tokenPriceInfo={tokenPrices[item.network]} + tokenPriceInfo={tokenPrices[getTickerForScope(item)]} showLayer={true} /> )} diff --git a/src/app/pages/SearchResultsPage/__tests__/SearchResultsList.test.tsx b/src/app/pages/SearchResultsPage/__tests__/SearchResultsList.test.tsx index e726877c4..cf9ef0771 100644 --- a/src/app/pages/SearchResultsPage/__tests__/SearchResultsList.test.tsx +++ b/src/app/pages/SearchResultsPage/__tests__/SearchResultsList.test.tsx @@ -8,6 +8,7 @@ import { } from '../../../utils/test-fixtures' import { Network } from '../../../../types/network' import { SearchResultsList } from '../SearchResultsList' +import { Ticker } from '../../../../types/ticker' describe('SearchResultsView', () => { beforeEach(() => { @@ -24,13 +25,13 @@ describe('SearchResultsView', () => { { title="test search" networkForTheme={Network.mainnet} tokenPrices={{ - [Network.mainnet]: { + [Ticker.ROSE]: { isLoading: false, isFree: false, price: 1, hasUsedCoinGecko: true, }, - [Network.testnet]: { + [Ticker.TEST]: { isLoading: false, isFree: true, hasUsedCoinGecko: false, diff --git a/src/coin-gecko/api.ts b/src/coin-gecko/api.ts index 7e160aad6..9e6afb431 100644 --- a/src/coin-gecko/api.ts +++ b/src/coin-gecko/api.ts @@ -1,8 +1,8 @@ import axios from 'axios' import type { AxiosResponse, AxiosError } from 'axios' import { useQuery } from '@tanstack/react-query' -import { getTickerForNetwork, NativeTicker, Ticker } from '../types/ticker' -import { Network } from '../types/network' +import { NativeTicker, Ticker } from '../types/ticker' +import { getTickerForScope } from '../config' import { RouteUtils } from '../app/utils/route-utils' import { exhaustedTypeWarning } from '../types/errors' @@ -78,12 +78,15 @@ export const useTokenPrice = (ticker: NativeTicker): TokenPriceInfo => { } } -export type AllTokenPrices = Record +export type AllTokenPrices = Record export const useAllTokenPrices = (): AllTokenPrices => { const results = {} as any - // The list of networks will never change on the run, so we can do this - // eslint-disable-next-line react-hooks/rules-of-hooks - RouteUtils.getEnabledNetworks().forEach(net => (results[net] = useTokenPrice(getTickerForNetwork(net)))) + RouteUtils.getEnabledScopes().forEach(scope => { + const ticker = getTickerForScope(scope) + // The list of networks will never change on the run, so we can do this + // eslint-disable-next-line react-hooks/rules-of-hooks + results[ticker] = useTokenPrice(ticker) + }) return results } diff --git a/src/config.ts b/src/config.ts index fa6ce54c4..fca7cf58e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,5 +1,8 @@ +// We get this from the generated code to avoid circular imports // eslint-disable-next-line no-restricted-imports -import { Layer } from './oasis-nexus/generated/api' // We get this from the generated code to avoid circular imports +import { Layer } from './oasis-nexus/generated/api' +import { getTickerForNetwork, NativeTicker } from './types/ticker' +import { SearchScope } from './types/searchScope' export const consensusDecimals = 9 @@ -8,6 +11,13 @@ type LayerNetwork = { address: string | undefined blockGasLimit: number | undefined runtimeId: string | undefined + + /** + * What do we call the native ticker on this layer? + * + * (If not given, the network's default token will be used.) + */ + ticker?: NativeTicker } type LayerConfig = { @@ -164,3 +174,12 @@ const stableDeploys = [...deploys.production, deploys.staging] export const isStableDeploy = stableDeploys.some(url => window.location.origin === url) export const getAppTitle = () => process.env.REACT_APP_META_TITLE + +export const getTickerForScope = ({ network, layer }: SearchScope): NativeTicker => { + const networkDefault = getTickerForNetwork(network) + + if (layer !== Layer.consensus) { + return paraTimesConfig[layer][network].ticker ?? networkDefault + } + return networkDefault +} diff --git a/src/oasis-nexus/api.ts b/src/oasis-nexus/api.ts index 500434eba..075f7a025 100644 --- a/src/oasis-nexus/api.ts +++ b/src/oasis-nexus/api.ts @@ -1,7 +1,7 @@ /** @file Wrappers around generated API */ import axios, { AxiosResponse } from 'axios' -import { consensusDecimals, paraTimesConfig } from '../config' +import { consensusDecimals, getTickerForScope, paraTimesConfig } from '../config' import * as generated from './generated/api' import { QueryKey, UseQueryOptions, UseQueryResult } from '@tanstack/react-query' import { @@ -167,7 +167,7 @@ export const useGetRuntimeTransactions: typeof generated.useGetRuntimeTransactio params?, options?, ) => { - const ticker = getTickerForNetwork(network) + const ticker = getTickerForScope({ network, layer: runtime }) return generated.useGetRuntimeTransactions(network, runtime, params, { ...options, request: { @@ -239,7 +239,7 @@ export const useGetRuntimeTransactionsTxHash: typeof generated.useGetRuntimeTran ) => { // Sometimes we will call this with an undefined txHash, so we must be careful here. const actualHash = txHash?.startsWith('0x') ? txHash.substring(2) : txHash - const ticker = getTickerForNetwork(network) + const ticker = getTickerForScope({ network, layer: runtime }) return generated.useGetRuntimeTransactionsTxHash(network, runtime, actualHash, { ...options, request: { @@ -320,7 +320,7 @@ export const useGetRuntimeAccountsAddress: typeof generated.useGetRuntimeAccount const oasisAddress = useTransformToOasisAddress(address) - const ticker = getTickerForNetwork(network) + const ticker = getTickerForScope({ network, layer: runtime }) const query = generated.useGetRuntimeAccountsAddress(network, runtime, oasisAddress!, { ...options, query: { @@ -755,7 +755,7 @@ export const useGetRuntimeEvents: typeof generated.useGetRuntimeEvents = ( event.body.amount.Amount, paraTimesConfig[runtime].decimals, ), - Denomination: getTickerForNetwork(network), + Denomination: getTickerForScope({ network, layer: runtime }), } : event.body.amount, }, From 32466aa06d2feb0eca1e9ea250ca891c99df622c Mon Sep 17 00:00:00 2001 From: Kristof Csillag Date: Fri, 16 Feb 2024 14:15:46 +0100 Subject: [PATCH 2/4] Update testnet faucet to support layer/token configuration --- .../ParatimeDashboardPage/ParaTimeSnapshot.tsx | 11 ++++++++--- .../ParatimeDashboardPage/TestnetFaucet.tsx | 15 ++++++++++----- src/app/utils/externalLinks.ts | 12 ------------ src/app/utils/faucet-links.ts | 18 ++++++++++++++++++ 4 files changed, 36 insertions(+), 20 deletions(-) create mode 100644 src/app/utils/faucet-links.ts diff --git a/src/app/pages/ParatimeDashboardPage/ParaTimeSnapshot.tsx b/src/app/pages/ParatimeDashboardPage/ParaTimeSnapshot.tsx index 683b7fec5..d81b1fd01 100644 --- a/src/app/pages/ParatimeDashboardPage/ParaTimeSnapshot.tsx +++ b/src/app/pages/ParatimeDashboardPage/ParaTimeSnapshot.tsx @@ -7,17 +7,20 @@ import { ActiveAccounts } from './ActiveAccounts' import { ChartDuration } from '../../utils/chart-utils' import { useTranslation } from 'react-i18next' import { useConstant } from '../../hooks/useConstant' -import { Network } from '../../../types/network' +import { Ticker } from '../../../types/ticker' +import { getTickerForScope } from '../../../config' import { getLayerLabels } from '../../utils/content' import { TestnetFaucet } from './TestnetFaucet' import { SearchScope } from '../../../types/searchScope' import { Snapshot, StyledGrid } from 'app/components/Snapshots/Snapshot' +import { getFaucetLink } from '../../utils/faucet-links' export const ParaTimeSnapshot: FC<{ scope: SearchScope }> = ({ scope }) => { const { t } = useTranslation() const defaultChartDurationValue = useConstant(() => ChartDuration.TODAY) const [chartDuration, setChartDuration] = useState(defaultChartDurationValue) const paratime = getLayerLabels(t)[scope.layer] + const ticker = getTickerForScope(scope) const handleDurationSelectedChange = (duration: ChartDuration | null) => { if (!duration) { return @@ -26,6 +29,8 @@ export const ParaTimeSnapshot: FC<{ scope: SearchScope }> = ({ scope }) => { setChartDuration(duration) } + const faucetLink = getFaucetLink(scope.network, scope.layer, ticker) + return ( <> = ({ scope }) => { - {scope.network === Network.mainnet && } - {scope.network === Network.testnet && } + {ticker === Ticker.ROSE && } + {faucetLink && } diff --git a/src/app/pages/ParatimeDashboardPage/TestnetFaucet.tsx b/src/app/pages/ParatimeDashboardPage/TestnetFaucet.tsx index 515ed53d5..c1f6341bc 100644 --- a/src/app/pages/ParatimeDashboardPage/TestnetFaucet.tsx +++ b/src/app/pages/ParatimeDashboardPage/TestnetFaucet.tsx @@ -1,22 +1,27 @@ import { FC } from 'react' import { useTranslation } from 'react-i18next' import { SnapshotCardExternalLink } from '../../components/Snapshots/SnapshotCardExternalLink' -import { faucet } from '../../utils/externalLinks' +import { getFaucetLink } from '../../utils/faucet-links' import { Layer } from '../../../oasis-nexus/api' +import { NativeTicker } from '../../../types/ticker' +import { Network } from '../../../types/network' type TestnetFaucetProps = { + network: Network layer: Layer + ticker: NativeTicker } -export const TestnetFaucet: FC = ({ layer }) => { +export const TestnetFaucet: FC = ({ network, layer, ticker }) => { const { t } = useTranslation() + const link = getFaucetLink(network, layer, ticker) - return ( + return link ? ( - ) + ) : null } diff --git a/src/app/utils/externalLinks.ts b/src/app/utils/externalLinks.ts index 12c21e695..42a930f1b 100644 --- a/src/app/utils/externalLinks.ts +++ b/src/app/utils/externalLinks.ts @@ -1,5 +1,3 @@ -import { Layer } from '../../oasis-nexus/api' - export const socialMedia = { telegram: process.env.REACT_APP_SOCIAL_TELEGRAM, twitter: process.env.REACT_APP_SOCIAL_TWITTER, @@ -48,16 +46,6 @@ export const github = { releaseTag: `${githubLink}releases/tag/`, } -const faucetUrl = 'https://faucet.testnet.oasis.dev/' -const faucetParaTimeBaseUrl = `${faucetUrl}?paratime=` -export const faucet = { - [Layer.consensus]: faucetUrl, - [Layer.emerald]: `${faucetParaTimeBaseUrl}emerald`, - [Layer.sapphire]: `${faucetParaTimeBaseUrl}sapphire`, - [Layer.pontusx]: 'mailto:contact@delta-dao.com?subject=tokens', - [Layer.cipher]: `${faucetParaTimeBaseUrl}cipher`, -} - export const api = { spec: `${process.env.REACT_APP_API}spec/v1.html`, } diff --git a/src/app/utils/faucet-links.ts b/src/app/utils/faucet-links.ts new file mode 100644 index 000000000..a361286c9 --- /dev/null +++ b/src/app/utils/faucet-links.ts @@ -0,0 +1,18 @@ +import { NativeTicker, Ticker } from 'types/ticker' +import { Network } from '../../types/network' +import { Layer } from '../../oasis-nexus/api' + +const testnetFaucetUrl = 'https://faucet.testnet.oasis.dev/' +const faucetParaTimeBaseUrl = `${testnetFaucetUrl}?paratime=` +const faucetLinks: Partial>>>>> = { + [Network.testnet]: { + [Layer.consensus]: { [Ticker.TEST]: testnetFaucetUrl }, + [Layer.emerald]: { [Ticker.TEST]: `${faucetParaTimeBaseUrl}emerald` }, + [Layer.sapphire]: { [Ticker.TEST]: `${faucetParaTimeBaseUrl}sapphire` }, + [Layer.pontusx]: { [Ticker.TEST]: 'mailto:contact@delta-dao.com?subject=tokens' }, + [Layer.cipher]: { [Ticker.TEST]: `${faucetParaTimeBaseUrl}cipher` }, + }, +} + +export const getFaucetLink = (network: Network, layer: Layer, ticker: NativeTicker) => + faucetLinks[network]?.[layer]?.[ticker] From 7daf5fac3d7f993bac6cfba592e406fa5f32111e Mon Sep 17 00:00:00 2001 From: Kristof Csillag Date: Tue, 20 Feb 2024 20:07:38 +0100 Subject: [PATCH 3/4] Major overhaul of native token / ticker handling - Rename a NativeTicker to Ticker - Add EUROe token (and logo) - Add metadata about known native tokens: - Ticker (symbol) - Is this a free token? - CoinGecko id - Generalize RosePriceCard to TokenPriceCard - Enhance layer token configuration - Describe the token, not the ticker - Support multiple tokens per layer - Define the tokens for the Pontus-X layer - CoinGecko: support pulling all wanted prices - Nexus API: - When loading data, observe the layer token configuration - Added RuntimeBalanceDisplay: a component for displaying balances in multiple tokens. - Added FiatMoneyAmount: a component for displaying fiat money value for balances in multiple tokens, summarizing value from all tokens. - ParaTimeSnapshot: display price/faucet for first token --- public/euroe.png | Bin 0 -> 2103 bytes src/app/components/Account/index.tsx | 85 +++++++----------- .../components/Balance/FiatMoneyAmount.tsx | 45 ++++++++++ .../Balance/RuntimeBalanceDisplay.tsx | 25 ++++++ src/app/components/Balance/hooks.ts | 76 ++++++++++++++++ .../{SmallLogo.tsx => SmallOasisLogo.tsx} | 2 +- src/app/components/logo/SmallTokenLogo.tsx | 15 ++++ .../AccountDetailsPage/AccountDetailsCard.tsx | 8 +- .../AccountDetailsPage/AccountDetailsView.tsx | 8 +- src/app/pages/AccountDetailsPage/index.tsx | 7 +- .../ParaTimeSnapshot.tsx | 16 ++-- .../ParatimeDashboardPage/TestnetFaucet.tsx | 4 +- .../{RosePriceCard.tsx => TokenPriceCard.tsx} | 23 ++--- .../CurrentFiatValue.tsx | 4 +- .../RuntimeTransactionDetailPage/index.tsx | 12 +-- .../pages/RuntimeTransactionsPage/index.tsx | 9 +- .../SearchResultsPage/SearchResultsList.tsx | 7 +- .../__tests__/SearchResultsList.test.tsx | 12 +++ .../TokenDashboardPage/TokenDetailsCard.tsx | 11 +-- src/app/utils/faucet-links.ts | 6 +- src/app/utils/test-fixtures.ts | 3 - src/coin-gecko/api.ts | 83 ++++++++--------- src/config.ts | 19 ++-- src/locales/en/translation.json | 7 +- src/oasis-nexus/api.ts | 31 +++---- src/types/ticker.ts | 35 ++++++-- 26 files changed, 356 insertions(+), 197 deletions(-) create mode 100644 public/euroe.png create mode 100644 src/app/components/Balance/FiatMoneyAmount.tsx create mode 100644 src/app/components/Balance/RuntimeBalanceDisplay.tsx create mode 100644 src/app/components/Balance/hooks.ts rename src/app/components/logo/{SmallLogo.tsx => SmallOasisLogo.tsx} (64%) create mode 100644 src/app/components/logo/SmallTokenLogo.tsx rename src/app/pages/ParatimeDashboardPage/{RosePriceCard.tsx => TokenPriceCard.tsx} (68%) diff --git a/public/euroe.png b/public/euroe.png new file mode 100644 index 0000000000000000000000000000000000000000..b478a0d38778821ecfc61e810d9c2d5e3e375465 GIT binary patch literal 2103 zcmV-72*~$|P)n^ZO?joq+u19ww}CXjZA24fU1U=yfYheWlOT`VrG_~@LbRYux|O(Ln?LKvO8 zHOU%#T|*roE^Mdyj=y3j{G~|o>*synbN)W(dCob{dBh?iaBtX2J2V0TU<;znV55xN z5lt9%Ky7yT7$RXX1a=uTAnYurK48Fsk%6wx^kT@Vaz1Rs7k0`?*Z6>KGVTY0Kr7IQ z@PbXe)+W7t^8b^N!WaSKh*Q9EgSW<&9{T5-$7~^4DoQ|WsKWtV(1XBkpiQu{oa6%6 z4c-yF1{^UFi@(`9UR45GLk|cXKkx`>pWrj4K65UZ8A3VZ`4{ke;6;z4f&Pw^MDn9~Nyy?Duy1-h9-pR9`VIn>v!zLjOq53jH%TSFa@cr?ETaVv%Ts#* zGX;QePQErlz|FK10QTDQ(Y;_~yhQt2LmeAu5CH53-C1oR3s$N?YgMK~wI}!43*ZLB zYY}_qC`}cFuAZ}W_nyaDcv31|d5JS;`|?=20jTT_Emfy`NgXwO+r_nt4p4XhRL zdx18v#&Xp(w{LA@`_{HaS3ZXuWB;Z5Z|4iYQ8WTu9D}V$-ol@-G*Cz&G>ri-sLeqH zs;xw|V6f2vnyW2HDJUgum(lDM)>N$jk0z2_zVd!mv%%41vN-WGQQum*!SIb@sy=4M znudBBR4@;(i(L|EbWU}CBFuSQT=7r))UXr*rn&880qg7J=mK;Td!nq1^t&?vu z{r?ItXv~VR0yASxeI506bySbUa=3s<>Q<*lK?Ho`EA2eC_iNQHiM@K5-yVKFpOV-# z>HtW1F|Hz-iVB1&9=E;~SfDNk6|Fa{jV z`Y_9TKYgj8_(;%F5@)@@IbZ}ROMN?25<6F+bXc4&t|Ugb1Fg>sx&Xw>MZoFa^Ze|k zSMYi&lVD21DQfFhao~az>*~A;)>Te{JdT6os3Lzw*Ivb`SXbv&FMx>?z?0?Z?K3mR z)9H%!Qe;BmLgWL9>%eiC*cww0&vARr#?><13nGTIm| z6j`r4oUYNp3<9!vMaIB#vVwdAq0Bag4S>CL@lS6|WN%Xz>v*)Y-wlO-0s1j>w9ib& zXd=mb?|)F`lGsOi&D)ZtfM3RV2!3a9$8v9y%B=hLF7R+H8kx}~HLIDAf1K|2taP4Lrb%*Du`jPSvdUjKc=qNs z>z-}QtXsGrz89yd#s3aw4Z!xL-Xc2}gpDH(NIZ9`=UD!Yk^sYlv6QdH-vep^+N_c% zB1%Oz-~kQ+PsgH>7pVyCNUVH*~=+hT*|j z!nfWZ1=fLWwx!-TY&MQ~)!=*aXk@{1UtVn-!-KJeuf_cfnezD-lU6K65)AtoctP;= zLce@L1VF|enp)hGpew?*fEt#HOJwtbcY*zgpT@f)3m%jxn1Cz^siqdU7w86V2kxxC zOIS*oLsQ>1c>Q8DaO4-L{m!>Tr?F68AEm^o96e|5*8hlat{Oc6cmvbZrmfL5$6yRpn$MJHhn< zM}gO1JQj~0FF6Y<>ksAsVuvh66L2@M3$!EKjnz1{56Q4eiEO4- zTes{P`!_%|j*711@0Bt611p;8rLQNR;Ky6n2F+>uxA(Q?i=*O%NI0qVV h(u@prFV6A#{{iCl+R9%=O@jac002ovPDHLkV1n(c?NI;# literal 0 HcmV?d00001 diff --git a/src/app/components/Account/index.tsx b/src/app/components/Account/index.tsx index 581834c96..bb87e8eab 100644 --- a/src/app/components/Account/index.tsx +++ b/src/app/components/Account/index.tsx @@ -1,12 +1,9 @@ import { FC } from 'react' import { useTranslation } from 'react-i18next' import { Link as RouterLink } from 'react-router-dom' -import Box from '@mui/material/Box' import { useScreenSize } from '../../hooks/useScreensize' -import { styled } from '@mui/material/styles' import { StyledDescriptionList, StyledListTitleWithAvatar } from '../../components/StyledDescriptionList' import { CopyToClipboard } from '../../components/CopyToClipboard' -import { CoinGeckoReferral } from '../../components/CoinGeckoReferral' import { TextSkeleton } from '../../components/Skeleton' import { EvmToken, type RuntimeAccount } from '../../../oasis-nexus/api' import { TokenPills } from './TokenPills' @@ -15,34 +12,29 @@ import { RouteUtils } from '../../utils/route-utils' import { accountTransactionsContainerId } from '../../pages/AccountDetailsPage/AccountTransactionsCard' import Link from '@mui/material/Link' import { DashboardLink } from '../../pages/ParatimeDashboardPage/DashboardLink' -import { getNameForTicker, Ticker } from '../../../types/ticker' -import { TokenPriceInfo } from '../../../coin-gecko/api' +import { getNameForTicker } from '../../../types/ticker' +import { AllTokenPrices } from '../../../coin-gecko/api' import { ContractCreatorInfo } from './ContractCreatorInfo' import { ContractVerificationIcon } from '../ContractVerificationIcon' import { TokenLink } from '../Tokens/TokenLink' -import BigNumber from 'bignumber.js' import { getPreciseNumberFormat } from '../../../locales/getPreciseNumberFormat' import { AccountAvatar } from '../AccountAvatar' - -export const FiatMoneyAmountBox = styled(Box)(() => ({ - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - flex: 1, -})) +import { RuntimeBalanceDisplay } from '../Balance/RuntimeBalanceDisplay' +import { calculateFiatValue } from '../Balance/hooks' +import { FiatMoneyAmount } from '../Balance/FiatMoneyAmount' +import { getTokensForScope } from '../../../config' type AccountProps = { account?: RuntimeAccount token?: EvmToken isLoading: boolean - tokenPriceInfo: TokenPriceInfo + tokenPrices: AllTokenPrices showLayer?: boolean } -export const Account: FC = ({ account, token, isLoading, tokenPriceInfo, showLayer }) => { +export const Account: FC = ({ account, token, isLoading, tokenPrices, showLayer }) => { const { t } = useTranslation() const { isMobile } = useScreenSize() - const balance = account?.balances[0]?.balance const address = account ? account.address_eth ?? account.address : undefined const transactionsLabel = account ? account.stats.num_txns.toLocaleString() : '' @@ -53,15 +45,10 @@ export const Account: FC = ({ account, token, isLoading, tokenPric )}#${accountTransactionsContainerId}` : undefined - const nativeToken = account?.ticker || Ticker.ROSE - const nativeTickerName = getNameForTicker(t, nativeToken) - const { - isLoading: isPriceLoading, - price: tokenFiatValue, - isFree: isTokenFree, - hasUsedCoinGecko, - } = tokenPriceInfo + const nativeTokens = getTokensForScope(account || { network: 'mainnet', layer: 'sapphire' }) + const nativeTickerNames = nativeTokens.map(token => getNameForTicker(t, token.ticker)) const contract = account?.evm_contract + const fiatValueInfo = calculateFiatValue(account?.balances, tokenPrices) return ( <> @@ -120,9 +107,7 @@ export const Account: FC = ({ account, token, isLoading, tokenPric
{t('common.balance')}
- {balance === undefined - ? t('common.missing') - : t('common.valueInToken', { ...getPreciseNumberFormat(balance), ticker: nativeTickerName })} +
{t('common.tokens')}
@@ -130,21 +115,11 @@ export const Account: FC = ({ account, token, isLoading, tokenPric - {!isPriceLoading && !isTokenFree && tokenFiatValue !== undefined && balance && ( + {!fiatValueInfo.loading && fiatValueInfo.hasValue && ( <>
{t('common.fiatValue')}
- - {t('common.fiatValueInUSD', { - value: new BigNumber(balance).multipliedBy(tokenFiatValue).toFixed(), - formatParams: { - value: { - currency: 'USD', - } satisfies Intl.NumberFormatOptions, - }, - })} - {hasUsedCoinGecko && } - +
)} @@ -160,21 +135,25 @@ export const Account: FC = ({ account, token, isLoading, tokenPric )} -
{t('account.totalReceived')}
-
- {t('common.valueInToken', { - ...getPreciseNumberFormat(account.stats.total_received), - ticker: nativeTickerName, - })} -
+ {nativeTokens.length === 1 && ( + <> +
{t('account.totalReceived')}
+
+ {t('common.valueInToken', { + ...getPreciseNumberFormat(account.stats.total_received), + ticker: nativeTickerNames[0], + })} +
-
{t('account.totalSent')}
-
- {t('common.valueInToken', { - ...getPreciseNumberFormat(account.stats.total_sent), - ticker: nativeTickerName, - })} -
+
{t('account.totalSent')}
+
+ {t('common.valueInToken', { + ...getPreciseNumberFormat(account.stats.total_sent), + ticker: nativeTickerNames[0], + })} +
+ + )} )} diff --git a/src/app/components/Balance/FiatMoneyAmount.tsx b/src/app/components/Balance/FiatMoneyAmount.tsx new file mode 100644 index 000000000..88a4cec51 --- /dev/null +++ b/src/app/components/Balance/FiatMoneyAmount.tsx @@ -0,0 +1,45 @@ +import { styled } from '@mui/material/styles' +import WarningIcon from '@mui/icons-material/WarningAmber' +import Box from '@mui/material/Box' +import { useTranslation } from 'react-i18next' +import { FC } from 'react' +import { CoinGeckoReferral } from '../CoinGeckoReferral' +import { FiatValueInfo } from './hooks' +import Tooltip from '@mui/material/Tooltip' +import Skeleton from '@mui/material/Skeleton' + +export const FiatMoneyAmountBox = styled(Box)(() => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: 4, + flex: 1, +})) + +export const FiatMoneyAmount: FC = ({ value, hasUsedCoinGecko, unknownTickers, loading }) => { + const { t } = useTranslation() + return ( + + + {t('common.fiatValueInUSD', { + value, + formatParams: { + value: { + currency: 'USD', // TODO: why are we fixated on USD? + } satisfies Intl.NumberFormatOptions, + }, + })} + {!!unknownTickers.length && ( + + + + )} + {loading && } + + {hasUsedCoinGecko && } + + ) +} diff --git a/src/app/components/Balance/RuntimeBalanceDisplay.tsx b/src/app/components/Balance/RuntimeBalanceDisplay.tsx new file mode 100644 index 000000000..0b3f70599 --- /dev/null +++ b/src/app/components/Balance/RuntimeBalanceDisplay.tsx @@ -0,0 +1,25 @@ +import { FC } from 'react' +import { RuntimeSdkBalance } from '../../../oasis-nexus/api' +import { useTranslation } from 'react-i18next' +import { getPreciseNumberFormat } from '../../../locales/getPreciseNumberFormat' + +export const RuntimeBalanceDisplay: FC<{ balances: RuntimeSdkBalance[] | undefined }> = ({ + balances = [], +}) => { + const { t } = useTranslation() + if (balances.length === 0 || balances[0].balance === undefined) { + return t('common.missing') + } + return ( + <> + {balances.map(balance => ( + + {t('common.valueInToken', { + ...getPreciseNumberFormat(balance.balance), + ticker: balance.token_symbol, + })} + + ))} + + ) +} diff --git a/src/app/components/Balance/hooks.ts b/src/app/components/Balance/hooks.ts new file mode 100644 index 000000000..1fa7ca2e8 --- /dev/null +++ b/src/app/components/Balance/hooks.ts @@ -0,0 +1,76 @@ +import { RuntimeSdkBalance } from '../../../oasis-nexus/api' +import { AllTokenPrices } from '../../../coin-gecko/api' +import BigNumber from 'bignumber.js' +import { Ticker } from '../../../types/ticker' + +export const hasRuntimeBalance = (balances: RuntimeSdkBalance[] = []) => + balances.some(balance => balance.token_decimals) + +export type FiatValueInfo = { + /** + * Do we have any known real value? + */ + hasValue: boolean + + /** + * The fiat value, to the best of our information + */ + value?: string + + /** + * Have we used CoinGecko for calculating this value? + */ + hasUsedCoinGecko: boolean + + /** + * Is the value of one of the tokens still being loaded? + */ + loading: boolean + + /** + * Any tokens for which we don't know the value? + */ + unknownTickers: Ticker[] +} + +export const calculateFiatValue = ( + balances: RuntimeSdkBalance[] = [], + tokenPrices: AllTokenPrices, +): FiatValueInfo => { + let hasValue = false + let value = new BigNumber(0) + let hasUsedCoinGecko = false + let loading = false + const unknown: Ticker[] = [] + + balances.forEach(balance => { + const priceInfo = tokenPrices[balance.token_symbol as Ticker] + if (priceInfo) { + if (priceInfo.isLoading) { + loading = true + } else { + hasUsedCoinGecko = hasUsedCoinGecko || priceInfo.hasUsedCoinGecko + if (!priceInfo.isFree) { + const tokenFiatValue = priceInfo.price + hasValue = true + if (tokenFiatValue === undefined) { + unknown.push(balance.token_symbol as Ticker) + } else { + value = value.plus(new BigNumber(balance.balance).multipliedBy(tokenFiatValue)) + } + } + } + } else { + unknown.push(balance.token_symbol as Ticker) + hasValue = true + } + }) + + return { + hasValue, + hasUsedCoinGecko, + loading, + unknownTickers: unknown, + value: value.toFixed(), + } +} diff --git a/src/app/components/logo/SmallLogo.tsx b/src/app/components/logo/SmallOasisLogo.tsx similarity index 64% rename from src/app/components/logo/SmallLogo.tsx rename to src/app/components/logo/SmallOasisLogo.tsx index 22fe2915d..38cfb7e94 100644 --- a/src/app/components/logo/SmallLogo.tsx +++ b/src/app/components/logo/SmallOasisLogo.tsx @@ -1,6 +1,6 @@ import { FC } from 'react' import logo from '../../../../public/logo512.png' -export const SmallLogo: FC<{ size?: number }> = ({ size = 25 }) => ( +export const SmallOasisLogo: FC<{ size?: number }> = ({ size = 25 }) => ( ) diff --git a/src/app/components/logo/SmallTokenLogo.tsx b/src/app/components/logo/SmallTokenLogo.tsx new file mode 100644 index 000000000..43a1add08 --- /dev/null +++ b/src/app/components/logo/SmallTokenLogo.tsx @@ -0,0 +1,15 @@ +import { FC } from 'react' +import { Ticker } from '../../../types/ticker' +import { SmallOasisLogo } from './SmallOasisLogo' +import euroELogo from '../../../../public/euroe.png' + +export const SmallTokenLogo: FC<{ ticker: Ticker }> = ({ ticker }) => { + switch (ticker) { + case 'ROSE': + return + case 'EUROe': + return {ticker} + default: + return null + } +} diff --git a/src/app/pages/AccountDetailsPage/AccountDetailsCard.tsx b/src/app/pages/AccountDetailsPage/AccountDetailsCard.tsx index a5611f4b4..23bfe2975 100644 --- a/src/app/pages/AccountDetailsPage/AccountDetailsCard.tsx +++ b/src/app/pages/AccountDetailsPage/AccountDetailsCard.tsx @@ -3,7 +3,7 @@ import { SubPageCard } from '../../components/SubPageCard' import { useTranslation } from 'react-i18next' import { EvmToken, RuntimeAccount } from '../../../oasis-nexus/api' import { AccountDetailsView } from './AccountDetailsView' -import { TokenPriceInfo } from '../../../coin-gecko/api' +import { AllTokenPrices } from '../../../coin-gecko/api' type AccountDetailsProps = { isLoading: boolean @@ -11,7 +11,7 @@ type AccountDetailsProps = { isContract: boolean account: RuntimeAccount | undefined token: EvmToken | undefined - tokenPriceInfo: TokenPriceInfo + tokenPrices: AllTokenPrices } export const AccountDetailsCard: FC = ({ @@ -20,7 +20,7 @@ export const AccountDetailsCard: FC = ({ isContract, account, token, - tokenPriceInfo, + tokenPrices, }) => { const { t } = useTranslation() return ( @@ -34,7 +34,7 @@ export const AccountDetailsCard: FC = ({ isError={isError} account={account} token={token} - tokenPriceInfo={tokenPriceInfo} + tokenPrices={tokenPrices} /> ) diff --git a/src/app/pages/AccountDetailsPage/AccountDetailsView.tsx b/src/app/pages/AccountDetailsPage/AccountDetailsView.tsx index 4cfabbc13..0c9f70225 100644 --- a/src/app/pages/AccountDetailsPage/AccountDetailsView.tsx +++ b/src/app/pages/AccountDetailsPage/AccountDetailsView.tsx @@ -1,7 +1,7 @@ import { FC } from 'react' import { EvmToken, RuntimeAccount } from '../../../oasis-nexus/api' import { useTranslation } from 'react-i18next' -import { TokenPriceInfo } from '../../../coin-gecko/api' +import { AllTokenPrices } from '../../../coin-gecko/api' import { CardEmptyState } from '../../components/CardEmptyState' import { Account } from '../../components/Account' @@ -10,9 +10,9 @@ export const AccountDetailsView: FC<{ isError: boolean account: RuntimeAccount | undefined token?: EvmToken - tokenPriceInfo: TokenPriceInfo + tokenPrices: AllTokenPrices showLayer?: boolean -}> = ({ isLoading, isError, account, token, tokenPriceInfo, showLayer }) => { +}> = ({ isLoading, isError, account, token, tokenPrices, showLayer }) => { const { t } = useTranslation() return isError ? ( @@ -21,7 +21,7 @@ export const AccountDetailsView: FC<{ account={account} token={token} isLoading={isLoading} - tokenPriceInfo={tokenPriceInfo} + tokenPrices={tokenPrices} showLayer={showLayer} /> ) diff --git a/src/app/pages/AccountDetailsPage/index.tsx b/src/app/pages/AccountDetailsPage/index.tsx index 098654a92..62b1ba7e7 100644 --- a/src/app/pages/AccountDetailsPage/index.tsx +++ b/src/app/pages/AccountDetailsPage/index.tsx @@ -3,8 +3,7 @@ import { useTranslation } from 'react-i18next' import { useHref, useLoaderData, useOutletContext } from 'react-router-dom' import { PageLayout } from '../../components/PageLayout' import { RouterTabs } from '../../components/RouterTabs' -import { useTokenPrice } from '../../../coin-gecko/api' -import { Ticker } from '../../../types/ticker' +import { useAllTokenPrices } from '../../../coin-gecko/api' import { EvmTokenType, RuntimeAccount } from '../../../oasis-nexus/api' import { accountTokenContainerId } from './AccountTokensCard' @@ -37,7 +36,7 @@ export const AccountDetailsPage: FC = () => { const isContract = !!account?.evm_contract const { token, isLoading: isTokenLoading } = useTokenInfo(scope, address, isContract) - const tokenPriceInfo = useTokenPrice(account?.ticker || Ticker.ROSE) + const tokenPrices = useAllTokenPrices() const { isLoading: areEventsLoading, isError: isEventsError, events } = useAccountEvents(scope, address) @@ -61,7 +60,7 @@ export const AccountDetailsPage: FC = () => { isContract={isContract} account={account} token={token} - tokenPriceInfo={tokenPriceInfo} + tokenPrices={tokenPrices} /> = ({ scope }) => { const defaultChartDurationValue = useConstant(() => ChartDuration.TODAY) const [chartDuration, setChartDuration] = useState(defaultChartDurationValue) const paratime = getLayerLabels(t)[scope.layer] - const ticker = getTickerForScope(scope) + const tokens = getTokensForScope(scope) + const mainToken = tokens[0] + const mainTicker = mainToken.ticker + const faucetLink = getFaucetLink(scope.network, scope.layer, mainTicker) const handleDurationSelectedChange = (duration: ChartDuration | null) => { if (!duration) { return @@ -29,8 +31,6 @@ export const ParaTimeSnapshot: FC<{ scope: SearchScope }> = ({ scope }) => { setChartDuration(duration) } - const faucetLink = getFaucetLink(scope.network, scope.layer, ticker) - return ( <> = ({ scope }) => { - {ticker === Ticker.ROSE && } - {faucetLink && } + {!mainToken.free && } + {faucetLink && } diff --git a/src/app/pages/ParatimeDashboardPage/TestnetFaucet.tsx b/src/app/pages/ParatimeDashboardPage/TestnetFaucet.tsx index c1f6341bc..b9f2a4832 100644 --- a/src/app/pages/ParatimeDashboardPage/TestnetFaucet.tsx +++ b/src/app/pages/ParatimeDashboardPage/TestnetFaucet.tsx @@ -3,13 +3,13 @@ import { useTranslation } from 'react-i18next' import { SnapshotCardExternalLink } from '../../components/Snapshots/SnapshotCardExternalLink' import { getFaucetLink } from '../../utils/faucet-links' import { Layer } from '../../../oasis-nexus/api' -import { NativeTicker } from '../../../types/ticker' +import { Ticker } from '../../../types/ticker' import { Network } from '../../../types/network' type TestnetFaucetProps = { network: Network layer: Layer - ticker: NativeTicker + ticker: Ticker } export const TestnetFaucet: FC = ({ network, layer, ticker }) => { diff --git a/src/app/pages/ParatimeDashboardPage/RosePriceCard.tsx b/src/app/pages/ParatimeDashboardPage/TokenPriceCard.tsx similarity index 68% rename from src/app/pages/ParatimeDashboardPage/RosePriceCard.tsx rename to src/app/pages/ParatimeDashboardPage/TokenPriceCard.tsx index e5a1ccc3a..4fba81f84 100644 --- a/src/app/pages/ParatimeDashboardPage/RosePriceCard.tsx +++ b/src/app/pages/ParatimeDashboardPage/TokenPriceCard.tsx @@ -4,10 +4,11 @@ import Box from '@mui/material/Box' import { styled } from '@mui/material/styles' import { CoinGeckoReferral } from '../../components/CoinGeckoReferral' import { SnapshotCard } from '../../components/Snapshots/SnapshotCard' -import { useGetRosePrice } from '../../../coin-gecko/api' +import { useTokenPrice } from '../../../coin-gecko/api' import { COLORS } from '../../../styles/theme/colors' import Typography from '@mui/material/Typography' -import { SmallLogo } from '../../components/logo/SmallLogo' +import { NativeTokenInfo } from '../../../types/ticker' +import { SmallTokenLogo } from '../../components/logo/SmallTokenLogo' const StyledBox = styled(Box)(({ theme }) => ({ position: 'absolute', @@ -15,20 +16,20 @@ const StyledBox = styled(Box)(({ theme }) => ({ left: theme.spacing(4), })) -const formatFiatRoseParams = { +const formatFiatParams = { value: { - currency: 'USD', + currency: 'USD', // TODO: why are we fixated on USD maximumFractionDigits: 5, } satisfies Intl.NumberFormatOptions, } -export const RosePriceCard: FC = () => { +export const TokenPriceCard: FC<{ token: NativeTokenInfo }> = ({ token }) => { const { t } = useTranslation() - const rosePriceQuery = useGetRosePrice() - const priceString = rosePriceQuery.data + const priceQuery = useTokenPrice(token.ticker) + const priceString = priceQuery.price ? t('common.fiatValueInUSD', { - value: rosePriceQuery.data, - formatParams: formatFiatRoseParams, + value: priceQuery.price, + formatParams: formatFiatParams, }) : '' @@ -36,8 +37,8 @@ export const RosePriceCard: FC = () => { - - {t('rosePrice.header')} + + {t('tokenPrice.header', { ticker: token.ticker })} } > diff --git a/src/app/pages/RuntimeTransactionDetailPage/CurrentFiatValue.tsx b/src/app/pages/RuntimeTransactionDetailPage/CurrentFiatValue.tsx index 15f4e8ecb..0ab93035c 100644 --- a/src/app/pages/RuntimeTransactionDetailPage/CurrentFiatValue.tsx +++ b/src/app/pages/RuntimeTransactionDetailPage/CurrentFiatValue.tsx @@ -1,6 +1,6 @@ import { FC } from 'react' import { useTranslation } from 'react-i18next' -import { FiatMoneyAmountBox } from '../../components/Account' +import { FiatMoneyAmountBox } from '../../components/Balance/FiatMoneyAmount' import Box from '@mui/material/Box' import Tooltip from '@mui/material/Tooltip' import { CoinGeckoReferral } from '../../components/CoinGeckoReferral' @@ -21,7 +21,7 @@ export const CurrentFiatValue: FC = ({ amount, price, has value: new BigNumber(amount).multipliedBy(price).toFixed(), formatParams: { value: { - currency: 'USD', + currency: 'USD', // TODO: why are we fixated on USD } satisfies Intl.NumberFormatOptions, }, })} diff --git a/src/app/pages/RuntimeTransactionDetailPage/index.tsx b/src/app/pages/RuntimeTransactionDetailPage/index.tsx index 5687cd23c..164ba0804 100644 --- a/src/app/pages/RuntimeTransactionDetailPage/index.tsx +++ b/src/app/pages/RuntimeTransactionDetailPage/index.tsx @@ -27,8 +27,7 @@ import { TransactionEvents } from '../../components/Transactions/TransactionEven import { useRequiredScopeParam } from '../../hooks/useScopeParam' import { DashboardLink } from '../ParatimeDashboardPage/DashboardLink' import { getNameForTicker, Ticker } from '../../../types/ticker' -import { getTickerForScope } from '../../../config' -import { TokenPriceInfo, useTokenPrice } from '../../../coin-gecko/api' +import { AllTokenPrices, useAllTokenPrices } from '../../../coin-gecko/api' import { CurrentFiatValue } from './CurrentFiatValue' import { AddressSwitch, AddressSwitchOption } from '../../components/AddressSwitch' import InfoIcon from '@mui/icons-material/Info' @@ -103,7 +102,7 @@ export const RuntimeTransactionDetailPage: FC = () => { data?.data, ) - const tokenPriceInfo = useTokenPrice(getTickerForScope(scope)) + const tokenPrices = useAllTokenPrices() if (!transaction && !isLoading) { throw AppErrors.NotFoundTxHash @@ -126,7 +125,7 @@ export const RuntimeTransactionDetailPage: FC = () => { @@ -167,14 +166,14 @@ export const RuntimeTransactionDetailView: FC<{ transaction: TransactionDetailRuntimeBlock | undefined showLayer?: boolean standalone?: boolean - tokenPriceInfo: TokenPriceInfo + tokenPrices: AllTokenPrices addressSwitchOption?: AddressSwitchOption }> = ({ isLoading, transaction, showLayer, standalone = false, - tokenPriceInfo, + tokenPrices, addressSwitchOption = AddressSwitchOption.ETH, }) => { const { t } = useTranslation() @@ -189,6 +188,7 @@ export const RuntimeTransactionDetailView: FC<{ const ticker = transaction?.ticker || Ticker.ROSE const tickerName = getNameForTicker(t, ticker) + const tokenPriceInfo = tokenPrices[ticker] return ( <> diff --git a/src/app/pages/RuntimeTransactionsPage/index.tsx b/src/app/pages/RuntimeTransactionsPage/index.tsx index 6b9ab1666..fa8104b5f 100644 --- a/src/app/pages/RuntimeTransactionsPage/index.tsx +++ b/src/app/pages/RuntimeTransactionsPage/index.tsx @@ -14,8 +14,7 @@ import { LoadMoreButton } from '../../components/LoadMoreButton' import { TableLayout, TableLayoutButton } from '../../components/TableLayoutButton' import { RuntimeTransactionDetailView } from '../RuntimeTransactionDetailPage' import { useRequiredScopeParam } from '../../hooks/useScopeParam' -import { useTokenPrice } from '../../../coin-gecko/api' -import { getTickerForScope } from '../../../config' +import { useAllTokenPrices } from '../../../coin-gecko/api' import { VerticalList } from '../../components/VerticalList' const limit = NUMBER_OF_ITEMS_ON_SEPARATE_PAGE @@ -35,7 +34,7 @@ export const RuntimeTransactionsPage: FC = () => { // we should call useGetConsensusTransactions() } - const tokenPriceInfo = useTokenPrice(getTickerForScope(scope)) + const tokenPrices = useAllTokenPrices() useEffect(() => { if (!isMobile) { @@ -117,7 +116,7 @@ export const RuntimeTransactionsPage: FC = () => { key={key} isLoading={true} transaction={undefined} - tokenPriceInfo={tokenPriceInfo} + tokenPrices={tokenPrices} standalone /> ))} @@ -127,7 +126,7 @@ export const RuntimeTransactionsPage: FC = () => { ))} diff --git a/src/app/pages/SearchResultsPage/SearchResultsList.tsx b/src/app/pages/SearchResultsPage/SearchResultsList.tsx index 2da0f092b..366259b74 100644 --- a/src/app/pages/SearchResultsPage/SearchResultsList.tsx +++ b/src/app/pages/SearchResultsPage/SearchResultsList.tsx @@ -21,7 +21,6 @@ import { AllTokenPrices } from '../../../coin-gecko/api' import { ResultListFrame } from './ResultListFrame' import { TokenDetails } from '../../components/Tokens/TokenDetails' import { ProposalDetailView } from '../ProposalDetailsPage' -import { getTickerForScope } from '../../../config' /** * Component for displaying a list of search results @@ -71,7 +70,7 @@ export const SearchResultsList: FC<{ )} @@ -87,7 +86,7 @@ export const SearchResultsList: FC<{ isLoading={false} isError={false} account={item} - tokenPriceInfo={tokenPrices[getTickerForScope(item)]} + tokenPrices={tokenPrices} showLayer={true} /> )} @@ -103,7 +102,7 @@ export const SearchResultsList: FC<{ isLoading={false} isError={false} account={item} - tokenPriceInfo={tokenPrices[getTickerForScope(item)]} + tokenPrices={tokenPrices} showLayer={true} /> )} diff --git a/src/app/pages/SearchResultsPage/__tests__/SearchResultsList.test.tsx b/src/app/pages/SearchResultsPage/__tests__/SearchResultsList.test.tsx index cf9ef0771..2805c549f 100644 --- a/src/app/pages/SearchResultsPage/__tests__/SearchResultsList.test.tsx +++ b/src/app/pages/SearchResultsPage/__tests__/SearchResultsList.test.tsx @@ -36,6 +36,12 @@ describe('SearchResultsView', () => { isFree: true, hasUsedCoinGecko: false, }, + [Ticker.EUROe]: { + isLoading: false, + isFree: false, + price: 1, + hasUsedCoinGecko: true, + }, }} title="test search" networkForTheme={Network.mainnet} @@ -74,6 +80,12 @@ describe('SearchResultsView', () => { isFree: true, hasUsedCoinGecko: false, }, + [Ticker.EUROe]: { + isLoading: false, + isFree: false, + price: 1, + hasUsedCoinGecko: true, + }, }} />, ) diff --git a/src/app/pages/TokenDashboardPage/TokenDetailsCard.tsx b/src/app/pages/TokenDashboardPage/TokenDetailsCard.tsx index 84262cbc3..add80eb64 100644 --- a/src/app/pages/TokenDashboardPage/TokenDetailsCard.tsx +++ b/src/app/pages/TokenDashboardPage/TokenDetailsCard.tsx @@ -11,17 +11,16 @@ import { useTranslation } from 'react-i18next' import { AccountLink } from '../../components/Account/AccountLink' import { CopyToClipboard } from '../../components/CopyToClipboard' import { VerificationIcon } from '../../components/ContractVerificationIcon' -import { getNameForTicker, Ticker } from '../../../types/ticker' import { DelayedContractCreatorInfo } from '../../components/Account/ContractCreatorInfo' import CardContent from '@mui/material/CardContent' import { TokenTypeTag } from '../../components/Tokens/TokenList' import { SearchScope } from '../../../types/searchScope' -import { getPreciseNumberFormat } from '../../../locales/getPreciseNumberFormat' import { RouteUtils } from '../../utils/route-utils' import { tokenTransfersContainerId } from '../../pages/TokenDashboardPage/TokenTransfersCard' import { tokenHoldersContainerId } from '../../pages/TokenDashboardPage/TokenHoldersCard' import { RoundedBalance } from 'app/components/RoundedBalance' import { HighlightedText } from '../../components/HighlightedText' +import { RuntimeBalanceDisplay } from '../../components/Balance/RuntimeBalanceDisplay' export const TokenDetailsCard: FC<{ scope: SearchScope; address: string; searchTerm: string }> = ({ scope, @@ -35,10 +34,6 @@ export const TokenDetailsCard: FC<{ scope: SearchScope; address: string; searchT const { account, isLoading: accountIsLoading } = useAccount(scope, address) const isLoading = tokenIsLoading || accountIsLoading - const balance = account?.balances[0]?.balance - const nativeToken = account?.ticker || Ticker.ROSE - const tickerName = getNameForTicker(t, nativeToken) - return ( @@ -79,9 +74,7 @@ export const TokenDetailsCard: FC<{ scope: SearchScope; address: string; searchT
{t('common.balance')}
- {balance === undefined - ? t('common.missing') - : t('common.valueInToken', { ...getPreciseNumberFormat(balance), ticker: tickerName })} +
{t('tokens.totalSupply')}
diff --git a/src/app/utils/faucet-links.ts b/src/app/utils/faucet-links.ts index a361286c9..1d473e68a 100644 --- a/src/app/utils/faucet-links.ts +++ b/src/app/utils/faucet-links.ts @@ -1,10 +1,10 @@ -import { NativeTicker, Ticker } from 'types/ticker' +import { Ticker } from 'types/ticker' import { Network } from '../../types/network' import { Layer } from '../../oasis-nexus/api' const testnetFaucetUrl = 'https://faucet.testnet.oasis.dev/' const faucetParaTimeBaseUrl = `${testnetFaucetUrl}?paratime=` -const faucetLinks: Partial>>>>> = { +const faucetLinks: Partial>>>>> = { [Network.testnet]: { [Layer.consensus]: { [Ticker.TEST]: testnetFaucetUrl }, [Layer.emerald]: { [Ticker.TEST]: `${faucetParaTimeBaseUrl}emerald` }, @@ -14,5 +14,5 @@ const faucetLinks: Partial +export const getFaucetLink = (network: Network, layer: Layer, ticker: Ticker) => faucetLinks[network]?.[layer]?.[ticker] diff --git a/src/app/utils/test-fixtures.ts b/src/app/utils/test-fixtures.ts index 93014f3af..1d7489cdb 100644 --- a/src/app/utils/test-fixtures.ts +++ b/src/app/utils/test-fixtures.ts @@ -1,5 +1,4 @@ import { EvmTokenType, groupAccountTokenBalances, Layer, RuntimeAccount } from '../../oasis-nexus/api' -import { Ticker } from '../../types/ticker' import { Network } from '../../types/network' import { AccountResult, BlockResult } from '../pages/SearchResultsPage/hooks' @@ -63,7 +62,6 @@ export const suggestedParsedAccount: RuntimeAccount = groupAccountTokenBalances( }, layer: Layer.emerald, network: Network.mainnet, - ticker: Ticker.ROSE, }) export const suggestedEmptyAccount: RuntimeAccount = groupAccountTokenBalances({ @@ -79,7 +77,6 @@ export const suggestedEmptyAccount: RuntimeAccount = groupAccountTokenBalances({ }, layer: Layer.emerald, network: Network.mainnet, - ticker: Ticker.ROSE, evm_contract: undefined, }) diff --git a/src/coin-gecko/api.ts b/src/coin-gecko/api.ts index 9e6afb431..87ec303eb 100644 --- a/src/coin-gecko/api.ts +++ b/src/coin-gecko/api.ts @@ -1,12 +1,13 @@ import axios from 'axios' import type { AxiosResponse, AxiosError } from 'axios' import { useQuery } from '@tanstack/react-query' -import { NativeTicker, Ticker } from '../types/ticker' -import { getTickerForScope } from '../config' +import { Ticker } from '../types/ticker' +import { getTokensForScope } from '../config' import { RouteUtils } from '../app/utils/route-utils' +import { uniq } from '../app/utils/helpers' import { exhaustedTypeWarning } from '../types/errors' -type GetRosePriceParams = { +type GetTokenPricesFromGeckoParams = { ids: string vs_currencies: string include_market_cap?: string @@ -16,13 +17,13 @@ type GetRosePriceParams = { precision?: string } -type GetRosePriceResponse = { - 'oasis-network': { - usd: number - } -} +type TokenPriceMap = Partial> + +type GetTokenPricesFromGeckoResponse = TokenPriceMap -export const getRosePrice = (params: GetRosePriceParams): Promise> => { +export const getTokenPricesFromGecko = ( + params: GetTokenPricesFromGeckoParams, +): Promise> => { return axios.get('https://api.coingecko.com/api/v3/simple/price', { params: { ...params }, }) @@ -30,16 +31,20 @@ export const getRosePrice = (params: GetRosePriceParams): Promise>, AxiosError, number>( - ['roseFiatPrice'], +export function useGetTokenPricesFromGecko(tokenIds: string[]) { + return useQuery, AxiosError>( + ['tokenFiatPrices'], () => - getRosePrice({ - ids: 'oasis-network', + getTokenPricesFromGecko({ + ids: tokenIds.join(','), vs_currencies: 'usd', }), { - select: ({ data }) => data['oasis-network'].usd, + select: ({ data }) => { + const result: TokenPriceMap = {} + Object.keys(data).forEach(key => (result[key] = (data as any)[key].usd)) // TODO why are we fixated on USD + return result as any + }, staleTime, }, ) @@ -52,41 +57,29 @@ export type TokenPriceInfo = { hasUsedCoinGecko: boolean } -export const useTokenPrice = (ticker: NativeTicker): TokenPriceInfo => { - const { isLoading: roseIsLoading, data: rosePrice } = useGetRosePrice() - switch (ticker) { - case Ticker.ROSE: - return { - price: rosePrice, - isLoading: roseIsLoading, - isFree: false, - hasUsedCoinGecko: true, - } - case Ticker.TEST: - return { - hasUsedCoinGecko: false, - isLoading: false, - isFree: true, - } - default: - exhaustedTypeWarning('Checking price of unknown token', ticker) - return { - isLoading: false, - hasUsedCoinGecko: false, - isFree: false, - } +export const useTokenPrice = (ticker: Ticker): TokenPriceInfo => { + const tokenPrices = useAllTokenPrices() + const price = tokenPrices[ticker] + if (!price) { + exhaustedTypeWarning('Checking price of unknown token ticker', ticker as any) } + return price || tokenPrices[Ticker.TEST]! } -export type AllTokenPrices = Record +export type AllTokenPrices = Partial> export const useAllTokenPrices = (): AllTokenPrices => { - const results = {} as any - RouteUtils.getEnabledScopes().forEach(scope => { - const ticker = getTickerForScope(scope) - // The list of networks will never change on the run, so we can do this - // eslint-disable-next-line react-hooks/rules-of-hooks - results[ticker] = useTokenPrice(ticker) + const tokens = uniq(RouteUtils.getEnabledScopes().map(getTokensForScope).flat()) + const geckoIds = tokens.map(token => token.geckoId).filter((id): id is string => !!id) + const { isLoading: geckoIsLoading, data: geckoPrices } = useGetTokenPricesFromGecko(geckoIds) + const results: AllTokenPrices = {} + tokens.forEach(token => { + results[token.ticker] = { + isLoading: geckoIsLoading, + isFree: !!token.free, + hasUsedCoinGecko: !!token.geckoId, + price: token.geckoId && geckoPrices ? (geckoPrices as any)[token.geckoId] : undefined, + } }) return results } diff --git a/src/config.ts b/src/config.ts index fca7cf58e..c4bce3fff 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,7 +1,7 @@ // We get this from the generated code to avoid circular imports // eslint-disable-next-line no-restricted-imports import { Layer } from './oasis-nexus/generated/api' -import { getTickerForNetwork, NativeTicker } from './types/ticker' +import { getTokenForNetwork, NativeToken, NativeTokenInfo } from './types/ticker' import { SearchScope } from './types/searchScope' export const consensusDecimals = 9 @@ -13,11 +13,11 @@ type LayerNetwork = { runtimeId: string | undefined /** - * What do we call the native ticker on this layer? + * What are the native tokens on this layer? * * (If not given, the network's default token will be used.) */ - ticker?: NativeTicker + tokens?: NativeTokenInfo[] } type LayerConfig = { @@ -131,6 +131,7 @@ const pontusxConfig: LayerConfig = { // See max_batch_gas https://github.com/oasisprotocol/sapphire-paratime/blob/main/runtime/src/lib.rs#L166 blockGasLimit: 15_000_000, runtimeId: '000000000000000000000000000000000000000000000000a6d1e3ebf60dff6c', + tokens: [NativeToken.EUROe, NativeToken.TEST], }, local: { activeNodes: undefined, @@ -175,11 +176,15 @@ export const isStableDeploy = stableDeploys.some(url => window.location.origin = export const getAppTitle = () => process.env.REACT_APP_META_TITLE -export const getTickerForScope = ({ network, layer }: SearchScope): NativeTicker => { - const networkDefault = getTickerForNetwork(network) +export const getTokensForScope = (scope: SearchScope | undefined): NativeTokenInfo[] => { + if (!scope) { + return [] + } + const { network, layer } = scope + const networkDefault = getTokenForNetwork(network) if (layer !== Layer.consensus) { - return paraTimesConfig[layer][network].ticker ?? networkDefault + return paraTimesConfig[layer][network].tokens ?? [networkDefault] } - return networkDefault + return [networkDefault] } diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index a16e267fb..578133e1f 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -7,6 +7,7 @@ "emptyTokenList": "This account holds no {{spec}} {{description}}.", "emptyTransactionList": "There are no transactions on record for this account.", "emptyTokenTransferList": "There are no token transfers on record for this account.", + "failedToLookUpTickers": "We don't have the price for: {{tickers}}", "ERC20": "ERC-20", "ERC721": "ERC-721", "lastNonce": "Last Nonce", @@ -290,9 +291,6 @@ "first": "First", "last": "Last" }, - "rosePrice": { - "header": "ROSE Price" - }, "social": { "description": "Be part of the community and stay in the loop on everything Oasis", "discord": "Discord", @@ -338,6 +336,9 @@ "tokenSnapshot": { "header": "Token Snapshot" }, + "tokenPrice": { + "header": "{{ticker}} Price" + }, "totalTransactions": { "header": "Total Transactions", "tooltip": "{{value, number}} total transactions" diff --git a/src/oasis-nexus/api.ts b/src/oasis-nexus/api.ts index 075f7a025..a8ff7f8b4 100644 --- a/src/oasis-nexus/api.ts +++ b/src/oasis-nexus/api.ts @@ -1,7 +1,7 @@ /** @file Wrappers around generated API */ import axios, { AxiosResponse } from 'axios' -import { consensusDecimals, getTickerForScope, paraTimesConfig } from '../config' +import { consensusDecimals, getTokensForScope, paraTimesConfig } from '../config' import * as generated from './generated/api' import { QueryKey, UseQueryOptions, UseQueryResult } from '@tanstack/react-query' import { @@ -17,7 +17,7 @@ import { import { fromBaseUnits, getEthAddressForAccount, getAccountSize } from '../app/utils/helpers' import { Network } from '../types/network' import { SearchScope } from '../types/searchScope' -import { getTickerForNetwork, NativeTicker } from '../types/ticker' +import { Ticker } from '../types/ticker' import { useTransformToOasisAddress } from '../app/hooks/useTransformToOasisAddress' import { useEffect, useState } from 'react' import { RpcUtils } from '../app/utils/rpc-utils' @@ -32,13 +32,13 @@ declare module './generated/api' { export interface Transaction { network: Network layer: Layer - ticker: NativeTicker + ticker: Ticker } export interface RuntimeTransaction { network: Network layer: Layer - ticker: NativeTicker + ticker: Ticker } export interface Block { @@ -54,7 +54,7 @@ declare module './generated/api' { export interface Account { network: Network layer: Layer - ticker: NativeTicker + ticker: Ticker size: string total: string } @@ -63,7 +63,6 @@ declare module './generated/api' { network: Network layer: Layer address_eth?: string - ticker: NativeTicker tokenBalances: Partial> } @@ -84,7 +83,7 @@ declare module './generated/api' { } export interface Validator { - ticker: NativeTicker + ticker: Ticker } export interface Proposal { @@ -129,7 +128,7 @@ export const useGetConsensusTransactions: typeof generated.useGetConsensusTransa params?, options?, ) => { - const ticker = getTickerForNetwork(network) + const ticker = getTokensForScope({ network, layer: Layer.consensus })[0].ticker return generated.useGetConsensusTransactions(network, params, { ...options, request: { @@ -167,7 +166,7 @@ export const useGetRuntimeTransactions: typeof generated.useGetRuntimeTransactio params?, options?, ) => { - const ticker = getTickerForScope({ network, layer: runtime }) + const ticker = getTokensForScope({ network, layer: runtime })[0].ticker // TODO: find this out from tx data return generated.useGetRuntimeTransactions(network, runtime, params, { ...options, request: { @@ -204,7 +203,7 @@ export const useGetConsensusTransactionsTxHash: typeof generated.useGetConsensus txHash, options?, ) => { - const ticker = getTickerForNetwork(network) + const ticker = getTokensForScope({ network, layer: Layer.consensus })[0].ticker return generated.useGetConsensusTransactionsTxHash(network, txHash, { ...options, request: { @@ -239,7 +238,7 @@ export const useGetRuntimeTransactionsTxHash: typeof generated.useGetRuntimeTran ) => { // Sometimes we will call this with an undefined txHash, so we must be careful here. const actualHash = txHash?.startsWith('0x') ? txHash.substring(2) : txHash - const ticker = getTickerForScope({ network, layer: runtime }) + const ticker = getTokensForScope({ network, layer: runtime })[0].ticker // TODO: find this out from tx data return generated.useGetRuntimeTransactionsTxHash(network, runtime, actualHash, { ...options, request: { @@ -276,7 +275,7 @@ export const useGetConsensusAccountsAddress: typeof generated.useGetConsensusAcc address, options?, ) => { - const ticker = getTickerForNetwork(network) + const ticker = getTokensForScope({ network, layer: Layer.consensus })[0].ticker return generated.useGetConsensusAccountsAddress(network, address, { ...options, request: { @@ -320,7 +319,6 @@ export const useGetRuntimeAccountsAddress: typeof generated.useGetRuntimeAccount const oasisAddress = useTransformToOasisAddress(address) - const ticker = getTickerForScope({ network, layer: runtime }) const query = generated.useGetRuntimeAccountsAddress(network, runtime, oasisAddress!, { ...options, query: { @@ -369,7 +367,6 @@ export const useGetRuntimeAccountsAddress: typeof generated.useGetRuntimeAccount ? fromBaseUnits(data.stats?.total_sent, paraTimesConfig[runtime].decimals) : '0', }, - ticker, }) }, ...arrayify(options?.request?.transformResponse), @@ -755,7 +752,7 @@ export const useGetRuntimeEvents: typeof generated.useGetRuntimeEvents = ( event.body.amount.Amount, paraTimesConfig[runtime].decimals, ), - Denomination: getTickerForScope({ network, layer: runtime }), + Denomination: getTokensForScope({ network, layer: runtime })[0].ticker, // TODO find this out from event data } : event.body.amount, }, @@ -891,7 +888,7 @@ export const useGetConsensusValidators: typeof generated.useGetConsensusValidato params?, options?, ) => { - const ticker = getTickerForNetwork(network) + const ticker = getTokensForScope({ network, layer: Layer.consensus })[0].ticker return generated.useGetConsensusValidators(network, params, { ...options, request: { @@ -922,7 +919,7 @@ export const useGetConsensusAccounts: typeof generated.useGetConsensusAccounts = params?, options?, ) => { - const ticker = getTickerForNetwork(network) + const ticker = getTokensForScope({ network, layer: Layer.consensus })[0].ticker return generated.useGetConsensusAccounts(network, params, { ...options, request: { diff --git a/src/types/ticker.ts b/src/types/ticker.ts index 5c7a19b73..047d5d888 100644 --- a/src/types/ticker.ts +++ b/src/types/ticker.ts @@ -1,21 +1,44 @@ import { Network } from './network' import { TFunction } from 'i18next' -export type NativeTicker = (typeof Ticker)[keyof typeof Ticker] +export type Ticker = (typeof Ticker)[keyof typeof Ticker] +// eslint-disable-next-line @typescript-eslint/no-redeclare export const Ticker = { ROSE: 'ROSE', TEST: 'TEST', + EUROe: 'EUROe', } as const -const networkTicker: Record = { - [Network.mainnet]: Ticker.ROSE, - [Network.testnet]: Ticker.TEST, +export type NativeTokenInfo = { + ticker: Ticker + free?: boolean + geckoId?: string } -export const getTickerForNetwork = (network: Network): NativeTicker => networkTicker[network] +export const NativeToken = { + ROSE: { + ticker: Ticker.ROSE, + geckoId: 'oasis-network', + }, + TEST: { + ticker: Ticker.TEST, + free: true, + }, + EUROe: { + ticker: Ticker.EUROe, + geckoId: 'euroe-stablecoin', + }, +} as const + +export const networkToken: Record = { + [Network.mainnet]: NativeToken.ROSE, + [Network.testnet]: NativeToken.TEST, +} + +export const getTokenForNetwork = (network: Network): NativeTokenInfo => networkToken[network] -export const getNameForTicker = (t: TFunction, ticker: string): string => { +export const getNameForTicker = (_t: TFunction, ticker: string): string => { // TODO: how do we translate ticker names? return ticker } From 3520ce16583774bcc9a77e5425ef3e5b80203bce Mon Sep 17 00:00:00 2001 From: Kristof Csillag Date: Wed, 21 Feb 2024 03:56:12 +0100 Subject: [PATCH 4/4] Support displaying fiat values in non-USD currencies The preferred fiat currency can now be configured per paratime / network. --- src/app/components/Account/index.tsx | 4 ++-- .../components/Balance/FiatMoneyAmount.tsx | 10 +++++++-- src/app/components/Balance/hooks.ts | 7 +++++++ src/app/pages/AccountDetailsPage/index.tsx | 3 ++- .../ParatimeDashboardPage/TokenPriceCard.tsx | 21 ++++++++++++------- .../CurrentFiatValue.tsx | 11 +++++++--- .../__tests__/CurrentFiatValue.test.tsx | 1 + .../RuntimeTransactionDetailPage/index.tsx | 3 ++- .../pages/RuntimeTransactionsPage/index.tsx | 3 ++- .../__tests__/SearchResultsList.test.tsx | 4 ++++ src/app/pages/SearchResultsPage/index.tsx | 3 ++- src/coin-gecko/api.ts | 18 +++++++++------- src/config.ts | 9 ++++++++ 13 files changed, 71 insertions(+), 26 deletions(-) diff --git a/src/app/components/Account/index.tsx b/src/app/components/Account/index.tsx index bb87e8eab..3e4073054 100644 --- a/src/app/components/Account/index.tsx +++ b/src/app/components/Account/index.tsx @@ -22,7 +22,7 @@ import { AccountAvatar } from '../AccountAvatar' import { RuntimeBalanceDisplay } from '../Balance/RuntimeBalanceDisplay' import { calculateFiatValue } from '../Balance/hooks' import { FiatMoneyAmount } from '../Balance/FiatMoneyAmount' -import { getTokensForScope } from '../../../config' +import { getFiatCurrencyForScope, getTokensForScope } from '../../../config' type AccountProps = { account?: RuntimeAccount @@ -48,7 +48,7 @@ export const Account: FC = ({ account, token, isLoading, tokenPric const nativeTokens = getTokensForScope(account || { network: 'mainnet', layer: 'sapphire' }) const nativeTickerNames = nativeTokens.map(token => getNameForTicker(t, token.ticker)) const contract = account?.evm_contract - const fiatValueInfo = calculateFiatValue(account?.balances, tokenPrices) + const fiatValueInfo = calculateFiatValue(account?.balances, tokenPrices, getFiatCurrencyForScope(account)) return ( <> diff --git a/src/app/components/Balance/FiatMoneyAmount.tsx b/src/app/components/Balance/FiatMoneyAmount.tsx index 88a4cec51..2d927273b 100644 --- a/src/app/components/Balance/FiatMoneyAmount.tsx +++ b/src/app/components/Balance/FiatMoneyAmount.tsx @@ -16,7 +16,13 @@ export const FiatMoneyAmountBox = styled(Box)(() => ({ flex: 1, })) -export const FiatMoneyAmount: FC = ({ value, hasUsedCoinGecko, unknownTickers, loading }) => { +export const FiatMoneyAmount: FC = ({ + value, + fiatCurrency, + hasUsedCoinGecko, + unknownTickers, + loading, +}) => { const { t } = useTranslation() return ( @@ -25,7 +31,7 @@ export const FiatMoneyAmount: FC = ({ value, hasUsedCoinGecko, un value, formatParams: { value: { - currency: 'USD', // TODO: why are we fixated on USD? + currency: fiatCurrency, } satisfies Intl.NumberFormatOptions, }, })} diff --git a/src/app/components/Balance/hooks.ts b/src/app/components/Balance/hooks.ts index 1fa7ca2e8..da6d55f0f 100644 --- a/src/app/components/Balance/hooks.ts +++ b/src/app/components/Balance/hooks.ts @@ -17,6 +17,11 @@ export type FiatValueInfo = { */ value?: string + /** + * The fiat currency used to express the value + */ + fiatCurrency: string + /** * Have we used CoinGecko for calculating this value? */ @@ -36,6 +41,7 @@ export type FiatValueInfo = { export const calculateFiatValue = ( balances: RuntimeSdkBalance[] = [], tokenPrices: AllTokenPrices, + fiatCurrency: string, ): FiatValueInfo => { let hasValue = false let value = new BigNumber(0) @@ -72,5 +78,6 @@ export const calculateFiatValue = ( loading, unknownTickers: unknown, value: value.toFixed(), + fiatCurrency, } } diff --git a/src/app/pages/AccountDetailsPage/index.tsx b/src/app/pages/AccountDetailsPage/index.tsx index 62b1ba7e7..b35bdba1e 100644 --- a/src/app/pages/AccountDetailsPage/index.tsx +++ b/src/app/pages/AccountDetailsPage/index.tsx @@ -18,6 +18,7 @@ import { AccountDetailsCard } from './AccountDetailsCard' import { AccountEventsCard } from './AccountEventsCard' import { DappBanner } from '../../components/DappBanner' import { AddressLoaderData } from '../../utils/route-utils' +import { getFiatCurrencyForScope } from '../../../config' export type AccountDetailsContext = { scope: SearchScope @@ -36,7 +37,7 @@ export const AccountDetailsPage: FC = () => { const isContract = !!account?.evm_contract const { token, isLoading: isTokenLoading } = useTokenInfo(scope, address, isContract) - const tokenPrices = useAllTokenPrices() + const tokenPrices = useAllTokenPrices(getFiatCurrencyForScope(scope)) const { isLoading: areEventsLoading, isError: isEventsError, events } = useAccountEvents(scope, address) diff --git a/src/app/pages/ParatimeDashboardPage/TokenPriceCard.tsx b/src/app/pages/ParatimeDashboardPage/TokenPriceCard.tsx index 4fba81f84..47400f34a 100644 --- a/src/app/pages/ParatimeDashboardPage/TokenPriceCard.tsx +++ b/src/app/pages/ParatimeDashboardPage/TokenPriceCard.tsx @@ -9,6 +9,8 @@ import { COLORS } from '../../../styles/theme/colors' import Typography from '@mui/material/Typography' import { NativeTokenInfo } from '../../../types/ticker' import { SmallTokenLogo } from '../../components/logo/SmallTokenLogo' +import { getFiatCurrencyForScope } from '../../../config' +import { useScopeParam } from '../../hooks/useScopeParam' const StyledBox = styled(Box)(({ theme }) => ({ position: 'absolute', @@ -16,16 +18,19 @@ const StyledBox = styled(Box)(({ theme }) => ({ left: theme.spacing(4), })) -const formatFiatParams = { - value: { - currency: 'USD', // TODO: why are we fixated on USD - maximumFractionDigits: 5, - } satisfies Intl.NumberFormatOptions, -} - export const TokenPriceCard: FC<{ token: NativeTokenInfo }> = ({ token }) => { const { t } = useTranslation() - const priceQuery = useTokenPrice(token.ticker) + const scope = useScopeParam() + const fiatCurrency = getFiatCurrencyForScope(scope) + const priceQuery = useTokenPrice(token.ticker, fiatCurrency) + + const formatFiatParams = { + value: { + currency: fiatCurrency, + maximumFractionDigits: 5, + } satisfies Intl.NumberFormatOptions, + } + const priceString = priceQuery.price ? t('common.fiatValueInUSD', { value: priceQuery.price, diff --git a/src/app/pages/RuntimeTransactionDetailPage/CurrentFiatValue.tsx b/src/app/pages/RuntimeTransactionDetailPage/CurrentFiatValue.tsx index 0ab93035c..5be5290ca 100644 --- a/src/app/pages/RuntimeTransactionDetailPage/CurrentFiatValue.tsx +++ b/src/app/pages/RuntimeTransactionDetailPage/CurrentFiatValue.tsx @@ -8,11 +8,16 @@ import HelpIcon from '@mui/icons-material/Help' import { TokenPriceInfo } from '../../../coin-gecko/api' import BigNumber from 'bignumber.js' -type CurrentFiatValueProps = Pick & { +type CurrentFiatValueProps = Pick & { amount: string } -export const CurrentFiatValue: FC = ({ amount, price, hasUsedCoinGecko }) => { +export const CurrentFiatValue: FC = ({ + amount, + price, + fiatCurrency, + hasUsedCoinGecko, +}) => { const { t } = useTranslation() return price === undefined ? null : ( @@ -21,7 +26,7 @@ export const CurrentFiatValue: FC = ({ amount, price, has value: new BigNumber(amount).multipliedBy(price).toFixed(), formatParams: { value: { - currency: 'USD', // TODO: why are we fixated on USD + currency: fiatCurrency, } satisfies Intl.NumberFormatOptions, }, })} diff --git a/src/app/pages/RuntimeTransactionDetailPage/__tests__/CurrentFiatValue.test.tsx b/src/app/pages/RuntimeTransactionDetailPage/__tests__/CurrentFiatValue.test.tsx index fab153459..829d29ba2 100644 --- a/src/app/pages/RuntimeTransactionDetailPage/__tests__/CurrentFiatValue.test.tsx +++ b/src/app/pages/RuntimeTransactionDetailPage/__tests__/CurrentFiatValue.test.tsx @@ -8,6 +8,7 @@ describe('CurrentFiatValue', () => { hasUsedCoinGecko={true} amount="1000000000100000000010000000001000000000.10000000001" price={0.55555} + fiatCurrency="usd" />, ) expect(screen.getByText('$555,550,000,055,555,000,005,555,500,000,555,550,000.06')).toBeInTheDocument() diff --git a/src/app/pages/RuntimeTransactionDetailPage/index.tsx b/src/app/pages/RuntimeTransactionDetailPage/index.tsx index 164ba0804..daec75fbb 100644 --- a/src/app/pages/RuntimeTransactionDetailPage/index.tsx +++ b/src/app/pages/RuntimeTransactionDetailPage/index.tsx @@ -38,6 +38,7 @@ import { LongDataDisplay } from '../../components/LongDataDisplay' import { getPreciseNumberFormat } from '../../../locales/getPreciseNumberFormat' import { base64ToHex } from '../../utils/helpers' import { DappBanner } from '../../components/DappBanner' +import { getFiatCurrencyForScope } from '../../../config' type TransactionSelectionResult = { wantedTransaction?: RuntimeTransaction @@ -102,7 +103,7 @@ export const RuntimeTransactionDetailPage: FC = () => { data?.data, ) - const tokenPrices = useAllTokenPrices() + const tokenPrices = useAllTokenPrices(getFiatCurrencyForScope(scope)) if (!transaction && !isLoading) { throw AppErrors.NotFoundTxHash diff --git a/src/app/pages/RuntimeTransactionsPage/index.tsx b/src/app/pages/RuntimeTransactionsPage/index.tsx index fa8104b5f..5c452dcc7 100644 --- a/src/app/pages/RuntimeTransactionsPage/index.tsx +++ b/src/app/pages/RuntimeTransactionsPage/index.tsx @@ -16,6 +16,7 @@ import { RuntimeTransactionDetailView } from '../RuntimeTransactionDetailPage' import { useRequiredScopeParam } from '../../hooks/useScopeParam' import { useAllTokenPrices } from '../../../coin-gecko/api' import { VerticalList } from '../../components/VerticalList' +import { getFiatCurrencyForScope } from '../../../config' const limit = NUMBER_OF_ITEMS_ON_SEPARATE_PAGE @@ -34,7 +35,7 @@ export const RuntimeTransactionsPage: FC = () => { // we should call useGetConsensusTransactions() } - const tokenPrices = useAllTokenPrices() + const tokenPrices = useAllTokenPrices(getFiatCurrencyForScope(scope)) useEffect(() => { if (!isMobile) { diff --git a/src/app/pages/SearchResultsPage/__tests__/SearchResultsList.test.tsx b/src/app/pages/SearchResultsPage/__tests__/SearchResultsList.test.tsx index 2805c549f..a4ed7119d 100644 --- a/src/app/pages/SearchResultsPage/__tests__/SearchResultsList.test.tsx +++ b/src/app/pages/SearchResultsPage/__tests__/SearchResultsList.test.tsx @@ -29,6 +29,7 @@ describe('SearchResultsView', () => { isLoading: false, isFree: false, price: 1, + fiatCurrency: 'usd', hasUsedCoinGecko: true, }, [Ticker.TEST]: { @@ -40,6 +41,7 @@ describe('SearchResultsView', () => { isLoading: false, isFree: false, price: 1, + fiatCurrency: 'usd', hasUsedCoinGecko: true, }, }} @@ -73,6 +75,7 @@ describe('SearchResultsView', () => { isLoading: false, isFree: false, price: 1, + fiatCurrency: 'usd', hasUsedCoinGecko: true, }, [Ticker.TEST]: { @@ -84,6 +87,7 @@ describe('SearchResultsView', () => { isLoading: false, isFree: false, price: 1, + fiatCurrency: 'usd', hasUsedCoinGecko: true, }, }} diff --git a/src/app/pages/SearchResultsPage/index.tsx b/src/app/pages/SearchResultsPage/index.tsx index 23c3c87f4..be85ded64 100644 --- a/src/app/pages/SearchResultsPage/index.tsx +++ b/src/app/pages/SearchResultsPage/index.tsx @@ -4,13 +4,14 @@ import { useScopeParam } from '../../hooks/useScopeParam' import { useSearch } from './hooks' import { SearchResultsView } from './SearchResultsView' import { useAllTokenPrices } from '../../../coin-gecko/api' +import { getFiatCurrencyForScope } from '../../../config' export const SearchResultsPage: FC = () => { const searchParams = useParamSearch() const scope = useScopeParam() const { results, isLoading } = useSearch(searchParams) - const tokenPrices = useAllTokenPrices() + const tokenPrices = useAllTokenPrices(getFiatCurrencyForScope(scope)) return ( , AxiosError>( ['tokenFiatPrices'], () => getTokenPricesFromGecko({ ids: tokenIds.join(','), - vs_currencies: 'usd', + vs_currencies: fiatCurrency ?? 'usd', }), { select: ({ data }) => { const result: TokenPriceMap = {} - Object.keys(data).forEach(key => (result[key] = (data as any)[key].usd)) // TODO why are we fixated on USD + Object.keys(data).forEach(key => { + result[key] = (data as any)[key][fiatCurrency] + }) return result as any }, staleTime, @@ -52,13 +54,14 @@ export function useGetTokenPricesFromGecko(tokenIds: string[]) { export type TokenPriceInfo = { price?: number + fiatCurrency?: string isLoading: boolean isFree: boolean hasUsedCoinGecko: boolean } -export const useTokenPrice = (ticker: Ticker): TokenPriceInfo => { - const tokenPrices = useAllTokenPrices() +export const useTokenPrice = (ticker: Ticker, fiatCurrency: string): TokenPriceInfo => { + const tokenPrices = useAllTokenPrices(fiatCurrency) const price = tokenPrices[ticker] if (!price) { exhaustedTypeWarning('Checking price of unknown token ticker', ticker as any) @@ -68,10 +71,10 @@ export const useTokenPrice = (ticker: Ticker): TokenPriceInfo => { export type AllTokenPrices = Partial> -export const useAllTokenPrices = (): AllTokenPrices => { +export const useAllTokenPrices = (fiatCurrency: string): AllTokenPrices => { const tokens = uniq(RouteUtils.getEnabledScopes().map(getTokensForScope).flat()) const geckoIds = tokens.map(token => token.geckoId).filter((id): id is string => !!id) - const { isLoading: geckoIsLoading, data: geckoPrices } = useGetTokenPricesFromGecko(geckoIds) + const { isLoading: geckoIsLoading, data: geckoPrices } = useGetTokenPricesFromGecko(geckoIds, fiatCurrency) const results: AllTokenPrices = {} tokens.forEach(token => { results[token.ticker] = { @@ -79,6 +82,7 @@ export const useAllTokenPrices = (): AllTokenPrices => { isFree: !!token.free, hasUsedCoinGecko: !!token.geckoId, price: token.geckoId && geckoPrices ? (geckoPrices as any)[token.geckoId] : undefined, + fiatCurrency: token.geckoId && geckoPrices ? fiatCurrency : 'xx', } }) return results diff --git a/src/config.ts b/src/config.ts index c4bce3fff..6cd9c4bfa 100644 --- a/src/config.ts +++ b/src/config.ts @@ -18,6 +18,11 @@ type LayerNetwork = { * (If not given, the network's default token will be used.) */ tokens?: NativeTokenInfo[] + + /** + * What fiat currency should we use for displaying value? + */ + fiatCurrency?: string } type LayerConfig = { @@ -132,6 +137,7 @@ const pontusxConfig: LayerConfig = { blockGasLimit: 15_000_000, runtimeId: '000000000000000000000000000000000000000000000000a6d1e3ebf60dff6c', tokens: [NativeToken.EUROe, NativeToken.TEST], + fiatCurrency: 'eur', }, local: { activeNodes: undefined, @@ -188,3 +194,6 @@ export const getTokensForScope = (scope: SearchScope | undefined): NativeTokenIn } return [networkDefault] } + +export const getFiatCurrencyForScope = (scope: SearchScope | undefined) => + (scope ? paraTimesConfig[scope.layer]?.[scope.network]?.fiatCurrency : undefined) ?? 'usd'