Merge pull request #182 from blockworks-foundation/trade-hot-keys
Trade Hot Keys
This commit is contained in:
commit
f6b3d2c5e7
|
@ -0,0 +1,494 @@
|
|||
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 } 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, TrashIcon } from '@heroicons/react/20/solid'
|
||||
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'
|
||||
|
||||
export type HotKey = {
|
||||
ioc: boolean
|
||||
keySequence: 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(['common', 'settings', 'trade'])
|
||||
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 (
|
||||
<>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="pr-6">
|
||||
<h2 className="mb-1 text-base">{t('settings:hot-keys')}</h2>
|
||||
<p>{t('settings:hot-keys-desc')}</p>
|
||||
</div>
|
||||
{hotKeys.length ? (
|
||||
<Button
|
||||
className="whitespace-nowrap"
|
||||
disabled={hotKeys.length >= 20}
|
||||
onClick={() => setShowHotKeyModal(true)}
|
||||
secondary
|
||||
>
|
||||
{t('settings:new-hot-key')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{hotKeys.length === 20 ? (
|
||||
<div className="mb-4">
|
||||
<InlineNotification
|
||||
type="warning"
|
||||
desc={t('settings:error-key-limit-reached')}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{hotKeys.length ? (
|
||||
<Table>
|
||||
<thead>
|
||||
<TrHead>
|
||||
<Th className="text-left">{t('settings:key-sequence')}</Th>
|
||||
<Th className="text-right">{t('trade:order-type')}</Th>
|
||||
<Th className="text-right">{t('trade:side')}</Th>
|
||||
<Th className="text-right">{t('trade:size')}</Th>
|
||||
<Th className="text-right">{t('price')}</Th>
|
||||
<Th className="text-right">{t('settings:options')}</Th>
|
||||
<Th />
|
||||
</TrHead>
|
||||
</thead>
|
||||
<tbody>
|
||||
{hotKeys.map((hk: HotKey) => {
|
||||
const {
|
||||
keySequence,
|
||||
orderSide,
|
||||
orderPrice,
|
||||
orderSize,
|
||||
orderSizeType,
|
||||
orderType,
|
||||
ioc,
|
||||
margin,
|
||||
reduceOnly,
|
||||
postOnly,
|
||||
} = hk
|
||||
const size =
|
||||
orderSizeType === 'percentage'
|
||||
? t('settings:percentage-of-max', { size: orderSize })
|
||||
: `$${orderSize}`
|
||||
const price = orderPrice
|
||||
? `${orderPrice}% ${
|
||||
orderSide === 'buy'
|
||||
? t('settings:below')
|
||||
: t('settings:above')
|
||||
} oracle`
|
||||
: t('trade:market')
|
||||
|
||||
const options = {
|
||||
margin: margin,
|
||||
IOC: ioc,
|
||||
post: postOnly,
|
||||
reduce: reduceOnly,
|
||||
}
|
||||
|
||||
return (
|
||||
<TrBody key={keySequence} className="text-right">
|
||||
<Td className="text-left">{keySequence}</Td>
|
||||
<Td className="text-right">{t(`trade:${orderType}`)}</Td>
|
||||
<Td className="text-right">{t(orderSide)}</Td>
|
||||
<Td className="text-right">{size}</Td>
|
||||
<Td className="text-right">{price}</Td>
|
||||
<Td className="text-right">
|
||||
{Object.entries(options).map((e) => {
|
||||
return e[1]
|
||||
? `${e[0] !== 'margin' ? ', ' : ''}${t(
|
||||
`trade:${e[0]}`
|
||||
)}`
|
||||
: ''
|
||||
})}
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex justify-end">
|
||||
<IconButton
|
||||
onClick={() => handleDeleteKey(keySequence)}
|
||||
size="small"
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Td>
|
||||
</TrBody>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
) : (
|
||||
<div className="rounded-lg border border-th-bkg-3 p-6">
|
||||
<div className="flex flex-col items-center">
|
||||
<KeyIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
|
||||
<p className="mb-4">{t('settings:no-hot-keys')}</p>
|
||||
<Button onClick={() => setShowHotKeyModal(true)}>
|
||||
<div className="flex items-center">
|
||||
{t('settings:new-hot-key')}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showHotKeyModal ? (
|
||||
<HotKeyModal
|
||||
isOpen={showHotKeyModal}
|
||||
onClose={() => setShowHotKeyModal(false)}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default HotKeysSettings
|
||||
|
||||
type FormErrors = Partial<Record<keyof HotKeyForm, string>>
|
||||
|
||||
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(['common', 'settings', 'trade'])
|
||||
const [hotKeys, setHotKeys] = useLocalStorageState<HotKey[]>(HOT_KEYS_KEY, [])
|
||||
const [hotKeyForm, setHotKeyForm] = useState<HotKeyForm>({
|
||||
...DEFAULT_FORM_VALUES,
|
||||
})
|
||||
const [formErrors, setFormErrors] = useState<FormErrors>({})
|
||||
|
||||
const handleSetForm = (propertyName: string, value: string | boolean) => {
|
||||
setFormErrors({})
|
||||
setHotKeyForm((prevState) => ({ ...prevState, [propertyName]: value }))
|
||||
}
|
||||
|
||||
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('settings:error-too-many-characters')
|
||||
}
|
||||
if (!alphanumericRegex.test(value)) {
|
||||
invalidFields[key] = t('settings:error-alphanumeric-only')
|
||||
}
|
||||
}
|
||||
for (const key of requiredFields) {
|
||||
const value = form[key] as string
|
||||
if (!value) {
|
||||
if (hotKeyForm.orderType === 'market') {
|
||||
if (key !== 'price') {
|
||||
invalidFields[key] = t('settings:error-required-field')
|
||||
}
|
||||
} else {
|
||||
invalidFields[key] = t('settings:error-required-field')
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const key of numberFields) {
|
||||
const value = form[key] as string
|
||||
if (value) {
|
||||
if (isNaN(parseFloat(value))) {
|
||||
invalidFields[key] = t('settings:error-must-be-number')
|
||||
}
|
||||
if (parseFloat(value) < 0) {
|
||||
invalidFields[key] = t('settings:error-must-be-above-zero')
|
||||
}
|
||||
if (parseFloat(value) > 100) {
|
||||
if (key === 'price') {
|
||||
invalidFields[key] = t('settings:error-must-be-below-100')
|
||||
} else {
|
||||
if (hotKeyForm.sizeType === 'percentage') {
|
||||
invalidFields[key] = t('settings:error-must-be-below-100')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
return invalidFields
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
const invalidFields = isFormValid(hotKeyForm)
|
||||
if (Object.keys(invalidFields).length) {
|
||||
return
|
||||
}
|
||||
const newHotKey = {
|
||||
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()
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<>
|
||||
<h2 className="mb-4 text-center">{t('settings:new-hot-key')}</h2>
|
||||
<div className="mb-4">
|
||||
<Label text={t('settings:base-key')} />
|
||||
<ButtonGroup
|
||||
activeValue={hotKeyForm.baseKey}
|
||||
onChange={(key) => handleSetForm('baseKey', key)}
|
||||
values={['shift', 'ctrl', 'option']}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Label text={t('settings:trigger-key')} />
|
||||
<Input
|
||||
hasError={formErrors.triggerKey !== undefined}
|
||||
type="text"
|
||||
value={hotKeyForm.triggerKey}
|
||||
onChange={(e) =>
|
||||
handleSetForm('triggerKey', e.target.value.toLowerCase())
|
||||
}
|
||||
/>
|
||||
{formErrors.triggerKey ? (
|
||||
<div className="mt-1">
|
||||
<InlineNotification
|
||||
type="error"
|
||||
desc={formErrors.triggerKey}
|
||||
hideBorder
|
||||
hidePadding
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Label text={t('settings:order-side')} />
|
||||
<ButtonGroup
|
||||
activeValue={hotKeyForm.side}
|
||||
names={[t('buy'), t('sell')]}
|
||||
onChange={(side) => handleSetForm('side', side)}
|
||||
values={['buy', 'sell']}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Label text={t('trade:order-type')} />
|
||||
<ButtonGroup
|
||||
activeValue={hotKeyForm.orderType}
|
||||
names={[t('trade:limit'), t('market')]}
|
||||
onChange={(type) => handleSetForm('orderType', type)}
|
||||
values={['limit', 'market']}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<Label text={t('settings:order-size-type')} />
|
||||
<ButtonGroup
|
||||
activeValue={hotKeyForm.sizeType}
|
||||
names={[t('settings:percentage'), t('settings:notional')]}
|
||||
onChange={(type) => handleSetForm('sizeType', type)}
|
||||
values={['percentage', 'notional']}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-start space-x-4">
|
||||
<div className="w-full">
|
||||
<Tooltip
|
||||
content={
|
||||
hotKeyForm.sizeType === 'notional'
|
||||
? t('settings:tooltip-hot-key-notional-size')
|
||||
: t('settings:tooltip-hot-key-percentage-size')
|
||||
}
|
||||
>
|
||||
<Label className="tooltip-underline" text={t('trade:size')} />
|
||||
</Tooltip>
|
||||
<Input
|
||||
hasError={formErrors.size !== undefined}
|
||||
type="text"
|
||||
value={hotKeyForm.size}
|
||||
onChange={(e) => handleSetForm('size', e.target.value)}
|
||||
suffix={hotKeyForm.sizeType === 'percentage' ? '%' : 'USD'}
|
||||
/>
|
||||
{formErrors.size ? (
|
||||
<div className="mt-1">
|
||||
<InlineNotification
|
||||
type="error"
|
||||
desc={formErrors.size}
|
||||
hideBorder
|
||||
hidePadding
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{hotKeyForm.orderType === 'limit' ? (
|
||||
<div className="w-full">
|
||||
<Tooltip content={t('settings:tooltip-hot-key-price')}>
|
||||
<Label className="tooltip-underline" text={t('price')} />
|
||||
</Tooltip>
|
||||
<Input
|
||||
hasError={formErrors.price !== undefined}
|
||||
type="text"
|
||||
value={hotKeyForm.price}
|
||||
onChange={(e) => handleSetForm('price', e.target.value)}
|
||||
placeholder="e.g. 1%"
|
||||
suffix="%"
|
||||
/>
|
||||
{formErrors.price ? (
|
||||
<div className="mt-1">
|
||||
<InlineNotification
|
||||
type="error"
|
||||
desc={formErrors.price}
|
||||
hideBorder
|
||||
hidePadding
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex flex-wrap md:flex-nowrap">
|
||||
{hotKeyForm.orderType === 'limit' ? (
|
||||
<div className="flex">
|
||||
<div className="mr-3 mt-4" id="trade-step-six">
|
||||
<Tooltip
|
||||
className="hidden md:block"
|
||||
delay={100}
|
||||
content={t('trade:tooltip-post')}
|
||||
>
|
||||
<Checkbox
|
||||
checked={hotKeyForm.post}
|
||||
onChange={(e) => handlePostOnlyChange(e.target.checked)}
|
||||
>
|
||||
{t('trade:post')}
|
||||
</Checkbox>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="mr-3 mt-4" id="trade-step-seven">
|
||||
<Tooltip
|
||||
className="hidden md:block"
|
||||
delay={100}
|
||||
content={t('trade:tooltip-ioc')}
|
||||
>
|
||||
<div className="flex items-center text-xs text-th-fgd-3">
|
||||
<Checkbox
|
||||
checked={hotKeyForm.ioc}
|
||||
onChange={(e) => handleIocChange(e.target.checked)}
|
||||
>
|
||||
IOC
|
||||
</Checkbox>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mt-4 mr-3" id="trade-step-eight">
|
||||
<Tooltip
|
||||
className="hidden md:block"
|
||||
delay={100}
|
||||
content={t('trade:tooltip-enable-margin')}
|
||||
>
|
||||
<Checkbox
|
||||
checked={hotKeyForm.margin}
|
||||
onChange={(e) => handleSetForm('margin', e.target.checked)}
|
||||
>
|
||||
{t('trade:margin')}
|
||||
</Checkbox>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="mr-3 mt-4">
|
||||
<Tooltip
|
||||
className="hidden md:block"
|
||||
delay={100}
|
||||
content={
|
||||
'Reduce will only decrease the size of an open position. This is often used for closing a position.'
|
||||
}
|
||||
>
|
||||
<div className="flex items-center text-xs text-th-fgd-3">
|
||||
<Checkbox
|
||||
checked={hotKeyForm.reduce}
|
||||
onChange={(e) => handleSetForm('reduce', e.target.checked)}
|
||||
>
|
||||
{t('trade:reduce-only')}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="mt-6 w-full" onClick={handleSave}>
|
||||
{t('settings:save-hot-key')}
|
||||
</Button>
|
||||
</>
|
||||
</Modal>
|
||||
)
|
||||
}
|
|
@ -42,7 +42,7 @@ const NotificationSettings = () => {
|
|||
<h2 className="text-base">{t('settings:notifications')}</h2>
|
||||
</div>
|
||||
{isAuth ? (
|
||||
<div className="flex items-center justify-between border-t border-th-bkg-3 p-4">
|
||||
<div className="flex items-center justify-between border-y border-th-bkg-3 p-4">
|
||||
<p>{t('settings:limit-order-filled')}</p>
|
||||
<Switch
|
||||
checked={!!data?.fillsNotifications}
|
||||
|
@ -55,7 +55,7 @@ const NotificationSettings = () => {
|
|||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-8 rounded-lg border border-th-bkg-3 p-6">
|
||||
<div className="rounded-lg border border-th-bkg-3 p-6">
|
||||
{connected ? (
|
||||
<div className="flex flex-col items-center">
|
||||
<BellIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
import { useViewport } from 'hooks/useViewport'
|
||||
import AnimationSettings from './AnimationSettings'
|
||||
import DisplaySettings from './DisplaySettings'
|
||||
import HotKeysSettings from './HotKeysSettings'
|
||||
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 (
|
||||
<div className="grid grid-cols-12">
|
||||
<div className="col-span-12 border-b border-th-bkg-3 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
|
||||
|
@ -14,9 +19,14 @@ const SettingsPage = () => {
|
|||
<div className="col-span-12 border-b border-th-bkg-3 pt-8 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
|
||||
<DisplaySettings />
|
||||
</div>
|
||||
<div className="col-span-12 border-b border-th-bkg-3 pt-8 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
|
||||
<div className="col-span-12 pt-8 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
|
||||
<NotificationSettings />
|
||||
</div>
|
||||
{!isMobile ? (
|
||||
<div className="col-span-12 pt-8 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
|
||||
<HotKeysSettings />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="col-span-12 border-b border-th-bkg-3 pt-8 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
|
||||
<AnimationSettings />
|
||||
</div>
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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 ? (
|
||||
<MobileTradeAdvancedPage />
|
||||
) : (
|
||||
<>
|
||||
<TradeHotKeys>
|
||||
<FavoriteMarketsBar />
|
||||
<ResponsiveGridLayout
|
||||
onBreakpointChange={(bp) => console.log('bp: ', bp)}
|
||||
|
@ -262,7 +263,7 @@ const TradeAdvancedPage = () => {
|
|||
{/* {!tourSettings?.trade_tour_seen && isOnboarded && connected ? (
|
||||
<TradeOnboardingTour />
|
||||
) : null} */}
|
||||
</>
|
||||
</TradeHotKeys>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,393 @@
|
|||
import {
|
||||
Group,
|
||||
MangoAccount,
|
||||
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 } from 'react'
|
||||
import Hotkeys from 'react-hot-keys'
|
||||
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'
|
||||
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 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
|
||||
|
||||
const calcBaseSize = (
|
||||
orderDetails: HotKey,
|
||||
maxSize: number,
|
||||
market: PerpMarket | Market,
|
||||
oraclePrice: number,
|
||||
quoteTokenIndex: number,
|
||||
group: Group,
|
||||
limitPrice?: number
|
||||
) => {
|
||||
const { orderSize, orderSide, orderSizeType, orderType } = orderDetails
|
||||
let baseSize: number
|
||||
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 {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
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 { 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 [soundSettings] = useLocalStorageState(
|
||||
SOUND_SETTINGS_KEY,
|
||||
INITIAL_SOUND_SETTINGS
|
||||
)
|
||||
|
||||
const handlePlaceOrder = useCallback(
|
||||
async (hkOrder: HotKey) => {
|
||||
const client = mangoStore.getState().client
|
||||
const group = mangoStore.getState().group
|
||||
const mangoAccount = mangoStore.getState().mangoAccount.current
|
||||
const actions = mangoStore.getState().actions
|
||||
const selectedMarket = mangoStore.getState().selectedMarket.current
|
||||
const {
|
||||
ioc,
|
||||
orderPrice,
|
||||
orderSide,
|
||||
orderType,
|
||||
postOnly,
|
||||
reduceOnly,
|
||||
margin,
|
||||
} = hkOrder
|
||||
|
||||
if (!group || !mangoAccount || !serumOrPerpMarket || !selectedMarket)
|
||||
return
|
||||
try {
|
||||
const orderMax =
|
||||
serumOrPerpMarket instanceof PerpMarket
|
||||
? calcPerpMax(mangoAccount, selectedMarket, orderSide)
|
||||
: calcSpotMarketMax(mangoAccount, selectedMarket, orderSide, margin)
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
// 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
|
||||
: 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',
|
||||
})
|
||||
}
|
||||
},
|
||||
[serumOrPerpMarket]
|
||||
)
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(keyName: string) => {
|
||||
const orderDetails = hotKeys.find(
|
||||
(hk: HotKey) => hk.keySequence === keyName
|
||||
)
|
||||
if (orderDetails) {
|
||||
handlePlaceOrder(orderDetails)
|
||||
}
|
||||
},
|
||||
[handlePlaceOrder, hotKeys]
|
||||
)
|
||||
|
||||
const showHotKeys =
|
||||
hotKeys.length &&
|
||||
asPath.includes('/trade') &&
|
||||
mangoAccountAddress &&
|
||||
!isUnownedAccount
|
||||
|
||||
return showHotKeys ? (
|
||||
<Hotkeys
|
||||
keyName={hotKeys.map((k: HotKey) => k.keySequence).toString()}
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
{children}
|
||||
</Hotkeys>
|
||||
) : (
|
||||
<>{children}</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TradeHotKeys
|
|
@ -65,6 +65,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",
|
||||
|
|
|
@ -17,6 +17,7 @@ export async function getStaticProps({ locale }: { locale: string }) {
|
|||
'profile',
|
||||
'search',
|
||||
'settings',
|
||||
'trade',
|
||||
])),
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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,40 @@
|
|||
"custom": "Custom",
|
||||
"dark": "Dark",
|
||||
"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",
|
||||
"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 +61,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 +72,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 +82,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",
|
||||
|
|
|
@ -36,6 +36,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",
|
||||
|
@ -65,6 +66,7 @@
|
|||
"price-provided-by": "Oracle by",
|
||||
"quote": "Quote",
|
||||
"realized-pnl": "Realized PnL",
|
||||
"reduce": "Reduce",
|
||||
"reduce-only": "Reduce Only",
|
||||
"sells": "Sells",
|
||||
"settle-funds": "Settle Funds",
|
||||
|
|
|
@ -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,40 @@
|
|||
"custom": "Custom",
|
||||
"dark": "Dark",
|
||||
"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",
|
||||
"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 +61,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 +72,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 +82,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",
|
||||
|
|
|
@ -36,6 +36,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",
|
||||
|
@ -65,6 +66,7 @@
|
|||
"price-provided-by": "Oracle by",
|
||||
"quote": "Quote",
|
||||
"realized-pnl": "Realized PnL",
|
||||
"reduce": "Reduce",
|
||||
"reduce-only": "Reduce Only",
|
||||
"sells": "Sells",
|
||||
"settle-funds": "Settle Funds",
|
||||
|
|
|
@ -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,40 @@
|
|||
"custom": "Custom",
|
||||
"dark": "Dark",
|
||||
"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",
|
||||
"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 +61,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 +72,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 +82,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",
|
||||
|
|
|
@ -36,6 +36,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",
|
||||
|
@ -65,6 +66,7 @@
|
|||
"price-provided-by": "Oracle by",
|
||||
"quote": "Quote",
|
||||
"realized-pnl": "Realized PnL",
|
||||
"reduce": "Reduce",
|
||||
"reduce-only": "Reduce Only",
|
||||
"sells": "Sells",
|
||||
"settle-funds": "Settle Funds",
|
||||
|
|
|
@ -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,20 @@
|
|||
"custom": "自定",
|
||||
"dark": "暗",
|
||||
"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",
|
||||
"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 +41,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 +63,7 @@
|
|||
"rpc-url": "输入RPC URL",
|
||||
"russian": "Русский",
|
||||
"save": "存",
|
||||
"save-hot-key": "Save Hot Key",
|
||||
"sign-to-notifications": "登录通知中心以更改设置",
|
||||
"slider": "滑块",
|
||||
"solana-beach": "Solana Beach",
|
||||
|
@ -48,11 +75,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",
|
||||
|
|
|
@ -31,14 +31,15 @@
|
|||
"insured": "{{token}} Insured",
|
||||
"last-updated": "Last updated",
|
||||
"limit": "Limit",
|
||||
"limit-price": "Limit Price",
|
||||
"long": "Long",
|
||||
"maker": "Maker",
|
||||
"limit-price": "限价价格",
|
||||
"long": "做多",
|
||||
"maker": "挂单者",
|
||||
"margin": "保证金",
|
||||
"market": "Market",
|
||||
"market-details": "{{market}}市场细节",
|
||||
"max-leverage": "最多杠杆",
|
||||
"min-order-size": "最小订单量",
|
||||
"maker-fee": "Maker Fee",
|
||||
"margin": "Margin",
|
||||
"market-details": "{{market}} Market Details",
|
||||
"max-leverage": "Max Leverage",
|
||||
"min-order-size": "Min Order Size",
|
||||
"min-order-size-error": "Min order size is {{minSize}} {{symbol}}",
|
||||
"more-details": "More Details",
|
||||
"no-balances": "No balances",
|
||||
|
@ -60,25 +61,25 @@
|
|||
"placing-order": "Placing Order",
|
||||
"positions": "Positions",
|
||||
"post": "Post",
|
||||
"preview-sound": "Preview Sound",
|
||||
"price-expect": "The price you receive may be worse than you expect and full execution is not guaranteed. Max slippage is 2.5% for your safety. The part of your position with slippage beyond 2.5% will not be closed.",
|
||||
"price-provided-by": "Oracle by",
|
||||
"quote": "Quote",
|
||||
"realized-pnl": "Realized PnL",
|
||||
"reduce-only": "Reduce Only",
|
||||
"sells": "Sells",
|
||||
"settle-funds": "Settle Funds",
|
||||
"settle-funds-error": "Failed to settle funds",
|
||||
"short": "Short",
|
||||
"show-asks": "Show Asks",
|
||||
"show-bids": "Show Bids",
|
||||
"side": "Side",
|
||||
"size": "Size",
|
||||
"spread": "Spread",
|
||||
"stable-price": "Stable Price",
|
||||
"taker": "Taker",
|
||||
"preview-sound": "声音预览",
|
||||
"price-expect": "您收到的价格可能与您预期有差异,并且无法保证完全执行。为了您的安全,最大滑点保持为 2.5%。超过 2.5%滑点的部分不会被平仓。",
|
||||
"price-provided-by": "语言机来自",
|
||||
"quote": "计价",
|
||||
"reduce": "Reduce",
|
||||
"reduce-only": "限减少",
|
||||
"sells": "卖单",
|
||||
"settle-funds": "借清资金",
|
||||
"settle-funds-error": "借清出错",
|
||||
"short": "做空",
|
||||
"show-asks": "显示要价",
|
||||
"show-bids": "显示出价",
|
||||
"side": "方向",
|
||||
"size": "數量",
|
||||
"spread": "差價",
|
||||
"stable-price": "穩定價格",
|
||||
"taker": "吃單者",
|
||||
"tick-size": "波動單位",
|
||||
"taker-fee": "Taker Fee",
|
||||
"tick-size": "Tick Size",
|
||||
"tooltip-borrow-balance": "You'll use your {{balance}} {{token}} balance and borrow {{borrowAmount}} {{token}} to execute this trade. The current {{token}} variable borrow rate is {{rate}}%",
|
||||
"tooltip-borrow-no-balance": "You'll borrow {{borrowAmount}} {{token}} to execute this trade. The current {{token}} variable borrow rate is {{rate}}%",
|
||||
"tooltip-enable-margin": "Enable spot margin for this trade",
|
||||
|
|
|
@ -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,20 @@
|
|||
"custom": "自定",
|
||||
"dark": "暗",
|
||||
"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",
|
||||
"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 +41,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 +63,7 @@
|
|||
"rpc-url": "輸入RPC URL",
|
||||
"russian": "Русский",
|
||||
"save": "存",
|
||||
"save-hot-key": "Save Hot Key",
|
||||
"sign-to-notifications": "登录通知中心以更改设置",
|
||||
"slider": "滑塊",
|
||||
"solana-beach": "Solana Beach",
|
||||
|
@ -48,11 +75,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",
|
||||
|
|
|
@ -31,14 +31,15 @@
|
|||
"insured": "{{token}} Insured",
|
||||
"last-updated": "Last updated",
|
||||
"limit": "Limit",
|
||||
"limit-price": "Limit Price",
|
||||
"long": "Long",
|
||||
"maker": "Maker",
|
||||
"maker-fee": "Maker Fee",
|
||||
"margin": "Margin",
|
||||
"market-details": "{{market}} Market Details",
|
||||
"max-leverage": "Max Leverage",
|
||||
"min-order-size": "Min Order Size",
|
||||
"limit-price": "限價價格",
|
||||
"long": "做多",
|
||||
"maker": "掛單者",
|
||||
"maker-fee": "掛單者 Fee",
|
||||
"margin": "保證金",
|
||||
"market": "Market",
|
||||
"market-details": "{{market}}市場細節",
|
||||
"max-leverage": "最多槓桿",
|
||||
"min-order-size": "最小訂單量",
|
||||
"min-order-size-error": "Min order size is {{minSize}} {{symbol}}",
|
||||
"more-details": "More Details",
|
||||
"no-balances": "No balances",
|
||||
|
@ -60,25 +61,26 @@
|
|||
"placing-order": "Placing Order",
|
||||
"positions": "Positions",
|
||||
"post": "Post",
|
||||
"preview-sound": "Preview Sound",
|
||||
"price-expect": "The price you receive may be worse than you expect and full execution is not guaranteed. Max slippage is 2.5% for your safety. The part of your position with slippage beyond 2.5% will not be closed.",
|
||||
"price-provided-by": "Oracle by",
|
||||
"quote": "Quote",
|
||||
"preview-sound": "聲音預覽",
|
||||
"price-expect": "您收到的價格可能與您預期有差異,並且無法保證完全執行。為了您的安全,最大滑點保持為 2.5%。超過 2.5%滑點的部分不會被平倉。",
|
||||
"price-provided-by": "語言機來自",
|
||||
"quote": "計價",
|
||||
"reduce": "Reduce",
|
||||
"reduce-only": "限減少",
|
||||
"sells": "賣單",
|
||||
"settle-funds": "借清資金",
|
||||
"settle-funds-error": "借清出錯",
|
||||
"short": "做空",
|
||||
"show-asks": "顯示要價",
|
||||
"show-bids": "顯示出價",
|
||||
"side": "方向",
|
||||
"size": "數量",
|
||||
"spread": "差價",
|
||||
"stable-price": "穩定價格",
|
||||
"taker": "吃單者",
|
||||
"taker-fee": "吃單者 Fee",
|
||||
"tick-size": "波動單位",
|
||||
"realized-pnl": "Realized PnL",
|
||||
"reduce-only": "Reduce Only",
|
||||
"sells": "Sells",
|
||||
"settle-funds": "Settle Funds",
|
||||
"settle-funds-error": "Failed to settle funds",
|
||||
"short": "Short",
|
||||
"show-asks": "Show Asks",
|
||||
"show-bids": "Show Bids",
|
||||
"side": "Side",
|
||||
"size": "Size",
|
||||
"spread": "Spread",
|
||||
"stable-price": "Stable Price",
|
||||
"taker": "Taker",
|
||||
"taker-fee": "Taker Fee",
|
||||
"tick-size": "Tick Size",
|
||||
"tooltip-borrow-balance": "You'll use your {{balance}} {{token}} balance and borrow {{borrowAmount}} {{token}} to execute this trade. The current {{token}} variable borrow rate is {{rate}}%",
|
||||
"tooltip-borrow-no-balance": "You'll borrow {{borrowAmount}} {{token}} to execute this trade. The current {{token}} variable borrow rate is {{rate}}%",
|
||||
"tooltip-enable-margin": "Enable spot margin for this trade",
|
||||
|
|
|
@ -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',
|
||||
|
|
13
yarn.lock
13
yarn.lock
|
@ -5379,6 +5379,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"
|
||||
|
@ -7139,6 +7144,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"
|
||||
|
|
Loading…
Reference in New Issue