From 0a122c0f794cd5ce67d34595fd6e1db9adfa1112 Mon Sep 17 00:00:00 2001 From: saml33 Date: Wed, 21 Jun 2023 09:16:58 +1000 Subject: [PATCH 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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 ? (