From dc9de1f2145d97ea233069c3ea9c744fca0de880 Mon Sep 17 00:00:00 2001 From: maro Date: Fri, 26 May 2023 16:19:04 +0900 Subject: [PATCH 01/54] feat: dezswap api integration - defined types. - refactored hooks. - used tanstack query and removed unnecessary atoms. - reduced unnecessary API calls. --- package.json | 2 +- src/App.tsx | 39 +-- src/api/index.ts | 45 ++++ .../Modal/TxBroadcastingModal/index.tsx | 4 +- src/components/Select/index.tsx | 2 +- src/components/SelectAssetForm/AssetItem.tsx | 251 ++++++++++++++++++ src/components/SelectAssetForm/index.tsx | 238 ++--------------- src/constants/network.ts | 20 +- src/hooks/modals/useConfirmationModal.tsx | 2 +- src/hooks/modals/useConnectWalletModal.tsx | 2 +- src/hooks/modals/useFirstProvideModal.tsx | 2 +- src/hooks/modals/useInvalidPathModal.tsx | 2 +- src/hooks/modals/useTxBroadcastingModal.tsx | 2 +- src/hooks/useAPI.ts | 206 ++++---------- src/hooks/useAssets.ts | 165 ++---------- src/hooks/useBalance.ts | 82 +++--- src/hooks/useBalanceMinusFee.ts | 16 +- src/hooks/useBookmark.ts | 2 +- src/hooks/useCustomAssets.ts | 118 ++++---- src/hooks/useFee.ts | 6 +- src/hooks/useIsInViewport.ts | 27 ++ src/hooks/useLCDClient.ts | 6 +- src/hooks/useLatestBlock.ts | 32 +-- src/hooks/useModal.ts | 4 +- src/hooks/useNetwork.ts | 4 +- src/hooks/usePair.ts | 189 ------------- src/hooks/usePairBookmark.ts | 2 +- src/hooks/usePairs.ts | 105 ++++++++ src/hooks/usePool.ts | 4 +- src/hooks/usePools.ts | 16 ++ src/hooks/useVerifiedAssets.ts | 31 +++ src/layout/Main/Footer.tsx | 2 +- src/layout/Main/Header.tsx | 6 +- src/layout/Main/index.tsx | 2 +- src/main.tsx | 2 +- src/pages/Pool/Create/InputGroup.tsx | 17 +- src/pages/Pool/Create/index.tsx | 34 +-- src/pages/Pool/ImportAssetModal.tsx | 42 ++- src/pages/Pool/PoolForm.tsx | 40 ++- src/pages/Pool/PoolItem.tsx | 33 ++- src/pages/Pool/PoolList.tsx | 10 +- src/pages/Pool/Provide/InputGroup.tsx | 17 +- src/pages/Pool/Provide/index.tsx | 50 ++-- src/pages/Pool/Select.tsx | 2 +- src/pages/Pool/Withdraw/InputGroup.tsx | 10 +- src/pages/Pool/Withdraw/index.tsx | 51 ++-- src/pages/Pool/Withdraw/useSimulate.ts | 2 +- src/pages/Pool/index.tsx | 100 +++---- src/pages/Trade/Swap/index.tsx | 26 +- src/pages/Trade/Swap/useSimulate.ts | 8 +- src/stores/assets.ts | 20 +- src/stores/pairs.ts | 11 +- src/types/api.d.ts | 45 ++++ src/types/common.d.ts | 13 +- src/types/factory.d.ts | 5 - src/types/router.d.ts | 3 - src/utils/index.ts | 2 +- yarn.lock | 93 ++----- 58 files changed, 1024 insertions(+), 1248 deletions(-) create mode 100644 src/api/index.ts create mode 100644 src/components/SelectAssetForm/AssetItem.tsx create mode 100644 src/hooks/useIsInViewport.ts delete mode 100644 src/hooks/usePair.ts create mode 100644 src/hooks/usePairs.ts create mode 100644 src/hooks/usePools.ts create mode 100644 src/hooks/useVerifiedAssets.ts create mode 100644 src/types/api.d.ts delete mode 100644 src/types/factory.d.ts delete mode 100644 src/types/router.d.ts diff --git a/package.json b/package.json index 33745600..73653cb9 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", "@rollup/plugin-inject": "^5.0.3", + "@tanstack/react-query": "^4.29.7", "@tippyjs/react": "^4.2.6", "@xpla/wallet-controller": "^0.4.1", "@xpla/wallet-provider": "^0.4.1", @@ -38,7 +39,6 @@ "react-grid-system": "^8.1.6", "react-hook-form": "^7.40.0", "react-modal": "^3.16.1", - "react-query": "^3.39.2", "react-router-dom": "^6.4.3", "react-tiny-popover": "^7.2.0", "resize-observer-polyfill": "^1.5.1", diff --git a/src/App.tsx b/src/App.tsx index 5f849ffe..b3163f5e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,25 +7,17 @@ import { useScreenClass, } from "react-grid-system"; import { gridConfiguration, SCREEN_CLASSES } from "constants/layout"; -import { useAtomValue, useSetAtom } from "jotai"; -import { verifiedAssetsAtom, verifiedIbcAssetsAtom } from "stores/assets"; -import { useAPI } from "hooks/useAPI"; +import { useAtomValue } from "jotai"; import MainLayout from "layout/Main"; import disclaimerLastSeenAtom from "stores/disclaimer"; import DisclaimerModal from "components/Modal/DisclaimerModal"; import globalElementsAtom from "stores/globalElements"; -import { VerifiedIbcAssets } from "types/token"; -import { useNetwork } from "./hooks/useNetwork"; setGridConfiguration(gridConfiguration); function App() { - const setKnownAssets = useSetAtom(verifiedAssetsAtom); - const setKnownIbcAssets = useSetAtom(verifiedIbcAssetsAtom); const disclaimerLastSeen = useAtomValue(disclaimerLastSeenAtom); const globalElements = useAtomValue(globalElementsAtom); - const api = useAPI(); - const network = useNetwork(); const screenClass = useScreenClass(); const isDisclaimerAgreed = useMemo(() => { if (!disclaimerLastSeen) return false; @@ -51,35 +43,6 @@ function App() { }); }, []); - useEffect(() => { - api.getVerifiedTokenInfo().then((assets) => { - setKnownAssets(assets); - }); - api.getVerifiedIbcTokenInfo().then((ibcAssets: VerifiedIbcAssets) => { - const updatedIbcAssets = ibcAssets[network.name]; - if (updatedIbcAssets) { - Promise.all( - Object.entries(updatedIbcAssets).map(([k, v]) => - api.getDecimal(v?.denom || "").then((d) => { - const asset = - updatedIbcAssets && updatedIbcAssets?.[k] - ? updatedIbcAssets?.[k] - : undefined; - if (d && asset) { - asset.decimals = d; - } - }), - ), - ).then(() => { - setKnownIbcAssets((current) => ({ - ...current, - [network.name]: updatedIbcAssets, - })); - }); - } - }); - }, [api, setKnownAssets, setKnownIbcAssets]); - const renderRoute = useCallback( ({ children, element, index, ...props }: RouteObject) => { return ( diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 00000000..0110310c --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,45 @@ +import axios from "axios"; +import { apiAddresses } from "constants/dezswap"; +import { Pair, Pairs, Pool, Token } from "types/api"; +import { NetworkName } from "types/common"; + +export type ApiVersion = "v1"; + +const api = (networkName: NetworkName, version: ApiVersion = "v1") => { + const apiClient = axios.create({ + baseURL: `${apiAddresses[networkName]?.baseUrl || ""}/${version}`, + }); + + if (version === "v1") { + return { + async getPairs() { + const res = await apiClient.get(`/pairs`); + return res.data; + }, + async getPair(address: string) { + const res = await apiClient.get(`/pairs/${address}`); + return res.data; + }, + async getPools() { + const res = await apiClient.get(`/pools`); + return res.data; + }, + async getPool(address: string) { + const res = await apiClient.get(`/pools/${address}`); + return res.data; + }, + async getTokens() { + const res = await apiClient.get(`/tokens`); + return res.data; + }, + async getToken(address: string) { + const res = await apiClient.get(`/tokens/${address}`); + return res.data; + }, + }; + } + + throw new Error(`Unsupported API version: ${version}`); +}; + +export default api; diff --git a/src/components/Modal/TxBroadcastingModal/index.tsx b/src/components/Modal/TxBroadcastingModal/index.tsx index 476ebfc8..1570d427 100644 --- a/src/components/Modal/TxBroadcastingModal/index.tsx +++ b/src/components/Modal/TxBroadcastingModal/index.tsx @@ -9,10 +9,10 @@ import { MouseEventHandler, useEffect, useMemo, useState } from "react"; import { ellipsisCenter, getTransactionLink } from "utils"; import { TxInfo } from "@xpla/xpla.js"; import { TxError } from "types/common"; -import { useLCDClient } from "hooks/useLCDClient"; +import useLCDClient from "hooks/useLCDClient"; import Panel from "components/Panel"; import Modal from "components/Modal"; -import { useNetwork } from "hooks/useNetwork"; +import useNetwork from "hooks/useNetwork"; import Typography from "components/Typography"; import { Col, Row, useScreenClass } from "react-grid-system"; import IconButton from "components/IconButton"; diff --git a/src/components/Select/index.tsx b/src/components/Select/index.tsx index d3ab8981..26fe9ce7 100644 --- a/src/components/Select/index.tsx +++ b/src/components/Select/index.tsx @@ -4,7 +4,7 @@ import { useMemo } from "react"; import iconDropdown from "assets/icons/icon-dropdown-arrow.svg"; import Typography from "components/Typography"; import { css } from "@emotion/react"; -import { useModal } from "hooks/useModal"; +import useModal from "hooks/useModal"; type Value = string | number; diff --git a/src/components/SelectAssetForm/AssetItem.tsx b/src/components/SelectAssetForm/AssetItem.tsx new file mode 100644 index 00000000..fdfcdbdc --- /dev/null +++ b/src/components/SelectAssetForm/AssetItem.tsx @@ -0,0 +1,251 @@ +import { css, useTheme } from "@emotion/react"; +import styled from "@emotion/styled"; +import IconButton from "components/IconButton"; +import Tooltip from "components/Tooltip"; +import Typography from "components/Typography"; +import { MOBILE_SCREEN_CLASS } from "constants/layout"; +import useBalance from "hooks/useBalance"; +import useIsInViewport from "hooks/useIsInViewport"; +import { useRef } from "react"; +import { Token } from "types/api"; +import { formatNumber, cutDecimal, amountToValue, ellipsisCenter } from "utils"; + +import iconToken from "assets/icons/icon-default-token.svg"; +import iconVerified from "assets/icons/icon-verified.svg"; +import iconBookmark from "assets/icons/icon-bookmark-default.svg"; +import iconBookmarkSelected from "assets/icons/icon-bookmark-selected.svg"; + +interface WrapperProps { + selected?: boolean; + invisible?: boolean; +} +const Wrapper = styled.div` + display: flex; + justify-content: flex-start; + align-items: center; + column-gap: 6px; + + width: 100%; + max-width: 100%; + height: auto; + position: relative; + padding: 16px 27px; + .${MOBILE_SCREEN_CLASS} & { + padding: 15px 13px; + } + + background-color: transparent; + cursor: pointer; + border: none; + margin: 0; + text-align: left; + overflow: hidden; + transition: all 0.125s cubic-bezier(0, 1, 0, 1); + max-height: 1280px; + + & .asset-address { + display: none; + } + + &:hover { + background-color: ${({ theme }) => theme.colors.text.background}; + & .asset-name { + display: none; + } + & .asset-address { + display: unset; + } + } + + ${({ selected, theme }) => + selected && + css` + background-color: ${theme.colors.text.background}; + & .asset-name { + display: none; + } + & .asset-address { + display: unset; + } + `} + ${({ invisible }) => + invisible && + css` + max-height: 0; + padding-top: 0; + padding-bottom: 0; + opacity: 0; + pointer-events: none; + .${MOBILE_SCREEN_CLASS} & { + padding-top: 0; + padding-bottom: 0; + } + `} +`; + +interface AssetIconProps { + src?: string; +} +const AssetIcon = styled.div` + width: 32px; + height: 32px; + min-width: 32px; + min-height: 32px; + position: relative; + display: inline-block; + padding: 0px 6px; + + background-color: ${({ theme }) => theme.colors.white}; + border-radius: 50%; + + ${({ src = iconToken }) => css` + background-image: url(${src || iconToken}); + `}; + background-size: 32px 32px; + background-position: 50% 50%; + background-repeat: no-repeat; +`; + +function Balance({ asset }: { asset?: Partial }) { + const balance = useBalance(asset?.token); + return ( + + {formatNumber( + cutDecimal(amountToValue(balance || 0, asset?.decimals) || 0, 3), + )} + + ); +} + +function AssetItem({ + asset, + selected, + hidden, + onClick, + isBookmarked, + isVerified, + onBookmarkToggle, +}: { + asset?: Partial; + selected?: boolean; + hidden?: boolean; + onClick?: React.MouseEventHandler; + isVerified?: boolean; + isBookmarked?: boolean; + onBookmarkToggle?: (address: string) => void; +}) { + const theme = useTheme(); + const wrapperRef = useRef(null); + const isIntersecting = useIsInViewport(wrapperRef); + return ( + + { + e.stopPropagation(); + if (onBookmarkToggle && asset?.token) { + onBookmarkToggle(asset?.token); + } + }} + /> + + + {isVerified && ( + +
+ + )} + +
div { + display: inline-block; + vertical-align: middle; + } + `} + > +
+ + {asset?.symbol} + + + {asset?.name} + + {ellipsisCenter(asset?.token, 6)} + + +
+ {isIntersecting && } +
+ + ); +} + +export default AssetItem; diff --git a/src/components/SelectAssetForm/index.tsx b/src/components/SelectAssetForm/index.tsx index ca0f7378..1c7e8daa 100644 --- a/src/components/SelectAssetForm/index.tsx +++ b/src/components/SelectAssetForm/index.tsx @@ -9,31 +9,19 @@ import React, { useState, } from "react"; import Typography from "components/Typography"; -import { - amountToValue, - cutDecimal, - ellipsisCenter, - formatNumber, - getIbcTokenHash, - isNativeTokenAddress, -} from "utils"; -import { Asset as OrgAsset } from "types/common"; -import iconToken from "assets/icons/icon-default-token.svg"; -import iconVerified from "assets/icons/icon-verified.svg"; -import IconButton from "components/IconButton"; +import { isNativeTokenAddress } from "utils"; import Input from "components/Input"; import Hr from "components/Hr"; import TabButton from "components/TabButton"; import useAssets from "hooks/useAssets"; -import iconBookmark from "assets/icons/icon-bookmark-default.svg"; -import iconBookmarkSelected from "assets/icons/icon-bookmark-selected.svg"; import useBookmark from "hooks/useBookmark"; import Panel from "components/Panel"; import { MOBILE_SCREEN_CLASS } from "constants/layout"; -import Tooltip from "components/Tooltip"; -import { useNetwork } from "hooks/useNetwork"; +import useNetwork from "hooks/useNetwork"; +import { Token } from "types/api"; +import AssetItem from "./AssetItem"; -type Asset = Partial; +type Asset = Partial; export type LPAsset = { address: string; assets: [Asset, Asset]; @@ -76,90 +64,6 @@ const AssetList = styled.div` } `; -const AssetItem = styled.div<{ selected?: boolean; invisible?: boolean }>` - display: flex; - justify-content: flex-start; - align-items: center; - column-gap: 6px; - - width: 100%; - max-width: 100%; - height: auto; - position: relative; - padding: 16px 27px; - .${MOBILE_SCREEN_CLASS} & { - padding: 15px 13px; - } - - background-color: transparent; - cursor: pointer; - border: none; - margin: 0; - text-align: left; - overflow: hidden; - transition: all 0.125s cubic-bezier(0, 1, 0, 1); - max-height: 1280px; - - & .asset-address { - display: none; - } - - &:hover { - background-color: ${({ theme }) => theme.colors.text.background}; - & .asset-name { - display: none; - } - & .asset-address { - display: unset; - } - } - - ${({ selected, theme }) => - selected && - css` - background-color: ${theme.colors.text.background}; - & .asset-name { - display: none; - } - & .asset-address { - display: unset; - } - `} - ${({ invisible }) => - invisible && - css` - max-height: 0; - padding-top: 0; - padding-bottom: 0; - opacity: 0; - pointer-events: none; - .${MOBILE_SCREEN_CLASS} & { - padding-top: 0; - padding-bottom: 0; - } - `} -`; - -const AssetIcon = styled.div<{ src?: string }>` - width: 32px; - height: 32px; - min-width: 32px; - min-height: 32px; - position: relative; - display: inline-block; - padding: 0px 6px; - - background-color: ${({ theme }) => theme.colors.white}; - border-radius: 50%; - - ${({ src = iconToken }) => css` - background-image: url(${src || iconToken}); - `}; - background-size: 32px 32px; - background-position: 50% 50%; - background-repeat: no-repeat; -`; - const NoResult = styled.div` position: absolute; width: 100%; @@ -186,7 +90,7 @@ function SelectAssetForm(props: SelectAssetFormProps) { const theme = useTheme(); const [searchKeyword, setSearchKeyword] = useState(""); const deferredSearchKeyword = useDeferredValue(searchKeyword); - const { getAsset, verifiedAssets, verifiedIbcAssets } = useAssets(); + const { getAsset } = useAssets(); const { bookmarks, toggleBookmark } = useBookmark(); const network = useNetwork(); const [tabIdx, setTabIdx] = useState(0); @@ -216,133 +120,25 @@ function SelectAssetForm(props: SelectAssetFormProps) { const items = filteredList?.map((address) => { const asset = getAsset(address); const isVerified = - !!verifiedAssets?.[address] || - isNativeTokenAddress(network.name, address) || - (verifiedIbcAssets && !!verifiedIbcAssets?.[getIbcTokenHash(address)]); + asset?.verified || isNativeTokenAddress(network.name, address); return ( - item?.toLowerCase().includes(deferredSearchKeyword.toLowerCase()), - ) < 0 + hidden={ + !asset?.symbol?.toLowerCase().includes(deferredSearchKeyword) && + !asset?.name?.toLowerCase().includes(deferredSearchKeyword) } + isVerified={isVerified} onClick={() => { if (handleSelect) { handleSelect(address); } }} - > - { - e.stopPropagation(); - toggleBookmark(address); - }} - /> - - - {isVerified && ( - -
- - )} - -
div { - display: inline-block; - vertical-align: middle; - } - `} - > -
- - {asset?.symbol} - - - {asset?.name} - - {ellipsisCenter(address, 6)} - - -
- - {formatNumber( - cutDecimal( - amountToValue(asset?.balance || 0, asset?.decimals) || 0, - 3, - ), - )} - -
- + isBookmarked={bookmarks?.includes(address)} + onBookmarkToggle={toggleBookmark} + /> ); }); @@ -372,8 +168,6 @@ function SelectAssetForm(props: SelectAssetFormProps) { theme, toggleBookmark, network, - verifiedAssets, - children, ]); useEffect(() => { diff --git a/src/constants/network.ts b/src/constants/network.ts index 3ffe0c55..988de944 100644 --- a/src/constants/network.ts +++ b/src/constants/network.ts @@ -1,4 +1,4 @@ -import { Asset } from "types/common"; +import { Token } from "types/api"; export type Network = { lcd: string; @@ -21,27 +21,31 @@ const networks: Record = { }, }; -export const nativeTokens: Record = { +export const nativeTokens: Record = { mainnet: [ { - address: XPLA_ADDRESS, + token: XPLA_ADDRESS, decimals: 18, name: XPLA_SYMBOL, symbol: XPLA_SYMBOL, total_supply: "", - iconSrc: "https://assets.xpla.io/icon/svg/XPLA.svg", - balance: "0", + icon: "https://assets.xpla.io/icon/svg/XPLA.svg", + chainId: networks.dimension.chainId, + verified: true, + protocol: "", }, ], testnet: [ { - address: XPLA_ADDRESS, + token: XPLA_ADDRESS, decimals: 18, name: XPLA_SYMBOL, symbol: XPLA_SYMBOL, total_supply: "", - iconSrc: "https://assets.xpla.io/icon/svg/XPLA.svg", - balance: "0", + icon: "https://assets.xpla.io/icon/svg/XPLA.svg", + chainId: networks.cube.chainId, + verified: true, + protocol: "", }, ], }; diff --git a/src/hooks/modals/useConfirmationModal.tsx b/src/hooks/modals/useConfirmationModal.tsx index ee21b64a..e3e0314d 100644 --- a/src/hooks/modals/useConfirmationModal.tsx +++ b/src/hooks/modals/useConfirmationModal.tsx @@ -1,6 +1,6 @@ import ConfirmationModal from "components/Modal/ConfirmationModal"; import useGlobalElement from "hooks/useGlobalElement"; -import { useModal } from "hooks/useModal"; +import useModal from "hooks/useModal"; import { useMemo } from "react"; const useConfirmationModal = ({ diff --git a/src/hooks/modals/useConnectWalletModal.tsx b/src/hooks/modals/useConnectWalletModal.tsx index 2770a36d..b766b2bb 100644 --- a/src/hooks/modals/useConnectWalletModal.tsx +++ b/src/hooks/modals/useConnectWalletModal.tsx @@ -1,7 +1,7 @@ import ConnectWalletModal from "components/Modal/ConnectWalletModal"; import { useMemo } from "react"; import useGlobalElement from "../useGlobalElement"; -import { useModal } from "../useModal"; +import useModal from "../useModal"; const useConnectWalletModal = () => { const modal = useModal(); diff --git a/src/hooks/modals/useFirstProvideModal.tsx b/src/hooks/modals/useFirstProvideModal.tsx index 2769e7d4..b7cd7850 100644 --- a/src/hooks/modals/useFirstProvideModal.tsx +++ b/src/hooks/modals/useFirstProvideModal.tsx @@ -1,7 +1,7 @@ import { useMemo } from "react"; import FirstProvideModal from "components/Modal/FirstProvideModal"; import useGlobalElement from "hooks/useGlobalElement"; -import { useModal } from "hooks/useModal"; +import useModal from "hooks/useModal"; const useFirstProvideModal = ({ addresses, diff --git a/src/hooks/modals/useInvalidPathModal.tsx b/src/hooks/modals/useInvalidPathModal.tsx index 4386d656..f4d73e6a 100644 --- a/src/hooks/modals/useInvalidPathModal.tsx +++ b/src/hooks/modals/useInvalidPathModal.tsx @@ -1,4 +1,4 @@ -import { useModal } from "hooks/useModal"; +import useModal from "hooks/useModal"; import { useMemo } from "react"; import useGlobalElement from "hooks/useGlobalElement"; import InvalidPathModal from "components/Modal/InvalidPathModal"; diff --git a/src/hooks/modals/useTxBroadcastingModal.tsx b/src/hooks/modals/useTxBroadcastingModal.tsx index 2f8b74b7..ba004ce4 100644 --- a/src/hooks/modals/useTxBroadcastingModal.tsx +++ b/src/hooks/modals/useTxBroadcastingModal.tsx @@ -1,6 +1,6 @@ import TxBroadcastingModal from "components/Modal/TxBroadcastingModal"; import useGlobalElement from "hooks/useGlobalElement"; -import { useModal } from "hooks/useModal"; +import useModal from "hooks/useModal"; import { useMemo } from "react"; import { TxError } from "types/common"; diff --git a/src/hooks/useAPI.ts b/src/hooks/useAPI.ts index 7eba2201..09f55372 100644 --- a/src/hooks/useAPI.ts +++ b/src/hooks/useAPI.ts @@ -1,159 +1,39 @@ import axios from "axios"; -import http from "http"; -import https from "https"; import { useCallback, useMemo } from "react"; -import { Pair, Pool, ReverseSimulation, Simulation } from "types/pair"; +import { ReverseSimulation, Simulation } from "types/pair"; import { useConnectedWallet } from "@xpla/wallet-provider"; import { generateReverseSimulationMsg, generateSimulationMsg, - queryMessages, } from "utils/dezswap"; -import { Pairs } from "types/factory"; -import { TokenInfo, VerifiedTokenInfo } from "types/token"; -import { apiAddresses, contractAddresses } from "constants/dezswap"; -import { useNetwork } from "hooks/useNetwork"; -import { useLCDClient } from "hooks/useLCDClient"; +import { VerifiedAssets, VerifiedIbcAssets } from "types/token"; +import { contractAddresses } from "constants/dezswap"; +import useNetwork from "hooks/useNetwork"; +import useLCDClient from "hooks/useLCDClient"; import { LatestBlock } from "types/common"; +import api, { ApiVersion } from "api"; interface TokenBalance { balance: string; } -export type ApiVersion = "v1"; - interface Decimal { decimals: number; } -export const useAPI = (version: ApiVersion = "v1") => { +const useAPI = (version: ApiVersion = "v1") => { const network = useNetwork(); const lcd = useLCDClient(); const connectedWallet = useConnectedWallet(); - const apiClient = axios.create({ - httpAgent: new http.Agent({ keepAlive: true }), - httpsAgent: new https.Agent({ keepAlive: true }), - }); const walletAddress = useMemo( () => connectedWallet?.walletAddress, [connectedWallet], ); - const getTokens = useCallback(async () => { - try { - const base = apiAddresses[network.name]?.baseUrl || ""; - const { data } = await apiClient.get<(TokenInfo & VerifiedTokenInfo)[]>( - `${base}/${version}/tokens`, - ); - return data; - } catch (err) { - console.error(err); - } - return []; - }, [lcd, network.name, version]); - - const getToken = useCallback( - async (address: string) => { - try { - const base = apiAddresses[network.name]?.baseUrl || ""; - const { data } = await apiClient.get( - `${base}/${version}/tokens/${address}`, - ); - return data; - } catch (err) { - console.error(err); - } - const res = await lcd.wasm.contractQuery(address, { - token_info: {}, - }); - return res; - }, - [lcd, network.name, version], - ); - - const getPairs = useCallback( - async (options?: Parameters[0]) => { - const contractAddress = contractAddresses[network.name]?.factory; - if (!contractAddress) { - return undefined; - } - - try { - const base = apiAddresses[network.name]?.baseUrl || ""; - const { data } = await apiClient.get(`${base}/${version}/pairs`); - return data; - } catch (err) { - console.error(err); - } - const res: Pairs = await lcd.wasm.contractQuery( - contractAddress, - queryMessages.getPairs(options), - ); - - return res; - }, - [lcd.wasm, network.name, version], - ); - - const getPair = useCallback( - async (contractAddress: string) => { - if (!contractAddress) { - return undefined; - } - - try { - const base = apiAddresses[network.name]?.baseUrl || ""; - const { data } = await apiClient.get( - `${base}/${version}/pairs/${contractAddress}`, - ); - return data; - } catch (err) { - console.error(err); - } - const res = await lcd.wasm.contractQuery(contractAddress, { - pair: {}, - }); - - return res; - }, - [lcd.wasm, network.name, version], - ); - - const getPools = useCallback(async () => { - try { - const base = apiAddresses[network.name]?.baseUrl || ""; - const { data } = await apiClient.get(`${base}/${version}/pools`); - return data; - } catch (err) { - console.error(err); - } - return []; - }, [network.name, version]); - - const getPool = useCallback( - async (contractAddress: string) => { - if (!contractAddress) { - return undefined; - } - - try { - const base = apiAddresses[network.name]?.baseUrl || ""; - const { data } = await apiClient.get( - `${base}/${version}/pools/${contractAddress}`, - ); - return data; - } catch (err) { - console.error(err); - } - const res: Pool = await lcd.wasm.contractQuery( - contractAddress, - queryMessages.getPool(), - ); - - return res; - }, - [lcd.wasm, network.name, version], + const apiClient = useMemo( + () => api(network.name, version), + [network.name, version], ); const simulate = useCallback( @@ -209,22 +89,34 @@ export const useAPI = (version: ApiVersion = "v1") => { [lcd, walletAddress], ); - const getVerifiedTokenInfo = useCallback(async () => { - const { data } = await apiClient.get( - "https://assets.xpla.io/cw20/tokens.json", - ); - return data; - }, []); + const getVerifiedTokenInfos = useCallback( + /** + * @Deprecated + */ + async () => { + const { data } = await axios.get( + "https://assets.xpla.io/cw20/tokens.json", + ); + return data; + }, + [], + ); - const getVerifiedIbcTokenInfo = useCallback(async () => { - const { data } = await apiClient.get( - "https://assets.xpla.io/ibc/tokens.json", - ); - return data; - }, []); + const getVerifiedIbcTokenInfos = useCallback( + /** + * @Deprecated + */ + async () => { + const { data } = await axios.get( + "https://assets.xpla.io/ibc/tokens.json", + ); + return data; + }, + [], + ); const getLatestBlockHeight = useCallback(async () => { - const { data } = await apiClient.get( + const { data } = await axios.get( `${network.lcd}/blocks/latest`, ); return data.block.header.height; @@ -241,41 +133,33 @@ export const useAPI = (version: ApiVersion = "v1") => { }); return res.decimals; }, - [network.name, lcd, walletAddress], + [network.name, lcd], ); - const api = useMemo( + return useMemo( () => ({ - getTokens, - getToken, - getPairs, - getPair, - getPool, + ...apiClient, simulate, reverseSimulate, getNativeTokenBalance, getTokenBalance, - getVerifiedTokenInfo, - getVerifiedIbcTokenInfo, + getVerifiedTokenInfos, + getVerifiedIbcTokenInfos, getLatestBlockHeight, getDecimal, }), [ - getTokens, - getToken, - getPairs, - getPair, - getPool, + apiClient, simulate, reverseSimulate, getNativeTokenBalance, getTokenBalance, - getVerifiedTokenInfo, - getVerifiedIbcTokenInfo, + getVerifiedTokenInfos, + getVerifiedIbcTokenInfos, getLatestBlockHeight, getDecimal, ], ); - - return api; }; + +export default useAPI; diff --git a/src/hooks/useAssets.ts b/src/hooks/useAssets.ts index 4875deca..6ef4cdee 100644 --- a/src/hooks/useAssets.ts +++ b/src/hooks/useAssets.ts @@ -1,155 +1,40 @@ -import { useCallback, useMemo, useRef } from "react"; -import { useAtom, useAtomValue } from "jotai"; -import { useAPI } from "hooks/useAPI"; -import { useNetwork } from "hooks/useNetwork"; -import assetsAtom, { - verifiedAssetsAtom, - verifiedIbcAssetsAtom, -} from "stores/assets"; +import { useCallback, useMemo } from "react"; +import useAPI from "hooks/useAPI"; +import useNetwork from "hooks/useNetwork"; import { AccAddress } from "@xpla/xpla.js"; -import { Asset, NetworkName } from "types/common"; -import { getIbcTokenHash, isNativeTokenAddress } from "utils"; -import { nativeTokens } from "constants/network"; +import { isNativeTokenAddress } from "utils"; import useCustomAssets from "hooks/useCustomAssets"; - -const UPDATE_INTERVAL_SEC = 5000; +import { useQuery } from "@tanstack/react-query"; const useAssets = () => { - const [assetStore, setAssetStore] = useAtom(assetsAtom); - const verifiedAssets = useAtomValue(verifiedAssetsAtom); - const verifiedIbcAssets = useAtomValue(verifiedIbcAssetsAtom); const api = useAPI(); const network = useNetwork(); const { getCustomAsset } = useCustomAssets(); - const fetchQueue = useRef<{ [K in NetworkName]?: AccAddress[] }>({ - mainnet: [], - testnet: [], - }); - const isFetching = useRef(false); - const { removeCustomAsset } = useCustomAssets(); - - const fetchAsset = useCallback(async () => { - isFetching.current = true; - try { - const networkName = network.name; - const store = assetStore[networkName] || []; - const address = fetchQueue.current[networkName]?.[0]; - - if (address) { - const index = store.findIndex((item) => item.address === address); - if (index >= 0) { - const currentAsset = store[index]; - if ( - new Date((currentAsset as Asset).updatedAt || 0)?.getTime() < - Date.now() - UPDATE_INTERVAL_SEC && - window.navigator.onLine - ) { - if (isNativeTokenAddress(network.name, address)) { - const asset = nativeTokens[network.name]?.find( - (item) => item.address === address, - ); - const balance = await api.getNativeTokenBalance(address); - if (asset) { - store[index] = { - ...asset, - balance: balance || "0", - updatedAt: new Date(), - }; - setAssetStore((current) => ({ - ...current, - [networkName]: assetStore[networkName], - })); - } - } else if ( - verifiedIbcAssets?.[networkName]?.[getIbcTokenHash(address)] - ) { - const asset = - verifiedIbcAssets?.[networkName]?.[getIbcTokenHash(address)]; - const balance = await api.getNativeTokenBalance(address); - if (asset) { - store[index] = { - ...asset, - total_supply: "", - address: asset.denom, - iconSrc: asset.icon, - balance: balance || "0", - updatedAt: new Date(), - }; - setAssetStore((current) => ({ - ...current, - [networkName]: assetStore[networkName], - })); - } - } else { - const token = await api.getToken(address); - if (verifiedAssets) { - const verifiedAsset = verifiedAssets?.[networkName]?.[address]; - const balance = await api.getTokenBalance(address); - - store[index] = { - ...token, - address, - balance: balance || "0", - iconSrc: verifiedAsset?.icon, - updatedAt: new Date(), - }; - setAssetStore((current) => ({ - ...current, - [networkName]: assetStore[networkName], - })); - } else if (!fetchQueue.current[networkName]?.includes(address)) { - fetchQueue.current[networkName]?.push(address); - } - } - removeCustomAsset(address); - } - } - } - } catch (error) { - console.log(error); - } - isFetching.current = false; - setTimeout(() => { - fetchQueue.current[network.name]?.shift(); - if (fetchQueue.current[network.name]?.length) { - fetchAsset(); - } - }, 100); - }, [network, assetStore, api, verifiedAssets, setAssetStore]); - const addFetchQueue = useCallback( - (address: string, networkName: NetworkName) => { - if ( - nativeTokens[networkName]?.some((item) => item.address === address) || - AccAddress.validate(address) || - (verifiedIbcAssets && - !!verifiedIbcAssets[networkName]?.[getIbcTokenHash(address)]) - ) { - if (!fetchQueue.current[networkName]?.includes(address)) { - fetchQueue.current[networkName]?.push(address); - } - } - if (!isFetching.current && window.navigator.onLine) { - fetchAsset(); - } + const { data: assets } = useQuery( + ["assets", network.name], + async () => { + const res = await api.getTokens(); + return res; + }, + { + enabled: !!network.name, + refetchOnReconnect: true, + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchInterval: false, }, - [fetchAsset], ); const getAsset = useCallback( - (address: string): Partial | undefined => { - const asset = assetStore[network.name]?.find( - (item) => item.address === address, - ); - if (!asset?.address) { + (address: string) => { + const asset = assets?.find((item) => item.token === address); + if (!asset?.token) { return getCustomAsset(address); } - if (window.navigator.onLine) { - addFetchQueue(asset.address, network.name); - } return asset; }, - [assetStore, network.name, getCustomAsset, addFetchQueue], + [assets, getCustomAsset], ); const validate = useCallback( @@ -157,18 +42,16 @@ const useAssets = () => { address && (AccAddress.validate(address) || isNativeTokenAddress(network.name, address) || - verifiedIbcAssets?.[network.name]?.[getIbcTokenHash(address)]), - [network.name, verifiedIbcAssets], + assets?.find((item) => item.token === address)?.verified), + [assets, network.name], ); return useMemo( () => ({ getAsset, validate, - verifiedAssets: verifiedAssets?.[network.name], - verifiedIbcAssets: verifiedIbcAssets?.[network.name], }), - [getAsset, validate, network.name, verifiedAssets, verifiedIbcAssets], + [getAsset, validate], ); }; diff --git a/src/hooks/useBalance.ts b/src/hooks/useBalance.ts index 9a03c563..d73df65f 100644 --- a/src/hooks/useBalance.ts +++ b/src/hooks/useBalance.ts @@ -1,58 +1,50 @@ -import { useEffect, useMemo, useState } from "react"; +import { useCallback, useMemo } from "react"; import { useConnectedWallet } from "@xpla/wallet-provider"; -import { useAPI } from "hooks/useAPI"; +import useAPI from "hooks/useAPI"; import { getIbcTokenHash, isNativeTokenAddress } from "utils"; -import { useAtomValue } from "jotai"; -import { verifiedIbcAssetsAtom } from "stores/assets"; -import { useNetwork } from "hooks/useNetwork"; +import useNetwork from "hooks/useNetwork"; +import { useQuery } from "@tanstack/react-query"; +import useVerifiedAssets from "./useVerifiedAssets"; -const UPDATE_INTERVAL = 2500; +const UPDATE_INTERVAL = 30000; -export const useBalance = (asset: string) => { +const useBalance = (address?: string) => { const connectedWallet = useConnectedWallet(); - const [balance, setBalance] = useState(); - const verifiedIbcAssets = useAtomValue(verifiedIbcAssetsAtom); + const { verifiedIbcAssets } = useVerifiedAssets(); const network = useNetwork(); const api = useAPI(); - useEffect(() => { - const fetchBalance = async () => { - if (!connectedWallet?.walletAddress || !asset) { - setBalance("0"); + const fetchBalance = useCallback(async () => { + if ( + address && + connectedWallet?.network.name && + connectedWallet?.walletAddress + ) { + if ( + isNativeTokenAddress(connectedWallet?.network.name, address) || + (verifiedIbcAssets && !!verifiedIbcAssets?.[getIbcTokenHash(address)]) + ) { + const value = await api.getNativeTokenBalance(address); + return `${value || 0}`; } + const value = await api.getTokenBalance(address); + return `${value || 0}`; + } + return "0"; + }, [api, address, connectedWallet, verifiedIbcAssets]); - if (asset && connectedWallet?.network.name) { - if ( - isNativeTokenAddress(connectedWallet?.network.name, asset) || - (verifiedIbcAssets && - !!verifiedIbcAssets[network.name]?.[getIbcTokenHash(asset)]) - ) { - api - .getNativeTokenBalance(asset) - .then((value) => - typeof value !== "undefined" - ? setBalance(`${value}`) - : setBalance("0"), - ); - } else { - api - .getTokenBalance(asset) - .then((value) => - typeof value !== "undefined" - ? setBalance(value) - : setBalance("0"), - ); - } - } - }; - - const intervalId = setInterval(() => fetchBalance(), UPDATE_INTERVAL); - fetchBalance(); - - return () => { - clearInterval(intervalId); - }; - }, [api, connectedWallet, asset, verifiedIbcAssets, network.name]); + const { data: balance } = useQuery({ + queryKey: ["balance", address, network.name], + queryFn: fetchBalance, + refetchInterval: UPDATE_INTERVAL, + refetchIntervalInBackground: true, + refetchOnMount: false, + refetchOnWindowFocus: false, + cacheTime: UPDATE_INTERVAL, + enabled: !!address, + }); return useMemo(() => balance, [balance]); }; + +export default useBalance; diff --git a/src/hooks/useBalanceMinusFee.ts b/src/hooks/useBalanceMinusFee.ts index f4d4424c..fe5545de 100644 --- a/src/hooks/useBalanceMinusFee.ts +++ b/src/hooks/useBalanceMinusFee.ts @@ -2,13 +2,11 @@ import { useEffect, useMemo, useState } from "react"; import { Numeric } from "@xpla/xpla.js"; import { XPLA_ADDRESS } from "constants/network"; import { amountToValue, valueToAmount } from "utils"; +import useBalance from "./useBalance"; -const useBalanceMinusFee = ( - address?: string, - balance?: string, - feeAmount?: string, -) => { - const [balanceMinusFee, setAsset1BalanceMinusFee] = useState(balance); +const useBalanceMinusFee = (address?: string, feeAmount?: string) => { + const balance = useBalance(address); + const [balanceMinusFee, setBalanceMinusFee] = useState(balance); useEffect(() => { if (balance) { @@ -16,12 +14,12 @@ const useBalanceMinusFee = ( const res = Numeric.parse(amountToValue(balance) || 0).minus( amountToValue(feeAmount) || 0, ); - setAsset1BalanceMinusFee(res.gt(0) ? valueToAmount(res) : "0"); + setBalanceMinusFee(res.gt(0) ? valueToAmount(res) : "0"); } else { - setAsset1BalanceMinusFee(balance); + setBalanceMinusFee(balance); } } else { - setAsset1BalanceMinusFee("0"); + setBalanceMinusFee("0"); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [balance, feeAmount]); diff --git a/src/hooks/useBookmark.ts b/src/hooks/useBookmark.ts index 1c726ba3..eb9f250b 100644 --- a/src/hooks/useBookmark.ts +++ b/src/hooks/useBookmark.ts @@ -1,5 +1,5 @@ import { useCallback, useMemo } from "react"; -import { useNetwork } from "hooks/useNetwork"; +import useNetwork from "hooks/useNetwork"; import { bookmarksAtom } from "stores/assets"; import { useAtom } from "jotai"; diff --git a/src/hooks/useCustomAssets.ts b/src/hooks/useCustomAssets.ts index c695bdc4..cc5ec08a 100644 --- a/src/hooks/useCustomAssets.ts +++ b/src/hooks/useCustomAssets.ts @@ -1,25 +1,25 @@ -import { useCallback, useMemo, useRef } from "react"; -import { useAtom, useAtomValue } from "jotai"; -import { useAPI } from "hooks/useAPI"; -import { useNetwork } from "hooks/useNetwork"; -import { - customAssetsAtom, - verifiedAssetsAtom, - verifiedIbcAssetsAtom, -} from "stores/assets"; +import { useCallback, useEffect, useMemo, useRef } from "react"; +import { useAtom } from "jotai"; +import useNetwork from "hooks/useNetwork"; +import { customAssetsAtom } from "stores/assets"; import { AccAddress } from "@xpla/xpla.js"; -import { Asset, NetworkName } from "types/common"; +import { NetworkName } from "types/common"; import { getIbcTokenHash, isNativeTokenAddress } from "utils"; import { nativeTokens } from "constants/network"; +import { Token } from "types/api"; +import { TokenInfo } from "types/token"; +import useLCDClient from "./useLCDClient"; +import usePairs from "./usePairs"; +import useVerifiedAssets from "./useVerifiedAssets"; const UPDATE_INTERVAL_SEC = 5000; const useCustomAssets = () => { const [customAssetStore, setCustomAssetStore] = useAtom(customAssetsAtom); - const verifiedAssets = useAtomValue(verifiedAssetsAtom); - const verifiedIbcAssets = useAtomValue(verifiedIbcAssetsAtom); + const { verifiedAssets, verifiedIbcAssets } = useVerifiedAssets(); + const { availableAssetAddresses } = usePairs(); - const api = useAPI(); + const lcd = useLCDClient(); const network = useNetwork(); const fetchQueue = useRef<{ [K in NetworkName]?: AccAddress[] }>({ mainnet: [], @@ -35,23 +35,21 @@ const useCustomAssets = () => { const address = fetchQueue.current[networkName]?.[0]; if (address) { - const index = store.findIndex((item) => item.address === address); + const index = store.findIndex((item) => item.token === address); if (index >= 0) { const currentAsset = store[index]; if ( - new Date((currentAsset as Asset).updatedAt || 0)?.getTime() < + new Date(currentAsset.updatedAt || 0)?.getTime() < Date.now() - UPDATE_INTERVAL_SEC && window.navigator.onLine ) { if (isNativeTokenAddress(network.name, address)) { const asset = nativeTokens[network.name]?.find( - (item) => item.address === address, + (item) => item.token === address, ); - const balance = await api.getNativeTokenBalance(address); if (asset) { store[index] = { ...asset, - balance: balance || "0", updatedAt: new Date(), }; setCustomAssetStore((current) => ({ @@ -59,19 +57,17 @@ const useCustomAssets = () => { [networkName]: customAssetStore[networkName], })); } - } else if ( - verifiedIbcAssets?.[networkName]?.[getIbcTokenHash(address)] - ) { - const asset = - verifiedIbcAssets?.[networkName]?.[getIbcTokenHash(address)]; - const balance = await api.getNativeTokenBalance(address); + } else if (verifiedIbcAssets?.[getIbcTokenHash(address)]) { + const asset = verifiedIbcAssets?.[getIbcTokenHash(address)]; if (asset) { store[index] = { ...asset, total_supply: "", - address: asset.denom, - iconSrc: asset.icon, - balance: balance || "0", + token: asset.denom, + icon: asset.icon, + chainId: network.chainID, + protocol: "", + verified: true, updatedAt: new Date(), }; setCustomAssetStore((current) => ({ @@ -80,16 +76,22 @@ const useCustomAssets = () => { })); } } else { - const token = await api.getToken(address); + const token = await lcd.wasm.contractQuery(address, { + token_info: {}, + }); if (verifiedAssets) { - const verifiedAsset = verifiedAssets?.[networkName]?.[address]; - const balance = await api.getTokenBalance(address); + const verifiedAsset = verifiedAssets?.[address]; store[index] = { - ...token, - address, - balance: balance || "0", - iconSrc: verifiedAsset?.icon, + name: token.name, + decimals: token.decimals, + symbol: token.symbol, + chainId: network.chainID, + protocol: "", + verified: !!verifiedAsset, + token: address, + total_supply: "", + icon: verifiedAsset?.icon || "", updatedAt: new Date(), }; setCustomAssetStore((current) => ({ @@ -113,15 +115,21 @@ const useCustomAssets = () => { fetchAsset(); } }, 100); - }, [network, customAssetStore, api, verifiedAssets, setCustomAssetStore]); + }, [ + network, + customAssetStore, + verifiedIbcAssets, + setCustomAssetStore, + lcd, + verifiedAssets, + ]); const addFetchQueue = useCallback( (address: string, networkName: NetworkName) => { if ( - nativeTokens[networkName]?.some((item) => item.address === address) || + nativeTokens[networkName]?.some((item) => item.token === address) || AccAddress.validate(address) || - (verifiedIbcAssets && - verifiedIbcAssets[networkName]?.[getIbcTokenHash(address)]) + (verifiedIbcAssets && verifiedIbcAssets?.[getIbcTokenHash(address)]) ) { if (!fetchQueue.current[networkName]?.includes(address)) { fetchQueue.current[networkName]?.push(address); @@ -131,19 +139,19 @@ const useCustomAssets = () => { fetchAsset(); } }, - [fetchAsset], + [fetchAsset, verifiedIbcAssets], ); const getAsset = useCallback( - (address: string): Partial | undefined => { + (address: string): Partial | undefined => { const asset = customAssetStore[network.name]?.find( - (item) => item.address === address, + (item) => item.token === address, ); - if (!asset?.address) { + if (!asset?.token) { return undefined; } if (window.navigator.onLine) { - addFetchQueue(asset.address, network.name); + addFetchQueue(asset.token, network.name); } return asset; }, @@ -151,9 +159,9 @@ const useCustomAssets = () => { ); const addCustomAsset = useCallback( - (asset: Asset) => { + (asset: Token) => { const store = customAssetStore[network.name] || []; - const index = store.findIndex((item) => item.address === asset.address); + const index = store.findIndex((item) => item.token === asset.token); if (index >= 0) { store[index] = asset; } else { @@ -163,18 +171,18 @@ const useCustomAssets = () => { ...current, [network.name]: store, })); - addFetchQueue(asset.address, network.name); + addFetchQueue(asset.token, network.name); }, [addFetchQueue, customAssetStore, network.name, setCustomAssetStore], ); const removeCustomAsset = useCallback( (address: string) => { - if (customAssetStore[network.name]?.some((a) => a.address === address)) { + if (customAssetStore[network.name]?.some((a) => a.token === address)) { setCustomAssetStore((current) => ({ ...current, [network.name]: customAssetStore[network.name]?.filter( - (a) => a.address !== address, + (a) => a.token !== address, ), })); } @@ -182,6 +190,12 @@ const useCustomAssets = () => { [customAssetStore, network.name, setCustomAssetStore], ); + useEffect(() => { + availableAssetAddresses.forEach((address) => { + removeCustomAsset(address); + }); + }, [availableAssetAddresses, removeCustomAsset]); + return useMemo( () => ({ customAssets: customAssetStore[network.name], @@ -189,7 +203,13 @@ const useCustomAssets = () => { removeCustomAsset, getCustomAsset: getAsset, }), - [addCustomAsset, customAssetStore, getAsset, network.name], + [ + addCustomAsset, + customAssetStore, + getAsset, + network.name, + removeCustomAsset, + ], ); }; diff --git a/src/hooks/useFee.ts b/src/hooks/useFee.ts index 2e05fcbd..2641c30d 100644 --- a/src/hooks/useFee.ts +++ b/src/hooks/useFee.ts @@ -2,9 +2,9 @@ import { CreateTxOptions, Fee } from "@xpla/xpla.js"; import { useConnectedWallet } from "@xpla/wallet-provider"; import { useDeferredValue, useEffect, useState } from "react"; import { AxiosError } from "axios"; -import { useLCDClient } from "hooks/useLCDClient"; +import useLCDClient from "hooks/useLCDClient"; -export const useFee = (txOptions?: CreateTxOptions) => { +const useFee = (txOptions?: CreateTxOptions) => { const connectedWallet = useConnectedWallet(); const lcd = useLCDClient(); const [fee, setFee] = useState(); @@ -106,3 +106,5 @@ export const useFee = (txOptions?: CreateTxOptions) => { return { fee, isLoading, isFailed, errMsg }; }; + +export default useFee; diff --git a/src/hooks/useIsInViewport.ts b/src/hooks/useIsInViewport.ts new file mode 100644 index 00000000..dc13ae6b --- /dev/null +++ b/src/hooks/useIsInViewport.ts @@ -0,0 +1,27 @@ +import { useState, useMemo, useEffect } from "react"; + +const useIsInViewport = (ref: React.RefObject) => { + const [isIntersecting, setIsIntersecting] = useState(false); + + const observer = useMemo( + () => + new IntersectionObserver(([entry]) => + setIsIntersecting(entry.isIntersecting), + ), + [], + ); + + useEffect(() => { + if (ref.current) { + observer.observe(ref.current); + } + + return () => { + observer.disconnect(); + }; + }, [ref, observer]); + + return isIntersecting; +}; + +export default useIsInViewport; diff --git a/src/hooks/useLCDClient.ts b/src/hooks/useLCDClient.ts index d117d6ba..2b74b06e 100644 --- a/src/hooks/useLCDClient.ts +++ b/src/hooks/useLCDClient.ts @@ -1,8 +1,8 @@ import { LCDClient } from "@xpla/xpla.js"; -import { useNetwork } from "hooks/useNetwork"; +import useNetwork from "hooks/useNetwork"; import { useMemo } from "react"; -export const useLCDClient = () => { +const useLCDClient = () => { const network = useNetwork(); return useMemo( () => @@ -14,3 +14,5 @@ export const useLCDClient = () => { [network], ); }; + +export default useLCDClient; diff --git a/src/hooks/useLatestBlock.ts b/src/hooks/useLatestBlock.ts index 910aac69..0ed598cc 100644 --- a/src/hooks/useLatestBlock.ts +++ b/src/hooks/useLatestBlock.ts @@ -1,26 +1,22 @@ -import { useEffect, useMemo, useState } from "react"; -import { useAPI } from "hooks/useAPI"; +import useAPI from "hooks/useAPI"; +import { useQuery } from "@tanstack/react-query"; +import useNetwork from "./useNetwork"; const UPDATE_INTERVAL = 3000; export const useLatestBlock = () => { - const [height, setHeight] = useState(); const api = useAPI(); + const network = useNetwork(); - useEffect(() => { - const fetchHeight = () => { - api - .getLatestBlockHeight() - .then((value) => typeof value !== "undefined" && setHeight(`${value}`)); - }; + const { data: height } = useQuery({ + queryKey: ["latestBlockHeight", network.name], + queryFn: api.getLatestBlockHeight, + refetchInterval: UPDATE_INTERVAL, + refetchIntervalInBackground: false, + refetchOnMount: false, + refetchOnWindowFocus: true, + refetchOnReconnect: true, + }); - const intervalId = setInterval(() => fetchHeight(), UPDATE_INTERVAL); - fetchHeight(); - - return () => { - clearInterval(intervalId); - }; - }, [api]); - - return useMemo(() => height, [height]); + return height; }; diff --git a/src/hooks/useModal.ts b/src/hooks/useModal.ts index 7fa30e2c..3f626e5e 100644 --- a/src/hooks/useModal.ts +++ b/src/hooks/useModal.ts @@ -1,6 +1,6 @@ import { useCallback, useMemo, useState } from "react"; -export const useModal = (defaultOption = false) => { +const useModal = (defaultOption = false) => { const [isOpen, setIsOpen] = useState(defaultOption); const open = useCallback((state = true) => setIsOpen(state), []); @@ -12,3 +12,5 @@ export const useModal = (defaultOption = false) => { [close, isOpen, open, toggle], ); }; + +export default useModal; diff --git a/src/hooks/useNetwork.ts b/src/hooks/useNetwork.ts index d790914e..1c5a9bab 100644 --- a/src/hooks/useNetwork.ts +++ b/src/hooks/useNetwork.ts @@ -2,7 +2,7 @@ import { useWallet } from "@xpla/wallet-provider"; import { useMemo } from "react"; import { NetworkName } from "types/common"; -export const useNetwork = () => { +const useNetwork = () => { const wallet = useWallet(); return useMemo( @@ -10,3 +10,5 @@ export const useNetwork = () => { [wallet.network], ); }; + +export default useNetwork; diff --git a/src/hooks/usePair.ts b/src/hooks/usePair.ts deleted file mode 100644 index 7eecca7f..00000000 --- a/src/hooks/usePair.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { useAPI } from "hooks/useAPI"; -import { NetworkName } from "types/common"; -import { useAtom, useSetAtom } from "jotai"; -import { queryMessages } from "utils/dezswap"; -import pairsAtom, { isPairsLoadingAtom } from "stores/pairs"; -import assetsAtom from "stores/assets"; -import { useNetwork } from "hooks/useNetwork"; -import useCustomAssets from "./useCustomAssets"; - -const LIMIT = 30; - -const usePairs = () => { - const network = useNetwork(); - const [pairs, setPairs] = useAtom(pairsAtom); - const [isLoading, setIsLoading] = useAtom(isPairsLoadingAtom); - const setAssets = useSetAtom(assetsAtom); - const isMainFetcher = useRef(false); - const api = useAPI(); - const { removeCustomAsset } = useCustomAssets(); - const lastPairAddr = useRef(""); - - const fetchPairs = useCallback( - async (options?: Parameters[0]) => { - try { - if (!network.name || !window.navigator.onLine) { - return; - } - setIsLoading(true); - const res = await api.getPairs(options); - if (res?.pairs) { - const newPairs = res.pairs.map((pair) => ({ - ...pair, - asset_addresses: pair.asset_infos.map((asset) => - "token" in asset - ? asset.token.contract_addr - : asset.native_token.denom, - ), - })); - const newLastPairAddr = newPairs[newPairs.length - 1].contract_addr; - if (newPairs.length && lastPairAddr.current !== newLastPairAddr) { - lastPairAddr.current = newLastPairAddr; - setPairs((current) => { - const currentPairs = current[network.name]?.data || []; - return { - ...current, - [network.name]: { - data: [...currentPairs, ...newPairs].filter( - (pair, index, array) => - index === - array.findIndex( - (p) => p.contract_addr === pair.contract_addr, - ), - ), - }, - }; - }); - return; - } - setIsLoading(false); - } - } catch (error) { - console.log(error); - } - }, - [api, network.name, setIsLoading, setPairs], - ); - - useEffect(() => { - if ((!isMainFetcher.current && !isLoading) || isMainFetcher.current) { - isMainFetcher.current = true; - const lastPair = pairs[network.name]?.data?.slice(-1)[0]; - fetchPairs({ - limit: LIMIT, - ...(lastPair ? { start_after: lastPair.asset_infos } : undefined), - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [fetchPairs, network.name, pairs]); - - const getPairedAddresses = useCallback( - (searchAddress: string) => { - const pairAddresses = pairs[network.name]?.data - ?.filter((pair) => { - return pair.asset_addresses.includes(searchAddress); - }) - .map((pair) => { - return pair.asset_addresses.find( - (address) => address !== searchAddress, - ) as string; - }); - return pairAddresses; - }, - [network, pairs], - ); - - const availableAssetAddresses = useMemo(() => { - return { - addresses: - pairs[network.name]?.data - ?.reduce((acc, pair) => { - return [...acc, ...pair.asset_addresses]; - }, [] as string[]) - ?.filter((asset, index, array) => array.indexOf(asset) === index) || - [], - networkName: network.name, - }; - }, [pairs, network.name]); - - useEffect(() => { - if (availableAssetAddresses) { - setAssets((current) => { - const { addresses, networkName } = availableAssetAddresses; - const currentAssetList = current[networkName] || []; - return { - ...current, - [networkName]: [ - ...currentAssetList, - ...addresses - .filter( - (address) => - currentAssetList.findIndex( - (item) => item.address === address, - ) < 0, - ) - .map((address) => ({ - address, - })), - ], - }; - }); - availableAssetAddresses.addresses.forEach((a) => removeCustomAsset(a)); - } - }, [availableAssetAddresses, setAssets, removeCustomAsset]); - - const getPair = useCallback( - (contractAddress: string) => { - return pairs[network.name]?.data?.find( - (pair) => pair.contract_addr === contractAddress, - ); - }, - [network.name, pairs], - ); - - const findPair = useCallback( - (addresses: [string, string]) => { - if (addresses[0] === addresses[1]) { - return undefined; - } - return pairs[network.name]?.data?.find( - (pair) => - pair.asset_addresses.includes(addresses[0]) && - pair.asset_addresses.includes(addresses[1]), - ); - }, - [network.name, pairs], - ); - - const findPairByLpAddress = useCallback( - (lpAddress: string) => { - return pairs[network.name]?.data?.find( - (pair) => pair.liquidity_token === lpAddress, - ); - }, - [network.name, pairs], - ); - - return useMemo( - () => ({ - pairs: pairs[network.name as NetworkName]?.data, - getPairedAddresses, - availableAssetAddresses, - getPair, - findPair, - findPairByLpAddress, - }), - [ - availableAssetAddresses, - getPairedAddresses, - network.name, - pairs, - getPair, - findPair, - findPairByLpAddress, - ], - ); -}; - -export default usePairs; diff --git a/src/hooks/usePairBookmark.ts b/src/hooks/usePairBookmark.ts index 3b68e08e..7ab4446b 100644 --- a/src/hooks/usePairBookmark.ts +++ b/src/hooks/usePairBookmark.ts @@ -1,5 +1,5 @@ import { useCallback, useMemo } from "react"; -import { useNetwork } from "hooks/useNetwork"; +import useNetwork from "hooks/useNetwork"; import { bookmarksAtom } from "stores/pairs"; import { useAtom } from "jotai"; diff --git a/src/hooks/usePairs.ts b/src/hooks/usePairs.ts new file mode 100644 index 00000000..bcf4031e --- /dev/null +++ b/src/hooks/usePairs.ts @@ -0,0 +1,105 @@ +import { useCallback, useMemo } from "react"; +import useAPI from "hooks/useAPI"; +import useNetwork from "hooks/useNetwork"; +import { useQuery } from "@tanstack/react-query"; + +const usePairs = () => { + const network = useNetwork(); + const api = useAPI(); + + const { data: pairs, isLoading } = useQuery({ + queryKey: ["pairs", network.name], + queryFn: async () => { + const res = await api.getPairs(); + return res?.pairs.map((pair) => ({ + ...pair, + asset_addresses: pair.asset_infos.map((assetInfo) => + "token" in assetInfo + ? assetInfo.token.contract_addr + : assetInfo.native_token.denom, + ), + })); + }, + refetchInterval: false, + refetchOnMount: false, + refetchOnReconnect: true, + refetchOnWindowFocus: false, + }); + + const getPairedAddresses = useCallback( + (searchAddress: string) => { + const pairAddresses = pairs + ?.filter((pair) => { + return pair.asset_addresses.includes(searchAddress); + }) + .map((pair) => { + return pair.asset_addresses.find( + (address) => address !== searchAddress, + ) as string; + }); + return pairAddresses; + }, + [pairs], + ); + + const availableAssetAddresses = useMemo(() => { + return ( + pairs + ?.reduce((acc, pair) => { + return [...acc, ...pair.asset_addresses]; + }, [] as string[]) + ?.filter((asset, index, array) => array.indexOf(asset) === index) || [] + ); + }, [pairs]); + + const getPair = useCallback( + (contractAddress: string) => { + return pairs?.find((pair) => pair.contract_addr === contractAddress); + }, + [pairs], + ); + + const findPair = useCallback( + (addresses: [string, string]) => { + if (addresses[0] === addresses[1]) { + return undefined; + } + return pairs?.find( + (pair) => + pair.asset_addresses.includes(addresses[0]) && + pair.asset_addresses.includes(addresses[1]), + ); + }, + [pairs], + ); + + const findPairByLpAddress = useCallback( + (lpAddress: string) => { + return pairs?.find((pair) => pair.liquidity_token === lpAddress); + }, + [pairs], + ); + + return useMemo( + () => ({ + pairs, + isLoading, + getPairedAddresses, + availableAssetAddresses, + getPair, + findPair, + findPairByLpAddress, + }), + [ + availableAssetAddresses, + getPairedAddresses, + pairs, + isLoading, + getPair, + findPair, + findPairByLpAddress, + ], + ); +}; + +export default usePairs; diff --git a/src/hooks/usePool.ts b/src/hooks/usePool.ts index f2aafb1c..838637e2 100644 --- a/src/hooks/usePool.ts +++ b/src/hooks/usePool.ts @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState } from "react"; -import { Pool } from "types/pair"; -import { useAPI } from "hooks/useAPI"; +import { Pool } from "types/api"; +import useAPI from "hooks/useAPI"; const usePool = (contractAddress?: string) => { const [pool, setPool] = useState(); diff --git a/src/hooks/usePools.ts b/src/hooks/usePools.ts new file mode 100644 index 00000000..bd1862f6 --- /dev/null +++ b/src/hooks/usePools.ts @@ -0,0 +1,16 @@ +import { useQuery } from "@tanstack/react-query"; +import useNetwork from "./useNetwork"; +import useAPI from "./useAPI"; + +const usePools = () => { + const network = useNetwork(); + const api = useAPI(); + const { data: pools } = useQuery(["pools", network.name], async () => { + const res = await api.getPools(); + return res; + }); + + return { pools }; +}; + +export default usePools; diff --git a/src/hooks/useVerifiedAssets.ts b/src/hooks/useVerifiedAssets.ts new file mode 100644 index 00000000..c5327069 --- /dev/null +++ b/src/hooks/useVerifiedAssets.ts @@ -0,0 +1,31 @@ +import { useQuery } from "@tanstack/react-query"; +import useAPI from "./useAPI"; +import useNetwork from "./useNetwork"; + +const useVerifiedAssets = () => { + const api = useAPI(); + const network = useNetwork(); + const { data: verifiedAssets } = useQuery({ + queryKey: ["verifiedAssets"], + queryFn: api.getVerifiedTokenInfos, + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchInterval: false, + refetchOnReconnect: true, + }); + const { data: verifiedIbcAssets } = useQuery({ + queryKey: ["verifiedIbcAssets"], + queryFn: api.getVerifiedIbcTokenInfos, + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchInterval: false, + refetchOnReconnect: true, + }); + + return { + verifiedAssets: verifiedAssets?.[network.name], + verifiedIbcAssets: verifiedIbcAssets?.[network.name], + }; +}; + +export default useVerifiedAssets; diff --git a/src/layout/Main/Footer.tsx b/src/layout/Main/Footer.tsx index ba6b4baa..7f9eff67 100644 --- a/src/layout/Main/Footer.tsx +++ b/src/layout/Main/Footer.tsx @@ -18,7 +18,7 @@ import { MOBILE_SCREEN_CLASS, TABLET_SCREEN_CLASS } from "constants/layout"; import { css } from "@emotion/react"; import { useLatestBlock } from "hooks/useLatestBlock"; import { getBlockLink } from "utils"; -import { useNetwork } from "hooks/useNetwork"; +import useNetwork from "hooks/useNetwork"; import Tooltip from "components/Tooltip"; const Wrapper = styled.footer` diff --git a/src/layout/Main/Header.tsx b/src/layout/Main/Header.tsx index 2debf76e..b4655e49 100644 --- a/src/layout/Main/Header.tsx +++ b/src/layout/Main/Header.tsx @@ -19,7 +19,7 @@ import { SMALL_BROWSER_SCREEN_CLASS, TABLET_SCREEN_CLASS, } from "constants/layout"; -import { useModal } from "hooks/useModal"; +import useModal from "hooks/useModal"; import { useConnectedWallet, useWallet } from "@xpla/wallet-provider"; import { amountToValue, @@ -28,7 +28,7 @@ import { formatNumber, getAddressLink, } from "utils"; -import { useBalance } from "hooks/useBalance"; +import useBalance from "hooks/useBalance"; import { XPLA_ADDRESS, XPLA_SYMBOL } from "constants/network"; import iconDropdown from "assets/icons/icon-dropdown-arrow.svg"; import iconXpla from "assets/icons/icon-xpla-24px.svg"; @@ -37,7 +37,7 @@ import { Popover } from "react-tiny-popover"; import Panel from "components/Panel"; import { css, useTheme } from "@emotion/react"; import Hr from "components/Hr"; -import { useNetwork } from "hooks/useNetwork"; +import useNetwork from "hooks/useNetwork"; import Box from "components/Box"; import Modal from "components/Modal"; import Copy from "components/Copy"; diff --git a/src/layout/Main/index.tsx b/src/layout/Main/index.tsx index 3f942ad4..fa9b2176 100644 --- a/src/layout/Main/index.tsx +++ b/src/layout/Main/index.tsx @@ -13,7 +13,7 @@ import iconTrade from "assets/icons/icon-trade.svg"; import iconPool from "assets/icons/icon-pool.svg"; import { useScreenClass } from "react-grid-system"; import { MOBILE_SCREEN_CLASS, TABLET_SCREEN_CLASS } from "constants/layout"; -import { useNetwork } from "hooks/useNetwork"; +import useNetwork from "hooks/useNetwork"; import Tooltip from "components/Tooltip"; import Footer from "./Footer"; diff --git a/src/main.tsx b/src/main.tsx index 2b5f8748..7fc42d21 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,7 +4,7 @@ import App from "App"; import ReactDOM from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; import theme from "styles/theme"; -import { QueryClient, QueryClientProvider } from "react-query"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import "simplebar"; import "simplebar/dist/simplebar.css"; import ResizeObserver from "resize-observer-polyfill"; diff --git a/src/pages/Pool/Create/InputGroup.tsx b/src/pages/Pool/Create/InputGroup.tsx index 4a03a2b6..b9fa555e 100644 --- a/src/pages/Pool/Create/InputGroup.tsx +++ b/src/pages/Pool/Create/InputGroup.tsx @@ -7,12 +7,13 @@ import Copy from "components/Copy"; import { NumberInput } from "components/Input"; import Typography from "components/Typography"; import { Col, Row, useScreenClass } from "react-grid-system"; -import { Asset } from "types/common"; import { formatNumber, formatDecimals, amountToValue } from "utils"; import iconDefaultToken from "assets/icons/icon-default-token.svg"; +import { Token } from "types/api"; +import useBalance from "hooks/useBalance"; interface InputGroupProps extends React.HTMLAttributes { - asset?: Partial; + asset?: Partial; onBalanceClick?( value: string, event: React.MouseEvent, @@ -44,6 +45,7 @@ const InputGroup = forwardRef( ({ asset, onBalanceClick, style, ...inputProps }, ref) => { const screenClass = useScreenClass(); const theme = useTheme(); + const balance = useBalance(asset?.token); return ( @@ -51,15 +53,12 @@ const InputGroup = forwardRef( - + {asset?.symbol} - + @@ -74,7 +73,7 @@ const InputGroup = forwardRef( onClick={(event) => { if (onBalanceClick) { onBalanceClick( - amountToValue(asset?.balance, asset?.decimals) || "", + amountToValue(balance, asset?.decimals) || "", event, ); } @@ -90,7 +89,7 @@ const InputGroup = forwardRef( > {formatNumber( formatDecimals( - amountToValue(asset?.balance, asset?.decimals) || 0, + amountToValue(balance, asset?.decimals) || 0, 3, ), )} diff --git a/src/pages/Pool/Create/index.tsx b/src/pages/Pool/Create/index.tsx index 2b775615..75bcdc8e 100644 --- a/src/pages/Pool/Create/index.tsx +++ b/src/pages/Pool/Create/index.tsx @@ -31,7 +31,7 @@ import { LOCKED_LP_SUPPLY, LP_DECIMALS } from "constants/dezswap"; import { CreateTxOptions, Numeric } from "@xpla/xpla.js"; import Typography from "components/Typography"; import useBalanceMinusFee from "hooks/useBalanceMinusFee"; -import { useFee } from "hooks/useFee"; +import useFee from "hooks/useFee"; import { XPLA_ADDRESS, XPLA_SYMBOL } from "constants/network"; import { generateCreatePoolMsg } from "utils/dezswap"; import { NetworkName } from "types/common"; @@ -40,7 +40,7 @@ import InputGroup from "pages/Pool/Provide/InputGroup"; import IconButton from "components/IconButton"; import iconLink from "assets/icons/icon-link.svg"; import useRequestPost from "hooks/useRequestPost"; -import { useNetwork } from "hooks/useNetwork"; +import useNetwork from "hooks/useNetwork"; import Message from "components/Message"; import useConnectWalletModal from "hooks/modals/useConnectWalletModal"; import InfoTable from "components/InfoTable"; @@ -146,9 +146,9 @@ function CreatePage() { const createTxOptions = useMemo( () => connectedWallet && - asset1?.address && + asset1?.token && formData.asset1Value && - asset2?.address && + asset2?.token && formData.asset2Value && !Numeric.parse(formData.asset1Value).isNaN() && !Numeric.parse(formData.asset2Value).isNaN() @@ -158,13 +158,13 @@ function CreatePage() { connectedWallet.walletAddress, [ { - address: asset1?.address || "", + address: asset1?.token || "", amount: valueToAmount(formData.asset1Value, asset1?.decimals) || "0", }, { - address: asset2?.address || "", + address: asset2?.token || "", amount: valueToAmount(formData.asset2Value, asset2?.decimals) || "0", @@ -192,22 +192,14 @@ function CreatePage() { return fee?.amount?.get(XPLA_ADDRESS)?.amount.toString() || "0"; }, [fee]); - const asset1Balance = useBalanceMinusFee( - asset1?.address, - asset1?.balance, - feeAmount, - ); - const asset2Balance = useBalanceMinusFee( - asset2?.address, - asset2?.balance, - feeAmount, - ); + const asset1Balance = useBalanceMinusFee(asset1?.token, feeAmount); + const asset2Balance = useBalanceMinusFee(asset2?.token, feeAmount); useEffect(() => { if ( connectedWallet && balanceApplied && - asset1?.address === XPLA_ADDRESS && + asset1?.token === XPLA_ADDRESS && formData.asset1Value && Numeric.parse(formData.asset1Value || 0).gt( Numeric.parse(amountToValue(asset1Balance, asset1?.decimals) || 0), @@ -227,7 +219,7 @@ function CreatePage() { if ( connectedWallet && balanceApplied && - asset2?.address === XPLA_ADDRESS && + asset2?.token === XPLA_ADDRESS && formData.asset2Value && Numeric.parse(formData.asset2Value || 0).gt( Numeric.parse(amountToValue(asset2Balance, asset2?.decimals) || 0), @@ -515,13 +507,13 @@ function CreatePage() { > ({ - key: asset?.address, + key: asset?.token, label: `${asset?.symbol} Address`, value: ( <> - {ellipsisCenter(asset?.address)}  + {ellipsisCenter(asset?.token)}  ("form"); const { customAssets, addCustomAsset } = useCustomAssets(); const { availableAssetAddresses } = usePairs(); - const { verifiedAssets, verifiedIbcAssets } = useAssets(); + const { verifiedAssets, verifiedIbcAssets } = useVerifiedAssets(); const network = useNetwork(); const api = useAPI(); @@ -67,8 +67,8 @@ function ImportAssetModal({ onFinish, ...modalProps }: ImportAssetModalProps) { ); const isDuplicated = useMemo(() => { return ( - customAssets?.some((item) => item.address === address) || - availableAssetAddresses?.addresses?.some((item) => item === address) + customAssets?.some((item) => item.token === address) || + availableAssetAddresses.some((item) => item === address) ); }, [address, availableAssetAddresses, customAssets]); @@ -100,10 +100,10 @@ function ImportAssetModal({ onFinish, ...modalProps }: ImportAssetModalProps) { const fetchAsset = async () => { if (isNativeToken) { const asset = nativeTokens[network.name]?.find( - (item) => item.address === deferredAddress, + (item) => item.token === deferredAddress, ); if (!isAborted && asset) { - setTokenInfo(asset as TokenInfo); + setTokenInfo(asset); } } else if (isIbcToken) { if (verifiedIbcAssets) { @@ -153,7 +153,14 @@ function ImportAssetModal({ onFinish, ...modalProps }: ImportAssetModalProps) { event.preventDefault(); if (tokenInfo) { - const asset = { ...tokenInfo, balance: balance || "0", address }; + const asset = { + ...tokenInfo, + chainId: network.chainID, + icon: iconSrc || "", + protocol: "", + token: address, + verified: false, + }; addCustomAsset(asset); setPage("complete"); } @@ -161,7 +168,14 @@ function ImportAssetModal({ onFinish, ...modalProps }: ImportAssetModalProps) { const onDone = () => { if (onFinish && tokenInfo) { - const asset = { ...tokenInfo, balance: balance || "0", address }; + const asset = { + ...tokenInfo, + chainId: network.chainID, + icon: iconSrc || "", + protocol: "", + token: address, + verified: false, + }; onFinish(asset); } }; diff --git a/src/pages/Pool/PoolForm.tsx b/src/pages/Pool/PoolForm.tsx index 1ae55a6c..bc831130 100644 --- a/src/pages/Pool/PoolForm.tsx +++ b/src/pages/Pool/PoolForm.tsx @@ -9,7 +9,7 @@ import Typography from "components/Typography"; import { MOBILE_SCREEN_CLASS, TABLET_SCREEN_CLASS } from "constants/layout"; import useAssets from "hooks/useAssets"; import useHashModal from "hooks/useHashModal"; -import usePairs from "hooks/usePair"; +import usePairs from "hooks/usePairs"; import { formatNumber, formatDecimals, @@ -21,10 +21,11 @@ import iconDropdown from "assets/icons/icon-dropdown-arrow.svg"; import iconDefaultAsset from "assets/icons/icon-default-token.svg"; import Button from "components/Button"; -import { useModal } from "hooks/useModal"; +import useModal from "hooks/useModal"; import { useAtom } from "jotai"; -import { useNetwork } from "hooks/useNetwork"; +import useNetwork from "hooks/useNetwork"; import { customAssetsAtom } from "stores/assets"; +import useBalance from "hooks/useBalance"; import PoolButton from "./PoolButton"; import ImportAssetModal from "./ImportAssetModal"; @@ -61,8 +62,8 @@ function PoolForm({ addresses, onChange: handleChange }: PoolFormProps) { const network = useNetwork(); const [customAssetStore] = useAtom(customAssetsAtom); const customAssetAddresses = useMemo(() => { - return customAssetStore[network.name]?.map((asset) => asset.address) || []; - }, [customAssetStore, [network.name]]); + return customAssetStore[network.name]?.map((asset) => asset.token) || []; + }, [customAssetStore, network.name]); const { getAsset } = useAssets(); @@ -76,6 +77,10 @@ function PoolForm({ addresses, onChange: handleChange }: PoolFormProps) { return address ? getAsset(address) : undefined; }); }, [getAsset, selectedAddress1, selectedAddress2]); + + const balance1 = useBalance(asset1?.token); + const balance2 = useBalance(asset2?.token); + const pair = useMemo(() => { return selectedAddress1 && selectedAddress2 ? findPair([selectedAddress1, selectedAddress2]) @@ -105,9 +110,19 @@ function PoolForm({ addresses, onChange: handleChange }: PoolFormProps) { `} > {[ - { key: "asset1", asset: asset1, modal: selectAsset1Modal }, - { key: "asset2", asset: asset2, modal: selectAsset2Modal }, - ].map(({ key, asset, modal }) => ( + { + key: "asset1", + asset: asset1, + modal: selectAsset1Modal, + balance: balance1, + }, + { + key: "asset2", + asset: asset2, + modal: selectAsset2Modal, + balance: balance2, + }, + ].map(({ key, asset, modal, balance }) => ( modal.open()}> {formatNumber( formatDecimals( - amountToValue(asset?.balance, asset?.decimals) || 0, + amountToValue(balance, asset?.decimals) || 0, 3, ), )} @@ -270,10 +285,7 @@ function PoolForm({ addresses, onChange: handleChange }: PoolFormProps) { }} > { if (handleChange) { const newAddresses = [...(addresses || [])]; diff --git a/src/pages/Pool/PoolItem.tsx b/src/pages/Pool/PoolItem.tsx index ffad9a27..93d5937c 100644 --- a/src/pages/Pool/PoolItem.tsx +++ b/src/pages/Pool/PoolItem.tsx @@ -4,8 +4,8 @@ import IconButton from "components/IconButton"; import Typography from "components/Typography"; import { LP_DECIMALS } from "constants/dezswap"; import useAssets from "hooks/useAssets"; -import { useBalance } from "hooks/useBalance"; -import { useNetwork } from "hooks/useNetwork"; +import useBalance from "hooks/useBalance"; +import useNetwork from "hooks/useNetwork"; import { useEffect, useMemo, useRef, useState } from "react"; import { Row, Col, useScreenClass } from "react-grid-system"; import { @@ -24,8 +24,9 @@ import Button from "components/Button"; import { Link } from "react-router-dom"; import { MOBILE_SCREEN_CLASS, TABLET_SCREEN_CLASS } from "constants/layout"; import Tooltip from "components/Tooltip"; +import { Pool } from "types/api"; +import usePairs from "hooks/usePairs"; import Expand from "./Expand"; -import { PoolExtended } from "."; const SimplePieChart = styled.div<{ data: number[] }>` width: 100%; @@ -145,7 +146,7 @@ const IconButtonAnchor = styled(IconButton.withComponent("a"))` `; interface PoolItemProps { - pool: PoolExtended; + pool: Pool; bookmarked?: boolean; onBookmarkClick?: React.MouseEventHandler; } @@ -157,11 +158,13 @@ function PoolItem({ pool, bookmarked, onBookmarkClick }: PoolItemProps) { ); const { getAsset } = useAssets(); const network = useNetwork(); - const lpBalance = useBalance(pool.pair.liquidity_token); + const { getPair } = usePairs(); + const pair = useMemo(() => getPair(pool.address), [getPair, pool]); + const lpBalance = useBalance(pair?.liquidity_token); const [asset1, asset2] = useMemo( - () => pool.pair.asset_addresses.map((address) => getAsset(address)), - [getAsset, pool], + () => pair?.asset_addresses.map((address) => getAsset(address)) || [], + [getAsset, pair], ); const userShare = useMemo(() => { @@ -184,7 +187,11 @@ function PoolItem({ pool, bookmarked, onBookmarkClick }: PoolItemProps) { />, , ], - [bookmarked, network, onBookmarkClick, pool], + [bookmarked, network, onBookmarkClick, pair], ); const [overflowActive, setOverflowActive] = useState(false); @@ -232,9 +239,9 @@ function PoolItem({ pool, bookmarked, onBookmarkClick }: PoolItemProps) { font-size: 0; `} > - + diff --git a/src/pages/Pool/PoolList.tsx b/src/pages/Pool/PoolList.tsx index 258a0900..8a03ecf1 100644 --- a/src/pages/Pool/PoolList.tsx +++ b/src/pages/Pool/PoolList.tsx @@ -7,11 +7,11 @@ import { MOBILE_SCREEN_CLASS, TABLET_SCREEN_CLASS } from "constants/layout"; import usePairBookmark from "hooks/usePairBookmark"; import { useScreenClass } from "react-grid-system"; import iconSortDisabled from "assets/icons/icon-sort-disabled.svg"; -import { PoolExtended } from "."; +import { Pool } from "types/api"; import PoolItem from "./PoolItem"; interface PoolListProps { - pools: PoolExtended[]; + pools: Pool[]; emptyMessage?: string; } @@ -129,10 +129,10 @@ function PoolList({ {pools.map((pool) => { return ( toggleBookmark(pool.pair.contract_addr)} + bookmarked={bookmarks?.includes(pool.address)} + onBookmarkClick={() => toggleBookmark(pool.address)} /> ); })} diff --git a/src/pages/Pool/Provide/InputGroup.tsx b/src/pages/Pool/Provide/InputGroup.tsx index 8c40618f..e5ae743e 100644 --- a/src/pages/Pool/Provide/InputGroup.tsx +++ b/src/pages/Pool/Provide/InputGroup.tsx @@ -7,12 +7,13 @@ import Copy from "components/Copy"; import { NumberInput } from "components/Input"; import Typography from "components/Typography"; import { Col, Row, useScreenClass } from "react-grid-system"; -import { Asset } from "types/common"; import { formatNumber, formatDecimals, amountToValue } from "utils"; import iconDefaultToken from "assets/icons/icon-default-token.svg"; +import { Token } from "types/api"; +import useBalance from "hooks/useBalance"; interface InputGroupProps extends React.HTMLAttributes { - asset?: Partial | null; + asset?: Partial | null; onBalanceClick?( value: string, event: React.MouseEvent, @@ -44,6 +45,7 @@ const InputGroup = forwardRef( ({ asset, onBalanceClick, style, ...inputProps }, ref) => { const screenClass = useScreenClass(); const theme = useTheme(); + const balance = useBalance(asset?.token); return ( @@ -51,15 +53,12 @@ const InputGroup = forwardRef( - + {asset?.symbol} - + @@ -74,7 +73,7 @@ const InputGroup = forwardRef( onClick={(event) => { if (onBalanceClick) { onBalanceClick( - amountToValue(asset?.balance, asset?.decimals) || "", + amountToValue(balance, asset?.decimals) || "", event, ); } @@ -90,7 +89,7 @@ const InputGroup = forwardRef( > {formatNumber( formatDecimals( - amountToValue(asset?.balance, asset?.decimals) || 0, + amountToValue(balance, asset?.decimals) || 0, 3, ), )} diff --git a/src/pages/Pool/Provide/index.tsx b/src/pages/Pool/Provide/index.tsx index 83bd0fa8..c3e75e5b 100644 --- a/src/pages/Pool/Provide/index.tsx +++ b/src/pages/Pool/Provide/index.tsx @@ -7,7 +7,7 @@ import { } from "react"; import Modal from "components/Modal"; import { useNavigate, useParams } from "react-router-dom"; -import usePairs from "hooks/usePair"; +import usePairs from "hooks/usePairs"; import useAssets from "hooks/useAssets"; import { useForm } from "react-hook-form"; import { Col, Row, useScreenClass } from "react-grid-system"; @@ -31,7 +31,7 @@ import { LOCKED_LP_SUPPLY, LP_DECIMALS } from "constants/dezswap"; import { AccAddress, CreateTxOptions, Numeric } from "@xpla/xpla.js"; import Typography from "components/Typography"; import useBalanceMinusFee from "hooks/useBalanceMinusFee"; -import { useFee } from "hooks/useFee"; +import useFee from "hooks/useFee"; import { XPLA_ADDRESS, XPLA_SYMBOL } from "constants/network"; import { generateAddLiquidityMsg } from "utils/dezswap"; import { NetworkName } from "types/common"; @@ -41,7 +41,7 @@ import InputGroup from "pages/Pool/Provide/InputGroup"; import IconButton from "components/IconButton"; import iconLink from "assets/icons/icon-link.svg"; import useRequestPost from "hooks/useRequestPost"; -import { useNetwork } from "hooks/useNetwork"; +import useNetwork from "hooks/useNetwork"; import usePool from "hooks/usePool"; import Message from "components/Message"; import useConnectWalletModal from "hooks/modals/useConnectWalletModal"; @@ -135,18 +135,16 @@ function ProvidePage() { isPoolEmpty ? { pairAddress: pairAddress || "", - asset1Address: asset1?.address || "", + asset1Address: asset1?.token || "", asset1Amount: valueToAmount(formData.asset1Value, asset1?.decimals) || "0", - asset2Address: asset2?.address || "", + asset2Address: asset2?.token || "", asset2Amount: valueToAmount(formData.asset2Value, asset2?.decimals) || "0", } : { pairAddress: pairAddress || "", - asset1Address: isReversed - ? asset2?.address || "" - : asset1?.address || "", + asset1Address: isReversed ? asset2?.token || "" : asset1?.token || "", asset1Amount: isReversed ? valueToAmount(formData.asset2Value, asset2?.decimals) || "0" : valueToAmount(formData.asset1Value, asset1?.decimals) || "0", @@ -158,9 +156,9 @@ function ProvidePage() { simulationResult?.estimatedAmount && !simulationResult?.isLoading && connectedWallet && - asset1?.address && + asset1?.token && formData.asset1Value && - asset2?.address && + asset2?.token && formData.asset2Value && !Numeric.parse(formData.asset1Value).isNaN() && !Numeric.parse(formData.asset2Value).isNaN() @@ -171,13 +169,13 @@ function ProvidePage() { pairAddress || "", [ { - address: asset1?.address || "", + address: asset1?.token || "", amount: valueToAmount(formData.asset1Value, asset1?.decimals) || "0", }, { - address: asset2?.address || "", + address: asset2?.token || "", amount: valueToAmount(formData.asset2Value, asset2?.decimals) || "0", @@ -209,17 +207,9 @@ function ProvidePage() { return fee?.amount?.get(XPLA_ADDRESS)?.amount.toString() || "0"; }, [fee]); - const asset1BalanceMinusFee = useBalanceMinusFee( - asset1?.address, - asset1?.balance, - feeAmount, - ); + const asset1BalanceMinusFee = useBalanceMinusFee(asset1?.token, feeAmount); - const asset2BalanceMinusFee = useBalanceMinusFee( - asset2?.address, - asset2?.balance, - feeAmount, - ); + const asset2BalanceMinusFee = useBalanceMinusFee(asset2?.token, feeAmount); const buttonMsg = useMemo(() => { if (formData.asset1Value) { @@ -293,7 +283,7 @@ function ProvidePage() { connectedWallet && balanceApplied && (!isReversed || isPoolEmpty) && - asset1?.address === XPLA_ADDRESS && + asset1?.token === XPLA_ADDRESS && formData.asset1Value && Numeric.parse(formData.asset1Value || 0).gt( Numeric.parse( @@ -316,7 +306,7 @@ function ProvidePage() { connectedWallet && balanceApplied && (isReversed || isPoolEmpty) && - asset2?.address === XPLA_ADDRESS && + asset2?.token === XPLA_ADDRESS && formData.asset2Value && Numeric.parse(formData.asset2Value || 0).gt( Numeric.parse( @@ -592,7 +582,7 @@ function ProvidePage() { "token" in a.info ? a.info.token.contract_addr : a.info.native_token.denom === - asset1?.address, + asset1?.token, )?.amount, asset1?.decimals, ) || "0", @@ -619,7 +609,7 @@ function ProvidePage() { "token" in a.info ? a.info.token.contract_addr : a.info.native_token.denom === - asset2?.address, + asset2?.token, )?.amount, asset2?.decimals, ) || "0", @@ -706,9 +696,9 @@ function ProvidePage() { label: `${asset1?.symbol || ""} Address`, value: ( - {ellipsisCenter(asset1?.address)}  + {ellipsisCenter(asset1?.token)}  - {ellipsisCenter(asset2?.address)}  + {ellipsisCenter(asset2?.token)}  { lpToken?: string; - assets?: (Partial | undefined)[]; + assets?: (Partial | undefined)[]; onBalanceClick?( value: string, event: React.MouseEvent, @@ -46,7 +46,7 @@ const InputGroup = forwardRef( {assets?.[0]?.symbol}( /> {assets?.[0]?.symbol} -  {assets?.[1]?.symbol} pair - ? pair.asset_addresses - .map((address) => getAsset(address)) - .map((a) => ({ - address: a?.address, - symbol: a?.symbol, - decimals: a?.decimals, - iconSrc: a?.iconSrc, - })) + ? pair.asset_addresses.map((address) => getAsset(address)) : [undefined, undefined], [getAsset, pair], ); useEffect(() => { const timerId = setTimeout(() => { - if (!asset1?.address || !asset2?.address) { + if (!asset1?.token || !asset2?.token) { errorMessageModal.open(); } }, 1500); @@ -126,7 +113,7 @@ function WithdrawPage() { criteriaMode: "all", mode: "all", }); - const { register, formState } = form; + const { register } = form; const { lpValue } = form.watch(); @@ -161,7 +148,7 @@ function WithdrawPage() { pairAddress || "", pair?.liquidity_token || "", valueToAmount(lpValue, LP_DECIMALS) || "0", - [asset1?.address, asset2?.address].map((address) => ({ + [asset1?.token, asset2?.token].map((address) => ({ address: address || "", amount: Numeric.parse( simulationResult?.estimatedAmount?.find( @@ -301,7 +288,7 @@ function WithdrawPage() { `} > {asset1?.symbol} a.address === asset1?.address, + (a) => a.address === asset1?.token, )?.amount, asset1?.decimals, ) || "0", @@ -383,7 +370,7 @@ function WithdrawPage() { `} > {asset2?.symbol} a.address === asset2?.address, + (a) => a.address === asset2?.token, )?.amount, asset2?.decimals, ) || "0", @@ -467,7 +454,7 @@ function WithdrawPage() { value: `${formatNumber( amountToValue( simulationResult?.estimatedAmount?.find( - (a) => a.address === asset1?.address, + (a) => a.address === asset1?.token, )?.amount, asset1?.decimals, ) || "", @@ -475,7 +462,7 @@ function WithdrawPage() { ${formatNumber( amountToValue( simulationResult?.estimatedAmount?.find( - (a) => a.address === asset2?.address, + (a) => a.address === asset2?.token, )?.amount, asset1?.decimals, ) || "", @@ -540,9 +527,9 @@ function WithdrawPage() { label: `${asset1?.symbol} Address`, value: ( <> - {ellipsisCenter(asset1?.address)}  + {ellipsisCenter(asset1?.token)}  - {ellipsisCenter(asset2?.address)}  + {ellipsisCenter(asset2?.token)}  ([]); + const { pools } = usePools(); const [selectedTabIndex, setSelectedTabIndex] = useState(0); const [addresses, setAddresses] = useState<[string | undefined, string | undefined]>(); const [selectedPair, setSelectedPair] = useState(); + const api = useAPI(); + + const balanceQueryResults = useQueries({ + queries: + pools?.map((pool) => ({ + queryKey: ["pool", pool.address], + queryFn: async () => { + const lpAddress = getPair(pool.address)?.liquidity_token; + if (lpAddress) { + const balance = await api.getTokenBalance(lpAddress); + return balance; + } + return "0"; + }, + enabled: !!pool.address, + refetchInterval: 30000, + refetchOnMount: false, + refetchOnReconnect: true, + refetchOnWindowFocus: false, + })) || [], + }); + + const balances = useMemo(() => { + return balanceQueryResults.map((item) => item.data); + }, [balanceQueryResults]); const poolList = useMemo(() => { - return pools.filter((item) => { + return pools?.filter((item, index) => { const isSelectedPair = - !selectedPair || item.pair.contract_addr === selectedPair.contract_addr; + !selectedPair || item.address === selectedPair.contract_addr; switch (selectedTabIndex) { case 0: return isSelectedPair; case 1: - return isSelectedPair && item.hasBalance; + return isSelectedPair && Numeric.parse(balances[index] || 0).gt(0); case 2: - return ( - isSelectedPair && !!bookmarks?.includes(item.pair.contract_addr) - ); + return isSelectedPair && !!bookmarks?.includes(item.address); default: // do nothing } return false; }); - }, [bookmarks, pools, selectedTabIndex, selectedPair]); + }, [pools, selectedPair, selectedTabIndex, balances, bookmarks]); const handleReloadClick = useCallback(() => { setAddresses(undefined); @@ -89,37 +106,6 @@ function PoolPage() { } }, [addresses, findPair]); - useEffect(() => { - let isAborted = false; - const fetchPools = async () => { - if (pairs) { - const res = await Promise.all( - pairs.map(async (item) => { - if (isAborted) { - return undefined; - } - const pool = await api.getPool(item.contract_addr); - let hasBalance = false; - try { - const balance = await api.getTokenBalance(item.liquidity_token); - hasBalance = Numeric.parse(balance || 0).gt(0); - } catch (error) { - console.log(error); - } - return { ...pool, pair: item, hasBalance }; - }), - ); - if (!isAborted) { - setPools(res.filter((pool) => pool) as PoolExtended[]); - } - } - }; - fetchPools(); - return () => { - isAborted = true; - }; - }, [api, pairs, location]); - useEffect(() => { setCurrentPage(1); }, [selectedTabIndex]); @@ -250,10 +236,12 @@ function PoolPage() { `} >
- {!!poolList.length && ( + {!!poolList?.length && ( { if (asset1 === undefined || asset2 === undefined) { @@ -428,11 +424,9 @@ function SwapPage() { }} > { const target = selectAsset1Modal.isOpen @@ -501,7 +495,7 @@ function SwapPage() { height: 20px; position: relative; background-image: ${`url(${ - asset1?.iconSrc || iconDefaultAsset + asset1?.icon || iconDefaultAsset })`}; background-position: 50% 50%; background-size: auto 20px; @@ -718,7 +712,7 @@ function SwapPage() { height: 20px; position: relative; background-image: ${`url(${ - asset2?.iconSrc || iconDefaultAsset + asset2?.icon || iconDefaultAsset })`}; background-position: 50% 50%; background-size: auto 20px; diff --git a/src/pages/Trade/Swap/useSimulate.ts b/src/pages/Trade/Swap/useSimulate.ts index f33edb62..c35da73c 100644 --- a/src/pages/Trade/Swap/useSimulate.ts +++ b/src/pages/Trade/Swap/useSimulate.ts @@ -1,10 +1,10 @@ import { useDeferredValue, useEffect, useMemo, useRef, useState } from "react"; import { Numeric } from "@xpla/xpla.js"; import { ReverseSimulation, Simulation } from "types/pair"; -import usePairs from "hooks/usePair"; -import { useNetwork } from "hooks/useNetwork"; -import { useAPI } from "hooks/useAPI"; -import { useLCDClient } from "hooks/useLCDClient"; +import usePairs from "hooks/usePairs"; +import useNetwork from "hooks/useNetwork"; +import useAPI from "hooks/useAPI"; +import useLCDClient from "hooks/useLCDClient"; const useSimulate = ({ fromAddress, diff --git a/src/stores/assets.ts b/src/stores/assets.ts index b73477ac..51898fde 100644 --- a/src/stores/assets.ts +++ b/src/stores/assets.ts @@ -1,23 +1,15 @@ import { atomWithStorage } from "jotai/utils"; -import { Asset, NetworkName } from "types/common"; -import { atom } from "jotai"; -import { VerifiedAssets, VerifiedIbcAssets } from "types/token"; +import { NetworkName } from "types/common"; +import { Token } from "types/api"; -const assetsAtom = atomWithStorage<{ - [K in NetworkName]?: Asset[]; -}>("assets", { mainnet: [], testnet: [] }); +interface CustomToken extends Token { + updatedAt?: Date; +} export const customAssetsAtom = atomWithStorage<{ - [K in NetworkName]?: Asset[]; + [K in NetworkName]?: CustomToken[]; }>("customAssets", { mainnet: [], testnet: [] }); -export const verifiedAssetsAtom = atom(undefined); -export const verifiedIbcAssetsAtom = atom( - undefined, -); - export const bookmarksAtom = atomWithStorage<{ [K in NetworkName]?: string[]; }>("bookmarks", { mainnet: [], testnet: [] }); - -export default assetsAtom; diff --git a/src/stores/pairs.ts b/src/stores/pairs.ts index 4d70c201..b1e0fb60 100644 --- a/src/stores/pairs.ts +++ b/src/stores/pairs.ts @@ -1,15 +1,6 @@ -import { atom } from "jotai"; import { atomWithStorage } from "jotai/utils"; -import { NetworkName, PairExtended } from "types/common"; - -const pairsAtom = atom<{ - [K in NetworkName]?: { data?: PairExtended[] }; -}>({}); - -export const isPairsLoadingAtom = atom(false); +import { NetworkName } from "types/common"; export const bookmarksAtom = atomWithStorage<{ [K in NetworkName]?: string[]; }>("pair-bookmarks", { mainnet: [], testnet: [] }); - -export default pairsAtom; diff --git a/src/types/api.d.ts b/src/types/api.d.ts new file mode 100644 index 00000000..ce7c7437 --- /dev/null +++ b/src/types/api.d.ts @@ -0,0 +1,45 @@ +export type AssetInfo = + | { + token: { + contract_addr: string; + }; + } + | { + native_token: { + denom: string; + }; + }; + +export interface Pair { + asset_decimals: [number, number]; + asset_infos: [AssetInfo, AssetInfo]; + contract_addr: string; + liquidity_token: string; +} + +export interface Pairs { + pairs: Pair[]; +} + +export interface PoolAsset { + info: AssetInfo; + amount: string; +} + +export interface Pool { + address: string; + assets: [PoolAsset, PoolAsset]; + total_share: string; +} + +export interface Token { + chainId: string; + decimals: number; + icon: string; + name: string; + protocol: string; + symbol: string; + token: string; + total_supply: string; + verified: boolean; +} diff --git a/src/types/common.d.ts b/src/types/common.d.ts index 3724afd1..f458db60 100644 --- a/src/types/common.d.ts +++ b/src/types/common.d.ts @@ -6,7 +6,6 @@ import { TxFailed, TxUnspecifiedError, } from "@xpla/wallet-provider"; -import { TokenInfo } from "types/token"; import { Pair } from "types/pair"; export type TxError = @@ -19,12 +18,12 @@ export type TxError = export type NetworkName = "testnet" | "mainnet"; -export interface Asset extends TokenInfo { - address: string; - iconSrc?: string; - balance: string; - updatedAt?: Date | string | number; -} +// export interface Asset extends TokenInfo { +// address: string; +// iconSrc?: string; +// balance: string; +// updatedAt?: Date | string | number; +// } export interface PairExtended extends Pair { asset_addresses: string[]; diff --git a/src/types/factory.d.ts b/src/types/factory.d.ts deleted file mode 100644 index 67809321..00000000 --- a/src/types/factory.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Pair } from "types/pair"; - -export interface Pairs { - pairs: Pair[]; -} diff --git a/src/types/router.d.ts b/src/types/router.d.ts deleted file mode 100644 index 5a84cb82..00000000 --- a/src/types/router.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type SimulateSwapOperations = { - amount: string; -}; diff --git a/src/utils/index.ts b/src/utils/index.ts index c7d6efcf..26833f27 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -21,7 +21,7 @@ export const cutDecimal = (value: Numeric.Input, decimals: number) => Numeric.parse(value).toFixed(decimals, Decimal.ROUND_FLOOR); export const isNativeTokenAddress = (network: string, address: string) => - nativeTokens[network].filter((n) => n.address === address).length > 0; + nativeTokens[network].filter((n) => n.token === address).length > 0; export const ellipsisCenter = (text = "", letterCountPerSide = 6) => { if (text.length <= letterCountPerSide * 2 + 3) { diff --git a/yarn.lock b/yarn.lock index ecef9c8a..50b48039 100644 --- a/yarn.lock +++ b/yarn.lock @@ -176,7 +176,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.19.0" -"@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.7", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2": +"@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.7": version "7.21.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.5.tgz#8492dddda9644ae3bda3b45eabe87382caee7200" integrity sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q== @@ -807,6 +807,19 @@ "@svgr/hast-util-to-babel-ast" "^7.0.0" svg-parser "^2.0.4" +"@tanstack/query-core@4.29.7": + version "4.29.7" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.29.7.tgz#9fe4587e23cb9566b937c518ffa44226041d388d" + integrity sha512-GXG4b5hV2Loir+h2G+RXhJdoZhJLnrBWsuLB2r0qBRyhWuXq9w/dWxzvpP89H0UARlH6Mr9DiVj4SMtpkF/aUA== + +"@tanstack/react-query@^4.29.7": + version "4.29.7" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.29.7.tgz#772996905a81ca64172582891c5a82e88dbafccd" + integrity sha512-ijBWEzAIo09fB1yd22slRZzprrZ5zMdWYzBnCg5qiXuFbH78uGN1qtGz8+Ed4MuhaPaYSD+hykn+QEKtQviEtg== + dependencies: + "@tanstack/query-core" "4.29.7" + use-sync-external-store "^1.2.0" + "@terra-money/legacy.proto@npm:@terra-money/terra.proto@^0.1.7": version "0.1.7" resolved "https://registry.yarnpkg.com/@terra-money/terra.proto/-/terra.proto-0.1.7.tgz#59c18f30da10d43200bab3ba8feb5b17e43a365f" @@ -1493,11 +1506,6 @@ big-integer@1.6.36: resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.36.tgz#78631076265d4ae3555c04f85e7d9d2f3a071a36" integrity sha512-t70bfa7HYEA1D9idDbmuv7YbsbVkQ+Hp+8KFSul4aE5e/i1bjCNIRYJZlA8Q8p0r9T8cF/RVvwUgRA//FydEyg== -big-integer@^1.6.16: - version "1.6.51" - resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" - integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== - bindings@^1.3.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" @@ -1565,20 +1573,6 @@ braces@^3.0.2: dependencies: fill-range "^7.0.1" -broadcast-channel@^3.4.1: - version "3.7.0" - resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-3.7.0.tgz#2dfa5c7b4289547ac3f6705f9c00af8723889937" - integrity sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg== - dependencies: - "@babel/runtime" "^7.7.2" - detect-node "^2.1.0" - js-sha3 "0.8.0" - microseconds "0.2.0" - nano-time "1.0.0" - oblivious-set "1.0.0" - rimraf "3.0.2" - unload "2.2.0" - brorand@^1.0.1, brorand@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" @@ -2072,11 +2066,6 @@ detect-browser@5.2.0: resolved "https://registry.yarnpkg.com/detect-browser/-/detect-browser-5.2.0.tgz#c9cd5afa96a6a19fda0bbe9e9be48a6b6e1e9c97" integrity sha512-tr7XntDAu50BVENgQfajMLzacmSe34D+qZc4zjnniz0ZVuw/TZcLcyxHQjYpJTM36sGEkZZlYLnIM1hH7alTMA== -detect-node@^2.0.4, detect-node@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" - integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== - diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" @@ -3419,14 +3408,6 @@ magic-string@^0.27.0: dependencies: "@jridgewell/sourcemap-codec" "^1.4.13" -match-sorter@^6.0.2: - version "6.3.1" - resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.3.1.tgz#98cc37fda756093424ddf3cbc62bfe9c75b92bda" - integrity sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw== - dependencies: - "@babel/runtime" "^7.12.5" - remove-accents "0.4.2" - md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -3454,11 +3435,6 @@ micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.2" picomatch "^2.3.1" -microseconds@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.2.0.tgz#233b25f50c62a65d861f978a4a4f8ec18797dc39" - integrity sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA== - miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" @@ -3531,13 +3507,6 @@ nan@^2.13.2: resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== -nano-time@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef" - integrity sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA== - dependencies: - big-integer "^1.6.16" - nanoid@^3.3.6: version "3.3.6" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" @@ -3681,11 +3650,6 @@ object.values@^1.1.6: define-properties "^1.1.4" es-abstract "^1.20.4" -oblivious-set@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/oblivious-set/-/oblivious-set-1.0.0.tgz#c8316f2c2fb6ff7b11b6158db3234c49f733c566" - integrity sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw== - once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -4050,15 +4014,6 @@ react-modal@^3.16.1: react-lifecycles-compat "^3.0.0" warning "^4.0.3" -react-query@^3.39.2: - version "3.39.3" - resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.39.3.tgz#4cea7127c6c26bdea2de5fb63e51044330b03f35" - integrity sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g== - dependencies: - "@babel/runtime" "^7.5.5" - broadcast-channel "^3.4.1" - match-sorter "^6.0.2" - react-refresh@^0.14.0: version "0.14.0" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e" @@ -4114,11 +4069,6 @@ regexp.prototype.flags@^1.4.3, regexp.prototype.flags@^1.5.0: define-properties "^1.2.0" functions-have-names "^1.2.3" -remove-accents@0.4.2: - version "0.4.2" - resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5" - integrity sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA== - require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -4175,7 +4125,7 @@ rfdc@^1.3.0: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== -rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: +rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -4693,14 +4643,6 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" -unload@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/unload/-/unload-2.2.0.tgz#ccc88fdcad345faa06a92039ec0f80b488880ef7" - integrity sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA== - dependencies: - "@babel/runtime" "^7.6.2" - detect-node "^2.0.4" - update-browserslist-db@^1.0.10: version "1.0.11" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" @@ -4724,6 +4666,11 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" +use-sync-external-store@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + utf-8-validate@^5.0.5: version "5.0.10" resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2" From 7dc7e4f6c36d8a60373244a8d7721d9f2e7dbb5c Mon Sep 17 00:00:00 2001 From: maro Date: Sat, 10 Jun 2023 17:40:36 +0900 Subject: [PATCH 02/54] feat: get token info from contract in import token modal --- src/pages/Pool/ImportAssetModal.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pages/Pool/ImportAssetModal.tsx b/src/pages/Pool/ImportAssetModal.tsx index 191b8033..abfd95be 100644 --- a/src/pages/Pool/ImportAssetModal.tsx +++ b/src/pages/Pool/ImportAssetModal.tsx @@ -33,6 +33,7 @@ import { nativeTokens } from "constants/network"; import imgSuccess from "assets/images/success-import.svg"; import useVerifiedAssets from "hooks/useVerifiedAssets"; import { Token } from "types/api"; +import useLCDClient from "hooks/useLCDClient"; interface ImportAssetModalProps extends ReactModal.Props { onFinish?(asset: Token): void; @@ -47,6 +48,7 @@ function ImportAssetModal({ onFinish, ...modalProps }: ImportAssetModalProps) { const { availableAssetAddresses } = usePairs(); const { verifiedAssets, verifiedIbcAssets } = useVerifiedAssets(); const network = useNetwork(); + const lcd = useLCDClient(); const api = useAPI(); @@ -117,7 +119,9 @@ function ImportAssetModal({ onFinish, ...modalProps }: ImportAssetModalProps) { } } else if (isValidAddress) { try { - const res = await api.getToken(deferredAddress); + const res = await lcd.wasm.contractQuery(address, { + token_info: {}, + }); if (!isAborted) { setTokenInfo(res); } @@ -141,7 +145,9 @@ function ImportAssetModal({ onFinish, ...modalProps }: ImportAssetModalProps) { isNativeToken, isIbcToken, verifiedIbcAssets, - network.name, + network, + lcd, + address, ]); const onSubmit: React.FormEventHandler = (event) => { From 111c724768ef1642dbb1ef7f3840c15b5f744e12 Mon Sep 17 00:00:00 2001 From: maro Date: Sat, 10 Jun 2023 17:42:15 +0900 Subject: [PATCH 03/54] chore: remove unnecessary lines --- src/pages/Pool/index.tsx | 8 ++------ src/types/common.d.ts | 7 ------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/pages/Pool/index.tsx b/src/pages/Pool/index.tsx index 848ad974..33b583c3 100644 --- a/src/pages/Pool/index.tsx +++ b/src/pages/Pool/index.tsx @@ -51,7 +51,7 @@ function PoolPage() { const [selectedPair, setSelectedPair] = useState(); const api = useAPI(); - const balanceQueryResults = useQueries({ + const balances = useQueries({ queries: pools?.map((pool) => ({ queryKey: ["pool", pool.address], @@ -69,11 +69,7 @@ function PoolPage() { refetchOnReconnect: true, refetchOnWindowFocus: false, })) || [], - }); - - const balances = useMemo(() => { - return balanceQueryResults.map((item) => item.data); - }, [balanceQueryResults]); + }).map((item) => item.data); const poolList = useMemo(() => { return pools?.filter((item, index) => { diff --git a/src/types/common.d.ts b/src/types/common.d.ts index f458db60..17684ab8 100644 --- a/src/types/common.d.ts +++ b/src/types/common.d.ts @@ -18,13 +18,6 @@ export type TxError = export type NetworkName = "testnet" | "mainnet"; -// export interface Asset extends TokenInfo { -// address: string; -// iconSrc?: string; -// balance: string; -// updatedAt?: Date | string | number; -// } - export interface PairExtended extends Pair { asset_addresses: string[]; } From b52bfd43bd8d5f51fe85f5c020ec4c0c1aed8ad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A7=88=EB=A1=9C?= Date: Wed, 21 Jun 2023 17:33:00 +0900 Subject: [PATCH 04/54] feat: dezswap api integration (#204) --- package.json | 2 +- src/App.tsx | 39 +-- src/api/index.ts | 45 ++++ .../Modal/TxBroadcastingModal/index.tsx | 4 +- src/components/Select/index.tsx | 2 +- src/components/SelectAssetForm/AssetItem.tsx | 251 ++++++++++++++++++ src/components/SelectAssetForm/index.tsx | 238 ++--------------- src/constants/network.ts | 20 +- src/hooks/modals/useConfirmationModal.tsx | 2 +- src/hooks/modals/useConnectWalletModal.tsx | 2 +- src/hooks/modals/useFirstProvideModal.tsx | 2 +- src/hooks/modals/useInvalidPathModal.tsx | 2 +- src/hooks/modals/useTxBroadcastingModal.tsx | 2 +- src/hooks/useAPI.ts | 206 ++++---------- src/hooks/useAssets.ts | 165 ++---------- src/hooks/useBalance.ts | 82 +++--- src/hooks/useBalanceMinusFee.ts | 16 +- src/hooks/useBookmark.ts | 2 +- src/hooks/useCustomAssets.ts | 118 ++++---- src/hooks/useFee.ts | 6 +- src/hooks/useIsInViewport.ts | 27 ++ src/hooks/useLCDClient.ts | 6 +- src/hooks/useLatestBlock.ts | 32 +-- src/hooks/useModal.ts | 4 +- src/hooks/useNetwork.ts | 4 +- src/hooks/usePair.ts | 189 ------------- src/hooks/usePairBookmark.ts | 2 +- src/hooks/usePairs.ts | 105 ++++++++ src/hooks/usePool.ts | 4 +- src/hooks/usePools.ts | 16 ++ src/hooks/useVerifiedAssets.ts | 31 +++ src/layout/Main/Footer.tsx | 2 +- src/layout/Main/Header.tsx | 6 +- src/layout/Main/index.tsx | 2 +- src/main.tsx | 2 +- src/pages/Pool/Create/InputGroup.tsx | 17 +- src/pages/Pool/Create/index.tsx | 34 +-- src/pages/Pool/ImportAssetModal.tsx | 52 ++-- src/pages/Pool/PoolForm.tsx | 40 ++- src/pages/Pool/PoolItem.tsx | 33 ++- src/pages/Pool/PoolList.tsx | 10 +- src/pages/Pool/Provide/InputGroup.tsx | 17 +- src/pages/Pool/Provide/index.tsx | 50 ++-- src/pages/Pool/Select.tsx | 2 +- src/pages/Pool/Withdraw/InputGroup.tsx | 10 +- src/pages/Pool/Withdraw/index.tsx | 51 ++-- src/pages/Pool/Withdraw/useSimulate.ts | 2 +- src/pages/Pool/index.tsx | 96 +++---- src/pages/Trade/Swap/index.tsx | 26 +- src/pages/Trade/Swap/useSimulate.ts | 8 +- src/stores/assets.ts | 20 +- src/stores/pairs.ts | 11 +- src/types/api.d.ts | 45 ++++ src/types/common.d.ts | 8 - src/types/factory.d.ts | 5 - src/types/router.d.ts | 3 - src/utils/index.ts | 2 +- yarn.lock | 93 ++----- 58 files changed, 1022 insertions(+), 1251 deletions(-) create mode 100644 src/api/index.ts create mode 100644 src/components/SelectAssetForm/AssetItem.tsx create mode 100644 src/hooks/useIsInViewport.ts delete mode 100644 src/hooks/usePair.ts create mode 100644 src/hooks/usePairs.ts create mode 100644 src/hooks/usePools.ts create mode 100644 src/hooks/useVerifiedAssets.ts create mode 100644 src/types/api.d.ts delete mode 100644 src/types/factory.d.ts delete mode 100644 src/types/router.d.ts diff --git a/package.json b/package.json index 33745600..73653cb9 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", "@rollup/plugin-inject": "^5.0.3", + "@tanstack/react-query": "^4.29.7", "@tippyjs/react": "^4.2.6", "@xpla/wallet-controller": "^0.4.1", "@xpla/wallet-provider": "^0.4.1", @@ -38,7 +39,6 @@ "react-grid-system": "^8.1.6", "react-hook-form": "^7.40.0", "react-modal": "^3.16.1", - "react-query": "^3.39.2", "react-router-dom": "^6.4.3", "react-tiny-popover": "^7.2.0", "resize-observer-polyfill": "^1.5.1", diff --git a/src/App.tsx b/src/App.tsx index 5f849ffe..b3163f5e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,25 +7,17 @@ import { useScreenClass, } from "react-grid-system"; import { gridConfiguration, SCREEN_CLASSES } from "constants/layout"; -import { useAtomValue, useSetAtom } from "jotai"; -import { verifiedAssetsAtom, verifiedIbcAssetsAtom } from "stores/assets"; -import { useAPI } from "hooks/useAPI"; +import { useAtomValue } from "jotai"; import MainLayout from "layout/Main"; import disclaimerLastSeenAtom from "stores/disclaimer"; import DisclaimerModal from "components/Modal/DisclaimerModal"; import globalElementsAtom from "stores/globalElements"; -import { VerifiedIbcAssets } from "types/token"; -import { useNetwork } from "./hooks/useNetwork"; setGridConfiguration(gridConfiguration); function App() { - const setKnownAssets = useSetAtom(verifiedAssetsAtom); - const setKnownIbcAssets = useSetAtom(verifiedIbcAssetsAtom); const disclaimerLastSeen = useAtomValue(disclaimerLastSeenAtom); const globalElements = useAtomValue(globalElementsAtom); - const api = useAPI(); - const network = useNetwork(); const screenClass = useScreenClass(); const isDisclaimerAgreed = useMemo(() => { if (!disclaimerLastSeen) return false; @@ -51,35 +43,6 @@ function App() { }); }, []); - useEffect(() => { - api.getVerifiedTokenInfo().then((assets) => { - setKnownAssets(assets); - }); - api.getVerifiedIbcTokenInfo().then((ibcAssets: VerifiedIbcAssets) => { - const updatedIbcAssets = ibcAssets[network.name]; - if (updatedIbcAssets) { - Promise.all( - Object.entries(updatedIbcAssets).map(([k, v]) => - api.getDecimal(v?.denom || "").then((d) => { - const asset = - updatedIbcAssets && updatedIbcAssets?.[k] - ? updatedIbcAssets?.[k] - : undefined; - if (d && asset) { - asset.decimals = d; - } - }), - ), - ).then(() => { - setKnownIbcAssets((current) => ({ - ...current, - [network.name]: updatedIbcAssets, - })); - }); - } - }); - }, [api, setKnownAssets, setKnownIbcAssets]); - const renderRoute = useCallback( ({ children, element, index, ...props }: RouteObject) => { return ( diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 00000000..0110310c --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,45 @@ +import axios from "axios"; +import { apiAddresses } from "constants/dezswap"; +import { Pair, Pairs, Pool, Token } from "types/api"; +import { NetworkName } from "types/common"; + +export type ApiVersion = "v1"; + +const api = (networkName: NetworkName, version: ApiVersion = "v1") => { + const apiClient = axios.create({ + baseURL: `${apiAddresses[networkName]?.baseUrl || ""}/${version}`, + }); + + if (version === "v1") { + return { + async getPairs() { + const res = await apiClient.get(`/pairs`); + return res.data; + }, + async getPair(address: string) { + const res = await apiClient.get(`/pairs/${address}`); + return res.data; + }, + async getPools() { + const res = await apiClient.get(`/pools`); + return res.data; + }, + async getPool(address: string) { + const res = await apiClient.get(`/pools/${address}`); + return res.data; + }, + async getTokens() { + const res = await apiClient.get(`/tokens`); + return res.data; + }, + async getToken(address: string) { + const res = await apiClient.get(`/tokens/${address}`); + return res.data; + }, + }; + } + + throw new Error(`Unsupported API version: ${version}`); +}; + +export default api; diff --git a/src/components/Modal/TxBroadcastingModal/index.tsx b/src/components/Modal/TxBroadcastingModal/index.tsx index 476ebfc8..1570d427 100644 --- a/src/components/Modal/TxBroadcastingModal/index.tsx +++ b/src/components/Modal/TxBroadcastingModal/index.tsx @@ -9,10 +9,10 @@ import { MouseEventHandler, useEffect, useMemo, useState } from "react"; import { ellipsisCenter, getTransactionLink } from "utils"; import { TxInfo } from "@xpla/xpla.js"; import { TxError } from "types/common"; -import { useLCDClient } from "hooks/useLCDClient"; +import useLCDClient from "hooks/useLCDClient"; import Panel from "components/Panel"; import Modal from "components/Modal"; -import { useNetwork } from "hooks/useNetwork"; +import useNetwork from "hooks/useNetwork"; import Typography from "components/Typography"; import { Col, Row, useScreenClass } from "react-grid-system"; import IconButton from "components/IconButton"; diff --git a/src/components/Select/index.tsx b/src/components/Select/index.tsx index d3ab8981..26fe9ce7 100644 --- a/src/components/Select/index.tsx +++ b/src/components/Select/index.tsx @@ -4,7 +4,7 @@ import { useMemo } from "react"; import iconDropdown from "assets/icons/icon-dropdown-arrow.svg"; import Typography from "components/Typography"; import { css } from "@emotion/react"; -import { useModal } from "hooks/useModal"; +import useModal from "hooks/useModal"; type Value = string | number; diff --git a/src/components/SelectAssetForm/AssetItem.tsx b/src/components/SelectAssetForm/AssetItem.tsx new file mode 100644 index 00000000..fdfcdbdc --- /dev/null +++ b/src/components/SelectAssetForm/AssetItem.tsx @@ -0,0 +1,251 @@ +import { css, useTheme } from "@emotion/react"; +import styled from "@emotion/styled"; +import IconButton from "components/IconButton"; +import Tooltip from "components/Tooltip"; +import Typography from "components/Typography"; +import { MOBILE_SCREEN_CLASS } from "constants/layout"; +import useBalance from "hooks/useBalance"; +import useIsInViewport from "hooks/useIsInViewport"; +import { useRef } from "react"; +import { Token } from "types/api"; +import { formatNumber, cutDecimal, amountToValue, ellipsisCenter } from "utils"; + +import iconToken from "assets/icons/icon-default-token.svg"; +import iconVerified from "assets/icons/icon-verified.svg"; +import iconBookmark from "assets/icons/icon-bookmark-default.svg"; +import iconBookmarkSelected from "assets/icons/icon-bookmark-selected.svg"; + +interface WrapperProps { + selected?: boolean; + invisible?: boolean; +} +const Wrapper = styled.div` + display: flex; + justify-content: flex-start; + align-items: center; + column-gap: 6px; + + width: 100%; + max-width: 100%; + height: auto; + position: relative; + padding: 16px 27px; + .${MOBILE_SCREEN_CLASS} & { + padding: 15px 13px; + } + + background-color: transparent; + cursor: pointer; + border: none; + margin: 0; + text-align: left; + overflow: hidden; + transition: all 0.125s cubic-bezier(0, 1, 0, 1); + max-height: 1280px; + + & .asset-address { + display: none; + } + + &:hover { + background-color: ${({ theme }) => theme.colors.text.background}; + & .asset-name { + display: none; + } + & .asset-address { + display: unset; + } + } + + ${({ selected, theme }) => + selected && + css` + background-color: ${theme.colors.text.background}; + & .asset-name { + display: none; + } + & .asset-address { + display: unset; + } + `} + ${({ invisible }) => + invisible && + css` + max-height: 0; + padding-top: 0; + padding-bottom: 0; + opacity: 0; + pointer-events: none; + .${MOBILE_SCREEN_CLASS} & { + padding-top: 0; + padding-bottom: 0; + } + `} +`; + +interface AssetIconProps { + src?: string; +} +const AssetIcon = styled.div` + width: 32px; + height: 32px; + min-width: 32px; + min-height: 32px; + position: relative; + display: inline-block; + padding: 0px 6px; + + background-color: ${({ theme }) => theme.colors.white}; + border-radius: 50%; + + ${({ src = iconToken }) => css` + background-image: url(${src || iconToken}); + `}; + background-size: 32px 32px; + background-position: 50% 50%; + background-repeat: no-repeat; +`; + +function Balance({ asset }: { asset?: Partial }) { + const balance = useBalance(asset?.token); + return ( + + {formatNumber( + cutDecimal(amountToValue(balance || 0, asset?.decimals) || 0, 3), + )} + + ); +} + +function AssetItem({ + asset, + selected, + hidden, + onClick, + isBookmarked, + isVerified, + onBookmarkToggle, +}: { + asset?: Partial; + selected?: boolean; + hidden?: boolean; + onClick?: React.MouseEventHandler; + isVerified?: boolean; + isBookmarked?: boolean; + onBookmarkToggle?: (address: string) => void; +}) { + const theme = useTheme(); + const wrapperRef = useRef(null); + const isIntersecting = useIsInViewport(wrapperRef); + return ( + + { + e.stopPropagation(); + if (onBookmarkToggle && asset?.token) { + onBookmarkToggle(asset?.token); + } + }} + /> + + + {isVerified && ( + +
+ + )} + +
div { + display: inline-block; + vertical-align: middle; + } + `} + > +
+ + {asset?.symbol} + + + {asset?.name} + + {ellipsisCenter(asset?.token, 6)} + + +
+ {isIntersecting && } +
+ + ); +} + +export default AssetItem; diff --git a/src/components/SelectAssetForm/index.tsx b/src/components/SelectAssetForm/index.tsx index ca0f7378..1c7e8daa 100644 --- a/src/components/SelectAssetForm/index.tsx +++ b/src/components/SelectAssetForm/index.tsx @@ -9,31 +9,19 @@ import React, { useState, } from "react"; import Typography from "components/Typography"; -import { - amountToValue, - cutDecimal, - ellipsisCenter, - formatNumber, - getIbcTokenHash, - isNativeTokenAddress, -} from "utils"; -import { Asset as OrgAsset } from "types/common"; -import iconToken from "assets/icons/icon-default-token.svg"; -import iconVerified from "assets/icons/icon-verified.svg"; -import IconButton from "components/IconButton"; +import { isNativeTokenAddress } from "utils"; import Input from "components/Input"; import Hr from "components/Hr"; import TabButton from "components/TabButton"; import useAssets from "hooks/useAssets"; -import iconBookmark from "assets/icons/icon-bookmark-default.svg"; -import iconBookmarkSelected from "assets/icons/icon-bookmark-selected.svg"; import useBookmark from "hooks/useBookmark"; import Panel from "components/Panel"; import { MOBILE_SCREEN_CLASS } from "constants/layout"; -import Tooltip from "components/Tooltip"; -import { useNetwork } from "hooks/useNetwork"; +import useNetwork from "hooks/useNetwork"; +import { Token } from "types/api"; +import AssetItem from "./AssetItem"; -type Asset = Partial; +type Asset = Partial; export type LPAsset = { address: string; assets: [Asset, Asset]; @@ -76,90 +64,6 @@ const AssetList = styled.div` } `; -const AssetItem = styled.div<{ selected?: boolean; invisible?: boolean }>` - display: flex; - justify-content: flex-start; - align-items: center; - column-gap: 6px; - - width: 100%; - max-width: 100%; - height: auto; - position: relative; - padding: 16px 27px; - .${MOBILE_SCREEN_CLASS} & { - padding: 15px 13px; - } - - background-color: transparent; - cursor: pointer; - border: none; - margin: 0; - text-align: left; - overflow: hidden; - transition: all 0.125s cubic-bezier(0, 1, 0, 1); - max-height: 1280px; - - & .asset-address { - display: none; - } - - &:hover { - background-color: ${({ theme }) => theme.colors.text.background}; - & .asset-name { - display: none; - } - & .asset-address { - display: unset; - } - } - - ${({ selected, theme }) => - selected && - css` - background-color: ${theme.colors.text.background}; - & .asset-name { - display: none; - } - & .asset-address { - display: unset; - } - `} - ${({ invisible }) => - invisible && - css` - max-height: 0; - padding-top: 0; - padding-bottom: 0; - opacity: 0; - pointer-events: none; - .${MOBILE_SCREEN_CLASS} & { - padding-top: 0; - padding-bottom: 0; - } - `} -`; - -const AssetIcon = styled.div<{ src?: string }>` - width: 32px; - height: 32px; - min-width: 32px; - min-height: 32px; - position: relative; - display: inline-block; - padding: 0px 6px; - - background-color: ${({ theme }) => theme.colors.white}; - border-radius: 50%; - - ${({ src = iconToken }) => css` - background-image: url(${src || iconToken}); - `}; - background-size: 32px 32px; - background-position: 50% 50%; - background-repeat: no-repeat; -`; - const NoResult = styled.div` position: absolute; width: 100%; @@ -186,7 +90,7 @@ function SelectAssetForm(props: SelectAssetFormProps) { const theme = useTheme(); const [searchKeyword, setSearchKeyword] = useState(""); const deferredSearchKeyword = useDeferredValue(searchKeyword); - const { getAsset, verifiedAssets, verifiedIbcAssets } = useAssets(); + const { getAsset } = useAssets(); const { bookmarks, toggleBookmark } = useBookmark(); const network = useNetwork(); const [tabIdx, setTabIdx] = useState(0); @@ -216,133 +120,25 @@ function SelectAssetForm(props: SelectAssetFormProps) { const items = filteredList?.map((address) => { const asset = getAsset(address); const isVerified = - !!verifiedAssets?.[address] || - isNativeTokenAddress(network.name, address) || - (verifiedIbcAssets && !!verifiedIbcAssets?.[getIbcTokenHash(address)]); + asset?.verified || isNativeTokenAddress(network.name, address); return ( - item?.toLowerCase().includes(deferredSearchKeyword.toLowerCase()), - ) < 0 + hidden={ + !asset?.symbol?.toLowerCase().includes(deferredSearchKeyword) && + !asset?.name?.toLowerCase().includes(deferredSearchKeyword) } + isVerified={isVerified} onClick={() => { if (handleSelect) { handleSelect(address); } }} - > - { - e.stopPropagation(); - toggleBookmark(address); - }} - /> - - - {isVerified && ( - -
- - )} - -
div { - display: inline-block; - vertical-align: middle; - } - `} - > -
- - {asset?.symbol} - - - {asset?.name} - - {ellipsisCenter(address, 6)} - - -
- - {formatNumber( - cutDecimal( - amountToValue(asset?.balance || 0, asset?.decimals) || 0, - 3, - ), - )} - -
- + isBookmarked={bookmarks?.includes(address)} + onBookmarkToggle={toggleBookmark} + /> ); }); @@ -372,8 +168,6 @@ function SelectAssetForm(props: SelectAssetFormProps) { theme, toggleBookmark, network, - verifiedAssets, - children, ]); useEffect(() => { diff --git a/src/constants/network.ts b/src/constants/network.ts index 3ffe0c55..988de944 100644 --- a/src/constants/network.ts +++ b/src/constants/network.ts @@ -1,4 +1,4 @@ -import { Asset } from "types/common"; +import { Token } from "types/api"; export type Network = { lcd: string; @@ -21,27 +21,31 @@ const networks: Record = { }, }; -export const nativeTokens: Record = { +export const nativeTokens: Record = { mainnet: [ { - address: XPLA_ADDRESS, + token: XPLA_ADDRESS, decimals: 18, name: XPLA_SYMBOL, symbol: XPLA_SYMBOL, total_supply: "", - iconSrc: "https://assets.xpla.io/icon/svg/XPLA.svg", - balance: "0", + icon: "https://assets.xpla.io/icon/svg/XPLA.svg", + chainId: networks.dimension.chainId, + verified: true, + protocol: "", }, ], testnet: [ { - address: XPLA_ADDRESS, + token: XPLA_ADDRESS, decimals: 18, name: XPLA_SYMBOL, symbol: XPLA_SYMBOL, total_supply: "", - iconSrc: "https://assets.xpla.io/icon/svg/XPLA.svg", - balance: "0", + icon: "https://assets.xpla.io/icon/svg/XPLA.svg", + chainId: networks.cube.chainId, + verified: true, + protocol: "", }, ], }; diff --git a/src/hooks/modals/useConfirmationModal.tsx b/src/hooks/modals/useConfirmationModal.tsx index ee21b64a..e3e0314d 100644 --- a/src/hooks/modals/useConfirmationModal.tsx +++ b/src/hooks/modals/useConfirmationModal.tsx @@ -1,6 +1,6 @@ import ConfirmationModal from "components/Modal/ConfirmationModal"; import useGlobalElement from "hooks/useGlobalElement"; -import { useModal } from "hooks/useModal"; +import useModal from "hooks/useModal"; import { useMemo } from "react"; const useConfirmationModal = ({ diff --git a/src/hooks/modals/useConnectWalletModal.tsx b/src/hooks/modals/useConnectWalletModal.tsx index 2770a36d..b766b2bb 100644 --- a/src/hooks/modals/useConnectWalletModal.tsx +++ b/src/hooks/modals/useConnectWalletModal.tsx @@ -1,7 +1,7 @@ import ConnectWalletModal from "components/Modal/ConnectWalletModal"; import { useMemo } from "react"; import useGlobalElement from "../useGlobalElement"; -import { useModal } from "../useModal"; +import useModal from "../useModal"; const useConnectWalletModal = () => { const modal = useModal(); diff --git a/src/hooks/modals/useFirstProvideModal.tsx b/src/hooks/modals/useFirstProvideModal.tsx index 2769e7d4..b7cd7850 100644 --- a/src/hooks/modals/useFirstProvideModal.tsx +++ b/src/hooks/modals/useFirstProvideModal.tsx @@ -1,7 +1,7 @@ import { useMemo } from "react"; import FirstProvideModal from "components/Modal/FirstProvideModal"; import useGlobalElement from "hooks/useGlobalElement"; -import { useModal } from "hooks/useModal"; +import useModal from "hooks/useModal"; const useFirstProvideModal = ({ addresses, diff --git a/src/hooks/modals/useInvalidPathModal.tsx b/src/hooks/modals/useInvalidPathModal.tsx index 4386d656..f4d73e6a 100644 --- a/src/hooks/modals/useInvalidPathModal.tsx +++ b/src/hooks/modals/useInvalidPathModal.tsx @@ -1,4 +1,4 @@ -import { useModal } from "hooks/useModal"; +import useModal from "hooks/useModal"; import { useMemo } from "react"; import useGlobalElement from "hooks/useGlobalElement"; import InvalidPathModal from "components/Modal/InvalidPathModal"; diff --git a/src/hooks/modals/useTxBroadcastingModal.tsx b/src/hooks/modals/useTxBroadcastingModal.tsx index 2f8b74b7..ba004ce4 100644 --- a/src/hooks/modals/useTxBroadcastingModal.tsx +++ b/src/hooks/modals/useTxBroadcastingModal.tsx @@ -1,6 +1,6 @@ import TxBroadcastingModal from "components/Modal/TxBroadcastingModal"; import useGlobalElement from "hooks/useGlobalElement"; -import { useModal } from "hooks/useModal"; +import useModal from "hooks/useModal"; import { useMemo } from "react"; import { TxError } from "types/common"; diff --git a/src/hooks/useAPI.ts b/src/hooks/useAPI.ts index 7eba2201..09f55372 100644 --- a/src/hooks/useAPI.ts +++ b/src/hooks/useAPI.ts @@ -1,159 +1,39 @@ import axios from "axios"; -import http from "http"; -import https from "https"; import { useCallback, useMemo } from "react"; -import { Pair, Pool, ReverseSimulation, Simulation } from "types/pair"; +import { ReverseSimulation, Simulation } from "types/pair"; import { useConnectedWallet } from "@xpla/wallet-provider"; import { generateReverseSimulationMsg, generateSimulationMsg, - queryMessages, } from "utils/dezswap"; -import { Pairs } from "types/factory"; -import { TokenInfo, VerifiedTokenInfo } from "types/token"; -import { apiAddresses, contractAddresses } from "constants/dezswap"; -import { useNetwork } from "hooks/useNetwork"; -import { useLCDClient } from "hooks/useLCDClient"; +import { VerifiedAssets, VerifiedIbcAssets } from "types/token"; +import { contractAddresses } from "constants/dezswap"; +import useNetwork from "hooks/useNetwork"; +import useLCDClient from "hooks/useLCDClient"; import { LatestBlock } from "types/common"; +import api, { ApiVersion } from "api"; interface TokenBalance { balance: string; } -export type ApiVersion = "v1"; - interface Decimal { decimals: number; } -export const useAPI = (version: ApiVersion = "v1") => { +const useAPI = (version: ApiVersion = "v1") => { const network = useNetwork(); const lcd = useLCDClient(); const connectedWallet = useConnectedWallet(); - const apiClient = axios.create({ - httpAgent: new http.Agent({ keepAlive: true }), - httpsAgent: new https.Agent({ keepAlive: true }), - }); const walletAddress = useMemo( () => connectedWallet?.walletAddress, [connectedWallet], ); - const getTokens = useCallback(async () => { - try { - const base = apiAddresses[network.name]?.baseUrl || ""; - const { data } = await apiClient.get<(TokenInfo & VerifiedTokenInfo)[]>( - `${base}/${version}/tokens`, - ); - return data; - } catch (err) { - console.error(err); - } - return []; - }, [lcd, network.name, version]); - - const getToken = useCallback( - async (address: string) => { - try { - const base = apiAddresses[network.name]?.baseUrl || ""; - const { data } = await apiClient.get( - `${base}/${version}/tokens/${address}`, - ); - return data; - } catch (err) { - console.error(err); - } - const res = await lcd.wasm.contractQuery(address, { - token_info: {}, - }); - return res; - }, - [lcd, network.name, version], - ); - - const getPairs = useCallback( - async (options?: Parameters[0]) => { - const contractAddress = contractAddresses[network.name]?.factory; - if (!contractAddress) { - return undefined; - } - - try { - const base = apiAddresses[network.name]?.baseUrl || ""; - const { data } = await apiClient.get(`${base}/${version}/pairs`); - return data; - } catch (err) { - console.error(err); - } - const res: Pairs = await lcd.wasm.contractQuery( - contractAddress, - queryMessages.getPairs(options), - ); - - return res; - }, - [lcd.wasm, network.name, version], - ); - - const getPair = useCallback( - async (contractAddress: string) => { - if (!contractAddress) { - return undefined; - } - - try { - const base = apiAddresses[network.name]?.baseUrl || ""; - const { data } = await apiClient.get( - `${base}/${version}/pairs/${contractAddress}`, - ); - return data; - } catch (err) { - console.error(err); - } - const res = await lcd.wasm.contractQuery(contractAddress, { - pair: {}, - }); - - return res; - }, - [lcd.wasm, network.name, version], - ); - - const getPools = useCallback(async () => { - try { - const base = apiAddresses[network.name]?.baseUrl || ""; - const { data } = await apiClient.get(`${base}/${version}/pools`); - return data; - } catch (err) { - console.error(err); - } - return []; - }, [network.name, version]); - - const getPool = useCallback( - async (contractAddress: string) => { - if (!contractAddress) { - return undefined; - } - - try { - const base = apiAddresses[network.name]?.baseUrl || ""; - const { data } = await apiClient.get( - `${base}/${version}/pools/${contractAddress}`, - ); - return data; - } catch (err) { - console.error(err); - } - const res: Pool = await lcd.wasm.contractQuery( - contractAddress, - queryMessages.getPool(), - ); - - return res; - }, - [lcd.wasm, network.name, version], + const apiClient = useMemo( + () => api(network.name, version), + [network.name, version], ); const simulate = useCallback( @@ -209,22 +89,34 @@ export const useAPI = (version: ApiVersion = "v1") => { [lcd, walletAddress], ); - const getVerifiedTokenInfo = useCallback(async () => { - const { data } = await apiClient.get( - "https://assets.xpla.io/cw20/tokens.json", - ); - return data; - }, []); + const getVerifiedTokenInfos = useCallback( + /** + * @Deprecated + */ + async () => { + const { data } = await axios.get( + "https://assets.xpla.io/cw20/tokens.json", + ); + return data; + }, + [], + ); - const getVerifiedIbcTokenInfo = useCallback(async () => { - const { data } = await apiClient.get( - "https://assets.xpla.io/ibc/tokens.json", - ); - return data; - }, []); + const getVerifiedIbcTokenInfos = useCallback( + /** + * @Deprecated + */ + async () => { + const { data } = await axios.get( + "https://assets.xpla.io/ibc/tokens.json", + ); + return data; + }, + [], + ); const getLatestBlockHeight = useCallback(async () => { - const { data } = await apiClient.get( + const { data } = await axios.get( `${network.lcd}/blocks/latest`, ); return data.block.header.height; @@ -241,41 +133,33 @@ export const useAPI = (version: ApiVersion = "v1") => { }); return res.decimals; }, - [network.name, lcd, walletAddress], + [network.name, lcd], ); - const api = useMemo( + return useMemo( () => ({ - getTokens, - getToken, - getPairs, - getPair, - getPool, + ...apiClient, simulate, reverseSimulate, getNativeTokenBalance, getTokenBalance, - getVerifiedTokenInfo, - getVerifiedIbcTokenInfo, + getVerifiedTokenInfos, + getVerifiedIbcTokenInfos, getLatestBlockHeight, getDecimal, }), [ - getTokens, - getToken, - getPairs, - getPair, - getPool, + apiClient, simulate, reverseSimulate, getNativeTokenBalance, getTokenBalance, - getVerifiedTokenInfo, - getVerifiedIbcTokenInfo, + getVerifiedTokenInfos, + getVerifiedIbcTokenInfos, getLatestBlockHeight, getDecimal, ], ); - - return api; }; + +export default useAPI; diff --git a/src/hooks/useAssets.ts b/src/hooks/useAssets.ts index 4875deca..6ef4cdee 100644 --- a/src/hooks/useAssets.ts +++ b/src/hooks/useAssets.ts @@ -1,155 +1,40 @@ -import { useCallback, useMemo, useRef } from "react"; -import { useAtom, useAtomValue } from "jotai"; -import { useAPI } from "hooks/useAPI"; -import { useNetwork } from "hooks/useNetwork"; -import assetsAtom, { - verifiedAssetsAtom, - verifiedIbcAssetsAtom, -} from "stores/assets"; +import { useCallback, useMemo } from "react"; +import useAPI from "hooks/useAPI"; +import useNetwork from "hooks/useNetwork"; import { AccAddress } from "@xpla/xpla.js"; -import { Asset, NetworkName } from "types/common"; -import { getIbcTokenHash, isNativeTokenAddress } from "utils"; -import { nativeTokens } from "constants/network"; +import { isNativeTokenAddress } from "utils"; import useCustomAssets from "hooks/useCustomAssets"; - -const UPDATE_INTERVAL_SEC = 5000; +import { useQuery } from "@tanstack/react-query"; const useAssets = () => { - const [assetStore, setAssetStore] = useAtom(assetsAtom); - const verifiedAssets = useAtomValue(verifiedAssetsAtom); - const verifiedIbcAssets = useAtomValue(verifiedIbcAssetsAtom); const api = useAPI(); const network = useNetwork(); const { getCustomAsset } = useCustomAssets(); - const fetchQueue = useRef<{ [K in NetworkName]?: AccAddress[] }>({ - mainnet: [], - testnet: [], - }); - const isFetching = useRef(false); - const { removeCustomAsset } = useCustomAssets(); - - const fetchAsset = useCallback(async () => { - isFetching.current = true; - try { - const networkName = network.name; - const store = assetStore[networkName] || []; - const address = fetchQueue.current[networkName]?.[0]; - - if (address) { - const index = store.findIndex((item) => item.address === address); - if (index >= 0) { - const currentAsset = store[index]; - if ( - new Date((currentAsset as Asset).updatedAt || 0)?.getTime() < - Date.now() - UPDATE_INTERVAL_SEC && - window.navigator.onLine - ) { - if (isNativeTokenAddress(network.name, address)) { - const asset = nativeTokens[network.name]?.find( - (item) => item.address === address, - ); - const balance = await api.getNativeTokenBalance(address); - if (asset) { - store[index] = { - ...asset, - balance: balance || "0", - updatedAt: new Date(), - }; - setAssetStore((current) => ({ - ...current, - [networkName]: assetStore[networkName], - })); - } - } else if ( - verifiedIbcAssets?.[networkName]?.[getIbcTokenHash(address)] - ) { - const asset = - verifiedIbcAssets?.[networkName]?.[getIbcTokenHash(address)]; - const balance = await api.getNativeTokenBalance(address); - if (asset) { - store[index] = { - ...asset, - total_supply: "", - address: asset.denom, - iconSrc: asset.icon, - balance: balance || "0", - updatedAt: new Date(), - }; - setAssetStore((current) => ({ - ...current, - [networkName]: assetStore[networkName], - })); - } - } else { - const token = await api.getToken(address); - if (verifiedAssets) { - const verifiedAsset = verifiedAssets?.[networkName]?.[address]; - const balance = await api.getTokenBalance(address); - - store[index] = { - ...token, - address, - balance: balance || "0", - iconSrc: verifiedAsset?.icon, - updatedAt: new Date(), - }; - setAssetStore((current) => ({ - ...current, - [networkName]: assetStore[networkName], - })); - } else if (!fetchQueue.current[networkName]?.includes(address)) { - fetchQueue.current[networkName]?.push(address); - } - } - removeCustomAsset(address); - } - } - } - } catch (error) { - console.log(error); - } - isFetching.current = false; - setTimeout(() => { - fetchQueue.current[network.name]?.shift(); - if (fetchQueue.current[network.name]?.length) { - fetchAsset(); - } - }, 100); - }, [network, assetStore, api, verifiedAssets, setAssetStore]); - const addFetchQueue = useCallback( - (address: string, networkName: NetworkName) => { - if ( - nativeTokens[networkName]?.some((item) => item.address === address) || - AccAddress.validate(address) || - (verifiedIbcAssets && - !!verifiedIbcAssets[networkName]?.[getIbcTokenHash(address)]) - ) { - if (!fetchQueue.current[networkName]?.includes(address)) { - fetchQueue.current[networkName]?.push(address); - } - } - if (!isFetching.current && window.navigator.onLine) { - fetchAsset(); - } + const { data: assets } = useQuery( + ["assets", network.name], + async () => { + const res = await api.getTokens(); + return res; + }, + { + enabled: !!network.name, + refetchOnReconnect: true, + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchInterval: false, }, - [fetchAsset], ); const getAsset = useCallback( - (address: string): Partial | undefined => { - const asset = assetStore[network.name]?.find( - (item) => item.address === address, - ); - if (!asset?.address) { + (address: string) => { + const asset = assets?.find((item) => item.token === address); + if (!asset?.token) { return getCustomAsset(address); } - if (window.navigator.onLine) { - addFetchQueue(asset.address, network.name); - } return asset; }, - [assetStore, network.name, getCustomAsset, addFetchQueue], + [assets, getCustomAsset], ); const validate = useCallback( @@ -157,18 +42,16 @@ const useAssets = () => { address && (AccAddress.validate(address) || isNativeTokenAddress(network.name, address) || - verifiedIbcAssets?.[network.name]?.[getIbcTokenHash(address)]), - [network.name, verifiedIbcAssets], + assets?.find((item) => item.token === address)?.verified), + [assets, network.name], ); return useMemo( () => ({ getAsset, validate, - verifiedAssets: verifiedAssets?.[network.name], - verifiedIbcAssets: verifiedIbcAssets?.[network.name], }), - [getAsset, validate, network.name, verifiedAssets, verifiedIbcAssets], + [getAsset, validate], ); }; diff --git a/src/hooks/useBalance.ts b/src/hooks/useBalance.ts index 9a03c563..d73df65f 100644 --- a/src/hooks/useBalance.ts +++ b/src/hooks/useBalance.ts @@ -1,58 +1,50 @@ -import { useEffect, useMemo, useState } from "react"; +import { useCallback, useMemo } from "react"; import { useConnectedWallet } from "@xpla/wallet-provider"; -import { useAPI } from "hooks/useAPI"; +import useAPI from "hooks/useAPI"; import { getIbcTokenHash, isNativeTokenAddress } from "utils"; -import { useAtomValue } from "jotai"; -import { verifiedIbcAssetsAtom } from "stores/assets"; -import { useNetwork } from "hooks/useNetwork"; +import useNetwork from "hooks/useNetwork"; +import { useQuery } from "@tanstack/react-query"; +import useVerifiedAssets from "./useVerifiedAssets"; -const UPDATE_INTERVAL = 2500; +const UPDATE_INTERVAL = 30000; -export const useBalance = (asset: string) => { +const useBalance = (address?: string) => { const connectedWallet = useConnectedWallet(); - const [balance, setBalance] = useState(); - const verifiedIbcAssets = useAtomValue(verifiedIbcAssetsAtom); + const { verifiedIbcAssets } = useVerifiedAssets(); const network = useNetwork(); const api = useAPI(); - useEffect(() => { - const fetchBalance = async () => { - if (!connectedWallet?.walletAddress || !asset) { - setBalance("0"); + const fetchBalance = useCallback(async () => { + if ( + address && + connectedWallet?.network.name && + connectedWallet?.walletAddress + ) { + if ( + isNativeTokenAddress(connectedWallet?.network.name, address) || + (verifiedIbcAssets && !!verifiedIbcAssets?.[getIbcTokenHash(address)]) + ) { + const value = await api.getNativeTokenBalance(address); + return `${value || 0}`; } + const value = await api.getTokenBalance(address); + return `${value || 0}`; + } + return "0"; + }, [api, address, connectedWallet, verifiedIbcAssets]); - if (asset && connectedWallet?.network.name) { - if ( - isNativeTokenAddress(connectedWallet?.network.name, asset) || - (verifiedIbcAssets && - !!verifiedIbcAssets[network.name]?.[getIbcTokenHash(asset)]) - ) { - api - .getNativeTokenBalance(asset) - .then((value) => - typeof value !== "undefined" - ? setBalance(`${value}`) - : setBalance("0"), - ); - } else { - api - .getTokenBalance(asset) - .then((value) => - typeof value !== "undefined" - ? setBalance(value) - : setBalance("0"), - ); - } - } - }; - - const intervalId = setInterval(() => fetchBalance(), UPDATE_INTERVAL); - fetchBalance(); - - return () => { - clearInterval(intervalId); - }; - }, [api, connectedWallet, asset, verifiedIbcAssets, network.name]); + const { data: balance } = useQuery({ + queryKey: ["balance", address, network.name], + queryFn: fetchBalance, + refetchInterval: UPDATE_INTERVAL, + refetchIntervalInBackground: true, + refetchOnMount: false, + refetchOnWindowFocus: false, + cacheTime: UPDATE_INTERVAL, + enabled: !!address, + }); return useMemo(() => balance, [balance]); }; + +export default useBalance; diff --git a/src/hooks/useBalanceMinusFee.ts b/src/hooks/useBalanceMinusFee.ts index f4d4424c..fe5545de 100644 --- a/src/hooks/useBalanceMinusFee.ts +++ b/src/hooks/useBalanceMinusFee.ts @@ -2,13 +2,11 @@ import { useEffect, useMemo, useState } from "react"; import { Numeric } from "@xpla/xpla.js"; import { XPLA_ADDRESS } from "constants/network"; import { amountToValue, valueToAmount } from "utils"; +import useBalance from "./useBalance"; -const useBalanceMinusFee = ( - address?: string, - balance?: string, - feeAmount?: string, -) => { - const [balanceMinusFee, setAsset1BalanceMinusFee] = useState(balance); +const useBalanceMinusFee = (address?: string, feeAmount?: string) => { + const balance = useBalance(address); + const [balanceMinusFee, setBalanceMinusFee] = useState(balance); useEffect(() => { if (balance) { @@ -16,12 +14,12 @@ const useBalanceMinusFee = ( const res = Numeric.parse(amountToValue(balance) || 0).minus( amountToValue(feeAmount) || 0, ); - setAsset1BalanceMinusFee(res.gt(0) ? valueToAmount(res) : "0"); + setBalanceMinusFee(res.gt(0) ? valueToAmount(res) : "0"); } else { - setAsset1BalanceMinusFee(balance); + setBalanceMinusFee(balance); } } else { - setAsset1BalanceMinusFee("0"); + setBalanceMinusFee("0"); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [balance, feeAmount]); diff --git a/src/hooks/useBookmark.ts b/src/hooks/useBookmark.ts index 1c726ba3..eb9f250b 100644 --- a/src/hooks/useBookmark.ts +++ b/src/hooks/useBookmark.ts @@ -1,5 +1,5 @@ import { useCallback, useMemo } from "react"; -import { useNetwork } from "hooks/useNetwork"; +import useNetwork from "hooks/useNetwork"; import { bookmarksAtom } from "stores/assets"; import { useAtom } from "jotai"; diff --git a/src/hooks/useCustomAssets.ts b/src/hooks/useCustomAssets.ts index c695bdc4..cc5ec08a 100644 --- a/src/hooks/useCustomAssets.ts +++ b/src/hooks/useCustomAssets.ts @@ -1,25 +1,25 @@ -import { useCallback, useMemo, useRef } from "react"; -import { useAtom, useAtomValue } from "jotai"; -import { useAPI } from "hooks/useAPI"; -import { useNetwork } from "hooks/useNetwork"; -import { - customAssetsAtom, - verifiedAssetsAtom, - verifiedIbcAssetsAtom, -} from "stores/assets"; +import { useCallback, useEffect, useMemo, useRef } from "react"; +import { useAtom } from "jotai"; +import useNetwork from "hooks/useNetwork"; +import { customAssetsAtom } from "stores/assets"; import { AccAddress } from "@xpla/xpla.js"; -import { Asset, NetworkName } from "types/common"; +import { NetworkName } from "types/common"; import { getIbcTokenHash, isNativeTokenAddress } from "utils"; import { nativeTokens } from "constants/network"; +import { Token } from "types/api"; +import { TokenInfo } from "types/token"; +import useLCDClient from "./useLCDClient"; +import usePairs from "./usePairs"; +import useVerifiedAssets from "./useVerifiedAssets"; const UPDATE_INTERVAL_SEC = 5000; const useCustomAssets = () => { const [customAssetStore, setCustomAssetStore] = useAtom(customAssetsAtom); - const verifiedAssets = useAtomValue(verifiedAssetsAtom); - const verifiedIbcAssets = useAtomValue(verifiedIbcAssetsAtom); + const { verifiedAssets, verifiedIbcAssets } = useVerifiedAssets(); + const { availableAssetAddresses } = usePairs(); - const api = useAPI(); + const lcd = useLCDClient(); const network = useNetwork(); const fetchQueue = useRef<{ [K in NetworkName]?: AccAddress[] }>({ mainnet: [], @@ -35,23 +35,21 @@ const useCustomAssets = () => { const address = fetchQueue.current[networkName]?.[0]; if (address) { - const index = store.findIndex((item) => item.address === address); + const index = store.findIndex((item) => item.token === address); if (index >= 0) { const currentAsset = store[index]; if ( - new Date((currentAsset as Asset).updatedAt || 0)?.getTime() < + new Date(currentAsset.updatedAt || 0)?.getTime() < Date.now() - UPDATE_INTERVAL_SEC && window.navigator.onLine ) { if (isNativeTokenAddress(network.name, address)) { const asset = nativeTokens[network.name]?.find( - (item) => item.address === address, + (item) => item.token === address, ); - const balance = await api.getNativeTokenBalance(address); if (asset) { store[index] = { ...asset, - balance: balance || "0", updatedAt: new Date(), }; setCustomAssetStore((current) => ({ @@ -59,19 +57,17 @@ const useCustomAssets = () => { [networkName]: customAssetStore[networkName], })); } - } else if ( - verifiedIbcAssets?.[networkName]?.[getIbcTokenHash(address)] - ) { - const asset = - verifiedIbcAssets?.[networkName]?.[getIbcTokenHash(address)]; - const balance = await api.getNativeTokenBalance(address); + } else if (verifiedIbcAssets?.[getIbcTokenHash(address)]) { + const asset = verifiedIbcAssets?.[getIbcTokenHash(address)]; if (asset) { store[index] = { ...asset, total_supply: "", - address: asset.denom, - iconSrc: asset.icon, - balance: balance || "0", + token: asset.denom, + icon: asset.icon, + chainId: network.chainID, + protocol: "", + verified: true, updatedAt: new Date(), }; setCustomAssetStore((current) => ({ @@ -80,16 +76,22 @@ const useCustomAssets = () => { })); } } else { - const token = await api.getToken(address); + const token = await lcd.wasm.contractQuery(address, { + token_info: {}, + }); if (verifiedAssets) { - const verifiedAsset = verifiedAssets?.[networkName]?.[address]; - const balance = await api.getTokenBalance(address); + const verifiedAsset = verifiedAssets?.[address]; store[index] = { - ...token, - address, - balance: balance || "0", - iconSrc: verifiedAsset?.icon, + name: token.name, + decimals: token.decimals, + symbol: token.symbol, + chainId: network.chainID, + protocol: "", + verified: !!verifiedAsset, + token: address, + total_supply: "", + icon: verifiedAsset?.icon || "", updatedAt: new Date(), }; setCustomAssetStore((current) => ({ @@ -113,15 +115,21 @@ const useCustomAssets = () => { fetchAsset(); } }, 100); - }, [network, customAssetStore, api, verifiedAssets, setCustomAssetStore]); + }, [ + network, + customAssetStore, + verifiedIbcAssets, + setCustomAssetStore, + lcd, + verifiedAssets, + ]); const addFetchQueue = useCallback( (address: string, networkName: NetworkName) => { if ( - nativeTokens[networkName]?.some((item) => item.address === address) || + nativeTokens[networkName]?.some((item) => item.token === address) || AccAddress.validate(address) || - (verifiedIbcAssets && - verifiedIbcAssets[networkName]?.[getIbcTokenHash(address)]) + (verifiedIbcAssets && verifiedIbcAssets?.[getIbcTokenHash(address)]) ) { if (!fetchQueue.current[networkName]?.includes(address)) { fetchQueue.current[networkName]?.push(address); @@ -131,19 +139,19 @@ const useCustomAssets = () => { fetchAsset(); } }, - [fetchAsset], + [fetchAsset, verifiedIbcAssets], ); const getAsset = useCallback( - (address: string): Partial | undefined => { + (address: string): Partial | undefined => { const asset = customAssetStore[network.name]?.find( - (item) => item.address === address, + (item) => item.token === address, ); - if (!asset?.address) { + if (!asset?.token) { return undefined; } if (window.navigator.onLine) { - addFetchQueue(asset.address, network.name); + addFetchQueue(asset.token, network.name); } return asset; }, @@ -151,9 +159,9 @@ const useCustomAssets = () => { ); const addCustomAsset = useCallback( - (asset: Asset) => { + (asset: Token) => { const store = customAssetStore[network.name] || []; - const index = store.findIndex((item) => item.address === asset.address); + const index = store.findIndex((item) => item.token === asset.token); if (index >= 0) { store[index] = asset; } else { @@ -163,18 +171,18 @@ const useCustomAssets = () => { ...current, [network.name]: store, })); - addFetchQueue(asset.address, network.name); + addFetchQueue(asset.token, network.name); }, [addFetchQueue, customAssetStore, network.name, setCustomAssetStore], ); const removeCustomAsset = useCallback( (address: string) => { - if (customAssetStore[network.name]?.some((a) => a.address === address)) { + if (customAssetStore[network.name]?.some((a) => a.token === address)) { setCustomAssetStore((current) => ({ ...current, [network.name]: customAssetStore[network.name]?.filter( - (a) => a.address !== address, + (a) => a.token !== address, ), })); } @@ -182,6 +190,12 @@ const useCustomAssets = () => { [customAssetStore, network.name, setCustomAssetStore], ); + useEffect(() => { + availableAssetAddresses.forEach((address) => { + removeCustomAsset(address); + }); + }, [availableAssetAddresses, removeCustomAsset]); + return useMemo( () => ({ customAssets: customAssetStore[network.name], @@ -189,7 +203,13 @@ const useCustomAssets = () => { removeCustomAsset, getCustomAsset: getAsset, }), - [addCustomAsset, customAssetStore, getAsset, network.name], + [ + addCustomAsset, + customAssetStore, + getAsset, + network.name, + removeCustomAsset, + ], ); }; diff --git a/src/hooks/useFee.ts b/src/hooks/useFee.ts index 2e05fcbd..2641c30d 100644 --- a/src/hooks/useFee.ts +++ b/src/hooks/useFee.ts @@ -2,9 +2,9 @@ import { CreateTxOptions, Fee } from "@xpla/xpla.js"; import { useConnectedWallet } from "@xpla/wallet-provider"; import { useDeferredValue, useEffect, useState } from "react"; import { AxiosError } from "axios"; -import { useLCDClient } from "hooks/useLCDClient"; +import useLCDClient from "hooks/useLCDClient"; -export const useFee = (txOptions?: CreateTxOptions) => { +const useFee = (txOptions?: CreateTxOptions) => { const connectedWallet = useConnectedWallet(); const lcd = useLCDClient(); const [fee, setFee] = useState(); @@ -106,3 +106,5 @@ export const useFee = (txOptions?: CreateTxOptions) => { return { fee, isLoading, isFailed, errMsg }; }; + +export default useFee; diff --git a/src/hooks/useIsInViewport.ts b/src/hooks/useIsInViewport.ts new file mode 100644 index 00000000..dc13ae6b --- /dev/null +++ b/src/hooks/useIsInViewport.ts @@ -0,0 +1,27 @@ +import { useState, useMemo, useEffect } from "react"; + +const useIsInViewport = (ref: React.RefObject) => { + const [isIntersecting, setIsIntersecting] = useState(false); + + const observer = useMemo( + () => + new IntersectionObserver(([entry]) => + setIsIntersecting(entry.isIntersecting), + ), + [], + ); + + useEffect(() => { + if (ref.current) { + observer.observe(ref.current); + } + + return () => { + observer.disconnect(); + }; + }, [ref, observer]); + + return isIntersecting; +}; + +export default useIsInViewport; diff --git a/src/hooks/useLCDClient.ts b/src/hooks/useLCDClient.ts index d117d6ba..2b74b06e 100644 --- a/src/hooks/useLCDClient.ts +++ b/src/hooks/useLCDClient.ts @@ -1,8 +1,8 @@ import { LCDClient } from "@xpla/xpla.js"; -import { useNetwork } from "hooks/useNetwork"; +import useNetwork from "hooks/useNetwork"; import { useMemo } from "react"; -export const useLCDClient = () => { +const useLCDClient = () => { const network = useNetwork(); return useMemo( () => @@ -14,3 +14,5 @@ export const useLCDClient = () => { [network], ); }; + +export default useLCDClient; diff --git a/src/hooks/useLatestBlock.ts b/src/hooks/useLatestBlock.ts index 910aac69..0ed598cc 100644 --- a/src/hooks/useLatestBlock.ts +++ b/src/hooks/useLatestBlock.ts @@ -1,26 +1,22 @@ -import { useEffect, useMemo, useState } from "react"; -import { useAPI } from "hooks/useAPI"; +import useAPI from "hooks/useAPI"; +import { useQuery } from "@tanstack/react-query"; +import useNetwork from "./useNetwork"; const UPDATE_INTERVAL = 3000; export const useLatestBlock = () => { - const [height, setHeight] = useState(); const api = useAPI(); + const network = useNetwork(); - useEffect(() => { - const fetchHeight = () => { - api - .getLatestBlockHeight() - .then((value) => typeof value !== "undefined" && setHeight(`${value}`)); - }; + const { data: height } = useQuery({ + queryKey: ["latestBlockHeight", network.name], + queryFn: api.getLatestBlockHeight, + refetchInterval: UPDATE_INTERVAL, + refetchIntervalInBackground: false, + refetchOnMount: false, + refetchOnWindowFocus: true, + refetchOnReconnect: true, + }); - const intervalId = setInterval(() => fetchHeight(), UPDATE_INTERVAL); - fetchHeight(); - - return () => { - clearInterval(intervalId); - }; - }, [api]); - - return useMemo(() => height, [height]); + return height; }; diff --git a/src/hooks/useModal.ts b/src/hooks/useModal.ts index 7fa30e2c..3f626e5e 100644 --- a/src/hooks/useModal.ts +++ b/src/hooks/useModal.ts @@ -1,6 +1,6 @@ import { useCallback, useMemo, useState } from "react"; -export const useModal = (defaultOption = false) => { +const useModal = (defaultOption = false) => { const [isOpen, setIsOpen] = useState(defaultOption); const open = useCallback((state = true) => setIsOpen(state), []); @@ -12,3 +12,5 @@ export const useModal = (defaultOption = false) => { [close, isOpen, open, toggle], ); }; + +export default useModal; diff --git a/src/hooks/useNetwork.ts b/src/hooks/useNetwork.ts index d790914e..1c5a9bab 100644 --- a/src/hooks/useNetwork.ts +++ b/src/hooks/useNetwork.ts @@ -2,7 +2,7 @@ import { useWallet } from "@xpla/wallet-provider"; import { useMemo } from "react"; import { NetworkName } from "types/common"; -export const useNetwork = () => { +const useNetwork = () => { const wallet = useWallet(); return useMemo( @@ -10,3 +10,5 @@ export const useNetwork = () => { [wallet.network], ); }; + +export default useNetwork; diff --git a/src/hooks/usePair.ts b/src/hooks/usePair.ts deleted file mode 100644 index 7eecca7f..00000000 --- a/src/hooks/usePair.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { useAPI } from "hooks/useAPI"; -import { NetworkName } from "types/common"; -import { useAtom, useSetAtom } from "jotai"; -import { queryMessages } from "utils/dezswap"; -import pairsAtom, { isPairsLoadingAtom } from "stores/pairs"; -import assetsAtom from "stores/assets"; -import { useNetwork } from "hooks/useNetwork"; -import useCustomAssets from "./useCustomAssets"; - -const LIMIT = 30; - -const usePairs = () => { - const network = useNetwork(); - const [pairs, setPairs] = useAtom(pairsAtom); - const [isLoading, setIsLoading] = useAtom(isPairsLoadingAtom); - const setAssets = useSetAtom(assetsAtom); - const isMainFetcher = useRef(false); - const api = useAPI(); - const { removeCustomAsset } = useCustomAssets(); - const lastPairAddr = useRef(""); - - const fetchPairs = useCallback( - async (options?: Parameters[0]) => { - try { - if (!network.name || !window.navigator.onLine) { - return; - } - setIsLoading(true); - const res = await api.getPairs(options); - if (res?.pairs) { - const newPairs = res.pairs.map((pair) => ({ - ...pair, - asset_addresses: pair.asset_infos.map((asset) => - "token" in asset - ? asset.token.contract_addr - : asset.native_token.denom, - ), - })); - const newLastPairAddr = newPairs[newPairs.length - 1].contract_addr; - if (newPairs.length && lastPairAddr.current !== newLastPairAddr) { - lastPairAddr.current = newLastPairAddr; - setPairs((current) => { - const currentPairs = current[network.name]?.data || []; - return { - ...current, - [network.name]: { - data: [...currentPairs, ...newPairs].filter( - (pair, index, array) => - index === - array.findIndex( - (p) => p.contract_addr === pair.contract_addr, - ), - ), - }, - }; - }); - return; - } - setIsLoading(false); - } - } catch (error) { - console.log(error); - } - }, - [api, network.name, setIsLoading, setPairs], - ); - - useEffect(() => { - if ((!isMainFetcher.current && !isLoading) || isMainFetcher.current) { - isMainFetcher.current = true; - const lastPair = pairs[network.name]?.data?.slice(-1)[0]; - fetchPairs({ - limit: LIMIT, - ...(lastPair ? { start_after: lastPair.asset_infos } : undefined), - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [fetchPairs, network.name, pairs]); - - const getPairedAddresses = useCallback( - (searchAddress: string) => { - const pairAddresses = pairs[network.name]?.data - ?.filter((pair) => { - return pair.asset_addresses.includes(searchAddress); - }) - .map((pair) => { - return pair.asset_addresses.find( - (address) => address !== searchAddress, - ) as string; - }); - return pairAddresses; - }, - [network, pairs], - ); - - const availableAssetAddresses = useMemo(() => { - return { - addresses: - pairs[network.name]?.data - ?.reduce((acc, pair) => { - return [...acc, ...pair.asset_addresses]; - }, [] as string[]) - ?.filter((asset, index, array) => array.indexOf(asset) === index) || - [], - networkName: network.name, - }; - }, [pairs, network.name]); - - useEffect(() => { - if (availableAssetAddresses) { - setAssets((current) => { - const { addresses, networkName } = availableAssetAddresses; - const currentAssetList = current[networkName] || []; - return { - ...current, - [networkName]: [ - ...currentAssetList, - ...addresses - .filter( - (address) => - currentAssetList.findIndex( - (item) => item.address === address, - ) < 0, - ) - .map((address) => ({ - address, - })), - ], - }; - }); - availableAssetAddresses.addresses.forEach((a) => removeCustomAsset(a)); - } - }, [availableAssetAddresses, setAssets, removeCustomAsset]); - - const getPair = useCallback( - (contractAddress: string) => { - return pairs[network.name]?.data?.find( - (pair) => pair.contract_addr === contractAddress, - ); - }, - [network.name, pairs], - ); - - const findPair = useCallback( - (addresses: [string, string]) => { - if (addresses[0] === addresses[1]) { - return undefined; - } - return pairs[network.name]?.data?.find( - (pair) => - pair.asset_addresses.includes(addresses[0]) && - pair.asset_addresses.includes(addresses[1]), - ); - }, - [network.name, pairs], - ); - - const findPairByLpAddress = useCallback( - (lpAddress: string) => { - return pairs[network.name]?.data?.find( - (pair) => pair.liquidity_token === lpAddress, - ); - }, - [network.name, pairs], - ); - - return useMemo( - () => ({ - pairs: pairs[network.name as NetworkName]?.data, - getPairedAddresses, - availableAssetAddresses, - getPair, - findPair, - findPairByLpAddress, - }), - [ - availableAssetAddresses, - getPairedAddresses, - network.name, - pairs, - getPair, - findPair, - findPairByLpAddress, - ], - ); -}; - -export default usePairs; diff --git a/src/hooks/usePairBookmark.ts b/src/hooks/usePairBookmark.ts index 3b68e08e..7ab4446b 100644 --- a/src/hooks/usePairBookmark.ts +++ b/src/hooks/usePairBookmark.ts @@ -1,5 +1,5 @@ import { useCallback, useMemo } from "react"; -import { useNetwork } from "hooks/useNetwork"; +import useNetwork from "hooks/useNetwork"; import { bookmarksAtom } from "stores/pairs"; import { useAtom } from "jotai"; diff --git a/src/hooks/usePairs.ts b/src/hooks/usePairs.ts new file mode 100644 index 00000000..bcf4031e --- /dev/null +++ b/src/hooks/usePairs.ts @@ -0,0 +1,105 @@ +import { useCallback, useMemo } from "react"; +import useAPI from "hooks/useAPI"; +import useNetwork from "hooks/useNetwork"; +import { useQuery } from "@tanstack/react-query"; + +const usePairs = () => { + const network = useNetwork(); + const api = useAPI(); + + const { data: pairs, isLoading } = useQuery({ + queryKey: ["pairs", network.name], + queryFn: async () => { + const res = await api.getPairs(); + return res?.pairs.map((pair) => ({ + ...pair, + asset_addresses: pair.asset_infos.map((assetInfo) => + "token" in assetInfo + ? assetInfo.token.contract_addr + : assetInfo.native_token.denom, + ), + })); + }, + refetchInterval: false, + refetchOnMount: false, + refetchOnReconnect: true, + refetchOnWindowFocus: false, + }); + + const getPairedAddresses = useCallback( + (searchAddress: string) => { + const pairAddresses = pairs + ?.filter((pair) => { + return pair.asset_addresses.includes(searchAddress); + }) + .map((pair) => { + return pair.asset_addresses.find( + (address) => address !== searchAddress, + ) as string; + }); + return pairAddresses; + }, + [pairs], + ); + + const availableAssetAddresses = useMemo(() => { + return ( + pairs + ?.reduce((acc, pair) => { + return [...acc, ...pair.asset_addresses]; + }, [] as string[]) + ?.filter((asset, index, array) => array.indexOf(asset) === index) || [] + ); + }, [pairs]); + + const getPair = useCallback( + (contractAddress: string) => { + return pairs?.find((pair) => pair.contract_addr === contractAddress); + }, + [pairs], + ); + + const findPair = useCallback( + (addresses: [string, string]) => { + if (addresses[0] === addresses[1]) { + return undefined; + } + return pairs?.find( + (pair) => + pair.asset_addresses.includes(addresses[0]) && + pair.asset_addresses.includes(addresses[1]), + ); + }, + [pairs], + ); + + const findPairByLpAddress = useCallback( + (lpAddress: string) => { + return pairs?.find((pair) => pair.liquidity_token === lpAddress); + }, + [pairs], + ); + + return useMemo( + () => ({ + pairs, + isLoading, + getPairedAddresses, + availableAssetAddresses, + getPair, + findPair, + findPairByLpAddress, + }), + [ + availableAssetAddresses, + getPairedAddresses, + pairs, + isLoading, + getPair, + findPair, + findPairByLpAddress, + ], + ); +}; + +export default usePairs; diff --git a/src/hooks/usePool.ts b/src/hooks/usePool.ts index f2aafb1c..838637e2 100644 --- a/src/hooks/usePool.ts +++ b/src/hooks/usePool.ts @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState } from "react"; -import { Pool } from "types/pair"; -import { useAPI } from "hooks/useAPI"; +import { Pool } from "types/api"; +import useAPI from "hooks/useAPI"; const usePool = (contractAddress?: string) => { const [pool, setPool] = useState(); diff --git a/src/hooks/usePools.ts b/src/hooks/usePools.ts new file mode 100644 index 00000000..bd1862f6 --- /dev/null +++ b/src/hooks/usePools.ts @@ -0,0 +1,16 @@ +import { useQuery } from "@tanstack/react-query"; +import useNetwork from "./useNetwork"; +import useAPI from "./useAPI"; + +const usePools = () => { + const network = useNetwork(); + const api = useAPI(); + const { data: pools } = useQuery(["pools", network.name], async () => { + const res = await api.getPools(); + return res; + }); + + return { pools }; +}; + +export default usePools; diff --git a/src/hooks/useVerifiedAssets.ts b/src/hooks/useVerifiedAssets.ts new file mode 100644 index 00000000..c5327069 --- /dev/null +++ b/src/hooks/useVerifiedAssets.ts @@ -0,0 +1,31 @@ +import { useQuery } from "@tanstack/react-query"; +import useAPI from "./useAPI"; +import useNetwork from "./useNetwork"; + +const useVerifiedAssets = () => { + const api = useAPI(); + const network = useNetwork(); + const { data: verifiedAssets } = useQuery({ + queryKey: ["verifiedAssets"], + queryFn: api.getVerifiedTokenInfos, + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchInterval: false, + refetchOnReconnect: true, + }); + const { data: verifiedIbcAssets } = useQuery({ + queryKey: ["verifiedIbcAssets"], + queryFn: api.getVerifiedIbcTokenInfos, + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchInterval: false, + refetchOnReconnect: true, + }); + + return { + verifiedAssets: verifiedAssets?.[network.name], + verifiedIbcAssets: verifiedIbcAssets?.[network.name], + }; +}; + +export default useVerifiedAssets; diff --git a/src/layout/Main/Footer.tsx b/src/layout/Main/Footer.tsx index ba6b4baa..7f9eff67 100644 --- a/src/layout/Main/Footer.tsx +++ b/src/layout/Main/Footer.tsx @@ -18,7 +18,7 @@ import { MOBILE_SCREEN_CLASS, TABLET_SCREEN_CLASS } from "constants/layout"; import { css } from "@emotion/react"; import { useLatestBlock } from "hooks/useLatestBlock"; import { getBlockLink } from "utils"; -import { useNetwork } from "hooks/useNetwork"; +import useNetwork from "hooks/useNetwork"; import Tooltip from "components/Tooltip"; const Wrapper = styled.footer` diff --git a/src/layout/Main/Header.tsx b/src/layout/Main/Header.tsx index 2debf76e..b4655e49 100644 --- a/src/layout/Main/Header.tsx +++ b/src/layout/Main/Header.tsx @@ -19,7 +19,7 @@ import { SMALL_BROWSER_SCREEN_CLASS, TABLET_SCREEN_CLASS, } from "constants/layout"; -import { useModal } from "hooks/useModal"; +import useModal from "hooks/useModal"; import { useConnectedWallet, useWallet } from "@xpla/wallet-provider"; import { amountToValue, @@ -28,7 +28,7 @@ import { formatNumber, getAddressLink, } from "utils"; -import { useBalance } from "hooks/useBalance"; +import useBalance from "hooks/useBalance"; import { XPLA_ADDRESS, XPLA_SYMBOL } from "constants/network"; import iconDropdown from "assets/icons/icon-dropdown-arrow.svg"; import iconXpla from "assets/icons/icon-xpla-24px.svg"; @@ -37,7 +37,7 @@ import { Popover } from "react-tiny-popover"; import Panel from "components/Panel"; import { css, useTheme } from "@emotion/react"; import Hr from "components/Hr"; -import { useNetwork } from "hooks/useNetwork"; +import useNetwork from "hooks/useNetwork"; import Box from "components/Box"; import Modal from "components/Modal"; import Copy from "components/Copy"; diff --git a/src/layout/Main/index.tsx b/src/layout/Main/index.tsx index 3f942ad4..fa9b2176 100644 --- a/src/layout/Main/index.tsx +++ b/src/layout/Main/index.tsx @@ -13,7 +13,7 @@ import iconTrade from "assets/icons/icon-trade.svg"; import iconPool from "assets/icons/icon-pool.svg"; import { useScreenClass } from "react-grid-system"; import { MOBILE_SCREEN_CLASS, TABLET_SCREEN_CLASS } from "constants/layout"; -import { useNetwork } from "hooks/useNetwork"; +import useNetwork from "hooks/useNetwork"; import Tooltip from "components/Tooltip"; import Footer from "./Footer"; diff --git a/src/main.tsx b/src/main.tsx index 2b5f8748..7fc42d21 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,7 +4,7 @@ import App from "App"; import ReactDOM from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; import theme from "styles/theme"; -import { QueryClient, QueryClientProvider } from "react-query"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import "simplebar"; import "simplebar/dist/simplebar.css"; import ResizeObserver from "resize-observer-polyfill"; diff --git a/src/pages/Pool/Create/InputGroup.tsx b/src/pages/Pool/Create/InputGroup.tsx index 4a03a2b6..b9fa555e 100644 --- a/src/pages/Pool/Create/InputGroup.tsx +++ b/src/pages/Pool/Create/InputGroup.tsx @@ -7,12 +7,13 @@ import Copy from "components/Copy"; import { NumberInput } from "components/Input"; import Typography from "components/Typography"; import { Col, Row, useScreenClass } from "react-grid-system"; -import { Asset } from "types/common"; import { formatNumber, formatDecimals, amountToValue } from "utils"; import iconDefaultToken from "assets/icons/icon-default-token.svg"; +import { Token } from "types/api"; +import useBalance from "hooks/useBalance"; interface InputGroupProps extends React.HTMLAttributes { - asset?: Partial; + asset?: Partial; onBalanceClick?( value: string, event: React.MouseEvent, @@ -44,6 +45,7 @@ const InputGroup = forwardRef( ({ asset, onBalanceClick, style, ...inputProps }, ref) => { const screenClass = useScreenClass(); const theme = useTheme(); + const balance = useBalance(asset?.token); return ( @@ -51,15 +53,12 @@ const InputGroup = forwardRef( - + {asset?.symbol} - + @@ -74,7 +73,7 @@ const InputGroup = forwardRef( onClick={(event) => { if (onBalanceClick) { onBalanceClick( - amountToValue(asset?.balance, asset?.decimals) || "", + amountToValue(balance, asset?.decimals) || "", event, ); } @@ -90,7 +89,7 @@ const InputGroup = forwardRef( > {formatNumber( formatDecimals( - amountToValue(asset?.balance, asset?.decimals) || 0, + amountToValue(balance, asset?.decimals) || 0, 3, ), )} diff --git a/src/pages/Pool/Create/index.tsx b/src/pages/Pool/Create/index.tsx index 2b775615..75bcdc8e 100644 --- a/src/pages/Pool/Create/index.tsx +++ b/src/pages/Pool/Create/index.tsx @@ -31,7 +31,7 @@ import { LOCKED_LP_SUPPLY, LP_DECIMALS } from "constants/dezswap"; import { CreateTxOptions, Numeric } from "@xpla/xpla.js"; import Typography from "components/Typography"; import useBalanceMinusFee from "hooks/useBalanceMinusFee"; -import { useFee } from "hooks/useFee"; +import useFee from "hooks/useFee"; import { XPLA_ADDRESS, XPLA_SYMBOL } from "constants/network"; import { generateCreatePoolMsg } from "utils/dezswap"; import { NetworkName } from "types/common"; @@ -40,7 +40,7 @@ import InputGroup from "pages/Pool/Provide/InputGroup"; import IconButton from "components/IconButton"; import iconLink from "assets/icons/icon-link.svg"; import useRequestPost from "hooks/useRequestPost"; -import { useNetwork } from "hooks/useNetwork"; +import useNetwork from "hooks/useNetwork"; import Message from "components/Message"; import useConnectWalletModal from "hooks/modals/useConnectWalletModal"; import InfoTable from "components/InfoTable"; @@ -146,9 +146,9 @@ function CreatePage() { const createTxOptions = useMemo( () => connectedWallet && - asset1?.address && + asset1?.token && formData.asset1Value && - asset2?.address && + asset2?.token && formData.asset2Value && !Numeric.parse(formData.asset1Value).isNaN() && !Numeric.parse(formData.asset2Value).isNaN() @@ -158,13 +158,13 @@ function CreatePage() { connectedWallet.walletAddress, [ { - address: asset1?.address || "", + address: asset1?.token || "", amount: valueToAmount(formData.asset1Value, asset1?.decimals) || "0", }, { - address: asset2?.address || "", + address: asset2?.token || "", amount: valueToAmount(formData.asset2Value, asset2?.decimals) || "0", @@ -192,22 +192,14 @@ function CreatePage() { return fee?.amount?.get(XPLA_ADDRESS)?.amount.toString() || "0"; }, [fee]); - const asset1Balance = useBalanceMinusFee( - asset1?.address, - asset1?.balance, - feeAmount, - ); - const asset2Balance = useBalanceMinusFee( - asset2?.address, - asset2?.balance, - feeAmount, - ); + const asset1Balance = useBalanceMinusFee(asset1?.token, feeAmount); + const asset2Balance = useBalanceMinusFee(asset2?.token, feeAmount); useEffect(() => { if ( connectedWallet && balanceApplied && - asset1?.address === XPLA_ADDRESS && + asset1?.token === XPLA_ADDRESS && formData.asset1Value && Numeric.parse(formData.asset1Value || 0).gt( Numeric.parse(amountToValue(asset1Balance, asset1?.decimals) || 0), @@ -227,7 +219,7 @@ function CreatePage() { if ( connectedWallet && balanceApplied && - asset2?.address === XPLA_ADDRESS && + asset2?.token === XPLA_ADDRESS && formData.asset2Value && Numeric.parse(formData.asset2Value || 0).gt( Numeric.parse(amountToValue(asset2Balance, asset2?.decimals) || 0), @@ -515,13 +507,13 @@ function CreatePage() { > ({ - key: asset?.address, + key: asset?.token, label: `${asset?.symbol} Address`, value: ( <> - {ellipsisCenter(asset?.address)}  + {ellipsisCenter(asset?.token)} 
("form"); const { customAssets, addCustomAsset } = useCustomAssets(); const { availableAssetAddresses } = usePairs(); - const { verifiedAssets, verifiedIbcAssets } = useAssets(); + const { verifiedAssets, verifiedIbcAssets } = useVerifiedAssets(); const network = useNetwork(); + const lcd = useLCDClient(); const api = useAPI(); @@ -67,8 +69,8 @@ function ImportAssetModal({ onFinish, ...modalProps }: ImportAssetModalProps) { ); const isDuplicated = useMemo(() => { return ( - customAssets?.some((item) => item.address === address) || - availableAssetAddresses?.addresses?.some((item) => item === address) + customAssets?.some((item) => item.token === address) || + availableAssetAddresses.some((item) => item === address) ); }, [address, availableAssetAddresses, customAssets]); @@ -100,10 +102,10 @@ function ImportAssetModal({ onFinish, ...modalProps }: ImportAssetModalProps) { const fetchAsset = async () => { if (isNativeToken) { const asset = nativeTokens[network.name]?.find( - (item) => item.address === deferredAddress, + (item) => item.token === deferredAddress, ); if (!isAborted && asset) { - setTokenInfo(asset as TokenInfo); + setTokenInfo(asset); } } else if (isIbcToken) { if (verifiedIbcAssets) { @@ -117,7 +119,9 @@ function ImportAssetModal({ onFinish, ...modalProps }: ImportAssetModalProps) { } } else if (isValidAddress) { try { - const res = await api.getToken(deferredAddress); + const res = await lcd.wasm.contractQuery(address, { + token_info: {}, + }); if (!isAborted) { setTokenInfo(res); } @@ -141,7 +145,9 @@ function ImportAssetModal({ onFinish, ...modalProps }: ImportAssetModalProps) { isNativeToken, isIbcToken, verifiedIbcAssets, - network.name, + network, + lcd, + address, ]); const onSubmit: React.FormEventHandler = (event) => { @@ -153,7 +159,14 @@ function ImportAssetModal({ onFinish, ...modalProps }: ImportAssetModalProps) { event.preventDefault(); if (tokenInfo) { - const asset = { ...tokenInfo, balance: balance || "0", address }; + const asset = { + ...tokenInfo, + chainId: network.chainID, + icon: iconSrc || "", + protocol: "", + token: address, + verified: false, + }; addCustomAsset(asset); setPage("complete"); } @@ -161,7 +174,14 @@ function ImportAssetModal({ onFinish, ...modalProps }: ImportAssetModalProps) { const onDone = () => { if (onFinish && tokenInfo) { - const asset = { ...tokenInfo, balance: balance || "0", address }; + const asset = { + ...tokenInfo, + chainId: network.chainID, + icon: iconSrc || "", + protocol: "", + token: address, + verified: false, + }; onFinish(asset); } }; diff --git a/src/pages/Pool/PoolForm.tsx b/src/pages/Pool/PoolForm.tsx index 1ae55a6c..bc831130 100644 --- a/src/pages/Pool/PoolForm.tsx +++ b/src/pages/Pool/PoolForm.tsx @@ -9,7 +9,7 @@ import Typography from "components/Typography"; import { MOBILE_SCREEN_CLASS, TABLET_SCREEN_CLASS } from "constants/layout"; import useAssets from "hooks/useAssets"; import useHashModal from "hooks/useHashModal"; -import usePairs from "hooks/usePair"; +import usePairs from "hooks/usePairs"; import { formatNumber, formatDecimals, @@ -21,10 +21,11 @@ import iconDropdown from "assets/icons/icon-dropdown-arrow.svg"; import iconDefaultAsset from "assets/icons/icon-default-token.svg"; import Button from "components/Button"; -import { useModal } from "hooks/useModal"; +import useModal from "hooks/useModal"; import { useAtom } from "jotai"; -import { useNetwork } from "hooks/useNetwork"; +import useNetwork from "hooks/useNetwork"; import { customAssetsAtom } from "stores/assets"; +import useBalance from "hooks/useBalance"; import PoolButton from "./PoolButton"; import ImportAssetModal from "./ImportAssetModal"; @@ -61,8 +62,8 @@ function PoolForm({ addresses, onChange: handleChange }: PoolFormProps) { const network = useNetwork(); const [customAssetStore] = useAtom(customAssetsAtom); const customAssetAddresses = useMemo(() => { - return customAssetStore[network.name]?.map((asset) => asset.address) || []; - }, [customAssetStore, [network.name]]); + return customAssetStore[network.name]?.map((asset) => asset.token) || []; + }, [customAssetStore, network.name]); const { getAsset } = useAssets(); @@ -76,6 +77,10 @@ function PoolForm({ addresses, onChange: handleChange }: PoolFormProps) { return address ? getAsset(address) : undefined; }); }, [getAsset, selectedAddress1, selectedAddress2]); + + const balance1 = useBalance(asset1?.token); + const balance2 = useBalance(asset2?.token); + const pair = useMemo(() => { return selectedAddress1 && selectedAddress2 ? findPair([selectedAddress1, selectedAddress2]) @@ -105,9 +110,19 @@ function PoolForm({ addresses, onChange: handleChange }: PoolFormProps) { `} > {[ - { key: "asset1", asset: asset1, modal: selectAsset1Modal }, - { key: "asset2", asset: asset2, modal: selectAsset2Modal }, - ].map(({ key, asset, modal }) => ( + { + key: "asset1", + asset: asset1, + modal: selectAsset1Modal, + balance: balance1, + }, + { + key: "asset2", + asset: asset2, + modal: selectAsset2Modal, + balance: balance2, + }, + ].map(({ key, asset, modal, balance }) => ( modal.open()}> {formatNumber( formatDecimals( - amountToValue(asset?.balance, asset?.decimals) || 0, + amountToValue(balance, asset?.decimals) || 0, 3, ), )} @@ -270,10 +285,7 @@ function PoolForm({ addresses, onChange: handleChange }: PoolFormProps) { }} > { if (handleChange) { const newAddresses = [...(addresses || [])]; diff --git a/src/pages/Pool/PoolItem.tsx b/src/pages/Pool/PoolItem.tsx index ffad9a27..93d5937c 100644 --- a/src/pages/Pool/PoolItem.tsx +++ b/src/pages/Pool/PoolItem.tsx @@ -4,8 +4,8 @@ import IconButton from "components/IconButton"; import Typography from "components/Typography"; import { LP_DECIMALS } from "constants/dezswap"; import useAssets from "hooks/useAssets"; -import { useBalance } from "hooks/useBalance"; -import { useNetwork } from "hooks/useNetwork"; +import useBalance from "hooks/useBalance"; +import useNetwork from "hooks/useNetwork"; import { useEffect, useMemo, useRef, useState } from "react"; import { Row, Col, useScreenClass } from "react-grid-system"; import { @@ -24,8 +24,9 @@ import Button from "components/Button"; import { Link } from "react-router-dom"; import { MOBILE_SCREEN_CLASS, TABLET_SCREEN_CLASS } from "constants/layout"; import Tooltip from "components/Tooltip"; +import { Pool } from "types/api"; +import usePairs from "hooks/usePairs"; import Expand from "./Expand"; -import { PoolExtended } from "."; const SimplePieChart = styled.div<{ data: number[] }>` width: 100%; @@ -145,7 +146,7 @@ const IconButtonAnchor = styled(IconButton.withComponent("a"))` `; interface PoolItemProps { - pool: PoolExtended; + pool: Pool; bookmarked?: boolean; onBookmarkClick?: React.MouseEventHandler; } @@ -157,11 +158,13 @@ function PoolItem({ pool, bookmarked, onBookmarkClick }: PoolItemProps) { ); const { getAsset } = useAssets(); const network = useNetwork(); - const lpBalance = useBalance(pool.pair.liquidity_token); + const { getPair } = usePairs(); + const pair = useMemo(() => getPair(pool.address), [getPair, pool]); + const lpBalance = useBalance(pair?.liquidity_token); const [asset1, asset2] = useMemo( - () => pool.pair.asset_addresses.map((address) => getAsset(address)), - [getAsset, pool], + () => pair?.asset_addresses.map((address) => getAsset(address)) || [], + [getAsset, pair], ); const userShare = useMemo(() => { @@ -184,7 +187,11 @@ function PoolItem({ pool, bookmarked, onBookmarkClick }: PoolItemProps) { />, , ], - [bookmarked, network, onBookmarkClick, pool], + [bookmarked, network, onBookmarkClick, pair], ); const [overflowActive, setOverflowActive] = useState(false); @@ -232,9 +239,9 @@ function PoolItem({ pool, bookmarked, onBookmarkClick }: PoolItemProps) { font-size: 0; `} > - + diff --git a/src/pages/Pool/PoolList.tsx b/src/pages/Pool/PoolList.tsx index 258a0900..8a03ecf1 100644 --- a/src/pages/Pool/PoolList.tsx +++ b/src/pages/Pool/PoolList.tsx @@ -7,11 +7,11 @@ import { MOBILE_SCREEN_CLASS, TABLET_SCREEN_CLASS } from "constants/layout"; import usePairBookmark from "hooks/usePairBookmark"; import { useScreenClass } from "react-grid-system"; import iconSortDisabled from "assets/icons/icon-sort-disabled.svg"; -import { PoolExtended } from "."; +import { Pool } from "types/api"; import PoolItem from "./PoolItem"; interface PoolListProps { - pools: PoolExtended[]; + pools: Pool[]; emptyMessage?: string; } @@ -129,10 +129,10 @@ function PoolList({ {pools.map((pool) => { return ( toggleBookmark(pool.pair.contract_addr)} + bookmarked={bookmarks?.includes(pool.address)} + onBookmarkClick={() => toggleBookmark(pool.address)} /> ); })} diff --git a/src/pages/Pool/Provide/InputGroup.tsx b/src/pages/Pool/Provide/InputGroup.tsx index 8c40618f..e5ae743e 100644 --- a/src/pages/Pool/Provide/InputGroup.tsx +++ b/src/pages/Pool/Provide/InputGroup.tsx @@ -7,12 +7,13 @@ import Copy from "components/Copy"; import { NumberInput } from "components/Input"; import Typography from "components/Typography"; import { Col, Row, useScreenClass } from "react-grid-system"; -import { Asset } from "types/common"; import { formatNumber, formatDecimals, amountToValue } from "utils"; import iconDefaultToken from "assets/icons/icon-default-token.svg"; +import { Token } from "types/api"; +import useBalance from "hooks/useBalance"; interface InputGroupProps extends React.HTMLAttributes { - asset?: Partial | null; + asset?: Partial | null; onBalanceClick?( value: string, event: React.MouseEvent, @@ -44,6 +45,7 @@ const InputGroup = forwardRef( ({ asset, onBalanceClick, style, ...inputProps }, ref) => { const screenClass = useScreenClass(); const theme = useTheme(); + const balance = useBalance(asset?.token); return ( @@ -51,15 +53,12 @@ const InputGroup = forwardRef( - + {asset?.symbol} - + @@ -74,7 +73,7 @@ const InputGroup = forwardRef( onClick={(event) => { if (onBalanceClick) { onBalanceClick( - amountToValue(asset?.balance, asset?.decimals) || "", + amountToValue(balance, asset?.decimals) || "", event, ); } @@ -90,7 +89,7 @@ const InputGroup = forwardRef( > {formatNumber( formatDecimals( - amountToValue(asset?.balance, asset?.decimals) || 0, + amountToValue(balance, asset?.decimals) || 0, 3, ), )} diff --git a/src/pages/Pool/Provide/index.tsx b/src/pages/Pool/Provide/index.tsx index 83bd0fa8..c3e75e5b 100644 --- a/src/pages/Pool/Provide/index.tsx +++ b/src/pages/Pool/Provide/index.tsx @@ -7,7 +7,7 @@ import { } from "react"; import Modal from "components/Modal"; import { useNavigate, useParams } from "react-router-dom"; -import usePairs from "hooks/usePair"; +import usePairs from "hooks/usePairs"; import useAssets from "hooks/useAssets"; import { useForm } from "react-hook-form"; import { Col, Row, useScreenClass } from "react-grid-system"; @@ -31,7 +31,7 @@ import { LOCKED_LP_SUPPLY, LP_DECIMALS } from "constants/dezswap"; import { AccAddress, CreateTxOptions, Numeric } from "@xpla/xpla.js"; import Typography from "components/Typography"; import useBalanceMinusFee from "hooks/useBalanceMinusFee"; -import { useFee } from "hooks/useFee"; +import useFee from "hooks/useFee"; import { XPLA_ADDRESS, XPLA_SYMBOL } from "constants/network"; import { generateAddLiquidityMsg } from "utils/dezswap"; import { NetworkName } from "types/common"; @@ -41,7 +41,7 @@ import InputGroup from "pages/Pool/Provide/InputGroup"; import IconButton from "components/IconButton"; import iconLink from "assets/icons/icon-link.svg"; import useRequestPost from "hooks/useRequestPost"; -import { useNetwork } from "hooks/useNetwork"; +import useNetwork from "hooks/useNetwork"; import usePool from "hooks/usePool"; import Message from "components/Message"; import useConnectWalletModal from "hooks/modals/useConnectWalletModal"; @@ -135,18 +135,16 @@ function ProvidePage() { isPoolEmpty ? { pairAddress: pairAddress || "", - asset1Address: asset1?.address || "", + asset1Address: asset1?.token || "", asset1Amount: valueToAmount(formData.asset1Value, asset1?.decimals) || "0", - asset2Address: asset2?.address || "", + asset2Address: asset2?.token || "", asset2Amount: valueToAmount(formData.asset2Value, asset2?.decimals) || "0", } : { pairAddress: pairAddress || "", - asset1Address: isReversed - ? asset2?.address || "" - : asset1?.address || "", + asset1Address: isReversed ? asset2?.token || "" : asset1?.token || "", asset1Amount: isReversed ? valueToAmount(formData.asset2Value, asset2?.decimals) || "0" : valueToAmount(formData.asset1Value, asset1?.decimals) || "0", @@ -158,9 +156,9 @@ function ProvidePage() { simulationResult?.estimatedAmount && !simulationResult?.isLoading && connectedWallet && - asset1?.address && + asset1?.token && formData.asset1Value && - asset2?.address && + asset2?.token && formData.asset2Value && !Numeric.parse(formData.asset1Value).isNaN() && !Numeric.parse(formData.asset2Value).isNaN() @@ -171,13 +169,13 @@ function ProvidePage() { pairAddress || "", [ { - address: asset1?.address || "", + address: asset1?.token || "", amount: valueToAmount(formData.asset1Value, asset1?.decimals) || "0", }, { - address: asset2?.address || "", + address: asset2?.token || "", amount: valueToAmount(formData.asset2Value, asset2?.decimals) || "0", @@ -209,17 +207,9 @@ function ProvidePage() { return fee?.amount?.get(XPLA_ADDRESS)?.amount.toString() || "0"; }, [fee]); - const asset1BalanceMinusFee = useBalanceMinusFee( - asset1?.address, - asset1?.balance, - feeAmount, - ); + const asset1BalanceMinusFee = useBalanceMinusFee(asset1?.token, feeAmount); - const asset2BalanceMinusFee = useBalanceMinusFee( - asset2?.address, - asset2?.balance, - feeAmount, - ); + const asset2BalanceMinusFee = useBalanceMinusFee(asset2?.token, feeAmount); const buttonMsg = useMemo(() => { if (formData.asset1Value) { @@ -293,7 +283,7 @@ function ProvidePage() { connectedWallet && balanceApplied && (!isReversed || isPoolEmpty) && - asset1?.address === XPLA_ADDRESS && + asset1?.token === XPLA_ADDRESS && formData.asset1Value && Numeric.parse(formData.asset1Value || 0).gt( Numeric.parse( @@ -316,7 +306,7 @@ function ProvidePage() { connectedWallet && balanceApplied && (isReversed || isPoolEmpty) && - asset2?.address === XPLA_ADDRESS && + asset2?.token === XPLA_ADDRESS && formData.asset2Value && Numeric.parse(formData.asset2Value || 0).gt( Numeric.parse( @@ -592,7 +582,7 @@ function ProvidePage() { "token" in a.info ? a.info.token.contract_addr : a.info.native_token.denom === - asset1?.address, + asset1?.token, )?.amount, asset1?.decimals, ) || "0", @@ -619,7 +609,7 @@ function ProvidePage() { "token" in a.info ? a.info.token.contract_addr : a.info.native_token.denom === - asset2?.address, + asset2?.token, )?.amount, asset2?.decimals, ) || "0", @@ -706,9 +696,9 @@ function ProvidePage() { label: `${asset1?.symbol || ""} Address`, value: ( - {ellipsisCenter(asset1?.address)}  + {ellipsisCenter(asset1?.token)}  - {ellipsisCenter(asset2?.address)}  + {ellipsisCenter(asset2?.token)}  { lpToken?: string; - assets?: (Partial | undefined)[]; + assets?: (Partial | undefined)[]; onBalanceClick?( value: string, event: React.MouseEvent, @@ -46,7 +46,7 @@ const InputGroup = forwardRef( {assets?.[0]?.symbol}( /> {assets?.[0]?.symbol} -  {assets?.[1]?.symbol} pair - ? pair.asset_addresses - .map((address) => getAsset(address)) - .map((a) => ({ - address: a?.address, - symbol: a?.symbol, - decimals: a?.decimals, - iconSrc: a?.iconSrc, - })) + ? pair.asset_addresses.map((address) => getAsset(address)) : [undefined, undefined], [getAsset, pair], ); useEffect(() => { const timerId = setTimeout(() => { - if (!asset1?.address || !asset2?.address) { + if (!asset1?.token || !asset2?.token) { errorMessageModal.open(); } }, 1500); @@ -126,7 +113,7 @@ function WithdrawPage() { criteriaMode: "all", mode: "all", }); - const { register, formState } = form; + const { register } = form; const { lpValue } = form.watch(); @@ -161,7 +148,7 @@ function WithdrawPage() { pairAddress || "", pair?.liquidity_token || "", valueToAmount(lpValue, LP_DECIMALS) || "0", - [asset1?.address, asset2?.address].map((address) => ({ + [asset1?.token, asset2?.token].map((address) => ({ address: address || "", amount: Numeric.parse( simulationResult?.estimatedAmount?.find( @@ -301,7 +288,7 @@ function WithdrawPage() { `} > {asset1?.symbol} a.address === asset1?.address, + (a) => a.address === asset1?.token, )?.amount, asset1?.decimals, ) || "0", @@ -383,7 +370,7 @@ function WithdrawPage() { `} > {asset2?.symbol} a.address === asset2?.address, + (a) => a.address === asset2?.token, )?.amount, asset2?.decimals, ) || "0", @@ -467,7 +454,7 @@ function WithdrawPage() { value: `${formatNumber( amountToValue( simulationResult?.estimatedAmount?.find( - (a) => a.address === asset1?.address, + (a) => a.address === asset1?.token, )?.amount, asset1?.decimals, ) || "", @@ -475,7 +462,7 @@ function WithdrawPage() { ${formatNumber( amountToValue( simulationResult?.estimatedAmount?.find( - (a) => a.address === asset2?.address, + (a) => a.address === asset2?.token, )?.amount, asset1?.decimals, ) || "", @@ -540,9 +527,9 @@ function WithdrawPage() { label: `${asset1?.symbol} Address`, value: ( <> - {ellipsisCenter(asset1?.address)}  + {ellipsisCenter(asset1?.token)}  - {ellipsisCenter(asset2?.address)}  + {ellipsisCenter(asset2?.token)}  ([]); + const { pools } = usePools(); const [selectedTabIndex, setSelectedTabIndex] = useState(0); const [addresses, setAddresses] = useState<[string | undefined, string | undefined]>(); const [selectedPair, setSelectedPair] = useState(); + const api = useAPI(); + + const balances = useQueries({ + queries: + pools?.map((pool) => ({ + queryKey: ["pool", pool.address], + queryFn: async () => { + const lpAddress = getPair(pool.address)?.liquidity_token; + if (lpAddress) { + const balance = await api.getTokenBalance(lpAddress); + return balance; + } + return "0"; + }, + enabled: !!pool.address, + refetchInterval: 30000, + refetchOnMount: false, + refetchOnReconnect: true, + refetchOnWindowFocus: false, + })) || [], + }).map((item) => item.data); const poolList = useMemo(() => { - return pools.filter((item) => { + return pools?.filter((item, index) => { const isSelectedPair = - !selectedPair || item.pair.contract_addr === selectedPair.contract_addr; + !selectedPair || item.address === selectedPair.contract_addr; switch (selectedTabIndex) { case 0: return isSelectedPair; case 1: - return isSelectedPair && item.hasBalance; + return isSelectedPair && Numeric.parse(balances[index] || 0).gt(0); case 2: - return ( - isSelectedPair && !!bookmarks?.includes(item.pair.contract_addr) - ); + return isSelectedPair && !!bookmarks?.includes(item.address); default: // do nothing } return false; }); - }, [bookmarks, pools, selectedTabIndex, selectedPair]); + }, [pools, selectedPair, selectedTabIndex, balances, bookmarks]); const handleReloadClick = useCallback(() => { setAddresses(undefined); @@ -89,37 +102,6 @@ function PoolPage() { } }, [addresses, findPair]); - useEffect(() => { - let isAborted = false; - const fetchPools = async () => { - if (pairs) { - const res = await Promise.all( - pairs.map(async (item) => { - if (isAborted) { - return undefined; - } - const pool = await api.getPool(item.contract_addr); - let hasBalance = false; - try { - const balance = await api.getTokenBalance(item.liquidity_token); - hasBalance = Numeric.parse(balance || 0).gt(0); - } catch (error) { - console.log(error); - } - return { ...pool, pair: item, hasBalance }; - }), - ); - if (!isAborted) { - setPools(res.filter((pool) => pool) as PoolExtended[]); - } - } - }; - fetchPools(); - return () => { - isAborted = true; - }; - }, [api, pairs, location]); - useEffect(() => { setCurrentPage(1); }, [selectedTabIndex]); @@ -250,10 +232,12 @@ function PoolPage() { `} >
- {!!poolList.length && ( + {!!poolList?.length && ( { if (asset1 === undefined || asset2 === undefined) { @@ -428,11 +424,9 @@ function SwapPage() { }} > { const target = selectAsset1Modal.isOpen @@ -501,7 +495,7 @@ function SwapPage() { height: 20px; position: relative; background-image: ${`url(${ - asset1?.iconSrc || iconDefaultAsset + asset1?.icon || iconDefaultAsset })`}; background-position: 50% 50%; background-size: auto 20px; @@ -718,7 +712,7 @@ function SwapPage() { height: 20px; position: relative; background-image: ${`url(${ - asset2?.iconSrc || iconDefaultAsset + asset2?.icon || iconDefaultAsset })`}; background-position: 50% 50%; background-size: auto 20px; diff --git a/src/pages/Trade/Swap/useSimulate.ts b/src/pages/Trade/Swap/useSimulate.ts index f33edb62..c35da73c 100644 --- a/src/pages/Trade/Swap/useSimulate.ts +++ b/src/pages/Trade/Swap/useSimulate.ts @@ -1,10 +1,10 @@ import { useDeferredValue, useEffect, useMemo, useRef, useState } from "react"; import { Numeric } from "@xpla/xpla.js"; import { ReverseSimulation, Simulation } from "types/pair"; -import usePairs from "hooks/usePair"; -import { useNetwork } from "hooks/useNetwork"; -import { useAPI } from "hooks/useAPI"; -import { useLCDClient } from "hooks/useLCDClient"; +import usePairs from "hooks/usePairs"; +import useNetwork from "hooks/useNetwork"; +import useAPI from "hooks/useAPI"; +import useLCDClient from "hooks/useLCDClient"; const useSimulate = ({ fromAddress, diff --git a/src/stores/assets.ts b/src/stores/assets.ts index b73477ac..51898fde 100644 --- a/src/stores/assets.ts +++ b/src/stores/assets.ts @@ -1,23 +1,15 @@ import { atomWithStorage } from "jotai/utils"; -import { Asset, NetworkName } from "types/common"; -import { atom } from "jotai"; -import { VerifiedAssets, VerifiedIbcAssets } from "types/token"; +import { NetworkName } from "types/common"; +import { Token } from "types/api"; -const assetsAtom = atomWithStorage<{ - [K in NetworkName]?: Asset[]; -}>("assets", { mainnet: [], testnet: [] }); +interface CustomToken extends Token { + updatedAt?: Date; +} export const customAssetsAtom = atomWithStorage<{ - [K in NetworkName]?: Asset[]; + [K in NetworkName]?: CustomToken[]; }>("customAssets", { mainnet: [], testnet: [] }); -export const verifiedAssetsAtom = atom(undefined); -export const verifiedIbcAssetsAtom = atom( - undefined, -); - export const bookmarksAtom = atomWithStorage<{ [K in NetworkName]?: string[]; }>("bookmarks", { mainnet: [], testnet: [] }); - -export default assetsAtom; diff --git a/src/stores/pairs.ts b/src/stores/pairs.ts index 4d70c201..b1e0fb60 100644 --- a/src/stores/pairs.ts +++ b/src/stores/pairs.ts @@ -1,15 +1,6 @@ -import { atom } from "jotai"; import { atomWithStorage } from "jotai/utils"; -import { NetworkName, PairExtended } from "types/common"; - -const pairsAtom = atom<{ - [K in NetworkName]?: { data?: PairExtended[] }; -}>({}); - -export const isPairsLoadingAtom = atom(false); +import { NetworkName } from "types/common"; export const bookmarksAtom = atomWithStorage<{ [K in NetworkName]?: string[]; }>("pair-bookmarks", { mainnet: [], testnet: [] }); - -export default pairsAtom; diff --git a/src/types/api.d.ts b/src/types/api.d.ts new file mode 100644 index 00000000..ce7c7437 --- /dev/null +++ b/src/types/api.d.ts @@ -0,0 +1,45 @@ +export type AssetInfo = + | { + token: { + contract_addr: string; + }; + } + | { + native_token: { + denom: string; + }; + }; + +export interface Pair { + asset_decimals: [number, number]; + asset_infos: [AssetInfo, AssetInfo]; + contract_addr: string; + liquidity_token: string; +} + +export interface Pairs { + pairs: Pair[]; +} + +export interface PoolAsset { + info: AssetInfo; + amount: string; +} + +export interface Pool { + address: string; + assets: [PoolAsset, PoolAsset]; + total_share: string; +} + +export interface Token { + chainId: string; + decimals: number; + icon: string; + name: string; + protocol: string; + symbol: string; + token: string; + total_supply: string; + verified: boolean; +} diff --git a/src/types/common.d.ts b/src/types/common.d.ts index 3724afd1..17684ab8 100644 --- a/src/types/common.d.ts +++ b/src/types/common.d.ts @@ -6,7 +6,6 @@ import { TxFailed, TxUnspecifiedError, } from "@xpla/wallet-provider"; -import { TokenInfo } from "types/token"; import { Pair } from "types/pair"; export type TxError = @@ -19,13 +18,6 @@ export type TxError = export type NetworkName = "testnet" | "mainnet"; -export interface Asset extends TokenInfo { - address: string; - iconSrc?: string; - balance: string; - updatedAt?: Date | string | number; -} - export interface PairExtended extends Pair { asset_addresses: string[]; } diff --git a/src/types/factory.d.ts b/src/types/factory.d.ts deleted file mode 100644 index 67809321..00000000 --- a/src/types/factory.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Pair } from "types/pair"; - -export interface Pairs { - pairs: Pair[]; -} diff --git a/src/types/router.d.ts b/src/types/router.d.ts deleted file mode 100644 index 5a84cb82..00000000 --- a/src/types/router.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type SimulateSwapOperations = { - amount: string; -}; diff --git a/src/utils/index.ts b/src/utils/index.ts index c7d6efcf..26833f27 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -21,7 +21,7 @@ export const cutDecimal = (value: Numeric.Input, decimals: number) => Numeric.parse(value).toFixed(decimals, Decimal.ROUND_FLOOR); export const isNativeTokenAddress = (network: string, address: string) => - nativeTokens[network].filter((n) => n.address === address).length > 0; + nativeTokens[network].filter((n) => n.token === address).length > 0; export const ellipsisCenter = (text = "", letterCountPerSide = 6) => { if (text.length <= letterCountPerSide * 2 + 3) { diff --git a/yarn.lock b/yarn.lock index ecef9c8a..50b48039 100644 --- a/yarn.lock +++ b/yarn.lock @@ -176,7 +176,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.19.0" -"@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.7", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2": +"@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.7": version "7.21.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.5.tgz#8492dddda9644ae3bda3b45eabe87382caee7200" integrity sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q== @@ -807,6 +807,19 @@ "@svgr/hast-util-to-babel-ast" "^7.0.0" svg-parser "^2.0.4" +"@tanstack/query-core@4.29.7": + version "4.29.7" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.29.7.tgz#9fe4587e23cb9566b937c518ffa44226041d388d" + integrity sha512-GXG4b5hV2Loir+h2G+RXhJdoZhJLnrBWsuLB2r0qBRyhWuXq9w/dWxzvpP89H0UARlH6Mr9DiVj4SMtpkF/aUA== + +"@tanstack/react-query@^4.29.7": + version "4.29.7" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.29.7.tgz#772996905a81ca64172582891c5a82e88dbafccd" + integrity sha512-ijBWEzAIo09fB1yd22slRZzprrZ5zMdWYzBnCg5qiXuFbH78uGN1qtGz8+Ed4MuhaPaYSD+hykn+QEKtQviEtg== + dependencies: + "@tanstack/query-core" "4.29.7" + use-sync-external-store "^1.2.0" + "@terra-money/legacy.proto@npm:@terra-money/terra.proto@^0.1.7": version "0.1.7" resolved "https://registry.yarnpkg.com/@terra-money/terra.proto/-/terra.proto-0.1.7.tgz#59c18f30da10d43200bab3ba8feb5b17e43a365f" @@ -1493,11 +1506,6 @@ big-integer@1.6.36: resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.36.tgz#78631076265d4ae3555c04f85e7d9d2f3a071a36" integrity sha512-t70bfa7HYEA1D9idDbmuv7YbsbVkQ+Hp+8KFSul4aE5e/i1bjCNIRYJZlA8Q8p0r9T8cF/RVvwUgRA//FydEyg== -big-integer@^1.6.16: - version "1.6.51" - resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" - integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== - bindings@^1.3.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" @@ -1565,20 +1573,6 @@ braces@^3.0.2: dependencies: fill-range "^7.0.1" -broadcast-channel@^3.4.1: - version "3.7.0" - resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-3.7.0.tgz#2dfa5c7b4289547ac3f6705f9c00af8723889937" - integrity sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg== - dependencies: - "@babel/runtime" "^7.7.2" - detect-node "^2.1.0" - js-sha3 "0.8.0" - microseconds "0.2.0" - nano-time "1.0.0" - oblivious-set "1.0.0" - rimraf "3.0.2" - unload "2.2.0" - brorand@^1.0.1, brorand@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" @@ -2072,11 +2066,6 @@ detect-browser@5.2.0: resolved "https://registry.yarnpkg.com/detect-browser/-/detect-browser-5.2.0.tgz#c9cd5afa96a6a19fda0bbe9e9be48a6b6e1e9c97" integrity sha512-tr7XntDAu50BVENgQfajMLzacmSe34D+qZc4zjnniz0ZVuw/TZcLcyxHQjYpJTM36sGEkZZlYLnIM1hH7alTMA== -detect-node@^2.0.4, detect-node@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" - integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== - diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" @@ -3419,14 +3408,6 @@ magic-string@^0.27.0: dependencies: "@jridgewell/sourcemap-codec" "^1.4.13" -match-sorter@^6.0.2: - version "6.3.1" - resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.3.1.tgz#98cc37fda756093424ddf3cbc62bfe9c75b92bda" - integrity sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw== - dependencies: - "@babel/runtime" "^7.12.5" - remove-accents "0.4.2" - md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -3454,11 +3435,6 @@ micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.2" picomatch "^2.3.1" -microseconds@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.2.0.tgz#233b25f50c62a65d861f978a4a4f8ec18797dc39" - integrity sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA== - miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" @@ -3531,13 +3507,6 @@ nan@^2.13.2: resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== -nano-time@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef" - integrity sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA== - dependencies: - big-integer "^1.6.16" - nanoid@^3.3.6: version "3.3.6" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" @@ -3681,11 +3650,6 @@ object.values@^1.1.6: define-properties "^1.1.4" es-abstract "^1.20.4" -oblivious-set@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/oblivious-set/-/oblivious-set-1.0.0.tgz#c8316f2c2fb6ff7b11b6158db3234c49f733c566" - integrity sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw== - once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -4050,15 +4014,6 @@ react-modal@^3.16.1: react-lifecycles-compat "^3.0.0" warning "^4.0.3" -react-query@^3.39.2: - version "3.39.3" - resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.39.3.tgz#4cea7127c6c26bdea2de5fb63e51044330b03f35" - integrity sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g== - dependencies: - "@babel/runtime" "^7.5.5" - broadcast-channel "^3.4.1" - match-sorter "^6.0.2" - react-refresh@^0.14.0: version "0.14.0" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e" @@ -4114,11 +4069,6 @@ regexp.prototype.flags@^1.4.3, regexp.prototype.flags@^1.5.0: define-properties "^1.2.0" functions-have-names "^1.2.3" -remove-accents@0.4.2: - version "0.4.2" - resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5" - integrity sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA== - require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -4175,7 +4125,7 @@ rfdc@^1.3.0: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== -rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: +rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -4693,14 +4643,6 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" -unload@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/unload/-/unload-2.2.0.tgz#ccc88fdcad345faa06a92039ec0f80b488880ef7" - integrity sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA== - dependencies: - "@babel/runtime" "^7.6.2" - detect-node "^2.0.4" - update-browserslist-db@^1.0.10: version "1.0.11" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" @@ -4724,6 +4666,11 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" +use-sync-external-store@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + utf-8-validate@^5.0.5: version "5.0.10" resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2" From a3543be1abf65beb2f48cc95aae268faa60094f7 Mon Sep 17 00:00:00 2001 From: maro Date: Wed, 28 Jun 2023 21:00:00 +0900 Subject: [PATCH 05/54] feat: layout for lock&drop - created new component, Switch. - renamed from Pool to Pools - seperated some components --- src/components/NavBar/index.tsx | 18 ++-- src/components/Switch/index.tsx | 48 ++++++++++ src/layout/Main/Header.tsx | 81 ++++++++++++++++- src/layout/Main/index.tsx | 4 +- .../AssetFormButton.tsx} | 9 +- .../PoolForm.tsx => Earn/AssetSelector.tsx} | 67 ++++---------- src/pages/{Pool => Earn}/Expand.tsx | 0 src/pages/{Pool => Earn}/ImportAssetModal.tsx | 0 src/pages/Earn/Lockdrop/index.tsx | 89 +++++++++++++++++++ .../Pools}/Create/InputGroup.tsx | 0 .../{Pool => Earn/Pools}/Create/index.tsx | 2 +- src/pages/{Pool => Earn/Pools}/PoolItem.tsx | 2 +- src/pages/{Pool => Earn/Pools}/PoolList.tsx | 0 .../Pools}/Provide/InputGroup.tsx | 0 .../{Pool => Earn/Pools}/Provide/index.tsx | 4 +- .../Pools}/Provide/useSimulate.ts | 0 src/pages/{Pool => Earn/Pools}/Select.tsx | 0 .../Pools}/Withdraw/InputGroup.tsx | 0 .../{Pool => Earn/Pools}/Withdraw/index.tsx | 4 +- .../Pools}/Withdraw/useSimulate.ts | 0 src/pages/{Pool => Earn/Pools}/index.tsx | 85 +++++++++--------- src/pages/Earn/index.tsx | 86 ++++++++++++++++++ src/routes.tsx | 59 ++++++++++-- 23 files changed, 436 insertions(+), 122 deletions(-) create mode 100644 src/components/Switch/index.tsx rename src/pages/{Pool/PoolButton.tsx => Earn/AssetFormButton.tsx} (71%) rename src/pages/{Pool/PoolForm.tsx => Earn/AssetSelector.tsx} (85%) rename src/pages/{Pool => Earn}/Expand.tsx (100%) rename src/pages/{Pool => Earn}/ImportAssetModal.tsx (100%) create mode 100644 src/pages/Earn/Lockdrop/index.tsx rename src/pages/{Pool => Earn/Pools}/Create/InputGroup.tsx (100%) rename src/pages/{Pool => Earn/Pools}/Create/index.tsx (99%) rename src/pages/{Pool => Earn/Pools}/PoolItem.tsx (99%) rename src/pages/{Pool => Earn/Pools}/PoolList.tsx (100%) rename src/pages/{Pool => Earn/Pools}/Provide/InputGroup.tsx (100%) rename src/pages/{Pool => Earn/Pools}/Provide/index.tsx (99%) rename src/pages/{Pool => Earn/Pools}/Provide/useSimulate.ts (100%) rename src/pages/{Pool => Earn/Pools}/Select.tsx (100%) rename src/pages/{Pool => Earn/Pools}/Withdraw/InputGroup.tsx (100%) rename src/pages/{Pool => Earn/Pools}/Withdraw/index.tsx (99%) rename src/pages/{Pool => Earn/Pools}/Withdraw/useSimulate.ts (100%) rename src/pages/{Pool => Earn/Pools}/index.tsx (80%) create mode 100644 src/pages/Earn/index.tsx diff --git a/src/components/NavBar/index.tsx b/src/components/NavBar/index.tsx index 8c08126c..53248cb3 100644 --- a/src/components/NavBar/index.tsx +++ b/src/components/NavBar/index.tsx @@ -1,11 +1,6 @@ import { css } from "@emotion/react"; import styled from "@emotion/styled"; -import { NavLink, NavLinkProps } from "react-router-dom"; - -interface NavBarProps { - items: (NavLinkProps & { key?: React.Key; disabled?: boolean })[]; - flex?: boolean; -} +import { NavLink } from "react-router-dom"; const Wrapper = styled.div` width: 100%; @@ -28,13 +23,14 @@ const NavItem = styled(NavLink, { opacity: 0.5; text-align: center; padding: 7px 0; + position: relative; ${({ flex }) => flex && css` flex: 1; `} - &.active:not(.disabled) { + &.active:not(.disabled), &:hover:not(.disabled) { opacity: 1; } @@ -43,6 +39,14 @@ const NavItem = styled(NavLink, { } `; +interface NavBarProps { + items: (React.ComponentProps & { + key?: React.Key; + disabled?: boolean; + })[]; + flex?: boolean; +} + function NavBar({ items, flex = true }: NavBarProps) { return ( diff --git a/src/components/Switch/index.tsx b/src/components/Switch/index.tsx new file mode 100644 index 00000000..6643090e --- /dev/null +++ b/src/components/Switch/index.tsx @@ -0,0 +1,48 @@ +import styled from "@emotion/styled"; + +const Wrapper = styled.label` + display: inline-block; + position: relative; + width: 48px; + height: 24px; + border: 5px solid ${({ theme }) => theme.colors.primary}; + border-radius: 30px; + background-color: ${({ theme }) => theme.colors.primary}; + box-sizing: content-box; + + cursor: pointer; + + & > input { + width: 0%; + height: 0%; + opacity: 0; + position: absolute; + z-index: -1; + } +`; + +const Handle = styled.div` + position: relative; + width: 50%; + height: 0; + padding-bottom: 50%; + background-color: ${({ theme }) => theme.colors.text.background}; + border-radius: 50%; + transition: left 0.125s cubic-bezier(1, 0, 0, 1); + left: 0; + + input:checked ~ & { + left: 50%; + } +`; + +function Switch(props: React.InputHTMLAttributes) { + return ( + + + + + ); +} + +export default Switch; diff --git a/src/layout/Main/Header.tsx b/src/layout/Main/Header.tsx index b4655e49..11ba9402 100644 --- a/src/layout/Main/Header.tsx +++ b/src/layout/Main/Header.tsx @@ -155,8 +155,18 @@ const navLinks = [ label: "Trade", }, { - path: "/pool", - label: "Pool", + path: "/earn", + label: "Earn", + children: [ + { + path: "/earn/pools", + label: "Pools", + }, + { + path: "/earn/lockdrop", + label: "Lock&Drop", + }, + ], }, ]; @@ -304,6 +314,16 @@ function Header() { key: item.path, to: item.path, disabled: item.disabled, + css: css` + .sub-menu { + display: none; + } + &:hover { + .sub-menu { + display: block; + } + } + `, children: item.disabled ? ( @@ -313,6 +333,63 @@ function Header() { ) : ( {item.label} + {item.children && ( +
+ + {item.children.map((child) => ( + + + {child.label} + + + ))} + +
+ )}
), }))} diff --git a/src/layout/Main/index.tsx b/src/layout/Main/index.tsx index fa9b2176..7f775991 100644 --- a/src/layout/Main/index.tsx +++ b/src/layout/Main/index.tsx @@ -76,9 +76,9 @@ const navLinks = [ label: "Trade", }, { - path: "/pool", + path: "/earn", icon: iconPool, - label: "Pool", + label: "Earn", }, ]; diff --git a/src/pages/Pool/PoolButton.tsx b/src/pages/Earn/AssetFormButton.tsx similarity index 71% rename from src/pages/Pool/PoolButton.tsx rename to src/pages/Earn/AssetFormButton.tsx index 7657c2b9..62836d15 100644 --- a/src/pages/Pool/PoolButton.tsx +++ b/src/pages/Earn/AssetFormButton.tsx @@ -1,11 +1,11 @@ -import { css, PropsOf } from "@emotion/react"; +import { PropsOf, css } from "@emotion/react"; import Button from "components/Button"; import Panel from "components/Panel"; import { MOBILE_SCREEN_CLASS } from "constants/layout"; -type PoolButtonProps = Omit, "size" | "block">; +type AssetFormButtonProps = Omit, "size" | "block">; -function PoolButton(props: PoolButtonProps) { +function AssetFormButton(props: AssetFormButtonProps) { return ( ); } -export default PoolButton; + +export default AssetFormButton; diff --git a/src/pages/Pool/PoolForm.tsx b/src/pages/Earn/AssetSelector.tsx similarity index 85% rename from src/pages/Pool/PoolForm.tsx rename to src/pages/Earn/AssetSelector.tsx index bc831130..a8d72988 100644 --- a/src/pages/Pool/PoolForm.tsx +++ b/src/pages/Earn/AssetSelector.tsx @@ -1,6 +1,5 @@ import { useMemo, useCallback } from "react"; import { Row, Col, useScreenClass } from "react-grid-system"; -import { Link } from "react-router-dom"; import styled from "@emotion/styled"; import { useTheme, css } from "@emotion/react"; import Modal from "components/Modal"; @@ -10,12 +9,7 @@ import { MOBILE_SCREEN_CLASS, TABLET_SCREEN_CLASS } from "constants/layout"; import useAssets from "hooks/useAssets"; import useHashModal from "hooks/useHashModal"; import usePairs from "hooks/usePairs"; -import { - formatNumber, - formatDecimals, - amountToValue, - convertIbcTokenAddressForPath, -} from "utils"; +import { formatNumber, formatDecimals, amountToValue } from "utils"; import iconPlus from "assets/icons/icon-plus.svg"; import iconDropdown from "assets/icons/icon-dropdown-arrow.svg"; import iconDefaultAsset from "assets/icons/icon-default-token.svg"; @@ -26,8 +20,16 @@ import { useAtom } from "jotai"; import useNetwork from "hooks/useNetwork"; import { customAssetsAtom } from "stores/assets"; import useBalance from "hooks/useBalance"; -import PoolButton from "./PoolButton"; import ImportAssetModal from "./ImportAssetModal"; +import AssetFormButton from "./AssetFormButton"; + +type AssetFormAddress = string | undefined; + +interface AssetFormProps { + addresses?: [AssetFormAddress, AssetFormAddress]; + onChange?: (addresses: [AssetFormAddress, AssetFormAddress]) => void; + extra?: React.ReactNode; +} const Wrapper = styled.div` width: 100%; @@ -47,14 +49,11 @@ const Wrapper = styled.div` } `; -type PoolFormAddress = string | undefined; - -interface PoolFormProps { - addresses?: [PoolFormAddress, PoolFormAddress]; - onChange?: (addresses: [PoolFormAddress, PoolFormAddress]) => void; -} - -function PoolForm({ addresses, onChange: handleChange }: PoolFormProps) { +function AssetSelector({ + addresses, + onChange: handleChange, + extra, +}: AssetFormProps) { const theme = useTheme(); const screenClass = useScreenClass(); @@ -124,7 +123,7 @@ function PoolForm({ addresses, onChange: handleChange }: PoolFormProps) { }, ].map(({ key, asset, modal, balance }) => ( - modal.open()}> + modal.open()}> - + ))} @@ -239,35 +238,7 @@ function PoolForm({ addresses, onChange: handleChange }: PoolFormProps) { } `} > - {!selectedAddress1 || !selectedAddress2 ? ( - - Select tokens first - - ) : undefined} - {selectedAddress1 && selectedAddress2 && pair ? ( - - - Add liquidity - - - ) : undefined} - {selectedAddress1 && selectedAddress2 && !pair ? ( - convertIbcTokenAddressForPath(a)) - .join("/")}`} - css={css` - text-decoration: none; - `} - > - Create a new pool - - ) : undefined} + {extra}
(); + const [selectedTabIndex, setSelectedTabIndex] = useState(0); + const [isLiveOnly, setIsLiveOnly] = useState(true); + + return ( + <> + + + +
+ setAddresses(newAddresses)} + /> +
+ + + + +
+ setSelectedTabIndex(index)} + /> +
+ + + + + + Live only + + + + { + setIsLiveOnly(event.currentTarget.checked); + }} + /> + + + +
+
+
+ + ); +} + +export default LockdropPage; diff --git a/src/pages/Pool/Create/InputGroup.tsx b/src/pages/Earn/Pools/Create/InputGroup.tsx similarity index 100% rename from src/pages/Pool/Create/InputGroup.tsx rename to src/pages/Earn/Pools/Create/InputGroup.tsx diff --git a/src/pages/Pool/Create/index.tsx b/src/pages/Earn/Pools/Create/index.tsx similarity index 99% rename from src/pages/Pool/Create/index.tsx rename to src/pages/Earn/Pools/Create/index.tsx index 75bcdc8e..df58e58c 100644 --- a/src/pages/Pool/Create/index.tsx +++ b/src/pages/Earn/Pools/Create/index.tsx @@ -36,7 +36,7 @@ import { XPLA_ADDRESS, XPLA_SYMBOL } from "constants/network"; import { generateCreatePoolMsg } from "utils/dezswap"; import { NetworkName } from "types/common"; import { useConnectedWallet } from "@xpla/wallet-provider"; -import InputGroup from "pages/Pool/Provide/InputGroup"; +import InputGroup from "pages/Earn/Pools/Provide/InputGroup"; import IconButton from "components/IconButton"; import iconLink from "assets/icons/icon-link.svg"; import useRequestPost from "hooks/useRequestPost"; diff --git a/src/pages/Pool/PoolItem.tsx b/src/pages/Earn/Pools/PoolItem.tsx similarity index 99% rename from src/pages/Pool/PoolItem.tsx rename to src/pages/Earn/Pools/PoolItem.tsx index 93d5937c..ab5e8bcb 100644 --- a/src/pages/Pool/PoolItem.tsx +++ b/src/pages/Earn/Pools/PoolItem.tsx @@ -26,7 +26,7 @@ import { MOBILE_SCREEN_CLASS, TABLET_SCREEN_CLASS } from "constants/layout"; import Tooltip from "components/Tooltip"; import { Pool } from "types/api"; import usePairs from "hooks/usePairs"; -import Expand from "./Expand"; +import Expand from "../Expand"; const SimplePieChart = styled.div<{ data: number[] }>` width: 100%; diff --git a/src/pages/Pool/PoolList.tsx b/src/pages/Earn/Pools/PoolList.tsx similarity index 100% rename from src/pages/Pool/PoolList.tsx rename to src/pages/Earn/Pools/PoolList.tsx diff --git a/src/pages/Pool/Provide/InputGroup.tsx b/src/pages/Earn/Pools/Provide/InputGroup.tsx similarity index 100% rename from src/pages/Pool/Provide/InputGroup.tsx rename to src/pages/Earn/Pools/Provide/InputGroup.tsx diff --git a/src/pages/Pool/Provide/index.tsx b/src/pages/Earn/Pools/Provide/index.tsx similarity index 99% rename from src/pages/Pool/Provide/index.tsx rename to src/pages/Earn/Pools/Provide/index.tsx index c3e75e5b..2e23b52d 100644 --- a/src/pages/Pool/Provide/index.tsx +++ b/src/pages/Earn/Pools/Provide/index.tsx @@ -16,7 +16,7 @@ import iconProvide from "assets/icons/icon-provide.svg"; import Expand from "components/Expanded"; import { MOBILE_SCREEN_CLASS } from "constants/layout"; import Button from "components/Button"; -import useSimulate from "pages/Pool/Provide/useSimulate"; +import useSimulate from "pages/Earn/Pools/Provide/useSimulate"; import { amountToValue, cutDecimal, @@ -37,7 +37,7 @@ import { generateAddLiquidityMsg } from "utils/dezswap"; import { NetworkName } from "types/common"; import { useConnectedWallet } from "@xpla/wallet-provider"; import useTxDeadlineMinutes from "hooks/useTxDeadlineMinutes"; -import InputGroup from "pages/Pool/Provide/InputGroup"; +import InputGroup from "pages/Earn/Pools/Provide/InputGroup"; import IconButton from "components/IconButton"; import iconLink from "assets/icons/icon-link.svg"; import useRequestPost from "hooks/useRequestPost"; diff --git a/src/pages/Pool/Provide/useSimulate.ts b/src/pages/Earn/Pools/Provide/useSimulate.ts similarity index 100% rename from src/pages/Pool/Provide/useSimulate.ts rename to src/pages/Earn/Pools/Provide/useSimulate.ts diff --git a/src/pages/Pool/Select.tsx b/src/pages/Earn/Pools/Select.tsx similarity index 100% rename from src/pages/Pool/Select.tsx rename to src/pages/Earn/Pools/Select.tsx diff --git a/src/pages/Pool/Withdraw/InputGroup.tsx b/src/pages/Earn/Pools/Withdraw/InputGroup.tsx similarity index 100% rename from src/pages/Pool/Withdraw/InputGroup.tsx rename to src/pages/Earn/Pools/Withdraw/InputGroup.tsx diff --git a/src/pages/Pool/Withdraw/index.tsx b/src/pages/Earn/Pools/Withdraw/index.tsx similarity index 99% rename from src/pages/Pool/Withdraw/index.tsx rename to src/pages/Earn/Pools/Withdraw/index.tsx index 792405dc..4043a501 100644 --- a/src/pages/Pool/Withdraw/index.tsx +++ b/src/pages/Earn/Pools/Withdraw/index.tsx @@ -6,7 +6,7 @@ import { MOBILE_DISPLAY_NUMBER_CNT, MOBILE_SCREEN_CLASS, } from "constants/layout"; -import InputGroup from "pages/Pool/Withdraw/InputGroup"; +import InputGroup from "pages/Earn/Pools/Withdraw/InputGroup"; import { amountToValue, cutDecimal, @@ -24,7 +24,7 @@ import iconLink from "assets/icons/icon-link.svg"; import Button from "components/Button"; import Modal from "components/Modal"; import { useNavigate, useParams } from "react-router-dom"; -import useSimulate from "pages/Pool/Withdraw/useSimulate"; +import useSimulate from "pages/Earn/Pools/Withdraw/useSimulate"; import usePairs from "hooks/usePairs"; import useNetwork from "hooks/useNetwork"; import { useForm } from "react-hook-form"; diff --git a/src/pages/Pool/Withdraw/useSimulate.ts b/src/pages/Earn/Pools/Withdraw/useSimulate.ts similarity index 100% rename from src/pages/Pool/Withdraw/useSimulate.ts rename to src/pages/Earn/Pools/Withdraw/useSimulate.ts diff --git a/src/pages/Pool/index.tsx b/src/pages/Earn/Pools/index.tsx similarity index 80% rename from src/pages/Pool/index.tsx rename to src/pages/Earn/Pools/index.tsx index 33b583c3..8af9d4b2 100644 --- a/src/pages/Pool/index.tsx +++ b/src/pages/Earn/Pools/index.tsx @@ -1,28 +1,26 @@ -import Hr from "components/Hr"; import Panel from "components/Panel"; import Typography from "components/Typography"; import { Col, Container, Row, useScreenClass } from "react-grid-system"; -import { Outlet } from "react-router-dom"; +import { Link, Outlet } from "react-router-dom"; import { css, Global } from "@emotion/react"; import Pagination from "components/Pagination"; import TabButton from "components/TabButton"; import { MOBILE_SCREEN_CLASS, TABLET_SCREEN_CLASS } from "constants/layout"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import usePairs from "hooks/usePairs"; import useAPI from "hooks/useAPI"; import { PairExtended } from "types/common"; import usePairBookmark from "hooks/usePairBookmark"; import { Numeric } from "@xpla/xpla.js"; -import IconButton from "components/IconButton"; -import iconReload from "assets/icons/icon-reload.svg"; -import iconReloadHover from "assets/icons/icon-reload-hover.svg"; import ScrollToTop from "components/ScrollToTop"; import usePools from "hooks/usePools"; import { useQueries } from "@tanstack/react-query"; +import { convertIbcTokenAddressForPath } from "utils"; import PoolList from "./PoolList"; import Select from "./Select"; -import PoolForm from "./PoolForm"; +import AssetSelector from "../AssetSelector"; +import AssetFormButton from "../AssetFormButton"; const timeBaseOptions = ["24h", "7d", "1m"]; const tabs = [ @@ -89,10 +87,6 @@ function PoolPage() { }); }, [pools, selectedPair, selectedTabIndex, balances, bookmarks]); - const handleReloadClick = useCallback(() => { - setAddresses(undefined); - }, []); - useEffect(() => { const [address1, address2] = addresses || []; if (address1 && address2) { @@ -121,46 +115,49 @@ function PoolPage() { /> - - - - Pool - - - - - - -
- setAddresses(value)} + extra={ + <> + {!addresses?.[0] || !addresses?.[1] ? ( + + Select tokens first + + ) : undefined} + {addresses?.[0] && addresses?.[1] && selectedPair ? ( + + + Add liquidity + + + ) : undefined} + {addresses?.[0] && addresses?.[1] && !selectedPair ? ( + convertIbcTokenAddressForPath(a)) + .join("/")}`} + css={css` + text-decoration: none; + `} + > + + Create a new pool + + + ) : undefined} + + } />
diff --git a/src/pages/Earn/index.tsx b/src/pages/Earn/index.tsx new file mode 100644 index 00000000..d1c7287e --- /dev/null +++ b/src/pages/Earn/index.tsx @@ -0,0 +1,86 @@ +import { css } from "@emotion/react"; +import Hr from "components/Hr"; +import IconButton from "components/IconButton"; +import Typography from "components/Typography"; +import { MOBILE_SCREEN_CLASS } from "constants/layout"; +import { Col, Container, Row, useScreenClass } from "react-grid-system"; + +import iconReload from "assets/icons/icon-reload.svg"; +import iconReloadHover from "assets/icons/icon-reload-hover.svg"; +import { NavLink as navLink, Outlet } from "react-router-dom"; +import styled from "@emotion/styled"; + +const NavBar = styled.div` + width: 100%; + height: auto; + position: relative; + display: flex; + justify-content: flex-start; + align-items: center; + gap: 30px; +`; + +const NavLink = styled(navLink)` + display: inline-block; + padding: 19px 0; + text-decoration: none; + border-bottom: 5px solid transparent; + opacity: 0.5; + + &.active { + opacity: 1; + border-bottom-color: ${({ theme }) => theme.colors.primary}; + } + + .${MOBILE_SCREEN_CLASS} & { + padding: 10px 0 10px 0; + } +`; + +function EarnPage() { + const screenClass = useScreenClass(); + return ( + <> + + + + + Pools + + + + + Lock&Drop + + + { + window.location.reload(); + }} + /> + +
+
+ + + ); +} + +export default EarnPage; diff --git a/src/routes.tsx b/src/routes.tsx index 22bdbd33..1f23288f 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -1,17 +1,30 @@ -import { Navigate, RouteProps } from "react-router-dom"; +import { Navigate, Outlet, RouteProps, useLocation } from "react-router-dom"; import PlaygroundPage from "pages/Playground"; import SwapPage from "pages/Trade/Swap"; -import ProvidePage from "pages/Pool/Provide"; -import WithdrawPage from "pages/Pool/Withdraw"; -import PoolPage from "pages/Pool"; +import ProvidePage from "pages/Earn/Pools/Provide"; +import WithdrawPage from "pages/Earn/Pools/Withdraw"; +import PoolPage from "pages/Earn/Pools"; import TradePage from "pages/Trade"; -import CreatePage from "pages/Pool/Create"; +import CreatePage from "pages/Earn/Pools/Create"; import Error404 from "pages/Error404"; +import EarnPage from "pages/Earn"; +import LockdropPage from "pages/Earn/Lockdrop"; export interface RouteObject extends Omit { children?: RouteObject[]; } +// For legacy links +function ReplaceToEarn() { + const location = useLocation(); + return ( + + ); +} + const routes: RouteObject[] = [ ...(import.meta.env?.DEV ? [{ path: "playground", element: }] @@ -27,14 +40,42 @@ const routes: RouteObject[] = [ }, { path: "pool", - element: , + element: , children: [ - { path: "create/:asset1Address/:asset2Address", element: }, - { path: "add-liquidity/:pairAddress", element: }, - { path: "withdraw/:pairAddress", element: }, + { index: true, element: }, + { + path: "create/:asset1Address/:asset2Address", + element: , + }, + { path: "add-liquidity/:pairAddress", element: }, + { path: "withdraw/:pairAddress", element: }, { path: "*", element: }, ], }, + { + path: "earn", + element: , + children: [ + { + path: "pools", + element: , + children: [ + { + path: "create/:asset1Address/:asset2Address", + element: , + }, + { path: "add-liquidity/:pairAddress", element: }, + { path: "withdraw/:pairAddress", element: }, + { path: "*", element: }, + ], + }, + { + path: "lockdrop", + element: , + }, + { index: true, element: }, + ], + }, { index: true, element: }, { path: "*", element: }, ]; From f63179a147e70c04bd81dc5114ff0c42af765b7c Mon Sep 17 00:00:00 2001 From: maro Date: Mon, 14 Aug 2023 02:48:14 +0900 Subject: [PATCH 06/54] feat: lock&drop --- package.json | 1 + src/assets/icons/icon-alarm.svg | 3 + src/assets/icons/icon-badge.svg | 9 + src/assets/icons/icon-slider-handle.svg | 3 + src/assets/icons/icon-sort-asc.svg | 4 + src/assets/icons/icon-sort-default.svg | 3 + src/assets/icons/icon-sort-desc.svg | 4 + src/components/Button/index.tsx | 3 + src/components/Message/index.tsx | 9 +- src/components/ProgressBar/index.tsx | 67 +- src/components/Slider/index.tsx | 145 ++++ src/constants/dezswap.ts | 3 + src/hooks/useAPI.ts | 70 ++ src/hooks/useLockdropBookmark.ts | 36 + src/hooks/useLockdropEvents.ts | 58 ++ src/pages/Earn/Lockdrop/LockdropEventItem.tsx | 759 ++++++++++++++++++ src/pages/Earn/Lockdrop/LockdropEventList.tsx | 37 + src/pages/Earn/Lockdrop/Stake/InputGroup.tsx | 151 ++++ src/pages/Earn/Lockdrop/Stake/index.tsx | 458 +++++++++++ .../Earn/Lockdrop/Stake/useEstimatedReward.ts | 44 + src/pages/Earn/Lockdrop/index.tsx | 277 ++++++- src/routes.tsx | 2 + src/stores/lockdrops.ts | 6 + src/styles/GlobalStyles/index.tsx | 5 + src/types/lockdrop.d.ts | 39 + src/utils/dezswap.ts | 26 + src/utils/index.ts | 24 + yarn.lock | 31 +- 28 files changed, 2256 insertions(+), 21 deletions(-) create mode 100644 src/assets/icons/icon-alarm.svg create mode 100644 src/assets/icons/icon-badge.svg create mode 100644 src/assets/icons/icon-slider-handle.svg create mode 100644 src/assets/icons/icon-sort-asc.svg create mode 100644 src/assets/icons/icon-sort-default.svg create mode 100644 src/assets/icons/icon-sort-desc.svg create mode 100644 src/components/Slider/index.tsx create mode 100644 src/hooks/useLockdropBookmark.ts create mode 100644 src/hooks/useLockdropEvents.ts create mode 100644 src/pages/Earn/Lockdrop/LockdropEventItem.tsx create mode 100644 src/pages/Earn/Lockdrop/LockdropEventList.tsx create mode 100644 src/pages/Earn/Lockdrop/Stake/InputGroup.tsx create mode 100644 src/pages/Earn/Lockdrop/Stake/index.tsx create mode 100644 src/pages/Earn/Lockdrop/Stake/useEstimatedReward.ts create mode 100644 src/stores/lockdrops.ts create mode 100644 src/types/lockdrop.d.ts diff --git a/package.json b/package.json index 73653cb9..7de9b401 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "decimal.js": "^10.2.1", "emotion-reset": "^3.0.1", "jotai": "^2.1.0", + "rc-slider": "^10.2.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-grid-system": "^8.1.6", diff --git a/src/assets/icons/icon-alarm.svg b/src/assets/icons/icon-alarm.svg new file mode 100644 index 00000000..61d86c0e --- /dev/null +++ b/src/assets/icons/icon-alarm.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/icon-badge.svg b/src/assets/icons/icon-badge.svg new file mode 100644 index 00000000..03cc5799 --- /dev/null +++ b/src/assets/icons/icon-badge.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/icons/icon-slider-handle.svg b/src/assets/icons/icon-slider-handle.svg new file mode 100644 index 00000000..6dfa2b97 --- /dev/null +++ b/src/assets/icons/icon-slider-handle.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/icon-sort-asc.svg b/src/assets/icons/icon-sort-asc.svg new file mode 100644 index 00000000..07761898 --- /dev/null +++ b/src/assets/icons/icon-sort-asc.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/icon-sort-default.svg b/src/assets/icons/icon-sort-default.svg new file mode 100644 index 00000000..655b0313 --- /dev/null +++ b/src/assets/icons/icon-sort-default.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/icon-sort-desc.svg b/src/assets/icons/icon-sort-desc.svg new file mode 100644 index 00000000..3d607979 --- /dev/null +++ b/src/assets/icons/icon-sort-desc.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 93768f07..841b4664 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -67,6 +67,9 @@ const Button = styled.button` &.active { background-color: ${theme.colors.selected}; } + &:disabled { + opacity: 0.5; + } `; case "link": return css` diff --git a/src/components/Message/index.tsx b/src/components/Message/index.tsx index 7074c50d..0fa03387 100644 --- a/src/components/Message/index.tsx +++ b/src/components/Message/index.tsx @@ -4,6 +4,7 @@ import iconCaution from "assets/icons/icon-caution.svg"; type MessageProps = React.PropsWithChildren<{ variant?: "guide" | "error" | "warning"; // TODO: "default" | "success" | "error" | "warning"; + showIcon?: boolean; }>; const Wrapper = styled.div>` @@ -77,11 +78,15 @@ const getIconSrc = (variant: MessageProps["variant"]) => { } }; -function Message({ variant = "error", children }: MessageProps) { +function Message({ + variant = "error", + showIcon = true, + children, +}: MessageProps) { const iconSrc = getIconSrc(variant); return ( - {iconSrc && } + {showIcon && iconSrc && } {children} ); diff --git a/src/components/ProgressBar/index.tsx b/src/components/ProgressBar/index.tsx index 3b441d32..e0a1163e 100644 --- a/src/components/ProgressBar/index.tsx +++ b/src/components/ProgressBar/index.tsx @@ -9,6 +9,9 @@ interface ProgressBarProps { max?: number; label?: [string, string]; disabled?: boolean; + variant?: "default" | "gradient"; + size?: "default" | "small"; + barStyle?: "default" | "rounded"; } const Wrapper = styled.div` @@ -30,22 +33,58 @@ const Row = styled.div` align-items: center; `; -const Bar = styled.div<{ disabled?: ProgressBarProps["disabled"] }>` +const Bar = styled.div` width: 100%; flex: 1; - height: 19px; - background-color: ${({ theme }) => theme.colors.secondary}; - border-radius: 12px; position: relative; overflow: hidden; + ${({ size }) => { + if (size === "small") { + return css` + height: 8px; + border-radius: 30px; + `; + } + return css` + height: 19px; + border-radius: 12px; + `; + }} + + ${({ variant, theme }) => { + if (variant === "gradient") { + return css` + background-color: #d9d9d9; + & > div { + background: ${theme.colors.gradient}; + } + `; + } + return css` + background-color: ${theme.colors.secondary}; + + & > div { + background-color: ${theme.colors.tertiary}; + } + `; + }} + & > div { width: 100%; height: 100%; - background-color: ${({ theme }) => theme.colors.tertiary}; - border-radius: 12px; - border-top-right-radius: 0; - border-bottom-right-radius: 0; + ${({ barStyle }) => { + if (barStyle === "rounded") { + return css` + border-radius: 30px; + `; + } + return css` + border-radius: 12px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + `; + }} position: absolute; top: 0; left: 0; @@ -61,15 +100,11 @@ const Bar = styled.div<{ disabled?: ProgressBarProps["disabled"] }>` `} `; -function ProgressBar({ - value, - min = 0, - max = 100, - label, - disabled, -}: ProgressBarProps) { +function ProgressBar(props: ProgressBarProps) { const theme = useTheme(); + const { value, min = 0, max = 100, label } = props; + const progress = useMemo(() => { const percent = ((value - min) / (max - min)) * 100; return percent > 100 ? 100 : percent; @@ -78,7 +113,7 @@ function ProgressBar({ return ( - +
ReactNode; +} + +const Wrapper = styled.div` + position: relative; + padding: 0 7px; + + .rc-slider { + height: 28px; + padding: 9px 0; + &-disabled { + background-color: unset; + } + + &-handle { + border: none !important; + background-color: transparent !important; + box-shadow: none !important; + width: 14px; + height: 28px; + margin-top: -9px; + border-radius: 2px; + opacity: 1; + transform: translateX(0); + z-index: 2; + background-image: url(${iconHandle}); + background-repeat: no-repeat; + background-position: 50% 50%; + background-size: contain; + + &-dragging { + border-color: ${({ theme }) => theme.colors.primary} !important; + box-shadow: none !important; + } + } + &-step { + height: 10px; + } + &-track { + background: ${({ theme }) => theme.colors.gradient}; + height: 10px; + } + &-rail { + width: calc(100% + 7px); + background-color: ${({ theme }) => theme.colors.text.placeholder}; + height: 10px; + } + &-dot { + display: none; + } + + /* To prevent unexpected bubbling */ + &::after { + content: ""; + width: 1000vw; + height: 1000vh; + position: fixed; + left: -50vw; + top: -50vh; + background-color: transparent; + z-index: 50000; + display: none; + transition: display 0.2s step-end; + } + + &:has(.rc-slider-handle-dragging)::after { + display: block; + } + } + + &::before { + content: ""; + width: 12px; + height: 28px; + position: absolute; + left: 0; + top: 0; + background-color: ${({ theme }) => theme.colors.disabled}; + z-index: 1; + } +`; + +function SliderHandle({ + origin, + props, + transformValue = (value) => value, +}: { + origin: Parameters["handleRender"]>[0]; + props: Parameters["handleRender"]>[1]; + transformValue?: SliderProps["transformValue"]; +}): ReactElement { + return ( + <> + {origin} + + {transformValue(props.value)} + + + ); +} + +function Slider({ showValue, transformValue, ...rcSliderProps }: SliderProps) { + return ( + + { + return ( + + ); + } + : undefined + } + {...rcSliderProps} + /> + + ); +} + +export default Slider; diff --git a/src/constants/dezswap.ts b/src/constants/dezswap.ts index 523ed3ec..3d62f570 100644 --- a/src/constants/dezswap.ts +++ b/src/constants/dezswap.ts @@ -4,15 +4,18 @@ export const contractAddresses: { [K in NetworkName]?: { factory: string; router: string; + lockdrop: string; }; } = { mainnet: { factory: "xpla1j33xdql0h4kpgj2mhggy4vutw655u90z7nyj4afhxgj4v5urtadq44e3vd", router: "xpla1uv4dz7ngaqwymvxggrjp3rnz3gs33szwjsnrxqg0ylkykqf8r7ns9s3cg4", + lockdrop: "", }, testnet: { factory: "xpla1j4kgjl6h4rt96uddtzdxdu39h0mhn4vrtydufdrk4uxxnrpsnw2qug2yx2", router: "xpla1pr40depxf8w50y58swdyhc0s2yjptd2xtqgnyfvkz6k40ng53gqqnyftkm", + lockdrop: "xpla1470mu6u8f4vspuuh3qt9julgme3mq0ua8ktm37svjkapzyxu62hq4882n8", }, }; diff --git a/src/hooks/useAPI.ts b/src/hooks/useAPI.ts index 09f55372..1f140070 100644 --- a/src/hooks/useAPI.ts +++ b/src/hooks/useAPI.ts @@ -13,6 +13,11 @@ import useNetwork from "hooks/useNetwork"; import useLCDClient from "hooks/useLCDClient"; import { LatestBlock } from "types/common"; import api, { ApiVersion } from "api"; +import { + LockdropEstimatedReward, + LockdropEvents, + LockdropUserInfo, +} from "types/lockdrop"; interface TokenBalance { balance: string; @@ -136,6 +141,65 @@ const useAPI = (version: ApiVersion = "v1") => { [network.name, lcd], ); + const getLockdropEvents = useCallback( + async (startAfter = 0) => { + const contractAddress = contractAddresses[network.name]?.lockdrop; + if (!contractAddress) { + return undefined; + } + + const res = await lcd.wasm.contractQuery( + contractAddress, + { + events_by_end: { start_after: startAfter }, + }, + ); + return res; + }, + [lcd, network.name], + ); + + const getLockdropUserInfo = useCallback( + async (lockdropEventAddress: string) => { + if (!walletAddress || !lockdropEventAddress) { + return undefined; + } + const res = await lcd.wasm.contractQuery( + lockdropEventAddress, + { + user_info: { + addr: walletAddress, + }, + }, + ); + return res; + }, + [lcd.wasm, walletAddress], + ); + + const getEstimatedLockdropReward = useCallback( + async ( + lockdropEventAddress: string, + amount: number | string, + duration: number, + ) => { + if (!walletAddress || !lockdropEventAddress) { + return undefined; + } + const res = await lcd.wasm.contractQuery( + lockdropEventAddress, + { + estimate: { + amount: `${amount}`, + duration, + }, + }, + ); + return res; + }, + [lcd.wasm, walletAddress], + ); + return useMemo( () => ({ ...apiClient, @@ -147,6 +211,9 @@ const useAPI = (version: ApiVersion = "v1") => { getVerifiedIbcTokenInfos, getLatestBlockHeight, getDecimal, + getLockdropEvents, + getLockdropUserInfo, + getEstimatedLockdropReward, }), [ apiClient, @@ -158,6 +225,9 @@ const useAPI = (version: ApiVersion = "v1") => { getVerifiedIbcTokenInfos, getLatestBlockHeight, getDecimal, + getLockdropEvents, + getLockdropUserInfo, + getEstimatedLockdropReward, ], ); }; diff --git a/src/hooks/useLockdropBookmark.ts b/src/hooks/useLockdropBookmark.ts new file mode 100644 index 00000000..5b31a76f --- /dev/null +++ b/src/hooks/useLockdropBookmark.ts @@ -0,0 +1,36 @@ +import { useCallback, useMemo } from "react"; +import useNetwork from "hooks/useNetwork"; +import { lockdropBookmarksAtom } from "stores/lockdrops"; +import { useAtom } from "jotai"; + +const useLockdropBookmark = () => { + const network = useNetwork(); + const [bookmarkStore, setBookmarkStore] = useAtom(lockdropBookmarksAtom); + + const toggleBookmark = useCallback( + (address: string) => + setBookmarkStore((current) => { + if (current[network.name]?.includes(address)) { + return { + ...current, + [network.name]: current[network.name]?.filter((v) => v !== address), + }; + } + return { + ...current, + [network.name]: [...(current[network.name] || []), address], + }; + }), + [network.name, setBookmarkStore], + ); + + return useMemo( + () => ({ + bookmarks: bookmarkStore[network.name], + toggleBookmark, + }), + [network.name, bookmarkStore, toggleBookmark], + ); +}; + +export default useLockdropBookmark; diff --git a/src/hooks/useLockdropEvents.ts b/src/hooks/useLockdropEvents.ts new file mode 100644 index 00000000..abe38e5c --- /dev/null +++ b/src/hooks/useLockdropEvents.ts @@ -0,0 +1,58 @@ +import { useQuery } from "@tanstack/react-query"; +import { useCallback, useMemo } from "react"; +import useAPI from "./useAPI"; +import useNetwork from "./useNetwork"; + +const useLockdropEvents = () => { + const network = useNetwork(); + const api = useAPI(); + const { data: lockdropEvents, isLoading } = useQuery({ + queryKey: ["lockdropEvents", network?.chainID], + queryFn: async () => { + const res = await api.getLockdropEvents(); + return res?.events || []; + }, + }); + + const getLockdropEvent = useCallback( + (address: string) => { + return lockdropEvents?.find((event) => event.addr === address); + }, + [lockdropEvents], + ); + + const getLockdropEventByRewardToken = useCallback( + (address: string) => { + return lockdropEvents?.find( + (event) => event.reward_token_addr === address, + ); + }, + [lockdropEvents], + ); + + const getLockdropEventByLPToken = useCallback( + (address: string) => { + return lockdropEvents?.find((event) => event.lp_token_addr === address); + }, + [lockdropEvents], + ); + + return useMemo( + () => ({ + lockdropEvents: lockdropEvents || [], + isLoading, + getLockdropEvent, + getLockdropEventByRewardToken, + getLockdropEventByLPToken, + }), + [ + getLockdropEvent, + getLockdropEventByLPToken, + getLockdropEventByRewardToken, + isLoading, + lockdropEvents, + ], + ); +}; + +export default useLockdropEvents; diff --git a/src/pages/Earn/Lockdrop/LockdropEventItem.tsx b/src/pages/Earn/Lockdrop/LockdropEventItem.tsx new file mode 100644 index 00000000..6d799ee4 --- /dev/null +++ b/src/pages/Earn/Lockdrop/LockdropEventItem.tsx @@ -0,0 +1,759 @@ +import Box from "components/Box"; +import styled from "@emotion/styled"; +import { MOBILE_SCREEN_CLASS, TABLET_SCREEN_CLASS } from "constants/layout"; +import { Col, Hidden, Row, Visible, useScreenClass } from "react-grid-system"; +import Button from "components/Button"; +import { css } from "@emotion/react"; +import Tooltip from "components/Tooltip"; +import Typography from "components/Typography"; +import { LP_DECIMALS } from "constants/dezswap"; +import { + formatNumber, + formatDecimals, + amountToValue, + formatDateTime, + getRemainDays, + getAddressLink, +} from "utils"; +import iconDefaultToken from "assets/icons/icon-default-token.svg"; +import iconOutlink from "assets/icons/icon-link.svg"; +import iconVerified from "assets/icons/icon-verified.svg"; +import iconAlarm from "assets/icons/icon-alarm.svg"; + +import iconBookmark from "assets/icons/icon-bookmark-default.svg"; +import iconBookmarkSelected from "assets/icons/icon-bookmark-selected.svg"; +import useAssets from "hooks/useAssets"; +import useNetwork from "hooks/useNetwork"; +import usePairs from "hooks/usePairs"; +import { useMemo } from "react"; +import ProgressBar from "components/ProgressBar"; +import { Link } from "react-router-dom"; +import { LockdropEvent, LockdropUserInfo } from "types/lockdrop"; +import IconButton from "components/IconButton"; +import useBalance from "hooks/useBalance"; +import Hr from "components/Hr"; +import { Numeric } from "@xpla/xpla.js"; +import Expand from "../Expand"; + +const TableRow = styled(Box)` + display: inline-flex; + justify-content: flex-start; + align-items: center; + flex-wrap: nowrap; + padding: 20px; + background: none; + gap: 20px; + & > div { + width: 160px; + color: ${({ theme }) => theme.colors.primary}; + font-size: 16px; + font-weight: 500; + &:first-of-type { + width: 32px; + } + &:nth-of-type(2) { + width: 244px; + } + &:last-of-type { + width: 116px; + } + + & > div { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + .${MOBILE_SCREEN_CLASS} &, + .${TABLET_SCREEN_CLASS} & { + overflow: unset; + white-space: normal; + text-overflow: unset; + word-break: break-all; + } + } + } + + .${MOBILE_SCREEN_CLASS} &, + .${TABLET_SCREEN_CLASS} & { + flex-direction: column; + gap: 20px; + + & > div { + width: 100%; + &:first-of-type { + width: 100%; + } + } + } +`; + +const Label = styled(Typography)` + line-height: 1; + white-space: nowrap; + margin-bottom: 15px; + .${MOBILE_SCREEN_CLASS} &, + .${TABLET_SCREEN_CLASS} & { + margin-bottom: 6px; + } +`; + +Label.defaultProps = { + color: "primary", + weight: 900, +}; + +const AssetIcon = styled.div<{ src?: string }>` + width: 32px; + height: 32px; + position: relative; + display: inline-block; + &::after { + content: ""; + width: 100%; + height: 100%; + position: absolute; + left: 0; + top: 0; + background-color: ${({ theme }) => theme.colors.white}; + border-radius: 50%; + background-image: ${({ src }) => `url(${src || iconDefaultToken})`}; + background-size: contain; + background-repeat: no-repeat; + background-position: 50% 50%; + } +`; + +const BodyWrapper = styled.div` + width: 100%; + height: auto; + position: relative; + + display: flex; + flex-direction: row; + justify-content: space-between; + + gap: 40px; + + .${MOBILE_SCREEN_CLASS} &, + .${TABLET_SCREEN_CLASS} & { + display: block; + + & > div { + &:first-of-type { + margin-bottom: 16px; + } + } + } +`; + +const OutlinkList = styled.div` + display: inline-flex; + flex-direction: column; + justify-content: flex-start; + + width: auto; + height: auto; + position: relative; + + gap: 10px; +`; + +const OutlinkItem = styled.a` + display: inline-block; + line-height: 19px; + font-size: 14px; + font-weight: 500; + font-stretch: normal; + font-style: normal; + letter-spacing: normal; + text-align: left; + color: ${({ theme }) => theme.colors.text.primary}; + text-decoration: none; + &::after { + content: ""; + display: inline-block; + width: 16px; + height: 16px; + position: relative; + margin-left: 5px; + background-image: url(${iconOutlink}); + background-size: contain; + background-repeat: no-repeat; + background-position: 50% 50%; + line-height: 19px; + vertical-align: middle; + } +`; + +const Verified = styled.div` + display: inline-block; + border: 2px solid ${({ theme }) => theme.colors.primary}; + padding: 5px 10px; + border-radius: 30px; + + &::before { + content: ""; + display: inline-block; + width: 18px; + height: 18px; + position: relative; + margin-right: 2px; + background-image: url(${iconVerified}); + background-size: contain; + background-repeat: no-repeat; + background-position: 50% 50%; + line-height: 19px; + vertical-align: middle; + } + + &::after { + content: "Verified"; + font-size: 14px; + font-weight: 900; + line-height: 19px; + color: ${({ theme }) => theme.colors.primary}; + } +`; + +const LockdropUserInfoItem = styled(Box)` + padding: 16px; + background-color: ${({ theme }) => theme.colors.white}; + border: 1px solid ${({ theme }) => theme.colors.primary}; + + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: flex-start; + gap: 30px; + + & > div { + &, + & *:not([type="button"]) { + color: ${({ theme }) => theme.colors.primary}; + } + + &:first-of-type { + flex: 1; + } + &:last-of-type { + width: 150px; + display: flex; + flex-direction: column; + gap: 10px; + } + } + + .${MOBILE_SCREEN_CLASS} &, + .${TABLET_SCREEN_CLASS} & { + flex-direction: column; + gap: 20px; + + & > div { + &:first-of-type { + width: 100%; + } + &:last-of-type { + width: 100%; + } + } + } +`; + +function LockdropUserInfoTable({ + data, +}: { + data: { + label: string; + value: string; + }[]; +}) { + return ( + + {data.map(({ label, value }) => ( + + + {label} + + + {value} + + + ))} + + ); +} + +function LockdropEventItem({ + event: lockdropEvent, + isBookmarked, + onBookmarkToggle, + userInfo: lockdropUserInfo, +}: { + event: LockdropEvent; + userInfo?: LockdropUserInfo; + isBookmarked?: boolean; + onBookmarkToggle?: (isBookmarked: boolean, eventAddress: string) => void; +}) { + const screenClass = useScreenClass(); + const isSmallScreen = [MOBILE_SCREEN_CLASS, TABLET_SCREEN_CLASS].includes( + screenClass, + ); + + const { getAsset } = useAssets(); + const network = useNetwork(); + const { findPairByLpAddress } = usePairs(); + const pair = useMemo( + () => findPairByLpAddress(lockdropEvent.lp_token_addr), + [findPairByLpAddress, lockdropEvent], + ); + + const [asset1, asset2] = useMemo( + () => pair?.asset_addresses.map((address) => getAsset(address)) || [], + [getAsset, pair], + ); + const rewardAsset = useMemo( + () => getAsset(lockdropEvent.reward_token_addr), + [getAsset, lockdropEvent], + ); + const lpBalance = useBalance(lockdropEvent.lp_token_addr); + + const isStakable = useMemo(() => { + const startAt = new Date(lockdropEvent.start_at * 1000); + const endAt = new Date(lockdropEvent.end_at * 1000); + const now = new Date(); + + return now >= startAt && now <= endAt; + }, [lockdropEvent]); + + const isCancelable = useMemo(() => { + const cancelableUntil = new Date(lockdropEvent.cancelable_until * 1000); + const now = new Date(); + + return now <= cancelableUntil; + }, [lockdropEvent]); + + const isNeedAction = useMemo(() => { + return lockdropUserInfo?.lockup_infos.some((info) => + Numeric.parse(info.claimable).gt(0), + ); + }, [lockdropUserInfo]); + + const extra = useMemo( + () => + isStakable + ? [ + + + , + ] + : [], + [lockdropEvent, isStakable], + ); + + const bookmarkButton = useMemo( + () => ( + { + e.stopPropagation(); + if (onBookmarkToggle && lockdropEvent.addr) { + onBookmarkToggle(!isBookmarked, lockdropEvent.addr); + } + }} + /> + ), + [isBookmarked, lockdropEvent, onBookmarkToggle], + ); + + return ( + + +
+ {bookmarkButton} +
+
+
+ {isSmallScreen && } + + + + + + + + + + {asset1?.symbol}-{asset2?.symbol} + + + + + + {bookmarkButton} + + +
+
+ {isSmallScreen && } +
+ {formatNumber( + formatDecimals( + amountToValue(lockdropEvent.total_locked_lp, LP_DECIMALS) || + "", + 2, + ), + )} +  LP +
+
+
+ {isSmallScreen && } +
+ {formatNumber( + formatDecimals( + amountToValue( + lockdropEvent.total_reward, + rewardAsset?.decimals, + ) || "", + 2, + ), + )} +   + {rewardAsset?.symbol} +
+
+
+ {isSmallScreen && } +
+ {formatNumber( + formatDecimals( + amountToValue(lpBalance || 0, LP_DECIMALS) || "", + 2, + ), + )} +  LP +
+
+
+ {isSmallScreen && } +
+ {getRemainDays(lockdropEvent.end_at * 1000)} days + + + Lock begins at +
+ {formatDateTime(lockdropEvent.end_at * 1000)} +
+
+ + Cancellation is available +
+ until  + {formatDateTime(lockdropEvent.cancelable_until * 1000)} +
+ + } + arrow + > + +
+
+
+ {isSmallScreen && !!extra.length &&
{extra}
} + + } + extra={!isSmallScreen ? extra : undefined} + > + + + Project Site + + Pair Info + + + + +
+ {!lockdropUserInfo?.lockup_infos?.length ? ( + +
+ +
+ +
 
+
+
+ ) : ( + lockdropUserInfo.lockup_infos.map((lockupInfo) => { + const eventEndAt = new Date(lockdropEvent.end_at * 1000); + const unlockAt = new Date(lockupInfo.unlock_second * 1000); + const now = new Date(); + + const isClaimable = Numeric.parse(lockupInfo.claimable || 0).gt( + 0, + ); + const isUnstakable = now >= unlockAt; + const isLocking = now > eventEndAt && now < unlockAt; + + return ( + +
+ + + + Locking Period + + :  + + +
+
+ + {formatDateTime(lockdropEvent.end_at * 1000)}  + -  + {formatDateTime(lockupInfo.unlock_second * 1000)} ( + {lockupInfo.duration} weeks) + +
+ + + + Remaining + + :  + + +
+
+ + {getRemainDays(lockupInfo.unlock_second * 1000)} +  days + +
+ +
+
+ +
+
+ +
+
+ {isStakable && ( + + + + )} + {isCancelable && ( + + )} + {(isLocking || isUnstakable) && ( + + )} + {(isLocking || isUnstakable) && ( + + )} +
+
+ ); + }) + )} +
+
+
+ ); +} + +export default LockdropEventItem; diff --git a/src/pages/Earn/Lockdrop/LockdropEventList.tsx b/src/pages/Earn/Lockdrop/LockdropEventList.tsx new file mode 100644 index 00000000..10e9d215 --- /dev/null +++ b/src/pages/Earn/Lockdrop/LockdropEventList.tsx @@ -0,0 +1,37 @@ +import { LockdropEvent, LockdropUserInfo } from "types/lockdrop"; + +import { css } from "@emotion/react"; +import useLockdropBookmark from "hooks/useLockdropBookmark"; +import LockdropEventItem from "./LockdropEventItem"; + +function LockdropEventList({ + events: lockdropEvents, + userInfos: lockdropUserInfos, +}: { + events?: LockdropEvent[]; + userInfos?: (LockdropUserInfo | undefined)[]; +}) { + const { bookmarks, toggleBookmark } = useLockdropBookmark(); + + return ( +
+ {lockdropEvents?.map((lockdropEvent, index) => ( + toggleBookmark(lockdropEvent.addr)} + userInfo={lockdropUserInfos?.[index]} + /> + ))} +
+ ); +} + +export default LockdropEventList; diff --git a/src/pages/Earn/Lockdrop/Stake/InputGroup.tsx b/src/pages/Earn/Lockdrop/Stake/InputGroup.tsx new file mode 100644 index 00000000..fccf035b --- /dev/null +++ b/src/pages/Earn/Lockdrop/Stake/InputGroup.tsx @@ -0,0 +1,151 @@ +import { forwardRef } from "react"; +import { css, useTheme } from "@emotion/react"; +import styled from "@emotion/styled"; +import Box from "components/Box"; +import Button from "components/Button"; +import Copy from "components/Copy"; +import { NumberInput } from "components/Input"; +import Typography from "components/Typography"; +import { Col, Row, useScreenClass } from "react-grid-system"; +import { formatNumber, formatDecimals, amountToValue } from "utils"; +import iconDefaultToken from "assets/icons/icon-default-token.svg"; +import { LP_DECIMALS } from "constants/dezswap"; +import useBalance from "hooks/useBalance"; +import { DISPLAY_DECIMAL, MOBILE_SCREEN_CLASS } from "constants/layout"; +import { Token } from "types/api"; + +interface InputGroupProps extends React.HTMLAttributes { + lpToken?: string; + assets?: (Partial | undefined)[]; + onBalanceClick?( + value: string, + event: React.MouseEvent, + ): void; +} + +const AssetButton = styled(Button)` + pointer-events: none; + border-radius: 30px; + height: 38px; + padding: 4px 9px; + justify-content: flex-start; + font-size: 16px; + font-weight: 700; +`; +const InputGroup = forwardRef( + ({ lpToken, assets, onBalanceClick, style, ...inputProps }, ref) => { + const screenClass = useScreenClass(); + const balance = useBalance(lpToken || ""); + const theme = useTheme(); + + return ( + + + + + + + {assets?.[0]?.symbol} + {assets?.[0]?.symbol} -  + {assets?.[1]?.symbol} + {assets?.[1]?.symbol} + + + + + + + + + { + if (onBalanceClick) { + onBalanceClick( + amountToValue(balance, LP_DECIMALS) || "", + event, + ); + } + }} + > + LP Balance:  + + {formatNumber( + formatDecimals( + amountToValue(balance, LP_DECIMALS) || 0, + DISPLAY_DECIMAL, + ), + )} + + + + + + + + + + + + + - + + + + + ); + }, +); + +export default InputGroup; diff --git a/src/pages/Earn/Lockdrop/Stake/index.tsx b/src/pages/Earn/Lockdrop/Stake/index.tsx new file mode 100644 index 00000000..652f91b2 --- /dev/null +++ b/src/pages/Earn/Lockdrop/Stake/index.tsx @@ -0,0 +1,458 @@ +import Modal from "components/Modal"; +import { DISPLAY_DECIMAL, MOBILE_SCREEN_CLASS } from "constants/layout"; +import { Col, Row, useScreenClass } from "react-grid-system"; +import { useNavigate, useParams, useSearchParams } from "react-router-dom"; +import useAssets from "hooks/useAssets"; +import usePairs from "hooks/usePairs"; +import { useCallback, useEffect, useMemo } from "react"; +import Box from "components/Box"; +import Typography from "components/Typography"; +import Slider from "components/Slider"; +import { css } from "@emotion/react"; +import Expand from "components/Expanded"; +import InfoTable from "components/InfoTable"; +import Button from "components/Button"; +import Message from "components/Message"; +import useLockdropEvents from "hooks/useLockdropEvents"; +import { + amountToValue, + cutDecimal, + ellipsisCenter, + filterNumberFormat, + formatDateTime, + formatDecimals, + formatNumber, + valueToAmount, +} from "utils"; +import { Controller, useForm } from "react-hook-form"; +import { LP_DECIMALS } from "constants/dezswap"; +import TooltipWithIcon from "components/Tooltip/TooltipWithIcon"; +import useSimulate from "pages/Earn/Pools/Withdraw/useSimulate"; +import { useConnectedWallet } from "@xpla/wallet-provider"; +import { generateIncreaseLockupContractMsg } from "utils/dezswap"; +import useFee from "hooks/useFee"; +import { XPLA_ADDRESS, XPLA_SYMBOL } from "constants/network"; +import { CreateTxOptions, Numeric } from "@xpla/xpla.js"; +import useRequestPost from "hooks/useRequestPost"; +import useBalance from "hooks/useBalance"; +import InputGroup from "./InputGroup"; +import useExpectedReward from "./useEstimatedReward"; + +enum FormKey { + lpValue = "lpValue", + duration = "duration", +} + +function StakePage() { + const { eventAddress } = useParams<{ eventAddress?: string }>(); + const [searchParams] = useSearchParams(); + const connectedWallet = useConnectedWallet(); + const form = useForm>({ + criteriaMode: "all", + mode: "all", + defaultValues: { + lpValue: "", + duration: "8", + }, + }); + + const { getAsset } = useAssets(); + const { findPairByLpAddress } = usePairs(); + const { getLockdropEvent } = useLockdropEvents(); + + const lockdropEvent = useMemo( + () => (eventAddress ? getLockdropEvent(eventAddress) : undefined), + [getLockdropEvent, eventAddress], + ); + + const lpValue = form.watch(FormKey.lpValue); + const duration = form.watch(FormKey.duration); + const lpBalance = useBalance(lockdropEvent?.lp_token_addr); + + const { register, formState } = form; + + const screenClass = useScreenClass(); + const navigate = useNavigate(); + const handleModalClose = () => { + navigate(-1); + }; + + const pair = useMemo( + () => + lockdropEvent + ? findPairByLpAddress(lockdropEvent.lp_token_addr) + : undefined, + [findPairByLpAddress, lockdropEvent], + ); + + const [asset1, asset2] = useMemo( + () => pair?.asset_addresses.map((address) => getAsset(address)) || [], + [getAsset, pair], + ); + + const rewardAsset = useMemo( + () => + lockdropEvent ? getAsset(lockdropEvent.reward_token_addr) : undefined, + [getAsset, lockdropEvent], + ); + + const simulationResult = useSimulate( + pair?.contract_addr || "", + pair?.liquidity_token || "", + valueToAmount(lpValue, LP_DECIMALS) || "0", + ); + + const { expectedReward } = useExpectedReward({ + lockdropEventAddress: lockdropEvent?.addr, + amount: valueToAmount(lpValue, LP_DECIMALS) || "0", + duration: Number(duration), + }); + + const estimatedLockingAmounts = useMemo(() => { + return [asset1, asset2].map( + (asset) => + `${formatNumber( + formatDecimals( + amountToValue( + simulationResult?.estimatedAmount?.find( + (a) => a.address === asset?.token, + )?.amount, + asset?.decimals, + ) || "0", + 3, + ), + )} ${asset?.symbol}`, + ); + }, [asset1, asset2, simulationResult]); + + useEffect(() => { + console.log("lockdropEvent"); + console.log(lockdropEvent); + }, [lockdropEvent]); + + useEffect(() => { + const defaultDuration = searchParams.get("duration"); + if (defaultDuration) { + form.setValue(FormKey.duration, defaultDuration); + } + }, [form, searchParams]); + + const txOptions = useMemo(() => { + if ( + !connectedWallet?.walletAddress || + !lockdropEvent?.addr || + !lockdropEvent?.lp_token_addr + ) { + return undefined; + } + return { + msgs: [ + generateIncreaseLockupContractMsg({ + senderAddress: connectedWallet?.walletAddress, + contractAddress: lockdropEvent?.addr, + lpTokenAddress: lockdropEvent?.lp_token_addr, + amount: valueToAmount(lpValue, LP_DECIMALS), + duration: Number(duration), + }), + ], + }; + }, [connectedWallet, duration, lockdropEvent, lpValue]); + + const { fee } = useFee(txOptions); + + const feeAmount = useMemo(() => { + return fee?.amount?.get(XPLA_ADDRESS)?.amount.toString() || "0"; + }, [fee]); + + const buttonMsg = useMemo(() => { + if (lpValue && Numeric.parse(lpValue).gt(0)) { + if ( + Numeric.parse(valueToAmount(lpValue, LP_DECIMALS) || 0).gt( + lpBalance || 0, + ) + ) { + return "Insufficient LP balance"; + } + + return "Lock"; + } + + return "Enter an amount"; + }, [lpBalance, lpValue]); + + const isValid = buttonMsg === "Lock"; + + const { requestPost } = useRequestPost(handleModalClose); + + const handleSubmit = useCallback>( + async (event) => { + event.preventDefault(); + + console.log("fdsafdsafdsa"); + if (txOptions && fee) { + console.log(txOptions); + console.log(fee); + requestPost({ txOptions, fee, formElement: event.currentTarget }); + } + }, + [fee, requestPost, txOptions], + ); + + return ( + handleModalClose()} + > +
* { + margin-bottom: 10px; + } + `} + > + { + form.setValue(FormKey.lpValue, value); + }} + {...register(FormKey.lpValue, { + setValueAs: (value) => filterNumberFormat(value, LP_DECIMALS), + required: true, + })} + /> + + + + + + Lock time + + + + + + End date:  + + {formatDateTime((lockdropEvent?.end_at || 0) * 1000)} + + + + + + {duration} + +  weeks + +
+ 4 WK +
+ ( + `${value} WK`} + onBlur={field.onBlur} + onChange={(value) => field.onChange(`${value}`)} + value={Number(field.value)} + /> + )} + /> +
+ 52 WK +
+
+ + + + {/* TODO: add tooltip */} + + Expected Rewards + + + + + + + + {formatNumber( + cutDecimal( + amountToValue(expectedReward, rewardAsset?.decimals) || 0, + DISPLAY_DECIMAL, + ), + )} + +  {rewardAsset?.symbol} + + + + + + - + + + + + + {asset1?.symbol || ""} - {asset2?.symbol || ""} + + } + preview={ + + {estimatedLockingAmounts.map((string) => ( + <> + {string} +
+ + ))} + + ), + tooltip: "Lorem ipsum", // TODO: add tooltip + }, + { + key: "fee", + label: "Fee", + tooltip: "The fee paid for executing the transaction.", + value: feeAmount + ? `${formatNumber( + cutDecimal( + amountToValue(feeAmount) || "0", + DISPLAY_DECIMAL, + ), + )} ${XPLA_SYMBOL}` + : "", + }, + ]} + /> + } + > +
+ +
+
+ {lockdropEvent?.cancelable_until && ( + + You can cancel until  + {formatDateTime(lockdropEvent.cancelable_until * 1000)} + + )} + + + +
+ ); +} + +export default StakePage; diff --git a/src/pages/Earn/Lockdrop/Stake/useEstimatedReward.ts b/src/pages/Earn/Lockdrop/Stake/useEstimatedReward.ts new file mode 100644 index 00000000..678325e1 --- /dev/null +++ b/src/pages/Earn/Lockdrop/Stake/useEstimatedReward.ts @@ -0,0 +1,44 @@ +import { useQuery } from "@tanstack/react-query"; +import useAPI from "hooks/useAPI"; +import { useMemo } from "react"; + +const useExpectedReward = ({ + lockdropEventAddress, + amount, + duration, +}: { + lockdropEventAddress?: string; + amount?: number | string; + duration?: number; +}) => { + const { getEstimatedLockdropReward } = useAPI(); + const { data } = useQuery({ + queryKey: [ + "estimatedLockdropReward", + lockdropEventAddress, + amount, + duration, + ], + queryFn: async () => { + if (!lockdropEventAddress || !amount || !duration) { + return null; + } + const res = await getEstimatedLockdropReward( + lockdropEventAddress, + amount, + duration, + ); + return res; + }, + }); + + return useMemo( + () => ({ + expectedReward: data?.estimated_reward, + totalReward: data?.lockdrop_total_reward, + }), + [data], + ); +}; + +export default useExpectedReward; diff --git a/src/pages/Earn/Lockdrop/index.tsx b/src/pages/Earn/Lockdrop/index.tsx index 917e50c1..36d68c2d 100644 --- a/src/pages/Earn/Lockdrop/index.tsx +++ b/src/pages/Earn/Lockdrop/index.tsx @@ -1,19 +1,195 @@ import { css } from "@emotion/react"; import ScrollToTop from "components/ScrollToTop"; -import { Col, Container, Row } from "react-grid-system"; -import { useState } from "react"; +import { Col, Container, Row, useScreenClass } from "react-grid-system"; +import { useEffect, useMemo, useState } from "react"; import Panel from "components/Panel"; import TabButton from "components/TabButton"; import Typography from "components/Typography"; import { MOBILE_SCREEN_CLASS, TABLET_SCREEN_CLASS } from "constants/layout"; import Switch from "components/Switch"; +import { Outlet, useLocation } from "react-router-dom"; +import useLockdropEvents from "hooks/useLockdropEvents"; +import Pagination from "components/Pagination"; +import useAPI from "hooks/useAPI"; +import { useQueries } from "@tanstack/react-query"; +import { Numeric } from "@xpla/xpla.js"; +import useLockdropBookmark from "hooks/useLockdropBookmark"; +import usePairs from "hooks/usePairs"; +import { LockdropEvent } from "types/lockdrop"; +import styled from "@emotion/styled"; +import Box from "components/Box"; + +import iconSortDefault from "assets/icons/icon-sort-default.svg"; +import iconSortAsc from "assets/icons/icon-sort-asc.svg"; +import iconSortDesc from "assets/icons/icon-sort-desc.svg"; +import IconButton from "components/IconButton"; import AssetSelector from "../AssetSelector"; +import LockdropEventList from "./LockdropEventList"; + +const sortIcons = { + default: iconSortDefault, + asc: iconSortAsc, + desc: iconSortDesc, +}; + +const TableHeader = styled(Box)` + display: inline-flex; + justify-content: flex-start; + align-items: center; + flex-wrap: nowrap; + padding: 14px 20px; + margin-bottom: 10px; + gap: 20px; + & > div { + width: 160px; + color: ${({ theme }) => theme.colors.primary}; + font-size: 14px; + font-weight: 900; + & > img { + vertical-align: middle; + } + } +`; + +const LIMIT = 8; function LockdropPage() { + const location = useLocation(); + + const screenClass = useScreenClass(); + const isSmallScreen = [MOBILE_SCREEN_CLASS, TABLET_SCREEN_CLASS].includes( + screenClass, + ); + const { lockdropEvents: originalLockdropEvents } = useLockdropEvents(); + const api = useAPI(); + const { findPair } = usePairs(); + + const lockdropEvents = useMemo(() => { + return originalLockdropEvents.map((lockdropEvent, index) => ({ + ...lockdropEvent, + index, + })); + }, [originalLockdropEvents]); + + const lockdropUserInfoResults = useQueries({ + queries: lockdropEvents.map((lockdropEvent) => ({ + queryKey: ["lockdropEvent", lockdropEvent.addr], + queryFn: async () => { + const res = await api.getLockdropUserInfo(lockdropEvent.addr); + if (!res) { + return null; + } + return res; + }, + + enabled: !!lockdropEvent.addr, + refetchInterval: 30000, + refetchOnMount: false, + refetchOnReconnect: true, + refetchOnWindowFocus: false, + })), + }); + + const lockdropUserInfos = lockdropUserInfoResults.map( + (result) => result.data || undefined, + ); + + const balances = useQueries({ + queries: + lockdropEvents?.map((item) => ({ + queryKey: ["balance", item.lp_token_addr], + queryFn: async () => { + const balance = await api.getTokenBalance(item.lp_token_addr); + return balance; + }, + enabled: !!item.lp_token_addr, + refetchInterval: 30000, + refetchOnMount: false, + refetchOnReconnect: true, + refetchOnWindowFocus: false, + })) || [], + }).map((item) => item.data); + const [addresses, setAddresses] = useState<[string | undefined, string | undefined]>(); const [selectedTabIndex, setSelectedTabIndex] = useState(0); const [isLiveOnly, setIsLiveOnly] = useState(true); + const [currentPage, setCurrentPage] = useState(1); + const { bookmarks } = useLockdropBookmark(); + + const selectedPair = useMemo(() => { + const address1 = addresses?.[0] || ""; + const address2 = addresses?.[1] || ""; + return findPair([address1, address2]); + }, [addresses, findPair]); + + const filteredLockdropEvents = lockdropEvents.filter( + (lockdropEvent, index) => { + if ( + isLiveOnly && + lockdropEvent.end_at + 52 * 7 * 60 * 60 * 24 < Date.now() / 1000 + ) { + return false; + } + if (selectedPair) { + return lockdropEvent.lp_token_addr === selectedPair.liquidity_token; + } + if (selectedTabIndex === 1) { + return Numeric.parse( + lockdropUserInfos[index]?.user_total_locked_lp_token || 0, + ).gt(0); + } + if (selectedTabIndex === 2) { + return bookmarks?.includes(lockdropEvent.addr); + } + return true; + }, + ); + + const [sortBy, setSortBy] = useState( + "end_at", + ); + const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); + + const sortedLockdropEvents = useMemo(() => { + const dir = sortDirection === "asc" ? 1 : -1; + return filteredLockdropEvents.sort((a, b) => { + if (sortBy === "balance") { + return Numeric.parse(balances[a.index] || 0).gt(balances[b.index] || 0) + ? dir + : -dir; + } + + if (a[sortBy] === b[sortBy]) { + return 0; + } + + try { + return Numeric.parse(a[sortBy]).gt(b[sortBy]) ? dir : -dir; + } catch (error) { + console.log(error); + } + return a[sortBy] > b[sortBy] ? dir : -dir; + }); + }, [balances, filteredLockdropEvents, sortBy, sortDirection]); + + useEffect(() => { + setCurrentPage(1); + }, [selectedTabIndex]); + + useEffect(() => { + lockdropUserInfoResults.forEach((result) => { + result.refetch(); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [location]); + + const onSortClick = (newSortBy: typeof sortBy) => { + setSortDirection((current) => + newSortBy !== sortBy || current === "asc" ? "desc" : "asc", + ); + setSortBy(newSortBy); + }; return ( <> @@ -80,8 +256,105 @@ function LockdropPage() { +
+ {!isSmallScreen && ( + +
 
+
Pool
+
+ Total Staked LP + onSortClick("total_locked_lp")} + /> +
+
+ Allocation + onSortClick("total_reward")} + /> +
+
+ My LP Balance + onSortClick("balance")} + /> +
+
+ Event Ends In + onSortClick("end_at")} + /> +
+
+ )} + lockdropUserInfos[item.index]) + .slice((currentPage - 1) * LIMIT, currentPage * LIMIT) || [] + } + /> +
+ { + setCurrentPage(value); + }} + /> + ); } diff --git a/src/routes.tsx b/src/routes.tsx index 1f23288f..d92d249c 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -9,6 +9,7 @@ import CreatePage from "pages/Earn/Pools/Create"; import Error404 from "pages/Error404"; import EarnPage from "pages/Earn"; import LockdropPage from "pages/Earn/Lockdrop"; +import StakePage from "pages/Earn/Lockdrop/Stake"; export interface RouteObject extends Omit { children?: RouteObject[]; @@ -72,6 +73,7 @@ const routes: RouteObject[] = [ { path: "lockdrop", element: , + children: [{ path: ":eventAddress", element: }], }, { index: true, element: }, ], diff --git a/src/stores/lockdrops.ts b/src/stores/lockdrops.ts new file mode 100644 index 00000000..5f0146d0 --- /dev/null +++ b/src/stores/lockdrops.ts @@ -0,0 +1,6 @@ +import { atomWithStorage } from "jotai/utils"; +import { NetworkName } from "types/common"; + +export const lockdropBookmarksAtom = atomWithStorage<{ + [K in NetworkName]?: string[]; +}>("lockdrop-bookmarks", { mainnet: [], testnet: [] }); diff --git a/src/styles/GlobalStyles/index.tsx b/src/styles/GlobalStyles/index.tsx index 8a8db72a..83df91b2 100644 --- a/src/styles/GlobalStyles/index.tsx +++ b/src/styles/GlobalStyles/index.tsx @@ -32,6 +32,11 @@ function GlobalStyles() { font-family: "Nunito", sans-serif; outline: none; } + + a:active { + text-decoration: none; + color: inherit; + } `} /> {/* React Modal */} diff --git a/src/types/lockdrop.d.ts b/src/types/lockdrop.d.ts new file mode 100644 index 00000000..e835b9db --- /dev/null +++ b/src/types/lockdrop.d.ts @@ -0,0 +1,39 @@ +export type LockdropEvent = { + id: number; + addr: string; + reward_token_addr: string; + lp_token_addr: string; + total_locked_lp: string; + total_reward: string; + start_at: number; + end_at: number; + cancelable_until: number; +}; + +export interface LockdropEvents { + events: LockdropEvent[]; +} + +export type LockdropLockupInfo = { + duration: number; + unlock_second: number; + locked_lp_token: string; + weighted_score: string; + total_reward: string; + claimable: string; + claimed: string; +}; + +export interface LockdropUserInfo { + lp_token_addr: string; + lockdrop_total_locked_lp_token: string; + lockdrop_total_reward: string; + user_total_locked_lp_token: string; + user_total_reward: string; + lockup_infos: LockdropLockupInfo[]; +} + +export type LockdropEstimatedReward = { + lockdrop_total_reward: string; + estimated_reward: string; +}; diff --git a/src/utils/dezswap.ts b/src/utils/dezswap.ts index f544665f..739ef60e 100644 --- a/src/utils/dezswap.ts +++ b/src/utils/dezswap.ts @@ -239,3 +239,29 @@ export const generateSwapMsg = ( getCoins([{ address: fromAssetAddress, amount }]), ); }; + +export const generateIncreaseLockupContractMsg = ({ + senderAddress, + contractAddress, + lpTokenAddress, + amount, + duration, +}: { + senderAddress: string; + contractAddress: string; + lpTokenAddress: string; + duration: number | string; + amount?: number | string; +}) => { + return new MsgExecuteContract(senderAddress, lpTokenAddress, { + send: { + msg: window.btoa( + JSON.stringify({ + increase_lockup: { duration: Number(duration) }, + }), + ), + amount: `${amount}`, + contract: contractAddress, + }, + }); +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index 26833f27..eca7e9d6 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -130,3 +130,27 @@ export const formatRatio = (value: number) => { } return value.toFixed(2); }; + +export const formatDateTime = (input: string | number | Date) => { + const date = new Date(input); + + return Intl.DateTimeFormat("en-US", { + year: "numeric", + month: "short", + day: "numeric", + hour: "numeric", + minute: "numeric", + hour12: false, + }).format(date); +}; + +export const getRemainDays = (input: string | number | Date) => { + const date = new Date(input); + const now = new Date(); + + const diff = date.getTime() - now.getTime(); + const res = Math.ceil(diff / (1000 * 3600 * 24)); + return res > 0 ? res : 0; +}; + +console.log(formatDateTime(new Date())); diff --git a/yarn.lock b/yarn.lock index 50b48039..c21ee21e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -176,6 +176,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.19.0" +"@babel/runtime@^7.10.1": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.6.tgz#57d64b9ae3cff1d67eb067ae117dac087f5bd438" + integrity sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ== + dependencies: + regenerator-runtime "^0.13.11" + "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.7": version "7.21.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.5.tgz#8492dddda9644ae3bda3b45eabe87382caee7200" @@ -1773,6 +1780,11 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: inherits "^2.0.1" safe-buffer "^5.0.1" +classnames@^2.2.5: + version "2.3.2" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" + integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== + clean-stack@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" @@ -3974,6 +3986,23 @@ randomfill@^1.0.3: randombytes "^2.0.5" safe-buffer "^5.1.0" +rc-slider@^10.2.1: + version "10.2.1" + resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-10.2.1.tgz#9b571d19f740adcacdde271f44901a47717fd8da" + integrity sha512-l355C/65iV4UFp7mXq5xBTNX2/tF2g74VWiTVlTpNp+6vjE/xaHHNiQq5Af+Uu28uUiqCuH/QXs5HfADL9KJ/A== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.5" + rc-util "^5.27.0" + +rc-util@^5.27.0: + version "5.34.1" + resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.34.1.tgz#0becf411d8f09bdb0f1b61322964f27efeeba642" + integrity sha512-SqiUT8Ssgh5C+hu4y887xwCrMNcxLm6ScOo8AFlWYYF3z9uNNiPpwwSjvicqOlWd79rNw1g44rnP7tz9MrO1ZQ== + dependencies: + "@babel/runtime" "^7.18.3" + react-is "^16.12.0" + react-dom@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" @@ -3994,7 +4023,7 @@ react-hook-form@^7.40.0: resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.43.9.tgz#84b56ac2f38f8e946c6032ccb760e13a1037c66d" integrity sha512-AUDN3Pz2NSeoxQ7Hs6OhQhDr6gtF9YRuutGDwPQqhSUAHJSgGl2VeY3qN19MG0SucpjgDiuMJ4iC5T5uB+eaNQ== -react-is@^16.13.1, react-is@^16.7.0: +react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== From 5f81e3ffed60a659ed02c880c8481f2b95a532b6 Mon Sep 17 00:00:00 2001 From: maro Date: Fri, 18 Aug 2023 01:38:32 +0900 Subject: [PATCH 07/54] feat: support claim/unstake/cancel, add styles - removed unnecessary lines --- src/hooks/useAPI.ts | 24 + src/hooks/useFee.ts | 16 +- src/hooks/useRequestPost.ts | 68 +- src/pages/Earn/Expand.tsx | 1 + src/pages/Earn/Lockdrop/LockdropEventItem.tsx | 791 ++++++++++-------- src/pages/Earn/Lockdrop/Stake/index.tsx | 9 - src/pages/Earn/Lockdrop/index.tsx | 2 +- src/utils/dezswap.ts | 48 ++ src/utils/index.ts | 2 - 9 files changed, 572 insertions(+), 389 deletions(-) diff --git a/src/hooks/useAPI.ts b/src/hooks/useAPI.ts index 1f140070..3c47f9ca 100644 --- a/src/hooks/useAPI.ts +++ b/src/hooks/useAPI.ts @@ -18,6 +18,7 @@ import { LockdropEvents, LockdropUserInfo, } from "types/lockdrop"; +import { CreateTxOptions } from "@xpla/xpla.js"; interface TokenBalance { balance: string; @@ -200,6 +201,27 @@ const useAPI = (version: ApiVersion = "v1") => { [lcd.wasm, walletAddress], ); + const estimateFee = useCallback( + async (txOptions: CreateTxOptions) => { + if (!connectedWallet) { + return undefined; + } + const account = await lcd.auth.accountInfo(connectedWallet.walletAddress); + const res = await lcd.tx.estimateFee( + [ + { + sequenceNumber: account.getSequenceNumber(), + publicKey: account.getPublicKey(), + }, + ], + txOptions, + ); + + return res; + }, + [connectedWallet, lcd], + ); + return useMemo( () => ({ ...apiClient, @@ -214,6 +236,7 @@ const useAPI = (version: ApiVersion = "v1") => { getLockdropEvents, getLockdropUserInfo, getEstimatedLockdropReward, + estimateFee, }), [ apiClient, @@ -228,6 +251,7 @@ const useAPI = (version: ApiVersion = "v1") => { getLockdropEvents, getLockdropUserInfo, getEstimatedLockdropReward, + estimateFee, ], ); }; diff --git a/src/hooks/useFee.ts b/src/hooks/useFee.ts index 2641c30d..1d4ae6ce 100644 --- a/src/hooks/useFee.ts +++ b/src/hooks/useFee.ts @@ -3,6 +3,7 @@ import { useConnectedWallet } from "@xpla/wallet-provider"; import { useDeferredValue, useEffect, useState } from "react"; import { AxiosError } from "axios"; import useLCDClient from "hooks/useLCDClient"; +import useAPI from "./useAPI"; const useFee = (txOptions?: CreateTxOptions) => { const connectedWallet = useConnectedWallet(); @@ -11,6 +12,7 @@ const useFee = (txOptions?: CreateTxOptions) => { const [isLoading, setIsLoading] = useState(false); const [isFailed, setIsFailed] = useState(false); const [errMsg, setErrMsg] = useState(""); + const api = useAPI(); const deferredCreateTxOptions = useDeferredValue(txOptions); @@ -38,18 +40,8 @@ const useFee = (txOptions?: CreateTxOptions) => { return; } - const account = await lcd.auth.accountInfo( - connectedWallet.walletAddress, - ); - const res = await lcd.tx.estimateFee( - [ - { - sequenceNumber: account.getSequenceNumber(), - publicKey: account.getPublicKey(), - }, - ], - deferredCreateTxOptions, - ); + const res = await api.estimateFee(deferredCreateTxOptions); + if (res && !isAborted) { setFee(res); setErrMsg(""); diff --git a/src/hooks/useRequestPost.ts b/src/hooks/useRequestPost.ts index 57812d11..854db0f0 100644 --- a/src/hooks/useRequestPost.ts +++ b/src/hooks/useRequestPost.ts @@ -23,30 +23,37 @@ const useRequestPost = (onDoneTx?: () => void, isModalParent = false) => { }); const [fee, setFee] = useState(); - const handleConfirm = useCallback(async () => { - if (txOptions && fee && connectedWallet?.availablePost) { - try { - txBroadcastModal.open(); - const result = await connectedWallet.post({ - ...txOptions, - fee, - }); - setTxResult(result); - } catch (error) { - console.log(error); - if ( - error instanceof CreateTxFailed && - connectedWallet?.connectType === ConnectType.WALLETCONNECT - ) { - error.message = - "Transaction creation failed, please check the details in your wallet and try again"; - } - if (error instanceof Error) { - setTxError(error); + + const postTx = useCallback( + async (createTxOptions: CreateTxOptions) => { + if (connectedWallet?.availablePost) { + try { + txBroadcastModal.open(); + const result = await connectedWallet.post(createTxOptions); + setTxResult(result); + } catch (error) { + console.log(error); + if ( + error instanceof CreateTxFailed && + connectedWallet?.connectType === ConnectType.WALLETCONNECT + ) { + error.message = + "Transaction creation failed, please check the details in your wallet and try again"; + } + if (error instanceof Error) { + setTxError(error); + } } } + }, + [connectedWallet, txBroadcastModal], + ); + + const handleConfirm = useCallback(async () => { + if (txOptions) { + postTx({ ...txOptions, fee: fee ?? txOptions.fee }); } - }, [connectedWallet, fee, txOptions, txBroadcastModal]); + }, [fee, postTx, txOptions]); const [node, setNode] = useState(); const confirmationModal = useConfirmationModal({ @@ -71,21 +78,34 @@ const useRequestPost = (onDoneTx?: () => void, isModalParent = false) => { const requestPost = useCallback( (args: { txOptions: CreateTxOptions; - fee: Fee; - formElement: HTMLFormElement; + fee?: Fee; + formElement?: HTMLFormElement; + skipConfirmation?: boolean; }) => { + if (args.skipConfirmation) { + if (args.txOptions) { + postTx({ ...args.txOptions, fee: args.fee ?? args.txOptions.fee }); + } + return; + } + startTransition(() => { setTxOptions(args.txOptions); setFee(args.fee); + if (!args.formElement) { + setNode(undefined); + return; + } const newNode = document.importNode(args.formElement, true); newNode.addEventListener("submit", (e) => { e.preventDefault(); }); setNode(newNode); }); + confirmationModal.open(); }, - [confirmationModal], + [confirmationModal, postTx], ); return { requestPost }; diff --git a/src/pages/Earn/Expand.tsx b/src/pages/Earn/Expand.tsx index d1cfa180..ea05477b 100644 --- a/src/pages/Earn/Expand.tsx +++ b/src/pages/Earn/Expand.tsx @@ -34,6 +34,7 @@ const Header = styled(Box)` position: unset; } padding: 0; + padding-right: 20px; .${SMALL_BROWSER_SCREEN_CLASS} &, .${LARGE_BROWSER_SCREEN_CLASS} & { diff --git a/src/pages/Earn/Lockdrop/LockdropEventItem.tsx b/src/pages/Earn/Lockdrop/LockdropEventItem.tsx index 6d799ee4..6259b7da 100644 --- a/src/pages/Earn/Lockdrop/LockdropEventItem.tsx +++ b/src/pages/Earn/Lockdrop/LockdropEventItem.tsx @@ -22,19 +22,40 @@ import iconAlarm from "assets/icons/icon-alarm.svg"; import iconBookmark from "assets/icons/icon-bookmark-default.svg"; import iconBookmarkSelected from "assets/icons/icon-bookmark-selected.svg"; +import iconBadge from "assets/icons/icon-badge.svg"; + import useAssets from "hooks/useAssets"; import useNetwork from "hooks/useNetwork"; import usePairs from "hooks/usePairs"; -import { useMemo } from "react"; +import { useCallback, useMemo } from "react"; import ProgressBar from "components/ProgressBar"; import { Link } from "react-router-dom"; import { LockdropEvent, LockdropUserInfo } from "types/lockdrop"; import IconButton from "components/IconButton"; import useBalance from "hooks/useBalance"; import Hr from "components/Hr"; -import { Numeric } from "@xpla/xpla.js"; +import { CreateTxOptions, Numeric } from "@xpla/xpla.js"; +import useAPI from "hooks/useAPI"; +import useRequestPost from "hooks/useRequestPost"; +import { + generateCancelLockdropMsg, + generateClaimLockdropMsg, + generateUnstakeLockdropMsg, +} from "utils/dezswap"; +import { useConnectedWallet } from "@xpla/wallet-provider"; import Expand from "../Expand"; +const Wrapper = styled(Box)<{ isNeedAction?: boolean }>` + ${({ isNeedAction, theme }) => + isNeedAction && + css` + padding: 2px; + background-image: ${theme.colors.gradient}; + `} + + border-radius: 14px; +`; + const TableRow = styled(Box)` display: inline-flex; justify-content: flex-start; @@ -44,6 +65,7 @@ const TableRow = styled(Box)` background: none; gap: 20px; & > div { + position: relative; width: 160px; color: ${({ theme }) => theme.colors.primary}; font-size: 16px; @@ -319,9 +341,14 @@ function LockdropEventItem({ screenClass, ); + const connectedWallet = useConnectedWallet(); + const api = useAPI(); const { getAsset } = useAssets(); const network = useNetwork(); const { findPairByLpAddress } = usePairs(); + const { requestPost } = useRequestPost(() => { + document.location.reload(); + }); const pair = useMemo( () => findPairByLpAddress(lockdropEvent.lp_token_addr), [findPairByLpAddress, lockdropEvent], @@ -358,6 +385,14 @@ function LockdropEventItem({ ); }, [lockdropUserInfo]); + const needActionBadge = useMemo( + () => + isNeedAction ? ( + + ) : null, + [isNeedAction], + ); + const extra = useMemo( () => isStakable @@ -391,368 +426,442 @@ function LockdropEventItem({ [isBookmarked, lockdropEvent, onBookmarkToggle], ); + const lockdropAction = useCallback( + async (messageType: "cancel" | "claim" | "unstake", duration: number) => { + if (!connectedWallet?.walletAddress) { + return; + } + const generateMsg = { + cancel: generateCancelLockdropMsg, + claim: generateClaimLockdropMsg, + unstake: generateUnstakeLockdropMsg, + }; + + const txOptions: CreateTxOptions = { + msgs: [ + generateMsg[messageType]({ + senderAddress: connectedWallet?.walletAddress, + contractAddress: lockdropEvent.addr, + duration, + }), + ], + }; + + const fee = await api.estimateFee(txOptions); + + if (fee) { + requestPost({ txOptions, fee, skipConfirmation: true }); + } + }, + [api, connectedWallet, lockdropEvent.addr, requestPost], + ); + return ( - - -
- {bookmarkButton} -
-
-
- {isSmallScreen && } - - - - - - - - + + +
+ {bookmarkButton} +
+
+
+ {isSmallScreen && } + + + - {asset1?.symbol}-{asset2?.symbol} + + - - - - - {bookmarkButton} - - -
-
- {isSmallScreen && } + + + {asset1?.symbol}-{asset2?.symbol} + + + + + + {bookmarkButton} + + +
- {formatNumber( - formatDecimals( - amountToValue(lockdropEvent.total_locked_lp, LP_DECIMALS) || - "", - 2, - ), - )} -  LP + {isSmallScreen && } +
+ {formatNumber( + formatDecimals( + amountToValue(lockdropEvent.total_locked_lp, LP_DECIMALS) || + "", + 2, + ), + )} +  LP +
-
-
- {isSmallScreen && }
- {formatNumber( - formatDecimals( - amountToValue( - lockdropEvent.total_reward, - rewardAsset?.decimals, - ) || "", - 2, - ), - )} -   - {rewardAsset?.symbol} + {isSmallScreen && } +
+ {formatNumber( + formatDecimals( + amountToValue( + lockdropEvent.total_reward, + rewardAsset?.decimals, + ) || "", + 2, + ), + )} +   + {rewardAsset?.symbol} +
-
-
- {isSmallScreen && }
- {formatNumber( - formatDecimals( - amountToValue(lpBalance || 0, LP_DECIMALS) || "", - 2, - ), - )} -  LP + {isSmallScreen && } +
+ {formatNumber( + formatDecimals( + amountToValue(lpBalance || 0, LP_DECIMALS) || "", + 2, + ), + )} +  LP +
-
-
- {isSmallScreen && }
- {getRemainDays(lockdropEvent.end_at * 1000)} days - - - Lock begins at -
- {formatDateTime(lockdropEvent.end_at * 1000)} -
-
- - Cancellation is available -
- until  - {formatDateTime(lockdropEvent.cancelable_until * 1000)} -
- - } - arrow - > - + + Event Ends In + {needActionBadge} + + + )} +
+ {getRemainDays(lockdropEvent.end_at * 1000)} days + + + Lock begins at +
+ {formatDateTime(lockdropEvent.end_at * 1000)} +
+
+ + Cancellation is available +
+ until  + {formatDateTime(lockdropEvent.cancelable_until * 1000)} +
+ + } + arrow + > + +
+
+ {!isSmallScreen && ( +
- + > + {needActionBadge} +
+ )}
-
- {isSmallScreen && !!extra.length &&
{extra}
} - - } - extra={!isSmallScreen ? extra : undefined} - > - - - Project Site - {extra}
} + + } + extra={!isSmallScreen ? extra : undefined} + > + + + Project Site + + Pair Info + + + + +
- Pair Info - - - - -
- {!lockdropUserInfo?.lockup_infos?.length ? ( - -
- -
- -
 
-
-
- ) : ( - lockdropUserInfo.lockup_infos.map((lockupInfo) => { - const eventEndAt = new Date(lockdropEvent.end_at * 1000); - const unlockAt = new Date(lockupInfo.unlock_second * 1000); - const now = new Date(); - - const isClaimable = Numeric.parse(lockupInfo.claimable || 0).gt( - 0, - ); - const isUnstakable = now >= unlockAt; - const isLocking = now > eventEndAt && now < unlockAt; - - return ( - -
- - - - Locking Period - - :  - - -
-
- - {formatDateTime(lockdropEvent.end_at * 1000)}  - -  - {formatDateTime(lockupInfo.unlock_second * 1000)} ( - {lockupInfo.duration} weeks) - -
- - - - Remaining - - :  - - -
-
- - {getRemainDays(lockupInfo.unlock_second * 1000)} -  days - -
- -
-
- +
+ +
+ +
 
+
+ + ) : ( + lockdropUserInfo.lockup_infos.map((lockupInfo) => { + const eventEndAt = new Date(lockdropEvent.end_at * 1000); + const unlockAt = new Date(lockupInfo.unlock_second * 1000); + const now = new Date(); + + const isClaimable = Numeric.parse(lockupInfo.claimable || 0).gt( + 0, + ); + const isUnstakable = now >= unlockAt; + const isLocking = now > eventEndAt && now < unlockAt; + + return ( + +
+ + + + Locking Period + + :  + + +
+
+ + {formatDateTime(lockdropEvent.end_at * 1000)} +   -  + {formatDateTime( + lockupInfo.unlock_second * 1000, + )}{" "} + ({lockupInfo.duration} weeks) + +
+ + + + Remaining + + :  + + +
+
+ + {getRemainDays(lockupInfo.unlock_second * 1000)} +  days + +
+ +
+
+ +
+
-
-
- -
-
- {isStakable && ( - -
+
+ {isStakable && ( + + + + )} + {isCancelable && ( + + )} + {(isLocking || isUnstakable) && ( + + )} + {(isLocking || isUnstakable) && ( + - - )} - {isCancelable && ( - - )} - {(isLocking || isUnstakable) && ( - - )} - {(isLocking || isUnstakable) && ( - - )} -
- - ); - }) - )} -
- - + )} +
+ + ); + }) + )} +
+
+ +
); } diff --git a/src/pages/Earn/Lockdrop/Stake/index.tsx b/src/pages/Earn/Lockdrop/Stake/index.tsx index 652f91b2..94a4ba53 100644 --- a/src/pages/Earn/Lockdrop/Stake/index.tsx +++ b/src/pages/Earn/Lockdrop/Stake/index.tsx @@ -125,11 +125,6 @@ function StakePage() { ); }, [asset1, asset2, simulationResult]); - useEffect(() => { - console.log("lockdropEvent"); - console.log(lockdropEvent); - }, [lockdropEvent]); - useEffect(() => { const defaultDuration = searchParams.get("duration"); if (defaultDuration) { @@ -187,11 +182,7 @@ function StakePage() { const handleSubmit = useCallback>( async (event) => { event.preventDefault(); - - console.log("fdsafdsafdsa"); if (txOptions && fee) { - console.log(txOptions); - console.log(fee); requestPost({ txOptions, fee, formElement: event.currentTarget }); } }, diff --git a/src/pages/Earn/Lockdrop/index.tsx b/src/pages/Earn/Lockdrop/index.tsx index 36d68c2d..bdcea436 100644 --- a/src/pages/Earn/Lockdrop/index.tsx +++ b/src/pages/Earn/Lockdrop/index.tsx @@ -37,7 +37,7 @@ const TableHeader = styled(Box)` justify-content: flex-start; align-items: center; flex-wrap: nowrap; - padding: 14px 20px; + padding: 14px 36px; margin-bottom: 10px; gap: 20px; & > div { diff --git a/src/utils/dezswap.ts b/src/utils/dezswap.ts index 739ef60e..80ea964e 100644 --- a/src/utils/dezswap.ts +++ b/src/utils/dezswap.ts @@ -265,3 +265,51 @@ export const generateIncreaseLockupContractMsg = ({ }, }); }; + +export const generateCancelLockdropMsg = ({ + senderAddress, + contractAddress, + duration, +}: { + senderAddress: string; + contractAddress: string; + duration: number | string; +}) => { + return new MsgExecuteContract(senderAddress, contractAddress, { + cancel: { + duration: Number(duration), + }, + }); +}; + +export const generateClaimLockdropMsg = ({ + senderAddress, + contractAddress, + duration, +}: { + senderAddress: string; + contractAddress: string; + duration: number | string; +}) => { + return new MsgExecuteContract(senderAddress, contractAddress, { + claim: { + duration: Number(duration), + }, + }); +}; + +export const generateUnstakeLockdropMsg = ({ + senderAddress, + contractAddress, + duration, +}: { + senderAddress: string; + contractAddress: string; + duration: number | string; +}) => { + return new MsgExecuteContract(senderAddress, contractAddress, { + unlock: { + duration: Number(duration), + }, + }); +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index eca7e9d6..3c95b97d 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -152,5 +152,3 @@ export const getRemainDays = (input: string | number | Date) => { const res = Math.ceil(diff / (1000 * 3600 * 24)); return res > 0 ? res : 0; }; - -console.log(formatDateTime(new Date())); From 13b1dc7bb1a188f9f3953bf69d2299506ad9f2d8 Mon Sep 17 00:00:00 2001 From: maro Date: Fri, 18 Aug 2023 15:42:28 +0900 Subject: [PATCH 08/54] fix: adjust gas --- src/hooks/useLCDClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useLCDClient.ts b/src/hooks/useLCDClient.ts index 2b74b06e..44046d9a 100644 --- a/src/hooks/useLCDClient.ts +++ b/src/hooks/useLCDClient.ts @@ -9,7 +9,7 @@ const useLCDClient = () => { new LCDClient({ URL: network.lcd, chainID: network.chainID, - gasAdjustment: 1.1, + gasAdjustment: 1.2, }), [network], ); From 8efd22578c1cfe806165fd9bca7925b1518459ec Mon Sep 17 00:00:00 2001 From: maro Date: Fri, 18 Aug 2023 15:47:47 +0900 Subject: [PATCH 09/54] fix: add tooltip on badge, remove comments --- src/pages/Earn/Lockdrop/LockdropEventItem.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pages/Earn/Lockdrop/LockdropEventItem.tsx b/src/pages/Earn/Lockdrop/LockdropEventItem.tsx index 6259b7da..c389a4e6 100644 --- a/src/pages/Earn/Lockdrop/LockdropEventItem.tsx +++ b/src/pages/Earn/Lockdrop/LockdropEventItem.tsx @@ -388,7 +388,9 @@ function LockdropEventItem({ const needActionBadge = useMemo( () => isNeedAction ? ( - + + + ) : null, [isNeedAction], ); @@ -610,6 +612,8 @@ function LockdropEventItem({ left: 100%; top: 50%; transform: translateY(-50%); + line-height: 1; + font-size: 0; `} > {needActionBadge} @@ -833,7 +837,7 @@ function LockdropEventItem({
-
- {isSmallScreen && } -
- {formatNumber( - formatDecimals( - amountToValue(lpBalance || 0, LP_DECIMALS) || "", - 2, - ), - )} -  LP -
-
{isSmallScreen && (