diff --git a/cypress/component/liquidity.test.tsx b/cypress/component/liquidity.test.tsx index 653c8e2a..c372e322 100644 --- a/cypress/component/liquidity.test.tsx +++ b/cypress/component/liquidity.test.tsx @@ -1,7 +1,7 @@ import '../../styles/globals.css'; import MockRouter from '../utils/router'; import Providers from 'components/Providers/Providers'; -import AddLiquidityComponent from 'components/Liquidity/Add/AddLiquidityComponent'; +import AddLiquidityComponent from 'components/Pools/Add/AddLiquidityComponent'; import { mockedFreighterConnector, sleep, testnetXLM } from '../utils/utils'; import { useApiTokens } from 'hooks/tokens/useApiTokens'; diff --git a/cypress/e2e/flows.test.ts b/cypress/e2e/flows.test.ts index 05834980..5476ea97 100644 --- a/cypress/e2e/flows.test.ts +++ b/cypress/e2e/flows.test.ts @@ -253,9 +253,9 @@ describe('Navigation flow', () => { cy.visit('/'); cy.get('[data-testid="navbar__container"]').click(); // cy.wait(1500); - cy.contains('Liquidity').click(); + cy.contains('Pools').click(); // cy.wait(3000); - cy.contains('List of your liquidity positions'); + cy.contains('Liquidity pools'); }); it('should navigate to bridge', () => { cy.visit('/'); diff --git a/package.json b/package.json index 614a6199..6feaef0f 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "react-window": "^1.8.9", "redux": "^4.2.1", "soroswap-router-sdk": "1.4.6", - "soroswap-ui": "^1.0.0", + "soroswap-ui": "^1.1.0", "swr": "^2.2.0", "typescript": "5.3.3", "use-resize-observer": "^9.1.0" diff --git a/pages/liquidity/add/[...tokens].tsx b/pages/pools/add/[...tokens].tsx similarity index 90% rename from pages/liquidity/add/[...tokens].tsx rename to pages/pools/add/[...tokens].tsx index f9af2c2f..fde30c7f 100644 --- a/pages/liquidity/add/[...tokens].tsx +++ b/pages/pools/add/[...tokens].tsx @@ -1,5 +1,5 @@ import { useSorobanReact } from '@soroban-react/core'; -import AddLiquidityComponent from 'components/Liquidity/Add/AddLiquidityComponent'; +import AddLiquidityComponent from 'components/Pools/Add/AddLiquidityComponent'; import SEO from 'components/SEO'; import { xlmTokenList } from 'constants/xlmToken'; import { useRouter } from 'next/router'; diff --git a/pages/liquidity/add/index.tsx b/pages/pools/add/index.tsx similarity index 79% rename from pages/liquidity/add/index.tsx rename to pages/pools/add/index.tsx index 671bc744..624881c8 100644 --- a/pages/liquidity/add/index.tsx +++ b/pages/pools/add/index.tsx @@ -1,4 +1,4 @@ -import AddLiquidityComponent from 'components/Liquidity/Add/AddLiquidityComponent'; +import AddLiquidityComponent from 'components/Pools/Add/AddLiquidityComponent'; import SEO from 'components/SEO'; import { useApiTokens } from 'hooks/tokens/useApiTokens'; import { useRouter } from 'next/router'; @@ -12,7 +12,7 @@ export default function AddLiquidityPage() { if (!tokens) return; const xlm = tokens.find((token) => token.code === 'XLM'); - if (xlm) router.push(`/liquidity/add/${xlm.contract}`); + if (xlm) router.push(`/pools/add/${xlm.contract}`); }, [tokens, router]); return ( diff --git a/pages/liquidity/index.tsx b/pages/pools/index.tsx similarity index 84% rename from pages/liquidity/index.tsx rename to pages/pools/index.tsx index dcf4c82a..e0058020 100644 --- a/pages/liquidity/index.tsx +++ b/pages/pools/index.tsx @@ -3,8 +3,8 @@ import { useSorobanReact } from '@soroban-react/core'; import { ButtonPrimary } from 'components/Buttons/Button'; import { WalletButton } from 'components/Buttons/WalletButton'; import { AutoColumn } from 'components/Column'; -import LiquidityPoolInfoModal from 'components/Liquidity/LiquidityPoolInfoModal'; -import { LPPercentage } from 'components/Liquidity/styleds'; +import LiquidityPoolInfoModal from 'components/Pools/LiquidityPoolInfoModal'; +import { LPPercentage } from 'components/Pools/styleds'; import CurrencyLogo from 'components/Logo/CurrencyLogo'; import { Dots } from 'components/Pool/styleds'; import { AutoRow } from 'components/Row'; @@ -18,6 +18,9 @@ import { useContext, useState } from 'react'; import SEO from '../../src/components/SEO'; import { DEFAULT_SLIPPAGE_INPUT_VALUE } from 'components/Settings/MaxSlippageSettings'; +import { PoolsTable } from 'components/Pools/PoolsTable'; + + const PageWrapper = styled(AutoColumn)` position: relative; background: ${({ theme }) => `linear-gradient(${theme.palette.customBackground.bg2}, ${theme.palette.customBackground.bg2 @@ -29,8 +32,8 @@ const PageWrapper = styled(AutoColumn)` border-radius: 16px; padding: 32px; transition: transform 250ms ease; - max-width: 875px; - width: 100%; + max-width: 99vw; + width: 95vw; display: flex; flex-direction: column; align-items: center; @@ -76,6 +79,7 @@ const StatusWrapper = styled('div')` border-radius: 16px; `; + export default function LiquidityPage() { const sorobanContext = useSorobanReact(); const { address } = sorobanContext; @@ -102,15 +106,19 @@ export default function LiquidityPage() { return ( <> - +
- Your liquidity - + Liquidity Pools + {address ? ( + router.push('/pools/add')}> + + Add Liquidity + + ) : ( + + )} -
- List of your liquidity positions -
+
{!address ? ( @@ -128,6 +136,13 @@ export default function LiquidityPage() { ) : lpTokens && lpTokens?.length > 0 ? ( + + Your liquidity + + +
+ List of your liquidity positions +
{lpTokens.map((obj: any, index: number) => ( handleLPClick(obj)} key={index}> @@ -154,7 +169,7 @@ export default function LiquidityPage() {
)} {address ? ( - router.push('/liquidity/add')}> + router.push('/pools/add')}> + Add Liquidity ) : ( diff --git a/pages/liquidity/remove/[...tokens].tsx b/pages/pools/remove/[...tokens].tsx similarity index 70% rename from pages/liquidity/remove/[...tokens].tsx rename to pages/pools/remove/[...tokens].tsx index 96909d9c..e4943433 100644 --- a/pages/liquidity/remove/[...tokens].tsx +++ b/pages/pools/remove/[...tokens].tsx @@ -1,4 +1,4 @@ -import RemoveLiquidityComponent from "components/Liquidity/Remove/RemoveLiquidityComponent"; +import RemoveLiquidityComponent from "components/Pools/Remove/RemoveLiquidityComponent"; import SEO from "components/SEO"; export default function MintPage() { diff --git a/src/components/Bridge/BridgeConfirmModal.tsx b/src/components/Bridge/BridgeConfirmModal.tsx index 23b0cc4d..881d3f5a 100644 --- a/src/components/Bridge/BridgeConfirmModal.tsx +++ b/src/components/Bridge/BridgeConfirmModal.tsx @@ -14,8 +14,8 @@ import { ButtonPrimary } from 'components/Buttons/Button'; import { CloseButton } from 'components/Buttons/CloseButton'; import { AutoColumn } from 'components/Column'; import CopyTxHash from 'components/CopyTxHash/CopyTxHash'; -import { DetailRowValue } from 'components/Liquidity/Add/AddModalFooter'; -import { Label } from 'components/Liquidity/Add/AddModalHeader'; +import { DetailRowValue } from 'components/Pools/Add/AddModalFooter'; +import { Label } from 'components/Pools/Add/AddModalHeader'; import { AnimatedEntranceConfirmationIcon, LoadingIndicatorOverlay, diff --git a/src/components/Liquidity/Add/AddLiquidityComponent.tsx b/src/components/Pools/Add/AddLiquidityComponent.tsx similarity index 99% rename from src/components/Liquidity/Add/AddLiquidityComponent.tsx rename to src/components/Pools/Add/AddLiquidityComponent.tsx index cd1bfa46..dabd9fb7 100644 --- a/src/components/Liquidity/Add/AddLiquidityComponent.tsx +++ b/src/components/Pools/Add/AddLiquidityComponent.tsx @@ -271,7 +271,7 @@ export default function AddLiquidityComponent({ handleAddLiquidity, ]); - const baseRoute = `/liquidity/add/`; + const baseRoute = `/pools/add/`; const handleCurrencyASelect = useCallback( (currencyA: TokenType) => { diff --git a/src/components/Liquidity/Add/AddModalFooter.tsx b/src/components/Pools/Add/AddModalFooter.tsx similarity index 100% rename from src/components/Liquidity/Add/AddModalFooter.tsx rename to src/components/Pools/Add/AddModalFooter.tsx diff --git a/src/components/Liquidity/Add/AddModalHeader.tsx b/src/components/Pools/Add/AddModalHeader.tsx similarity index 100% rename from src/components/Liquidity/Add/AddModalHeader.tsx rename to src/components/Pools/Add/AddModalHeader.tsx diff --git a/src/components/Liquidity/AddRemoveHeader.tsx b/src/components/Pools/AddRemoveHeader.tsx similarity index 99% rename from src/components/Liquidity/AddRemoveHeader.tsx rename to src/components/Pools/AddRemoveHeader.tsx index 2fca843c..b626625e 100644 --- a/src/components/Liquidity/AddRemoveHeader.tsx +++ b/src/components/Pools/AddRemoveHeader.tsx @@ -87,7 +87,7 @@ export function AddRemoveTabs({ { // if (adding) { // // not 100% sure both of these are needed diff --git a/src/components/Liquidity/LiquidityPoolInfoModal.tsx b/src/components/Pools/LiquidityPoolInfoModal.tsx similarity index 94% rename from src/components/Liquidity/LiquidityPoolInfoModal.tsx rename to src/components/Pools/LiquidityPoolInfoModal.tsx index e99b5445..b889963a 100644 --- a/src/components/Liquidity/LiquidityPoolInfoModal.tsx +++ b/src/components/Pools/LiquidityPoolInfoModal.tsx @@ -12,7 +12,7 @@ import { formatTokenAmount } from 'helpers/format'; import { useRouter } from 'next/router'; import { LPPercentage } from './styleds'; -const ContentWrapper = styled('div')<{ isMobile: boolean }>` +const ContentWrapper = styled('div') <{ isMobile: boolean }>` display: flex; flex-direction: column; gap: 24px; @@ -37,12 +37,12 @@ export default function LiquidityPoolInfoModal({ if (!selectedLP) return null; const handleAddClick = () => { - router.push(`/liquidity/add/${selectedLP.token_0?.contract}/${selectedLP.token_1?.contract}`); + router.push(`/pools/add/${selectedLP.token_0?.contract}/${selectedLP.token_1?.contract}`); }; const handleRemoveClick = () => { router.push( - `/liquidity/remove/${selectedLP.token_0?.contract}/${selectedLP.token_1?.contract}`, + `/pools/remove/${selectedLP.token_0?.contract}/${selectedLP.token_1?.contract}`, ); }; diff --git a/src/components/Pools/PoolsTable.tsx b/src/components/Pools/PoolsTable.tsx new file mode 100644 index 00000000..b7e9158f --- /dev/null +++ b/src/components/Pools/PoolsTable.tsx @@ -0,0 +1,431 @@ +import { useEffect, useState } from 'react'; +import { useSorobanReact } from '@soroban-react/core'; +import { useRouter } from 'next/router'; + +import { TokenType } from 'interfaces'; + +import useGetMyBalances from 'hooks/useGetMyBalances'; +import useGetLpTokens from 'hooks/useGetLpTokens'; +import useTable from 'hooks/useTable'; + +import { LpTokensObj } from 'functions/getLpTokens'; +import { formatNumberToMoney, shouldShortenCode } from 'helpers/utils'; + +import LiquidityPoolInfoModal from 'components/Pools/LiquidityPoolInfoModal'; +import { ButtonPrimary } from 'components/Buttons/Button'; +import CurrencyLogo from 'components/Logo/CurrencyLogo'; +import { + Box, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TablePagination, + TableRow, + TableSortLabel, + Card, + Skeleton, + styled +} from 'soroswap-ui'; +import { visuallyHidden } from '@mui/utils'; +import { fetchTokens } from 'services/tokens'; + + +interface PoolData { + address: string; + fees24h: number; + feesChartData: any[]; + feesYearly: number; + reserveA: string; + reserveB: string; + tokenA: TokenType; + tokenAPrice: number; + tokenB: TokenType; + tokenBPrice: number; + tvl: number; + tvlChartData: any[]; + volume7d: number; + volume24h: number; + volumeChartData: any[]; + totalShares: number; +} + +interface PoolsCell { + name: string; + address: string; + tvl: string; + volume24h: string; + volume7d: string; + fees24h: string; + feesYearly: string; + totalShares: string; +} + +interface HeadCell { + id: keyof PoolsCell; + label: string; + numeric: boolean; + align: 'left' | 'center' | 'right'; +} + +const headCells: readonly HeadCell[] = [ + { + id: 'name', + numeric: false, + label: 'Pool', + align: 'left', + }, + { + id: 'tvl', + numeric: false, + label: 'TVL', + align: 'center', + }, + { + id: 'volume24h', + numeric: false, + label: 'Volume 24h', + align: 'center', + }, + { + id: 'volume7d', + numeric: true, + label: 'Volume 7d', + align: 'center', + }, + { + id: 'fees24h', + numeric: true, + label: 'Fees 24h', + align: 'center', + }, + { + id: 'feesYearly', + numeric: true, + label: 'Fees Yearly', + align: 'center', + }, + { + id: 'totalShares', + numeric: true, + label: 'Shares', + align: 'center' + }, +]; +interface BalancesTableProps { + onRequestSort: ( + event: React.MouseEvent, + property: keyof PoolsCell | 'shares', + ) => void; + order: 'asc' | 'desc'; + orderBy: string; +} +export const StyledTableCell = styled(TableCell)(({ theme }) => ({ + border: 0, + height: 10, +})); + +function PoolsTableHead(props: BalancesTableProps) { + const { order, orderBy, onRequestSort } = props; + const createSortHandler = + (property: keyof PoolsCell | 'shares') => (event: React.MouseEvent) => { + onRequestSort(event, property); + }; + const { address } = useSorobanReact(); + return ( + + + {headCells.map((headCell) => { + if (headCell.id === 'totalShares' && !address) return null; + return ( + + + {headCell.label} + {orderBy === headCell.id ? ( + + {order === 'desc' ? 'sorted descending' : 'sorted ascending'} + + ) : null} + + + ) + } + )} + {address && Action} + + + ); +} + +export function PoolsTable(props: any) { + const { nativeToken } = props; + const { tokens, tokenBalancesResponse } = useGetMyBalances(); + const { lpTokens, isLoading, mutate } = useGetLpTokens(); + const router = useRouter(); + const { address } = useSorobanReact(); + const { activeChain } = useSorobanReact() + const [rows, setRows] = useState([]); + const [loadingRows, setLoadingRows] = useState(true); + const [loadingShares, setLoadingShares] = useState(true); + const [isModalOpen, setModalOpen] = useState(false); + const [selectedLP, setSelectedLP] = useState(); + + const { + order, + orderBy, + handleRequestSort, + visibleRows, + emptyRows, + rowsPerPage, + page, + handleChangePage, + handleChangeRowsPerPage, + } = useTable({ + rows, + defaultOrder: 'desc', + defaultOrderBy: 'tvl', + defaultRowsPerPage: 5, + }); + + const filterTokens = async (rows: any) => { + if (!activeChain) return; + let allowedTokens = await fetchTokens(activeChain?.id).then((data) => { + switch (activeChain?.id) { + case 'testnet': + return data[1].assets; + case 'mainnet': + return data.assets; + default: + return data; + } + }); + const filteredRows = rows.filter((row: any) => + allowedTokens.some((token: any) => token.contract === row.tokenA.contract) && + allowedTokens.some((token: any) => token.contract === row.tokenB.contract) || + allowedTokens.some((token: any) => token.contract === row.tokenB.contract) && + allowedTokens.some((token: any) => token.contract === row.tokenA.contract) + ); + return filteredRows; + } + + useEffect(() => { + const fetchPools = async () => { + const response = await fetch(`https://info.soroswap.finance/api/pairs?network=${activeChain?.id.toUpperCase()}`); + if (address && lpTokens) { + response.json().then(async (data) => { + const pools = data.map((pool: any) => { + const tempShare = lpTokens?.find((lpToken: any) => lpToken.address === pool.address)?.balance; + return { + ...pool, + totalShares: tempShare ?? 0, + }; + }); + const filteredTokens = await filterTokens(pools); + setRows(filteredTokens); + setLoadingShares(false); + }) + } else { + response.json().then(async (data) => { + const filteredTokens = await filterTokens(data); + setRows(filteredTokens); + }) + } + setLoadingRows(false); + } + fetchPools(); + }, [activeChain, address, lpTokens]); + + + const handleLPClick = (obj: any) => { + const parsedData: LpTokensObj = { + token_0: obj.tokenA, + token_1: obj.tokenB, + balance: obj.totalShares, + lpPercentage: obj.totalShares, + status: 'Active', + reserve0: obj.reserveA, + reserve1: obj.reserveB, + totalShares: obj.totalShares, + myReserve0: obj.reserveA, + myReserve1: obj.reserveB, + }; + setSelectedLP(parsedData); + setModalOpen(true); + }; + + if (rows.length === 0 && !loadingRows) { + return No pools found.; + } + if (rows.length === 0 && loadingRows) { + const skeletonRow = () => { + return ( + + + + + + + + + + + + + + + + + + + + {address && ( + <> + + + + + + + + + )} + + ) + } + return ( + + + + + {skeletonRow()} + {skeletonRow()} + {skeletonRow()} + {skeletonRow()} + {skeletonRow()} + +
+
) + } + + const tableButtonStyle = { height: 10, fontSize: 12, width: 120, justifySelf: 'center', borderRadius: 16 } + return ( + + + + + {visibleRows.map((row, index) => { + return ( + + + + token.contract === row.tokenA.contract)} + size={'16px'} + style={{ marginRight: '4px' }} + /> + token.contract === row.tokenB.contract)} + size={'16px'} + style={{ marginRight: '8px' }} + /> + + + {shouldShortenCode(row.tokenA?.code)} /{" "} + {shouldShortenCode(row.tokenB?.code)} + + + {formatNumberToMoney(row.tvl, 2)} + + + {formatNumberToMoney(row.volume24h / 10 ** 7, 2)} + + + {formatNumberToMoney(row.volume7d / 10 ** 7, 2)} + + + {formatNumberToMoney(row.fees24h / 10 ** 7, 2)} + + + {formatNumberToMoney(row.feesYearly / 10 ** 7, 2)} + + {address && ( + + {(loadingShares || isLoading) ? + : formatNumberToMoney(row.totalShares / 10 ** 7, 2) + } + + )} + {address && ( + + {(loadingShares || isLoading) && + + } + {row.totalShares === 0 && ( + router.push(`pools/add/${row.tokenA.contract}/${row.tokenB.contract}`)}> + + Add Liquidity + + )} + {row.totalShares > 0 && ( + handleLPClick(row)}> + Manage + + )} + + )} + + ); + })} + {emptyRows > 0 && ( + + + + )} + +
+ + setModalOpen(false)} + /> +
+ ); +} \ No newline at end of file diff --git a/src/components/Liquidity/Remove/RemoveLiquidityComponent.tsx b/src/components/Pools/Remove/RemoveLiquidityComponent.tsx similarity index 99% rename from src/components/Liquidity/Remove/RemoveLiquidityComponent.tsx rename to src/components/Pools/Remove/RemoveLiquidityComponent.tsx index 0f0eaabb..f7598763 100644 --- a/src/components/Liquidity/Remove/RemoveLiquidityComponent.tsx +++ b/src/components/Pools/Remove/RemoveLiquidityComponent.tsx @@ -144,7 +144,7 @@ export default function RemoveLiquidityComponent() { // if there was a tx hash, we want to clear the input if (txHash) { onUserInput(Field.LIQUIDITY_PERCENT, ''); - router.push('/liquidity'); + router.push('/pools'); } setTxHash(undefined); }, [onUserInput, router, txHash]); diff --git a/src/components/Liquidity/styleds.tsx b/src/components/Pools/styleds.tsx similarity index 100% rename from src/components/Liquidity/styleds.tsx rename to src/components/Pools/styleds.tsx diff --git a/src/functions/getLpTokens.tsx b/src/functions/getLpTokens.tsx index 3e2ebb51..9aabb5a3 100644 --- a/src/functions/getLpTokens.tsx +++ b/src/functions/getLpTokens.tsx @@ -44,7 +44,7 @@ const getLpResultsFromBackendPairs = async ( if (pairLpTokens != 0) { const token_0 = await findToken(element.tokenA, tokensAsMap, sorobanContext); const token_1 = await findToken(element.tokenB, tokensAsMap, sorobanContext); - + const address = element.address; const totalShares = await getTotalShares(element.address, sorobanContext); const lpPercentage = BigNumber(pairLpTokens as BigNumber) @@ -62,6 +62,7 @@ const getLpResultsFromBackendPairs = async ( .dividedBy(Number(totalShares)); const toReturn = { + address, token_0, token_1, balance: pairLpTokens, diff --git a/src/helpers/utils.tsx b/src/helpers/utils.tsx index 7aa35a54..34484d74 100644 --- a/src/helpers/utils.tsx +++ b/src/helpers/utils.tsx @@ -1,6 +1,7 @@ import BigNumber from 'bignumber.js'; import * as StellarSdk from '@stellar/stellar-sdk'; import { I128 } from './xdr'; +import { shortenAddress } from './address'; let xdr = StellarSdk.xdr; @@ -203,3 +204,33 @@ export function bigNumberToU32(value: BigNumber): StellarSdk.xdr.ScVal { return xdr.ScVal.scvU32(num); } + +export const formatNumberToMoney = ( + number: number | undefined, + decimals: number = 7 +) => { + if (!number) return "-"; + + if (typeof number === "string") { + number = parseFloat(number); + } + + if (typeof number !== "number") return "$0.00"; + + if (number > 1000000000) { + return `$${(number / 1000000000).toFixed(2)}b`; + } + if (number > 1000000) { + return `$${(number / 1000000).toFixed(2)}m`; + } + if (number > 1000) { + return `$${(number / 1000).toFixed(2)}k`; + } + return `$${number.toFixed(decimals)}`; +}; + +export const shouldShortenCode = (contract: string) => { + if (!contract) return; + if (contract.length > 10) return shortenAddress(contract); + return contract; +}; diff --git a/yarn.lock b/yarn.lock index 1f87f26b..7289a8db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8120,10 +8120,10 @@ soroswap-router-sdk@1.4.6: tiny-invariant "^1.3.1" toformat "^2.0.0" -soroswap-ui@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/soroswap-ui/-/soroswap-ui-1.0.0.tgz#38e141745ef10c73dd287ac9a4f5a4c5db1e0085" - integrity sha512-50YxrEv+JDL4V+zgLgYrulk05zeWa0r6jtuMMxCDJHYbzxI8Klsd1Sa4eUa6FHeQ66HlwpVEwXrUReYeAK1C8A== +soroswap-ui@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/soroswap-ui/-/soroswap-ui-1.1.0.tgz#b8a78311cf9f7f64f5f6b42dcdb25f1093c43cb3" + integrity sha512-igV11/zIt+0joH8q9SCVi4oZWDTTUkHfft+3uae63HwbtV+oLz+vTn0CBy0PHhvOv5Hy0pBWkpnTsjwkil0VXA== dependencies: "@emotion/react" "^11.11.4" "@emotion/styled" "^11.11.5"