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/public/euroe.png b/public/euroe.png new file mode 100644 index 000000000..b478a0d38 Binary files /dev/null and b/public/euroe.png differ diff --git a/src/app/components/Account/index.tsx b/src/app/components/Account/index.tsx index 581834c96..3e4073054 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 { getFiatCurrencyForScope, 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, getFiatCurrencyForScope(account)) 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..2d927273b --- /dev/null +++ b/src/app/components/Balance/FiatMoneyAmount.tsx @@ -0,0 +1,51 @@ +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, + fiatCurrency, + hasUsedCoinGecko, + unknownTickers, + loading, +}) => { + const { t } = useTranslation() + return ( + + + {t('common.fiatValueInUSD', { + value, + formatParams: { + value: { + currency: fiatCurrency, + } 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..da6d55f0f --- /dev/null +++ b/src/app/components/Balance/hooks.ts @@ -0,0 +1,83 @@ +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 + + /** + * The fiat currency used to express the value + */ + fiatCurrency: 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, + fiatCurrency: string, +): 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(), + fiatCurrency, + } +} 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..b35bdba1e 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' @@ -19,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 @@ -37,7 +37,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(getFiatCurrencyForScope(scope)) const { isLoading: areEventsLoading, isError: isEventsError, events } = useAccountEvents(scope, address) @@ -61,7 +61,7 @@ export const AccountDetailsPage: FC = () => { isContract={isContract} account={account} token={token} - tokenPriceInfo={tokenPriceInfo} + tokenPrices={tokenPrices} /> = ({ scope }) => { const { t } = useTranslation() const defaultChartDurationValue = useConstant(() => ChartDuration.TODAY) const [chartDuration, setChartDuration] = useState(defaultChartDurationValue) const paratime = getLayerLabels(t)[scope.layer] + 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 @@ -48,8 +53,8 @@ export const ParaTimeSnapshot: FC<{ scope: SearchScope }> = ({ scope }) => { - {scope.network === Network.mainnet && } - {scope.network === Network.testnet && } + {!mainToken.free && } + {faucetLink && } diff --git a/src/app/pages/ParatimeDashboardPage/TestnetFaucet.tsx b/src/app/pages/ParatimeDashboardPage/TestnetFaucet.tsx index 515ed53d5..b9f2a4832 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 { Ticker } from '../../../types/ticker' +import { Network } from '../../../types/network' type TestnetFaucetProps = { + network: Network layer: Layer + ticker: Ticker } -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/pages/ParatimeDashboardPage/RosePriceCard.tsx b/src/app/pages/ParatimeDashboardPage/TokenPriceCard.tsx similarity index 58% rename from src/app/pages/ParatimeDashboardPage/RosePriceCard.tsx rename to src/app/pages/ParatimeDashboardPage/TokenPriceCard.tsx index e5a1ccc3a..47400f34a 100644 --- a/src/app/pages/ParatimeDashboardPage/RosePriceCard.tsx +++ b/src/app/pages/ParatimeDashboardPage/TokenPriceCard.tsx @@ -4,10 +4,13 @@ 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' +import { getFiatCurrencyForScope } from '../../../config' +import { useScopeParam } from '../../hooks/useScopeParam' const StyledBox = styled(Box)(({ theme }) => ({ position: 'absolute', @@ -15,20 +18,23 @@ const StyledBox = styled(Box)(({ theme }) => ({ left: theme.spacing(4), })) -const formatFiatRoseParams = { - value: { - currency: '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 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: rosePriceQuery.data, - formatParams: formatFiatRoseParams, + value: priceQuery.price, + formatParams: formatFiatParams, }) : '' @@ -36,8 +42,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..5be5290ca 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' @@ -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', + 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 29e820fd6..daec75fbb 100644 --- a/src/app/pages/RuntimeTransactionDetailPage/index.tsx +++ b/src/app/pages/RuntimeTransactionDetailPage/index.tsx @@ -26,8 +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 { TokenPriceInfo, useTokenPrice } from '../../../coin-gecko/api' +import { getNameForTicker, Ticker } from '../../../types/ticker' +import { AllTokenPrices, useAllTokenPrices } from '../../../coin-gecko/api' import { CurrentFiatValue } from './CurrentFiatValue' import { AddressSwitch, AddressSwitchOption } from '../../components/AddressSwitch' import InfoIcon from '@mui/icons-material/Info' @@ -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 tokenPriceInfo = useTokenPrice(getTickerForNetwork(scope.network)) + const tokenPrices = useAllTokenPrices(getFiatCurrencyForScope(scope)) if (!transaction && !isLoading) { throw AppErrors.NotFoundTxHash @@ -125,7 +126,7 @@ export const RuntimeTransactionDetailPage: FC = () => { @@ -166,14 +167,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() @@ -188,6 +189,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 4419d2f35..5c452dcc7 100644 --- a/src/app/pages/RuntimeTransactionsPage/index.tsx +++ b/src/app/pages/RuntimeTransactionsPage/index.tsx @@ -14,9 +14,9 @@ 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 { getTickerForNetwork } from '../../../types/ticker' +import { useAllTokenPrices } from '../../../coin-gecko/api' import { VerticalList } from '../../components/VerticalList' +import { getFiatCurrencyForScope } from '../../../config' 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 tokenPrices = useAllTokenPrices(getFiatCurrencyForScope(scope)) useEffect(() => { if (!isMobile) { @@ -117,7 +117,7 @@ export const RuntimeTransactionsPage: FC = () => { key={key} isLoading={true} transaction={undefined} - tokenPriceInfo={tokenPriceInfo} + tokenPrices={tokenPrices} standalone /> ))} @@ -127,7 +127,7 @@ export const RuntimeTransactionsPage: FC = () => { ))} diff --git a/src/app/pages/SearchResultsPage/SearchResultsList.tsx b/src/app/pages/SearchResultsPage/SearchResultsList.tsx index 4eb4945fd..366259b74 100644 --- a/src/app/pages/SearchResultsPage/SearchResultsList.tsx +++ b/src/app/pages/SearchResultsPage/SearchResultsList.tsx @@ -70,7 +70,7 @@ export const SearchResultsList: FC<{ )} @@ -86,7 +86,7 @@ export const SearchResultsList: FC<{ isLoading={false} isError={false} account={item} - tokenPriceInfo={tokenPrices[item.network]} + tokenPrices={tokenPrices} showLayer={true} /> )} @@ -102,7 +102,7 @@ export const SearchResultsList: FC<{ isLoading={false} isError={false} account={item} - tokenPriceInfo={tokenPrices[item.network]} + 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 e726877c4..a4ed7119d 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,17 +25,25 @@ describe('SearchResultsView', () => { { title="test search" networkForTheme={Network.mainnet} tokenPrices={{ - [Network.mainnet]: { + [Ticker.ROSE]: { isLoading: false, isFree: false, price: 1, + fiatCurrency: 'usd', hasUsedCoinGecko: true, }, - [Network.testnet]: { + [Ticker.TEST]: { isLoading: false, isFree: true, hasUsedCoinGecko: false, }, + [Ticker.EUROe]: { + 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 ( = ({ 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/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..1d473e68a --- /dev/null +++ b/src/app/utils/faucet-links.ts @@ -0,0 +1,18 @@ +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>>>>> = { + [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: 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 7e160aad6..ab7f74497 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 { getTickerForNetwork, NativeTicker, Ticker } from '../types/ticker' -import { Network } from '../types/network' +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> -export const getRosePrice = (params: GetRosePriceParams): Promise> => { +type GetTokenPricesFromGeckoResponse = TokenPriceMap + +export const getTokenPricesFromGecko = ( + params: GetTokenPricesFromGeckoParams, +): Promise> => { return axios.get('https://api.coingecko.com/api/v3/simple/price', { params: { ...params }, }) @@ -30,16 +31,22 @@ export const getRosePrice = (params: GetRosePriceParams): Promise>, AxiosError, number>( - ['roseFiatPrice'], +export function useGetTokenPricesFromGecko(tokenIds: string[], fiatCurrency: string) { + return useQuery, AxiosError>( + ['tokenFiatPrices'], () => - getRosePrice({ - ids: 'oasis-network', - vs_currencies: 'usd', + getTokenPricesFromGecko({ + ids: tokenIds.join(','), + vs_currencies: fiatCurrency ?? 'usd', }), { - select: ({ data }) => data['oasis-network'].usd, + select: ({ data }) => { + const result: TokenPriceMap = {} + Object.keys(data).forEach(key => { + result[key] = (data as any)[key][fiatCurrency] + }) + return result as any + }, staleTime, }, ) @@ -47,43 +54,36 @@ export function useGetRosePrice() { export type TokenPriceInfo = { price?: number + fiatCurrency?: string isLoading: boolean isFree: boolean 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, fiatCurrency: string): TokenPriceInfo => { + const tokenPrices = useAllTokenPrices(fiatCurrency) + 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 - // 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)))) +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, fiatCurrency) + 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, + fiatCurrency: token.geckoId && geckoPrices ? fiatCurrency : 'xx', + } + }) return results } diff --git a/src/config.ts b/src/config.ts index fa6ce54c4..6cd9c4bfa 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 { getTokenForNetwork, NativeToken, NativeTokenInfo } from './types/ticker' +import { SearchScope } from './types/searchScope' export const consensusDecimals = 9 @@ -8,6 +11,18 @@ type LayerNetwork = { address: string | undefined blockGasLimit: number | undefined runtimeId: string | undefined + + /** + * What are the native tokens on this layer? + * + * (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 = { @@ -121,6 +136,8 @@ 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], + fiatCurrency: 'eur', }, local: { activeNodes: undefined, @@ -164,3 +181,19 @@ 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 getTokensForScope = (scope: SearchScope | undefined): NativeTokenInfo[] => { + if (!scope) { + return [] + } + const { network, layer } = scope + const networkDefault = getTokenForNetwork(network) + + if (layer !== Layer.consensus) { + return paraTimesConfig[layer][network].tokens ?? [networkDefault] + } + return [networkDefault] +} + +export const getFiatCurrencyForScope = (scope: SearchScope | undefined) => + (scope ? paraTimesConfig[scope.layer]?.[scope.network]?.fiatCurrency : undefined) ?? 'usd' 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 500434eba..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, 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 = getTickerForNetwork(network) + 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 = getTickerForNetwork(network) + 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 = getTickerForNetwork(network) 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: getTickerForNetwork(network), + 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 }