From 0a122c0f794cd5ce67d34595fd6e1db9adfa1112 Mon Sep 17 00:00:00 2001 From: saml33 Date: Wed, 21 Jun 2023 09:16:58 +1000 Subject: [PATCH 01/22] hot keys modal form --- components/settings/HotKeysSettings.tsx | 155 ++++++++++++++++++++++++ components/settings/SettingsPage.tsx | 4 + utils/constants.ts | 2 + 3 files changed, 161 insertions(+) create mode 100644 components/settings/HotKeysSettings.tsx diff --git a/components/settings/HotKeysSettings.tsx b/components/settings/HotKeysSettings.tsx new file mode 100644 index 00000000..dcedd30e --- /dev/null +++ b/components/settings/HotKeysSettings.tsx @@ -0,0 +1,155 @@ +import ButtonGroup from '@components/forms/ButtonGroup' +import Input from '@components/forms/Input' +import Label from '@components/forms/Label' +import Select from '@components/forms/Select' +import Button from '@components/shared/Button' +import Modal from '@components/shared/Modal' +import Tooltip from '@components/shared/Tooltip' +import { KeyIcon } from '@heroicons/react/20/solid' +import mangoStore from '@store/mangoStore' +import useLocalStorageState from 'hooks/useLocalStorageState' +import { useTranslation } from 'next-i18next' +import { useState } from 'react' +import { ModalProps } from 'types/modal' +import { HOT_KEYS_KEY } from 'utils/constants' + +const HotKeysSettings = () => { + const { t } = useTranslation('settings') + const [hotKeys] = useLocalStorageState(HOT_KEYS_KEY, []) + const [showHotKeyModal, setShowHotKeyModal] = useState(false) + return ( + <> +

{t('hot-keys')}

+

{t('hot-keys-desc')}

+ {hotKeys.length ? ( +
+ ) : ( +
+
+ +

{t('no-hot-keys')}

+ +
+
+ )} + {showHotKeyModal ? ( + setShowHotKeyModal(false)} + /> + ) : null} + + ) +} + +export default HotKeysSettings + +// type HotKey = { +// keySequence: string +// market: string +// orderSide: 'buy/long' | 'sell/short' +// orderSizeType: 'percentage' | 'notional' +// orderSize: string +// orderType: 'limit' | 'market' +// orderPrice: string +// } + +const HotKeyModal = ({ isOpen, onClose }: ModalProps) => { + const { t } = useTranslation('settings') + const perpMarkets = mangoStore((s) => s.perpMarkets) + const serumMarkets = mangoStore((s) => s.serumMarkets) + const allMarkets = + perpMarkets.length && serumMarkets.length + ? [ + 'All', + ...perpMarkets.map((m) => m.name), + ...serumMarkets.map((m) => m.name), + ] + : ['All'] + const [keySequence, setKeySequence] = useState('') + const [market, setMarket] = useState('All') + const [orderPrice, setOrderPrice] = useState('') + const [orderSide, setOrderSide] = useState('buy/long') + const [orderSizeType, setOrderSizeType] = useState('percentage') + const [orderSize, setOrderSize] = useState('') + const [orderType, setOrderType] = useState('limit') + return ( + + <> +

{t('create-hot-key')}

+
+
+
+
+
+
+
+
+
+
+
+
+ {orderType === 'limit' ? ( +
+ + + setOrderPrice(e.target.value)} + placeholder="e.g. 1%" + suffix="%" + /> +
+ ) : null} + + +
+ ) +} diff --git a/components/settings/SettingsPage.tsx b/components/settings/SettingsPage.tsx index c0bb54d3..a9a9d395 100644 --- a/components/settings/SettingsPage.tsx +++ b/components/settings/SettingsPage.tsx @@ -1,5 +1,6 @@ import AnimationSettings from './AnimationSettings' import DisplaySettings from './DisplaySettings' +import HotKeysSettings from './HotKeysSettings' import NotificationSettings from './NotificationSettings' import PreferredExplorerSettings from './PreferredExplorerSettings' import RpcSettings from './RpcSettings' @@ -17,6 +18,9 @@ const SettingsPage = () => {
+
+ +
diff --git a/utils/constants.ts b/utils/constants.ts index 8130b991..d487cbfd 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -59,6 +59,8 @@ export const STATS_TAB_KEY = 'activeStatsTab-0.1' export const USE_ORDERBOOK_FEED_KEY = 'useOrderbookFeed-0.1' +export const HOT_KEYS_KEY = 'hotKeys-0.1' + // Unused export const PROFILE_CATEGORIES = [ 'borrower', From 8ed973a063053c9faf53cae86efdde69c97803d9 Mon Sep 17 00:00:00 2001 From: saml33 Date: Thu, 22 Jun 2023 10:48:15 +1000 Subject: [PATCH 02/22] place order on hot key --- components/settings/HotKeysSettings.tsx | 203 ++++++++++++--- components/trade/AdvancedTradeForm.tsx | 2 +- components/trade/TradeAdvancedPage.tsx | 5 +- components/trade/TradeHotKeys.tsx | 318 ++++++++++++++++++++++++ package.json | 1 + yarn.lock | 13 + 6 files changed, 506 insertions(+), 36 deletions(-) create mode 100644 components/trade/TradeHotKeys.tsx diff --git a/components/settings/HotKeysSettings.tsx b/components/settings/HotKeysSettings.tsx index dcedd30e..c6281b36 100644 --- a/components/settings/HotKeysSettings.tsx +++ b/components/settings/HotKeysSettings.tsx @@ -1,28 +1,50 @@ import ButtonGroup from '@components/forms/ButtonGroup' +import Checkbox from '@components/forms/Checkbox' import Input from '@components/forms/Input' import Label from '@components/forms/Label' -import Select from '@components/forms/Select' -import Button from '@components/shared/Button' +import Button, { LinkButton } from '@components/shared/Button' import Modal from '@components/shared/Modal' import Tooltip from '@components/shared/Tooltip' import { KeyIcon } from '@heroicons/react/20/solid' -import mangoStore from '@store/mangoStore' import useLocalStorageState from 'hooks/useLocalStorageState' import { useTranslation } from 'next-i18next' -import { useState } from 'react' +import { useCallback, useState } from 'react' import { ModalProps } from 'types/modal' import { HOT_KEYS_KEY } from 'utils/constants' +export type HotKey = { + ioc: boolean + keySequence: string + // market: string + margin: boolean + orderSide: 'buy' | 'sell' + orderSizeType: 'percentage' | 'notional' + orderSize: string + orderType: 'limit' | 'market' + orderPrice: string + postOnly: boolean + reduceOnly: boolean +} + const HotKeysSettings = () => { const { t } = useTranslation('settings') const [hotKeys] = useLocalStorageState(HOT_KEYS_KEY, []) const [showHotKeyModal, setShowHotKeyModal] = useState(false) return ( <> -

{t('hot-keys')}

+
+

{t('hot-keys')}

+ setShowHotKeyModal(true)}> + Create New Hot Key + +

{t('hot-keys-desc')}

