From 4557a8189a8b9e1b1a130ca2935f44a1131291d2 Mon Sep 17 00:00:00 2001 From: Keng Ye <40191153+kyleleow@users.noreply.github.com> Date: Fri, 17 Mar 2023 10:05:39 +0800 Subject: [PATCH] chore(release): 1.9.0 (#1761) * feature(proof-of-backing): add proof of backing page (#1737) * feat: add header and nav menu * refactor: list of token backed and their addresses * feat: add desktop view table * feat: add mobile view cards Refactor token backed address constants to be ready for quantum * feat: add table bottom border radius Add margin bottom to table and card * refactor: throw undefined during error Update border color of more dropdown link * feat: use numeric format for thousand separator * fix: break words on mobile net supply * chore: add try catch to getAllToken * chore: remove grid-rows * feat: add quantum backing address For dETH, dBTC, dUSDC, dUSDT * feat: add dEUROC Display quantum back address in table and card * feat: display net supply even when token is not burnt Check if token isDAT and non-LPS to follow same logic in tokens/id page Update backing page UI to include info * chore: top align net supply * chore: add link to token detail page * chore: left-align net supply table header * fix: navigating to cake backed address Add text color to token and net supply on hover * fix(dex): dirty fix poolpair link on mobile (#1760) fix: dirty fix poolpair link on mobile TODO: refactor --- src/constants/TokenBackedAddress.ts | 123 +++++++++++++++ src/layouts/components/Header.tsx | 12 +- src/pages/dex/_components/PoolPairsCards.tsx | 5 +- .../_components/BackingCard.tsx | 137 ++++++++++++++++ .../_components/BackingTable.tsx | 149 ++++++++++++++++++ src/pages/proof-of-backing/index.page.tsx | 85 ++++++++++ src/pages/tokens/[id].page.tsx | 89 ++++++----- src/pages/tokens/shared/getAllTokens.ts | 21 +++ 8 files changed, 574 insertions(+), 47 deletions(-) create mode 100644 src/constants/TokenBackedAddress.ts create mode 100644 src/pages/proof-of-backing/_components/BackingCard.tsx create mode 100644 src/pages/proof-of-backing/_components/BackingTable.tsx create mode 100644 src/pages/proof-of-backing/index.page.tsx create mode 100644 src/pages/tokens/shared/getAllTokens.ts diff --git a/src/constants/TokenBackedAddress.ts b/src/constants/TokenBackedAddress.ts new file mode 100644 index 000000000..279a7e4f7 --- /dev/null +++ b/src/constants/TokenBackedAddress.ts @@ -0,0 +1,123 @@ +interface BackedAddress { + [key: string]: { + cake: { + link: string; + address: string; + }; + quantum?: { + link: string; + address: string; + }; + }; +} + +export const TOKEN_BACKED_ADDRESS: BackedAddress = { + BTC: { + cake: { + link: "https://www.blockchain.com/btc/address/38pZuWUti3vSQuvuFYs8Lwbyje8cmaGhrT", + address: "38pZuWUti3vSQuvuFYs8Lwbyje8cmaGhrT", + }, + quantum: { + link: "https://etherscan.io/address/0x11901fd641f3a2d3a986d6745a2ff1d5fea988eb", + address: "0x11901fd641f3a2d3a986d6745a2ff1d5fea988eb", + }, + }, + ETH: { + cake: { + link: "https://etherscan.io/address/0x94fa70d079d76279e1815ce403e9b985bccc82ac", + address: "0x94fa70d079d76279e1815ce403e9b985bccc82ac", + }, + quantum: { + link: "https://etherscan.io/address/0x11901fd641f3a2d3a986d6745a2ff1d5fea988eb", + address: "0x11901fd641f3a2d3a986d6745a2ff1d5fea988eb", + }, + }, + USDT: { + cake: { + link: "https://etherscan.io/address/0x94fa70d079d76279e1815ce403e9b985bccc82ac", + address: "0x94fa70d079d76279e1815ce403e9b985bccc82ac", + }, + quantum: { + link: "https://etherscan.io/address/0x11901fd641f3a2d3a986d6745a2ff1d5fea988eb", + address: "0x11901fd641f3a2d3a986d6745a2ff1d5fea988eb", + }, + }, + USDC: { + cake: { + link: "https://etherscan.io/address/0x94fa70d079d76279e1815ce403e9b985bccc82ac", + address: "0x94fa70d079d76279e1815ce403e9b985bccc82ac", + }, + quantum: { + link: "https://etherscan.io/address/0x11901fd641f3a2d3a986d6745a2ff1d5fea988eb", + address: "0x11901fd641f3a2d3a986d6745a2ff1d5fea988eb", + }, + }, + EUROC: { + cake: { + link: "https://etherscan.io/address/0x94fa70d079d76279e1815ce403e9b985bccc82ac", + address: "0x94fa70d079d76279e1815ce403e9b985bccc82ac", + }, + quantum: { + link: "https://etherscan.io/address/0x11901fd641f3a2d3a986d6745a2ff1d5fea988eb", + address: "0x11901fd641f3a2d3a986d6745a2ff1d5fea988eb", + }, + }, + LTC: { + cake: { + link: "https://live.blockcypher.com/ltc/address/MLYQxJfnUfVqRwfYXjDJfmLbyA77hqzSXE", + address: "MLYQxJfnUfVqRwfYXjDJfmLbyA77hqzSXE", + }, + }, + BCH: { + cake: { + link: "https://www.blockchain.com/bch/address/38wFczGqaaGLRub2U7CWeWkMuPDwhMVMRf", + address: "38wFczGqaaGLRub2U7CWeWkMuPDwhMVMRf", + }, + }, + DOGE: { + cake: { + link: "https://dogechain.info/address/D7jrXDgPYck8jL9eYvRrc7Ze8n2e2Loyba", + address: "D7jrXDgPYck8jL9eYvRrc7Ze8n2e2Loyba", + }, + }, +}; + +interface TokenBacked { + name: string; + symbol: string; +} + +export const TOKEN_BACKED: TokenBacked[] = [ + { + name: "dBTC", + symbol: "BTC", + }, + { + name: "dETH", + symbol: "ETH", + }, + { + name: "dUSDT", + symbol: "USDT", + }, + { + name: "dUSDC", + symbol: "USDC", + }, + { + name: "dEUROC", + symbol: "EUROC", + }, + { + name: "dLTC", + symbol: "LTC", + }, + { + name: "dBCH", + symbol: "BCH", + }, + { + name: "dDOGE", + symbol: "DOGE", + }, +]; diff --git a/src/layouts/components/Header.tsx b/src/layouts/components/Header.tsx index 7910bc91a..f319cf334 100644 --- a/src/layouts/components/Header.tsx +++ b/src/layouts/components/Header.tsx @@ -522,7 +522,7 @@ const DropDownLink = React.forwardRef( href={item.link} onClick={close} className={classNames( - "px-6 py-3.5 cursor-pointer text-sm border-gray-200 hover:text-primary-500 dark:hover:text-dark-50", + "px-6 py-3.5 cursor-pointer text-sm border-gray-200 dark:border-dark-gray-300 border-b-[0.5px] hover:text-primary-500 dark:hover:text-dark-50", { "dark:text-dark-50 text-primary-500": routerPathName.includes( item.rootPathName @@ -548,6 +548,11 @@ const dropDownLinks = [ link: "/tokens", rootPathName: "tokens", }, + { + name: "Proof of Backing", + link: "/proof-of-backing", + rootPathName: "proof-of-backing", + }, ]; let drawerMenuItemLinks = [ @@ -595,4 +600,9 @@ let drawerMenuItemLinks = [ pathname: "/tokens", testId: "Tokens", }, + { + text: "Proof of Backing", + pathname: "/proof-of-backing", + testId: "/proof-of-backing", + }, ]; diff --git a/src/pages/dex/_components/PoolPairsCards.tsx b/src/pages/dex/_components/PoolPairsCards.tsx index 52cd58c4e..38959c8ac 100644 --- a/src/pages/dex/_components/PoolPairsCards.tsx +++ b/src/pages/dex/_components/PoolPairsCards.tsx @@ -127,7 +127,10 @@ export function PoolPairsCard({ path={`/dex/${ poolPair.displaySymbol.includes("DUSD") || poolPair.displaySymbol.includes("dUSDT") || - poolPair.displaySymbol.includes("dUSDC") + poolPair.displaySymbol.includes("dUSDC") || + poolPair.displaySymbol.includes("dBTC") || + poolPair.displaySymbol.includes("dETH") || + poolPair.displaySymbol.includes("dEUROC") ? poolPair.displaySymbol : poolPair.tokenA.displaySymbol }`} diff --git a/src/pages/proof-of-backing/_components/BackingCard.tsx b/src/pages/proof-of-backing/_components/BackingCard.tsx new file mode 100644 index 000000000..04940d0ad --- /dev/null +++ b/src/pages/proof-of-backing/_components/BackingCard.tsx @@ -0,0 +1,137 @@ +import { Link } from "@components/commons/link/Link"; +import { InfoHoverPopover } from "@components/commons/popover/InfoHoverPopover"; +import { getAssetIcon } from "@components/icons/assets/tokens"; +import classNames from "classnames"; +import { TOKEN_BACKED_ADDRESS } from "constants/TokenBackedAddress"; +import React, { useState } from "react"; +import { + MdOutlineKeyboardArrowUp, + MdOutlineKeyboardArrowDown, +} from "react-icons/md"; +import { NumericFormat } from "react-number-format"; +import { TokenWithBacking } from "../index.page"; + +export function BackingCard({ + token, +}: { + token: TokenWithBacking; +}): JSX.Element { + const [isExpanded, setIsExpanded] = useState(false); + const Icon = getAssetIcon(token.symbol); + const backedAddress = TOKEN_BACKED_ADDRESS[token.symbol]; + + return ( +
+
+
+ + + {token.displaySymbol} + +
+
+ + +
+ VIEW +
+
+ +
setIsExpanded(!isExpanded)} + data-testid="OnChainGovernance.CardView.Toggle" + > + {!isExpanded ? ( + + ) : ( + + )} +
+
+
+ {isExpanded && ( +
+
+
+ Net Supply + +
+
+ {token.netSupply === undefined ? ( + "N/A" + ) : ( + + )} +
+
+
+ Backing Address + +
+
+
+ Cake +
+ {backedAddress.cake !== undefined ? ( + + {backedAddress.cake.address} + + ) : ( +
+ N/A +
+ )} +
+
+
+ Quantum + +
+ {backedAddress.quantum !== undefined ? ( + + {backedAddress.quantum.address} + + ) : ( +
+ N/A +
+ )} +
+
+ )} +
+ ); +} diff --git a/src/pages/proof-of-backing/_components/BackingTable.tsx b/src/pages/proof-of-backing/_components/BackingTable.tsx new file mode 100644 index 000000000..6f7a55fa7 --- /dev/null +++ b/src/pages/proof-of-backing/_components/BackingTable.tsx @@ -0,0 +1,149 @@ +import { Link } from "@components/commons/link/Link"; +import { InfoHoverPopover } from "@components/commons/popover/InfoHoverPopover"; +import { getAssetIcon } from "@components/icons/assets/tokens"; +import classNames from "classnames"; +import { TOKEN_BACKED_ADDRESS } from "constants/TokenBackedAddress"; +import { NumericFormat } from "react-number-format"; +import { TokenWithBacking } from "../index.page"; + +export function BackingTable({ + tokens, +}: { + tokens: TokenWithBacking[]; +}): JSX.Element { + return ( +
+ + <> + {tokens.map((token, index) => ( + + ))} + +
+ ); +} + +function TableHeader(): JSX.Element { + return ( + <> +
+ Token +
+
+ Net Supply + +
+
+ Backing Address + +
+
+ Cake +
+
+ Quantum + +
+ + ); +} + +function TableRow({ + token, + isLast, +}: { + token: TokenWithBacking; + isLast?: boolean; +}): JSX.Element { + const backedAddress = TOKEN_BACKED_ADDRESS[token.symbol]; + const Icon = getAssetIcon(token.symbol); + return ( + + +
+ + + + {token.displaySymbol} + + +
+
+ {token.netSupply === undefined ? ( + "N/A" + ) : ( + + )} +
+ {backedAddress.cake !== undefined ? ( + { + e.stopPropagation(); + }} + > + {backedAddress.cake.address} + + ) : ( +
+ N/A +
+ )} + {backedAddress.quantum !== undefined ? ( + { + e.stopPropagation(); + }} + > + {backedAddress.quantum.address} + + ) : ( +
+ N/A +
+ )} + + + ); +} diff --git a/src/pages/proof-of-backing/index.page.tsx b/src/pages/proof-of-backing/index.page.tsx new file mode 100644 index 000000000..2ea365e19 --- /dev/null +++ b/src/pages/proof-of-backing/index.page.tsx @@ -0,0 +1,85 @@ +import { + GetServerSidePropsContext, + GetServerSidePropsResult, + InferGetServerSidePropsType, +} from "next"; +import { Container } from "@components/commons/Container"; +import { getWhaleApiClient } from "@contexts/WhaleContext"; +import { getAllTokens } from "pages/tokens/shared/getAllTokens"; +import BigNumber from "bignumber.js"; +import { TOKEN_BACKED } from "constants/TokenBackedAddress"; +import { BackingTable } from "./_components/BackingTable"; +import { BackingCard } from "./_components/BackingCard"; + +interface ProofOfBackingPageProps { + tokens: TokenWithBacking[]; +} + +export interface TokenWithBacking { + symbol: string; + displaySymbol: string; + netSupply?: string; +} + +export default function ProofOfBackingPage( + props: InferGetServerSidePropsType +): JSX.Element { + return ( + +
+

+ Proof of Backing +

+ + All tokens have backed collateral from which they are minted. See + proof of the backed amount on the addresses below. + +
+
+ +
+
+ {props.tokens.map((token) => ( + + ))} +
+
+ ); +} + +export async function getServerSideProps( + context: GetServerSidePropsContext +): Promise> { + const api = getWhaleApiClient(context); + const tokenList = await getAllTokens(api); + const burntTokenList = await api.address + .listToken("8defichainBurnAddressXXXXXXXdRQkSm") + .catch(() => {}); + const result: TokenWithBacking[] = []; + TOKEN_BACKED.forEach((token) => { + const _token = tokenList.find((t) => t.displaySymbol === token.name); + const _burntToken = burntTokenList?.find( + (t) => t.displaySymbol === token.name + ); + if (_token !== undefined) { + result.push({ + displaySymbol: _token.displaySymbol, + symbol: _token.symbol, + netSupply: BigNumber(_token.minted) + .minus(_burntToken?.amount ?? 0) + .toFixed(8), + }); + } else { + result.push({ + displaySymbol: token.name, + symbol: token.symbol, + }); + } + }); + + return { + props: { + tokens: result, + }, + }; +} diff --git a/src/pages/tokens/[id].page.tsx b/src/pages/tokens/[id].page.tsx index d7e2776a9..4b8374ff2 100644 --- a/src/pages/tokens/[id].page.tsx +++ b/src/pages/tokens/[id].page.tsx @@ -17,8 +17,13 @@ import BigNumber from "bignumber.js"; import { NumericFormat } from "react-number-format"; import { Head } from "@components/commons/Head"; import { WhaleApiClient } from "@defichain/whale-api-client"; +import { + TOKEN_BACKED, + TOKEN_BACKED_ADDRESS, +} from "constants/TokenBackedAddress"; import { getTokenName } from "../../utils/commons/token/getTokenName"; import { isAlphanumeric, isNumeric } from "../../utils/commons/StringValidator"; +import { getAllTokens } from "./shared/getAllTokens"; interface TokenAssetPageProps { token: TokenData; @@ -28,39 +33,48 @@ export default function TokenIdPage( props: InferGetServerSidePropsType ): JSX.Element { const api = useWhaleApiClient(); - const [burnedAmount, setBurnedAmount] = useState( - new BigNumber(0) - ); - const [netSupply, setNetSupply] = useState( - new BigNumber(0) - ); + const [burnedAmount, setBurnedAmount] = useState(); + const [netSupply, setNetSupply] = useState(); useEffect(() => { api.address .listToken("8defichainBurnAddressXXXXXXXdRQkSm") .then((data) => { - const filteredTokens = data.filter( + const burntToken = data.find( (token) => token.symbol === props.token.symbol ); - if (filteredTokens.length === 1 && filteredTokens[0].symbol !== "DFI") { - setBurnedAmount(new BigNumber(filteredTokens[0].amount)); - setNetSupply( - new BigNumber(props.token.minted).minus(filteredTokens[0].amount) - ); - } else { - setBurnedAmount(undefined); - setNetSupply(undefined); + + if ( + props.token.isDAT && + !props.token.isLPS && + props.token.symbol !== "DFI" + ) { + if (burntToken !== undefined) { + setBurnedAmount(new BigNumber(burntToken.amount)); + setNetSupply( + new BigNumber(props.token.minted).minus(burntToken.amount) + ); + } else { + setBurnedAmount(new BigNumber(0)); + setNetSupply(new BigNumber(props.token.minted)); + } } }) .catch(() => { - setNetSupply(undefined); + if ( + props.token.isDAT && + !props.token.isLPS && + props.token.symbol !== "DFI" + ) { + setBurnedAmount(undefined); + setNetSupply(new BigNumber(props.token.minted)); + } }); }, []); return ( <> -
@@ -258,17 +272,7 @@ function ListLeft({ } function BackingAddress({ tokenSymbol }: { tokenSymbol: string }): JSX.Element { - const tokensWithBackingAddress = [ - "BCH", - "LTC", - "DOGE", - "BTC", - "ETH", - "USDC", - "USDT", - ]; - - if (!tokensWithBackingAddress.includes(tokenSymbol)) { + if (!TOKEN_BACKED.map((token) => token.symbol).includes(tokenSymbol)) { return <>; } @@ -279,42 +283,43 @@ function BackingAddress({ tokenSymbol }: { tokenSymbol: string }): JSX.Element { case "BCH": return ( ); case "LTC": return ( ); case "DOGE": return ( ); case "BTC": return ( ); case "ETH": case "USDC": case "USDT": + case "EUROC": return ( ); @@ -328,14 +333,8 @@ async function getTokenByParam( param: string, api: WhaleApiClient ): Promise { - const tokenList: TokenData[] = []; + const tokenList: TokenData[] = await getAllTokens(api); - let tokenResponse = await api.tokens.list(200); - tokenList.push(...tokenResponse); - while (tokenResponse.hasNext) { - tokenResponse = await api.tokens.list(200, tokenResponse.nextToken); - tokenList.push(...tokenResponse); - } return tokenList.find((t) => { if (t.isDAT || t.isLPS) { return t.displaySymbol.toLowerCase() === param.toLowerCase(); diff --git a/src/pages/tokens/shared/getAllTokens.ts b/src/pages/tokens/shared/getAllTokens.ts new file mode 100644 index 000000000..10ed31edf --- /dev/null +++ b/src/pages/tokens/shared/getAllTokens.ts @@ -0,0 +1,21 @@ +import { WhaleApiClient } from "@defichain/whale-api-client"; +import { TokenData } from "@defichain/whale-api-client/dist/api/tokens"; + +export async function getAllTokens(api: WhaleApiClient) { + const result: TokenData[] = []; + let hasNext = false; + let nextToken: string | undefined; + + try { + do { + const tokenResponse = await api.tokens.list(200, nextToken); + hasNext = tokenResponse.hasNext; + nextToken = tokenResponse.nextToken; + result.push(...tokenResponse); + } while (hasNext); + } catch (e) { + console.error("Error while retrieving all tokens", e); + } + + return result; +}