From eb11d0666edd65703c104001dc1523f0311bf6a8 Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz <43585069+Sharqiewicz@users.noreply.github.com> Date: Thu, 12 Sep 2024 17:03:12 +0200 Subject: [PATCH] fetch assets from batching server (#542) * fetch assets from batching server * fix lint * fetch prices from batching server * use tanstack react query for usePriceFetcher * simplify the code, download the price only from the batching server * update readme.md * update readme.md * fix portfolio table sorting, fix table cells width * fix tables styles * add BATCHING_SERVER_URL * Amened README.md --------- Co-authored-by: Marcel Ebert --- README.md | 14 +++- src/components/Table/index.tsx | 22 ++++-- src/components/nabla/Swap/To.tsx | 31 +++++---- src/hooks/useAssetRegistryMetadata.ts | 2 +- src/hooks/useBalances.ts | 6 +- src/hooks/usePriceFetcher.ts | 97 ++++++++++++--------------- src/pages/dashboard/Portfolio.tsx | 11 +-- src/pages/staking/CollatorColumns.tsx | 12 ++-- src/shared/constants.ts | 4 +- 9 files changed, 106 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index 54515bc8..603946a2 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ Yarn version specified in the `packageManager` field. **Important:** Modern [Yarn](https://yarnpkg.com/getting-started/install) releases should not be installed globally or via npm - use Corepack instead. -**Note:** If you are using Volta to manage your Node.js versions, you need to follow the -instructions [here](https://yarnpkg.com/corepack#volta). +**Note:** If you are using Volta to manage your Node.js versions, you need to follow the instructions +[here](https://yarnpkg.com/corepack#volta). ### `yarn install` @@ -48,7 +48,15 @@ We call on `version.cjs` to show the commit version on the sidebar.\ We also create a file, on the fly, a file named `_redirects` that will serve the index.html instead of giving a 404 no matter what URL the browser requests. -## Fixing type issues +## Troubleshooting + +### Missing price information + +If you are missing the price information about the assets on the dashboard page, you are probably experiencing a CORS +problem with the batching server. If you want to fetch prices locally, you can use the proxy server available +at [pendulum-tools](https://github.com/pendulum-chain/pendulum-tools). Change url in `src/hooks/usePriceFetcher.ts` file to `http://localhost:3000` + +### Fixing type issues If you encounter issues with the IDE not detecting the type overwrites of the `@pendulum-chain/types` package properly, make sure that all the `@polkadot/xxx` packages match the same version used in the types package. It is also important diff --git a/src/components/Table/index.tsx b/src/components/Table/index.tsx index 28b2678e..86669326 100644 --- a/src/components/Table/index.tsx +++ b/src/components/Table/index.tsx @@ -55,10 +55,13 @@ export type TableProps = { fontSize?: string; /** Sets the global font size for the Table. */ rowCallback?: (row: Row, index: number) => void; + /** If true, the table will have fixed columns */ + tableFixed?: boolean; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any const defaultData: any[] = []; +const loading = <>{repeat(, 6)}; const Table = ({ data = defaultData, @@ -73,6 +76,7 @@ const Table = ({ fontSize, title, rowCallback, + tableFixed, }: TableProps): JSX.Element | null => { const totalCount = data.length; @@ -96,7 +100,7 @@ const Table = ({ : undefined; }, [sortBy]); - const { getHeaderGroups, getRowModel, getPageCount, nextPage, previousPage, setGlobalFilter, getState } = + const { getHeaderGroups, getRowModel, getPageCount, nextPage, previousPage, setGlobalFilter, getState, setSorting } = useReactTable({ columns: tableColumns, data: tableData, @@ -133,7 +137,7 @@ const Table = ({ } font-semibold ${className})`} > {title &&
{title}
} - +
{getHeaderGroups().map((headerGroup) => ( @@ -144,7 +148,17 @@ const Table = ({ key={header.id} colSpan={header.colSpan} className={`${isSortable ? 'cursor-pointer' : ''}`} - onClick={header.column.getToggleSortingHandler()} + onClick={() => { + if (isSortable) { + const currentSort = header.column.getIsSorted(); + setSorting([ + { + id: header.column.id, + desc: currentSort === 'asc', + }, + ]); + } + }} >
({ key={cell.id} className={`${cell.column.columnDef.meta?.className || ''} ${ (index % 2 ? evenRowsClassname : oddRowsClassname) || 'bg-base-200' - }`} + } ${tableFixed ? 'table-fixed' : ''}`} > {flexRender(cell.column.columnDef.cell, cell.getContext())} diff --git a/src/components/nabla/Swap/To.tsx b/src/components/nabla/Swap/To.tsx index eb5ed171..b88c78da 100644 --- a/src/components/nabla/Swap/To.tsx +++ b/src/components/nabla/Swap/To.tsx @@ -4,7 +4,6 @@ import { Button } from 'react-daisyui'; import { useFormContext } from 'react-hook-form'; import Big from 'big.js'; -import pendulumIcon from '../../../assets/pendulum-icon.svg'; import { UseTokenOutAmountResult } from '../../../hooks/nabla/useTokenOutAmount'; import useBoolean from '../../../hooks/useBoolean'; import { NumberLoader } from '../../Loader'; @@ -48,9 +47,9 @@ export function To({ }, [toAmountQuote.data?.amountOut.preciseString, setValue]); return ( -
-
-
+
+
+
{toAmountQuote.isLoading ? ( ) : toAmountQuote.data !== undefined ? ( @@ -62,7 +61,7 @@ export function To({ className="hover:opacity-80" title="Refresh" > - + ) : ( <>0 @@ -70,19 +69,19 @@ export function To({
-
-
{toToken ? : '$ -'}
+
+
{toToken ? : '$ -'}
{walletAccount && (
Balance:{' '} @@ -95,13 +94,13 @@ export function To({
)}
-
+
-
+
{fromToken !== undefined && toToken !== undefined && @@ -115,9 +114,9 @@ export function To({
- +
diff --git a/src/hooks/useAssetRegistryMetadata.ts b/src/hooks/useAssetRegistryMetadata.ts index 5130131a..9fb75a1e 100644 --- a/src/hooks/useAssetRegistryMetadata.ts +++ b/src/hooks/useAssetRegistryMetadata.ts @@ -48,7 +48,7 @@ export const useAssetRegistryMetadata = (): UseAssetRegistryMetadata => { const fetchMetadata = useCallback(async () => { if (api) { const fetchedMetadata = await api.query.assetRegistry.metadata.entries(); - return fetchedMetadata.map((m) => convertToOrmlAssetRegistryAssetMetadata(m)); + return fetchedMetadata.map(convertToOrmlAssetRegistryAssetMetadata); } return []; diff --git a/src/hooks/useBalances.ts b/src/hooks/useBalances.ts index f5eb53fb..b2475b80 100644 --- a/src/hooks/useBalances.ts +++ b/src/hooks/useBalances.ts @@ -36,8 +36,8 @@ function useBalances() { ); useEffect(() => { - const getTokensBalances = async () => { - if (!walletAccount) return []; + const getTokensBalances = async (): Promise => { + if (!walletAccount) return Promise.resolve(); const assets = getAllAssetsMetadata(); const walletAddress = ss58Format ? getAddressForFormat(walletAccount.address, ss58Format) : walletAccount.address; @@ -74,7 +74,7 @@ function useBalances() { }), ); - return setBalances(tokensBalances); + setBalances(tokensBalances); }; getTokensBalances().catch(console.error); diff --git a/src/hooks/usePriceFetcher.ts b/src/hooks/usePriceFetcher.ts index aa460cdb..783e4c36 100644 --- a/src/hooks/usePriceFetcher.ts +++ b/src/hooks/usePriceFetcher.ts @@ -1,11 +1,12 @@ -import { useCallback, useEffect, useState } from 'preact/compat'; +import { SpacewalkPrimitivesCurrencyId } from '@polkadot/types/lookup'; +import { useCallback } from 'preact/compat'; +import { useQuery } from '@tanstack/react-query'; +import { isEqual } from 'lodash'; import { TenantName } from '../models/Tenant'; import useSwitchChain from './useSwitchChain'; -import { useNodeInfoState } from '../NodeInfoProvider'; import { nativeToDecimal } from '../shared/parseNumbers/metric'; -import { SpacewalkPrimitivesCurrencyId } from '@polkadot/types/lookup'; import { useAssetRegistryMetadata } from './useAssetRegistryMetadata'; -import { isEqual } from 'lodash'; +import { BATCHING_SERVER_URL } from '../shared/constants'; export interface DiaKeys { blockchain: string; @@ -20,68 +21,58 @@ function diaKeysToString(diaKeys: DiaKeys) { type PricesCache = { [diaKeys: string]: number }; export const usePriceFetcher = () => { - const [pricesCache, setPricesCache] = useState({}); const { currentTenant } = useSwitchChain(); - const { api } = useNodeInfoState().state; const { getAllAssetsMetadata } = useAssetRegistryMetadata(); + const allAssetsMetadata = getAllAssetsMetadata(); + const diaKeys = allAssetsMetadata.map((asset) => asset.metadata.additional.diaKeys); - useEffect(() => { - if (!api) return; - - const fetchPrices = async () => { - const allPrices = await api.query.diaOracleModule.coinInfosMap.entries(); + const getPriceFromBatchingServer = useCallback(async () => { + try { + const response = await fetch(BATCHING_SERVER_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(diaKeys.map(({ blockchain, symbol }) => ({ blockchain, symbol }))), + }); - const prices = allPrices.map(([key, value]) => { - const keyJson = key.toHuman() as unknown as DiaKeys[]; - const assetKeys = keyJson[0]; - const priceRaw = value.price; - const price = nativeToDecimal(priceRaw.toString()).toNumber(); + if (!response.ok) { + console.error('Batching server response was not ok:', response.status, response.statusText); + return {}; + } - return { assetKeys, price }; - }); + const batchingServerPrices = await response.json(); - setPricesCache((prev) => { - const newPricesCache = { ...prev }; - prices.forEach(({ assetKeys, price }) => { - newPricesCache[diaKeysToString(assetKeys)] = price; - }); + return batchingServerPrices.reduce( + (acc: PricesCache, { symbol, price, blockchain }: { symbol: string; price: number; blockchain: string }) => { + acc[diaKeysToString({ symbol, blockchain })] = nativeToDecimal(price.toString()).toNumber(); + return acc; + }, + {}, + ); + } catch (error) { + console.error('Error fetching prices from batching server:', error); + return {}; + } + }, [diaKeys]); - return newPricesCache; - }); - }; + const { data: pricesCache = {} } = useQuery({ + queryKey: ['prices', allAssetsMetadata], + queryFn: getPriceFromBatchingServer, + }); - fetchPrices().catch(console.error); - }, [api]); + const getTokenPriceForKeys = useCallback((asset: DiaKeys) => pricesCache[diaKeysToString(asset)] || 0, [pricesCache]); - const getTokenPriceForKeys = useCallback( - async (asset: DiaKeys) => { - try { - const diaKeys = diaKeysToString(asset); - const cachedAssetPrice = pricesCache[diaKeys]; - if (cachedAssetPrice) return cachedAssetPrice; - } catch (e) { - console.error(e); - } - return 0; - }, - [pricesCache], + const handleNativeTokenPrice = useCallback( + () => (currentTenant === TenantName.Pendulum ? pricesCache['Pendulum:PEN'] : pricesCache['Amplitude:AMPE']), + [currentTenant, pricesCache], ); - const handleNativeTokenPrice = useCallback(() => { - if (currentTenant === TenantName.Pendulum) return pricesCache['Pendulum:PEN']; - return pricesCache['Amplitude:AMPE']; - }, [currentTenant, pricesCache]); - const getTokenPriceForCurrency = useCallback( - async (currency: SpacewalkPrimitivesCurrencyId) => { + (currency: SpacewalkPrimitivesCurrencyId) => { if (currency.toHuman() === 'Native') return handleNativeTokenPrice(); - const asset = getAllAssetsMetadata().find((asset) => { - return isEqual(asset.currencyId.toHuman(), currency.toHuman()); - }); - if (!asset) { - return 0; - } - return getTokenPriceForKeys(asset.metadata.additional.diaKeys); + + const asset = getAllAssetsMetadata().find((asset) => isEqual(asset.currencyId.toHuman(), currency.toHuman())); + + return asset ? getTokenPriceForKeys(asset.metadata.additional.diaKeys) : 0; }, [getAllAssetsMetadata, getTokenPriceForKeys, handleNativeTokenPrice], ); diff --git a/src/pages/dashboard/Portfolio.tsx b/src/pages/dashboard/Portfolio.tsx index 79e69e6b..49066b51 100644 --- a/src/pages/dashboard/Portfolio.tsx +++ b/src/pages/dashboard/Portfolio.tsx @@ -13,24 +13,25 @@ function Portfolio() { }, []); return ( -
-
-
Wallet
+
+
+
Wallet
$ {accountTotalBalance.toPrecision(2)}
{walletAccount && (
)} {!walletAccount &&
You need to connect a wallet in order to see your Portfolio.
} diff --git a/src/pages/staking/CollatorColumns.tsx b/src/pages/staking/CollatorColumns.tsx index 955644c3..2896c55d 100644 --- a/src/pages/staking/CollatorColumns.tsx +++ b/src/pages/staking/CollatorColumns.tsx @@ -69,7 +69,7 @@ export const delegatorsColumn: ColumnDef = { className="tooltip tooltip-error before:whitespace-pre-wrap before:content-[attr(data-tip)]" data-tip="The collator candidate has reached maximum number of delegators" > - + )} @@ -124,7 +124,7 @@ export const actionsColumn = ({ const isDelegator = row.original.candidate.delegators.find(({ owner }) => owner === userAccountAddress); const canStake = isDelegator || (walletAccount && !maxDelegatorsReached && (!userStaking || canUnstake)); return ( -
+
diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 18fe0f38..b572b95a 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -2,5 +2,5 @@ export const MINUTE_IN_MILLISECONDS = 60 * 1000; export const SECONDS_IN_A_DAY = 86400; export const BLOCK_TIME_SEC = 12; - -export const PENDULUM_SUPPORT_CHAT_URL = 'https://discord.com/channels/841944723979108403/1247164487883292672' \ No newline at end of file +export const PENDULUM_SUPPORT_CHAT_URL = 'https://discord.com/channels/841944723979108403/1247164487883292672'; +export const BATCHING_SERVER_URL = 'https://batching-server.pendulumchain.tech/currencies';