{hotKeys.length ? ( -
+ hotKeys.map((k: HotKey) => ( +
+

{k.keySequence}

+
+ )) ) : (
@@ -46,35 +68,76 @@ const HotKeysSettings = () => { export default HotKeysSettings -// type HotKey = { -// keySequence: string -// market: string -// orderSide: 'buy/long' | 'sell/short' -// orderSizeType: 'percentage' | 'notional' -// orderSize: string -// orderType: 'limit' | 'market' -// orderPrice: string -// } - +// add ioc, postOnly and reduceOnly checkboxes const HotKeyModal = ({ isOpen, onClose }: ModalProps) => { - const { t } = useTranslation('settings') - const perpMarkets = mangoStore((s) => s.perpMarkets) - const serumMarkets = mangoStore((s) => s.serumMarkets) - const allMarkets = - perpMarkets.length && serumMarkets.length - ? [ - 'All', - ...perpMarkets.map((m) => m.name), - ...serumMarkets.map((m) => m.name), - ] - : ['All'] + const { t } = useTranslation(['settings', 'trade']) + const [hotKeys, setHotKeys] = useLocalStorageState(HOT_KEYS_KEY, []) + // const perpMarkets = mangoStore((s) => s.perpMarkets) + // const serumMarkets = mangoStore((s) => s.serumMarkets) + // const allMarkets = + // perpMarkets.length && serumMarkets.length + // ? [ + // 'All', + // ...perpMarkets.map((m) => m.name), + // ...serumMarkets.map((m) => m.name), + // ] + // : ['All'] const [keySequence, setKeySequence] = useState('') - const [market, setMarket] = useState('All') + // const [market, setMarket] = useState('All') const [orderPrice, setOrderPrice] = useState('') - const [orderSide, setOrderSide] = useState('buy/long') + const [orderSide, setOrderSide] = useState('buy') const [orderSizeType, setOrderSizeType] = useState('percentage') const [orderSize, setOrderSize] = useState('') const [orderType, setOrderType] = useState('limit') + const [postOnly, setPostOnly] = useState(false) + const [ioc, setIoc] = useState(false) + const [margin, setMargin] = useState(false) + const [reduceOnly, setReduceOnly] = useState(false) + + const handlePostOnlyChange = useCallback( + (postOnly: boolean) => { + let updatedIoc = ioc + if (postOnly) { + updatedIoc = !postOnly + } + setPostOnly(postOnly) + setIoc(updatedIoc) + }, + [ioc] + ) + + const handleIocChange = useCallback( + (ioc: boolean) => { + let updatedPostOnly = postOnly + if (ioc) { + updatedPostOnly = !ioc + } + setPostOnly(updatedPostOnly) + setIoc(ioc) + }, + [postOnly] + ) + + const handleSave = () => { + const newHotKey = { + keySequence: keySequence, + // market: market, + orderSide: orderSide, + orderSizeType: orderSizeType, + orderSize: orderSize, + orderType: orderType, + orderPrice: orderPrice, + ioc, + margin, + postOnly, + reduceOnly, + } + setHotKeys([...hotKeys, newHotKey]) + onClose() + } + + const disabled = + !keySequence || (orderType === 'limit' && !orderPrice) || !orderSize return ( <> @@ -87,7 +150,7 @@ const HotKeyModal = ({ isOpen, onClose }: ModalProps) => { onChange={(e) => setKeySequence(e.target.value)} />
-
+ {/*
+
*/}
@@ -148,7 +211,81 @@ const HotKeyModal = ({ isOpen, onClose }: ModalProps) => { />
) : null} - +
+ {orderType === 'limit' ? ( +
+
+ + handlePostOnlyChange(e.target.checked)} + > + {t('trade:post')} + + +
+
+ +
+ handleIocChange(e.target.checked)} + > + IOC + +
+
+
+
+ ) : null} +
+ + setMargin(e.target.checked)} + > + {t('trade:margin')} + + +
+
+ +
+ setReduceOnly(e.target.checked)} + > + {t('trade:reduce-only')} + +
+
+
+
+ ) diff --git a/components/trade/AdvancedTradeForm.tsx b/components/trade/AdvancedTradeForm.tsx index 15cd99de..a67a7dca 100644 --- a/components/trade/AdvancedTradeForm.tsx +++ b/components/trade/AdvancedTradeForm.tsx @@ -59,7 +59,7 @@ import InlineNotification from '@components/shared/InlineNotification' const set = mangoStore.getState().set -const successSound = new Howl({ +export const successSound = new Howl({ src: ['/sounds/swap-success.mp3'], volume: 0.5, }) diff --git a/components/trade/TradeAdvancedPage.tsx b/components/trade/TradeAdvancedPage.tsx index 67642779..fd38e15d 100644 --- a/components/trade/TradeAdvancedPage.tsx +++ b/components/trade/TradeAdvancedPage.tsx @@ -16,6 +16,7 @@ import OrderbookAndTrades from './OrderbookAndTrades' import FavoriteMarketsBar from './FavoriteMarketsBar' import useLocalStorageState from 'hooks/useLocalStorageState' import { TRADE_LAYOUT_KEY } from 'utils/constants' +import TradeHotKeys from './TradeHotKeys' export type TradeLayout = | 'chartLeft' @@ -206,7 +207,7 @@ const TradeAdvancedPage = () => { return showMobileView ? ( ) : ( - <> + console.log('bp: ', bp)} @@ -262,7 +263,7 @@ const TradeAdvancedPage = () => { {/* {!tourSettings?.trade_tour_seen && isOnboarded && connected ? ( ) : null} */} - + ) } diff --git a/components/trade/TradeHotKeys.tsx b/components/trade/TradeHotKeys.tsx new file mode 100644 index 00000000..a620ad9d --- /dev/null +++ b/components/trade/TradeHotKeys.tsx @@ -0,0 +1,318 @@ +import { + Group, + PerpMarket, + PerpOrderSide, + PerpOrderType, + Serum3Market, + Serum3OrderType, + Serum3SelfTradeBehavior, + Serum3Side, +} from '@blockworks-foundation/mango-v4' +import { HotKey } from '@components/settings/HotKeysSettings' +import mangoStore from '@store/mangoStore' +import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react' +import Hotkeys from 'react-hot-keys' +import { isMangoError } from 'types' +import { HOT_KEYS_KEY, SOUND_SETTINGS_KEY } from 'utils/constants' +import { notify } from 'utils/notifications' +import { calculateLimitPriceForMarketOrder } from 'utils/tradeForm' +import { successSound } from './AdvancedTradeForm' +import useLocalStorageState from 'hooks/useLocalStorageState' +import { INITIAL_SOUND_SETTINGS } from '@components/settings/SoundSettings' +import useSelectedMarket from 'hooks/useSelectedMarket' +import { floorToDecimal, getDecimalCount } from 'utils/numbers' +import { useSpotMarketMax } from './SpotSlider' +import useMangoAccount from 'hooks/useMangoAccount' +import { Market } from '@project-serum/serum' +import { useRouter } from 'next/router' + +const set = mangoStore.getState().set + +const calcBaseSize = ( + orderDetails: HotKey, + maxSize: number, + market: PerpMarket | Market, + oraclePrice: number, + quoteTokenIndex: number, + group: Group, + limitPrice?: number +) => { + const { orderSize, orderSide, orderSizeType, orderType } = orderDetails + let quoteSize: number + if (!quoteTokenIndex) { + quoteSize = + orderSizeType === 'percentage' + ? (Number(orderSize) / 100) * maxSize + : Number(orderSize) + } else { + const quoteBank = group.getFirstBankByTokenIndex(quoteTokenIndex) + const quotePrice = quoteBank.uiPrice + const orderSizeInQuote = Number(orderSize) / quotePrice + quoteSize = + orderSizeType === 'percentage' + ? (orderSizeInQuote / 100) * maxSize + : orderSizeInQuote + } + + let baseSize: number + if (orderType === 'market') { + if (orderSide === 'buy') { + baseSize = floorToDecimal( + quoteSize / oraclePrice, + getDecimalCount(market.minOrderSize) + ).toNumber() + } else { + if (orderSizeType === 'percentage') { + baseSize = floorToDecimal( + (Number(orderSize) / 100) * maxSize, + getDecimalCount(market.minOrderSize) + ).toNumber() + } else { + baseSize = floorToDecimal( + Number(orderSize) / oraclePrice, + getDecimalCount(market.minOrderSize) + ).toNumber() + } + } + } else { + const price = limitPrice ? limitPrice : 0 + baseSize = floorToDecimal( + quoteSize / price, + getDecimalCount(market.minOrderSize) + ).toNumber() + } + return baseSize +} + +const TradeHotKeys = ({ children }: { children: ReactNode }) => { + const { + price: oraclePrice, + selectedMarket, + serumOrPerpMarket, + } = useSelectedMarket() + const { mangoAccount } = useMangoAccount() + const { asPath } = useRouter() + const [hotKeys] = useLocalStorageState(HOT_KEYS_KEY, []) + const [placingOrder, setPlacingOrder] = useState(false) + const [useMargin, setUseMargin] = useState(false) + const [side, setSide] = useState('buy') + const [soundSettings] = useLocalStorageState( + SOUND_SETTINGS_KEY, + INITIAL_SOUND_SETTINGS + ) + const spotMax = useSpotMarketMax( + mangoAccount, + selectedMarket, + side, + useMargin + ) + + const perpMax = useMemo(() => { + const group = mangoStore.getState().group + if ( + !mangoAccount || + !group || + !selectedMarket || + selectedMarket instanceof Serum3Market + ) + return 0 + try { + if (side === 'buy') { + return mangoAccount.getMaxQuoteForPerpBidUi( + group, + selectedMarket.perpMarketIndex + ) + } else { + return mangoAccount.getMaxBaseForPerpAskUi( + group, + selectedMarket.perpMarketIndex + ) + } + } catch (e) { + console.error('Error calculating max leverage: ', e) + return 0 + } + }, [mangoAccount, side, selectedMarket]) + + const handlePlaceOrder = useCallback( + async (hkOrder: HotKey) => { + const client = mangoStore.getState().client + const group = mangoStore.getState().group + const mangoAccount = mangoStore.getState().mangoAccount.current + // const tradeForm = mangoStore.getState().tradeForm + const actions = mangoStore.getState().actions + const selectedMarket = mangoStore.getState().selectedMarket.current + const { ioc, orderPrice, orderSide, orderType, postOnly, reduceOnly } = + hkOrder + + if (!group || !mangoAccount || !serumOrPerpMarket || !selectedMarket) + return + setPlacingOrder(true) + try { + const orderMax = + serumOrPerpMarket instanceof PerpMarket ? perpMax : spotMax + const quoteTokenIndex = + selectedMarket instanceof PerpMarket + ? 0 + : selectedMarket.quoteTokenIndex + let baseSize: number + let price: number + if (orderType === 'market') { + baseSize = calcBaseSize( + hkOrder, + orderMax, + serumOrPerpMarket, + oraclePrice, + quoteTokenIndex, + group + ) + const orderbook = mangoStore.getState().selectedMarket.orderbook + price = calculateLimitPriceForMarketOrder( + orderbook, + baseSize, + orderSide + ) + } else { + // change in price from oracle for limit order + const priceChange = (Number(orderPrice) / 100) * oraclePrice + // subtract price change for buy limit, add for sell limit + const rawPrice = + orderSide === 'buy' + ? oraclePrice - priceChange + : oraclePrice + priceChange + price = floorToDecimal( + rawPrice, + getDecimalCount(serumOrPerpMarket.tickSize) + ).toNumber() + baseSize = calcBaseSize( + hkOrder, + orderMax, + serumOrPerpMarket, + oraclePrice, + quoteTokenIndex, + group, + price + ) + } + + if (selectedMarket instanceof Serum3Market) { + const spotOrderType = ioc + ? Serum3OrderType.immediateOrCancel + : postOnly && orderType !== 'market' + ? Serum3OrderType.postOnly + : Serum3OrderType.limit + const tx = await client.serum3PlaceOrder( + group, + mangoAccount, + selectedMarket.serumMarketExternal, + orderSide === 'buy' ? Serum3Side.bid : Serum3Side.ask, + price, + baseSize, + Serum3SelfTradeBehavior.decrementTake, + spotOrderType, + Date.now(), + 10 + ) + actions.fetchOpenOrders(true) + set((s) => { + s.successAnimation.trade = true + }) + if (soundSettings['swap-success']) { + successSound.play() + } + notify({ + type: 'success', + title: 'Transaction successful', + txid: tx, + }) + } else if (selectedMarket instanceof PerpMarket) { + const perpOrderType = + orderType === 'market' + ? PerpOrderType.market + : ioc + ? PerpOrderType.immediateOrCancel + : postOnly + ? PerpOrderType.postOnly + : PerpOrderType.limit + console.log('perpOrderType', perpOrderType) + + const tx = await client.perpPlaceOrder( + group, + mangoAccount, + selectedMarket.perpMarketIndex, + orderSide === 'buy' ? PerpOrderSide.bid : PerpOrderSide.ask, + price, + Math.abs(baseSize), + undefined, // maxQuoteQuantity + Date.now(), + perpOrderType, + selectedMarket.reduceOnly || reduceOnly, + undefined, + undefined + ) + actions.fetchOpenOrders(true) + set((s) => { + s.successAnimation.trade = true + }) + if (soundSettings['swap-success']) { + successSound.play() + } + notify({ + type: 'success', + title: 'Transaction successful', + txid: tx, + }) + } + } catch (e) { + console.error('Place trade error:', e) + if (!isMangoError(e)) return + notify({ + title: 'There was an issue.', + description: e.message, + txid: e?.txid, + type: 'error', + }) + } finally { + setPlacingOrder(false) + } + }, + [perpMax, serumOrPerpMarket, spotMax] + ) + + const onKeyDown = useCallback( + (keyName: string) => { + console.log('sdfsdf') + const orderDetails = hotKeys.find( + (hk: HotKey) => hk.keySequence === keyName + ) + if (orderDetails) { + setUseMargin(orderDetails.margin) + setSide(orderDetails.orderSide) + handlePlaceOrder(orderDetails) + } + }, + [handlePlaceOrder, hotKeys] + ) + + useEffect(() => { + if (placingOrder) { + notify({ + type: 'success', + title: 'Placing order for...', + }) + } + }, [placingOrder]) + + return hotKeys.length && asPath.includes('/trade') ? ( + k.keySequence).toString()} + onKeyDown={onKeyDown} + > + {children} + + ) : ( + <>{children} + ) +} + +export default TradeHotKeys diff --git a/package.json b/package.json index f0e3bed7..773b3823 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "react-dom": "18.2.0", "react-flip-numbers": "3.0.5", "react-grid-layout": "1.3.4", + "react-hot-keys": "2.7.2", "react-nice-dates": "3.1.0", "react-number-format": "4.9.2", "react-tsparticles": "2.2.4", diff --git a/yarn.lock b/yarn.lock index 4aba88c7..f4067ab5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5408,6 +5408,11 @@ hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: dependencies: react-is "^16.7.0" +hotkeys-js@^3.8.1: + version "3.10.2" + resolved "https://registry.yarnpkg.com/hotkeys-js/-/hotkeys-js-3.10.2.tgz#cf52661904f5a13a973565cb97085fea2f5ae257" + integrity sha512-Z6vLmJTYzkbZZXlBkhrYB962Q/rZGc/WHQiyEGu9ZZVF7bAeFDjjDa31grWREuw9Ygb4zmlov2bTkPYqj0aFnQ== + howler@2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/howler/-/howler-2.2.3.tgz#a2eff9b08b586798e7a2ee17a602a90df28715da" @@ -7168,6 +7173,14 @@ react-grid-layout@1.3.4: react-draggable "^4.0.0" react-resizable "^3.0.4" +react-hot-keys@2.7.2: + version "2.7.2" + resolved "https://registry.yarnpkg.com/react-hot-keys/-/react-hot-keys-2.7.2.tgz#7d2b02b7e2cf69182ea71ca01885446ebfae01d2" + integrity sha512-Z7eSh7SU6s52+zP+vkfFoNk0x4kgEmnwqDiyACKv53crK2AZ7FUaBLnf+vxLor3dvtId9murLmKOsrJeYgeHWw== + dependencies: + hotkeys-js "^3.8.1" + prop-types "^15.7.2" + react-i18next@^11.18.0: version "11.18.6" resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.18.6.tgz#e159c2960c718c1314f1e8fcaa282d1c8b167887" From 6337b696e726d2f6f1d51b843c4a047dc7c0bc93 Mon Sep 17 00:00:00 2001 From: saml33 Date: Fri, 23 Jun 2023 13:37:34 +1000 Subject: [PATCH 03/22] form validation --- components/account/AccountPage.tsx | 8 +- components/settings/HotKeysSettings.tsx | 412 +++++++++++++------ components/settings/NotificationSettings.tsx | 4 +- components/settings/SettingsPage.tsx | 14 +- components/trade/TradeHotKeys.tsx | 13 +- 5 files changed, 314 insertions(+), 137 deletions(-) diff --git a/components/account/AccountPage.tsx b/components/account/AccountPage.tsx index 5a1b5270..3dadc17b 100644 --- a/components/account/AccountPage.tsx +++ b/components/account/AccountPage.tsx @@ -59,7 +59,11 @@ const fetchFundingTotals = async (mangoAccountPk: string) => { const stats: TotalAccountFundingItem[] = entries .map(([key, value]) => { - return { ...value, market: key } + return { + long_funding: value.long_funding * -1, + short_funding: value.short_funding * -1, + market: key, + } }) .filter((x) => x) @@ -208,7 +212,7 @@ const AccountPage = () => { const interestTotalValue = useMemo(() => { if (totalInterestData.length) { return totalInterestData.reduce( - (a, c) => a + c.borrow_interest_usd + c.deposit_interest_usd, + (a, c) => a + (c.borrow_interest_usd * -1 + c.deposit_interest_usd), 0 ) } diff --git a/components/settings/HotKeysSettings.tsx b/components/settings/HotKeysSettings.tsx index c6281b36..021b476e 100644 --- a/components/settings/HotKeysSettings.tsx +++ b/components/settings/HotKeysSettings.tsx @@ -2,20 +2,21 @@ import ButtonGroup from '@components/forms/ButtonGroup' import Checkbox from '@components/forms/Checkbox' import Input from '@components/forms/Input' import Label from '@components/forms/Label' -import Button, { LinkButton } from '@components/shared/Button' +import Button, { IconButton, LinkButton } from '@components/shared/Button' +import InlineNotification from '@components/shared/InlineNotification' import Modal from '@components/shared/Modal' +import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements' import Tooltip from '@components/shared/Tooltip' -import { KeyIcon } from '@heroicons/react/20/solid' +import { KeyIcon, TrashIcon } from '@heroicons/react/20/solid' import useLocalStorageState from 'hooks/useLocalStorageState' import { useTranslation } from 'next-i18next' -import { useCallback, useState } from 'react' +import { useState } from 'react' import { ModalProps } from 'types/modal' import { HOT_KEYS_KEY } from 'utils/constants' export type HotKey = { ioc: boolean keySequence: string - // market: string margin: boolean orderSide: 'buy' | 'sell' orderSizeType: 'percentage' | 'notional' @@ -28,25 +29,96 @@ export type HotKey = { const HotKeysSettings = () => { const { t } = useTranslation('settings') - const [hotKeys] = useLocalStorageState(HOT_KEYS_KEY, []) + const [hotKeys, setHotKeys] = useLocalStorageState(HOT_KEYS_KEY, []) const [showHotKeyModal, setShowHotKeyModal] = useState(false) + + const handleDeleteKey = (key: string) => { + const newKeys = hotKeys.filter((hk: HotKey) => hk.keySequence !== key) + setHotKeys([...newKeys]) + } + return ( <>

{t('hot-keys')}

- setShowHotKeyModal(true)}> - Create New Hot Key - + {hotKeys.length ? ( + setShowHotKeyModal(true)}> + {t('create-new-key')} + + ) : null}

{t('hot-keys-desc')}

{hotKeys.length ? ( - hotKeys.map((k: HotKey) => ( -
-

{k.keySequence}

-
- )) + + + + + + + + + + + + {hotKeys.map((hk: HotKey) => { + const { + keySequence, + orderSide, + orderPrice, + orderSize, + orderSizeType, + orderType, + ioc, + margin, + reduceOnly, + postOnly, + } = hk + const size = + orderSizeType === 'percentage' + ? `${orderSize}% of max` + : `$${orderSize}` + const price = orderPrice ? `${orderPrice}% from oracle` : 'market' + + const options = { + margin: margin, + IOC: ioc, + post: postOnly, + reduce: reduceOnly, + } + + return ( + + + + + + + + + + ) + })} + +
{t('key')}{t('order-type')}{t('side')}{t('size')}{t('price')}{t('options')} + +
{keySequence}{orderType}{orderSide}{size}{price} + {Object.entries(options).map((e) => { + return e[1] + ? `${e[0] !== 'margin' ? ', ' : ''}${e[0]}` + : '' + })} + +
+ handleDeleteKey(keySequence)} + size="small" + > + + +
+
) : ( -
+

{t('no-hot-keys')}

@@ -68,151 +140,243 @@ const HotKeysSettings = () => { export default HotKeysSettings -// add ioc, postOnly and reduceOnly checkboxes +type FormErrors = Partial> + +type HotKeyForm = { + baseKey: string + triggerKey: string + price: string + side: 'buy' | 'sell' + size: string + sizeType: 'percentage' | 'notional' + orderType: 'limit' | 'market' + ioc: boolean + post: boolean + margin: boolean + reduce: boolean +} + +const DEFAULT_FORM_VALUES: HotKeyForm = { + baseKey: 'shift', + triggerKey: '', + price: '', + side: 'buy', + size: '', + sizeType: 'percentage', + orderType: 'limit', + ioc: false, + post: false, + margin: false, + reduce: false, +} + const HotKeyModal = ({ isOpen, onClose }: ModalProps) => { const { t } = useTranslation(['settings', 'trade']) const [hotKeys, setHotKeys] = useLocalStorageState(HOT_KEYS_KEY, []) - // const perpMarkets = mangoStore((s) => s.perpMarkets) - // const serumMarkets = mangoStore((s) => s.serumMarkets) - // const allMarkets = - // perpMarkets.length && serumMarkets.length - // ? [ - // 'All', - // ...perpMarkets.map((m) => m.name), - // ...serumMarkets.map((m) => m.name), - // ] - // : ['All'] - const [keySequence, setKeySequence] = useState('') - // const [market, setMarket] = useState('All') - const [orderPrice, setOrderPrice] = useState('') - const [orderSide, setOrderSide] = useState('buy') - const [orderSizeType, setOrderSizeType] = useState('percentage') - const [orderSize, setOrderSize] = useState('') - const [orderType, setOrderType] = useState('limit') - const [postOnly, setPostOnly] = useState(false) - const [ioc, setIoc] = useState(false) - const [margin, setMargin] = useState(false) - const [reduceOnly, setReduceOnly] = useState(false) + const [hotKeyForm, setHotKeyForm] = useState({ + ...DEFAULT_FORM_VALUES, + }) + const [formErrors, setFormErrors] = useState({}) - const handlePostOnlyChange = useCallback( - (postOnly: boolean) => { - let updatedIoc = ioc - if (postOnly) { - updatedIoc = !postOnly - } - setPostOnly(postOnly) - setIoc(updatedIoc) - }, - [ioc] - ) + const handleSetForm = (propertyName: string, value: string | boolean) => { + setFormErrors({}) + setHotKeyForm((prevState) => ({ ...prevState, [propertyName]: value })) + } - const handleIocChange = useCallback( - (ioc: boolean) => { - let updatedPostOnly = postOnly - if (ioc) { - updatedPostOnly = !ioc + const handlePostOnlyChange = (postOnly: boolean) => { + if (postOnly) { + handleSetForm('ioc', !postOnly) + } + handleSetForm('post', postOnly) + } + + const handleIocChange = (ioc: boolean) => { + if (ioc) { + handleSetForm('post', !ioc) + } + handleSetForm('ioc', ioc) + } + + const isFormValid = (form: HotKeyForm) => { + const invalidFields: FormErrors = {} + setFormErrors({}) + const triggerKey: (keyof HotKeyForm)[] = ['triggerKey'] + const requiredFields: (keyof HotKeyForm)[] = ['size', 'price', 'triggerKey'] + const numberFields: (keyof HotKeyForm)[] = ['size', 'price'] + const alphanumericRegex = /^[a-zA-Z0-9]+$/ + for (const key of triggerKey) { + const value = form[key] as string + if (value.length > 1) { + invalidFields[key] = t('error-too-many-characters') } - setPostOnly(updatedPostOnly) - setIoc(ioc) - }, - [postOnly] - ) + if (!alphanumericRegex.test(value)) { + invalidFields[key] = t('error-alphanumeric-only') + } + } + for (const key of requiredFields) { + const value = form[key] as string + if (!value) { + if (hotKeyForm.orderType === 'market') { + console.log(key, invalidFields[key]) + if (key !== 'price') { + invalidFields[key] = t('error-required-field') + } + } else { + invalidFields[key] = t('error-required-field') + } + } + } + for (const key of numberFields) { + const value = form[key] as string + if (value) { + if (isNaN(parseFloat(value))) { + invalidFields[key] = t('error-must-be-number') + } + if (parseFloat(value) < 0) { + invalidFields[key] = t('error-must-be-above-zero') + } + if (parseFloat(value) > 100) { + if (key === 'price') { + invalidFields[key] = t('error-must-be-below-100') + } else { + if (hotKeyForm.sizeType === 'percentage') { + invalidFields[key] = t('error-must-be-below-100') + } + } + } + } + } + if (Object.keys(invalidFields).length) { + setFormErrors(invalidFields) + } + return invalidFields + } const handleSave = () => { + const invalidFields = isFormValid(hotKeyForm) + if (Object.keys(invalidFields).length) { + return + } const newHotKey = { - keySequence: keySequence, - // market: market, - orderSide: orderSide, - orderSizeType: orderSizeType, - orderSize: orderSize, - orderType: orderType, - orderPrice: orderPrice, - ioc, - margin, - postOnly, - reduceOnly, + keySequence: `${hotKeyForm.baseKey}+${hotKeyForm.triggerKey}`, + orderSide: hotKeyForm.side, + orderSizeType: hotKeyForm.sizeType, + orderSize: hotKeyForm.size, + orderType: hotKeyForm.orderType, + orderPrice: hotKeyForm.price, + ioc: hotKeyForm.ioc, + margin: hotKeyForm.margin, + postOnly: hotKeyForm.post, + reduceOnly: hotKeyForm.reduce, } setHotKeys([...hotKeys, newHotKey]) onClose() } - const disabled = - !keySequence || (orderType === 'limit' && !orderPrice) || !orderSize return ( <>

{t('create-hot-key')}

-
- {/*
-
*/} +
+
-
-
- {orderType === 'limit' ? ( -
- - +
+
+
- ) : null} + {hotKeyForm.orderType === 'limit' ? ( +
+ + + handleSetForm('price', e.target.value)} + placeholder="e.g. 1%" + suffix="%" + /> + {formErrors.price ? ( +
+ +
+ ) : null} +
+ ) : null} +
- {orderType === 'limit' ? ( + {hotKeyForm.orderType === 'limit' ? (
{ content={t('trade:tooltip-post')} > handlePostOnlyChange(e.target.checked)} > {t('trade:post')} @@ -236,7 +400,7 @@ const HotKeyModal = ({ isOpen, onClose }: ModalProps) => { >
handleIocChange(e.target.checked)} > IOC @@ -253,8 +417,8 @@ const HotKeyModal = ({ isOpen, onClose }: ModalProps) => { content={t('trade:tooltip-enable-margin')} > setMargin(e.target.checked)} + checked={hotKeyForm.margin} + onChange={(e) => handleSetForm('margin', e.target.checked)} > {t('trade:margin')} @@ -270,8 +434,8 @@ const HotKeyModal = ({ isOpen, onClose }: ModalProps) => { >
setReduceOnly(e.target.checked)} + checked={hotKeyForm.reduce} + onChange={(e) => handleSetForm('reduce', e.target.checked)} > {t('trade:reduce-only')} @@ -279,11 +443,7 @@ const HotKeyModal = ({ isOpen, onClose }: ModalProps) => {
- diff --git a/components/settings/NotificationSettings.tsx b/components/settings/NotificationSettings.tsx index 50ad70a2..adca0257 100644 --- a/components/settings/NotificationSettings.tsx +++ b/components/settings/NotificationSettings.tsx @@ -42,7 +42,7 @@ const NotificationSettings = () => {

{t('settings:notifications')}

{isAuth ? ( -
+

{t('settings:limit-order-filled')}

{ />
) : ( -
+
{connected ? (
diff --git a/components/settings/SettingsPage.tsx b/components/settings/SettingsPage.tsx index a9a9d395..fc812e27 100644 --- a/components/settings/SettingsPage.tsx +++ b/components/settings/SettingsPage.tsx @@ -1,3 +1,4 @@ +import { useViewport } from 'hooks/useViewport' import AnimationSettings from './AnimationSettings' import DisplaySettings from './DisplaySettings' import HotKeysSettings from './HotKeysSettings' @@ -5,8 +6,11 @@ import NotificationSettings from './NotificationSettings' import PreferredExplorerSettings from './PreferredExplorerSettings' import RpcSettings from './RpcSettings' import SoundSettings from './SoundSettings' +import { breakpoints } from 'utils/theme' const SettingsPage = () => { + const { width } = useViewport() + const isMobile = width ? width < breakpoints.lg : false return (
@@ -15,12 +19,14 @@ const SettingsPage = () => {
-
+
-
- -
+ {!isMobile ? ( +
+ +
+ ) : null}
diff --git a/components/trade/TradeHotKeys.tsx b/components/trade/TradeHotKeys.tsx index a620ad9d..4877fc70 100644 --- a/components/trade/TradeHotKeys.tsx +++ b/components/trade/TradeHotKeys.tsx @@ -25,6 +25,7 @@ import { useSpotMarketMax } from './SpotSlider' import useMangoAccount from 'hooks/useMangoAccount' import { Market } from '@project-serum/serum' import { useRouter } from 'next/router' +import useUnownedAccount from 'hooks/useUnownedAccount' const set = mangoStore.getState().set @@ -90,7 +91,8 @@ const TradeHotKeys = ({ children }: { children: ReactNode }) => { selectedMarket, serumOrPerpMarket, } = useSelectedMarket() - const { mangoAccount } = useMangoAccount() + const { mangoAccount, mangoAccountAddress } = useMangoAccount() + const { isUnownedAccount } = useUnownedAccount() const { asPath } = useRouter() const [hotKeys] = useLocalStorageState(HOT_KEYS_KEY, []) const [placingOrder, setPlacingOrder] = useState(false) @@ -139,7 +141,6 @@ const TradeHotKeys = ({ children }: { children: ReactNode }) => { const client = mangoStore.getState().client const group = mangoStore.getState().group const mangoAccount = mangoStore.getState().mangoAccount.current - // const tradeForm = mangoStore.getState().tradeForm const actions = mangoStore.getState().actions const selectedMarket = mangoStore.getState().selectedMarket.current const { ioc, orderPrice, orderSide, orderType, postOnly, reduceOnly } = @@ -303,7 +304,13 @@ const TradeHotKeys = ({ children }: { children: ReactNode }) => { } }, [placingOrder]) - return hotKeys.length && asPath.includes('/trade') ? ( + const showHotKeys = + hotKeys.length && + asPath.includes('/trade') && + mangoAccountAddress && + !isUnownedAccount + + return showHotKeys ? ( k.keySequence).toString()} onKeyDown={onKeyDown} From a8fd16eb29d52814f2c17eac7c8314110ee05570 Mon Sep 17 00:00:00 2001 From: saml33 Date: Sat, 24 Jun 2023 22:44:15 +1000 Subject: [PATCH 04/22] errors and translations --- components/settings/HotKeysSettings.tsx | 99 +++++---- components/trade/TradeHotKeys.tsx | 256 +++++++++++++++--------- pages/settings.tsx | 1 + public/locales/en/settings.json | 29 +++ public/locales/en/trade.json | 2 + public/locales/es/trade.json | 2 + public/locales/ru/trade.json | 2 + public/locales/zh/trade.json | 2 + public/locales/zh_tw/trade.json | 2 + 9 files changed, 263 insertions(+), 132 deletions(-) diff --git a/components/settings/HotKeysSettings.tsx b/components/settings/HotKeysSettings.tsx index 021b476e..1ebdc0bc 100644 --- a/components/settings/HotKeysSettings.tsx +++ b/components/settings/HotKeysSettings.tsx @@ -2,7 +2,7 @@ import ButtonGroup from '@components/forms/ButtonGroup' import Checkbox from '@components/forms/Checkbox' import Input from '@components/forms/Input' import Label from '@components/forms/Label' -import Button, { IconButton, LinkButton } from '@components/shared/Button' +import Button, { IconButton } from '@components/shared/Button' import InlineNotification from '@components/shared/InlineNotification' import Modal from '@components/shared/Modal' import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements' @@ -28,7 +28,7 @@ export type HotKey = { } const HotKeysSettings = () => { - const { t } = useTranslation('settings') + const { t } = useTranslation(['common', 'settings', 'trade']) const [hotKeys, setHotKeys] = useLocalStorageState(HOT_KEYS_KEY, []) const [showHotKeyModal, setShowHotKeyModal] = useState(false) @@ -39,25 +39,27 @@ const HotKeysSettings = () => { return ( <> -
-

{t('hot-keys')}

+
+
+

{t('settings:hot-keys')}

+

{t('settings:hot-keys-desc')}

+
{hotKeys.length ? ( - setShowHotKeyModal(true)}> - {t('create-new-key')} - + ) : null}
-

{t('hot-keys-desc')}

{hotKeys.length ? ( - - - - + + + + - + @@ -77,9 +79,15 @@ const HotKeysSettings = () => { } = hk const size = orderSizeType === 'percentage' - ? `${orderSize}% of max` + ? t('settings:percentage-of-max', { size: orderSize }) : `$${orderSize}` - const price = orderPrice ? `${orderPrice}% from oracle` : 'market' + const price = orderPrice + ? `${orderPrice}% ${ + orderSide === 'buy' + ? t('settings:below') + : t('settings:above') + } oracle` + : t('trade:market') const options = { margin: margin, @@ -91,14 +99,16 @@ const HotKeysSettings = () => { return ( - - + + @@ -121,9 +131,11 @@ const HotKeysSettings = () => {
-

{t('no-hot-keys')}

+

{t('settings:no-hot-keys')}

@@ -171,7 +183,7 @@ const DEFAULT_FORM_VALUES: HotKeyForm = { } const HotKeyModal = ({ isOpen, onClose }: ModalProps) => { - const { t } = useTranslation(['settings', 'trade']) + const { t } = useTranslation(['common', 'settings', 'trade']) const [hotKeys, setHotKeys] = useLocalStorageState(HOT_KEYS_KEY, []) const [hotKeyForm, setHotKeyForm] = useState({ ...DEFAULT_FORM_VALUES, @@ -207,10 +219,10 @@ const HotKeyModal = ({ isOpen, onClose }: ModalProps) => { for (const key of triggerKey) { const value = form[key] as string if (value.length > 1) { - invalidFields[key] = t('error-too-many-characters') + invalidFields[key] = t('settings:error-too-many-characters') } if (!alphanumericRegex.test(value)) { - invalidFields[key] = t('error-alphanumeric-only') + invalidFields[key] = t('settings:error-alphanumeric-only') } } for (const key of requiredFields) { @@ -219,10 +231,10 @@ const HotKeyModal = ({ isOpen, onClose }: ModalProps) => { if (hotKeyForm.orderType === 'market') { console.log(key, invalidFields[key]) if (key !== 'price') { - invalidFields[key] = t('error-required-field') + invalidFields[key] = t('settings:error-required-field') } } else { - invalidFields[key] = t('error-required-field') + invalidFields[key] = t('settings:error-required-field') } } } @@ -230,17 +242,17 @@ const HotKeyModal = ({ isOpen, onClose }: ModalProps) => { const value = form[key] as string if (value) { if (isNaN(parseFloat(value))) { - invalidFields[key] = t('error-must-be-number') + invalidFields[key] = t('settings:error-must-be-number') } if (parseFloat(value) < 0) { - invalidFields[key] = t('error-must-be-above-zero') + invalidFields[key] = t('settings:error-must-be-above-zero') } if (parseFloat(value) > 100) { if (key === 'price') { - invalidFields[key] = t('error-must-be-below-100') + invalidFields[key] = t('settings:error-must-be-below-100') } else { if (hotKeyForm.sizeType === 'percentage') { - invalidFields[key] = t('error-must-be-below-100') + invalidFields[key] = t('settings:error-must-be-below-100') } } } @@ -276,9 +288,9 @@ const HotKeyModal = ({ isOpen, onClose }: ModalProps) => { return ( <> -

{t('create-hot-key')}

+

{t('settings:new-hot-key')}

-
-
-
-
-
-
{hotKeyForm.orderType === 'limit' ? (
- + {
diff --git a/components/trade/TradeHotKeys.tsx b/components/trade/TradeHotKeys.tsx index 4877fc70..48f9690c 100644 --- a/components/trade/TradeHotKeys.tsx +++ b/components/trade/TradeHotKeys.tsx @@ -1,5 +1,6 @@ import { Group, + MangoAccount, PerpMarket, PerpOrderSide, PerpOrderType, @@ -10,9 +11,9 @@ import { } from '@blockworks-foundation/mango-v4' import { HotKey } from '@components/settings/HotKeysSettings' import mangoStore from '@store/mangoStore' -import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react' +import { ReactNode, useCallback } from 'react' import Hotkeys from 'react-hot-keys' -import { isMangoError } from 'types' +import { GenericMarket, isMangoError } from 'types' import { HOT_KEYS_KEY, SOUND_SETTINGS_KEY } from 'utils/constants' import { notify } from 'utils/notifications' import { calculateLimitPriceForMarketOrder } from 'utils/tradeForm' @@ -21,11 +22,11 @@ import useLocalStorageState from 'hooks/useLocalStorageState' import { INITIAL_SOUND_SETTINGS } from '@components/settings/SoundSettings' import useSelectedMarket from 'hooks/useSelectedMarket' import { floorToDecimal, getDecimalCount } from 'utils/numbers' -import { useSpotMarketMax } from './SpotSlider' import useMangoAccount from 'hooks/useMangoAccount' import { Market } from '@project-serum/serum' import { useRouter } from 'next/router' import useUnownedAccount from 'hooks/useUnownedAccount' +import { useTranslation } from 'next-i18next' const set = mangoStore.getState().set @@ -39,102 +40,145 @@ const calcBaseSize = ( limitPrice?: number ) => { const { orderSize, orderSide, orderSizeType, orderType } = orderDetails - let quoteSize: number - if (!quoteTokenIndex) { - quoteSize = - orderSizeType === 'percentage' - ? (Number(orderSize) / 100) * maxSize - : Number(orderSize) - } else { - const quoteBank = group.getFirstBankByTokenIndex(quoteTokenIndex) - const quotePrice = quoteBank.uiPrice - const orderSizeInQuote = Number(orderSize) / quotePrice - quoteSize = - orderSizeType === 'percentage' - ? (orderSizeInQuote / 100) * maxSize - : orderSizeInQuote - } - let baseSize: number - if (orderType === 'market') { - if (orderSide === 'buy') { + let quoteSize: number + if (orderSide === 'buy') { + // assumes USDC = $1 as tokenIndex is 0 + if (!quoteTokenIndex) { + quoteSize = + orderSizeType === 'percentage' + ? (Number(orderSize) / 100) * maxSize + : Number(orderSize) + } else { + // required for non USDC quote tokens + const quoteBank = group.getFirstBankByTokenIndex(quoteTokenIndex) + const quotePrice = quoteBank.uiPrice + const orderSizeInQuote = Number(orderSize) / quotePrice + quoteSize = + orderSizeType === 'percentage' + ? (orderSizeInQuote / 100) * maxSize + : orderSizeInQuote + } + if (orderType === 'market') { baseSize = floorToDecimal( quoteSize / oraclePrice, getDecimalCount(market.minOrderSize) ).toNumber() } else { - if (orderSizeType === 'percentage') { - baseSize = floorToDecimal( - (Number(orderSize) / 100) * maxSize, - getDecimalCount(market.minOrderSize) - ).toNumber() - } else { + const price = limitPrice ? limitPrice : 0 + baseSize = floorToDecimal( + quoteSize / price, + getDecimalCount(market.minOrderSize) + ).toNumber() + } + } else { + if (orderSizeType === 'percentage') { + baseSize = floorToDecimal( + (Number(orderSize) / 100) * maxSize, + getDecimalCount(market.minOrderSize) + ).toNumber() + } else { + if (orderType === 'market') { baseSize = floorToDecimal( Number(orderSize) / oraclePrice, getDecimalCount(market.minOrderSize) ).toNumber() + } else { + const price = limitPrice ? limitPrice : 0 + baseSize = floorToDecimal( + Number(orderSize) / price, + getDecimalCount(market.minOrderSize) + ).toNumber() } } - } else { - const price = limitPrice ? limitPrice : 0 - baseSize = floorToDecimal( - quoteSize / price, - getDecimalCount(market.minOrderSize) - ).toNumber() } return baseSize } +const calcSpotMarketMax = ( + mangoAccount: MangoAccount | undefined, + selectedMarket: GenericMarket | undefined, + side: string, + useMargin: boolean +) => { + const spotBalances = mangoStore.getState().mangoAccount.spotBalances + const group = mangoStore.getState().group + if (!mangoAccount || !group || !selectedMarket) return 0 + if (!(selectedMarket instanceof Serum3Market)) return 0 + + let leverageMax = 0 + let spotMax = 0 + try { + if (side === 'buy') { + leverageMax = mangoAccount.getMaxQuoteForSerum3BidUi( + group, + selectedMarket.serumMarketExternal + ) + const bank = group.getFirstBankByTokenIndex( + selectedMarket.quoteTokenIndex + ) + const balance = mangoAccount.getTokenBalanceUi(bank) + const unsettled = spotBalances[bank.mint.toString()]?.unsettled || 0 + spotMax = balance + unsettled + } else { + leverageMax = mangoAccount.getMaxBaseForSerum3AskUi( + group, + selectedMarket.serumMarketExternal + ) + const bank = group.getFirstBankByTokenIndex(selectedMarket.baseTokenIndex) + const balance = mangoAccount.getTokenBalanceUi(bank) + const unsettled = spotBalances[bank.mint.toString()]?.unsettled || 0 + spotMax = balance + unsettled + } + return useMargin ? leverageMax : Math.max(spotMax, 0) + } catch (e) { + console.error('Error calculating max size: ', e) + return 0 + } +} + +const calcPerpMax = ( + mangoAccount: MangoAccount, + selectedMarket: GenericMarket, + side: string +) => { + const group = mangoStore.getState().group + if ( + !mangoAccount || + !group || + !selectedMarket || + selectedMarket instanceof Serum3Market + ) + return 0 + try { + if (side === 'buy') { + return mangoAccount.getMaxQuoteForPerpBidUi( + group, + selectedMarket.perpMarketIndex + ) + } else { + return mangoAccount.getMaxBaseForPerpAskUi( + group, + selectedMarket.perpMarketIndex + ) + } + } catch (e) { + console.error('Error calculating max leverage: ', e) + return 0 + } +} + const TradeHotKeys = ({ children }: { children: ReactNode }) => { - const { - price: oraclePrice, - selectedMarket, - serumOrPerpMarket, - } = useSelectedMarket() - const { mangoAccount, mangoAccountAddress } = useMangoAccount() + const { t } = useTranslation(['common', 'settings']) + const { price: oraclePrice, serumOrPerpMarket } = useSelectedMarket() + const { mangoAccountAddress } = useMangoAccount() const { isUnownedAccount } = useUnownedAccount() const { asPath } = useRouter() const [hotKeys] = useLocalStorageState(HOT_KEYS_KEY, []) - const [placingOrder, setPlacingOrder] = useState(false) - const [useMargin, setUseMargin] = useState(false) - const [side, setSide] = useState('buy') const [soundSettings] = useLocalStorageState( SOUND_SETTINGS_KEY, INITIAL_SOUND_SETTINGS ) - const spotMax = useSpotMarketMax( - mangoAccount, - selectedMarket, - side, - useMargin - ) - - const perpMax = useMemo(() => { - const group = mangoStore.getState().group - if ( - !mangoAccount || - !group || - !selectedMarket || - selectedMarket instanceof Serum3Market - ) - return 0 - try { - if (side === 'buy') { - return mangoAccount.getMaxQuoteForPerpBidUi( - group, - selectedMarket.perpMarketIndex - ) - } else { - return mangoAccount.getMaxBaseForPerpAskUi( - group, - selectedMarket.perpMarketIndex - ) - } - } catch (e) { - console.error('Error calculating max leverage: ', e) - return 0 - } - }, [mangoAccount, side, selectedMarket]) const handlePlaceOrder = useCallback( async (hkOrder: HotKey) => { @@ -143,15 +187,23 @@ const TradeHotKeys = ({ children }: { children: ReactNode }) => { const mangoAccount = mangoStore.getState().mangoAccount.current const actions = mangoStore.getState().actions const selectedMarket = mangoStore.getState().selectedMarket.current - const { ioc, orderPrice, orderSide, orderType, postOnly, reduceOnly } = - hkOrder + const { + ioc, + orderPrice, + orderSide, + orderType, + postOnly, + reduceOnly, + margin, + } = hkOrder if (!group || !mangoAccount || !serumOrPerpMarket || !selectedMarket) return - setPlacingOrder(true) try { const orderMax = - serumOrPerpMarket instanceof PerpMarket ? perpMax : spotMax + serumOrPerpMarket instanceof PerpMarket + ? calcPerpMax(mangoAccount, selectedMarket, orderSide) + : calcSpotMarketMax(mangoAccount, selectedMarket, orderSide, margin) const quoteTokenIndex = selectedMarket instanceof PerpMarket ? 0 @@ -196,6 +248,36 @@ const TradeHotKeys = ({ children }: { children: ReactNode }) => { ) } + // check if size < max + if (orderSide === 'buy') { + if (baseSize * price > orderMax) { + notify({ + type: 'error', + title: t('settings:error-order-exceeds-max'), + }) + return + } + } else { + console.log(baseSize, orderMax) + if (baseSize > orderMax) { + notify({ + type: 'error', + title: t('settings:error-order-exceeds-max'), + }) + return + } + } + + notify({ + type: 'info', + title: t('settings:placing-order'), + description: `${t(orderSide)} ${baseSize} ${selectedMarket.name} ${ + orderType === 'limit' + ? `${t('settings:at')} ${price}` + : `${t('settings:at')} ${t('market')}` + }`, + }) + if (selectedMarket instanceof Serum3Market) { const spotOrderType = ioc ? Serum3OrderType.immediateOrCancel @@ -273,37 +355,23 @@ const TradeHotKeys = ({ children }: { children: ReactNode }) => { txid: e?.txid, type: 'error', }) - } finally { - setPlacingOrder(false) } }, - [perpMax, serumOrPerpMarket, spotMax] + [serumOrPerpMarket] ) const onKeyDown = useCallback( (keyName: string) => { - console.log('sdfsdf') const orderDetails = hotKeys.find( (hk: HotKey) => hk.keySequence === keyName ) if (orderDetails) { - setUseMargin(orderDetails.margin) - setSide(orderDetails.orderSide) handlePlaceOrder(orderDetails) } }, [handlePlaceOrder, hotKeys] ) - useEffect(() => { - if (placingOrder) { - notify({ - type: 'success', - title: 'Placing order for...', - }) - } - }, [placingOrder]) - const showHotKeys = hotKeys.length && asPath.includes('/trade') && diff --git a/pages/settings.tsx b/pages/settings.tsx index b0f4eaea..13a3524f 100644 --- a/pages/settings.tsx +++ b/pages/settings.tsx @@ -17,6 +17,7 @@ export async function getStaticProps({ locale }: { locale: string }) { 'profile', 'search', 'settings', + 'trade', ])), }, } diff --git a/public/locales/en/settings.json b/public/locales/en/settings.json index 4ff063a5..536cbabc 100644 --- a/public/locales/en/settings.json +++ b/public/locales/en/settings.json @@ -1,7 +1,11 @@ { + "above": "Above", "animations": "Animations", + "at": "at", "avocado": "Avocado", "banana": "Banana", + "base-key": "Base Key", + "below": "Below", "blueberry": "Blueberry", "bottom-left": "Bottom-Left", "bottom-right": "Bottom-Right", @@ -16,18 +20,38 @@ "custom": "Custom", "dark": "Dark", "display": "Display", + "error-alphanumeric-only": "Alphanumeric characters only", + "error-must-be-above-zero": "Must be greater than zero", + "error-must-be-below-100": "Must be below 100", + "error-must-be-number": "Must be a number", + "error-order-exceeds-max": "Order exceeds max size", + "error-required-field": "This field is required", + "error-too-many-characters": "Enter one alphanumeric character", "english": "English", "high-contrast": "High Contrast", + "hot-keys": "Hot Keys", + "hot-keys-desc": "Create hot keys to place trades", + "key-sequence": "Key Sequence", "language": "Language", "light": "Light", "lychee": "Lychee", "mango": "Mango", "mango-classic": "Mango Classic", "medium": "Medium", + "new-hot-key": "New Hot Key", + "no-hot-keys": "Create your first hot key", "notification-position": "Notification Position", + "notional": "Notional", "number-scroll": "Number Scroll", "olive": "Olive", + "options": "Options", + "oracle": "Oracle", "orderbook-flash": "Orderbook Flash", + "order-side": "Order Side", + "order-size-type": "Order Size Type", + "percentage": "Percentage", + "percentage-of-max": "{{size}}% of Max", + "placing-order": "Placing Order...", "preferred-explorer": "Preferred Explorer", "recent-trades": "Recent Trades", "rpc": "RPC", @@ -35,6 +59,7 @@ "rpc-url": "Enter RPC URL", "russian": "Русский", "save": "Save", + "save-hot-key": "Save Hot Key", "slider": "Slider", "solana-beach": "Solana Beach", "solana-explorer": "Solana Explorer", @@ -45,6 +70,9 @@ "swap-success": "Swap/Trade Success", "swap-trade-size-selector": "Swap/Trade Size Selector", "theme": "Theme", + "tooltip-hot-key-notional-size": "Set size as a USD value.", + "tooltip-hot-key-percentage-size": "Set size as a percentage of your max leverage.", + "tooltip-hot-key-price": "Set a price as a percentage change from the oracle price.", "top-left": "Top-Left", "top-right": "Top-Right", "trade-layout": "Trade Layout", @@ -52,6 +80,7 @@ "transaction-success": "Transaction Success", "trade-chart": "Trade Chart", "trading-view": "Trading View", + "trigger-key": "Trigger Key", "notifications": "Notifications", "limit-order-filled": "Limit Order Fills", "orderbook-bandwidth-saving": "Orderbook Bandwidth Saving", diff --git a/public/locales/en/trade.json b/public/locales/en/trade.json index 2730df67..7ac4d281 100644 --- a/public/locales/en/trade.json +++ b/public/locales/en/trade.json @@ -35,6 +35,7 @@ "maker": "Maker", "maker-fee": "Maker Fee", "margin": "Margin", + "market": "Market", "market-details": "{{market}} Market Details", "max-leverage": "Max Leverage", "min-order-size": "Min Order Size", @@ -63,6 +64,7 @@ "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.", "price-provided-by": "Oracle by", "quote": "Quote", + "reduce": "Reduce", "reduce-only": "Reduce Only", "sells": "Sells", "settle-funds": "Settle Funds", diff --git a/public/locales/es/trade.json b/public/locales/es/trade.json index 2730df67..7ac4d281 100644 --- a/public/locales/es/trade.json +++ b/public/locales/es/trade.json @@ -35,6 +35,7 @@ "maker": "Maker", "maker-fee": "Maker Fee", "margin": "Margin", + "market": "Market", "market-details": "{{market}} Market Details", "max-leverage": "Max Leverage", "min-order-size": "Min Order Size", @@ -63,6 +64,7 @@ "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.", "price-provided-by": "Oracle by", "quote": "Quote", + "reduce": "Reduce", "reduce-only": "Reduce Only", "sells": "Sells", "settle-funds": "Settle Funds", diff --git a/public/locales/ru/trade.json b/public/locales/ru/trade.json index 2730df67..7ac4d281 100644 --- a/public/locales/ru/trade.json +++ b/public/locales/ru/trade.json @@ -35,6 +35,7 @@ "maker": "Maker", "maker-fee": "Maker Fee", "margin": "Margin", + "market": "Market", "market-details": "{{market}} Market Details", "max-leverage": "Max Leverage", "min-order-size": "Min Order Size", @@ -63,6 +64,7 @@ "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.", "price-provided-by": "Oracle by", "quote": "Quote", + "reduce": "Reduce", "reduce-only": "Reduce Only", "sells": "Sells", "settle-funds": "Settle Funds", diff --git a/public/locales/zh/trade.json b/public/locales/zh/trade.json index b9d96f98..f6fe9efb 100644 --- a/public/locales/zh/trade.json +++ b/public/locales/zh/trade.json @@ -34,6 +34,7 @@ "long": "做多", "maker": "挂单者", "margin": "保证金", + "market": "Market", "market-details": "{{market}}市场细节", "max-leverage": "最多杠杆", "min-order-size": "最小订单量", @@ -62,6 +63,7 @@ "price-expect": "您收到的价格可能与您预期有差异,并且无法保证完全执行。为了您的安全,最大滑点保持为 2.5%。超过 2.5%滑点的部分不会被平仓。", "price-provided-by": "语言机来自", "quote": "计价", + "reduce": "Reduce", "reduce-only": "限减少", "sells": "卖单", "settle-funds": "借清资金", diff --git a/public/locales/zh_tw/trade.json b/public/locales/zh_tw/trade.json index 3ff0d3f5..42619060 100644 --- a/public/locales/zh_tw/trade.json +++ b/public/locales/zh_tw/trade.json @@ -35,6 +35,7 @@ "maker": "掛單者", "maker-fee": "掛單者 Fee", "margin": "保證金", + "market": "Market", "market-details": "{{market}}市場細節", "max-leverage": "最多槓桿", "min-order-size": "最小訂單量", @@ -63,6 +64,7 @@ "price-expect": "您收到的價格可能與您預期有差異,並且無法保證完全執行。為了您的安全,最大滑點保持為 2.5%。超過 2.5%滑點的部分不會被平倉。", "price-provided-by": "語言機來自", "quote": "計價", + "reduce": "Reduce", "reduce-only": "限減少", "sells": "賣單", "settle-funds": "借清資金", From adac0ada69f36b84b3c657f1bcf5f2c9a6806d2a Mon Sep 17 00:00:00 2001 From: saml33 Date: Sat, 24 Jun 2023 23:06:19 +1000 Subject: [PATCH 05/22] update description --- components/settings/HotKeysSettings.tsx | 8 +++++-- public/locales/en/settings.json | 2 +- public/locales/es/settings.json | 29 +++++++++++++++++++++++++ public/locales/ru/settings.json | 29 +++++++++++++++++++++++++ public/locales/zh/settings.json | 29 +++++++++++++++++++++++++ public/locales/zh_tw/settings.json | 29 +++++++++++++++++++++++++ 6 files changed, 123 insertions(+), 3 deletions(-) diff --git a/components/settings/HotKeysSettings.tsx b/components/settings/HotKeysSettings.tsx index 1ebdc0bc..78a0f672 100644 --- a/components/settings/HotKeysSettings.tsx +++ b/components/settings/HotKeysSettings.tsx @@ -40,12 +40,16 @@ const HotKeysSettings = () => { return ( <>
-
+

{t('settings:hot-keys')}

{t('settings:hot-keys-desc')}

{hotKeys.length ? ( - ) : null} diff --git a/public/locales/en/settings.json b/public/locales/en/settings.json index 536cbabc..18ea47e5 100644 --- a/public/locales/en/settings.json +++ b/public/locales/en/settings.json @@ -30,7 +30,7 @@ "english": "English", "high-contrast": "High Contrast", "hot-keys": "Hot Keys", - "hot-keys-desc": "Create hot keys to place trades", + "hot-keys-desc": "Use hot keys to place orders on the trade page. They execute on the market you're viewing and are not market specific.", "key-sequence": "Key Sequence", "language": "Language", "light": "Light", diff --git a/public/locales/es/settings.json b/public/locales/es/settings.json index 4ff063a5..18ea47e5 100644 --- a/public/locales/es/settings.json +++ b/public/locales/es/settings.json @@ -1,7 +1,11 @@ { + "above": "Above", "animations": "Animations", + "at": "at", "avocado": "Avocado", "banana": "Banana", + "base-key": "Base Key", + "below": "Below", "blueberry": "Blueberry", "bottom-left": "Bottom-Left", "bottom-right": "Bottom-Right", @@ -16,18 +20,38 @@ "custom": "Custom", "dark": "Dark", "display": "Display", + "error-alphanumeric-only": "Alphanumeric characters only", + "error-must-be-above-zero": "Must be greater than zero", + "error-must-be-below-100": "Must be below 100", + "error-must-be-number": "Must be a number", + "error-order-exceeds-max": "Order exceeds max size", + "error-required-field": "This field is required", + "error-too-many-characters": "Enter one alphanumeric character", "english": "English", "high-contrast": "High Contrast", + "hot-keys": "Hot Keys", + "hot-keys-desc": "Use hot keys to place orders on the trade page. They execute on the market you're viewing and are not market specific.", + "key-sequence": "Key Sequence", "language": "Language", "light": "Light", "lychee": "Lychee", "mango": "Mango", "mango-classic": "Mango Classic", "medium": "Medium", + "new-hot-key": "New Hot Key", + "no-hot-keys": "Create your first hot key", "notification-position": "Notification Position", + "notional": "Notional", "number-scroll": "Number Scroll", "olive": "Olive", + "options": "Options", + "oracle": "Oracle", "orderbook-flash": "Orderbook Flash", + "order-side": "Order Side", + "order-size-type": "Order Size Type", + "percentage": "Percentage", + "percentage-of-max": "{{size}}% of Max", + "placing-order": "Placing Order...", "preferred-explorer": "Preferred Explorer", "recent-trades": "Recent Trades", "rpc": "RPC", @@ -35,6 +59,7 @@ "rpc-url": "Enter RPC URL", "russian": "Русский", "save": "Save", + "save-hot-key": "Save Hot Key", "slider": "Slider", "solana-beach": "Solana Beach", "solana-explorer": "Solana Explorer", @@ -45,6 +70,9 @@ "swap-success": "Swap/Trade Success", "swap-trade-size-selector": "Swap/Trade Size Selector", "theme": "Theme", + "tooltip-hot-key-notional-size": "Set size as a USD value.", + "tooltip-hot-key-percentage-size": "Set size as a percentage of your max leverage.", + "tooltip-hot-key-price": "Set a price as a percentage change from the oracle price.", "top-left": "Top-Left", "top-right": "Top-Right", "trade-layout": "Trade Layout", @@ -52,6 +80,7 @@ "transaction-success": "Transaction Success", "trade-chart": "Trade Chart", "trading-view": "Trading View", + "trigger-key": "Trigger Key", "notifications": "Notifications", "limit-order-filled": "Limit Order Fills", "orderbook-bandwidth-saving": "Orderbook Bandwidth Saving", diff --git a/public/locales/ru/settings.json b/public/locales/ru/settings.json index 4ff063a5..18ea47e5 100644 --- a/public/locales/ru/settings.json +++ b/public/locales/ru/settings.json @@ -1,7 +1,11 @@ { + "above": "Above", "animations": "Animations", + "at": "at", "avocado": "Avocado", "banana": "Banana", + "base-key": "Base Key", + "below": "Below", "blueberry": "Blueberry", "bottom-left": "Bottom-Left", "bottom-right": "Bottom-Right", @@ -16,18 +20,38 @@ "custom": "Custom", "dark": "Dark", "display": "Display", + "error-alphanumeric-only": "Alphanumeric characters only", + "error-must-be-above-zero": "Must be greater than zero", + "error-must-be-below-100": "Must be below 100", + "error-must-be-number": "Must be a number", + "error-order-exceeds-max": "Order exceeds max size", + "error-required-field": "This field is required", + "error-too-many-characters": "Enter one alphanumeric character", "english": "English", "high-contrast": "High Contrast", + "hot-keys": "Hot Keys", + "hot-keys-desc": "Use hot keys to place orders on the trade page. They execute on the market you're viewing and are not market specific.", + "key-sequence": "Key Sequence", "language": "Language", "light": "Light", "lychee": "Lychee", "mango": "Mango", "mango-classic": "Mango Classic", "medium": "Medium", + "new-hot-key": "New Hot Key", + "no-hot-keys": "Create your first hot key", "notification-position": "Notification Position", + "notional": "Notional", "number-scroll": "Number Scroll", "olive": "Olive", + "options": "Options", + "oracle": "Oracle", "orderbook-flash": "Orderbook Flash", + "order-side": "Order Side", + "order-size-type": "Order Size Type", + "percentage": "Percentage", + "percentage-of-max": "{{size}}% of Max", + "placing-order": "Placing Order...", "preferred-explorer": "Preferred Explorer", "recent-trades": "Recent Trades", "rpc": "RPC", @@ -35,6 +59,7 @@ "rpc-url": "Enter RPC URL", "russian": "Русский", "save": "Save", + "save-hot-key": "Save Hot Key", "slider": "Slider", "solana-beach": "Solana Beach", "solana-explorer": "Solana Explorer", @@ -45,6 +70,9 @@ "swap-success": "Swap/Trade Success", "swap-trade-size-selector": "Swap/Trade Size Selector", "theme": "Theme", + "tooltip-hot-key-notional-size": "Set size as a USD value.", + "tooltip-hot-key-percentage-size": "Set size as a percentage of your max leverage.", + "tooltip-hot-key-price": "Set a price as a percentage change from the oracle price.", "top-left": "Top-Left", "top-right": "Top-Right", "trade-layout": "Trade Layout", @@ -52,6 +80,7 @@ "transaction-success": "Transaction Success", "trade-chart": "Trade Chart", "trading-view": "Trading View", + "trigger-key": "Trigger Key", "notifications": "Notifications", "limit-order-filled": "Limit Order Fills", "orderbook-bandwidth-saving": "Orderbook Bandwidth Saving", diff --git a/public/locales/zh/settings.json b/public/locales/zh/settings.json index 37a9621f..ede7c4b2 100644 --- a/public/locales/zh/settings.json +++ b/public/locales/zh/settings.json @@ -1,7 +1,11 @@ { + "above": "Above", "animations": "动画", + "at": "at", "avocado": "酪梨", "banana": "香蕉", + "base-key": "Base Key", + "below": "Below", "blueberry": "蓝莓", "bottom-left": "左下", "bottom-right": "右下", @@ -16,8 +20,18 @@ "custom": "自定", "dark": "暗", "display": "显示", + "error-alphanumeric-only": "Alphanumeric characters only", + "error-must-be-above-zero": "Must be greater than zero", + "error-must-be-below-100": "Must be below 100", + "error-must-be-number": "Must be a number", + "error-order-exceeds-max": "Order exceeds max size", + "error-required-field": "This field is required", + "error-too-many-characters": "Enter one alphanumeric character", "english": "English", "high-contrast": "高对比度", + "hot-keys": "Hot Keys", + "hot-keys-desc": "Use hot keys to place orders on the trade page. They execute on the market you're viewing and are not market specific.", + "key-sequence": "Key Sequence", "language": "语言", "light": "光", "limit-order-filled": "限价单成交", @@ -25,11 +39,21 @@ "mango": "芒果", "mango-classic": "芒果经典", "medium": "中", + "new-hot-key": "New Hot Key", + "no-hot-keys": "Create your first hot key", "notification-position": "通知位置", + "notional": "Notional", "notifications": "通知", "number-scroll": "数字滑动", "olive": "橄榄", + "options": "Options", + "oracle": "Oracle", "orderbook-flash": "挂单薄闪光", + "order-side": "Order Side", + "order-size-type": "Order Size Type", + "percentage": "Percentage", + "percentage-of-max": "{{size}}% of Max", + "placing-order": "Placing Order...", "preferred-explorer": "首选探索器", "recent-trades": "最近交易", "rpc": "RPC", @@ -37,6 +61,7 @@ "rpc-url": "输入RPC URL", "russian": "Русский", "save": "存", + "save-hot-key": "Save Hot Key", "sign-to-notifications": "登录通知中心以更改设置", "slider": "滑块", "solana-beach": "Solana Beach", @@ -48,11 +73,15 @@ "swap-success": "换币/交易成功", "swap-trade-size-selector": "换币/交易大小选择器", "theme": "模式", + "tooltip-hot-key-notional-size": "Set size as a USD value.", + "tooltip-hot-key-percentage-size": "Set size as a percentage of your max leverage.", + "tooltip-hot-key-price": "Set a price as a percentage change from the oracle price.", "top-left": "左上", "top-right": "右上", "trade-chart": "交易图表", "trade-layout": "交易布局", "trading-view": "Trading View", + "trigger-key": "Trigger Key", "transaction-fail": "交易失败", "transaction-success": "交易成功", "orderbook-bandwidth-saving": "Orderbook Bandwidth Saving", diff --git a/public/locales/zh_tw/settings.json b/public/locales/zh_tw/settings.json index 9d33405c..270331ec 100644 --- a/public/locales/zh_tw/settings.json +++ b/public/locales/zh_tw/settings.json @@ -1,7 +1,11 @@ { + "above": "Above", "animations": "動畫", + "at": "at", "avocado": "酪梨", "banana": "香蕉", + "base-key": "Base Key", + "below": "Below", "blueberry": "藍莓", "bottom-left": "左下", "bottom-right": "右下", @@ -16,8 +20,18 @@ "custom": "自定", "dark": "暗", "display": "顯示", + "error-alphanumeric-only": "Alphanumeric characters only", + "error-must-be-above-zero": "Must be greater than zero", + "error-must-be-below-100": "Must be below 100", + "error-must-be-number": "Must be a number", + "error-order-exceeds-max": "Order exceeds max size", + "error-required-field": "This field is required", + "error-too-many-characters": "Enter one alphanumeric character", "english": "English", "high-contrast": "高對比度", + "hot-keys": "Hot Keys", + "hot-keys-desc": "Use hot keys to place orders on the trade page. They execute on the market you're viewing and are not market specific.", + "key-sequence": "Key Sequence", "language": "語言", "light": "光", "limit-order-filled": "限价单成交", @@ -25,11 +39,21 @@ "mango": "芒果", "mango-classic": "芒果經典", "medium": "中", + "new-hot-key": "New Hot Key", + "no-hot-keys": "Create your first hot key", "notification-position": "通知位置", + "notional": "Notional", "notifications": "通知", "number-scroll": "數字滑動", "olive": "橄欖", + "options": "Options", + "oracle": "Oracle", "orderbook-flash": "掛單薄閃光", + "order-side": "Order Side", + "order-size-type": "Order Size Type", + "percentage": "Percentage", + "percentage-of-max": "{{size}}% of Max", + "placing-order": "Placing Order...", "preferred-explorer": "首選探索器", "recent-trades": "最近交易", "rpc": "RPC", @@ -37,6 +61,7 @@ "rpc-url": "輸入RPC URL", "russian": "Русский", "save": "存", + "save-hot-key": "Save Hot Key", "sign-to-notifications": "登录通知中心以更改设置", "slider": "滑塊", "solana-beach": "Solana Beach", @@ -48,11 +73,15 @@ "swap-success": "換幣/交易成功", "swap-trade-size-selector": "換幣/交易大小選擇器", "theme": "模式", + "tooltip-hot-key-notional-size": "Set size as a USD value.", + "tooltip-hot-key-percentage-size": "Set size as a percentage of your max leverage.", + "tooltip-hot-key-price": "Set a price as a percentage change from the oracle price.", "top-left": "左上", "top-right": "右上", "trade-chart": "交易圖表", "trade-layout": "交易佈局", "trading-view": "Trading View", + "trigger-key": "Trigger Key", "transaction-fail": "交易失敗", "transaction-success": "交易成功", "orderbook-bandwidth-saving": "Orderbook Bandwidth Saving", From 4e56e3db078ebbdfb6823736f904558329918324 Mon Sep 17 00:00:00 2001 From: saml33 Date: Mon, 26 Jun 2023 11:02:52 +1000 Subject: [PATCH 06/22] disallow hot keys already in use --- components/settings/HotKeysSettings.tsx | 6 +++++- public/locales/en/settings.json | 1 + public/locales/es/settings.json | 1 + public/locales/ru/settings.json | 1 + public/locales/zh/settings.json | 1 + public/locales/zh_tw/settings.json | 1 + 6 files changed, 10 insertions(+), 1 deletion(-) diff --git a/components/settings/HotKeysSettings.tsx b/components/settings/HotKeysSettings.tsx index 78a0f672..78db29b1 100644 --- a/components/settings/HotKeysSettings.tsx +++ b/components/settings/HotKeysSettings.tsx @@ -233,7 +233,6 @@ const HotKeyModal = ({ isOpen, onClose }: ModalProps) => { const value = form[key] as string if (!value) { if (hotKeyForm.orderType === 'market') { - console.log(key, invalidFields[key]) if (key !== 'price') { invalidFields[key] = t('settings:error-required-field') } @@ -262,6 +261,11 @@ const HotKeyModal = ({ isOpen, onClose }: ModalProps) => { } } } + const newKeySequence = `${form.baseKey}+${form.triggerKey}` + const keyExists = hotKeys.find((k) => k.keySequence === newKeySequence) + if (keyExists) { + invalidFields.triggerKey = t('settings:error-key-in-use') + } if (Object.keys(invalidFields).length) { setFormErrors(invalidFields) } diff --git a/public/locales/en/settings.json b/public/locales/en/settings.json index 18ea47e5..cf584439 100644 --- a/public/locales/en/settings.json +++ b/public/locales/en/settings.json @@ -21,6 +21,7 @@ "dark": "Dark", "display": "Display", "error-alphanumeric-only": "Alphanumeric characters only", + "error-key-in-use": "Hot key already in use. Choose a unique key", "error-must-be-above-zero": "Must be greater than zero", "error-must-be-below-100": "Must be below 100", "error-must-be-number": "Must be a number", diff --git a/public/locales/es/settings.json b/public/locales/es/settings.json index 18ea47e5..cf584439 100644 --- a/public/locales/es/settings.json +++ b/public/locales/es/settings.json @@ -21,6 +21,7 @@ "dark": "Dark", "display": "Display", "error-alphanumeric-only": "Alphanumeric characters only", + "error-key-in-use": "Hot key already in use. Choose a unique key", "error-must-be-above-zero": "Must be greater than zero", "error-must-be-below-100": "Must be below 100", "error-must-be-number": "Must be a number", diff --git a/public/locales/ru/settings.json b/public/locales/ru/settings.json index 18ea47e5..cf584439 100644 --- a/public/locales/ru/settings.json +++ b/public/locales/ru/settings.json @@ -21,6 +21,7 @@ "dark": "Dark", "display": "Display", "error-alphanumeric-only": "Alphanumeric characters only", + "error-key-in-use": "Hot key already in use. Choose a unique key", "error-must-be-above-zero": "Must be greater than zero", "error-must-be-below-100": "Must be below 100", "error-must-be-number": "Must be a number", diff --git a/public/locales/zh/settings.json b/public/locales/zh/settings.json index ede7c4b2..19bc0e4b 100644 --- a/public/locales/zh/settings.json +++ b/public/locales/zh/settings.json @@ -21,6 +21,7 @@ "dark": "暗", "display": "显示", "error-alphanumeric-only": "Alphanumeric characters only", + "error-key-in-use": "Hot key already in use. Choose a unique key", "error-must-be-above-zero": "Must be greater than zero", "error-must-be-below-100": "Must be below 100", "error-must-be-number": "Must be a number", diff --git a/public/locales/zh_tw/settings.json b/public/locales/zh_tw/settings.json index 270331ec..57c5278b 100644 --- a/public/locales/zh_tw/settings.json +++ b/public/locales/zh_tw/settings.json @@ -21,6 +21,7 @@ "dark": "暗", "display": "顯示", "error-alphanumeric-only": "Alphanumeric characters only", + "error-key-in-use": "Hot key already in use. Choose a unique key", "error-must-be-above-zero": "Must be greater than zero", "error-must-be-below-100": "Must be below 100", "error-must-be-number": "Must be a number", From 743fbb3c4025f8905eea9bcc17a2945426cbb058 Mon Sep 17 00:00:00 2001 From: saml33 Date: Mon, 26 Jun 2023 11:11:31 +1000 Subject: [PATCH 07/22] limit number of keys per user to 20 --- components/settings/HotKeysSettings.tsx | 9 +++++++++ public/locales/en/settings.json | 1 + public/locales/es/settings.json | 1 + public/locales/ru/settings.json | 1 + public/locales/zh/settings.json | 1 + public/locales/zh_tw/settings.json | 1 + 6 files changed, 14 insertions(+) diff --git a/components/settings/HotKeysSettings.tsx b/components/settings/HotKeysSettings.tsx index 78db29b1..e6cdff90 100644 --- a/components/settings/HotKeysSettings.tsx +++ b/components/settings/HotKeysSettings.tsx @@ -47,6 +47,7 @@ const HotKeysSettings = () => { {hotKeys.length ? ( ) : null}
+ {hotKeys.length === 20 ? ( +
+ +
+ ) : null} {hotKeys.length ? (
{t('key')}{t('order-type')}{t('side')}{t('size')}{t('settings:key-sequence')}{t('trade:order-type')}{t('trade:side')}{t('trade:size')} {t('price')}{t('options')}{t('settings:options')}
{keySequence}{orderType}{orderSide}{t(`trade:${orderType}`)}{t(orderSide)} {size} {price} {Object.entries(options).map((e) => { return e[1] - ? `${e[0] !== 'margin' ? ', ' : ''}${e[0]}` + ? `${e[0] !== 'margin' ? ', ' : ''}${t( + `trade:${e[0]}` + )}` : '' })}
diff --git a/public/locales/en/settings.json b/public/locales/en/settings.json index cf584439..91c6aee4 100644 --- a/public/locales/en/settings.json +++ b/public/locales/en/settings.json @@ -22,6 +22,7 @@ "display": "Display", "error-alphanumeric-only": "Alphanumeric characters only", "error-key-in-use": "Hot key already in use. Choose a unique key", + "error-key-limit-reached": "You've reached the maximum number of hot keys", "error-must-be-above-zero": "Must be greater than zero", "error-must-be-below-100": "Must be below 100", "error-must-be-number": "Must be a number", diff --git a/public/locales/es/settings.json b/public/locales/es/settings.json index cf584439..91c6aee4 100644 --- a/public/locales/es/settings.json +++ b/public/locales/es/settings.json @@ -22,6 +22,7 @@ "display": "Display", "error-alphanumeric-only": "Alphanumeric characters only", "error-key-in-use": "Hot key already in use. Choose a unique key", + "error-key-limit-reached": "You've reached the maximum number of hot keys", "error-must-be-above-zero": "Must be greater than zero", "error-must-be-below-100": "Must be below 100", "error-must-be-number": "Must be a number", diff --git a/public/locales/ru/settings.json b/public/locales/ru/settings.json index cf584439..91c6aee4 100644 --- a/public/locales/ru/settings.json +++ b/public/locales/ru/settings.json @@ -22,6 +22,7 @@ "display": "Display", "error-alphanumeric-only": "Alphanumeric characters only", "error-key-in-use": "Hot key already in use. Choose a unique key", + "error-key-limit-reached": "You've reached the maximum number of hot keys", "error-must-be-above-zero": "Must be greater than zero", "error-must-be-below-100": "Must be below 100", "error-must-be-number": "Must be a number", diff --git a/public/locales/zh/settings.json b/public/locales/zh/settings.json index 19bc0e4b..59a2f40d 100644 --- a/public/locales/zh/settings.json +++ b/public/locales/zh/settings.json @@ -22,6 +22,7 @@ "display": "显示", "error-alphanumeric-only": "Alphanumeric characters only", "error-key-in-use": "Hot key already in use. Choose a unique key", + "error-key-limit-reached": "You've reached the maximum number of hot keys", "error-must-be-above-zero": "Must be greater than zero", "error-must-be-below-100": "Must be below 100", "error-must-be-number": "Must be a number", diff --git a/public/locales/zh_tw/settings.json b/public/locales/zh_tw/settings.json index 57c5278b..c2fa321c 100644 --- a/public/locales/zh_tw/settings.json +++ b/public/locales/zh_tw/settings.json @@ -22,6 +22,7 @@ "display": "顯示", "error-alphanumeric-only": "Alphanumeric characters only", "error-key-in-use": "Hot key already in use. Choose a unique key", + "error-key-limit-reached": "You've reached the maximum number of hot keys", "error-must-be-above-zero": "Must be greater than zero", "error-must-be-below-100": "Must be below 100", "error-must-be-number": "Must be a number", From c296946f30a50b1d6b78662df3750ba6b43dcb3b Mon Sep 17 00:00:00 2001 From: saml33 Date: Mon, 26 Jun 2023 11:14:28 +1000 Subject: [PATCH 08/22] convert trigger key to lower case --- components/settings/HotKeysSettings.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/settings/HotKeysSettings.tsx b/components/settings/HotKeysSettings.tsx index e6cdff90..be9060f2 100644 --- a/components/settings/HotKeysSettings.tsx +++ b/components/settings/HotKeysSettings.tsx @@ -320,7 +320,9 @@ const HotKeyModal = ({ isOpen, onClose }: ModalProps) => { hasError={formErrors.triggerKey !== undefined} type="text" value={hotKeyForm.triggerKey} - onChange={(e) => handleSetForm('triggerKey', e.target.value)} + onChange={(e) => + handleSetForm('triggerKey', e.target.value.toLowerCase()) + } /> {formErrors.triggerKey ? (
From 780dcfae51139925520813477a78eb4cba815c77 Mon Sep 17 00:00:00 2001 From: saml33 Date: Fri, 7 Jul 2023 12:54:53 +1000 Subject: [PATCH 09/22] add hnt logo --- next.config.js | 7 +------ public/icons/hnt.svg | 11 +++++++++++ utils/constants.ts | 1 + 3 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 public/icons/hnt.svg diff --git a/next.config.js b/next.config.js index 26d9b600..7e1bf3c5 100644 --- a/next.config.js +++ b/next.config.js @@ -5,12 +5,7 @@ const webpack = require('webpack') const nextConfig = { i18n, images: { - domains: [ - 'raw.githubusercontent.com', - 'arweave.net', - 'www.dual.finance', - 'storage.googleapis.com', - ], + domains: ['raw.githubusercontent.com', 'arweave.net', 'www.dual.finance'], }, reactStrictMode: true, //proxy for openserum api cors diff --git a/public/icons/hnt.svg b/public/icons/hnt.svg new file mode 100644 index 00000000..1666ca8c --- /dev/null +++ b/public/icons/hnt.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/utils/constants.ts b/utils/constants.ts index 36fb3960..86ddda77 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -110,6 +110,7 @@ export const CUSTOM_TOKEN_ICONS: { [key: string]: boolean } = { dual: true, eth: true, 'eth (portal)': true, + hnt: true, jitosol: true, ldo: true, mngo: true, From fc03c5705b9629fa43abb63d56542965b3fc8a22 Mon Sep 17 00:00:00 2001 From: saml33 Date: Fri, 7 Jul 2023 15:01:47 +1000 Subject: [PATCH 10/22] use usequery for account performance --- components/account/AccountChart.tsx | 4 +- components/account/AccountPage.tsx | 87 ++++++++++++++++++--------- components/modals/PnlHistoryModal.tsx | 20 ++---- components/wallet/ConnectedMenu.tsx | 4 -- store/mangoStore.ts | 46 -------------- 5 files changed, 66 insertions(+), 95 deletions(-) diff --git a/components/account/AccountChart.tsx b/components/account/AccountChart.tsx index ba0728ee..488548b4 100644 --- a/components/account/AccountChart.tsx +++ b/components/account/AccountChart.tsx @@ -26,7 +26,7 @@ const AccountChart = ({ chartToShow: ChartToShow setChartToShow: (chart: ChartToShow) => void customTooltip?: ContentType - data: PerformanceDataItem[] | HourlyFundingChartData[] + data: PerformanceDataItem[] | HourlyFundingChartData[] | undefined hideChart: () => void loading?: boolean yDecimals?: number @@ -36,7 +36,7 @@ const AccountChart = ({ const [daysToShow, setDaysToShow] = useState('1') const chartData = useMemo(() => { - if (!data.length) return [] + if (!data || !data.length) return [] if (chartToShow === 'cumulative-interest-value') { return data.map((d) => ({ interest_value: diff --git a/components/account/AccountPage.tsx b/components/account/AccountPage.tsx index 8ea838f7..c75b5bdb 100644 --- a/components/account/AccountPage.tsx +++ b/components/account/AccountPage.tsx @@ -29,7 +29,6 @@ import { MANGO_DATA_API_URL, // IS_ONBOARDED_KEY } from 'utils/constants' -import { useWallet } from '@solana/wallet-adapter-react' import useLocalStorageState from 'hooks/useLocalStorageState' // import AccountOnboardingTour from '@components/tours/AccountOnboardingTour' import dayjs from 'dayjs' @@ -42,7 +41,9 @@ import FormatNumericValue from '@components/shared/FormatNumericValue' import HealthBar from './HealthBar' import AssetsLiabilities from './AssetsLiabilities' import { + AccountPerformanceData, AccountVolumeTotalData, + EmptyObject, FormattedHourlyAccountVolumeData, HourlyAccountVolumeData, PerformanceDataItem, @@ -54,6 +55,34 @@ import VolumeChart from './VolumeChart' const TABS = ['account-value', 'account:assets-liabilities'] +const fetchAccountPerformance = async ( + mangoAccountPk: string, + range: number +) => { + try { + const response = await fetch( + `${MANGO_DATA_API_URL}/stats/performance_account?mango-account=${mangoAccountPk}&start-date=${dayjs() + .subtract(range, 'day') + .format('YYYY-MM-DD')}` + ) + const parsedResponse: null | EmptyObject | AccountPerformanceData[] = + await response.json() + if (parsedResponse && Object.keys(parsedResponse)?.length) { + const entries = Object.entries(parsedResponse).sort((a, b) => + b[0].localeCompare(a[0]) + ) + const stats = entries.map(([key, value]) => { + return { ...value, time: key } as PerformanceDataItem + }) + + return stats.reverse() + } else return [] + } catch (e) { + console.error('Failed to load account performance data', e) + return [] + } +} + const fetchFundingTotals = async (mangoAccountPk: string) => { try { const data = await fetch( @@ -177,14 +206,8 @@ export type ChartToShow = const AccountPage = () => { const { t } = useTranslation(['common', 'account']) - const { connected } = useWallet() const { group } = useMangoGroup() const { mangoAccount, mangoAccountAddress } = useMangoAccount() - const actions = mangoStore.getState().actions - const performanceLoading = mangoStore( - (s) => s.mangoAccount.performance.loading - ) - const performanceData = mangoStore((s) => s.mangoAccount.performance.data) const totalInterestData = mangoStore( (s) => s.mangoAccount.interestTotals.data @@ -207,11 +230,27 @@ const AccountPage = () => { ) useEffect(() => { - if (mangoAccountAddress || (!mangoAccountAddress && connected)) { - actions.fetchAccountPerformance(mangoAccountAddress, 31) + if (mangoAccountAddress) { + const actions = mangoStore.getState().actions actions.fetchAccountInterestTotals(mangoAccountAddress) } - }, [actions, mangoAccountAddress, connected]) + }, [mangoAccountAddress]) + + const { + data: performanceData, + isLoading: loadingPerformanceData, + isFetching: fetchingPerformanceData, + } = useQuery( + ['performance', mangoAccountAddress], + () => fetchAccountPerformance(mangoAccountAddress, 31), + { + cacheTime: 1000 * 60 * 10, + staleTime: 1000 * 60, + retry: 3, + refetchOnWindowFocus: false, + enabled: !!mangoAccountAddress, + } + ) const { data: fundingData, @@ -307,10 +346,6 @@ const AccountPage = () => { } const handleCloseDailyPnlModal = () => { - const set = mangoStore.getState().set - set((s) => { - s.mangoAccount.performance.data = oneDayPerformanceData - }) setShowPnlHistory(false) } @@ -339,6 +374,7 @@ const AccountPage = () => { if ( accountValue && oneDayPerformanceData.length && + performanceData && performanceData.length ) { const accountValueChange = @@ -404,22 +440,11 @@ const AccountPage = () => { | 'hourly-funding' | 'hourly-volume' ) => { - if (chartName === 'cumulative-interest-value' || interestTotalValue < -1) { - setChartToShow(chartName) - } - if (chartName === 'pnl') { - setChartToShow(chartName) - } - if (chartName === 'hourly-funding') { - setChartToShow(chartName) - } - if (chartName === 'hourly-volume') { - setChartToShow(chartName) - } + setChartToShow(chartName) } const latestAccountData = useMemo(() => { - if (!accountValue || !performanceData.length) return [] + if (!accountValue || !performanceData || !performanceData.length) return [] const latestDataItem = performanceData[performanceData.length - 1] return [ { @@ -440,6 +465,8 @@ const AccountPage = () => { const loadingHourlyVolume = fetchingHourlyVolumeData || loadingHourlyVolumeData + const performanceLoading = loadingPerformanceData || fetchingPerformanceData + return !chartToShow ? ( <>
@@ -881,6 +908,8 @@ const AccountPage = () => { ) : null} */} {showPnlHistory ? ( { @@ -901,7 +930,7 @@ const AccountPage = () => { diff --git a/components/modals/PnlHistoryModal.tsx b/components/modals/PnlHistoryModal.tsx index 96a19f68..a511151d 100644 --- a/components/modals/PnlHistoryModal.tsx +++ b/components/modals/PnlHistoryModal.tsx @@ -1,9 +1,7 @@ import { ModalProps } from '../../types/modal' import Modal from '../shared/Modal' -import mangoStore from '@store/mangoStore' import { useTranslation } from 'next-i18next' -import { useEffect, useMemo } from 'react' -import useMangoAccount from 'hooks/useMangoAccount' +import { useMemo } from 'react' import dayjs from 'dayjs' import Change from '@components/shared/Change' import SheenLoader from '@components/shared/SheenLoader' @@ -16,30 +14,24 @@ interface PnlChange { } interface PnlHistoryModalProps { + loading: boolean + performanceData: PerformanceDataItem[] | undefined pnlChangeToday: number } type ModalCombinedProps = PnlHistoryModalProps & ModalProps const PnlHistoryModal = ({ + loading, isOpen, onClose, + performanceData, pnlChangeToday, }: ModalCombinedProps) => { const { t } = useTranslation('account') - const { mangoAccountAddress } = useMangoAccount() - const actions = mangoStore.getState().actions - const loading = mangoStore((s) => s.mangoAccount.performance.loading) - const performanceData = mangoStore((s) => s.mangoAccount.performance.data) - - useEffect(() => { - if (mangoAccountAddress) { - actions.fetchAccountPerformance(mangoAccountAddress, 31) - } - }, [actions, mangoAccountAddress]) const dailyValues: PnlChange[] = useMemo(() => { - if (!performanceData.length) return [] + if (!performanceData || !performanceData.length) return [] const dailyPnl = performanceData.filter((d: PerformanceDataItem) => { const startTime = new Date().getTime() - 30 * 86400000 diff --git a/components/wallet/ConnectedMenu.tsx b/components/wallet/ConnectedMenu.tsx index 7e891282..0352d716 100644 --- a/components/wallet/ConnectedMenu.tsx +++ b/components/wallet/ConnectedMenu.tsx @@ -46,10 +46,6 @@ const ConnectedMenu = () => { state.mangoAccount.initialLoad = true state.mangoAccount.openOrders = {} state.mangoAccount.interestTotals = { data: [], loading: false } - state.mangoAccount.performance = { - data: [], - loading: true, - } }) disconnect() notify({ diff --git a/store/mangoStore.ts b/store/mangoStore.ts index dc4f0efd..f5fce086 100644 --- a/store/mangoStore.ts +++ b/store/mangoStore.ts @@ -150,10 +150,6 @@ export type MangoStore = { perpPositions: PerpPosition[] spotBalances: SpotBalances interestTotals: { data: TotalInterestDataItem[]; loading: boolean } - performance: { - data: PerformanceDataItem[] - loading: boolean - } swapHistory: { data: SwapHistoryItem[] initialLoad: boolean @@ -245,10 +241,6 @@ export type MangoStore = { params?: string, limit?: number ) => Promise - fetchAccountPerformance: ( - mangoAccountPk: string, - range: number - ) => Promise fetchGroup: () => Promise reloadMangoAccount: () => Promise fetchMangoAccounts: (ownerPk: PublicKey) => Promise @@ -313,7 +305,6 @@ const mangoStore = create()( perpPositions: [], spotBalances: {}, interestTotals: { data: [], loading: false }, - performance: { data: [], loading: true }, swapHistory: { data: [], loading: true, initialLoad: true }, tradeHistory: { data: [], loading: true }, }, @@ -438,43 +429,6 @@ const mangoStore = create()( }) } }, - fetchAccountPerformance: async ( - mangoAccountPk: string, - range: number - ) => { - const set = get().set - try { - const response = await fetch( - `${MANGO_DATA_API_URL}/stats/performance_account?mango-account=${mangoAccountPk}&start-date=${dayjs() - .subtract(range, 'day') - .format('YYYY-MM-DD')}` - ) - const parsedResponse: - | null - | EmptyObject - | AccountPerformanceData[] = await response.json() - - if (parsedResponse && Object.keys(parsedResponse)?.length) { - const entries = Object.entries(parsedResponse).sort((a, b) => - b[0].localeCompare(a[0]) - ) - - const stats = entries.map(([key, value]) => { - return { ...value, time: key } as PerformanceDataItem - }) - - set((state) => { - state.mangoAccount.performance.data = stats.reverse() - }) - } - } catch (e) { - console.error('Failed to load account performance data', e) - } finally { - set((state) => { - state.mangoAccount.performance.loading = false - }) - } - }, fetchActivityFeed: async ( mangoAccountPk: string, offset = 0, From aa7b9fc45bb9beae5d1f4f0c5606437bbb357a38 Mon Sep 17 00:00:00 2001 From: saml33 Date: Fri, 7 Jul 2023 16:00:17 +1000 Subject: [PATCH 11/22] fix pnl change today --- components/account/AccountPage.tsx | 32 ++++++++++++++---------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/components/account/AccountPage.tsx b/components/account/AccountPage.tsx index c75b5bdb..802947ad 100644 --- a/components/account/AccountPage.tsx +++ b/components/account/AccountPage.tsx @@ -370,24 +370,22 @@ const AccountPage = () => { } }, [mangoAccount, group, accountValue]) - const [accountValueChange, oneDayPnlChange] = useMemo(() => { - if ( - accountValue && - oneDayPerformanceData.length && - performanceData && - performanceData.length - ) { + const [accountValueChange, rollingDailyPnlChange, pnlChangeToday] = + useMemo(() => { + if (!accountValue || !accountPnl || !oneDayPerformanceData.length) + return [0, 0, 0] const accountValueChange = accountValue - oneDayPerformanceData[0].account_equity - const startDayPnl = oneDayPerformanceData[0].pnl - const endDayPnl = - oneDayPerformanceData[oneDayPerformanceData.length - 1].pnl - const oneDayPnlChange = endDayPnl - startDayPnl + const startHour = oneDayPerformanceData.find((item) => { + const itemHour = new Date(item.time).getHours() + return itemHour === 0 + }) + const startDayPnl = startHour?.pnl + const rollingDailyPnlChange = accountPnl - oneDayPerformanceData[0].pnl + const pnlChangeToday = startDayPnl ? accountPnl - startDayPnl : 0 - return [accountValueChange, oneDayPnlChange] - } - return [0, 0] - }, [accountValue, oneDayPerformanceData, performanceData]) + return [accountValueChange, rollingDailyPnlChange, pnlChangeToday] + }, [accountPnl, accountValue, oneDayPerformanceData]) const interestTotalValue = useMemo(() => { if (totalInterestData.length) { @@ -761,7 +759,7 @@ const AccountPage = () => { />

- +

{t('rolling-change')}

@@ -910,7 +908,7 @@ const AccountPage = () => { From 9a7304acaaabde482d312aaf0d43f385a1bf5aa6 Mon Sep 17 00:00:00 2001 From: saml33 Date: Fri, 7 Jul 2023 20:25:26 +1000 Subject: [PATCH 12/22] move account data fetching functions out of component --- components/account/AccountPage.tsx | 158 ++--------------------------- utils/account.ts | 152 +++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 151 deletions(-) create mode 100644 utils/account.ts diff --git a/components/account/AccountPage.tsx b/components/account/AccountPage.tsx index 802947ad..77c582f6 100644 --- a/components/account/AccountPage.tsx +++ b/components/account/AccountPage.tsx @@ -26,7 +26,6 @@ import Change from '../shared/Change' import Tooltip from '@components/shared/Tooltip' import { ANIMATION_SETTINGS_KEY, - MANGO_DATA_API_URL, // IS_ONBOARDED_KEY } from 'utils/constants' import useLocalStorageState from 'hooks/useLocalStorageState' @@ -40,162 +39,19 @@ import PnlHistoryModal from '@components/modals/PnlHistoryModal' import FormatNumericValue from '@components/shared/FormatNumericValue' import HealthBar from './HealthBar' import AssetsLiabilities from './AssetsLiabilities' -import { - AccountPerformanceData, - AccountVolumeTotalData, - EmptyObject, - FormattedHourlyAccountVolumeData, - HourlyAccountVolumeData, - PerformanceDataItem, - TotalAccountFundingItem, -} from 'types' +import { PerformanceDataItem } from 'types' import { useQuery } from '@tanstack/react-query' import FundingChart from './FundingChart' import VolumeChart from './VolumeChart' +import { + fetchAccountPerformance, + fetchFundingTotals, + fetchHourlyVolume, + fetchVolumeTotals, +} from 'utils/account' const TABS = ['account-value', 'account:assets-liabilities'] -const fetchAccountPerformance = async ( - mangoAccountPk: string, - range: number -) => { - try { - const response = await fetch( - `${MANGO_DATA_API_URL}/stats/performance_account?mango-account=${mangoAccountPk}&start-date=${dayjs() - .subtract(range, 'day') - .format('YYYY-MM-DD')}` - ) - const parsedResponse: null | EmptyObject | AccountPerformanceData[] = - await response.json() - if (parsedResponse && Object.keys(parsedResponse)?.length) { - const entries = Object.entries(parsedResponse).sort((a, b) => - b[0].localeCompare(a[0]) - ) - const stats = entries.map(([key, value]) => { - return { ...value, time: key } as PerformanceDataItem - }) - - return stats.reverse() - } else return [] - } catch (e) { - console.error('Failed to load account performance data', e) - return [] - } -} - -const fetchFundingTotals = async (mangoAccountPk: string) => { - try { - const data = await fetch( - `${MANGO_DATA_API_URL}/stats/funding-account-total?mango-account=${mangoAccountPk}` - ) - const res = await data.json() - if (res) { - const entries: [string, Omit][] = - Object.entries(res) - - const stats: TotalAccountFundingItem[] = entries - .map(([key, value]) => { - return { - long_funding: value.long_funding * -1, - short_funding: value.short_funding * -1, - market: key, - } - }) - .filter((x) => x) - - return stats - } - } catch (e) { - console.log('Failed to fetch account funding', e) - } -} - -const fetchVolumeTotals = async (mangoAccountPk: string) => { - try { - const [perpTotal, spotTotal] = await Promise.all([ - fetch( - `${MANGO_DATA_API_URL}/stats/perp-volume-total?mango-account=${mangoAccountPk}` - ), - fetch( - `${MANGO_DATA_API_URL}/stats/spot-volume-total?mango-account=${mangoAccountPk}` - ), - ]) - - const [perpTotalData, spotTotalData] = await Promise.all([ - perpTotal.json(), - spotTotal.json(), - ]) - - const combinedData = [perpTotalData, spotTotalData] - if (combinedData.length) { - return combinedData.reduce((a, c) => { - const entries: AccountVolumeTotalData[] = Object.entries(c) - const marketVol = entries.reduce((a, c) => { - return a + c[1].volume_usd - }, 0) - return a + marketVol - }, 0) - } - return 0 - } catch (e) { - console.log('Failed to fetch spot volume', e) - return 0 - } -} - -const formatHourlyVolumeData = (data: HourlyAccountVolumeData[]) => { - if (!data || !data.length) return [] - const formattedData: FormattedHourlyAccountVolumeData[] = [] - - // Loop through each object in the original data array - for (const obj of data) { - // Loop through the keys (markets) in each object - for (const market in obj) { - // Loop through the timestamps in each market - for (const timestamp in obj[market]) { - // Find the corresponding entry in the formatted data array based on the timestamp - let entry = formattedData.find((item) => item.time === timestamp) - - // If the entry doesn't exist, create a new entry - if (!entry) { - entry = { time: timestamp, total_volume_usd: 0, markets: {} } - formattedData.push(entry) - } - - // Increment the total_volume_usd by the volume_usd value - entry.total_volume_usd += obj[market][timestamp].volume_usd - - // Add or update the market entry in the markets object - entry.markets[market] = obj[market][timestamp].volume_usd - } - } - } - - return formattedData -} - -const fetchHourlyVolume = async (mangoAccountPk: string) => { - try { - const [perpHourly, spotHourly] = await Promise.all([ - fetch( - `${MANGO_DATA_API_URL}/stats/perp-volume-hourly?mango-account=${mangoAccountPk}` - ), - fetch( - `${MANGO_DATA_API_URL}/stats/spot-volume-hourly?mango-account=${mangoAccountPk}` - ), - ]) - - const [perpHourlyData, spotHourlyData] = await Promise.all([ - perpHourly.json(), - spotHourly.json(), - ]) - const hourlyVolume = [perpHourlyData, spotHourlyData] - return formatHourlyVolumeData(hourlyVolume) - } catch (e) { - console.log('Failed to fetch spot volume', e) - } -} - export type ChartToShow = | '' | 'account-value' diff --git a/utils/account.ts b/utils/account.ts new file mode 100644 index 00000000..3c6a7c84 --- /dev/null +++ b/utils/account.ts @@ -0,0 +1,152 @@ +import { + AccountPerformanceData, + AccountVolumeTotalData, + EmptyObject, + FormattedHourlyAccountVolumeData, + HourlyAccountVolumeData, + PerformanceDataItem, + TotalAccountFundingItem, +} from 'types' +import { MANGO_DATA_API_URL } from './constants' +import dayjs from 'dayjs' + +export const fetchAccountPerformance = async ( + mangoAccountPk: string, + range: number +) => { + try { + const response = await fetch( + `${MANGO_DATA_API_URL}/stats/performance_account?mango-account=${mangoAccountPk}&start-date=${dayjs() + .subtract(range, 'day') + .format('YYYY-MM-DD')}` + ) + const parsedResponse: null | EmptyObject | AccountPerformanceData[] = + await response.json() + if (parsedResponse && Object.keys(parsedResponse)?.length) { + const entries = Object.entries(parsedResponse).sort((a, b) => + b[0].localeCompare(a[0]) + ) + const stats = entries.map(([key, value]) => { + return { ...value, time: key } as PerformanceDataItem + }) + + return stats.reverse() + } else return [] + } catch (e) { + console.error('Failed to load account performance data', e) + return [] + } +} + +export const fetchFundingTotals = async (mangoAccountPk: string) => { + try { + const data = await fetch( + `${MANGO_DATA_API_URL}/stats/funding-account-total?mango-account=${mangoAccountPk}` + ) + const res = await data.json() + if (res) { + const entries: [string, Omit][] = + Object.entries(res) + + const stats: TotalAccountFundingItem[] = entries + .map(([key, value]) => { + return { + long_funding: value.long_funding * -1, + short_funding: value.short_funding * -1, + market: key, + } + }) + .filter((x) => x) + + return stats + } + } catch (e) { + console.log('Failed to fetch account funding', e) + } +} + +export const fetchVolumeTotals = async (mangoAccountPk: string) => { + try { + const [perpTotal, spotTotal] = await Promise.all([ + fetch( + `${MANGO_DATA_API_URL}/stats/perp-volume-total?mango-account=${mangoAccountPk}` + ), + fetch( + `${MANGO_DATA_API_URL}/stats/spot-volume-total?mango-account=${mangoAccountPk}` + ), + ]) + + const [perpTotalData, spotTotalData] = await Promise.all([ + perpTotal.json(), + spotTotal.json(), + ]) + + const combinedData = [perpTotalData, spotTotalData] + if (combinedData.length) { + return combinedData.reduce((a, c) => { + const entries: AccountVolumeTotalData[] = Object.entries(c) + const marketVol = entries.reduce((a, c) => { + return a + c[1].volume_usd + }, 0) + return a + marketVol + }, 0) + } + return 0 + } catch (e) { + console.log('Failed to fetch spot volume', e) + return 0 + } +} + +const formatHourlyVolumeData = (data: HourlyAccountVolumeData[]) => { + if (!data || !data.length) return [] + const formattedData: FormattedHourlyAccountVolumeData[] = [] + + // Loop through each object in the original data array + for (const obj of data) { + // Loop through the keys (markets) in each object + for (const market in obj) { + // Loop through the timestamps in each market + for (const timestamp in obj[market]) { + // Find the corresponding entry in the formatted data array based on the timestamp + let entry = formattedData.find((item) => item.time === timestamp) + + // If the entry doesn't exist, create a new entry + if (!entry) { + entry = { time: timestamp, total_volume_usd: 0, markets: {} } + formattedData.push(entry) + } + + // Increment the total_volume_usd by the volume_usd value + entry.total_volume_usd += obj[market][timestamp].volume_usd + + // Add or update the market entry in the markets object + entry.markets[market] = obj[market][timestamp].volume_usd + } + } + } + + return formattedData +} + +export const fetchHourlyVolume = async (mangoAccountPk: string) => { + try { + const [perpHourly, spotHourly] = await Promise.all([ + fetch( + `${MANGO_DATA_API_URL}/stats/perp-volume-hourly?mango-account=${mangoAccountPk}` + ), + fetch( + `${MANGO_DATA_API_URL}/stats/spot-volume-hourly?mango-account=${mangoAccountPk}` + ), + ]) + + const [perpHourlyData, spotHourlyData] = await Promise.all([ + perpHourly.json(), + spotHourly.json(), + ]) + const hourlyVolume = [perpHourlyData, spotHourlyData] + return formatHourlyVolumeData(hourlyVolume) + } catch (e) { + console.log('Failed to fetch spot volume', e) + } +} From e00af212769085b42a1e76a542be5e67ee9cb644 Mon Sep 17 00:00:00 2001 From: saml33 Date: Fri, 7 Jul 2023 22:34:49 +1000 Subject: [PATCH 13/22] split account page into more components --- components/account/AccountHeroStats.tsx | 501 +++++++++++++++++++ components/account/AccountPage.tsx | 635 ++---------------------- components/account/AccountValue.tsx | 148 ++++++ 3 files changed, 681 insertions(+), 603 deletions(-) create mode 100644 components/account/AccountHeroStats.tsx create mode 100644 components/account/AccountValue.tsx diff --git a/components/account/AccountHeroStats.tsx b/components/account/AccountHeroStats.tsx new file mode 100644 index 00000000..9e17eb75 --- /dev/null +++ b/components/account/AccountHeroStats.tsx @@ -0,0 +1,501 @@ +import mangoStore from '@store/mangoStore' +import useMangoAccount from 'hooks/useMangoAccount' +import useMangoGroup from 'hooks/useMangoGroup' +import { useTranslation } from 'next-i18next' +import { useEffect, useMemo } from 'react' +import { ChartToShow } from './AccountPage' +import { useQuery } from '@tanstack/react-query' +import { fetchFundingTotals, fetchVolumeTotals } from 'utils/account' +import Tooltip from '@components/shared/Tooltip' +import { + HealthType, + toUiDecimalsForQuote, +} from '@blockworks-foundation/mango-v4' +import HealthBar from './HealthBar' +import FormatNumericValue from '@components/shared/FormatNumericValue' +import { IconButton } from '@components/shared/Button' +import { CalendarIcon, ChartBarIcon } from '@heroicons/react/20/solid' +import Change from '@components/shared/Change' +import SheenLoader from '@components/shared/SheenLoader' +import { FormattedHourlyAccountVolumeData, PerformanceDataItem } from 'types' + +const AccountHeroStats = ({ + accountPnl, + accountValue, + hourlyVolumeData, + loadingHourlyVolume, + rollingDailyData, + setShowPnlHistory, + setChartToShow, +}: { + accountPnl: number + accountValue: number + hourlyVolumeData: FormattedHourlyAccountVolumeData[] | undefined + loadingHourlyVolume: boolean + rollingDailyData: PerformanceDataItem[] + setShowPnlHistory: (show: boolean) => void + setChartToShow: (view: ChartToShow) => void +}) => { + const { t } = useTranslation(['common', 'account']) + const { group } = useMangoGroup() + const { mangoAccount, mangoAccountAddress } = useMangoAccount() + + const totalInterestData = mangoStore( + (s) => s.mangoAccount.interestTotals.data + ) + + useEffect(() => { + if (mangoAccountAddress) { + const actions = mangoStore.getState().actions + actions.fetchAccountInterestTotals(mangoAccountAddress) + } + }, [mangoAccountAddress]) + + const { + data: fundingData, + isLoading: loadingFunding, + isFetching: fetchingFunding, + } = useQuery( + ['funding', mangoAccountAddress], + () => fetchFundingTotals(mangoAccountAddress), + { + cacheTime: 1000 * 60 * 10, + staleTime: 1000 * 60, + retry: 3, + refetchOnWindowFocus: false, + enabled: !!mangoAccountAddress, + } + ) + + const { + data: volumeTotalData, + isLoading: loadingVolumeTotalData, + isFetching: fetchingVolumeTotalData, + } = useQuery( + ['total-volume', mangoAccountAddress], + () => fetchVolumeTotals(mangoAccountAddress), + { + cacheTime: 1000 * 60 * 10, + staleTime: 1000 * 60, + retry: 3, + refetchOnWindowFocus: false, + enabled: !!mangoAccountAddress, + } + ) + + const maintHealth = useMemo(() => { + return group && mangoAccount + ? mangoAccount.getHealthRatioUi(group, HealthType.maint) + : 0 + }, [mangoAccount, group]) + + const leverage = useMemo(() => { + if (!group || !mangoAccount) return 0 + const assetsValue = toUiDecimalsForQuote( + mangoAccount.getAssetsValue(group).toNumber() + ) + + if (isNaN(assetsValue / accountValue)) { + return 0 + } else { + return Math.abs(1 - assetsValue / accountValue) + } + }, [mangoAccount, group, accountValue]) + + const rollingDailyPnlChange = useMemo(() => { + if (!accountPnl || !rollingDailyData.length) return 0 + return accountPnl - rollingDailyData[0].pnl + }, [accountPnl, rollingDailyData]) + + const interestTotalValue = useMemo(() => { + if (totalInterestData.length) { + return totalInterestData.reduce( + (a, c) => a + (c.borrow_interest_usd * -1 + c.deposit_interest_usd), + 0 + ) + } + return 0.0 + }, [totalInterestData]) + + const fundingTotalValue = useMemo(() => { + if (fundingData?.length && mangoAccountAddress) { + return fundingData.reduce( + (a, c) => a + c.long_funding + c.short_funding, + 0 + ) + } + return 0.0 + }, [fundingData, mangoAccountAddress]) + + const oneDayInterestChange = useMemo(() => { + if (rollingDailyData.length) { + const first = rollingDailyData[0] + const latest = rollingDailyData[rollingDailyData.length - 1] + + const startDayInterest = + first.borrow_interest_cumulative_usd + + first.deposit_interest_cumulative_usd + + const endDayInterest = + latest.borrow_interest_cumulative_usd + + latest.deposit_interest_cumulative_usd + + return endDayInterest - startDayInterest + } + return 0.0 + }, [rollingDailyData]) + + const dailyVolume = useMemo(() => { + if (!hourlyVolumeData || !hourlyVolumeData.length) return 0 + // Calculate the current time in milliseconds + const currentTime = new Date().getTime() + + // Calculate the start time for the last 24 hours in milliseconds + const last24HoursStartTime = currentTime - 24 * 60 * 60 * 1000 + + // Filter the formatted data based on the timestamp + const last24HoursData = hourlyVolumeData.filter((entry) => { + const timestampMs = new Date(entry.time).getTime() + return timestampMs >= last24HoursStartTime && timestampMs <= currentTime + }) + + const volume = last24HoursData.reduce((a, c) => a + c.total_volume_usd, 0) + return volume + }, [hourlyVolumeData]) + + const handleChartToShow = ( + viewName: + | 'pnl' + | 'cumulative-interest-value' + | 'hourly-funding' + | 'hourly-volume' + ) => { + setChartToShow(viewName) + } + + const loadingTotalVolume = fetchingVolumeTotalData || loadingVolumeTotalData + + return ( + <> +
+
+
+ +

+ Health describes how close your account is to liquidation. + The lower your account health is the more likely you are to + get liquidated when prices fluctuate. +

+ {maintHealth < 100 && mangoAccountAddress ? ( + <> +

+ Your account health is {maintHealth}% +

+

+ + Scenario: + {' '} + If the prices of all your liabilities increase by{' '} + {maintHealth}%, even for just a moment, some of your + liabilities will be liquidated. +

+

+ + Scenario: + {' '} + If the value of your total collateral decreases by{' '} + {( + (1 - 1 / ((maintHealth || 0) / 100 + 1)) * + 100 + ).toFixed(2)} + % , some of your liabilities will be liquidated. +

+

+ These are examples. A combination of events can also + lead to liquidation. +

+ + ) : null} +
+ } + > +

+ {t('health')} +

+ +
+

+ {maintHealth}% +

+ +
+ + + {t('leverage')}: + + + x + + +
+
+
+
+ +

+ {t('free-collateral')} +

+
+

+ +

+ + + {t('total')}: + + + + + +
+
+
+
+
+ +

+ {t('pnl')} +

+
+ {mangoAccountAddress ? ( +
+ + handleChartToShow('pnl')} + > + + + + + setShowPnlHistory(true)} + > + + + +
+ ) : null} +
+

+ +

+
+ +

{t('rolling-change')}

+
+
+
+
+
+
+

+ {t('account:lifetime-volume')} +

+ {mangoAccountAddress ? ( + + handleChartToShow('hourly-volume')} + > + + + + ) : null} +
+ {loadingTotalVolume && mangoAccountAddress ? ( + +
+ + ) : ( +

+ +

+ )} + + {t('account:daily-volume')}: + {loadingHourlyVolume && mangoAccountAddress ? ( + +
+ + ) : ( + + + + )} + +
+
+
+
+
+ +

+ {t('total-interest-earned')} +

+
+ {mangoAccountAddress ? ( + + + handleChartToShow('cumulative-interest-value') + } + > + + + + ) : null} +
+

+ +

+
+ +

{t('rolling-change')}

+
+
+
+
+
+ +

+ {t('account:total-funding-earned')} +

+
+ {mangoAccountAddress ? ( + + handleChartToShow('hourly-funding')} + > + + + + ) : null} +
+ {(loadingFunding || fetchingFunding) && mangoAccountAddress ? ( + +
+ + ) : ( +

+ +

+ )} +
+
+ + ) +} + +export default AccountHeroStats diff --git a/components/account/AccountPage.tsx b/components/account/AccountPage.tsx index 77c582f6..ff9f3298 100644 --- a/components/account/AccountPage.tsx +++ b/components/account/AccountPage.tsx @@ -1,54 +1,25 @@ -import { - HealthType, - toUiDecimalsForQuote, -} from '@blockworks-foundation/mango-v4' +import { toUiDecimalsForQuote } from '@blockworks-foundation/mango-v4' import { useTranslation } from 'next-i18next' -import { useEffect, useMemo, useState } from 'react' +import { useMemo, useState } from 'react' import AccountActions from './AccountActions' -import mangoStore from '@store/mangoStore' -import { formatCurrencyValue } from '../../utils/numbers' -import FlipNumbers from 'react-flip-numbers' -import SimpleAreaChart from '@components/shared/SimpleAreaChart' -import { COLORS } from '../../styles/colors' -import { useTheme } from 'next-themes' -import { IconButton } from '../shared/Button' -import { - ArrowsPointingOutIcon, - CalendarIcon, - ChartBarIcon, -} from '@heroicons/react/20/solid' -import { Transition } from '@headlessui/react' import AccountTabs from './AccountTabs' -import SheenLoader from '../shared/SheenLoader' import AccountChart from './AccountChart' import useMangoAccount from '../../hooks/useMangoAccount' -import Change from '../shared/Change' -import Tooltip from '@components/shared/Tooltip' -import { - ANIMATION_SETTINGS_KEY, - // IS_ONBOARDED_KEY -} from 'utils/constants' import useLocalStorageState from 'hooks/useLocalStorageState' // import AccountOnboardingTour from '@components/tours/AccountOnboardingTour' import dayjs from 'dayjs' -import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettings' import { useViewport } from 'hooks/useViewport' import { breakpoints } from 'utils/theme' import useMangoGroup from 'hooks/useMangoGroup' import PnlHistoryModal from '@components/modals/PnlHistoryModal' -import FormatNumericValue from '@components/shared/FormatNumericValue' -import HealthBar from './HealthBar' import AssetsLiabilities from './AssetsLiabilities' import { PerformanceDataItem } from 'types' import { useQuery } from '@tanstack/react-query' import FundingChart from './FundingChart' import VolumeChart from './VolumeChart' -import { - fetchAccountPerformance, - fetchFundingTotals, - fetchHourlyVolume, - fetchVolumeTotals, -} from 'utils/account' +import { fetchAccountPerformance, fetchHourlyVolume } from 'utils/account' +import AccountHeroStats from './AccountHeroStats' +import AccountValue from './AccountValue' const TABS = ['account-value', 'account:assets-liabilities'] @@ -64,34 +35,17 @@ const AccountPage = () => { const { t } = useTranslation(['common', 'account']) const { group } = useMangoGroup() const { mangoAccount, mangoAccountAddress } = useMangoAccount() - - const totalInterestData = mangoStore( - (s) => s.mangoAccount.interestTotals.data - ) const [chartToShow, setChartToShow] = useState('') - const [showExpandChart, setShowExpandChart] = useState(false) const [showPnlHistory, setShowPnlHistory] = useState(false) - const { theme } = useTheme() const { width } = useViewport() const isMobile = width ? width < breakpoints.md : false // const tourSettings = mangoStore((s) => s.settings.tours) // const [isOnBoarded] = useLocalStorageState(IS_ONBOARDED_KEY) - const [animationSettings] = useLocalStorageState( - ANIMATION_SETTINGS_KEY, - INITIAL_ANIMATION_SETTINGS - ) const [activeTab, setActiveTab] = useLocalStorageState( 'accountHeroKey-0.1', 'account-value' ) - useEffect(() => { - if (mangoAccountAddress) { - const actions = mangoStore.getState().actions - actions.fetchAccountInterestTotals(mangoAccountAddress) - } - }, [mangoAccountAddress]) - const { data: performanceData, isLoading: loadingPerformanceData, @@ -108,38 +62,6 @@ const AccountPage = () => { } ) - const { - data: fundingData, - isLoading: loadingFunding, - isFetching: fetchingFunding, - } = useQuery( - ['funding', mangoAccountAddress], - () => fetchFundingTotals(mangoAccountAddress), - { - cacheTime: 1000 * 60 * 10, - staleTime: 1000 * 60, - retry: 3, - refetchOnWindowFocus: false, - enabled: !!mangoAccountAddress, - } - ) - - const { - data: volumeTotalData, - isLoading: loadingVolumeTotalData, - isFetching: fetchingVolumeTotalData, - } = useQuery( - ['total-volume', mangoAccountAddress], - () => fetchVolumeTotals(mangoAccountAddress), - { - cacheTime: 1000 * 60 * 10, - staleTime: 1000 * 60, - retry: 3, - refetchOnWindowFocus: false, - enabled: !!mangoAccountAddress, - } - ) - const { data: hourlyVolumeData, isLoading: loadingHourlyVolumeData, @@ -156,25 +78,7 @@ const AccountPage = () => { } ) - const dailyVolume = useMemo(() => { - if (!hourlyVolumeData || !hourlyVolumeData.length) return 0 - // Calculate the current time in milliseconds - const currentTime = new Date().getTime() - - // Calculate the start time for the last 24 hours in milliseconds - const last24HoursStartTime = currentTime - 24 * 60 * 60 * 1000 - - // Filter the formatted data based on the timestamp - const last24HoursData = hourlyVolumeData.filter((entry) => { - const timestampMs = new Date(entry.time).getTime() - return timestampMs >= last24HoursStartTime && timestampMs <= currentTime - }) - - const volume = last24HoursData.reduce((a, c) => a + c.total_volume_usd, 0) - return volume - }, [hourlyVolumeData]) - - const oneDayPerformanceData: PerformanceDataItem[] | [] = useMemo(() => { + const rollingDailyData: PerformanceDataItem[] | [] = useMemo(() => { if (!performanceData || !performanceData.length) return [] const nowDate = new Date() return performanceData.filter((d) => { @@ -183,20 +87,6 @@ const AccountPage = () => { }) }, [performanceData]) - const onHoverMenu = (open: boolean, action: string) => { - if ( - (!open && action === 'onMouseEnter') || - (open && action === 'onMouseLeave') - ) { - setShowExpandChart(!open) - } - } - - const handleShowAccountValueChart = () => { - setChartToShow('account-value') - setShowExpandChart(false) - } - const handleHideChart = () => { setChartToShow('') } @@ -213,89 +103,17 @@ const AccountPage = () => { ] }, [group, mangoAccount]) - const leverage = useMemo(() => { - if (!group || !mangoAccount) return 0 - const assetsValue = toUiDecimalsForQuote( - mangoAccount.getAssetsValue(group).toNumber() - ) + const pnlChangeToday = useMemo(() => { + if (!accountPnl || !rollingDailyData.length) return 0 + const startHour = rollingDailyData.find((item) => { + const itemHour = new Date(item.time).getHours() + return itemHour === 0 + }) + const startDayPnl = startHour?.pnl + const pnlChangeToday = startDayPnl ? accountPnl - startDayPnl : 0 - if (isNaN(assetsValue / accountValue)) { - return 0 - } else { - return Math.abs(1 - assetsValue / accountValue) - } - }, [mangoAccount, group, accountValue]) - - const [accountValueChange, rollingDailyPnlChange, pnlChangeToday] = - useMemo(() => { - if (!accountValue || !accountPnl || !oneDayPerformanceData.length) - return [0, 0, 0] - const accountValueChange = - accountValue - oneDayPerformanceData[0].account_equity - const startHour = oneDayPerformanceData.find((item) => { - const itemHour = new Date(item.time).getHours() - return itemHour === 0 - }) - const startDayPnl = startHour?.pnl - const rollingDailyPnlChange = accountPnl - oneDayPerformanceData[0].pnl - const pnlChangeToday = startDayPnl ? accountPnl - startDayPnl : 0 - - return [accountValueChange, rollingDailyPnlChange, pnlChangeToday] - }, [accountPnl, accountValue, oneDayPerformanceData]) - - const interestTotalValue = useMemo(() => { - if (totalInterestData.length) { - return totalInterestData.reduce( - (a, c) => a + (c.borrow_interest_usd * -1 + c.deposit_interest_usd), - 0 - ) - } - return 0.0 - }, [totalInterestData]) - - const fundingTotalValue = useMemo(() => { - if (fundingData?.length && mangoAccountAddress) { - return fundingData.reduce( - (a, c) => a + c.long_funding + c.short_funding, - 0 - ) - } - return 0.0 - }, [fundingData, mangoAccountAddress]) - - const oneDayInterestChange = useMemo(() => { - if (oneDayPerformanceData.length) { - const first = oneDayPerformanceData[0] - const latest = oneDayPerformanceData[oneDayPerformanceData.length - 1] - - const startDayInterest = - first.borrow_interest_cumulative_usd + - first.deposit_interest_cumulative_usd - - const endDayInterest = - latest.borrow_interest_cumulative_usd + - latest.deposit_interest_cumulative_usd - - return endDayInterest - startDayInterest - } - return 0.0 - }, [oneDayPerformanceData]) - - const maintHealth = useMemo(() => { - return group && mangoAccount - ? mangoAccount.getHealthRatioUi(group, HealthType.maint) - : 0 - }, [mangoAccount, group]) - - const handleChartToShow = ( - chartName: - | 'pnl' - | 'cumulative-interest-value' - | 'hourly-funding' - | 'hourly-volume' - ) => { - setChartToShow(chartName) - } + return pnlChangeToday + }, [accountPnl, rollingDailyData]) const latestAccountData = useMemo(() => { if (!accountValue || !performanceData || !performanceData.length) return [] @@ -315,7 +133,6 @@ const AccountPage = () => { ] }, [accountPnl, accountValue, performanceData]) - const loadingTotalVolume = fetchingVolumeTotalData || loadingVolumeTotalData const loadingHourlyVolume = fetchingHourlyVolumeData || loadingHourlyVolumeData @@ -342,93 +159,13 @@ const AccountPage = () => {
{activeTab === 'account-value' ? ( -
-
-
- {animationSettings['number-scroll'] ? ( - group && mangoAccount ? ( - - ) : ( - - ) - ) : ( - - )} -
-
- -

- {t('rolling-change')} -

-
-
- {!performanceLoading ? ( - oneDayPerformanceData.length ? ( -
- onHoverMenu(showExpandChart, 'onMouseEnter') - } - onMouseLeave={() => - onHoverMenu(showExpandChart, 'onMouseLeave') - } - > - = 0 - ? COLORS.UP[theme] - : COLORS.DOWN[theme] - } - data={oneDayPerformanceData.concat(latestAccountData)} - name="accountValue" - xKey="time" - yKey="account_equity" - /> - - handleShowAccountValueChart()} - > - - - -
- ) : null - ) : mangoAccountAddress ? ( - -
- - ) : null} -
+ ) : null} {activeTab === 'account:assets-liabilities' ? ( @@ -439,323 +176,15 @@ const AccountPage = () => {
-
-
-
- -

- Health describes how close your account is to liquidation. - The lower your account health is the more likely you are to - get liquidated when prices fluctuate. -

- {maintHealth < 100 && mangoAccountAddress ? ( - <> -

- Your account health is {maintHealth}% -

-

- - Scenario: - {' '} - If the prices of all your liabilities increase by{' '} - {maintHealth}%, even for just a moment, some of your - liabilities will be liquidated. -

-

- - Scenario: - {' '} - If the value of your total collateral decreases by{' '} - {( - (1 - 1 / ((maintHealth || 0) / 100 + 1)) * - 100 - ).toFixed(2)} - % , some of your liabilities will be liquidated. -

-

- These are examples. A combination of events can also - lead to liquidation. -

- - ) : null} -
- } - > -

- {t('health')} -

- -
-

- {maintHealth}% -

- -
- - - {t('leverage')}: - - - x - - -
-
-
-
- -

- {t('free-collateral')} -

-
-

- -

- - - {t('total')}: - - - - - -
-
-
-
-
- -

- {t('pnl')} -

-
- {mangoAccountAddress ? ( -
- - handleChartToShow('pnl')} - > - - - - - setShowPnlHistory(true)} - > - - - -
- ) : null} -
-

- -

-
- -

{t('rolling-change')}

-
-
-
-
-
-
-

- {t('account:lifetime-volume')} -

- {mangoAccountAddress ? ( - - handleChartToShow('hourly-volume')} - > - - - - ) : null} -
- {loadingTotalVolume && mangoAccountAddress ? ( - -
- - ) : ( -

- -

- )} - - {t('account:daily-volume')}: - {loadingHourlyVolume && mangoAccountAddress ? ( - -
- - ) : ( - - - - )} - -
-
-
-
-
- -

- {t('total-interest-earned')} -

-
- {mangoAccountAddress ? ( - - - handleChartToShow('cumulative-interest-value') - } - > - - - - ) : null} -
-

- -

-
- -

{t('rolling-change')}

-
-
-
-
-
- -

- {t('account:total-funding-earned')} -

-
- {mangoAccountAddress ? ( - - handleChartToShow('hourly-funding')} - > - - - - ) : null} -
- {(loadingFunding || fetchingFunding) && mangoAccountAddress ? ( - -
- - ) : ( -

- -

- )} -
-
+ {/* {!tourSettings?.account_tour_seen && isOnBoarded && connected ? ( diff --git a/components/account/AccountValue.tsx b/components/account/AccountValue.tsx new file mode 100644 index 00000000..e9a9c868 --- /dev/null +++ b/components/account/AccountValue.tsx @@ -0,0 +1,148 @@ +import { formatCurrencyValue } from '../../utils/numbers' +import FlipNumbers from 'react-flip-numbers' +import SimpleAreaChart from '@components/shared/SimpleAreaChart' +import { COLORS } from '../../styles/colors' +import { IconButton } from '../shared/Button' +import { ArrowsPointingOutIcon } from '@heroicons/react/20/solid' +import { Transition } from '@headlessui/react' +import SheenLoader from '../shared/SheenLoader' +import Change from '../shared/Change' +import FormatNumericValue from '@components/shared/FormatNumericValue' +import { useTheme } from 'next-themes' +import useLocalStorageState from 'hooks/useLocalStorageState' +import { ANIMATION_SETTINGS_KEY } from 'utils/constants' +import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettings' +import useMangoGroup from 'hooks/useMangoGroup' +import useMangoAccount from 'hooks/useMangoAccount' +import { PerformanceDataItem } from 'types' +import { useMemo, useState } from 'react' +import { useTranslation } from 'next-i18next' +import { useViewport } from 'hooks/useViewport' +import { breakpoints } from 'utils/theme' +import { ChartToShow } from './AccountPage' + +const AccountValue = ({ + accountValue, + latestAccountData, + loading, + rollingDailyData, + setChartToShow, +}: { + accountValue: number + latestAccountData: PerformanceDataItem[] + loading: boolean + rollingDailyData: PerformanceDataItem[] + setChartToShow: (chart: ChartToShow) => void +}) => { + const { t } = useTranslation('common') + const { theme } = useTheme() + const { group } = useMangoGroup() + const { mangoAccount, mangoAccountAddress } = useMangoAccount() + const [showExpandChart, setShowExpandChart] = useState(false) + const [animationSettings] = useLocalStorageState( + ANIMATION_SETTINGS_KEY, + INITIAL_ANIMATION_SETTINGS + ) + const { width } = useViewport() + const isMobile = width ? width < breakpoints.md : false + + const accountValueChange = useMemo(() => { + if (!accountValue || !rollingDailyData.length) return 0 + const accountValueChange = accountValue - rollingDailyData[0].account_equity + return accountValueChange + }, [accountValue, rollingDailyData]) + + const onHoverMenu = (open: boolean, action: string) => { + if ( + (!open && action === 'onMouseEnter') || + (open && action === 'onMouseLeave') + ) { + setShowExpandChart(!open) + } + } + + const handleShowAccountValueChart = () => { + setChartToShow('account-value') + setShowExpandChart(false) + } + + return ( +
+
+
+ {animationSettings['number-scroll'] ? ( + group && mangoAccount ? ( + + ) : ( + + ) + ) : ( + + )} +
+
+ +

{t('rolling-change')}

+
+
+ {!loading ? ( + rollingDailyData.length ? ( +
onHoverMenu(showExpandChart, 'onMouseEnter')} + onMouseLeave={() => onHoverMenu(showExpandChart, 'onMouseLeave')} + > + = 0 ? COLORS.UP[theme] : COLORS.DOWN[theme] + } + data={rollingDailyData.concat(latestAccountData)} + name="accountValue" + xKey="time" + yKey="account_equity" + /> + + handleShowAccountValueChart()} + > + + + +
+ ) : null + ) : mangoAccountAddress ? ( + +
+ + ) : null} +
+ ) +} + +export default AccountValue From 7d42a02deffd310841d0d492e7967e0abd46610c Mon Sep 17 00:00:00 2001 From: saml33 Date: Fri, 7 Jul 2023 23:39:48 +1000 Subject: [PATCH 14/22] remove current borrow value tooltip --- components/borrow/BorrowPage.tsx | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/components/borrow/BorrowPage.tsx b/components/borrow/BorrowPage.tsx index 7a332736..6de51e7c 100644 --- a/components/borrow/BorrowPage.tsx +++ b/components/borrow/BorrowPage.tsx @@ -1,5 +1,4 @@ import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettings' -import Tooltip from '@components/shared/Tooltip' import useLocalStorageState from 'hooks/useLocalStorageState' import useMangoAccount from 'hooks/useMangoAccount' import useMangoGroup from 'hooks/useMangoGroup' @@ -94,16 +93,9 @@ const BorrowPage = () => {
- -

- {t('borrow:current-borrow-value')} -

-
+

+ {t('borrow:current-borrow-value')} +

{animationSettings['number-scroll'] ? ( group && mangoAccount ? ( From b4b3000294d8d81f77e31e0747548c1d1b0d08e9 Mon Sep 17 00:00:00 2001 From: saml33 Date: Sat, 8 Jul 2023 20:05:09 +1000 Subject: [PATCH 15/22] add orca logo --- public/icons/orca.svg | 19 +++++++++++++++++++ utils/constants.ts | 1 + 2 files changed, 20 insertions(+) create mode 100644 public/icons/orca.svg diff --git a/public/icons/orca.svg b/public/icons/orca.svg new file mode 100644 index 00000000..4d7c6d5e --- /dev/null +++ b/public/icons/orca.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/utils/constants.ts b/utils/constants.ts index 86ddda77..4c70a6d8 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -115,6 +115,7 @@ export const CUSTOM_TOKEN_ICONS: { [key: string]: boolean } = { ldo: true, mngo: true, msol: true, + orca: true, ray: true, rndr: true, sol: true, From 9221c2c8d5b101d1fb9f3a49be78148739c4dd77 Mon Sep 17 00:00:00 2001 From: saml33 Date: Sat, 8 Jul 2023 21:41:10 +1000 Subject: [PATCH 16/22] disable account action buttons disallowed for delegates --- components/AccountsButton.tsx | 6 +++++- components/account/AccountActions.tsx | 5 ++++- components/account/ActionsLinkButton.tsx | 4 +++- components/modals/DelegateModal.tsx | 2 +- components/modals/MangoAccountsListModal.tsx | 6 +++++- public/locales/en/common.json | 5 +++-- public/locales/es/common.json | 5 +++-- public/locales/ru/common.json | 5 +++-- public/locales/zh/common.json | 5 +++-- public/locales/zh_tw/common.json | 5 +++-- 10 files changed, 33 insertions(+), 15 deletions(-) diff --git a/components/AccountsButton.tsx b/components/AccountsButton.tsx index a0cb4d68..d3ba9d07 100644 --- a/components/AccountsButton.tsx +++ b/components/AccountsButton.tsx @@ -8,10 +8,12 @@ import { DEFAULT_DELEGATE } from './modals/DelegateModal' import MangoAccountsListModal from './modals/MangoAccountsListModal' import SheenLoader from './shared/SheenLoader' import Tooltip from './shared/Tooltip' +import useUnownedAccount from 'hooks/useUnownedAccount' const AccountsButton = () => { const { t } = useTranslation('common') const { mangoAccount, initialLoad } = useMangoAccount() + const { isDelegatedAccount } = useUnownedAccount() const [showCreateAccountModal, setShowCreateAccountModal] = useState(false) const [showMangoAccountsModal, setShowMangoAccountsModal] = useState(false) @@ -37,7 +39,9 @@ const AccountsButton = () => { {mangoAccount.delegate.toString() !== DEFAULT_DELEGATE ? ( diff --git a/components/account/AccountActions.tsx b/components/account/AccountActions.tsx index cd143b39..4d523c9c 100644 --- a/components/account/AccountActions.tsx +++ b/components/account/AccountActions.tsx @@ -48,7 +48,7 @@ const AccountActions = () => { const [showDelegateModal, setShowDelegateModal] = useState(false) const [showCreateAccountModal, setShowCreateAccountModal] = useState(false) const { connected } = useWallet() - const { isUnownedAccount } = useUnownedAccount() + const { isDelegatedAccount, isUnownedAccount } = useUnownedAccount() const { width } = useViewport() const isMobile = width ? width < breakpoints.sm : false @@ -129,6 +129,7 @@ const AccountActions = () => { {t('copy-address')} setShowEditAccountModal(true)} > @@ -136,6 +137,7 @@ const AccountActions = () => { {t('edit-account')} setShowDelegateModal(true)} > @@ -143,6 +145,7 @@ const AccountActions = () => { {t('delegate-account')} setShowCloseAccountModal(true)} > diff --git a/components/account/ActionsLinkButton.tsx b/components/account/ActionsLinkButton.tsx index 9523f20b..72f5ad37 100644 --- a/components/account/ActionsLinkButton.tsx +++ b/components/account/ActionsLinkButton.tsx @@ -4,17 +4,19 @@ import { ReactNode } from 'react' const ActionsLinkButton = ({ children, + disabled, mangoAccount, onClick, }: { children: ReactNode + disabled?: boolean mangoAccount: MangoAccount onClick: () => void }) => { return ( {children} diff --git a/components/modals/DelegateModal.tsx b/components/modals/DelegateModal.tsx index 03147a06..2ae4d27d 100644 --- a/components/modals/DelegateModal.tsx +++ b/components/modals/DelegateModal.tsx @@ -51,7 +51,7 @@ const DelegateModal = ({ isOpen, onClose }: ModalProps) => { title: address !== DEFAULT_DELEGATE ? t('delegate-account-info', { - address: abbreviateAddress(new PublicKey(address)), + delegate: abbreviateAddress(new PublicKey(address)), }) : 'Account delegation removed', type: 'success', diff --git a/components/modals/MangoAccountsListModal.tsx b/components/modals/MangoAccountsListModal.tsx index 096e7f20..88341ead 100644 --- a/components/modals/MangoAccountsListModal.tsx +++ b/components/modals/MangoAccountsListModal.tsx @@ -29,6 +29,7 @@ import { DEFAULT_DELEGATE } from './DelegateModal' import Tooltip from '@components/shared/Tooltip' import { abbreviateAddress } from 'utils/formatting' import { handleCopyAddress } from '@components/account/AccountActions' +import useUnownedAccount from 'hooks/useUnownedAccount' const MangoAccountsListModal = ({ isOpen, @@ -38,6 +39,7 @@ const MangoAccountsListModal = ({ onClose: () => void }) => { const { t } = useTranslation('common') + const { isDelegatedAccount } = useUnownedAccount() const { mangoAccount, initialLoad: loading } = useMangoAccount() const mangoAccounts = mangoStore((s) => s.mangoAccounts) const actions = mangoStore.getState().actions @@ -147,7 +149,9 @@ const MangoAccountsListModal = ({
diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 5195b442..e804d344 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -59,7 +59,7 @@ "date-to": "Date To", "delegate": "Delegate", "delegate-account": "Delegate Account", - "delegate-account-info": "Account delegated to {{address}}", + "delegate-account-info": "Account delegated to: {{delegate}}", "delegate-desc": "Delegate your Mango account to another wallet address", "delegate-placeholder": "Enter a wallet address to delegate to", "delete": "Delete", @@ -179,6 +179,7 @@ "withdraw-amount": "Withdraw Amount", "list-market-token": "List Market/Token", "vote": "Vote", - "yes": "Yes" + "yes": "Yes", + "you": "You" } \ No newline at end of file diff --git a/public/locales/es/common.json b/public/locales/es/common.json index 5195b442..e804d344 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -59,7 +59,7 @@ "date-to": "Date To", "delegate": "Delegate", "delegate-account": "Delegate Account", - "delegate-account-info": "Account delegated to {{address}}", + "delegate-account-info": "Account delegated to: {{delegate}}", "delegate-desc": "Delegate your Mango account to another wallet address", "delegate-placeholder": "Enter a wallet address to delegate to", "delete": "Delete", @@ -179,6 +179,7 @@ "withdraw-amount": "Withdraw Amount", "list-market-token": "List Market/Token", "vote": "Vote", - "yes": "Yes" + "yes": "Yes", + "you": "You" } \ No newline at end of file diff --git a/public/locales/ru/common.json b/public/locales/ru/common.json index 5195b442..e804d344 100644 --- a/public/locales/ru/common.json +++ b/public/locales/ru/common.json @@ -59,7 +59,7 @@ "date-to": "Date To", "delegate": "Delegate", "delegate-account": "Delegate Account", - "delegate-account-info": "Account delegated to {{address}}", + "delegate-account-info": "Account delegated to: {{delegate}}", "delegate-desc": "Delegate your Mango account to another wallet address", "delegate-placeholder": "Enter a wallet address to delegate to", "delete": "Delete", @@ -179,6 +179,7 @@ "withdraw-amount": "Withdraw Amount", "list-market-token": "List Market/Token", "vote": "Vote", - "yes": "Yes" + "yes": "Yes", + "you": "You" } \ No newline at end of file diff --git a/public/locales/zh/common.json b/public/locales/zh/common.json index 6a0e5838..29a8c5be 100644 --- a/public/locales/zh/common.json +++ b/public/locales/zh/common.json @@ -59,7 +59,7 @@ "date-to": "至", "delegate": "委托", "delegate-account": "委托帐户", - "delegate-account-info": "帐户委托给 {{address}}", + "delegate-account-info": "帐户委托给: {{delegate}}", "delegate-desc": "以Mango帐户委托给别的钱包地址", "delegate-placeholder": "输入受委钱包地执", "delete": "删除", @@ -179,6 +179,7 @@ "withdraw-amount": "取款额", "list-market-token": "List Market/Token", "vote": "投票", - "yes": "是" + "yes": "是", + "you": "You" } \ No newline at end of file diff --git a/public/locales/zh_tw/common.json b/public/locales/zh_tw/common.json index 5061696d..5ae776fa 100644 --- a/public/locales/zh_tw/common.json +++ b/public/locales/zh_tw/common.json @@ -59,7 +59,7 @@ "date-to": "至", "delegate": "委託", "delegate-account": "委託帳戶", - "delegate-account-info": "帳戶委託給 {{address}}", + "delegate-account-info": "帳戶委託給: {{delegate}}", "delegate-desc": "以Mango帳戶委託給別的錢包地址", "delegate-placeholder": "輸入受委錢包地執", "delete": "刪除", @@ -179,5 +179,6 @@ "withdraw-amount": "取款額", "list-market-token": "List Market/Token", "vote": "投票", - "yes": "是" + "yes": "是", + "you": "You" } From cea9a8908e4bfe4259ad0754c6bf8c1a7ea8f5b4 Mon Sep 17 00:00:00 2001 From: tjs Date: Sat, 8 Jul 2023 19:50:40 -0400 Subject: [PATCH 17/22] refactor data fetching out of account page into hooks --- components/account/AccountHeroStats.tsx | 9 ++-- components/account/AccountPage.tsx | 61 +++---------------------- components/account/AccountValue.tsx | 4 +- components/modals/PnlHistoryModal.tsx | 7 ++- hooks/useAccountHourlyVolumeStats.ts | 33 +++++++++++++ hooks/useAccountPerformanceData.ts | 44 ++++++++++++++++++ 6 files changed, 92 insertions(+), 66 deletions(-) create mode 100644 hooks/useAccountHourlyVolumeStats.ts create mode 100644 hooks/useAccountPerformanceData.ts diff --git a/components/account/AccountHeroStats.tsx b/components/account/AccountHeroStats.tsx index 9e17eb75..fa24bf26 100644 --- a/components/account/AccountHeroStats.tsx +++ b/components/account/AccountHeroStats.tsx @@ -17,21 +17,18 @@ import { IconButton } from '@components/shared/Button' import { CalendarIcon, ChartBarIcon } from '@heroicons/react/20/solid' import Change from '@components/shared/Change' import SheenLoader from '@components/shared/SheenLoader' -import { FormattedHourlyAccountVolumeData, PerformanceDataItem } from 'types' +import { PerformanceDataItem } from 'types' +import useAccountHourlyVolumeStats from 'hooks/useAccountHourlyVolumeStats' const AccountHeroStats = ({ accountPnl, accountValue, - hourlyVolumeData, - loadingHourlyVolume, rollingDailyData, setShowPnlHistory, setChartToShow, }: { accountPnl: number accountValue: number - hourlyVolumeData: FormattedHourlyAccountVolumeData[] | undefined - loadingHourlyVolume: boolean rollingDailyData: PerformanceDataItem[] setShowPnlHistory: (show: boolean) => void setChartToShow: (view: ChartToShow) => void @@ -39,6 +36,8 @@ const AccountHeroStats = ({ const { t } = useTranslation(['common', 'account']) const { group } = useMangoGroup() const { mangoAccount, mangoAccountAddress } = useMangoAccount() + const { hourlyVolumeData, loadingHourlyVolume } = + useAccountHourlyVolumeStats() const totalInterestData = mangoStore( (s) => s.mangoAccount.interestTotals.data diff --git a/components/account/AccountPage.tsx b/components/account/AccountPage.tsx index ff9f3298..20b9d98a 100644 --- a/components/account/AccountPage.tsx +++ b/components/account/AccountPage.tsx @@ -13,13 +13,12 @@ import { breakpoints } from 'utils/theme' import useMangoGroup from 'hooks/useMangoGroup' import PnlHistoryModal from '@components/modals/PnlHistoryModal' import AssetsLiabilities from './AssetsLiabilities' -import { PerformanceDataItem } from 'types' -import { useQuery } from '@tanstack/react-query' import FundingChart from './FundingChart' import VolumeChart from './VolumeChart' -import { fetchAccountPerformance, fetchHourlyVolume } from 'utils/account' import AccountHeroStats from './AccountHeroStats' import AccountValue from './AccountValue' +import useAccountPerformanceData from 'hooks/useAccountPerformanceData' +import useAccountHourlyVolumeStats from 'hooks/useAccountHourlyVolumeStats' const TABS = ['account-value', 'account:assets-liabilities'] @@ -34,7 +33,7 @@ export type ChartToShow = const AccountPage = () => { const { t } = useTranslation(['common', 'account']) const { group } = useMangoGroup() - const { mangoAccount, mangoAccountAddress } = useMangoAccount() + const { mangoAccount } = useMangoAccount() const [chartToShow, setChartToShow] = useState('') const [showPnlHistory, setShowPnlHistory] = useState(false) const { width } = useViewport() @@ -45,47 +44,9 @@ const AccountPage = () => { 'accountHeroKey-0.1', 'account-value' ) - - const { - data: performanceData, - isLoading: loadingPerformanceData, - isFetching: fetchingPerformanceData, - } = useQuery( - ['performance', mangoAccountAddress], - () => fetchAccountPerformance(mangoAccountAddress, 31), - { - cacheTime: 1000 * 60 * 10, - staleTime: 1000 * 60, - retry: 3, - refetchOnWindowFocus: false, - enabled: !!mangoAccountAddress, - } - ) - - const { - data: hourlyVolumeData, - isLoading: loadingHourlyVolumeData, - isFetching: fetchingHourlyVolumeData, - } = useQuery( - ['hourly-volume', mangoAccountAddress], - () => fetchHourlyVolume(mangoAccountAddress), - { - cacheTime: 1000 * 60 * 10, - staleTime: 1000 * 60, - retry: 3, - refetchOnWindowFocus: false, - enabled: !!mangoAccountAddress, - } - ) - - const rollingDailyData: PerformanceDataItem[] | [] = useMemo(() => { - if (!performanceData || !performanceData.length) return [] - const nowDate = new Date() - return performanceData.filter((d) => { - const dataTime = new Date(d.time).getTime() - return dataTime >= nowDate.getTime() - 86400000 - }) - }, [performanceData]) + const { performanceData, rollingDailyData } = useAccountPerformanceData() + const { hourlyVolumeData, loadingHourlyVolume } = + useAccountHourlyVolumeStats() const handleHideChart = () => { setChartToShow('') @@ -133,11 +94,6 @@ const AccountPage = () => { ] }, [accountPnl, accountValue, performanceData]) - const loadingHourlyVolume = - fetchingHourlyVolumeData || loadingHourlyVolumeData - - const performanceLoading = loadingPerformanceData || fetchingPerformanceData - return !chartToShow ? ( <>
@@ -162,7 +118,6 @@ const AccountPage = () => { @@ -179,8 +134,6 @@ const AccountPage = () => { { ) : null} */} {showPnlHistory ? ( void }) => { @@ -44,6 +43,7 @@ const AccountValue = ({ INITIAL_ANIMATION_SETTINGS ) const { width } = useViewport() + const { performanceLoading: loading } = useAccountPerformanceData() const isMobile = width ? width < breakpoints.md : false const accountValueChange = useMemo(() => { diff --git a/components/modals/PnlHistoryModal.tsx b/components/modals/PnlHistoryModal.tsx index a511151d..81cf8a69 100644 --- a/components/modals/PnlHistoryModal.tsx +++ b/components/modals/PnlHistoryModal.tsx @@ -7,6 +7,7 @@ import Change from '@components/shared/Change' import SheenLoader from '@components/shared/SheenLoader' import { NoSymbolIcon } from '@heroicons/react/20/solid' import { PerformanceDataItem } from 'types' +import useAccountPerformanceData from 'hooks/useAccountPerformanceData' interface PnlChange { time: string @@ -14,21 +15,19 @@ interface PnlChange { } interface PnlHistoryModalProps { - loading: boolean - performanceData: PerformanceDataItem[] | undefined pnlChangeToday: number } type ModalCombinedProps = PnlHistoryModalProps & ModalProps const PnlHistoryModal = ({ - loading, isOpen, onClose, - performanceData, pnlChangeToday, }: ModalCombinedProps) => { const { t } = useTranslation('account') + const { performanceData, performanceLoading: loading } = + useAccountPerformanceData() const dailyValues: PnlChange[] = useMemo(() => { if (!performanceData || !performanceData.length) return [] diff --git a/hooks/useAccountHourlyVolumeStats.ts b/hooks/useAccountHourlyVolumeStats.ts new file mode 100644 index 00000000..1b56478a --- /dev/null +++ b/hooks/useAccountHourlyVolumeStats.ts @@ -0,0 +1,33 @@ +import { useQuery } from '@tanstack/react-query' +import { fetchHourlyVolume } from 'utils/account' +import useMangoAccount from './useMangoAccount' + +export default function useAccountHourlyVolumeStats() { + const { mangoAccountAddress } = useMangoAccount() + + const { + data: hourlyVolumeData, + isLoading: loadingHourlyVolumeData, + isFetching: fetchingHourlyVolumeData, + } = useQuery( + ['hourly-volume', mangoAccountAddress], + () => fetchHourlyVolume(mangoAccountAddress), + { + cacheTime: 1000 * 60 * 10, + staleTime: 1000 * 60, + retry: 3, + refetchOnWindowFocus: false, + enabled: !!mangoAccountAddress, + } + ) + + const loadingHourlyVolume = + fetchingHourlyVolumeData || loadingHourlyVolumeData + + return { + hourlyVolumeData, + loadingHourlyVolumeData, + fetchingHourlyVolumeData, + loadingHourlyVolume, + } +} diff --git a/hooks/useAccountPerformanceData.ts b/hooks/useAccountPerformanceData.ts new file mode 100644 index 00000000..f8b6f0e8 --- /dev/null +++ b/hooks/useAccountPerformanceData.ts @@ -0,0 +1,44 @@ +import { useQuery } from '@tanstack/react-query' +import { fetchAccountPerformance } from 'utils/account' +import useMangoAccount from './useMangoAccount' +import { useMemo } from 'react' +import { PerformanceDataItem } from 'types' + +export default function useAccountPerformanceData() { + const { mangoAccountAddress } = useMangoAccount() + + const { + data: performanceData, + isLoading: loadingPerformanceData, + isFetching: fetchingPerformanceData, + } = useQuery( + ['performance', mangoAccountAddress], + () => fetchAccountPerformance(mangoAccountAddress, 31), + { + cacheTime: 1000 * 60 * 10, + staleTime: 1000 * 60, + retry: 3, + refetchOnWindowFocus: false, + enabled: !!mangoAccountAddress, + } + ) + + const rollingDailyData: PerformanceDataItem[] | [] = useMemo(() => { + if (!performanceData || !performanceData.length) return [] + const nowDate = new Date() + return performanceData.filter((d) => { + const dataTime = new Date(d.time).getTime() + return dataTime >= nowDate.getTime() - 86400000 + }) + }, [performanceData]) + + const performanceLoading = loadingPerformanceData || fetchingPerformanceData + + return { + performanceData, + rollingDailyData, + loadingPerformanceData, + fetchingPerformanceData, + performanceLoading, + } +} From 5bbba8727c48cc51ba459dfc1935c7e834684c6e Mon Sep 17 00:00:00 2001 From: saml33 Date: Sun, 9 Jul 2023 19:47:52 +1000 Subject: [PATCH 18/22] add scaled init asset/liab weight to dashboard --- pages/dashboard/index.tsx | 12 ++++++++---- utils/governance/listingTools.ts | 2 ++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/pages/dashboard/index.tsx b/pages/dashboard/index.tsx index bf482d83..213e3e29 100644 --- a/pages/dashboard/index.tsx +++ b/pages/dashboard/index.tsx @@ -468,7 +468,7 @@ const Dashboard: NextPage = () => { /> { `${ suggestedFields.maintAssetWeight || formattedBankValues.maintAssetWeight - }/ + } / ${ suggestedFields.maintLiabWeight || formattedBankValues.maintLiabWeight @@ -485,7 +485,7 @@ const Dashboard: NextPage = () => { /> { `${ suggestedFields.initAssetWeight || formattedBankValues.initAssetWeight - }/ + } / ${ suggestedFields.initLiabWeight || formattedBankValues.initLiabWeight }` } /> + { maintLiabWeight: bank.maintLiabWeight.toFixed(2), initAssetWeight: bank.initAssetWeight.toFixed(2), initLiabWeight: bank.initLiabWeight.toFixed(2), + scaledInitAssetWeight: bank.scaledInitAssetWeight(bank.price).toFixed(2), + scaledInitLiabWeight: bank.scaledInitLiabWeight(bank.price).toFixed(2), depositWeightScale: toUiDecimalsForQuote(bank.depositWeightScaleStartQuote), borrowWeightScale: toUiDecimalsForQuote(bank.borrowWeightScaleStartQuote), rate0: (100 * bank.rate0.toNumber()).toFixed(2), From da346e22712eaab43127c91220f33735b0a2d435 Mon Sep 17 00:00:00 2001 From: saml33 Date: Sun, 9 Jul 2023 20:06:17 +1000 Subject: [PATCH 19/22] add notional value to perp unexpanded mobile view --- components/trade/PerpPositions.tsx | 98 ++++++++++++++---------------- 1 file changed, 47 insertions(+), 51 deletions(-) diff --git a/components/trade/PerpPositions.tsx b/components/trade/PerpPositions.tsx index 1ec4d700..8e3c0390 100644 --- a/components/trade/PerpPositions.tsx +++ b/components/trade/PerpPositions.tsx @@ -321,6 +321,7 @@ const PerpPositions = () => { mangoAccount ) const unsettledPnl = position.getUnsettledPnlUi(market) + const notional = Math.abs(floorBasePosition) * market._uiPrice return ( {({ open }) => ( @@ -348,33 +349,33 @@ const PerpPositions = () => { decimals={getDecimalCount( market.minOrderSize )} - />{' '} - - {market.name.split('-')[0]} - + /> | - 0 - ? 'text-th-up' - : 'text-th-down' - }`} - > - + +
- +
+ 0 ? 'text-th-up' : 'text-th-down' + }`} + > + + + +
{
@@ -488,33 +486,31 @@ const PerpPositions = () => {

{t('trade:unrealized-pnl')}

-

- - } - delay={100} + + } + delay={100} + > + = 0 + ? 'text-th-up' + : 'text-th-down' + }`} > - = 0 - ? 'text-th-up' - : 'text-th-down' - }`} - > - - - -

+ + +

ROE

From b053906c64ec62e231c6e2d18e353a5da83718fe Mon Sep 17 00:00:00 2001 From: saml33 Date: Sun, 9 Jul 2023 20:41:20 +1000 Subject: [PATCH 20/22] add notional value to mobile open orders --- components/trade/OpenOrders.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/components/trade/OpenOrders.tsx b/components/trade/OpenOrders.tsx index d3d5fb8c..316a78e6 100644 --- a/components/trade/OpenOrders.tsx +++ b/components/trade/OpenOrders.tsx @@ -455,11 +455,9 @@ const OpenOrders = () => { ) : ( -
- - {market.name} - -
+ + {market.name} + )} {o instanceof PerpOrder ? ( @@ -482,7 +480,10 @@ const OpenOrders = () => { {' '} {quoteBank && quoteBank.name !== 'USDC' ? ( @@ -491,6 +492,9 @@ const OpenOrders = () => { ) : null}

+ + +
) : ( From 6f04ac55a7f3dc941b56921c3e4e0fd3e015aec2 Mon Sep 17 00:00:00 2001 From: saml33 Date: Sun, 9 Jul 2023 22:24:00 +1000 Subject: [PATCH 21/22] align mobile balance table styles and fix eth interest --- components/ThemeSwitcher.tsx | 31 ----- components/TokenList.tsx | 192 +++++++++++++++++----------- components/shared/BalancesTable.tsx | 30 +++-- components/shared/IconDropMenu.tsx | 81 ------------ 4 files changed, 131 insertions(+), 203 deletions(-) delete mode 100644 components/ThemeSwitcher.tsx delete mode 100644 components/shared/IconDropMenu.tsx diff --git a/components/ThemeSwitcher.tsx b/components/ThemeSwitcher.tsx deleted file mode 100644 index 5994d6d8..00000000 --- a/components/ThemeSwitcher.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { useTranslation } from 'next-i18next' -import { useTheme } from 'next-themes' -import ThemesIcon from './icons/ThemesIcon' -import { THEMES } from './settings/DisplaySettings' -import { LinkButton } from './shared/Button' -import IconDropMenu from './shared/IconDropMenu' - -const ThemeSwitcher = () => { - const { t } = useTranslation('settings') - const { theme, setTheme } = useTheme() - return ( - } - panelClassName="rounded-t-none" - > - {THEMES.map((value) => ( - setTheme(t(value))} - key={value} - > - {t(value)} - - ))} - - ) -} - -export default ThemeSwitcher diff --git a/components/TokenList.tsx b/components/TokenList.tsx index 261f0e92..ad7d3bab 100644 --- a/components/TokenList.tsx +++ b/components/TokenList.tsx @@ -1,19 +1,18 @@ import { Bank, MangoAccount } from '@blockworks-foundation/mango-v4' -import { Disclosure, Transition } from '@headlessui/react' +import { Disclosure, Popover, Transition } from '@headlessui/react' import { ChevronDownIcon, EllipsisHorizontalIcon, + XMarkIcon, } from '@heroicons/react/20/solid' import { useTranslation } from 'next-i18next' -import Image from 'next/legacy/image' import { useRouter } from 'next/router' -import { useCallback, useMemo, useState } from 'react' +import { Fragment, useCallback, useMemo, useState } from 'react' import { useViewport } from '../hooks/useViewport' import mangoStore from '@store/mangoStore' import { breakpoints } from '../utils/theme' import Switch from './forms/Switch' import ContentBox from './shared/ContentBox' -import IconDropMenu from './shared/IconDropMenu' import Tooltip from './shared/Tooltip' import { formatTokenSymbol } from 'utils/tokens' import useMangoAccount from 'hooks/useMangoAccount' @@ -109,9 +108,12 @@ const TokenList = () => { const bank = b.bank const tokenBalance = b.balance + const symbol = bank.name === 'MSOL' ? 'mSOL' : bank.name const hasInterestEarned = totalInterestData.find( - (d) => d.symbol === bank.name + (d) => + d.symbol.toLowerCase() === symbol.toLowerCase() || + (symbol === 'ETH (Portal)' && d.symbol === 'ETH') ) const interestAmount = hasInterestEarned @@ -136,7 +138,7 @@ const TokenList = () => {
-

{bank.name}

+

{symbol}

{filteredBanks.map((b) => { const bank = b.bank + const symbol = bank.name === 'MSOL' ? 'mSOL' : bank.name const inOrders = spotBalances[bank.mint.toString()]?.inOrders || 0 const unsettled = spotBalances[bank.mint.toString()]?.unsettled || 0 @@ -77,7 +78,7 @@ const BalancesTable = () => {
- {bank.name} + {symbol}
@@ -221,9 +223,13 @@ const MobileTokenListItem = ({ bank }: { bank: BankWithBalance }) => { ) const tokenBank = bank.bank const mint = tokenBank.mint - const symbol = tokenBank.name + const symbol = tokenBank.name === 'MSOL' ? 'mSOL' : tokenBank.name - const hasInterestEarned = totalInterestData.find((d) => d.symbol === symbol) + const hasInterestEarned = totalInterestData.find( + (d) => + d.symbol.toLowerCase() === symbol.toLowerCase() || + (symbol === 'ETH (Portal)' && d.symbol === 'ETH') + ) const interestAmount = hasInterestEarned ? hasInterestEarned.borrow_interest * -1 + @@ -249,31 +255,32 @@ const MobileTokenListItem = ({ bank }: { bank: BankWithBalance }) => { className={`w-full border-t border-th-bkg-3 p-4 text-left first:border-t-0 focus:outline-none`} >
-
-
+
+

{symbol}

+
+
+
+

- - -

+ + +
-
-
- {

+
+ +
@@ -426,65 +436,93 @@ const ActionsMenu = ({ router.push(`/trade?name=${spotMarket?.name}`, undefined, { shallow: true }) }, [spotMarket, router]) - const logoURI = useMemo(() => { - if (!bank || !mangoTokens?.length) return '' - return mangoTokens.find((t) => t.address === bank.mint.toString())?.logoURI - }, [bank, mangoTokens]) - return ( <> {isUnownedAccount ? null : ( - } - panelClassName="w-40 shadow-md" - postion="leftBottom" - > -
-
- + + {({ open }) => ( +
+ + {open ? ( + + ) : ( + + )} + {t('actions')} + + + +
+
+ +
+

{formatTokenSymbol(bank.name)}

+
+ handleShowActionModals(bank.name, 'deposit')} + > + {t('deposit')} + + {balance < 0 ? ( + handleShowActionModals(bank.name, 'repay')} + > + {t('repay')} + + ) : null} + {balance && balance > 0 ? ( + + handleShowActionModals(bank.name, 'withdraw') + } + > + {t('withdraw')} + + ) : null} + handleShowActionModals(bank.name, 'borrow')} + > + {t('borrow')} + + + {t('swap')} + + {spotMarket ? ( + + {t('trade')} + + ) : null} +
+
-

{formatTokenSymbol(bank.name)}

-
- handleShowActionModals(bank.name, 'deposit')} - > - {t('deposit')} - - {balance < 0 ? ( - handleShowActionModals(bank.name, 'repay')} - > - {t('repay')} - - ) : null} - {balance && balance > 0 ? ( - handleShowActionModals(bank.name, 'withdraw')} - > - {t('withdraw')} - - ) : null} - handleShowActionModals(bank.name, 'borrow')} - > - {t('borrow')} - - - {t('swap')} - - {spotMarket ? ( - - {t('trade')} - - ) : null} - + )} + )} {showDepositModal ? ( {
@@ -104,6 +105,7 @@ const BalancesTable = () => {
{filteredBanks.map((b, i) => { const bank = b.bank + const symbol = bank.name === 'MSOL' ? 'mSOL' : bank.name const inOrders = spotBalances[bank.mint.toString()]?.inOrders || 0 const unsettled = spotBalances[bank.mint.toString()]?.unsettled || 0 @@ -119,29 +121,29 @@ const BalancesTable = () => { >
-
+
-
-

- {bank.name} -

+

{symbol}

+
+
+
-

+ -

+
+
-
{ if (!balance) return

0

return ( -

+

{!isUnownedAccount && !isMobile ? ( asPath.includes('/trade') && isBaseOrQuote ? ( { - const panelPosition = { - bottomLeft: size === 'large' ? 'left-0 top-14' : 'left-0 top-12', - bottomRight: size === 'large' ? 'right-0 top-14' : 'right-0 top-12', - topLeft: size === 'large' ? 'left-0 bottom-14' : 'left-0 bottom-12', - topRight: size === 'large' ? 'right-0 bottom-14' : 'right-0 bottom-12', - leftBottom: size === 'large' ? 'right-14 bottom-0' : 'right-12 bottom-0', - leftTop: size === 'large' ? 'right-14 top-0' : 'right-12 top-0', - rightBottom: size === 'large' ? 'left-14 bottom-0' : 'left-12 bottom-0', - rightTop: size === 'large' ? 'left-14 top-0' : 'left-12 top-0', - } - return ( - - {({ open }) => ( -

- - {open ? : icon} - - - - {children} - - -
- )} - - ) -} - -export default IconDropMenu From 920a901a661dd5b940d74f5b17452db542515379 Mon Sep 17 00:00:00 2001 From: saml33 Date: Mon, 10 Jul 2023 11:33:09 +1000 Subject: [PATCH 22/22] fix trade form logos --- hooks/useSelectedMarket.ts | 39 ++++++++++++++++++++++++++------------ public/icons/ethpo.svg | 36 +++++++++++++++++++++++++++++++++++ public/icons/wbtcpo.svg | 26 +++++++++++++++++++++++++ utils/constants.ts | 2 ++ 4 files changed, 91 insertions(+), 12 deletions(-) create mode 100644 public/icons/ethpo.svg create mode 100644 public/icons/wbtcpo.svg diff --git a/hooks/useSelectedMarket.ts b/hooks/useSelectedMarket.ts index 8adc93ff..5c994a61 100644 --- a/hooks/useSelectedMarket.ts +++ b/hooks/useSelectedMarket.ts @@ -4,6 +4,7 @@ import { useMemo } from 'react' import { floorToDecimal, getDecimalCount } from 'utils/numbers' import useJupiterMints from './useJupiterMints' import useMangoGroup from './useMangoGroup' +import { CUSTOM_TOKEN_ICONS } from 'utils/constants' export default function useSelectedMarket() { const { group } = useMangoGroup() @@ -53,13 +54,22 @@ export default function useSelectedMarket() { const baseLogoURI = useMemo(() => { if (!baseSymbol || !mangoTokens.length) return '' - const token = - mangoTokens.find((t) => t.symbol.toUpperCase() === baseSymbol) || - mangoTokens.find((t) => t.symbol.toUpperCase()?.includes(baseSymbol)) - if (token) { - return token.logoURI + const lowerCaseBaseSymbol = baseSymbol.toLowerCase() + const hasCustomIcon = CUSTOM_TOKEN_ICONS[lowerCaseBaseSymbol] + if (hasCustomIcon) { + return `/icons/${lowerCaseBaseSymbol}.svg` + } else { + const token = + mangoTokens.find( + (t) => t.symbol.toLowerCase() === lowerCaseBaseSymbol + ) || + mangoTokens.find((t) => + t.symbol.toLowerCase()?.includes(lowerCaseBaseSymbol) + ) + if (token) { + return token.logoURI + } } - return '' }, [baseSymbol, mangoTokens]) const quoteBank = useMemo(() => { @@ -78,13 +88,18 @@ export default function useSelectedMarket() { const quoteLogoURI = useMemo(() => { if (!quoteSymbol || !mangoTokens.length) return '' - const token = mangoTokens.find( - (t) => t.symbol.toUpperCase() === quoteSymbol - ) - if (token) { - return token.logoURI + const lowerCaseQuoteSymbol = quoteSymbol.toLowerCase() + const hasCustomIcon = CUSTOM_TOKEN_ICONS[lowerCaseQuoteSymbol] + if (hasCustomIcon) { + return `/icons/${lowerCaseQuoteSymbol}.svg` + } else { + const token = mangoTokens.find( + (t) => t.symbol.toLowerCase() === lowerCaseQuoteSymbol + ) + if (token) { + return token.logoURI + } } - return '' }, [quoteSymbol, mangoTokens]) return { diff --git a/public/icons/ethpo.svg b/public/icons/ethpo.svg new file mode 100644 index 00000000..0b2a4619 --- /dev/null +++ b/public/icons/ethpo.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/wbtcpo.svg b/public/icons/wbtcpo.svg new file mode 100644 index 00000000..cffdb69d --- /dev/null +++ b/public/icons/wbtcpo.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/utils/constants.ts b/utils/constants.ts index 45891daa..bcb6ad30 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -111,6 +111,7 @@ export const CUSTOM_TOKEN_ICONS: { [key: string]: boolean } = { dai: true, dual: true, eth: true, + ethpo: true, 'eth (portal)': true, hnt: true, jitosol: true, @@ -124,5 +125,6 @@ export const CUSTOM_TOKEN_ICONS: { [key: string]: boolean } = { stsol: true, usdc: true, usdt: true, + wbtcpo: true, 'wbtc (portal)': true, }