diff --git a/apps/web/src/components/Header.tsx b/apps/web/src/components/Header.tsx index 34099985b..d38b35e77 100644 --- a/apps/web/src/components/Header.tsx +++ b/apps/web/src/components/Header.tsx @@ -2,8 +2,13 @@ import Image from "next/image"; import Link from "next/link"; import ConnectButton from "./ConnectButton"; import Banner from "./Banner"; +import Navigation from "./Navigation"; -export default function Header(): JSX.Element { +export default function Header({ + isBridgeUp, +}: { + isBridgeUp: boolean; +}): JSX.Element { return (
@@ -18,10 +23,20 @@ export default function Header(): JSX.Element { />
+ {isBridgeUp && ( +
+ +
+ )}
+ {isBridgeUp && ( +
+ +
+ )} ); } diff --git a/apps/web/src/components/Link.tsx b/apps/web/src/components/Link.tsx new file mode 100644 index 000000000..0c313a206 --- /dev/null +++ b/apps/web/src/components/Link.tsx @@ -0,0 +1,44 @@ +/* eslint-disable no-restricted-imports */ +import { LinkProps as NextLinkProps } from "next/dist/client/link"; +import { useRouter } from "next/router"; +import NextLink from "next/link"; +import { PropsWithChildren } from "react"; +import { UrlObject } from "url"; +import { useNetworkEnvironmentContext } from "@contexts/NetworkEnvironmentContext"; + +export interface LinkUrlObject extends UrlObject { + query?: Record; +} + +interface LinkProps extends NextLinkProps { + href: LinkUrlObject; +} + +/** + * Overrides the default next/link to provide ability to 'keep ?network= query string'. + * This allows `` usage to be network agnostic where ?network= are automatically appended. + * + * To keep implementation simple, LinkProps enforce href to be strictly a `UrlObject` object + * where query is a `Record`. Hence only use this for internal linking. + * + * @param {PropsWithChildren} props + */ +export function Link(props: PropsWithChildren): JSX.Element { + const { children, href } = props; + const router = useRouter(); + const networkQuery = router.query.network; + + const { networkEnv } = useNetworkEnvironmentContext(); + if (networkQuery) { + href.query = { + ...(href.query ?? {}), + network: networkEnv, + }; + } + + return ( + + {children} + + ); +} diff --git a/apps/web/src/components/Navigation.tsx b/apps/web/src/components/Navigation.tsx new file mode 100644 index 000000000..e570ed226 --- /dev/null +++ b/apps/web/src/components/Navigation.tsx @@ -0,0 +1,33 @@ +import clsx from "clsx"; +import { useRouter } from "next/router"; +import { Link } from "./Link"; + +export default function Navigation() { + const router = useRouter(); + const isRoot = router.pathname === "/"; + + return ( +
+ + + Bridge + + + + + Liquidity + + +
+ ); +} diff --git a/apps/web/src/components/OverviewList.tsx b/apps/web/src/components/OverviewList.tsx new file mode 100644 index 000000000..bc7d271a6 --- /dev/null +++ b/apps/web/src/components/OverviewList.tsx @@ -0,0 +1,279 @@ +import { PropsWithChildren } from "react"; +import { useDeFiScanContext } from "@contexts/DeFiScanContext"; +import { NetworkI, networks } from "@contexts/NetworkContext"; +import Image from "next/image"; +import NumericFormat from "@components/commons/NumericFormat"; +import BigNumber from "bignumber.js"; +import { FiArrowUpRight, FiChevronDown, FiChevronUp } from "react-icons/fi"; +import { useContractContext } from "@contexts/ContractContext"; +import { Network, TokenDetailI, TokensI } from "types"; +import clsx from "clsx"; +import useResponsive from "@hooks/useResponsive"; +import { Disclosure } from "@headlessui/react"; +import IconTooltip from "./commons/IconTooltip"; + +function TokenOrNetworkInfo({ + token, + iconClass, + nameClass, + onClose, +}: { + token: TokenDetailI | NetworkI; + iconClass?: string; + nameClass?: string; + onClose?: () => void; +}) { + return ( +
+
+ {token.name} + + {token.name} + {(token as TokenDetailI).subtitle && ( + + {(token as TokenDetailI).subtitle} + + )} + +
+ {onClose !== undefined && ( + + )} +
+ ); +} + +function AddressComponent({ + address, + isDeFiAddress, +}: { + address: string; + isDeFiAddress: boolean; +}) { + const { ExplorerURL } = useContractContext(); + const { getAddressUrl } = useDeFiScanContext(); + const url = isDeFiAddress + ? getAddressUrl(address) + : `${ExplorerURL}/address/${address}`; + return ( + + {address} +
+ + View +
+
+ ); +} + +function BorderDiv({ + children, + className, +}: PropsWithChildren<{ className: string }>) { + return ( +
+ {children} +
+ ); +} + +function TokenDetails({ + network, + token, + isDeFiAddress, + amount, + containerClass, + onClose, +}: { + network: NetworkI; + token: TokenDetailI; + isDeFiAddress: boolean; + amount: BigNumber; + containerClass?: string; + onClose?: () => void; +}) { + const { BridgeV1, HotWalletAddress } = useContractContext(); + const address = isDeFiAddress ? HotWalletAddress : BridgeV1.address; + return ( +
+
+ +
+
+ + Blockchain + + +
+
+
+ Liquidity + +
+ +
+
+ Address + +
+
+ ); +} + +export default function OverviewList({ balances }) { + const [firstNetwork, secondNetwork] = networks; + const { isMobile } = useResponsive(); + + const getAmount = (symbol: string, network): BigNumber => { + if (network === Network.DeFiChain) { + return new BigNumber(balances.DFC?.[symbol] ?? 0); + } + return new BigNumber(balances.EVM?.[symbol] ?? 0); + }; + + function getTokenRow(item: TokensI, onClose?: () => void) { + return ( + <> + + + + ); + } + + function getTokenCard(item: TokensI) { + return ( + + {({ open, close }) => ( + <> + {!open && ( + +
+
+
+ +
+
+ +
+
+ +
+
+ )} + + {getTokenRow(item, close)} + + + )} +
+ ); + } + + return ( + <> +
+
+
+ Token +
+
+ Blockchain +
+
+ Liquidity + +
+
+ Address +
+
+
+
+ {secondNetwork.tokens.map((item) => ( + + {isMobile ? getTokenCard(item) : getTokenRow(item)} + + ))} +
+ + ); +} diff --git a/apps/web/src/components/ScreenContainer.tsx b/apps/web/src/components/ScreenContainer.tsx index 4d7217b86..6c13ae55f 100644 --- a/apps/web/src/components/ScreenContainer.tsx +++ b/apps/web/src/components/ScreenContainer.tsx @@ -21,7 +21,7 @@ export default function ScreenContainer({ return (
-
+
{isBridgeUp || is404 ?
{children}
: } diff --git a/apps/web/src/components/commons/IconTooltip.tsx b/apps/web/src/components/commons/IconTooltip.tsx index 446fb3815..5a3a3785e 100644 --- a/apps/web/src/components/commons/IconTooltip.tsx +++ b/apps/web/src/components/commons/IconTooltip.tsx @@ -9,6 +9,7 @@ interface Props { title?: string; position?: "top" | "right"; customIconColor?: string; + size?: number; } export default function IconTooltip({ @@ -16,6 +17,7 @@ export default function IconTooltip({ title, position = "top", customIconColor, + size, }: Props): JSX.Element { const [tooltipOffset, setTooltipOffset] = useState(); const [isMobileModalOpen, setIsMobileModalOpen] = useState(false); @@ -34,7 +36,7 @@ export default function IconTooltip({ return (
(!isWeb ? setIsMobileModalOpen(true) : null)} /> diff --git a/apps/web/src/config/index.ts b/apps/web/src/config/index.ts index a0dbfd112..66e7257f1 100644 --- a/apps/web/src/config/index.ts +++ b/apps/web/src/config/index.ts @@ -10,6 +10,7 @@ export const MAINNET_CONFIG: ContractContextI = { address: "0x54346d39976629b65ba54eac1c9ef0af3be1921b", abi: BridgeV1, }, + HotWalletAddress: "df1qgq0rjw09hr6vr7sny2m55hkr5qgze5l9hcm0lg", Erc20Tokens: { WBTC: { address: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" }, USDT: { address: "0xdAC17F958D2ee523a2206206994597C13D831ec7" }, @@ -28,6 +29,7 @@ export const TESTNET_CONFIG: ContractContextI = { address: "0x96E5E1d6377ffA08B9c08B066f430e33e3c4C9ef", abi: BridgeV1Testnet, }, + HotWalletAddress: "tf1qsckyp02vdzaf95cjl5dr95n8stcalze0pfswcp", Erc20Tokens: { WBTC: { address: "0xD723D679d1A3b23d0Aafe4C0812f61DDA84fc043" }, USDT: { address: "0xA218A0EA9a888e3f6E2dfFdf4066885f596F07bF" }, diff --git a/apps/web/src/layouts/contexts/DeFiScanContext.tsx b/apps/web/src/layouts/contexts/DeFiScanContext.tsx index 2c5cec741..f5ace5bae 100644 --- a/apps/web/src/layouts/contexts/DeFiScanContext.tsx +++ b/apps/web/src/layouts/contexts/DeFiScanContext.tsx @@ -4,6 +4,8 @@ import { useNetworkEnvironmentContext } from "./NetworkEnvironmentContext"; interface DeFiScanContextI { getTransactionUrl: (txid: string, rawtx?: string) => string; + getPOBUrl: () => string; + getAddressUrl: (address: string) => string; } const DeFiScanContext = createContext(undefined as any); @@ -45,6 +47,21 @@ export function getTxURLByNetwork( return baseUrl; } +export function getPOBURLByNetwork(network: EnvironmentNetwork): string { + let baseUrl = `${baseDefiScanUrl}/proof-of-backing/`; + baseUrl += getNetworkParams(network); + return baseUrl; +} + +export function getAddressUrlByNetwork( + network: EnvironmentNetwork, + address: string +): string { + let baseUrl = `${baseDefiScanUrl}/address/${address}`; + baseUrl += getNetworkParams(network); + return baseUrl; +} + export function getURLByNetwork( path: string, network: EnvironmentNetwork, @@ -65,6 +82,9 @@ export function DeFiScanProvider({ () => ({ getTransactionUrl: (txid: string, rawtx?: string): string => getTxURLByNetwork(networkEnv, txid, rawtx), + getPOBUrl: (): string => getPOBURLByNetwork(networkEnv), + getAddressUrl: (address: string): string => + getAddressUrlByNetwork(networkEnv, address), }), [networkEnv] ); diff --git a/apps/web/src/layouts/contexts/NetworkContext.tsx b/apps/web/src/layouts/contexts/NetworkContext.tsx index 6d1b47065..ceceab814 100644 --- a/apps/web/src/layouts/contexts/NetworkContext.tsx +++ b/apps/web/src/layouts/contexts/NetworkContext.tsx @@ -26,7 +26,7 @@ interface NetworkContextI { resetNetworkSelection: () => void; } -interface NetworkI { +export interface NetworkI { name: Network; icon: string; tokens: { diff --git a/apps/web/src/pages/liquidity/index.tsx b/apps/web/src/pages/liquidity/index.tsx new file mode 100644 index 000000000..2c16e0de4 --- /dev/null +++ b/apps/web/src/pages/liquidity/index.tsx @@ -0,0 +1,88 @@ +import Logging from "@api/logging"; +import { useState, useEffect } from "react"; +import { useDeFiScanContext } from "@contexts/DeFiScanContext"; +import { EnvironmentNetwork } from "@waveshq/walletkit-core"; +import OverviewList from "@components/OverviewList"; +import { useBridgeBalancesMutation } from "@store/index"; +import { useNetworkEnvironmentContext } from "@contexts/NetworkEnvironmentContext"; +import BASE_URLS from "../../config/networkUrl"; + +export default function LiquidityOverview({ defaultBalances }) { + const { getPOBUrl } = useDeFiScanContext(); + const { networkEnv } = useNetworkEnvironmentContext(); + + const [balances, seBalances] = useState(defaultBalances); + const [getBridgeBalance] = useBridgeBalancesMutation(); + + useEffect(() => { + getBridgeBalance({}) + .unwrap() + .then((data) => { + seBalances(data); + }); + }, [networkEnv]); + + return ( +
+
+
+

+ Liquidity overview +

+
+ + The current liquidity of each token available on Quantum. For + proof of backing, + + + view here + +
+
+ +
+
+ ); +} + +export async function getServerSideProps({ query }) { + let isBridgeUp = true; + let defaultBalances = {}; + try { + const res = await fetch( + `https://wallet.defichain.com/api/v0/bridge/status` + ); + const [data, statusCode] = await Promise.all([res.json(), res.status]); + if (statusCode === 200) { + isBridgeUp = data?.isUp; + } else { + Logging.error("Get bridge status API error."); + } + // fetch balance + const baseUrl = + BASE_URLS[query.network] ?? BASE_URLS[EnvironmentNetwork.MainNet]; + const balancesRes = await fetch(`${baseUrl}/balances`); + const [balance, balancesSC] = await Promise.all([ + balancesRes.json(), + balancesRes.status, + ]); + if (balancesSC === 200) { + defaultBalances = balance; + } else { + Logging.error("Get bridge balance API error."); + } + return { + props: { isBridgeUp, defaultBalances }, // will be passed to the page component as props + }; + } catch (e) { + Logging.error(`${e}`); + return { + props: { isBridgeUp, defaultBalances }, // will be passed to the page component as props + }; + } +} diff --git a/apps/web/src/store/defichain.tsx b/apps/web/src/store/defichain.tsx index f87172643..26af7b6db 100644 --- a/apps/web/src/store/defichain.tsx +++ b/apps/web/src/store/defichain.tsx @@ -115,6 +115,12 @@ export const bridgeApi = createApi({ method: "GET", }), }), + bridgeBalances: builder.mutation({ + query: ({ baseUrl }) => ({ + url: `${baseUrl}/balances`, + method: "GET", + }), + }), bridgeSettings: builder.query({ query: ({ baseUrl }) => ({ url: `${baseUrl}/settings`, diff --git a/apps/web/src/store/index.ts b/apps/web/src/store/index.ts index 9756953cd..10601d934 100644 --- a/apps/web/src/store/index.ts +++ b/apps/web/src/store/index.ts @@ -14,6 +14,8 @@ const useConfirmEthTxnMutation = () => useWrappedMutation(bridgeApi.useConfirmEthTxnMutation); const useAllocateDfcFundMutation = () => useWrappedMutation(bridgeApi.useAllocateDfcFundMutation); +const useBridgeBalancesMutation = () => + useWrappedMutation(bridgeApi.useBridgeBalancesMutation); const useBalanceEvmMutation = () => useWrappedMutation(bridgeApi.useBalanceEvmMutation); const useBalanceDfcMutation = () => @@ -31,6 +33,7 @@ export { useGetAddressDetailMutation, useConfirmEthTxnMutation, useAllocateDfcFundMutation, + useBridgeBalancesMutation, useBalanceEvmMutation, useBalanceDfcMutation, useLazyBridgeStatusQuery, diff --git a/apps/web/src/styles/globals.scss b/apps/web/src/styles/globals.scss index f544edfde..6b5766ff0 100644 --- a/apps/web/src/styles/globals.scss +++ b/apps/web/src/styles/globals.scss @@ -90,6 +90,8 @@ rgba(0, 0, 0, 0.5) ), linear-gradient(72.57deg, #ec008c 17.15%, #5b10ff 89.41%, #0468d6 102.56%); + + --accent-gradient-6 : linear-gradient(120.02deg, rgba(255, 255, 255, 0.25) 8.27%, rgba(223, 243, 167, 0.2) 33.42%, rgba(99, 173, 241, 0.3) 68.31%, rgba(255, 0, 175, 0.5) 103.28%); } body { @@ -125,6 +127,17 @@ body { .fill-bg-gradient-5 { background: var(--accent-gradient-5) border-box; } + .border-gradient-6 { + position: relative; + } + .border-gradient-6::before { + background: var(--accent-gradient-6); + -webkit-mask: linear-gradient(#fff 0 0) content-box, + linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + } + .dark-card-bg { background: var(--ui-dark-card-bg) border-box; } diff --git a/apps/web/src/types.ts b/apps/web/src/types.ts index d8bde0730..d267c2ea9 100644 --- a/apps/web/src/types.ts +++ b/apps/web/src/types.ts @@ -98,6 +98,7 @@ interface ContractConfigI { export interface ContractContextI { EthereumRpcUrl: string; ExplorerURL: string; + HotWalletAddress: string; BridgeV1: ContractConfigI; Erc20Tokens: Record; }