From 5770720b5f19731f7465b1e502d87a0f9466c5ef Mon Sep 17 00:00:00 2001 From: saml33 Date: Mon, 27 Feb 2023 14:09:31 +1100 Subject: [PATCH 01/71] add share positions to twitter --- components/icons/PositionShareIcon.tsx | 89 +++++ components/icons/TwitterIcon.tsx | 17 + components/modals/SharePositionModal.tsx | 178 ++++++++++ components/shared/SideBadge.tsx | 7 +- components/trade/PerpPositions.tsx | 428 +++++++++++++---------- components/trade/PerpSideBadge.tsx | 2 +- hooks/useScreenshot.tsx | 79 +++++ package.json | 1 + public/images/space.svg | 71 ++++ public/images/underwater.svg | 183 ++++++++++ public/locales/en/trade.json | 3 + public/locales/es/trade.json | 3 + public/locales/ru/trade.json | 3 + public/locales/zh/trade.json | 4 + public/locales/zh_tw/trade.json | 4 + public/logos/logo-with-text.svg | 52 +++ tailwind.config.js | 5 +- yarn.lock | 34 ++ 18 files changed, 963 insertions(+), 200 deletions(-) create mode 100644 components/icons/PositionShareIcon.tsx create mode 100644 components/icons/TwitterIcon.tsx create mode 100644 components/modals/SharePositionModal.tsx create mode 100644 hooks/useScreenshot.tsx create mode 100644 public/images/space.svg create mode 100644 public/images/underwater.svg create mode 100644 public/logos/logo-with-text.svg diff --git a/components/icons/PositionShareIcon.tsx b/components/icons/PositionShareIcon.tsx new file mode 100644 index 00000000..30f12cec --- /dev/null +++ b/components/icons/PositionShareIcon.tsx @@ -0,0 +1,89 @@ +const PositionShareIcon = ({ className }: { className?: string }) => { + return ( + + + + + + + + + + + + + + + + + + + + + + ) +} + +export default PositionShareIcon diff --git a/components/icons/TwitterIcon.tsx b/components/icons/TwitterIcon.tsx new file mode 100644 index 00000000..57203b62 --- /dev/null +++ b/components/icons/TwitterIcon.tsx @@ -0,0 +1,17 @@ +export const TwitterIcon = ({ className }: { className?: string }) => { + return ( + + + + ) +} diff --git a/components/modals/SharePositionModal.tsx b/components/modals/SharePositionModal.tsx new file mode 100644 index 00000000..06b55405 --- /dev/null +++ b/components/modals/SharePositionModal.tsx @@ -0,0 +1,178 @@ +import { Group, PerpPosition } from '@blockworks-foundation/mango-v4' +import { TwitterIcon } from '@components/icons/TwitterIcon' +import Button from '@components/shared/Button' +import { Dialog } from '@headlessui/react' +import { DocumentDuplicateIcon } from '@heroicons/react/20/solid' +import { useScreenshot } from 'hooks/useScreenshot' +import { useTranslation } from 'next-i18next' +import { createRef, useEffect, useMemo, useState } from 'react' +import { ModalProps } from 'types/modal' +import { formatNumericValue, getDecimalCount } from 'utils/numbers' + +interface SharePositionModalProps { + group: Group + position: PerpPosition +} + +type ModalCombinedProps = SharePositionModalProps & ModalProps + +async function copyToClipboard(image: any) { + try { + image.toBlob((blob: any) => { + navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]) + }, 'image/png') + } catch (error) { + console.error(error) + } +} + +const SharePositionModal = ({ + group, + isOpen, + onClose, + position, +}: ModalCombinedProps) => { + const { t } = useTranslation('trade') + const ref = createRef() + const [copied, setCopied] = useState(false) + const [showButton, setShowButton] = useState(true) + const [image, takeScreenshot] = useScreenshot() + const market = group.getPerpMarketByMarketIndex(position.marketIndex) + const basePosition = position.getBasePositionUi(market) + const entryPrice = position.getAverageEntryPriceUi(market) + const side = basePosition > 0 ? 'long' : 'short' + + const roi = useMemo(() => { + if (!market) return 0 + const indexPrice = market.uiPrice + const roi = ((indexPrice - entryPrice) / entryPrice) * 100 + return roi + }, [market, entryPrice]) + + useEffect(() => { + if (image) { + copyToClipboard(image) + setCopied(true) + setShowButton(true) + } + }, [image]) + + useEffect(() => { + // if the button is hidden we are taking a screenshot + if (!showButton) { + takeScreenshot(ref.current as HTMLElement) + } + }, [showButton]) + + const handleCopyToClipboard = () => { + setShowButton(false) + } + + return ( + + + ) +} + +export default SharePositionModal diff --git a/components/shared/SideBadge.tsx b/components/shared/SideBadge.tsx index 128b352e..65bf3709 100644 --- a/components/shared/SideBadge.tsx +++ b/components/shared/SideBadge.tsx @@ -1,12 +1,13 @@ import React, { FunctionComponent } from 'react' type SideBadgeProps = { + isPerp?: boolean side: string } -const SideBadge: FunctionComponent = ({ side }) => { +const SideBadge: FunctionComponent = ({ side, isPerp }) => { if (side !== 'buy' && side !== 'sell') { - return
Unknown
+ return
Unknown
} const isBid = side === 'buy' @@ -20,7 +21,7 @@ const SideBadge: FunctionComponent = ({ side }) => { } uppercase md:-my-0.5 md:px-1.5 md:py-0.5 md:text-xs`} > - {isBid ? 'Buy' : 'Sell'} + {isPerp ? (isBid ? 'long' : 'short') : side} ) } diff --git a/components/trade/PerpPositions.tsx b/components/trade/PerpPositions.tsx index dfd64b44..960091fe 100644 --- a/components/trade/PerpPositions.tsx +++ b/components/trade/PerpPositions.tsx @@ -1,5 +1,7 @@ import { PerpMarket, PerpPosition } from '@blockworks-foundation/mango-v4' -import Button, { LinkButton } from '@components/shared/Button' +import { TwitterIcon } from '@components/icons/TwitterIcon' +import SharePositionModal from '@components/modals/SharePositionModal' +import Button, { IconButton, LinkButton } from '@components/shared/Button' import ConnectEmptyState from '@components/shared/ConnectEmptyState' import FormatNumericValue from '@components/shared/FormatNumericValue' import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements' @@ -27,6 +29,10 @@ const PerpPositions = () => { const [positionToClose, setPositionToClose] = useState( null ) + const [showShareModal, setShowShareModal] = useState(false) + const [positionToShare, setPositionToShare] = useState( + null + ) const perpPositions = mangoStore((s) => s.mangoAccount.perpPositions) const { selectedMarket } = useSelectedMarket() const { connected } = useWallet() @@ -71,222 +77,260 @@ const PerpPositions = () => { setPositionToClose(null) }, []) + const handleShowShare = (position: PerpPosition) => { + setPositionToShare(position) + setShowShareModal(true) + } + if (!group) return null const openPerpPositions = Object.values(perpPositions).filter((p) => p.basePositionLots.toNumber() ) - return mangoAccountAddress && openPerpPositions.length ? ( - showTableView ? ( - <> -
- - - - - - - - - - - {!isUnownedAccount ? - - {openPerpPositions.map((position) => { - const market = group.getPerpMarketByMarketIndex( - position.marketIndex - ) - const basePosition = position.getBasePositionUi(market) - const floorBasePosition = floorToDecimal( - basePosition, - getDecimalCount(market.minOrderSize) - ).toNumber() - const isSelectedMarket = - selectedMarket instanceof PerpMarket && - selectedMarket.perpMarketIndex === position.marketIndex + return ( + <> + {mangoAccountAddress && openPerpPositions.length ? ( + showTableView ? ( + <> +
+
{t('market')}{t('trade:side')}{t('trade:size')}{t('trade:notional')}{t('trade:entry-price')}{`${t('trade:unsettled')} ${t( - 'pnl' - )}`}{t('pnl')} : null} - -
+ + + + + + + + + + {isUnownedAccount ? + + {openPerpPositions.map((position, index) => { + const market = group.getPerpMarketByMarketIndex( + position.marketIndex + ) + const basePosition = position.getBasePositionUi(market) + const floorBasePosition = floorToDecimal( + basePosition, + getDecimalCount(market.minOrderSize) + ).toNumber() + const isSelectedMarket = + selectedMarket instanceof PerpMarket && + selectedMarket.perpMarketIndex === position.marketIndex - if (!basePosition) return null + if (!basePosition) return null - const unsettledPnl = position.getUnsettledPnlUi(market) - const cummulativePnl = - position.cumulativePnlOverPositionLifetimeUi(market) + const unsettledPnl = position.getUnsettledPnlUi(market) + const cummulativePnl = + position.cumulativePnlOverPositionLifetimeUi(market) - return ( - - - - + + + + + + + {isUnownedAccount ? ( + + ) : null} + + ) + })} + +
{t('market')}{t('trade:side')}{t('trade:size')}{t('trade:notional')}{t('trade:entry-price')}{`${t('trade:unsettled')} ${t( + 'pnl' + )}`}{t('pnl')} : null} + +
- - - - -

- {isSelectedMarket ? ( - - handlePositionClick(floorBasePosition, market) + return ( + +

+ + + + +

+ {isSelectedMarket ? ( + + handlePositionClick(floorBasePosition, market) + } + > + + + ) : ( + + )} +

+
+ + isUsd + /> + + + + + 0 ? 'text-th-up' : 'text-th-down' + }`} + > + + +
+ + + handleShowShare(openPerpPositions[index]) + } + disabled={!group || !basePosition} + > + + +
+
+
+ {showMarketCloseModal && positionToClose ? ( + + ) : null} + + ) : ( + <> + {openPerpPositions.map((position) => { + const market = group.getPerpMarketByMarketIndex( + position.marketIndex + ) + const basePosition = position.getBasePositionUi(market) + const floorBasePosition = floorToDecimal( + basePosition, + getDecimalCount(market.minOrderSize) + ).toNumber() + const isSelectedMarket = + selectedMarket instanceof PerpMarket && + selectedMarket.perpMarketIndex === position.marketIndex + + if (!basePosition) return null + const cummulativePnl = + position.cumulativePnlOverPositionLifetimeUi(market) + return ( +
+
+ +
+ +

+ + {isSelectedMarket ? ( + + handlePositionClick(floorBasePosition, market) + } + > + + + ) : ( - - ) : ( + )} + + {' at '} + - )} +

- - - - - - - - - - - +
+
+
0 ? 'text-th-up' : 'text-th-down' }`} > - +
{!isUnownedAccount ? ( - - - + ) : null} - - ) - })} - - +
+
+ ) + })} + + ) + ) : mangoAccountAddress || connected ? ( +
+ +

{t('trade:no-positions')}

- {showMarketCloseModal && positionToClose ? ( - - ) : null} - - ) : ( - <> - {openPerpPositions.map((position) => { - const market = group.getPerpMarketByMarketIndex(position.marketIndex) - const basePosition = position.getBasePositionUi(market) - const floorBasePosition = floorToDecimal( - basePosition, - getDecimalCount(market.minOrderSize) - ).toNumber() - const isSelectedMarket = - selectedMarket instanceof PerpMarket && - selectedMarket.perpMarketIndex === position.marketIndex - - if (!basePosition) return null - const cummulativePnl = - position.cumulativePnlOverPositionLifetimeUi(market) - return ( -
-
- -
- -

- - {isSelectedMarket ? ( - - handlePositionClick(floorBasePosition, market) - } - > - - - ) : ( - - )} - - {' at '} - - - -

-
-
-
-
0 ? 'text-th-up' : 'text-th-down' - }`} - > - -
- {!isUnownedAccount ? ( - - ) : null} -
-
- ) - })} - - ) - ) : mangoAccountAddress || connected ? ( -
- -

{t('trade:no-positions')}

-
- ) : ( -
- -
+ ) : ( +
+ +
+ )} + {showShareModal ? ( + setShowShareModal(false)} + position={positionToShare!} + /> + ) : null} + ) } diff --git a/components/trade/PerpSideBadge.tsx b/components/trade/PerpSideBadge.tsx index fce1c8c5..8e8d891c 100644 --- a/components/trade/PerpSideBadge.tsx +++ b/components/trade/PerpSideBadge.tsx @@ -4,7 +4,7 @@ const PerpSideBadge = ({ basePosition }: { basePosition: number }) => { return ( <> {basePosition !== 0 ? ( - 0 ? 'buy' : 'sell'} /> + 0 ? 'buy' : 'sell'} isPerp /> ) : ( '--' )} diff --git a/hooks/useScreenshot.tsx b/hooks/useScreenshot.tsx new file mode 100644 index 00000000..7c387c3b --- /dev/null +++ b/hooks/useScreenshot.tsx @@ -0,0 +1,79 @@ +import { useState } from 'react' +import html2canvas from 'html2canvas' + +/** + * @module Main_Hook + * Hook return + * @typedef {Array} HookReturn + * @property {string} HookReturn[0] - image string + * @property {string} HookReturn[1] - take screen shot string + * @property {object} HookReturn[2] - errors + */ + +/** + * hook for creating screenshot from html node + * @returns {HookReturn} + */ +const useScreenshot: () => [ + HTMLCanvasElement | null, + (node: HTMLElement) => Promise, + { error: null } +] = () => { + const [image, setImage] = useState(null) + const [error, setError] = useState(null) + /** + * convert html node to image + * @param {HTMLElement} node + */ + const takeScreenshot = (node: HTMLElement) => { + if (!node) { + throw new Error('You should provide correct html node.') + } + return html2canvas(node) + .then((canvas) => { + const croppedCanvas = document.createElement('canvas') + const croppedCanvasContext = croppedCanvas.getContext('2d') + // init data + const cropPositionTop = 0 + const cropPositionLeft = 0 + const cropWidth = canvas.width + const cropHeight = canvas.height + + croppedCanvas.width = cropWidth + croppedCanvas.height = cropHeight + + croppedCanvasContext?.drawImage( + canvas, + cropPositionLeft, + cropPositionTop + ) + + setImage(croppedCanvas) + return croppedCanvas + }) + .catch(setError) + } + + return [ + image, + takeScreenshot, + { + error, + }, + ] +} + +/** + * creates name of file + * @param {string} extension + * @param {string[]} parts of file name + */ +const createFileName = (extension = '', ...names: any) => { + if (!extension) { + return '' + } + + return `${names.join('')}.${extension}` +} + +export { useScreenshot, createFileName } diff --git a/package.json b/package.json index 80406036..aacd656f 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "decimal.js": "10.4.0", "howler": "2.2.3", "html-react-parser": "3.0.4", + "html2canvas": "1.4.1", "immer": "9.0.12", "klinecharts": "8.6.3", "lodash": "4.17.21", diff --git a/public/images/space.svg b/public/images/space.svg new file mode 100644 index 00000000..f9ff40df --- /dev/null +++ b/public/images/space.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/underwater.svg b/public/images/underwater.svg new file mode 100644 index 00000000..de025a5a --- /dev/null +++ b/public/images/underwater.svg @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/locales/en/trade.json b/public/locales/en/trade.json index b8e314f8..83c8b2d5 100644 --- a/public/locales/en/trade.json +++ b/public/locales/en/trade.json @@ -11,6 +11,8 @@ "connect-positions": "Connect to view your perp positions", "connect-trade-history": "Connect to view your trade history", "connect-unsettled": "Connect to view your unsettled funds", + "copy-and-share": "Copy Image to Clipboard", + "current-price": "Current Price", "entry-price": "Entry Price", "est-slippage": "Est. Slippage", "funding-rate": "Funding Rate", @@ -62,6 +64,7 @@ "tooltip-stable-price": "Stable price is used in a safety mechanism that limits a user's ability to enter risky positions when the oracle price is changing rapidly", "trade-sounds-tooltip": "Play a sound alert for every new trade", "trades": "Trades", + "tweet-position": "Share to Twitter", "unsettled": "Unsettled", "volume-alert": "Volume Alert", "volume-alert-desc": "Play a sound whenever volume exceeds your alert threshold" diff --git a/public/locales/es/trade.json b/public/locales/es/trade.json index b8e314f8..83c8b2d5 100644 --- a/public/locales/es/trade.json +++ b/public/locales/es/trade.json @@ -11,6 +11,8 @@ "connect-positions": "Connect to view your perp positions", "connect-trade-history": "Connect to view your trade history", "connect-unsettled": "Connect to view your unsettled funds", + "copy-and-share": "Copy Image to Clipboard", + "current-price": "Current Price", "entry-price": "Entry Price", "est-slippage": "Est. Slippage", "funding-rate": "Funding Rate", @@ -62,6 +64,7 @@ "tooltip-stable-price": "Stable price is used in a safety mechanism that limits a user's ability to enter risky positions when the oracle price is changing rapidly", "trade-sounds-tooltip": "Play a sound alert for every new trade", "trades": "Trades", + "tweet-position": "Share to Twitter", "unsettled": "Unsettled", "volume-alert": "Volume Alert", "volume-alert-desc": "Play a sound whenever volume exceeds your alert threshold" diff --git a/public/locales/ru/trade.json b/public/locales/ru/trade.json index b8e314f8..83c8b2d5 100644 --- a/public/locales/ru/trade.json +++ b/public/locales/ru/trade.json @@ -11,6 +11,8 @@ "connect-positions": "Connect to view your perp positions", "connect-trade-history": "Connect to view your trade history", "connect-unsettled": "Connect to view your unsettled funds", + "copy-and-share": "Copy Image to Clipboard", + "current-price": "Current Price", "entry-price": "Entry Price", "est-slippage": "Est. Slippage", "funding-rate": "Funding Rate", @@ -62,6 +64,7 @@ "tooltip-stable-price": "Stable price is used in a safety mechanism that limits a user's ability to enter risky positions when the oracle price is changing rapidly", "trade-sounds-tooltip": "Play a sound alert for every new trade", "trades": "Trades", + "tweet-position": "Share to Twitter", "unsettled": "Unsettled", "volume-alert": "Volume Alert", "volume-alert-desc": "Play a sound whenever volume exceeds your alert threshold" diff --git a/public/locales/zh/trade.json b/public/locales/zh/trade.json index bbade51d..83c8b2d5 100644 --- a/public/locales/zh/trade.json +++ b/public/locales/zh/trade.json @@ -11,6 +11,8 @@ "connect-positions": "Connect to view your perp positions", "connect-trade-history": "Connect to view your trade history", "connect-unsettled": "Connect to view your unsettled funds", + "copy-and-share": "Copy Image to Clipboard", + "current-price": "Current Price", "entry-price": "Entry Price", "est-slippage": "Est. Slippage", "funding-rate": "Funding Rate", @@ -43,6 +45,7 @@ "preview-sound": "Preview Sound", "price-expect": "The price you receive may be worse than you expect and full execution is not guaranteed. Max slippage is 2.5% for your safety. The part of your position with slippage beyond 2.5% will not be closed.", "quote": "Quote", + "reduce-only": "Reduce Only", "sells": "Sells", "settle-funds": "Settle Funds", "settle-funds-error": "Failed to settle funds", @@ -61,6 +64,7 @@ "tooltip-stable-price": "Stable price is used in a safety mechanism that limits a user's ability to enter risky positions when the oracle price is changing rapidly", "trade-sounds-tooltip": "Play a sound alert for every new trade", "trades": "Trades", + "tweet-position": "Share to Twitter", "unsettled": "Unsettled", "volume-alert": "Volume Alert", "volume-alert-desc": "Play a sound whenever volume exceeds your alert threshold" diff --git a/public/locales/zh_tw/trade.json b/public/locales/zh_tw/trade.json index bbade51d..83c8b2d5 100644 --- a/public/locales/zh_tw/trade.json +++ b/public/locales/zh_tw/trade.json @@ -11,6 +11,8 @@ "connect-positions": "Connect to view your perp positions", "connect-trade-history": "Connect to view your trade history", "connect-unsettled": "Connect to view your unsettled funds", + "copy-and-share": "Copy Image to Clipboard", + "current-price": "Current Price", "entry-price": "Entry Price", "est-slippage": "Est. Slippage", "funding-rate": "Funding Rate", @@ -43,6 +45,7 @@ "preview-sound": "Preview Sound", "price-expect": "The price you receive may be worse than you expect and full execution is not guaranteed. Max slippage is 2.5% for your safety. The part of your position with slippage beyond 2.5% will not be closed.", "quote": "Quote", + "reduce-only": "Reduce Only", "sells": "Sells", "settle-funds": "Settle Funds", "settle-funds-error": "Failed to settle funds", @@ -61,6 +64,7 @@ "tooltip-stable-price": "Stable price is used in a safety mechanism that limits a user's ability to enter risky positions when the oracle price is changing rapidly", "trade-sounds-tooltip": "Play a sound alert for every new trade", "trades": "Trades", + "tweet-position": "Share to Twitter", "unsettled": "Unsettled", "volume-alert": "Volume Alert", "volume-alert-desc": "Play a sound whenever volume exceeds your alert threshold" diff --git a/public/logos/logo-with-text.svg b/public/logos/logo-with-text.svg new file mode 100644 index 00000000..3c82dd00 --- /dev/null +++ b/public/logos/logo-with-text.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tailwind.config.js b/tailwind.config.js index abea19bd..a285b38f 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -15,10 +15,7 @@ module.exports = { 'spin-fast': 'spin 0.5s linear infinite', }, backgroundImage: { - 'long-loss': "url('/share_images/bg-long-loss.png')", - 'long-profit': "url('/share_images/bg-long-profit.png')", - 'short-loss': "url('/share_images/bg-short-loss.png')", - 'short-profit': "url('/share_images/bg-short-profit.png')", + 'share-position': "url('/images/share-image.png')", }, boxShadow: { bottomBar: '0px -4px 8px -1px rgba(0,0,0,0.2)', diff --git a/yarn.lock b/yarn.lock index c8e192ca..ee88a5a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2306,6 +2306,11 @@ base58check@^2.0.0: dependencies: bs58 "^3.0.0" +base64-arraybuffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc" + integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ== + base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -2896,6 +2901,13 @@ crypto-js@^4.1.1: resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf" integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw== +css-line-break@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0" + integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w== + dependencies: + utrie "^1.0.2" + css-unit-converter@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.2.tgz#4c77f5a1954e6dbff60695ecb214e3270436ab21" @@ -4169,6 +4181,14 @@ html-react-parser@3.0.4: react-property "2.0.0" style-to-js "1.1.1" +html2canvas@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543" + integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA== + dependencies: + css-line-break "^2.1.0" + text-segmentation "^1.0.3" + htmlparser2@8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.1.tgz#abaa985474fcefe269bc761a779b544d7196d010" @@ -6399,6 +6419,13 @@ text-encoding-utf-8@^1.0.2: resolved "https://registry.yarnpkg.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13" integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg== +text-segmentation@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943" + integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw== + dependencies: + utrie "^1.0.2" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -6927,6 +6954,13 @@ util@^0.12.0: safe-buffer "^5.1.2" which-typed-array "^1.1.2" +utrie@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645" + integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw== + dependencies: + base64-arraybuffer "^1.0.2" + uuid@^3.3.2: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" From 6220907b0c30018a4fe7cb8f98ced80fc179edc5 Mon Sep 17 00:00:00 2001 From: saml33 Date: Mon, 27 Feb 2023 14:30:58 +1100 Subject: [PATCH 02/71] remove unused icon --- components/icons/PositionShareIcon.tsx | 89 ------------------------ components/modals/SharePositionModal.tsx | 8 ++- hooks/useScreenshot.tsx | 2 +- 3 files changed, 6 insertions(+), 93 deletions(-) delete mode 100644 components/icons/PositionShareIcon.tsx diff --git a/components/icons/PositionShareIcon.tsx b/components/icons/PositionShareIcon.tsx deleted file mode 100644 index 30f12cec..00000000 --- a/components/icons/PositionShareIcon.tsx +++ /dev/null @@ -1,89 +0,0 @@ -const PositionShareIcon = ({ className }: { className?: string }) => { - return ( - - - - - - - - - - - - - - - - - - - - - - ) -} - -export default PositionShareIcon diff --git a/components/modals/SharePositionModal.tsx b/components/modals/SharePositionModal.tsx index 06b55405..1d316549 100644 --- a/components/modals/SharePositionModal.tsx +++ b/components/modals/SharePositionModal.tsx @@ -16,10 +16,12 @@ interface SharePositionModalProps { type ModalCombinedProps = SharePositionModalProps & ModalProps -async function copyToClipboard(image: any) { +async function copyToClipboard(image: HTMLCanvasElement) { try { - image.toBlob((blob: any) => { - navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]) + image.toBlob((blob: Blob | null) => { + if (blob) { + navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]) + } }, 'image/png') } catch (error) { console.error(error) diff --git a/hooks/useScreenshot.tsx b/hooks/useScreenshot.tsx index 7c387c3b..c0ced493 100644 --- a/hooks/useScreenshot.tsx +++ b/hooks/useScreenshot.tsx @@ -68,7 +68,7 @@ const useScreenshot: () => [ * @param {string} extension * @param {string[]} parts of file name */ -const createFileName = (extension = '', ...names: any) => { +const createFileName = (extension = '', ...names: string[]) => { if (!extension) { return '' } From 49612197b8ce0e9f0c8c489d721271210120f56d Mon Sep 17 00:00:00 2001 From: saml33 Date: Mon, 27 Feb 2023 15:44:59 +1100 Subject: [PATCH 03/71] bg images --- components/modals/SharePositionModal.tsx | 100 ++++++----- components/trade/PerpPositions.tsx | 4 +- public/images/space.svg | 68 ++++---- public/images/underwater.svg | 211 +++++++++++++---------- 4 files changed, 197 insertions(+), 186 deletions(-) diff --git a/components/modals/SharePositionModal.tsx b/components/modals/SharePositionModal.tsx index 1d316549..d2d49cff 100644 --- a/components/modals/SharePositionModal.tsx +++ b/components/modals/SharePositionModal.tsx @@ -83,62 +83,58 @@ const SharePositionModal = ({
-
- Mango -
-
-
-
-
-

- {side} -

- | -

- {market.name} -

-
-
= 0 ? 'text-th-up' : 'text-th-down' +
+
+ Mango +
+

- {roi >= 0 ? '+' : ''} - {roi.toFixed(2)}% -

-
-

- {t('entry-price')} -

-

- {formatNumericValue( - entryPrice, - getDecimalCount(market.tickSize) - )} -

-
-
-

- {t('current-price')} -

-

- {formatNumericValue( - market.uiPrice, - getDecimalCount(market.tickSize) - )} -

-
+ {side} +

+ | +

+ {market.name} +

+
+
= 0 ? 'text-th-up' : 'text-th-down' + }`} + > + {roi >= 0 ? '+' : ''} + {roi.toFixed(2)}% +
+
+

+ {t('entry-price')} +

+

+ {formatNumericValue( + entryPrice, + getDecimalCount(market.tickSize) + )} +

+
+
+

+ {t('current-price')} +

+

+ {formatNumericValue( + market.uiPrice, + getDecimalCount(market.tickSize) + )} +

diff --git a/components/trade/PerpPositions.tsx b/components/trade/PerpPositions.tsx index 960091fe..f97171e7 100644 --- a/components/trade/PerpPositions.tsx +++ b/components/trade/PerpPositions.tsx @@ -106,7 +106,7 @@ const PerpPositions = () => { 'pnl' )}`} {t('pnl')} - {isUnownedAccount ? : null} + {!isUnownedAccount ? : null} @@ -192,7 +192,7 @@ const PerpPositions = () => { > - {isUnownedAccount ? ( + {!isUnownedAccount ? (
) : bank?.name === 'USDC' || @@ -123,14 +121,17 @@ const SpotMarketsTable = () => { ) : (
- {serumMarkets.map((market) => { - return ( - - ) - })} + {serumMarkets + .slice() + .sort((a, b) => a.name.localeCompare(b.name)) + .map((market) => { + return ( + + ) + })}
)} @@ -141,38 +142,38 @@ export default SpotMarketsTable const MobileSpotMarketItem = ({ market }: { market: Serum3Market }) => { const { t } = useTranslation('common') - const { isLoading: loadingPrices, data: coingeckoPrices } = useCoingecko() + const { data: birdeyePrices, isLoading: loadingPrices } = + useBirdeyeMarketPrices() const { group } = useMangoGroup() const { theme } = useTheme() const bank = group?.getFirstBankByTokenIndex(market.baseTokenIndex) - const coingeckoData = useMemo(() => { + const birdeyeData = useMemo(() => { if (!loadingPrices && bank) { - return coingeckoPrices.find( - (asset) => asset.symbol.toUpperCase() === bank?.name + return birdeyePrices.find( + (m) => m.mint === market.serumMarketExternal.toString() ) } return null }, [loadingPrices, bank]) const change = useMemo(() => { - if (coingeckoData) { + if (birdeyeData && bank) { return ( - ((coingeckoData.prices[coingeckoData.prices.length - 1][1] - - coingeckoData.prices[0][1]) / - coingeckoData.prices[0][1]) * + ((bank.uiPrice - birdeyeData.data[0].value) / + birdeyeData.data[0].value) * 100 ) } return 0 - }, [coingeckoData]) + }, [birdeyeData, bank]) const chartData = useMemo(() => { - if (coingeckoData) { - return coingeckoData.prices + if (birdeyeData) { + return birdeyeData.data } return undefined - }, [coingeckoData]) + }, [birdeyeData]) return (
@@ -200,11 +201,11 @@ const MobileSpotMarketItem = ({ market }: { market: Serum3Market }) => { color={change >= 0 ? COLORS.UP[theme] : COLORS.DOWN[theme]} data={chartData} name={bank!.name} - xKey="0" - yKey="1" + xKey="unixTime" + yKey="value" />
- ) : bank?.name === 'USDC' || bank?.name === 'USDT' ? null : ( + ) : (

{t('unavailable')}

) ) : ( diff --git a/hooks/useBirdeyeMarketPrices.ts b/hooks/useBirdeyeMarketPrices.ts new file mode 100644 index 00000000..37ca7e53 --- /dev/null +++ b/hooks/useBirdeyeMarketPrices.ts @@ -0,0 +1,57 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Serum3Market } from '@blockworks-foundation/mango-v4' +import mangoStore from '@store/mangoStore' +import { useQuery } from '@tanstack/react-query' +import { makeApiRequest } from 'apis/birdeye/helpers' + +interface PriceResponse { + address: string + unixTime: number + value: number +} + +const fetchBirdeyePrices = async ( + spotMarkets: Serum3Market[] +): Promise<{ data: PriceResponse[]; mint: string }[]> => { + const mints = spotMarkets.map((market) => + market.serumMarketExternal.toString() + ) + + const promises: any = [] + for (const mint of mints) { + const queryEnd = Math.floor(Date.now() / 1000) + const queryStart = queryEnd - 86400 + const query = `defi/history_price?address=${mint}&address_type=pair&type=30m&time_from=${queryStart}&time_to=${queryEnd}` + promises.push(makeApiRequest(query)) + } + + const responses = await Promise.all(promises) + if (responses.length) { + return responses.map((res) => ({ + data: res.data.items, + mint: res.data.items[0].address, + })) + } + + return [] +} + +export const useBirdeyeMarketPrices = () => { + const spotMarkets = mangoStore((s) => s.serumMarkets) + const res = useQuery( + ['birdeye-market-prices'], + () => fetchBirdeyePrices(spotMarkets), + { + cacheTime: 1000 * 60 * 15, + staleTime: 1000 * 60 * 10, + retry: 3, + enabled: !!spotMarkets?.length, + refetchOnWindowFocus: false, + } + ) + + return { + isLoading: res?.isLoading, + data: res?.data || [], + } +} diff --git a/hooks/useBirdeyeTokenPrices.ts b/hooks/useBirdeyeTokenPrices.ts new file mode 100644 index 00000000..5284e504 --- /dev/null +++ b/hooks/useBirdeyeTokenPrices.ts @@ -0,0 +1,56 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useQuery } from '@tanstack/react-query' +import { makeApiRequest } from 'apis/birdeye/helpers' +import { useMemo } from 'react' +import { Token } from 'types/jupiter' +import useJupiterMints from './useJupiterMints' + +interface PriceResponse { + address: string + unixTime: number + value: number +} + +const fetchBirdeyePrices = async ( + mangoTokens: Token[] +): Promise<{ data: PriceResponse[]; mint: string }[]> => { + const mints = mangoTokens.map((token) => token.address) + + const promises: any = [] + for (const mint of mints) { + const queryEnd = Math.floor(Date.now() / 1000) + const queryStart = queryEnd - 86400 + const query = `defi/history_price?address=${mint}&address_type=token&type=30m&time_from=${queryStart}&time_to=${queryEnd}` + promises.push(makeApiRequest(query)) + } + + const responses = await Promise.all(promises) + if (responses.length) { + return responses.map((res) => ({ + data: res.data.items, + mint: res.data.items[0].address, + })) + } + + return [] +} + +export const useBirdeyeTokenPrices = () => { + const { mangoTokens } = useJupiterMints() + const res = useQuery( + ['birdeye-token-prices'], + () => fetchBirdeyePrices(mangoTokens), + { + cacheTime: 1000 * 60 * 15, + staleTime: 1000 * 60 * 10, + retry: 3, + enabled: !!mangoTokens?.length, + refetchOnWindowFocus: false, + } + ) + + return { + isLoading: res?.isLoading, + data: res?.data || [], + } +} From a5f310c07c62dfb4f7f166739485f4d7ac3bf683 Mon Sep 17 00:00:00 2001 From: saml33 Date: Tue, 4 Apr 2023 09:20:59 +1000 Subject: [PATCH 39/71] use birdeye for spot market change --- components/trade/AdvancedMarketHeader.tsx | 88 ++++++++--------------- hooks/useBirdeyeMarketPrices.ts | 1 + 2 files changed, 30 insertions(+), 59 deletions(-) diff --git a/components/trade/AdvancedMarketHeader.tsx b/components/trade/AdvancedMarketHeader.tsx index 26c0a0c7..1fe187a1 100644 --- a/components/trade/AdvancedMarketHeader.tsx +++ b/components/trade/AdvancedMarketHeader.tsx @@ -1,16 +1,12 @@ -import { Bank, PerpMarket, Serum3Market } from '@blockworks-foundation/mango-v4' +import { Bank, PerpMarket } from '@blockworks-foundation/mango-v4' import { IconButton } from '@components/shared/Button' import Change from '@components/shared/Change' import { getOneDayPerpStats } from '@components/stats/PerpMarketsTable' import { ChartBarIcon } from '@heroicons/react/20/solid' -import { Market } from '@project-serum/serum' import mangoStore from '@store/mangoStore' -import { useQuery } from '@tanstack/react-query' -import useJupiterMints from 'hooks/useJupiterMints' import useSelectedMarket from 'hooks/useSelectedMarket' import { useTranslation } from 'next-i18next' import { useEffect, useMemo, useState } from 'react' -import { Token } from 'types/jupiter' import { formatCurrencyValue, getDecimalCount, @@ -19,30 +15,9 @@ import { import MarketSelectDropdown from './MarketSelectDropdown' import PerpFundingRate from './PerpFundingRate' import { BorshAccountsCoder } from '@coral-xyz/anchor' - -type ResponseType = { - prices: [number, number][] - market_caps: [number, number][] - total_volumes: [number, number][] -} - -const fetchTokenChange = async ( - mangoTokens: Token[], - baseAddress: string -): Promise => { - let coingeckoId = mangoTokens.find((t) => t.address === baseAddress) - ?.extensions?.coingeckoId - - if (baseAddress === '3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh') { - coingeckoId = 'bitcoin' - } - - const response = await fetch( - `https://api.coingecko.com/api/v3/coins/${coingeckoId}/market_chart?vs_currency=usd&days=1` - ) - const data = await response.json() - return data -} +import { useBirdeyeMarketPrices } from 'hooks/useBirdeyeMarketPrices' +import SheenLoader from '@components/shared/SheenLoader' +import usePrevious from '@components/shared/usePrevious' const AdvancedMarketHeader = ({ showChart, @@ -59,9 +34,11 @@ const AdvancedMarketHeader = ({ selectedMarket, } = useSelectedMarket() const selectedMarketName = mangoStore((s) => s.selectedMarket.name) - const { mangoTokens } = useJupiterMints() const connection = mangoStore((s) => s.connection) const [price, setPrice] = useState(stalePrice) + const { data: birdeyePrices, isLoading: loadingPrices } = + useBirdeyeMarketPrices() + const previousMarketName = usePrevious(selectedMarketName) //subscribe to the market oracle account useEffect(() => { @@ -116,29 +93,17 @@ const AdvancedMarketHeader = ({ } }, [serumOrPerpMarket]) - const spotBaseAddress = useMemo(() => { - const group = mangoStore.getState().group - if (group && selectedMarket && selectedMarket instanceof Serum3Market) { - return group - .getFirstBankByTokenIndex(selectedMarket.baseTokenIndex) - .mint.toString() - } - }, [selectedMarket]) - - const spotChangeResponse = useQuery( - ['coingecko-tokens', spotBaseAddress], - () => fetchTokenChange(mangoTokens, spotBaseAddress!), - { - cacheTime: 1000 * 60 * 15, - staleTime: 1000 * 60 * 10, - retry: 3, - enabled: - !!spotBaseAddress && - serumOrPerpMarket instanceof Market && - mangoTokens.length > 0, - refetchOnWindowFocus: false, - } - ) + const birdeyeData = useMemo(() => { + if ( + !birdeyePrices?.length || + !selectedMarket || + selectedMarket instanceof PerpMarket + ) + return + return birdeyePrices.find( + (m) => m.mint === selectedMarket.serumMarketExternal.toString() + ) + }, [birdeyePrices, selectedMarket]) const change = useMemo(() => { if (!price || !serumOrPerpMarket) return 0 @@ -149,18 +114,17 @@ const AdvancedMarketHeader = ({ ? ((price - changeData[0].price) / changeData[0].price) * 100 : 0 } else { - if (!spotChangeResponse.data) return 0 + if (!birdeyeData || selectedMarketName !== previousMarketName) return 0 return ( - ((price - spotChangeResponse.data.prices?.[0][1]) / - spotChangeResponse.data.prices?.[0][1]) * - 100 + ((price - birdeyeData.data[0].value) / birdeyeData.data[0].value) * 100 ) } }, [ - spotChangeResponse, + birdeyeData, price, serumOrPerpMarket, perpStats, + previousMarketName, selectedMarketName, ]) @@ -191,7 +155,13 @@ const AdvancedMarketHeader = ({
{t('rolling-change')}
- + {!loadingPrices ? ( + + ) : ( + +
+ + )}
{serumOrPerpMarket instanceof PerpMarket ? ( <> diff --git a/hooks/useBirdeyeMarketPrices.ts b/hooks/useBirdeyeMarketPrices.ts index 37ca7e53..74ec5fa6 100644 --- a/hooks/useBirdeyeMarketPrices.ts +++ b/hooks/useBirdeyeMarketPrices.ts @@ -51,6 +51,7 @@ export const useBirdeyeMarketPrices = () => { ) return { + isFetching: res?.isFetching, isLoading: res?.isLoading, data: res?.data || [], } From fdf10af08216fd788267987d09bbb0965c03a8f4 Mon Sep 17 00:00:00 2001 From: saml33 Date: Tue, 4 Apr 2023 11:37:03 +1000 Subject: [PATCH 40/71] use birdeye for token stats price chart --- components/token/CoingeckoStats.tsx | 127 +++++++++++++--------------- components/token/PriceChart.tsx | 13 +-- components/token/TokenPage.tsx | 46 +++++----- hooks/useBirdeyeMarketPrices.ts | 9 +- hooks/useBirdeyeTokenPrices.ts | 15 +--- 5 files changed, 100 insertions(+), 110 deletions(-) diff --git a/components/token/CoingeckoStats.tsx b/components/token/CoingeckoStats.tsx index aba7aa90..80149650 100644 --- a/components/token/CoingeckoStats.tsx +++ b/components/token/CoingeckoStats.tsx @@ -4,9 +4,11 @@ import ChartRangeButtons from '@components/shared/ChartRangeButtons' import FormatNumericValue from '@components/shared/FormatNumericValue' import SheenLoader from '@components/shared/SheenLoader' import { ArrowSmallUpIcon, NoSymbolIcon } from '@heroicons/react/20/solid' +import { useQuery } from '@tanstack/react-query' +import { makeApiRequest } from 'apis/birdeye/helpers' import dayjs from 'dayjs' import relativeTime from 'dayjs/plugin/relativeTime' -import { useCoingecko } from 'hooks/useCoingecko' +import { BirdeyePriceResponse } from 'hooks/useBirdeyeMarketPrices' import parse from 'html-react-parser' import { useTranslation } from 'next-i18next' import dynamic from 'next/dynamic' @@ -34,40 +36,55 @@ const DEFAULT_COINGECKO_VALUES = { total_volume: 0, } +interface BirdeyeResponse { + data: { items: BirdeyePriceResponse[] } + success: boolean +} + +const fetchBirdeyePrices = async ( + daysToShow: string, + mint: string +): Promise => { + const interval = daysToShow === '1' ? '30m' : daysToShow === '7' ? '1H' : '4H' + const queryEnd = Math.floor(Date.now() / 1000) + const queryStart = queryEnd - parseInt(daysToShow) * 86400 + const query = `defi/history_price?address=${mint}&address_type=token&type=${interval}&time_from=${queryStart}&time_to=${queryEnd}` + const response: BirdeyeResponse = await makeApiRequest(query) + + if (response.success && response?.data?.items) { + return response.data.items + } + return [] +} + const CoingeckoStats = ({ bank, coingeckoData, - coingeckoId, }: { bank: Bank // TODO: Add Coingecko api types // eslint-disable-next-line @typescript-eslint/no-explicit-any coingeckoData: any - coingeckoId: string }) => { const { t } = useTranslation(['common', 'token']) const [showFullDesc, setShowFullDesc] = useState(false) const [daysToShow, setDaysToShow] = useState('1') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const [chartData, setChartData] = useState<{ prices: any[] } | null>(null) - const [loadChartData, setLoadChartData] = useState(true) - const { isLoading: loadingPrices, data: coingeckoPrices } = useCoingecko() - const handleDaysToShow = async (days: string) => { - if (days !== '1') { - try { - const response = await fetch( - `https://api.coingecko.com/api/v3/coins/${coingeckoId}/market_chart?vs_currency=usd&days=${days}` - ) - const data = await response.json() - setLoadChartData(false) - setChartData(data) - } catch { - setLoadChartData(false) - } + const { + data: birdeyePrices, + isLoading: loadingBirdeyePrices, + isFetching: fetchingBirdeyePrices, + } = useQuery( + ['birdeye-token-prices', daysToShow, bank], + () => fetchBirdeyePrices(daysToShow, bank.mint.toString()), + { + cacheTime: 1000 * 60 * 15, + staleTime: 1000 * 60 * 10, + retry: 3, + enabled: !!bank, + refetchOnWindowFocus: false, } - setDaysToShow(days) - } + ) const { ath, @@ -82,28 +99,7 @@ const CoingeckoStats = ({ max_supply, total_supply, total_volume, - } = coingeckoData ? coingeckoData.market_data : DEFAULT_COINGECKO_VALUES - - const loadingChart = useMemo(() => { - return daysToShow == '1' ? loadingPrices : loadChartData - }, [loadChartData, loadingPrices]) - - const coingeckoTokenPrices = useMemo(() => { - if (daysToShow === '1' && coingeckoPrices.length && bank) { - const tokenPriceData = coingeckoPrices.find( - (asset) => asset.symbol.toUpperCase() === bank.name.toUpperCase() - ) - - if (tokenPriceData) { - return tokenPriceData.prices - } - } else { - if (chartData && !loadingChart) { - return chartData.prices - } - } - return [] - }, [coingeckoPrices, bank, daysToShow, chartData, loadingChart]) + } = coingeckoData ? coingeckoData : DEFAULT_COINGECKO_VALUES const truncateDescription = (desc: string) => desc.substring(0, (desc + ' ').lastIndexOf(' ', 144)) @@ -140,35 +136,30 @@ const CoingeckoStats = ({
) : null} - {!loadingChart ? ( - coingeckoTokenPrices.length ? ( - <> -
-

{bank.name} Price Chart

- handleDaysToShow(v)} - /> -
- - - ) : bank?.name === 'USDC' || bank?.name === 'USDT' ? null : ( -
- -

{t('token:chart-unavailable')}

-
- ) - ) : ( +
+

{bank.name} Price Chart

+ setDaysToShow(v)} + /> +
+ {birdeyePrices?.length ? ( + + ) : loadingBirdeyePrices || fetchingBirdeyePrices ? (
-
+
+ ) : ( +
+
+ +

{t('chart-unavailable')}

+
+
)}
diff --git a/components/token/PriceChart.tsx b/components/token/PriceChart.tsx index fa1b1487..f490ce02 100644 --- a/components/token/PriceChart.tsx +++ b/components/token/PriceChart.tsx @@ -1,4 +1,5 @@ import { formatDateAxis } from '@components/shared/DetailedAreaChart' +import { BirdeyePriceResponse } from 'hooks/useBirdeyeMarketPrices' import { useTheme } from 'next-themes' import { useMemo } from 'react' import { Area, AreaChart, ResponsiveContainer, XAxis, YAxis } from 'recharts' @@ -9,13 +10,13 @@ const PriceChart = ({ prices, daysToShow, }: { - prices: number[][] + prices: BirdeyePriceResponse[] daysToShow: number }) => { const { theme } = useTheme() const change = useMemo(() => { - return prices[prices.length - 1][1] - prices[0][1] + return prices[prices.length - 1].value - prices[0].value }, [prices]) return ( @@ -44,14 +45,14 @@ const PriceChart = ({ = 0 ? COLORS.UP[theme] : COLORS.DOWN[theme]} strokeWidth={1.5} fill="url(#gradientArea)" /> formatCurrencyValue(x)} tickLine={false} - width={prices[0][1] < 0.00001 ? 100 : 60} + width={prices[0].value < 0.00001 ? 100 : 60} /> diff --git a/components/token/TokenPage.tsx b/components/token/TokenPage.tsx index ae74b8e2..e6deef8a 100644 --- a/components/token/TokenPage.tsx +++ b/components/token/TokenPage.tsx @@ -106,21 +106,22 @@ const TokenPage = () => { } }, [bank, mangoTokens]) - const coingeckoTokenInfo = useQuery( - ['coingecko-token-info', coingeckoId], - () => fetchTokenInfo(coingeckoId), - { - cacheTime: 1000 * 60 * 15, - staleTime: 1000 * 60 * 5, - retry: 3, - refetchOnWindowFocus: false, - enabled: !!coingeckoId, - } - ) + const { data: coingeckoTokenInfo, isLoading: loadingCoingeckoInfo } = + useQuery( + ['coingecko-token-info', coingeckoId], + () => fetchTokenInfo(coingeckoId), + { + cacheTime: 1000 * 60 * 15, + staleTime: 1000 * 60 * 5, + retry: 3, + refetchOnWindowFocus: false, + enabled: !!coingeckoId, + } + ) const { high_24h, low_24h, price_change_percentage_24h } = - coingeckoTokenInfo.data - ? coingeckoTokenInfo.data.market_data + coingeckoTokenInfo?.market_data + ? coingeckoTokenInfo.market_data : DEFAULT_COINGECKO_VALUES return ( @@ -131,9 +132,9 @@ const TokenPage = () => {
- {coingeckoTokenInfo.data ? ( + {coingeckoTokenInfo ? (

- {coingeckoTokenInfo.data.name}{' '} + {coingeckoTokenInfo.name}{' '} {bank.name}

) : ( @@ -155,13 +156,13 @@ const TokenPage = () => { )}
- {coingeckoTokenInfo.data ? ( + {coingeckoTokenInfo?.market_data ? (
) : null}
- {coingeckoTokenInfo.data ? ( + {coingeckoTokenInfo?.market_data ? ( {
{bank ? : null} - {coingeckoTokenInfo.data && coingeckoId ? ( + {coingeckoTokenInfo?.market_data ? ( + ) : loadingCoingeckoInfo && coingeckoId ? ( +
+ +
+ +
) : (
🦎 diff --git a/hooks/useBirdeyeMarketPrices.ts b/hooks/useBirdeyeMarketPrices.ts index 74ec5fa6..c2897804 100644 --- a/hooks/useBirdeyeMarketPrices.ts +++ b/hooks/useBirdeyeMarketPrices.ts @@ -1,10 +1,9 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import { Serum3Market } from '@blockworks-foundation/mango-v4' import mangoStore from '@store/mangoStore' import { useQuery } from '@tanstack/react-query' import { makeApiRequest } from 'apis/birdeye/helpers' -interface PriceResponse { +export interface BirdeyePriceResponse { address: string unixTime: number value: number @@ -12,12 +11,12 @@ interface PriceResponse { const fetchBirdeyePrices = async ( spotMarkets: Serum3Market[] -): Promise<{ data: PriceResponse[]; mint: string }[]> => { +): Promise<{ data: BirdeyePriceResponse[]; mint: string }[]> => { const mints = spotMarkets.map((market) => market.serumMarketExternal.toString() ) - const promises: any = [] + const promises = [] for (const mint of mints) { const queryEnd = Math.floor(Date.now() / 1000) const queryStart = queryEnd - 86400 @@ -38,7 +37,7 @@ const fetchBirdeyePrices = async ( export const useBirdeyeMarketPrices = () => { const spotMarkets = mangoStore((s) => s.serumMarkets) - const res = useQuery( + const res = useQuery( ['birdeye-market-prices'], () => fetchBirdeyePrices(spotMarkets), { diff --git a/hooks/useBirdeyeTokenPrices.ts b/hooks/useBirdeyeTokenPrices.ts index 5284e504..05ac3994 100644 --- a/hooks/useBirdeyeTokenPrices.ts +++ b/hooks/useBirdeyeTokenPrices.ts @@ -1,22 +1,15 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import { useQuery } from '@tanstack/react-query' import { makeApiRequest } from 'apis/birdeye/helpers' -import { useMemo } from 'react' import { Token } from 'types/jupiter' +import { BirdeyePriceResponse } from './useBirdeyeMarketPrices' import useJupiterMints from './useJupiterMints' -interface PriceResponse { - address: string - unixTime: number - value: number -} - const fetchBirdeyePrices = async ( mangoTokens: Token[] -): Promise<{ data: PriceResponse[]; mint: string }[]> => { +): Promise<{ data: BirdeyePriceResponse[]; mint: string }[]> => { const mints = mangoTokens.map((token) => token.address) - const promises: any = [] + const promises = [] for (const mint of mints) { const queryEnd = Math.floor(Date.now() / 1000) const queryStart = queryEnd - 86400 @@ -37,7 +30,7 @@ const fetchBirdeyePrices = async ( export const useBirdeyeTokenPrices = () => { const { mangoTokens } = useJupiterMints() - const res = useQuery( + const res = useQuery( ['birdeye-token-prices'], () => fetchBirdeyePrices(mangoTokens), { From d34fa4a503381c35e9b0e732aa6f7d4bc01a600c Mon Sep 17 00:00:00 2001 From: saml33 Date: Tue, 4 Apr 2023 12:48:31 +1000 Subject: [PATCH 41/71] add change to market select dropdown --- components/trade/AdvancedMarketHeader.tsx | 22 +++++---- components/trade/MarketSelectDropdown.tsx | 56 +++++++++++++++++++++-- 2 files changed, 66 insertions(+), 12 deletions(-) diff --git a/components/trade/AdvancedMarketHeader.tsx b/components/trade/AdvancedMarketHeader.tsx index 1fe187a1..10c834dc 100644 --- a/components/trade/AdvancedMarketHeader.tsx +++ b/components/trade/AdvancedMarketHeader.tsx @@ -28,6 +28,7 @@ const AdvancedMarketHeader = ({ }) => { const { t } = useTranslation(['common', 'trade']) const perpStats = mangoStore((s) => s.perpStats.data) + const loadingPerpStats = mangoStore((s) => s.perpStats.loading) const { serumOrPerpMarket, price: stalePrice, @@ -87,11 +88,9 @@ const AdvancedMarketHeader = ({ }, [connection, selectedMarket]) useEffect(() => { - if (serumOrPerpMarket instanceof PerpMarket) { - const actions = mangoStore.getState().actions - actions.fetchPerpStats() - } - }, [serumOrPerpMarket]) + const actions = mangoStore.getState().actions + actions.fetchPerpStats() + }, []) const birdeyeData = useMemo(() => { if ( @@ -106,7 +105,12 @@ const AdvancedMarketHeader = ({ }, [birdeyePrices, selectedMarket]) const change = useMemo(() => { - if (!price || !serumOrPerpMarket) return 0 + if ( + !price || + !serumOrPerpMarket || + selectedMarketName !== previousMarketName + ) + return 0 if (serumOrPerpMarket instanceof PerpMarket) { const changeData = getOneDayPerpStats(perpStats, selectedMarketName) @@ -114,7 +118,7 @@ const AdvancedMarketHeader = ({ ? ((price - changeData[0].price) / changeData[0].price) * 100 : 0 } else { - if (!birdeyeData || selectedMarketName !== previousMarketName) return 0 + if (!birdeyeData) return 0 return ( ((price - birdeyeData.data[0].value) / birdeyeData.data[0].value) * 100 ) @@ -155,11 +159,11 @@ const AdvancedMarketHeader = ({
{t('rolling-change')}
- {!loadingPrices ? ( + {!loadingPrices && !loadingPerpStats ? ( ) : ( -
+
)}
diff --git a/components/trade/MarketSelectDropdown.tsx b/components/trade/MarketSelectDropdown.tsx index a460b28d..c0dd32e3 100644 --- a/components/trade/MarketSelectDropdown.tsx +++ b/components/trade/MarketSelectDropdown.tsx @@ -1,8 +1,13 @@ // import ChartRangeButtons from '@components/shared/ChartRangeButtons' +import Change from '@components/shared/Change' import FavoriteMarketButton from '@components/shared/FavoriteMarketButton' +import SheenLoader from '@components/shared/SheenLoader' +import { getOneDayPerpStats } from '@components/stats/PerpMarketsTable' import { Popover } from '@headlessui/react' import { ChevronDownIcon } from '@heroicons/react/20/solid' import mangoStore from '@store/mangoStore' +import { useBirdeyeMarketPrices } from 'hooks/useBirdeyeMarketPrices' +import useMangoGroup from 'hooks/useMangoGroup' import useSelectedMarket from 'hooks/useSelectedMarket' import { useTranslation } from 'next-i18next' import Link from 'next/link' @@ -15,6 +20,11 @@ const MarketSelectDropdown = () => { const { selectedMarket } = useSelectedMarket() const serumMarkets = mangoStore((s) => s.serumMarkets) const allPerpMarkets = mangoStore((s) => s.perpMarkets) + const perpStats = mangoStore((s) => s.perpStats.data) + const loadingPerpStats = mangoStore((s) => s.perpStats.loading) + const { group } = useMangoGroup() + const { data: birdeyePrices, isLoading: loadingPrices } = + useBirdeyeMarketPrices() // const [spotBaseFilter, setSpotBaseFilter] = useState('All') const perpMarkets = useMemo(() => { @@ -61,10 +71,17 @@ const MarketSelectDropdown = () => { } mt-0.5 ml-2 h-6 w-6 flex-shrink-0 text-th-fgd-2`} /> - +

{t('perp')}

{perpMarkets?.length ? perpMarkets.map((m) => { + const changeData = getOneDayPerpStats(perpStats, m.name) + + const change = changeData.length + ? ((m.uiPrice - changeData[0].price) / + changeData[0].price) * + 100 + : 0 return (
{
- +
+ {!loadingPerpStats ? ( + + ) : ( + +
+ + )} + +
) }) @@ -105,6 +131,21 @@ const MarketSelectDropdown = () => { .map((x) => x) .sort((a, b) => a.name.localeCompare(b.name)) .map((m) => { + const birdeyeData = birdeyePrices?.length + ? birdeyePrices.find( + (market) => + market.mint === m.serumMarketExternal.toString() + ) + : null + const bank = group?.getFirstBankByTokenIndex( + m.baseTokenIndex + ) + const change = + birdeyeData && bank + ? ((bank.uiPrice - birdeyeData.data[0].value) / + birdeyeData.data[0].value) * + 100 + : 0 return (
{
- +
+ {!loadingPrices ? ( + + ) : ( + +
+ + )} + +
) })} From 4e7cfaf5019e9d45f6d86ee139a71b629f9cc24a Mon Sep 17 00:00:00 2001 From: saml33 Date: Thu, 6 Apr 2023 10:07:36 +1000 Subject: [PATCH 42/71] round pnl to 2dp --- components/trade/PerpPositions.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/components/trade/PerpPositions.tsx b/components/trade/PerpPositions.tsx index 5dc05495..a3f6885c 100644 --- a/components/trade/PerpPositions.tsx +++ b/components/trade/PerpPositions.tsx @@ -202,11 +202,15 @@ const PerpPositions = () => { cummulativePnl > 0 ? 'text-th-up' : 'text-th-down' }`} > - + {!isUnownedAccount ? ( -
+
handleShowShare(openPerpPositions[index]) } From a67dccd3bd30f7777fbb187ce377855eef0a5e6e Mon Sep 17 00:00:00 2001 From: saml33 Date: Thu, 6 Apr 2023 10:22:20 +1000 Subject: [PATCH 43/71] implement pr feedback --- components/stats/MangoPerpStatsCharts.tsx | 4 +- hooks/useBirdeyeMarketPrices.ts | 6 +-- hooks/useBirdeyeTokenPrices.ts | 49 ----------------------- store/mangoStore.ts | 2 +- 4 files changed, 6 insertions(+), 55 deletions(-) delete mode 100644 hooks/useBirdeyeTokenPrices.ts diff --git a/components/stats/MangoPerpStatsCharts.tsx b/components/stats/MangoPerpStatsCharts.tsx index b7855e8f..79fae1ce 100644 --- a/components/stats/MangoPerpStatsCharts.tsx +++ b/components/stats/MangoPerpStatsCharts.tsx @@ -35,7 +35,7 @@ const MangoPerpStatsCharts = () => { // }, [perpMarkets]) const totalFeeValues = useMemo(() => { - if (!perpStats.length) return [] + if (!perpStats || !perpStats.length) return [] const values = perpStats.reduce((a: FeeValueItem[], c: PerpStatsItem) => { const hasDate = a.find((d: FeeValueItem) => d.date === c.date_hour) if (!hasDate) { @@ -52,7 +52,7 @@ const MangoPerpStatsCharts = () => { }, [perpStats]) const totalOpenInterestValues = useMemo(() => { - if (!perpStats.length) return [] + if (!perpStats || !perpStats.length) return [] const values = perpStats.reduce((a: OiValueItem[], c: PerpStatsItem) => { const hasDate = a.find((d: OiValueItem) => d.date === c.date_hour) if (!hasDate) { diff --git a/hooks/useBirdeyeMarketPrices.ts b/hooks/useBirdeyeMarketPrices.ts index c2897804..5033b18e 100644 --- a/hooks/useBirdeyeMarketPrices.ts +++ b/hooks/useBirdeyeMarketPrices.ts @@ -17,15 +17,15 @@ const fetchBirdeyePrices = async ( ) const promises = [] + const queryEnd = Math.floor(Date.now() / 1000) + const queryStart = queryEnd - 86400 for (const mint of mints) { - const queryEnd = Math.floor(Date.now() / 1000) - const queryStart = queryEnd - 86400 const query = `defi/history_price?address=${mint}&address_type=pair&type=30m&time_from=${queryStart}&time_to=${queryEnd}` promises.push(makeApiRequest(query)) } const responses = await Promise.all(promises) - if (responses.length) { + if (responses?.length) { return responses.map((res) => ({ data: res.data.items, mint: res.data.items[0].address, diff --git a/hooks/useBirdeyeTokenPrices.ts b/hooks/useBirdeyeTokenPrices.ts deleted file mode 100644 index 05ac3994..00000000 --- a/hooks/useBirdeyeTokenPrices.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { useQuery } from '@tanstack/react-query' -import { makeApiRequest } from 'apis/birdeye/helpers' -import { Token } from 'types/jupiter' -import { BirdeyePriceResponse } from './useBirdeyeMarketPrices' -import useJupiterMints from './useJupiterMints' - -const fetchBirdeyePrices = async ( - mangoTokens: Token[] -): Promise<{ data: BirdeyePriceResponse[]; mint: string }[]> => { - const mints = mangoTokens.map((token) => token.address) - - const promises = [] - for (const mint of mints) { - const queryEnd = Math.floor(Date.now() / 1000) - const queryStart = queryEnd - 86400 - const query = `defi/history_price?address=${mint}&address_type=token&type=30m&time_from=${queryStart}&time_to=${queryEnd}` - promises.push(makeApiRequest(query)) - } - - const responses = await Promise.all(promises) - if (responses.length) { - return responses.map((res) => ({ - data: res.data.items, - mint: res.data.items[0].address, - })) - } - - return [] -} - -export const useBirdeyeTokenPrices = () => { - const { mangoTokens } = useJupiterMints() - const res = useQuery( - ['birdeye-token-prices'], - () => fetchBirdeyePrices(mangoTokens), - { - cacheTime: 1000 * 60 * 15, - staleTime: 1000 * 60 * 10, - retry: 3, - enabled: !!mangoTokens?.length, - refetchOnWindowFocus: false, - } - ) - - return { - isLoading: res?.isLoading, - data: res?.data || [], - } -} diff --git a/store/mangoStore.ts b/store/mangoStore.ts index 11149b2f..d83d5795 100644 --- a/store/mangoStore.ts +++ b/store/mangoStore.ts @@ -163,7 +163,7 @@ export type MangoStore = { perpMarkets: PerpMarket[] perpStats: { loading: boolean - data: PerpStatsItem[] + data: PerpStatsItem[] | null } profile: { details: ProfileDetails | null From a8a8fd9fdafd50640b49194390382bfe3e4c618c Mon Sep 17 00:00:00 2001 From: saml33 Date: Thu, 6 Apr 2023 11:15:22 +1000 Subject: [PATCH 44/71] remove formatting from max swap amount value --- components/swap/MaxSwapAmount.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/swap/MaxSwapAmount.tsx b/components/swap/MaxSwapAmount.tsx index 54c75643..13db5b54 100644 --- a/components/swap/MaxSwapAmount.tsx +++ b/components/swap/MaxSwapAmount.tsx @@ -2,7 +2,7 @@ import MaxAmountButton from '@components/shared/MaxAmountButton' import mangoStore from '@store/mangoStore' import Decimal from 'decimal.js' import { useTranslation } from 'next-i18next' -import { formatNumericValue } from 'utils/numbers' +import { floorToDecimal } from 'utils/numbers' import { useTokenMax } from './useTokenMax' const MaxSwapAmount = ({ @@ -23,7 +23,7 @@ const MaxSwapAmount = ({ if (mangoAccountLoading) return null const setMax = (value: Decimal) => { - setAmountIn(formatNumericValue(value, decimals)) + setAmountIn(floorToDecimal(value, decimals).toFixed()) } return ( From 6a2630b6aac0c38ef8cd729506c4b00cef78653c Mon Sep 17 00:00:00 2001 From: saml33 Date: Thu, 6 Apr 2023 12:12:26 +1000 Subject: [PATCH 45/71] update mango logo --- public/logos/logo-mark.svg | 95 ++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 51 deletions(-) diff --git a/public/logos/logo-mark.svg b/public/logos/logo-mark.svg index a61c429f..767b3f89 100644 --- a/public/logos/logo-mark.svg +++ b/public/logos/logo-mark.svg @@ -1,52 +1,45 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 014ab321d1959847e9eaa3142bacecaab4d1f5fd Mon Sep 17 00:00:00 2001 From: saml33 Date: Thu, 6 Apr 2023 13:48:59 +1000 Subject: [PATCH 46/71] fix trade form submit on button group --- components/forms/ButtonGroup.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/components/forms/ButtonGroup.tsx b/components/forms/ButtonGroup.tsx index d7b38e12..2802760e 100644 --- a/components/forms/ButtonGroup.tsx +++ b/components/forms/ButtonGroup.tsx @@ -52,6 +52,7 @@ const ButtonGroup = ({ style={{ width: `${100 / values.length}%`, }} + type="button" > {names ? (unit ? names[i] + unit : names[i]) : unit ? v + unit : v} From 86670e1b2f177231772cbd918d890197dbe49f3a Mon Sep 17 00:00:00 2001 From: saml33 Date: Thu, 6 Apr 2023 20:43:22 +1000 Subject: [PATCH 47/71] update share meta --- pages/_app.tsx | 4 ++-- public/images/1200x600-share.png | Bin 0 -> 137148 bytes 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 public/images/1200x600-share.png diff --git a/pages/_app.tsx b/pages/_app.tsx index 97fe6397..a0f69581 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -94,11 +94,11 @@ function MyApp({ Component, pageProps }: AppProps) { diff --git a/public/images/1200x600-share.png b/public/images/1200x600-share.png new file mode 100644 index 0000000000000000000000000000000000000000..4aebb493f0893029222055b7c8db13d53fbac2f7 GIT binary patch literal 137148 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 z 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 WRJi5~vkle&0000 Date: Fri, 7 Apr 2023 11:59:37 +0100 Subject: [PATCH 48/71] Bump client version --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6777c560..9a2bb9bc 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "postinstall": "tar -xzC public -f vendor/charting_library.tgz;tar -xzC public -f vendor/datafeeds.tgz" }, "dependencies": { - "@blockworks-foundation/mango-v4": "^0.9.14", + "@blockworks-foundation/mango-v4": "^0.9.15", "@headlessui/react": "1.6.6", "@heroicons/react": "2.0.10", "@project-serum/anchor": "0.25.0", @@ -63,7 +63,7 @@ }, "peerDependencies": { "@project-serum/anchor": "0.25.0", - "@project-serum/serum": ">=0.13.62", + "@project-serum/serum": "0.13.65", "@solana/web3.js": ">=1.70.1" }, "devDependencies": { From b52499afcb8ed5c279250f5864d7114821f6d1e4 Mon Sep 17 00:00:00 2001 From: saml33 Date: Sat, 8 Apr 2023 20:56:39 +1000 Subject: [PATCH 49/71] curly braces isunownedaccount --- components/trade/PerpPositions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/trade/PerpPositions.tsx b/components/trade/PerpPositions.tsx index a3f6885c..bb9f0cb9 100644 --- a/components/trade/PerpPositions.tsx +++ b/components/trade/PerpPositions.tsx @@ -40,7 +40,7 @@ const PerpPositions = () => { const { selectedMarket } = useSelectedMarket() const { connected } = useWallet() const { mangoAccountAddress } = useMangoAccount() - const isUnownedAccount = useUnownedAccount() + const { isUnownedAccount } = useUnownedAccount() const { width } = useViewport() const showTableView = width ? width > breakpoints.md : false const { asPath } = useRouter() From 598c29fe1384e4244248a90659965ef0bf0ea565 Mon Sep 17 00:00:00 2001 From: tjs Date: Sat, 8 Apr 2023 13:24:21 -0400 Subject: [PATCH 50/71] use tradeform for checkboxes --- components/trade/AdvancedTradeForm.tsx | 6 ++++-- yarn.lock | 8 ++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/components/trade/AdvancedTradeForm.tsx b/components/trade/AdvancedTradeForm.tsx index 25c6cab3..c0b006dd 100644 --- a/components/trade/AdvancedTradeForm.tsx +++ b/components/trade/AdvancedTradeForm.tsx @@ -362,6 +362,8 @@ const AdvancedTradeForm = () => { : tradeForm.postOnly ? PerpOrderType.postOnly : PerpOrderType.limit + console.log('perpOrderType', perpOrderType) + const tx = await client.perpPlaceOrder( group, mangoAccount, @@ -580,7 +582,7 @@ const AdvancedTradeForm = () => { content={t('trade:tooltip-post')} > handlePostOnlyChange(e.target.checked)} > {t('trade:post')} @@ -596,7 +598,7 @@ const AdvancedTradeForm = () => { >
handleIocChange(e.target.checked)} > IOC diff --git a/yarn.lock b/yarn.lock index 73c7ee61..52d5f277 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14,10 +14,10 @@ dependencies: regenerator-runtime "^0.13.11" -"@blockworks-foundation/mango-v4@^0.9.14": - version "0.9.14" - resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.9.14.tgz#e46f890d3321e5ea9b1f7cb84713e64b68d8af45" - integrity sha512-g0Bv5q5znOPk9GTI3cKsXLxWF+lNF/sn1xLg0RNDwyuVCejM0eqJ2X6hfFIBRGVSolScvG+1w5BuEO8sBcD+Hg== +"@blockworks-foundation/mango-v4@^0.9.15": + version "0.9.16" + resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.9.16.tgz#9a3e70c95e46f112e7a204cd80d42a1c26e7d484" + integrity sha512-tyGtLiVQeWwxggSnkv1tqT3akeIpsiFtAcNuXPiJKfm/zwgPhLLOAsFbl0cr9IGqtV7uY/+CmKSqL2dHJD2czg== dependencies: "@coral-xyz/anchor" "^0.26.0" "@project-serum/serum" "0.13.65" From b3989b42bb462fe5916eac06cb4b8a6a51505ed9 Mon Sep 17 00:00:00 2001 From: rjpeterson Date: Sat, 8 Apr 2023 22:00:24 -0700 Subject: [PATCH 51/71] revert english profile.json --- public/locales/en/profile.json | 78 +++++++++++++++++----------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/public/locales/en/profile.json b/public/locales/en/profile.json index 3a66cbd6..1943d93e 100644 --- a/public/locales/en/profile.json +++ b/public/locales/en/profile.json @@ -1,44 +1,44 @@ { - "browse-profiles": "瀏覽", - "choose-profile": "帳戶頭像", - "connect-view-profile": "連接錢包來查看帳戶", + "browse-profiles": "Browse", + "choose-profile": "Profile Image", + "connect-view-profile": "Connect your wallet to view your profile", "day-trader": "Day Trader", "degen": "Degen", - "discretionary": "零錢", - "edit-profile": "編輯帳戶", - "edit-profile-pic": "換帳戶頭像", - "follow": "追蹤", - "following": "追蹤中", - "invalid-characters": "限制於字母、数字和空格", - "length-error": "標籤必須二十個字母以下", - "market-maker": "做市商", - "no-followers": "無追蹤者", - "no-followers-desc": "以隱身模式交易😎", - "no-following": "還沒追蹤別帳戶", - "no-following-desc": "跟朋友團結比較安全吧", - "no-nfts": "😞查不到NFT...", - "no-profile-exists": "此帳戶不存在...", - "profile": "帳戶", - "profile-api-error": "無法更新帳戶。請稍候再試", - "profile-fetch-fail": "查帳戶細節出錯", - "profile-name": "帳戶標籤", - "profile-pic-failure": "設置頭像失敗", - "profile-pic-success": "設置頭像成功", - "profile-pic-remove-failure": "刪除頭像失敗", - "profile-pic-remove-success": "刪除頭像成功", - "profile-update-fail": "更新帳戶出錯", - "profile-update-success": "帳戶已更新", - "remove": "刪除", - "save-profile": "保存帳戶", - "set-profile-pic": "設置頭像", - "swing-trader": "擺動交易者", - "total-pnl": "組合總盈虧", - "total-value": "組合總價值", - "trader": "交易者", - "trader-category": "交易模式", - "unfollow": "取消追蹤", - "uniqueness-api-fail": "無法檢查個人檔案標籤的唯一性", - "uniqueness-fail": "個人檔案標籤已被使用。試試另一個", + "discretionary": "Discretionary", + "edit-profile": "Edit Profile", + "edit-profile-pic": "Edit Profile Pic", + "follow": "Follow", + "following": "Following", + "invalid-characters": "Only alphanumeric characters and single spaces allowed", + "length-error": "Names must be less than 20 characters", + "market-maker": "Market Maker", + "no-followers": "No Followers", + "no-followers-desc": "Trading in stealth mode 😎", + "no-following": "No Accounts Followed", + "no-following-desc": "The lone sheep is in danger of the wolf", + "no-nfts": "😞 No NFTs found...", + "no-profile-exists": "This profile doesn't exist...", + "profile": "Profile", + "profile-api-error": "Profile update is unavailable. Please try again later", + "profile-fetch-fail": "Failed to fetch profile details", + "profile-name": "Profile Name", + "profile-pic-failure": "Failed to set profile pic", + "profile-pic-success": "Successfully set profile pic", + "profile-pic-remove-failure": "Failed to remove profile pic", + "profile-pic-remove-success": "Successfully removed profile pic", + "profile-update-fail": "Failed to update profile", + "profile-update-success": "Profile updated", + "remove": "Remove", + "save-profile": "Save Profile", + "set-profile-pic": "Set Profile Pic", + "swing-trader": "Swing Trader", + "total-pnl": "Total Portfolio PnL", + "total-value": "Total Portfolio Value", + "trader": "Trader", + "trader-category": "Trader Category", + "unfollow": "Unfollow", + "uniqueness-api-fail": "Failed to check profile name uniqueness", + "uniqueness-fail": "Profile name is taken. Try another one", "yolo": "YOLO", - "your-profile": "您的帳戶" + "your-profile": "Your Profile" } \ No newline at end of file From 797a7b31c64fd0144f802d8f31590424f7e35051 Mon Sep 17 00:00:00 2001 From: saml33 Date: Tue, 11 Apr 2023 20:33:58 +1000 Subject: [PATCH 52/71] increase mobile more menu height --- components/mobile/BottomBar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/mobile/BottomBar.tsx b/components/mobile/BottomBar.tsx index 818a2266..2d8566f5 100644 --- a/components/mobile/BottomBar.tsx +++ b/components/mobile/BottomBar.tsx @@ -107,14 +107,14 @@ const MoreMenuPanel = ({ const { t } = useTranslation(['common', 'search']) return (
setShowPanel(false)} hideBg> - +
Date: Tue, 11 Apr 2023 20:50:51 +1000 Subject: [PATCH 53/71] full width notifications on mobile --- components/shared/Notification.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/components/shared/Notification.tsx b/components/shared/Notification.tsx index dbc7c57f..f3d19a57 100644 --- a/components/shared/Notification.tsx +++ b/components/shared/Notification.tsx @@ -91,7 +91,7 @@ const NotificationList = () => { if (!mounted) return null return ( -
+
{notifications.filter((n) => n.show).length > 1 ? ( ) } diff --git a/components/stats/PerpStats.tsx b/components/stats/PerpStats.tsx deleted file mode 100644 index f3b87aa9..00000000 --- a/components/stats/PerpStats.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { PerpMarket } from '@blockworks-foundation/mango-v4' -import { useState } from 'react' -import PerpMarketDetails from './PerpMarketDetails' -import PerpMarketsTable from './PerpMarketsTable' - -const PerpStats = () => { - const [showPerpDetails, setShowPerpDetails] = useState( - null - ) - return !showPerpDetails ? ( - - ) : ( - - ) -} - -export default PerpStats diff --git a/components/stats/PerpStatsPage.tsx b/components/stats/PerpStatsPage.tsx new file mode 100644 index 00000000..887017b8 --- /dev/null +++ b/components/stats/PerpStatsPage.tsx @@ -0,0 +1,75 @@ +import useMangoGroup from 'hooks/useMangoGroup' +import { useRouter } from 'next/router' +import { useEffect, useMemo } from 'react' +import MarketLogos from '@components/trade/MarketLogos' +import mangoStore from '@store/mangoStore' +import PerpMarketDetails from './PerpMarketDetails' + +const PerpStatsPage = () => { + const router = useRouter() + const { market } = router.query + const { group } = useMangoGroup() + const perpStats = mangoStore((s) => s.perpStats.data) + // const [animationSettings] = useLocalStorageState( + // ANIMATION_SETTINGS_KEY, + // INITIAL_ANIMATION_SETTINGS + // ) + + useEffect(() => { + if (!perpStats || !perpStats.length) { + const actions = mangoStore.getState().actions + actions.fetchPerpStats() + } + }, [perpStats]) + + const marketDetails = useMemo(() => { + if (!group || !market) return + return group.getPerpMarketByName(market.toString().toUpperCase()) + }, [group, market]) + + const marketStats = useMemo(() => { + if (!marketDetails || !perpStats || !perpStats.length) return [] + const marketStats = perpStats + .filter((stat) => stat.market_index === marketDetails.perpMarketIndex) + .reverse() + // const change = marketStats.length + // ? ((marketDetails.uiPrice - marketStats[0].price) / + // marketStats[0].price) * + // 100 + // : 0 + return marketStats + }, [marketDetails, perpStats]) + + return marketDetails ? ( + <> +
+
+
+ +

{marketDetails.name}

+
+ {/*
+
+ {animationSettings['number-scroll'] ? ( + + ) : ( + + )} +
+ +
*/} +
+
+ + + ) : null +} + +export default PerpStatsPage diff --git a/components/stats/StatsPage.tsx b/components/stats/StatsPage.tsx index 682a256e..8fbf3127 100644 --- a/components/stats/StatsPage.tsx +++ b/components/stats/StatsPage.tsx @@ -1,11 +1,13 @@ import TabButtons from '@components/shared/TabButtons' import mangoStore from '@store/mangoStore' +import useLocalStorageState from 'hooks/useLocalStorageState' import useMangoGroup from 'hooks/useMangoGroup' import { useViewport } from 'hooks/useViewport' -import { useEffect, useMemo, useState } from 'react' +import { useEffect, useMemo } from 'react' +import { STATS_TAB_KEY } from 'utils/constants' import { breakpoints } from 'utils/theme' import MangoStats from './MangoStats' -import PerpStats from './PerpStats' +import PerpMarketsTable from './PerpMarketsTable' import SpotMarketsTable from './SpotMarketsTable' import TokenStats from './TokenStats' @@ -13,17 +15,21 @@ import TokenStats from './TokenStats' const TABS = ['tokens', 'perp-markets', 'spot-markets', 'mango-stats'] const StatsPage = () => { - const [activeTab, setActiveTab] = useState('tokens') + const [activeTab, setActiveTab] = useLocalStorageState( + STATS_TAB_KEY, + 'tokens' + ) const actions = mangoStore.getState().actions + const perpStats = mangoStore((s) => s.perpStats.data) const { group } = useMangoGroup() const { width } = useViewport() const fullWidthTabs = width ? width < breakpoints.lg : false useEffect(() => { - if (group) { + if (group && (!perpStats || !perpStats.length)) { actions.fetchPerpStats() } - }, [group]) + }, [group, perpStats]) const tabsWithCount: [string, number][] = useMemo(() => { return TABS.map((t) => [t, 0]) @@ -51,7 +57,7 @@ const TabContent = ({ activeTab }: { activeTab: string }) => { case 'tokens': return case 'perp-markets': - return + return case 'spot-markets': return case 'mango-stats': diff --git a/pages/perp-stats/[market].tsx b/pages/perp-stats/[market].tsx new file mode 100644 index 00000000..345c5114 --- /dev/null +++ b/pages/perp-stats/[market].tsx @@ -0,0 +1,32 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import PerpStatsPage from '@components/stats/PerpStatsPage' +import type { NextPage } from 'next' +import { serverSideTranslations } from 'next-i18next/serverSideTranslations' + +export async function getStaticProps({ locale }: { locale: string }) { + return { + props: { + ...(await serverSideTranslations(locale, [ + 'common', + 'profile', + 'search', + 'settings', + 'trade', + ])), + }, + } +} + +export async function getStaticPaths() { + return { paths: [], fallback: true } +} + +const PerpStats: NextPage = () => { + return ( +
+ +
+ ) +} + +export default PerpStats diff --git a/utils/constants.ts b/utils/constants.ts index 765da69e..44fce971 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -55,6 +55,8 @@ export const ACCEPT_TERMS_KEY = 'termsOfUseAccepted-0.1' export const TRADE_LAYOUT_KEY = 'tradeLayoutKey-0.1' +export const STATS_TAB_KEY = 'activeStatsTab-0.1' + // Unused export const PROFILE_CATEGORIES = [ 'borrower', From 7f88073f036ee2976e14604c6dfd8c12419cdb55 Mon Sep 17 00:00:00 2001 From: saml33 Date: Thu, 13 Apr 2023 10:31:22 +1000 Subject: [PATCH 57/71] fix token stats price chart x-axis --- components/token/CoingeckoStats.tsx | 2 +- components/token/PriceChart.tsx | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/components/token/CoingeckoStats.tsx b/components/token/CoingeckoStats.tsx index 80149650..513e44e7 100644 --- a/components/token/CoingeckoStats.tsx +++ b/components/token/CoingeckoStats.tsx @@ -75,7 +75,7 @@ const CoingeckoStats = ({ isLoading: loadingBirdeyePrices, isFetching: fetchingBirdeyePrices, } = useQuery( - ['birdeye-token-prices', daysToShow, bank], + ['birdeye-token-prices', daysToShow, bank.mint], () => fetchBirdeyePrices(daysToShow, bank.mint.toString()), { cacheTime: 1000 * 60 * 15, diff --git a/components/token/PriceChart.tsx b/components/token/PriceChart.tsx index f490ce02..98ffb693 100644 --- a/components/token/PriceChart.tsx +++ b/components/token/PriceChart.tsx @@ -1,4 +1,5 @@ import { formatDateAxis } from '@components/shared/DetailedAreaChart' +import dayjs from 'dayjs' import { BirdeyePriceResponse } from 'hooks/useBirdeyeMarketPrices' import { useTheme } from 'next-themes' import { useMemo } from 'react' @@ -60,7 +61,9 @@ const PriceChart = ({ fontSize: 10, }} tickLine={false} - tickFormatter={(d) => formatDateAxis(d, daysToShow)} + tickFormatter={(d) => + formatDateAxis(dayjs(d * 1000).toISOString(), daysToShow) + } /> Date: Thu, 13 Apr 2023 21:56:18 +1000 Subject: [PATCH 58/71] use url param --- components/stats/PerpMarketsTable.tsx | 9 ++------ components/stats/StatsPage.tsx | 8 ++++++- pages/perp-stats/[market].tsx | 32 --------------------------- 3 files changed, 9 insertions(+), 40 deletions(-) delete mode 100644 pages/perp-stats/[market].tsx diff --git a/components/stats/PerpMarketsTable.tsx b/components/stats/PerpMarketsTable.tsx index 6a351114..9e48d9b2 100644 --- a/components/stats/PerpMarketsTable.tsx +++ b/components/stats/PerpMarketsTable.tsx @@ -45,13 +45,8 @@ export const getOneDayPerpStats = ( } const goToPerpMarketDetails = (market: PerpMarket, router: NextRouter) => { - router.push( - { - pathname: `/perp-stats/${market.name}`, - query: { details: 'cool' }, - }, - `/perp-stats/${market.name}` - ) + const query = { ...router.query, ['market']: market.name } + router.push({ pathname: router.pathname, query }) } const PerpMarketsTable = () => { diff --git a/components/stats/StatsPage.tsx b/components/stats/StatsPage.tsx index 8fbf3127..356e855a 100644 --- a/components/stats/StatsPage.tsx +++ b/components/stats/StatsPage.tsx @@ -3,11 +3,13 @@ import mangoStore from '@store/mangoStore' import useLocalStorageState from 'hooks/useLocalStorageState' import useMangoGroup from 'hooks/useMangoGroup' import { useViewport } from 'hooks/useViewport' +import { useRouter } from 'next/router' import { useEffect, useMemo } from 'react' import { STATS_TAB_KEY } from 'utils/constants' import { breakpoints } from 'utils/theme' import MangoStats from './MangoStats' import PerpMarketsTable from './PerpMarketsTable' +import PerpStatsPage from './PerpStatsPage' import SpotMarketsTable from './SpotMarketsTable' import TokenStats from './TokenStats' @@ -24,6 +26,8 @@ const StatsPage = () => { const { group } = useMangoGroup() const { width } = useViewport() const fullWidthTabs = width ? width < breakpoints.lg : false + const router = useRouter() + const { market } = router.query useEffect(() => { if (group && (!perpStats || !perpStats.length)) { @@ -34,7 +38,9 @@ const StatsPage = () => { const tabsWithCount: [string, number][] = useMemo(() => { return TABS.map((t) => [t, 0]) }, []) - return ( + return market ? ( + + ) : (
{ - return ( -
- -
- ) -} - -export default PerpStats From 144d25fc9d0ed086cd1c3bca005d6cc064cac629 Mon Sep 17 00:00:00 2001 From: saml33 Date: Thu, 13 Apr 2023 22:35:23 +1000 Subject: [PATCH 59/71] save show zero balance state --- components/AccountsButton.tsx | 5 +++- components/TokenList.tsx | 44 +++++++++++++++++------------------ 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/components/AccountsButton.tsx b/components/AccountsButton.tsx index e74f69c1..552b5727 100644 --- a/components/AccountsButton.tsx +++ b/components/AccountsButton.tsx @@ -6,6 +6,7 @@ import { abbreviateAddress } from 'utils/formatting' import CreateAccountModal from './modals/CreateAccountModal' import { DEFAULT_DELEGATE } from './modals/DelegateModal' import MangoAccountsListModal from './modals/MangoAccountsListModal' +import SheenLoader from './shared/SheenLoader' import Tooltip from './shared/Tooltip' const AccountsButton = () => { @@ -44,7 +45,9 @@ const AccountsButton = () => { ) : null}
) : initialLoad ? ( - {t('loading')}... + +
+ ) : ( 🥭 diff --git a/components/TokenList.tsx b/components/TokenList.tsx index c180c402..a86d2ce7 100644 --- a/components/TokenList.tsx +++ b/components/TokenList.tsx @@ -5,11 +5,10 @@ import { EllipsisHorizontalIcon, QuestionMarkCircleIcon, } from '@heroicons/react/20/solid' -import { useWallet } from '@solana/wallet-adapter-react' import { useTranslation } from 'next-i18next' import Image from 'next/legacy/image' import { useRouter } from 'next/router' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' import { useViewport } from '../hooks/useViewport' import mangoStore from '@store/mangoStore' import { breakpoints } from '../utils/theme' @@ -24,7 +23,7 @@ import { Table, Td, Th, TrBody, TrHead } from './shared/TableElements' import DepositWithdrawModal from './modals/DepositWithdrawModal' import BorrowRepayModal from './modals/BorrowRepayModal' import { WRAPPED_SOL_MINT } from '@project-serum/serum/lib/token-instructions' -import { USDC_MINT } from 'utils/constants' +import { SHOW_ZERO_BALANCES_KEY, USDC_MINT } from 'utils/constants' import { PublicKey } from '@solana/web3.js' import ActionsLinkButton from './account/ActionsLinkButton' import FormatNumericValue from './shared/FormatNumericValue' @@ -33,12 +32,15 @@ import useBanksWithBalances, { BankWithBalance, } from 'hooks/useBanksWithBalances' import useUnownedAccount from 'hooks/useUnownedAccount' +import useLocalStorageState from 'hooks/useLocalStorageState' const TokenList = () => { const { t } = useTranslation(['common', 'token', 'trade']) - const { connected } = useWallet() - const [showZeroBalances, setShowZeroBalances] = useState(true) - const { mangoAccount } = useMangoAccount() + const [showZeroBalances, setShowZeroBalances] = useLocalStorageState( + SHOW_ZERO_BALANCES_KEY, + true + ) + const { mangoAccount, mangoAccountAddress } = useMangoAccount() const spotBalances = mangoStore((s) => s.mangoAccount.spotBalances) const { mangoTokens } = useJupiterMints() const totalInterestData = mangoStore( @@ -50,30 +52,26 @@ const TokenList = () => { const filteredBanks = useMemo(() => { if (banks.length) { - return showZeroBalances + return showZeroBalances || !mangoAccountAddress ? banks : banks.filter((b) => Math.abs(b.balance) > 0) } return [] - }, [banks, showZeroBalances]) - - useEffect(() => { - if (!connected) { - setShowZeroBalances(true) - } - }, [connected]) + }, [banks, mangoAccountAddress, showZeroBalances]) return ( -
- setShowZeroBalances(!showZeroBalances)} - > - {t('show-zero-balances')} - -
+ {mangoAccountAddress ? ( +
+ setShowZeroBalances(!showZeroBalances)} + > + {t('show-zero-balances')} + +
+ ) : null} {showTableView ? ( From b09596322e30bbaa563eea12be57dd31af68ba11 Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Thu, 13 Apr 2023 16:55:32 +0200 Subject: [PATCH 60/71] Revert "Revert "settle fees along with pnl"" This reverts commit bc113df1c3cc4112a44c28b9e720000402f05883. --- components/trade/UnsettledTrades.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/trade/UnsettledTrades.tsx b/components/trade/UnsettledTrades.tsx index 3b0366d8..f7447a89 100644 --- a/components/trade/UnsettledTrades.tsx +++ b/components/trade/UnsettledTrades.tsx @@ -104,11 +104,12 @@ const UnsettledTrades = ({ const unprofitableAccount = mangoAccountPnl > 0 ? settleCandidates[0].account : mangoAccount - const txid = await client.perpSettlePnl( + const txid = await client.perpSettlePnlAndFees( group, profitableAccount, unprofitableAccount, mangoAccount, + mangoAccount, market.perpMarketIndex ) actions.reloadMangoAccount() From 17ea3b2be4638f9ff4da0f83b329280f49387fa7 Mon Sep 17 00:00:00 2001 From: saml33 Date: Fri, 14 Apr 2023 09:40:20 +1000 Subject: [PATCH 61/71] swaps disabled message --- components/swap/SwapForm.tsx | 25 +++++++++++++------------ components/swap/SwapPage.tsx | 7 +++++++ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/components/swap/SwapForm.tsx b/components/swap/SwapForm.tsx index 4e6fcf52..8edbcafc 100644 --- a/components/swap/SwapForm.tsx +++ b/components/swap/SwapForm.tsx @@ -438,9 +438,9 @@ const SwapForm = () => { setShowConfirm={setShowConfirm} amountIn={amountInAsDecimal} inputSymbol={inputBank?.name} - amountOut={ - selectedRoute ? amountOutAsDecimal.toNumber() : undefined - } + // amountOut={ + // selectedRoute ? amountOutAsDecimal.toNumber() : undefined + // } isDelegatedAccount={isDelegatedAccount} /> ) : ( @@ -507,7 +507,7 @@ export default SwapForm const SwapFormSubmitButton = ({ amountIn, - amountOut, + // amountOut, inputSymbol, loadingSwapDetails, selectedRoute, @@ -516,7 +516,7 @@ const SwapFormSubmitButton = ({ isDelegatedAccount, }: { amountIn: Decimal - amountOut: number | undefined + // amountOut: number | undefined inputSymbol: string | undefined loadingSwapDetails: boolean selectedRoute: RouteInfo | undefined | null @@ -533,12 +533,12 @@ const SwapFormSubmitButton = ({ ? amountWithBorrow.lt(amountIn) : tokenMax.lt(amountIn) - const disabled = - connected && - (!amountIn.toNumber() || - showInsufficientBalance || - !amountOut || - !selectedRoute) + // const disabled = + // connected && + // (!amountIn.toNumber() || + // showInsufficientBalance || + // !amountOut || + // !selectedRoute) const onClick = connected ? () => setShowConfirm(true) : handleConnect @@ -547,7 +547,8 @@ const SwapFormSubmitButton = ({ ) : null} - {/* - - */} + + + ) : ( +
+ + {voteType !== undefined ? ( +
+

{t('current-vote')}

+ + {voteType === VoteKind.Approve ? ( + + + {t('yes')} + + ) : ( + + + {t('no')} + + )} + +
+ ) : null} +
+ )} + + {mangoMint && ( +
+ + +
+ )} + + ) : null +} + +export default ProposalCard diff --git a/components/governance/Vote/Vote.tsx b/components/governance/Vote/Vote.tsx new file mode 100644 index 00000000..16c0fa2b --- /dev/null +++ b/components/governance/Vote/Vote.tsx @@ -0,0 +1,117 @@ +import { ProgramAccount, Proposal, ProposalState } from '@solana/spl-governance' +import GovernanceStore from '@store/governanceStore' +import mangoStore from '@store/mangoStore' +import { useEffect, useState } from 'react' +import { isInCoolOffTime } from 'utils/governance/proposals' +import { MintInfo } from '@solana/spl-token' +import { MANGO_MINT } from 'utils/constants' +import { PublicKey } from '@solana/web3.js' +import dynamic from 'next/dynamic' +import { fmtTokenAmount, tryGetMint } from 'utils/governance/tools' +import { BN } from '@project-serum/anchor' +import { MANGO_MINT_DECIMALS } from 'utils/governance/constants' +import { useTranslation } from 'next-i18next' +import SheenLoader from '@components/shared/SheenLoader' +import { NoSymbolIcon } from '@heroicons/react/20/solid' + +const ProposalCard = dynamic(() => import('./ProposalCard')) +const OnBoarding = dynamic(() => import('../OnBoarding')) + +const Vote = () => { + const { t } = useTranslation('governance') + const connection = mangoStore((s) => s.connection) + const governances = GovernanceStore((s) => s.governances) + const proposals = GovernanceStore((s) => s.proposals) + const loadingProposals = GovernanceStore((s) => s.loadingProposals) + const voter = GovernanceStore((s) => s.voter) + const loadingVoter = GovernanceStore((s) => s.loadingVoter) + const loadingRealm = GovernanceStore((s) => s.loadingRealm) + + const [mangoMint, setMangoMint] = useState(null) + const [votingProposals, setVotingProposals] = useState< + ProgramAccount[] + >([]) + + useEffect(() => { + if (proposals) { + const activeProposals = Object.values(proposals).filter((x) => { + const governance = + governances && governances[x.account.governance.toBase58()] + const votingEnded = + governance && x.account.getTimeToVoteEnd(governance.account) < 0 + + const coolOff = isInCoolOffTime(x.account, governance?.account) + + return ( + !coolOff && !votingEnded && x.account.state === ProposalState.Voting + ) + }) + setVotingProposals(activeProposals) + } else { + setVotingProposals([]) + } + }, [governances, proposals]) + + useEffect(() => { + const handleGetMangoMint = async () => { + const mangoMint = await tryGetMint(connection, new PublicKey(MANGO_MINT)) + setMangoMint(mangoMint!.account) + } + handleGetMangoMint() + }, []) + + return ( +
+
+

{t('active-proposals')}

+

+ {t('your-votes')}{' '} + + {!loadingVoter + ? fmtTokenAmount(voter.voteWeight, MANGO_MINT_DECIMALS) + : 0} + +

+
+ {loadingProposals || loadingRealm ? ( +
+ +
+ + +
+ +
+ ) : ( + <> + {!loadingVoter ? ( + + ) : null} +
+ {votingProposals.length ? ( + votingProposals.map( + (x) => + mangoMint && ( + + ) + ) + ) : ( +
+
+ +

{t('no-active-proposals')}

+
+
+ )} +
+ + )} +
+ ) +} + +export default Vote diff --git a/components/governance/Vote/VoteCountdown.tsx b/components/governance/Vote/VoteCountdown.tsx new file mode 100644 index 00000000..acba7ac9 --- /dev/null +++ b/components/governance/Vote/VoteCountdown.tsx @@ -0,0 +1,115 @@ +import React, { useEffect, useState } from 'react' +import { Governance, Proposal } from '@solana/spl-governance' +import dayjs from 'dayjs' +import { useTranslation } from 'next-i18next' + +interface CountdownState { + days: number + hours: number + minutes: number + seconds: number +} + +const ZeroCountdown: CountdownState = { + days: 0, + hours: 0, + minutes: 0, + seconds: 0, +} + +const isZeroCountdown = (state: CountdownState) => + state.days === 0 && + state.hours === 0 && + state.minutes === 0 && + state.seconds === 0 + +export function VoteCountdown({ + proposal, + governance, +}: { + proposal: Proposal + governance: Governance +}) { + const { t } = useTranslation(['governance']) + + const [countdown, setCountdown] = useState(ZeroCountdown) + + useEffect(() => { + if (proposal.isVoteFinalized()) { + setCountdown(ZeroCountdown) + return + } + + const getTimeToVoteEnd = () => { + const now = dayjs().unix() + + let timeToVoteEnd = proposal.isPreVotingState() + ? governance.config.maxVotingTime + : (proposal.votingAt?.toNumber() ?? 0) + + governance.config.maxVotingTime - + now + + if (timeToVoteEnd <= 0) { + return ZeroCountdown + } + + const days = Math.floor(timeToVoteEnd / 86400) + timeToVoteEnd -= days * 86400 + + const hours = Math.floor(timeToVoteEnd / 3600) % 24 + timeToVoteEnd -= hours * 3600 + + const minutes = Math.floor(timeToVoteEnd / 60) % 60 + timeToVoteEnd -= minutes * 60 + + const seconds = Math.floor(timeToVoteEnd % 60) + + return { days, hours, minutes, seconds } + } + + const updateCountdown = () => { + const newState = getTimeToVoteEnd() + setCountdown(newState) + } + + const interval = setInterval(() => { + updateCountdown() + }, 1000) + + updateCountdown() + return () => clearInterval(interval) + }, [proposal, governance]) + + return ( + <> + {isZeroCountdown(countdown) ? ( +
{t('voting-ended')}
+ ) : ( +
+
{t('ends')}
+ {countdown && countdown.days > 0 && ( + <> +
+ {countdown.days}d +
+ : + + )} +
{countdown.hours}h
+ : +
+ {countdown.minutes}m +
+ {!countdown.days && ( + <> + : +
+ {countdown.seconds}s +
+ + )} +
+ )} + + ) +} diff --git a/components/governance/Vote/VotePage.tsx b/components/governance/Vote/VotePage.tsx new file mode 100644 index 00000000..e802fde6 --- /dev/null +++ b/components/governance/Vote/VotePage.tsx @@ -0,0 +1,15 @@ +import dynamic from 'next/dynamic' +import GovernancePageWrapper from '../GovernancePageWrapper' + +const Vote = dynamic(() => import('./Vote')) + +const VotePage = () => { + return ( +
+ + + +
+ ) +} +export default VotePage diff --git a/components/governance/Vote/VoteProgress.tsx b/components/governance/Vote/VoteProgress.tsx new file mode 100644 index 00000000..93c655cf --- /dev/null +++ b/components/governance/Vote/VoteProgress.tsx @@ -0,0 +1,95 @@ +import Tooltip from '@components/shared/Tooltip' +import { + CheckCircleIcon, + InformationCircleIcon, +} from '@heroicons/react/20/solid' +import { Governance, ProgramAccount, Proposal } from '@solana/spl-governance' +import { MintInfo } from '@solana/spl-token' +import GovernanceStore from '@store/governanceStore' +import { useTranslation } from 'next-i18next' +import { getMintMaxVoteWeight } from 'utils/governance/proposals' +import { fmtTokenAmount } from 'utils/governance/tools' + +type Props = { + governance: ProgramAccount + proposal: ProgramAccount + communityMint: MintInfo +} + +const QuorumProgress = ({ governance, proposal, communityMint }: Props) => { + const { t } = useTranslation(['governance']) + + const realm = GovernanceStore((s) => s.realm) + + const voteThresholdPct = + governance.account.config.communityVoteThreshold.value || 0 + + const maxVoteWeight = + realm && + getMintMaxVoteWeight( + communityMint, + realm.account.config.communityMintMaxVoteWeightSource + ) + + const minimumYesVotes = + fmtTokenAmount(maxVoteWeight!, communityMint.decimals) * + (voteThresholdPct / 100) + + const yesVoteCount = fmtTokenAmount( + proposal.account.getYesVoteCount(), + communityMint.decimals + ) + + const rawYesVotesRequired = minimumYesVotes - yesVoteCount + const votesRequiredInRange = rawYesVotesRequired < 0 ? 0 : rawYesVotesRequired + const yesVoteProgress = votesRequiredInRange + ? 100 - (votesRequiredInRange / minimumYesVotes) * 100 + : 100 + const yesVotesRequired = + communityMint.decimals == 0 + ? Math.ceil(votesRequiredInRange) + : votesRequiredInRange + + return ( +
+
+
+
+

{t('approval-q')}

+ + + +
+ {typeof yesVoteProgress !== 'undefined' && yesVoteProgress < 100 ? ( +

{`${( + yesVotesRequired ?? 0 + ).toLocaleString(undefined, { + maximumFractionDigits: 0, + })} ${(yesVoteProgress ?? 0) > 0 ? 'more' : ''} Yes vote${ + (yesVotesRequired ?? 0) > 1 ? 's' : '' + } required`}

+ ) : ( +
+ +

+ {t('required-approval-achieved')} +

+
+ )} +
+
+
+
= 100 ? 'bg-th-up' : 'bg-th-fgd-2' + } flex rounded`} + >
+
+
+ ) +} + +export default QuorumProgress diff --git a/components/governance/Vote/VoteResult.tsx b/components/governance/Vote/VoteResult.tsx new file mode 100644 index 00000000..6e795878 --- /dev/null +++ b/components/governance/Vote/VoteResult.tsx @@ -0,0 +1,67 @@ +import { Proposal } from '@solana/spl-governance' +import VoteResultsBar from './VoteResultBar' +import { fmtTokenAmount } from 'utils/governance/tools' +import { MintInfo } from '@solana/spl-token' +import { useTranslation } from 'next-i18next' + +type VoteResultsProps = { + proposal: Proposal + communityMint: MintInfo +} + +const VoteResults = ({ proposal, communityMint }: VoteResultsProps) => { + const { t } = useTranslation(['governance']) + + const yesVoteCount = fmtTokenAmount( + proposal.getYesVoteCount(), + communityMint.decimals + ) + const noVoteCount = fmtTokenAmount( + proposal.getNoVoteCount(), + communityMint.decimals + ) + const totalVoteCount = yesVoteCount + noVoteCount + const getRelativeVoteCount = (voteCount: number) => + totalVoteCount === 0 ? 0 : (voteCount / totalVoteCount) * 100 + const relativeYesVotes = getRelativeVoteCount(yesVoteCount) + const relativeNoVotes = getRelativeVoteCount(noVoteCount) + + return ( +
+ {proposal ? ( +
+
+
+

{t('yes-votes')}

+

+ {(yesVoteCount ?? 0).toLocaleString()} + + {relativeYesVotes?.toFixed(1)}% + +

+
+
+

{t('no-votes')}

+

+ {(noVoteCount ?? 0).toLocaleString()} + + {relativeNoVotes?.toFixed(1)}% + +

+
+
+ +
+ ) : ( + <> +
+ + )} +
+ ) +} + +export default VoteResults diff --git a/components/governance/Vote/VoteResultBar.tsx b/components/governance/Vote/VoteResultBar.tsx new file mode 100644 index 00000000..8fbede2f --- /dev/null +++ b/components/governance/Vote/VoteResultBar.tsx @@ -0,0 +1,42 @@ +type VoteResultsBarProps = { + approveVotePercentage: number + denyVotePercentage: number +} + +const VoteResultsBar = ({ + approveVotePercentage = 0, + denyVotePercentage = 0, +}: VoteResultsBarProps) => { + return ( + <> +
+
2 || approveVotePercentage < 0.01 + ? approveVotePercentage + : 2 + }%`, + }} + className={`flex rounded-l bg-th-up ${ + denyVotePercentage < 0.01 && 'rounded' + }`} + >
+
2 || denyVotePercentage < 0.01 + ? denyVotePercentage + : 2 + }%`, + }} + className={`flex rounded-r bg-th-down ${ + approveVotePercentage < 0.01 && 'rounded' + }`} + >
+
+ + ) +} + +export default VoteResultsBar diff --git a/components/mobile/BottomBar.tsx b/components/mobile/BottomBar.tsx index 2d8566f5..845e6b01 100644 --- a/components/mobile/BottomBar.tsx +++ b/components/mobile/BottomBar.tsx @@ -146,6 +146,11 @@ const MoreMenuPanel = ({ path="/governance/listToken" icon={} /> + } + /> import('@components/governance/ListToken/ListTokenPage') +) export async function getStaticProps({ locale }: { locale: string }) { return { @@ -17,7 +21,7 @@ export async function getStaticProps({ locale }: { locale: string }) { } const Governance: NextPage = () => { - return + return } export default Governance diff --git a/pages/governance/vote.tsx b/pages/governance/vote.tsx new file mode 100644 index 00000000..32fd7030 --- /dev/null +++ b/pages/governance/vote.tsx @@ -0,0 +1,25 @@ +import type { NextPage } from 'next' +import { serverSideTranslations } from 'next-i18next/serverSideTranslations' +import dynamic from 'next/dynamic' + +const VotePage = dynamic(() => import('@components/governance/Vote/VotePage')) + +export async function getStaticProps({ locale }: { locale: string }) { + return { + props: { + ...(await serverSideTranslations(locale, [ + 'governance', + 'search', + 'common', + 'onboarding', + 'profile', + ])), + }, + } +} + +const ListToken: NextPage = () => { + return +} + +export default ListToken diff --git a/public/locales/en/common.json b/public/locales/en/common.json index e732f01c..1accc710 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -167,6 +167,7 @@ "wallet-disconnected": "Disconnected from wallet", "withdraw": "Withdraw", "withdraw-amount": "Withdraw Amount", - "list-token": "List Token" + "list-token": "List Token", + "vote": "Vote" } \ No newline at end of file diff --git a/public/locales/en/governance.json b/public/locales/en/governance.json index 40f107d8..72795295 100644 --- a/public/locales/en/governance.json +++ b/public/locales/en/governance.json @@ -3,7 +3,7 @@ "cancel": "Cancel", "connect-wallet": "Connect Wallet", "on-boarding-title": "Looks like currently connected wallet doesn't have any MNGO deposited inside realms", - "on-boarding-description": "Before you continue. Deposit {{amount}} MNGO into", + "on-boarding-description": "Before you continue. Deposit at least {{amount}} MNGO into", "on-boarding-deposit-info": "Your MNGO will be locked for the duration of the proposal.", "tokens-deposited": "Tokens Deposited", "new-listing": "New Token Listing", @@ -54,5 +54,21 @@ "market-name": "Market Name", "proposal-title": "Proposal Title", "proposal-des": "Proposal Description", - "error-proposal-creation": "Error on proposal creation or no confirmation of transactions" + "error-proposal-creation": "Error on proposal creation or no confirmation of transactions", + "vote-yes": "Vote Yes", + "vote-no": "Vote No", + "relinquish-vote": "Relinquish Vote", + "voting-ended": "Voting ended", + "ends": "Ends", + "approval-q": "Approval Quorum", + "required-approval-achieved": "Required approval achieved", + "no-votes": "No Votes", + "yes-votes": "Yes Votes", + "quorum-description": "Proposals must reach a minimum number of 'Yes' votes before they are eligible to pass. If the minimum is reached but there are more 'No' votes when voting ends the proposal will fail.", + "active-proposals": "Active Proposals", + "no-active-proposals": "No active proposals", + "your-votes": "Your Votes:", + "yes": "Yes", + "no": "No", + "current-vote": "Current Vote:" } \ No newline at end of file diff --git a/public/locales/es/common.json b/public/locales/es/common.json index e732f01c..1accc710 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -167,6 +167,7 @@ "wallet-disconnected": "Disconnected from wallet", "withdraw": "Withdraw", "withdraw-amount": "Withdraw Amount", - "list-token": "List Token" + "list-token": "List Token", + "vote": "Vote" } \ No newline at end of file diff --git a/public/locales/es/governance.json b/public/locales/es/governance.json index 40f107d8..72795295 100644 --- a/public/locales/es/governance.json +++ b/public/locales/es/governance.json @@ -3,7 +3,7 @@ "cancel": "Cancel", "connect-wallet": "Connect Wallet", "on-boarding-title": "Looks like currently connected wallet doesn't have any MNGO deposited inside realms", - "on-boarding-description": "Before you continue. Deposit {{amount}} MNGO into", + "on-boarding-description": "Before you continue. Deposit at least {{amount}} MNGO into", "on-boarding-deposit-info": "Your MNGO will be locked for the duration of the proposal.", "tokens-deposited": "Tokens Deposited", "new-listing": "New Token Listing", @@ -54,5 +54,21 @@ "market-name": "Market Name", "proposal-title": "Proposal Title", "proposal-des": "Proposal Description", - "error-proposal-creation": "Error on proposal creation or no confirmation of transactions" + "error-proposal-creation": "Error on proposal creation or no confirmation of transactions", + "vote-yes": "Vote Yes", + "vote-no": "Vote No", + "relinquish-vote": "Relinquish Vote", + "voting-ended": "Voting ended", + "ends": "Ends", + "approval-q": "Approval Quorum", + "required-approval-achieved": "Required approval achieved", + "no-votes": "No Votes", + "yes-votes": "Yes Votes", + "quorum-description": "Proposals must reach a minimum number of 'Yes' votes before they are eligible to pass. If the minimum is reached but there are more 'No' votes when voting ends the proposal will fail.", + "active-proposals": "Active Proposals", + "no-active-proposals": "No active proposals", + "your-votes": "Your Votes:", + "yes": "Yes", + "no": "No", + "current-vote": "Current Vote:" } \ No newline at end of file diff --git a/public/locales/ru/common.json b/public/locales/ru/common.json index e732f01c..1accc710 100644 --- a/public/locales/ru/common.json +++ b/public/locales/ru/common.json @@ -167,6 +167,7 @@ "wallet-disconnected": "Disconnected from wallet", "withdraw": "Withdraw", "withdraw-amount": "Withdraw Amount", - "list-token": "List Token" + "list-token": "List Token", + "vote": "Vote" } \ No newline at end of file diff --git a/public/locales/ru/governance.json b/public/locales/ru/governance.json index 40f107d8..72795295 100644 --- a/public/locales/ru/governance.json +++ b/public/locales/ru/governance.json @@ -3,7 +3,7 @@ "cancel": "Cancel", "connect-wallet": "Connect Wallet", "on-boarding-title": "Looks like currently connected wallet doesn't have any MNGO deposited inside realms", - "on-boarding-description": "Before you continue. Deposit {{amount}} MNGO into", + "on-boarding-description": "Before you continue. Deposit at least {{amount}} MNGO into", "on-boarding-deposit-info": "Your MNGO will be locked for the duration of the proposal.", "tokens-deposited": "Tokens Deposited", "new-listing": "New Token Listing", @@ -54,5 +54,21 @@ "market-name": "Market Name", "proposal-title": "Proposal Title", "proposal-des": "Proposal Description", - "error-proposal-creation": "Error on proposal creation or no confirmation of transactions" + "error-proposal-creation": "Error on proposal creation or no confirmation of transactions", + "vote-yes": "Vote Yes", + "vote-no": "Vote No", + "relinquish-vote": "Relinquish Vote", + "voting-ended": "Voting ended", + "ends": "Ends", + "approval-q": "Approval Quorum", + "required-approval-achieved": "Required approval achieved", + "no-votes": "No Votes", + "yes-votes": "Yes Votes", + "quorum-description": "Proposals must reach a minimum number of 'Yes' votes before they are eligible to pass. If the minimum is reached but there are more 'No' votes when voting ends the proposal will fail.", + "active-proposals": "Active Proposals", + "no-active-proposals": "No active proposals", + "your-votes": "Your Votes:", + "yes": "Yes", + "no": "No", + "current-vote": "Current Vote:" } \ No newline at end of file diff --git a/public/locales/zh/common.json b/public/locales/zh/common.json index e732f01c..1accc710 100644 --- a/public/locales/zh/common.json +++ b/public/locales/zh/common.json @@ -167,6 +167,7 @@ "wallet-disconnected": "Disconnected from wallet", "withdraw": "Withdraw", "withdraw-amount": "Withdraw Amount", - "list-token": "List Token" + "list-token": "List Token", + "vote": "Vote" } \ No newline at end of file diff --git a/public/locales/zh/governance.json b/public/locales/zh/governance.json index 40f107d8..72795295 100644 --- a/public/locales/zh/governance.json +++ b/public/locales/zh/governance.json @@ -3,7 +3,7 @@ "cancel": "Cancel", "connect-wallet": "Connect Wallet", "on-boarding-title": "Looks like currently connected wallet doesn't have any MNGO deposited inside realms", - "on-boarding-description": "Before you continue. Deposit {{amount}} MNGO into", + "on-boarding-description": "Before you continue. Deposit at least {{amount}} MNGO into", "on-boarding-deposit-info": "Your MNGO will be locked for the duration of the proposal.", "tokens-deposited": "Tokens Deposited", "new-listing": "New Token Listing", @@ -54,5 +54,21 @@ "market-name": "Market Name", "proposal-title": "Proposal Title", "proposal-des": "Proposal Description", - "error-proposal-creation": "Error on proposal creation or no confirmation of transactions" + "error-proposal-creation": "Error on proposal creation or no confirmation of transactions", + "vote-yes": "Vote Yes", + "vote-no": "Vote No", + "relinquish-vote": "Relinquish Vote", + "voting-ended": "Voting ended", + "ends": "Ends", + "approval-q": "Approval Quorum", + "required-approval-achieved": "Required approval achieved", + "no-votes": "No Votes", + "yes-votes": "Yes Votes", + "quorum-description": "Proposals must reach a minimum number of 'Yes' votes before they are eligible to pass. If the minimum is reached but there are more 'No' votes when voting ends the proposal will fail.", + "active-proposals": "Active Proposals", + "no-active-proposals": "No active proposals", + "your-votes": "Your Votes:", + "yes": "Yes", + "no": "No", + "current-vote": "Current Vote:" } \ No newline at end of file diff --git a/public/locales/zh_tw/common.json b/public/locales/zh_tw/common.json index d69975fd..801ab217 100644 --- a/public/locales/zh_tw/common.json +++ b/public/locales/zh_tw/common.json @@ -1,99 +1,4 @@ { - "404-description": "或者從來不存在...", - "404-heading": "此頁被清算了", - "accept-terms": "接受條款", - "accept-terms-desc": "繼續,即表示您接受Mango", - "account": "帳戶", - "account-balance": "帳戶餘額", - "account-closed": "關戶成功👋", - "account-name": "帳戶標籤", - "account-name-desc": "以標籤來整理帳戶", - "account-settings": "帳戶設定", - "account-update-failed": "更新帳戶出錯", - "account-update-success": "更新帳戶成功", - "account-value": "帳戶價值", - "accounts": "帳戶", - "actions": "動作", - "add-new-account": "開戶", - "agree-and-continue": "同意並繼續", - "all": "全部", - "amount": "數量", - "amount-owed": "欠款", - "asset-liability-weight": "資產/債務權重", - "asset-liability-weight-desc": "資產權重在賬戶健康計算中對質押品價值進行扣減。資產權重越低,資產對質押品的影響越小。債務權重恰恰相反(在健康計算中增加債務價值)。", - "asset-weight": "資產權重", - "asset-weight-desc": "資產權重在賬戶健康計算中對質押品價值進行扣減。資產權重越低,資產對質押品的影響越小。", - "available": "可用", - "available-balance": "可用餘額", - "bal": "餘額", - "balance": "餘額", - "balances": "餘額", - "borrow": "借貨", - "borrow-amount": "借貸數量", - "borrow-fee": "借貸費用", - "borrow-funds": "進行借貸", - "borrow-rate": "借貸APR", - "buy": "買", - "cancel": "取消", - "chart-unavailable": "無法顯示圖表", - "clear-all": "清除全部", - "close": "關閉", - "close-account": "關戶", - "close-account-desc": "你確定嗎? 關戶就無法恢復", - "closing-account": "正在關閉帳戶...", - "collateral-value": "質押品價值", - "connect": "連接", - "connect-balances": "連接而查看資產餘額", - "connect-helper": "連接來開始", - "copy-address": "拷貝地址", - "copy-address-success": "地址被拷貝: {{pk}}", - "country-not-allowed": "你的國家{{country}}不允許使用Mango", - "country-not-allowed-tooltip": "你正在使用MangoDAO提供的開源介面。由於監管的不確定性因此處於謀些地區的人的行動會受到限制。", - "create-account": "開戶", - "creating-account": "正在開戶...", - "cumulative-interest-value": "總累積利息", - "daily-volume": "24小時交易量", - "date": "日期", - "date-from": "從", - "date-to": "至", - "delegate": "委託", - "delegate-account": "委託帳戶", - "delegate-account-info": "帳戶委託給 {{address}}", - "delegate-desc": "以Mango帳戶委託給別的錢包地址", - "delegate-placeholder": "輸入受委錢包地執", - "delete": "刪除", - "deposit": "存款", - "deposit-amount": "存款數量", - "deposit-more-sol": "您的SOL錢包餘額太低。請多存入以支付交易", - "deposit-rate": "存款APR", - "disconnect": "斷開連接", - "documentation": "文檔", - "edit": "編輯", - "edit-account": "編輯帳戶標籤", - "edit-profile-image": "切換頭像", - "explorer": "瀏覽器", - "fee": "費用", - "fees": "費用", - "free-collateral": "可用的質押品", - "funding": "資金費", - "get-started": "開始", - "governance": "治理", - "health": "健康度", - "health-impact": "健康影響", - "health-tooltip": "此在您進行交易之前預測您賬戶的健康狀況。第一個值是您當前的帳戶健康狀況,第二個值是您的預測帳戶健康狀況。", - "history": "紀錄", - "insufficient-sol": "Solana需要0.04454 SOL租金才能創建Mango賬戶。您關閉帳戶時租金將被退還。", - "interest-earned": "獲取利息", - "interest-earned-paid": "獲取利息", - "leaderboard": "排行榜", - "learn": "學", - "leverage": "槓桿", - "liability-weight": "債務權重", - "liquidity": "流動性", - "list-token": "創造市場", - "loading": "加載中", - "loan-origination-fee": "借貸費用", - "loan-origination-fee-tooltip": "執行借貸費用是{{fee}}。", "mango": "Mango", "mango-stats": "Mango統計", "market": "市場", @@ -167,5 +72,7 @@ "wallet-balance": "錢包餘額", "wallet-disconnected": "已斷開錢包連接", "withdraw": "取款", - "withdraw-amount": "取款額" -} \ No newline at end of file + "withdraw-amount": "取款額", + "list-token": "列表代币", + "vote": "投票" +} diff --git a/public/locales/zh_tw/governance.json b/public/locales/zh_tw/governance.json index 40f107d8..72795295 100644 --- a/public/locales/zh_tw/governance.json +++ b/public/locales/zh_tw/governance.json @@ -3,7 +3,7 @@ "cancel": "Cancel", "connect-wallet": "Connect Wallet", "on-boarding-title": "Looks like currently connected wallet doesn't have any MNGO deposited inside realms", - "on-boarding-description": "Before you continue. Deposit {{amount}} MNGO into", + "on-boarding-description": "Before you continue. Deposit at least {{amount}} MNGO into", "on-boarding-deposit-info": "Your MNGO will be locked for the duration of the proposal.", "tokens-deposited": "Tokens Deposited", "new-listing": "New Token Listing", @@ -54,5 +54,21 @@ "market-name": "Market Name", "proposal-title": "Proposal Title", "proposal-des": "Proposal Description", - "error-proposal-creation": "Error on proposal creation or no confirmation of transactions" + "error-proposal-creation": "Error on proposal creation or no confirmation of transactions", + "vote-yes": "Vote Yes", + "vote-no": "Vote No", + "relinquish-vote": "Relinquish Vote", + "voting-ended": "Voting ended", + "ends": "Ends", + "approval-q": "Approval Quorum", + "required-approval-achieved": "Required approval achieved", + "no-votes": "No Votes", + "yes-votes": "Yes Votes", + "quorum-description": "Proposals must reach a minimum number of 'Yes' votes before they are eligible to pass. If the minimum is reached but there are more 'No' votes when voting ends the proposal will fail.", + "active-proposals": "Active Proposals", + "no-active-proposals": "No active proposals", + "your-votes": "Your Votes:", + "yes": "Yes", + "no": "No", + "current-vote": "Current Vote:" } \ No newline at end of file diff --git a/store/governanceStore.ts b/store/governanceStore.ts index 65b0d393..05cd56c6 100644 --- a/store/governanceStore.ts +++ b/store/governanceStore.ts @@ -1,5 +1,7 @@ import { AnchorProvider, BN } from '@project-serum/anchor' import { + getAllProposals, + getProposal, getTokenOwnerRecord, getTokenOwnerRecordAddress, Governance, @@ -17,8 +19,8 @@ import { } from 'utils/governance/constants' import { getDeposits } from 'utils/governance/fetch/deposits' import { + accountsToPubkeyMap, fetchGovernances, - fetchProposals, fetchRealm, } from 'utils/governance/tools' import { ConnectionContext, EndpointTypes } from 'utils/governance/types' @@ -37,19 +39,21 @@ type IGovernanceStore = { vsrClient: VsrClient | null loadingRealm: boolean loadingVoter: boolean + loadingProposals: boolean voter: { voteWeight: BN - wallet: PublicKey tokenOwnerRecord: ProgramAccount | undefined | null } set: (x: (x: IGovernanceStore) => void) => void initConnection: (connection: Connection) => void initRealm: (connectionContext: ConnectionContext) => void - fetchVoterWeight: ( + fetchVoter: ( wallet: PublicKey, vsrClient: VsrClient, connectionContext: ConnectionContext ) => void + resetVoter: () => void + updateProposals: (proposalPk: PublicKey) => void } const GovernanceStore = create((set, get) => ({ @@ -60,13 +64,13 @@ const GovernanceStore = create((set, get) => ({ vsrClient: null, loadingRealm: false, loadingVoter: false, + loadingProposals: false, voter: { voteWeight: new BN(0), - wallet: PublicKey.default, tokenOwnerRecord: null, }, set: (fn) => set(produce(fn)), - fetchVoterWeight: async ( + fetchVoter: async ( wallet: PublicKey, vsrClient: VsrClient, connectionContext: ConnectionContext @@ -99,11 +103,17 @@ const GovernanceStore = create((set, get) => ({ }) set((state) => { state.voter.voteWeight = votingPower - state.voter.wallet = wallet state.voter.tokenOwnerRecord = tokenOwnerRecord state.loadingVoter = false }) }, + resetVoter: () => { + const set = get().set + set((state) => { + state.voter.voteWeight = new BN(0) + state.voter.tokenOwnerRecord = null + }) + }, initConnection: async (connection) => { const set = get().set const connectionContext = { @@ -141,18 +151,40 @@ const GovernanceStore = create((set, get) => ({ realmId: MANGO_REALM_PK, }), ]) - const proposals = await fetchProposals({ - connectionContext: connectionContext, - programId: MANGO_GOVERNANCE_PROGRAM, - governances: Object.keys(governances).map((x) => new PublicKey(x)), - }) set((state) => { + state.loadingProposals = true + }) + const proposals = await getAllProposals( + connectionContext.current, + MANGO_GOVERNANCE_PROGRAM, + MANGO_REALM_PK + ) + const proposalsObj = accountsToPubkeyMap(proposals.flatMap((p) => p)) + set((state) => { + state.loadingProposals = false state.realm = realm state.governances = governances - state.proposals = proposals + state.proposals = proposalsObj state.loadingRealm = false }) }, + updateProposals: async (proposalPk: PublicKey) => { + const state = get() + const set = get().set + set((state) => { + state.loadingProposals = true + }) + const proposal = await getProposal( + state.connectionContext!.current!, + proposalPk + ) + const newProposals = { ...state.proposals } + newProposals[proposal.pubkey.toBase58()] = proposal + set((state) => { + state.proposals = newProposals + state.loadingProposals = false + }) + }, })) export default GovernanceStore diff --git a/utils/governance/fetch/deposits.ts b/utils/governance/fetch/deposits.ts index c73030b9..40d74dd7 100644 --- a/utils/governance/fetch/deposits.ts +++ b/utils/governance/fetch/deposits.ts @@ -1,4 +1,4 @@ -import { MintInfo } from '@blockworks-foundation/mango-v4' +import { MintInfo } from '@solana/spl-token' import { BN, EventParser } from '@coral-xyz/anchor' import { Connection, PublicKey, Transaction } from '@solana/web3.js' import { diff --git a/utils/governance/fetch/getProposals.ts b/utils/governance/fetch/getProposals.ts deleted file mode 100644 index ff876100..00000000 --- a/utils/governance/fetch/getProposals.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { bs58 } from '@project-serum/anchor/dist/cjs/utils/bytes' -import { - deserializeBorsh, - getGovernanceSchemaForAccount, - GovernanceAccountType, - ProgramAccount, - Proposal, -} from '@solana/spl-governance' -import { PublicKey } from '@solana/web3.js' -import { ConnectionContext } from '../types' - -export const getProposals = async ( - pubkeys: PublicKey[], - connection: ConnectionContext, - programId: PublicKey -) => { - const proposalsRaw = await fetch(connection.endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify([ - ...pubkeys.map((x) => { - return getProposalsFilter( - programId, - connection, - bs58.encode(Uint8Array.from([GovernanceAccountType.ProposalV1])), - x - ) - }), - ...pubkeys.map((x) => { - return getProposalsFilter( - programId, - connection, - bs58.encode(Uint8Array.from([GovernanceAccountType.ProposalV2])), - x - ) - }), - ]), - }) - - const accounts: ProgramAccount[] = [] - const proposalsData = await proposalsRaw.json() - - const rawAccounts = proposalsData - ? // eslint-disable-next-line @typescript-eslint/no-explicit-any - proposalsData.flatMap((x: any) => x.result) - : [] - for (const rawAccount of rawAccounts) { - try { - const getSchema = getGovernanceSchemaForAccount - const data = Buffer.from(rawAccount.account.data[0], 'base64') - const accountType = data[0] - const account: ProgramAccount = { - pubkey: new PublicKey(rawAccount.pubkey), - account: deserializeBorsh(getSchema(accountType), Proposal, data), - owner: new PublicKey(rawAccount.account.owner), - } - - accounts.push(account) - } catch (ex) { - console.info(`Can't deserialize @ ${rawAccount.pubkey}, ${ex}.`) - } - } - const acc: ProgramAccount[][] = [] - const reducedAccounts = accounts.reduce((acc, current) => { - const exsitingIdx = acc.findIndex((x) => - x.find( - (x) => - x.account.governance.toBase58() === - current.account.governance.toBase58() - ) - ) - if (exsitingIdx > -1) { - acc[exsitingIdx].push(current) - } else { - acc.push([current]) - } - return acc - }, acc) - return reducedAccounts -} - -const getProposalsFilter = ( - programId: PublicKey, - connection: ConnectionContext, - memcmpBytes: string, - pk: PublicKey -) => { - return { - jsonrpc: '2.0', - id: 1, - method: 'getProgramAccounts', - params: [ - programId.toBase58(), - { - commitment: connection.current.commitment, - encoding: 'base64', - filters: [ - { - memcmp: { - offset: 0, // number of bytes - bytes: memcmpBytes, // base58 encoded string - }, - }, - { - memcmp: { - offset: 1, - bytes: pk.toBase58(), - }, - }, - ], - }, - ], - } -} diff --git a/utils/governance/instructions/castVote.ts b/utils/governance/instructions/castVote.ts new file mode 100644 index 00000000..eddb4b94 --- /dev/null +++ b/utils/governance/instructions/castVote.ts @@ -0,0 +1,127 @@ +import { Connection, Keypair, TransactionInstruction } from '@solana/web3.js' +import { + ChatMessageBody, + getGovernanceProgramVersion, + GOVERNANCE_CHAT_PROGRAM_ID, + Proposal, + TokenOwnerRecord, + VoteChoice, + VoteKind, + withPostChatMessage, +} from '@solana/spl-governance' +import { ProgramAccount } from '@solana/spl-governance' + +import { Vote } from '@solana/spl-governance' + +import { withCastVote } from '@solana/spl-governance' +import { VsrClient } from '../voteStakeRegistryClient' +import { MANGO_GOVERNANCE_PROGRAM, MANGO_REALM_PK } from '../constants' +import { updateVoterWeightRecord } from './updateVoteWeightRecord' +import { WalletContextState } from '@solana/wallet-adapter-react' +import { MangoClient } from '@blockworks-foundation/mango-v4' +import { notify } from 'utils/notifications' + +export async function castVote( + connection: Connection, + wallet: WalletContextState, + proposal: ProgramAccount, + tokenOwnerRecord: ProgramAccount, + voteKind: VoteKind, + vsrClient: VsrClient, + mangoClient: MangoClient, + message?: ChatMessageBody | undefined +) { + const signers: Keypair[] = [] + const instructions: TransactionInstruction[] = [] + + const walletPubkey = wallet.publicKey! + const governanceAuthority = walletPubkey + const payer = walletPubkey + const programVersion = await getGovernanceProgramVersion( + connection, + MANGO_GOVERNANCE_PROGRAM + ) + + const { updateVoterWeightRecordIx, voterWeightPk } = + await updateVoterWeightRecord(vsrClient, walletPubkey) + instructions.push(updateVoterWeightRecordIx) + + // It is not clear that defining these extraneous fields, `deny` and `veto`, is actually necessary. + // See: https://discord.com/channels/910194960941338677/910630743510777926/1044741454175674378 + const vote = + voteKind === VoteKind.Approve + ? new Vote({ + voteType: VoteKind.Approve, + approveChoices: [new VoteChoice({ rank: 0, weightPercentage: 100 })], + deny: undefined, + veto: undefined, + }) + : voteKind === VoteKind.Deny + ? new Vote({ + voteType: VoteKind.Deny, + approveChoices: undefined, + deny: true, + veto: undefined, + }) + : voteKind == VoteKind.Veto + ? new Vote({ + voteType: VoteKind.Veto, + veto: true, + deny: undefined, + approveChoices: undefined, + }) + : new Vote({ + voteType: VoteKind.Abstain, + veto: undefined, + deny: undefined, + approveChoices: undefined, + }) + + const tokenMint = proposal.account.governingTokenMint + + await withCastVote( + instructions, + MANGO_GOVERNANCE_PROGRAM, + programVersion, + MANGO_REALM_PK, + proposal.account.governance, + proposal.pubkey, + proposal.account.tokenOwnerRecord, + tokenOwnerRecord.pubkey, + governanceAuthority, + tokenMint, + vote, + payer, + voterWeightPk + ) + + if (message) { + const { updateVoterWeightRecordIx, voterWeightPk } = + await updateVoterWeightRecord(vsrClient, walletPubkey) + instructions.push(updateVoterWeightRecordIx) + + await withPostChatMessage( + instructions, + signers, + GOVERNANCE_CHAT_PROGRAM_ID, + MANGO_GOVERNANCE_PROGRAM, + MANGO_REALM_PK, + proposal.account.governance, + proposal.pubkey, + tokenOwnerRecord.pubkey, + governanceAuthority, + payer, + undefined, + message, + voterWeightPk + ) + } + + const tx = await mangoClient.sendAndConfirmTransaction(instructions) + notify({ + title: 'Transaction confirmed', + type: 'success', + txid: tx, + noSound: true, + }) +} diff --git a/utils/governance/instructions/createProposal.ts b/utils/governance/instructions/createProposal.ts index b1a3e19b..3a3b1c08 100644 --- a/utils/governance/instructions/createProposal.ts +++ b/utils/governance/instructions/createProposal.ts @@ -4,7 +4,6 @@ import { getSignatoryRecordAddress, ProgramAccount, serializeInstructionToBase64, - SYSTEM_PROGRAM_ID, TokenOwnerRecord, VoteType, WalletSigner, @@ -22,12 +21,8 @@ import { import { chunk } from 'lodash' import { MANGO_MINT } from 'utils/constants' import { MANGO_GOVERNANCE_PROGRAM, MANGO_REALM_PK } from '../constants' -import { DEFAULT_VSR_ID, VsrClient } from '../voteStakeRegistryClient' -import { - getRegistrarPDA, - getVoterPDA, - getVoterWeightPDA, -} from '../accounts/vsrAccounts' +import { VsrClient } from '../voteStakeRegistryClient' +import { updateVoterWeightRecord } from './updateVoteWeightRecord' export const createProposal = async ( connection: Connection, @@ -57,27 +52,8 @@ export const createProposal = async ( const options = ['Approve'] const useDenyOption = true - //will run only if plugin is connected with realm - const { registrar } = await getRegistrarPDA( - MANGO_REALM_PK, - new PublicKey(MANGO_MINT), - DEFAULT_VSR_ID - ) - const { voter } = await getVoterPDA(registrar, walletPk, DEFAULT_VSR_ID) - const { voterWeightPk } = await getVoterWeightPDA( - registrar, - walletPk, - DEFAULT_VSR_ID - ) - const updateVoterWeightRecordIx = await client.program.methods - .updateVoterWeightRecord() - .accounts({ - registrar, - voter, - voterWeightRecord: voterWeightPk, - systemProgram: SYSTEM_PROGRAM_ID, - }) - .instruction() + const { updateVoterWeightRecordIx, voterWeightPk } = + await updateVoterWeightRecord(client, walletPk) instructions.push(updateVoterWeightRecordIx) const proposalAddress = await withCreateProposal( diff --git a/utils/governance/instructions/relinquishVote.ts b/utils/governance/instructions/relinquishVote.ts new file mode 100644 index 00000000..95a0ae42 --- /dev/null +++ b/utils/governance/instructions/relinquishVote.ts @@ -0,0 +1,52 @@ +import { Connection, PublicKey, TransactionInstruction } from '@solana/web3.js' +import { + getGovernanceProgramVersion, + Proposal, + TokenOwnerRecord, + withRelinquishVote, +} from '@solana/spl-governance' +import { ProgramAccount } from '@solana/spl-governance' +import { MANGO_GOVERNANCE_PROGRAM, MANGO_REALM_PK } from '../constants' +import { WalletContextState } from '@solana/wallet-adapter-react' +import { MangoClient } from '@blockworks-foundation/mango-v4' +import { notify } from 'utils/notifications' + +export async function relinquishVote( + connection: Connection, + wallet: WalletContextState, + proposal: ProgramAccount, + tokenOwnerRecord: ProgramAccount, + mangoClient: MangoClient, + voteRecord: PublicKey +) { + const instructions: TransactionInstruction[] = [] + const governanceAuthority = wallet.publicKey! + const beneficiary = wallet.publicKey! + + const programVersion = await getGovernanceProgramVersion( + connection, + MANGO_GOVERNANCE_PROGRAM + ) + + await withRelinquishVote( + instructions, + MANGO_GOVERNANCE_PROGRAM, + programVersion, + MANGO_REALM_PK, + proposal.account.governance, + proposal.pubkey, + tokenOwnerRecord.pubkey, + proposal.account.governingTokenMint, + voteRecord, + governanceAuthority, + beneficiary + ) + + const tx = await mangoClient.sendAndConfirmTransaction(instructions) + notify({ + title: 'Transaction confirmed', + type: 'success', + txid: tx, + noSound: true, + }) +} diff --git a/utils/governance/instructions/updateVoteWeightRecord.ts b/utils/governance/instructions/updateVoteWeightRecord.ts new file mode 100644 index 00000000..12508e71 --- /dev/null +++ b/utils/governance/instructions/updateVoteWeightRecord.ts @@ -0,0 +1,36 @@ +import { PublicKey } from '@solana/web3.js' +import { MANGO_MINT, MANGO_REALM_PK } from '../constants' +import { DEFAULT_VSR_ID, VsrClient } from '../voteStakeRegistryClient' +import { + getRegistrarPDA, + getVoterPDA, + getVoterWeightPDA, +} from '../accounts/vsrAccounts' +import { SYSTEM_PROGRAM_ID } from '@solana/spl-governance' + +export const updateVoterWeightRecord = async ( + client: VsrClient, + walletPk: PublicKey +) => { + const { registrar } = await getRegistrarPDA( + MANGO_REALM_PK, + new PublicKey(MANGO_MINT), + DEFAULT_VSR_ID + ) + const { voter } = await getVoterPDA(registrar, walletPk, DEFAULT_VSR_ID) + const { voterWeightPk } = await getVoterWeightPDA( + registrar, + walletPk, + DEFAULT_VSR_ID + ) + const updateVoterWeightRecordIx = await client!.program.methods + .updateVoterWeightRecord() + .accounts({ + registrar, + voter, + voterWeightRecord: voterWeightPk, + systemProgram: SYSTEM_PROGRAM_ID, + }) + .instruction() + return { updateVoterWeightRecordIx, voterWeightPk } +} diff --git a/utils/governance/proposals.ts b/utils/governance/proposals.ts new file mode 100644 index 00000000..23251292 --- /dev/null +++ b/utils/governance/proposals.ts @@ -0,0 +1,67 @@ +import { BN } from '@project-serum/anchor' +import { + Governance, + MintMaxVoteWeightSource, + MintMaxVoteWeightSourceType, + Proposal, + ProposalState, +} from '@solana/spl-governance' +import BigNumber from 'bignumber.js' +import dayjs from 'dayjs' +import { MintInfo } from '@solana/spl-token' + +export const isInCoolOffTime = ( + proposal: Proposal | undefined, + governance: Governance | undefined +) => { + const mainVotingEndedAt = proposal?.signingOffAt + ?.addn(governance?.config.maxVotingTime || 0) + .toNumber() + + const votingCoolOffTime = governance?.config.votingCoolOffTime || 0 + const canFinalizeAt = mainVotingEndedAt + ? mainVotingEndedAt + votingCoolOffTime + : mainVotingEndedAt + + const endOfProposalAndCoolOffTime = canFinalizeAt + ? dayjs(1000 * canFinalizeAt!) + : undefined + + const isInCoolOffTime = endOfProposalAndCoolOffTime + ? dayjs().isBefore(endOfProposalAndCoolOffTime) && + mainVotingEndedAt && + dayjs().isAfter(mainVotingEndedAt * 1000) + : undefined + + return !!isInCoolOffTime && proposal!.state !== ProposalState.Defeated +} + +/** Returns max VoteWeight for given mint and max source */ +export function getMintMaxVoteWeight( + mint: MintInfo, + maxVoteWeightSource: MintMaxVoteWeightSource +) { + if (maxVoteWeightSource.type === MintMaxVoteWeightSourceType.SupplyFraction) { + const supplyFraction = maxVoteWeightSource.getSupplyFraction() + + const maxVoteWeight = new BigNumber(supplyFraction.toString()) + .multipliedBy(mint.supply.toString()) + .shiftedBy(-MintMaxVoteWeightSource.SUPPLY_FRACTION_DECIMALS) + + return new BN(maxVoteWeight.dp(0, BigNumber.ROUND_DOWN).toString()) + } else { + // absolute value + return maxVoteWeightSource.value + } +} + +export const calculatePct = (c = new BN(0), total?: BN) => { + if (total?.isZero()) { + return 0 + } + + return new BN(100) + .mul(c) + .div(total ?? new BN(1)) + .toNumber() +} diff --git a/utils/governance/tools.ts b/utils/governance/tools.ts index bb90a219..8e7e0e13 100644 --- a/utils/governance/tools.ts +++ b/utils/governance/tools.ts @@ -1,4 +1,3 @@ -import { MintInfo } from '@blockworks-foundation/mango-v4' import { getGovernanceAccounts, getRealm, @@ -7,10 +6,8 @@ import { pubkeyFilter, } from '@solana/spl-governance' import { Connection, PublicKey } from '@solana/web3.js' -import { getProposals } from './fetch/getProposals' -import { ConnectionContext } from './types' import { TokenProgramAccount } from './accounts/vsrAccounts' -import { u64, MintLayout } from '@solana/spl-token' +import { u64, MintLayout, MintInfo } from '@solana/spl-token' import BN from 'bn.js' export async function fetchRealm({ @@ -43,25 +40,6 @@ export async function fetchGovernances({ return governancesMap } -export async function fetchProposals({ - connectionContext, - programId, - governances, -}: { - connectionContext: ConnectionContext - programId: PublicKey - governances: PublicKey[] -}) { - const proposalsByGovernance = await getProposals( - governances, - connectionContext, - programId - ) - - const proposals = accountsToPubkeyMap(proposalsByGovernance.flatMap((p) => p)) - return proposals -} - export function accountsToPubkeyMap(accounts: ProgramAccount[]) { return arrayToRecord(accounts, (a) => a.pubkey.toBase58()) } From b9ae7aa5588254a0af6a8e76c98b99407712a4f2 Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Sun, 16 Apr 2023 09:05:08 +0200 Subject: [PATCH 68/71] Fix net borrows in window in dashboard Signed-off-by: microwavedcola1 --- pages/dashboard/index.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pages/dashboard/index.tsx b/pages/dashboard/index.tsx index 2dc922cc..dad31dde 100644 --- a/pages/dashboard/index.tsx +++ b/pages/dashboard/index.tsx @@ -376,11 +376,12 @@ const Dashboard: NextPage = () => { /> From 59a241d40ba2b906ad440b75438ecfe58d1217c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Brzezin=CC=81ski?= Date: Sun, 16 Apr 2023 13:27:33 +0200 Subject: [PATCH 69/71] fix lavamoat --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index 3421840b..04d3247a 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,8 @@ "@solana/wallet-adapter-wallets>@solana/wallet-adapter-torus>@toruslabs/solana-embed>@toruslabs/base-controllers>@toruslabs/broadcast-channel>@toruslabs/eccrypto>secp256k1": true, "@solana/wallet-adapter-wallets>@solana/wallet-adapter-torus>@toruslabs/solana-embed>@toruslabs/base-controllers>ethereumjs-util>ethereum-cryptography>secp256k1": true, "@solana/wallet-adapter-wallets>@solana/wallet-adapter-torus>@toruslabs/solana-embed>@toruslabs/openlogin-jrpc>@toruslabs/openlogin-utils>keccak": true, + "@metaplex-foundation/js>@bundlr-network/client>arbundles>secp256k1": true, + "@metaplex-foundation/js>@bundlr-network/client>arbundles>keccak": true, "@solana/web3.js>bigint-buffer": false, "@solana/web3.js>rpc-websockets>bufferutil": true, "@solana/web3.js>rpc-websockets>utf-8-validate": true, From 4dba708f6c7e3611f1ddd880a6d8f0c6ed2ffd41 Mon Sep 17 00:00:00 2001 From: saml33 Date: Mon, 17 Apr 2023 13:18:51 +1000 Subject: [PATCH 70/71] fix activity feed explorer link --- components/account/ActivityFeedTable.tsx | 204 +++++++++++++---------- 1 file changed, 117 insertions(+), 87 deletions(-) diff --git a/components/account/ActivityFeedTable.tsx b/components/account/ActivityFeedTable.tsx index 6a408e2d..4d97feb9 100644 --- a/components/account/ActivityFeedTable.tsx +++ b/components/account/ActivityFeedTable.tsx @@ -317,100 +317,31 @@ const ActivityFeedTable = () => { const fee = getFee(activity, mangoAccountAddress) const isExpandable = isLiquidationFeedItem(activity) || isPerpTradeFeedItem(activity) - return ( + return isExpandable ? ( {({ open }) => ( <> +
- - - - - - @@ -427,6 +358,43 @@ const ActivityFeedTable = () => { )} + ) : ( + + + + ) })} @@ -472,6 +440,68 @@ const ActivityFeedTable = () => { export default ActivityFeedTable +interface SharedTableBodyProps { + block_datetime: string + activity_type: string + amounts: { + credit: { value: string; symbol: string } + debit: { value: string; symbol: string } + } + isExpandable: boolean + fee: { value: string; symbol: string } + isOpenbook: boolean + value: number +} + +const SharedTableBody = ({ + block_datetime, + activity_type, + amounts, + isExpandable, + fee, + isOpenbook, + value, +}: SharedTableBodyProps) => { + const { t } = useTranslation('activity') + return ( + <> + + + + + + + + ) +} + const MobileActivityFeedItem = ({ activity, getValue, From 90ae89154daee8fa55aed4a8641b7094b0862df0 Mon Sep 17 00:00:00 2001 From: saml33 Date: Mon, 17 Apr 2023 14:25:21 +1000 Subject: [PATCH 71/71] use url param for token page --- components/stats/StatsPage.tsx | 4 +++ components/stats/TokenStats.tsx | 29 +++++++++++++----- components/token/ChartTabs.tsx | 6 +++- components/token/TopTokenAccounts.tsx | 3 +- pages/token/[token].tsx | 43 --------------------------- 5 files changed, 33 insertions(+), 52 deletions(-) delete mode 100644 pages/token/[token].tsx diff --git a/components/stats/StatsPage.tsx b/components/stats/StatsPage.tsx index 356e855a..d51a7cb6 100644 --- a/components/stats/StatsPage.tsx +++ b/components/stats/StatsPage.tsx @@ -1,4 +1,5 @@ import TabButtons from '@components/shared/TabButtons' +import TokenPage from '@components/token/TokenPage' import mangoStore from '@store/mangoStore' import useLocalStorageState from 'hooks/useLocalStorageState' import useMangoGroup from 'hooks/useMangoGroup' @@ -28,6 +29,7 @@ const StatsPage = () => { const fullWidthTabs = width ? width < breakpoints.lg : false const router = useRouter() const { market } = router.query + const { token } = router.query useEffect(() => { if (group && (!perpStats || !perpStats.length)) { @@ -40,6 +42,8 @@ const StatsPage = () => { }, []) return market ? ( + ) : token ? ( + ) : (
diff --git a/components/stats/TokenStats.tsx b/components/stats/TokenStats.tsx index e6c71103..a0760a0b 100644 --- a/components/stats/TokenStats.tsx +++ b/components/stats/TokenStats.tsx @@ -13,7 +13,7 @@ import { LinkButton } from '../shared/Button' import ContentBox from '../shared/ContentBox' import Tooltip from '@components/shared/Tooltip' import { Bank, toUiDecimals } from '@blockworks-foundation/mango-v4' -import { useRouter } from 'next/router' +import { NextRouter, useRouter } from 'next/router' import useJupiterMints from 'hooks/useJupiterMints' import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements' import useMangoGroup from 'hooks/useMangoGroup' @@ -40,10 +40,15 @@ const TokenStats = () => { } }, [group]) - const goToTokenPage = (bank: Bank) => { - router.push(`/token/${bank.name.split(' ')[0].toUpperCase()}`, undefined, { - shallow: true, - }) + // const goToTokenPage = (bank: Bank) => { + // router.push(`/token/${bank.name.split(' ')[0].toUpperCase()}`, undefined, { + // shallow: true, + // }) + // } + + const goToTokenPage = (token: string, router: NextRouter) => { + const query = { ...router.query, ['token']: token } + router.push({ pathname: router.pathname, query }) } return group ? ( @@ -128,7 +133,12 @@ const TokenStats = () => { goToTokenPage(bank)} + onClick={() => + goToTokenPage( + bank.name.split(' ')[0].toUpperCase(), + router + ) + } >
-

- {dayjs(block_datetime).format('ddd D MMM')} -

-

- {dayjs(block_datetime).format('h:mma')} -

-
- {t(`activity:${activity_type}`)} - - {amounts.credit.value}{' '} - - {amounts.credit.symbol} - - - {amounts.debit.value}{' '} - - {amounts.debit.symbol} - - - {fee.value}{' '} - - {fee.symbol} - - = 0 - ? 'text-th-up' - : 'text-th-down' - }`} - > - {value > 0 && - activity_type !== 'swap' && - !isOpenbook && - !isExpandable - ? '+' - : ''} - - - {!isExpandable ? ( -
- - -
- -
-
-
-
- ) : ( -
- -
- )} +
+ +
+
+ + +
+ +
+
+
+
+
+

{dayjs(block_datetime).format('ddd D MMM')}

+

+ {dayjs(block_datetime).format('h:mma')} +

+
{t(`activity:${activity_type}`)} + {amounts.credit.value}{' '} + {amounts.credit.symbol} + + {amounts.debit.value}{' '} + {amounts.debit.symbol} + + {fee.value}{' '} + {fee.symbol} + = 0 + ? 'text-th-up' + : 'text-th-down' + }`} + > + {value > 0 && activity_type !== 'swap' && !isOpenbook && !isExpandable + ? '+' + : ''} + +
@@ -396,7 +406,12 @@ const TokenStats = () => {
goToTokenPage(bank)} + onClick={() => + goToTokenPage( + bank.name.split(' ')[0].toUpperCase(), + router + ) + } > {t('token:token-details')} diff --git a/components/token/ChartTabs.tsx b/components/token/ChartTabs.tsx index cdd01330..46fb91dc 100644 --- a/components/token/ChartTabs.tsx +++ b/components/token/ChartTabs.tsx @@ -34,7 +34,11 @@ const ChartTabs = ({ token }: { token: string }) => { const statsHistory = useMemo(() => { if (!tokenStats?.length) return [] return tokenStats.reduce((a: TokenStatsItem[], c: TokenStatsItem) => { - if (c.symbol === token) { + if ( + c.symbol === token || + // ETH needs to be renamed ETH (Portal) in tokenStats db + (c.symbol === 'ETH' && token === 'ETH (Portal)') + ) { const copy = { ...c } copy.deposit_apr = copy.deposit_apr * 100 copy.borrow_apr = copy.borrow_apr * 100 diff --git a/components/token/TopTokenAccounts.tsx b/components/token/TopTokenAccounts.tsx index 6be0ea73..a6f1a040 100644 --- a/components/token/TopTokenAccounts.tsx +++ b/components/token/TopTokenAccounts.tsx @@ -197,7 +197,8 @@ const LeaderboardRow = ({

- + {/* remove isUsd when api returns token amount rather than value */} +

diff --git a/pages/token/[token].tsx b/pages/token/[token].tsx deleted file mode 100644 index c4bb94a6..00000000 --- a/pages/token/[token].tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import TokenPage from '@components/token/TokenPage' -import { CLUSTER } from '@store/mangoStore' -import type { NextPage } from 'next' -import { serverSideTranslations } from 'next-i18next/serverSideTranslations' - -export async function getStaticProps({ locale }: { locale: string }) { - return { - props: { - ...(await serverSideTranslations(locale, [ - 'common', - 'profile', - 'search', - 'settings', - 'token', - ])), - }, - } -} - -export const getStaticPaths = async () => { - const url = - CLUSTER === 'devnet' - ? 'https://api.jup.ag/api/tokens/devnet' - : 'https://cache.jup.ag/tokens' - const response = await fetch(url) - const data = await response.json() - const paths = data.map((t: any) => ({ - params: { token: t.symbol.toUpperCase() }, - })) - - return { paths, fallback: false } -} - -const Token: NextPage = () => { - return ( -
- -
- ) -} - -export default Token