diff --git a/src/components/Stats/PoolList/PoolList.tsx b/src/components/Stats/PoolList/PoolList.tsx index 51601fe6..bb3dd564 100644 --- a/src/components/Stats/PoolList/PoolList.tsx +++ b/src/components/Stats/PoolList/PoolList.tsx @@ -5,6 +5,7 @@ import { useStyles } from './style' import { Grid } from '@mui/material' import { PaginationList } from '@components/PaginationList/PaginationList' import { SortTypePoolList } from '@store/consts/static' +import { Network } from '@invariant-labs/a0-sdk' interface PoolListInterface { data: Array<{ @@ -15,6 +16,8 @@ interface PoolListInterface { volume: number TVL: number fee: number + addressFrom: string + addressTo: string // apy: number // apyData: { // fees: number @@ -22,9 +25,10 @@ interface PoolListInterface { // accumulatedFarmsSingleTick: number // } }> + network: Network } -const PoolList: React.FC = ({ data }) => { +const PoolList: React.FC = ({ data, network }) => { const { classes } = useStyles() const [page, setPage] = React.useState(1) const [sortType, setSortType] = React.useState(SortTypePoolList.VOLUME_DESC) @@ -92,6 +96,9 @@ const PoolList: React.FC = ({ data }) => { // apy={element.apy} // apyData={element.apyData} key={index} + addressFrom={element.addressFrom} + addressTo={element.addressTo} + network={network} /> ))} {pages > 1 ? ( diff --git a/src/components/Stats/PoolListItem/PoolListItem.tsx b/src/components/Stats/PoolListItem/PoolListItem.tsx index a7e4d614..7ba91f26 100644 --- a/src/components/Stats/PoolListItem/PoolListItem.tsx +++ b/src/components/Stats/PoolListItem/PoolListItem.tsx @@ -2,12 +2,14 @@ import React from 'react' import { theme } from '@static/theme' import { useStyles } from './style' import { Box, Grid, Typography, useMediaQuery } from '@mui/material' -import { formatNumbers, showPrefix } from '@utils/utils' +import { addressToTicker, formatNumbers, parseFeeToPathFee, showPrefix } from '@utils/utils' import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown' import ArrowDropUpIcon from '@mui/icons-material/ArrowDropUp' import { useNavigate } from 'react-router-dom' import icons from '@static/icons' import { SortTypePoolList } from '@store/consts/static' +import { Network } from '@invariant-labs/a0-sdk' +import { PERCENTAGE_SCALE } from '@invariant-labs/a0-sdk/target/consts' interface IProps { TVL?: number @@ -22,6 +24,9 @@ interface IProps { sortType?: SortTypePoolList onSort?: (type: SortTypePoolList) => void hideBottomLine?: boolean + addressFrom?: string + addressTo?: string + network?: Network // apy?: number // apyData?: { // fees: number @@ -42,7 +47,10 @@ const PoolListItem: React.FC = ({ tokenIndex, sortType, onSort, - hideBottomLine = false + hideBottomLine = false, + addressFrom, + addressTo, + network // apy = 0, // apyData = { // fees: 0, @@ -56,11 +64,15 @@ const PoolListItem: React.FC = ({ const isXs = useMediaQuery(theme.breakpoints.down('xs')) const handleOpenPosition = () => { - navigate(`/newPosition/${symbolFrom}/${symbolTo}/0_01`) + navigate( + `/newPosition/${addressToTicker(network ?? Network.Testnet, addressFrom ?? '')}/${addressToTicker(network ?? Network.Testnet, addressTo ?? '')}/${parseFeeToPathFee(BigInt(Math.round(fee * 10 ** Number(PERCENTAGE_SCALE - 2n))))}` + ) } const handleOpenSwap = () => { - navigate(`/exchange/${symbolFrom}/${symbolTo}`) + navigate( + `/exchange/${addressToTicker(network ?? Network.Testnet, addressFrom ?? '')}/${addressToTicker(network ?? Network.Testnet, addressTo ?? '')}` + ) } return ( diff --git a/src/containers/WrappedStats/WrappedStats.tsx b/src/containers/WrappedStats/WrappedStats.tsx index 31a77c48..00cfd4b7 100644 --- a/src/containers/WrappedStats/WrappedStats.tsx +++ b/src/containers/WrappedStats/WrappedStats.tsx @@ -5,16 +5,16 @@ import useStyles from './styles' import { Grid, Typography } from '@mui/material' import { Network } from '@invariant-labs/a0-sdk' import { EmptyPlaceholder } from '@components/EmptyPlaceholder/EmptyPlaceholder' -// import { -// fees24, -// isLoading, -// liquidityPlot, -// poolsStatsWithTokensDetails, -// tokensStatsWithTokensDetails, -// tvl24, -// volume24, -// volumePlot -// } from '@store/selectors/stats' +import { + fees24, + isLoading, + liquidityPlot, + poolsStatsWithTokensDetails, + tokensStatsWithTokensDetails, + tvl24, + volume24, + volumePlot +} from '@store/selectors/stats' import { networkType } from '@store/selectors/connection' import { actions } from '@store/reducers/stats' import Volume from '@components/Stats/Volume/Volume' @@ -22,30 +22,20 @@ import Liquidity from '@components/Stats/Liquidity/Liquidity' import VolumeBar from '@components/Stats/volumeBar/VolumeBar' import TokensList from '@components/Stats/TokensList/TokensList' import PoolList from '@components/Stats/PoolList/PoolList' -import { - isLoadingStats, - liquidityPlotData, - fees24h, - poolsList, - tokensList, - tvl24h, - volume24h, - volumePlotData -} from './mockStats' export const WrappedStats: React.FC = () => { const { classes } = useStyles() const dispatch = useDispatch() - // const poolsList = useSelector(poolsStatsWithTokensDetails) - // const tokensList = useSelector(tokensStatsWithTokensDetails) - // const volume24h = useSelector(volume24) - // const tvl24h = useSelector(tvl24) - // const fees24h = useSelector(fees24) - // const volumePlotData = useSelector(volumePlot) - // const liquidityPlotData = useSelector(liquidityPlot) - // const isLoadingStats = useSelector(isLoading) + const poolsList = useSelector(poolsStatsWithTokensDetails) + const tokensList = useSelector(tokensStatsWithTokensDetails) + const volume24h = useSelector(volume24) + const tvl24h = useSelector(tvl24) + const fees24h = useSelector(fees24) + const volumePlotData = useSelector(volumePlot) + const liquidityPlotData = useSelector(liquidityPlot) + const isLoadingStats = useSelector(isLoading) const currentNetwork = useSelector(networkType) useEffect(() => { @@ -95,11 +85,12 @@ export const WrappedStats: React.FC = () => { ({ - icon: tokenData.tokenDetails.logoURI, - name: tokenData.tokenDetails.name, - symbol: tokenData.tokenDetails.symbol, + icon: tokenData.tokenDetails?.logoURI, + name: tokenData.tokenDetails?.name, + symbol: tokenData.tokenDetails?.symbol, price: tokenData.price, - priceChange: tokenData.priceChange, + // priceChange: tokenData.priceChange, + priceChange: 0, volume: tokenData.volume24, TVL: tokenData.tvl }))} @@ -108,13 +99,15 @@ export const WrappedStats: React.FC = () => { Top pools ({ - symbolFrom: poolData.tokenXDetails.symbol, - symbolTo: poolData.tokenYDetails.symbol, - iconFrom: poolData.tokenXDetails.logoURI, - iconTo: poolData.tokenYDetails.logoURI, + symbolFrom: poolData.tokenXDetails?.symbol, + symbolTo: poolData.tokenYDetails?.symbol, + iconFrom: poolData.tokenXDetails?.logoURI, + iconTo: poolData.tokenYDetails?.logoURI, volume: poolData.volume24, TVL: poolData.tvl, - fee: poolData.fee + fee: poolData.fee, + addressFrom: poolData.tokenX, + addressTo: poolData.tokenY // apy: poolData.apy, // apyData: { // fees: poolData.apy, @@ -130,6 +123,7 @@ export const WrappedStats: React.FC = () => { // accumulatedFarmsAvg: accumulatedAverageAPY?.[poolData.poolAddress.toString()] ?? 0 // } }))} + network={currentNetwork} /> )} diff --git a/src/pages/StatsPage/index.tsx b/src/pages/StatsPage/index.tsx index a4f6ecca..95870a58 100644 --- a/src/pages/StatsPage/index.tsx +++ b/src/pages/StatsPage/index.tsx @@ -1,15 +1,13 @@ import { Grid } from '@mui/material' import { useStyles } from './styles' -import comingSoon from '../../static/png/coming-soon.png' -// import WrappedStats from '@containers/WrappedStats/WrappedStats' +import WrappedStats from '@containers/WrappedStats/WrappedStats' export const StatsPage: React.FC = () => { const { classes } = useStyles() return ( - Coming soon - {/* */} + ) } diff --git a/src/store/consts/types.ts b/src/store/consts/types.ts index 6772254a..ba5394a4 100644 --- a/src/store/consts/types.ts +++ b/src/store/consts/types.ts @@ -78,3 +78,18 @@ export enum Chain { AlephZero = 'Aleph Zero', Eclipse = 'Eclipse' } + +export interface SnapshotValueData { + tokenBNFromBeginning: string + usdValue24: number +} + +export interface PoolSnapshot { + timestamp: number + volumeX: SnapshotValueData + volumeY: SnapshotValueData + liquidityX: SnapshotValueData + liquidityY: SnapshotValueData + feeX: SnapshotValueData + feeY: SnapshotValueData +} diff --git a/src/store/reducers/stats.ts b/src/store/reducers/stats.ts index 1b792752..472237a7 100644 --- a/src/store/reducers/stats.ts +++ b/src/store/reducers/stats.ts @@ -14,13 +14,11 @@ export interface Value24H { export interface TokenStatsData { address: string price: number - priceChange: number volume24: number tvl: number } export interface PoolStatsData { - poolAddress: string tokenX: string tokenY: string fee: number diff --git a/src/store/sagas/index.ts b/src/store/sagas/index.ts index 6abd3e53..0ca38379 100644 --- a/src/store/sagas/index.ts +++ b/src/store/sagas/index.ts @@ -4,8 +4,11 @@ import { poolsSaga } from './pools' import { positionsSaga } from './positions' import { swapSaga } from './swap' import { walletSaga } from './wallet' +import { statsHandler } from './stats' function* rootSaga(): Generator { - yield all([connectionSaga, walletSaga, poolsSaga, positionsSaga, swapSaga].map(spawn)) + yield all( + [connectionSaga, walletSaga, poolsSaga, positionsSaga, swapSaga, statsHandler].map(spawn) + ) } export default rootSaga diff --git a/src/store/sagas/stats.ts b/src/store/sagas/stats.ts new file mode 100644 index 00000000..f9093f2f --- /dev/null +++ b/src/store/sagas/stats.ts @@ -0,0 +1,204 @@ +import { PoolKey } from '@invariant-labs/a0-sdk' +import { actions, PoolStatsData, TimeData, TokenStatsData } from '@store/reducers/stats' +import { actions as poolsActions } from '@store/reducers/pools' +import { networkType } from '@store/selectors/connection' +import { tokens } from '@store/selectors/pools' +import { address } from '@store/selectors/wallet' +import { getNetworkStats, getTokenDataByAddresses, printBigint } from '@utils/utils' +import { call, put, select, takeEvery } from 'typed-redux-saga' +import { getPSP22 } from './connection' + +export function* getStats(): Generator { + try { + const currentNetwork = yield* select(networkType) + const walletAddress = yield* select(address) + const psp22 = yield* getPSP22() + + const data = yield* call(getNetworkStats, currentNetwork.toLowerCase()) + + const volume24 = { + value: 0, + change: 0 + } + const tvl24 = { + value: 0, + change: 0 + } + const fees24 = { + value: 0, + change: 0 + } + + const tokensDataObject: Record = {} + let poolsData: PoolStatsData[] = [] + + const volumeForTimestamps: Record = {} + const liquidityForTimestamps: Record = {} + const feesForTimestamps: Record = {} + + const lastTimestamp = Math.max( + ...Object.values(data) + .filter(snaps => snaps.length > 0) + .map(snaps => +snaps[snaps.length - 1].timestamp) + ) + + Object.entries(data).forEach(([poolKey, snapshots]) => { + const parsedPoolKey: PoolKey = JSON.parse(poolKey) + + if (!tokensDataObject[parsedPoolKey.tokenX]) { + tokensDataObject[parsedPoolKey.tokenX] = { + address: parsedPoolKey.tokenX, + price: 0, + volume24: 0, + tvl: 0 + } + } + + if (!tokensDataObject[parsedPoolKey.tokenY]) { + tokensDataObject[parsedPoolKey.tokenY] = { + address: parsedPoolKey.tokenY, + price: 0, + volume24: 0, + tvl: 0 + } + } + + if (!snapshots.length) { + poolsData.push({ + volume24: 0, + tvl: 0, + tokenX: parsedPoolKey.tokenX, + tokenY: parsedPoolKey.tokenY, + fee: +printBigint(parsedPoolKey.feeTier.fee, 10n) + // apy: poolsApy[address] ?? 0, + }) + return + } + + const tokenX = parsedPoolKey.tokenX + const tokenY = parsedPoolKey.tokenY + + const lastSnapshot = snapshots[snapshots.length - 1] + + tokensDataObject[tokenX].volume24 += + lastSnapshot.timestamp === lastTimestamp ? lastSnapshot.volumeX.usdValue24 : 0 + tokensDataObject[tokenY].volume24 += + lastSnapshot.timestamp === lastTimestamp ? lastSnapshot.volumeY.usdValue24 : 0 + tokensDataObject[tokenX].tvl += lastSnapshot.liquidityX.usdValue24 + tokensDataObject[tokenY].tvl += lastSnapshot.liquidityY.usdValue24 + + poolsData.push({ + volume24: + lastSnapshot.timestamp === lastTimestamp + ? lastSnapshot.volumeX.usdValue24 + lastSnapshot.volumeY.usdValue24 + : 0, + tvl: + lastSnapshot.timestamp === lastTimestamp + ? lastSnapshot.liquidityX.usdValue24 + lastSnapshot.liquidityY.usdValue24 + : 0, + tokenX: parsedPoolKey.tokenX, + tokenY: parsedPoolKey.tokenY, + fee: +printBigint(parsedPoolKey.feeTier.fee, 10n) + // apy: poolsApy[address] ?? 0, + }) + + snapshots.slice(-30).forEach(snapshot => { + const timestamp = snapshot.timestamp.toString() + + if (!volumeForTimestamps[timestamp]) { + volumeForTimestamps[timestamp] = 0 + } + + if (!liquidityForTimestamps[timestamp]) { + liquidityForTimestamps[timestamp] = 0 + } + + if (!feesForTimestamps[timestamp]) { + feesForTimestamps[timestamp] = 0 + } + + volumeForTimestamps[timestamp] += snapshot.volumeX.usdValue24 + snapshot.volumeY.usdValue24 + liquidityForTimestamps[timestamp] += + snapshot.liquidityX.usdValue24 + snapshot.liquidityY.usdValue24 + feesForTimestamps[timestamp] += snapshot.feeX.usdValue24 + snapshot.feeY.usdValue24 + }) + }) + + const volumePlot: TimeData[] = Object.entries(volumeForTimestamps) + .map(([timestamp, value]) => ({ + timestamp: +timestamp, + value + })) + .sort((a, b) => a.timestamp - b.timestamp) + const liquidityPlot: TimeData[] = Object.entries(liquidityForTimestamps) + .map(([timestamp, value]) => ({ + timestamp: +timestamp, + value + })) + .sort((a, b) => a.timestamp - b.timestamp) + const feePlot: TimeData[] = Object.entries(feesForTimestamps) + .map(([timestamp, value]) => ({ + timestamp: +timestamp, + value + })) + .sort((a, b) => a.timestamp - b.timestamp) + + const tiersToOmit = [0.001, 0.003] + + poolsData = poolsData.filter(pool => !tiersToOmit.includes(pool.fee)) + + volume24.value = volumePlot.length ? volumePlot[volumePlot.length - 1].value : 0 + tvl24.value = liquidityPlot.length ? liquidityPlot[liquidityPlot.length - 1].value : 0 + fees24.value = feePlot.length ? feePlot[feePlot.length - 1].value : 0 + + const prevVolume24 = volumePlot.length > 1 ? volumePlot[volumePlot.length - 2].value : 0 + const prevTvl24 = liquidityPlot.length > 1 ? liquidityPlot[liquidityPlot.length - 2].value : 0 + const prevFees24 = feePlot.length > 1 ? feePlot[feePlot.length - 2].value : 0 + + volume24.change = prevVolume24 ? ((volume24.value - prevVolume24) / prevVolume24) * 100 : 0 + tvl24.change = prevTvl24 ? ((tvl24.value - prevTvl24) / prevTvl24) * 100 : 0 + fees24.change = prevFees24 ? ((fees24.value - prevFees24) / prevFees24) * 100 : 0 + + yield* put( + actions.setCurrentStats({ + volume24, + tvl24, + fees24, + tokensData: Object.values(tokensDataObject), + poolsData, + volumePlot, + liquidityPlot + }) + ) + + const allTokens = yield* select(tokens) + + const unknownTokens = new Set() + + Object.keys(data).forEach(poolKey => { + const parsedPoolKey: PoolKey = JSON.parse(poolKey) + + if (!allTokens[parsedPoolKey.tokenX]) { + unknownTokens.add(parsedPoolKey.tokenX) + } + + if (!allTokens[parsedPoolKey.tokenY]) { + unknownTokens.add(parsedPoolKey.tokenY) + } + }) + + const unknownTokensData = yield* call( + getTokenDataByAddresses, + [...unknownTokens], + psp22, + walletAddress + ) + yield* put(poolsActions.addTokens(unknownTokensData)) + } catch (error) { + console.log(error) + } +} + +export function* statsHandler(): Generator { + yield* takeEvery(actions.getCurrentStats, getStats) +} diff --git a/src/utils/utils.ts b/src/utils/utils.ts index ca126cab..bf622ca6 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -62,6 +62,7 @@ import { BestTier, CoinGeckoAPIData, FormatNumberThreshold, + PoolSnapshot, PrefixConfig, Token, TokenPriceData @@ -1186,3 +1187,11 @@ export const findClosestIndexByValue = (arr: number[], value: number): number => } return high } + +export const getNetworkStats = async (name: string): Promise> => { + const { data } = await axios.get>( + `https://stats.invariant.app/a0/full/${name}` + ) + + return data +}