Merge branch 'main' of github.com:blockworks-foundation/mango-v4-ui

This commit is contained in:
Adrian Brzeziński 2023-11-07 11:19:15 +01:00
commit c0e378b65a
51 changed files with 1523 additions and 773 deletions

View File

@ -122,7 +122,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
</div>
{/* note: overflow-x-hidden below prevents position sticky from working in activity feed */}
<div
className={`w-full overflow-x-hidden transition-all duration-${sideBarAnimationDuration} ease-in-out ${
className={`w-full transition-all duration-${sideBarAnimationDuration} ease-in-out ${
isCollapsed ? 'md:pl-[64px]' : 'pl-[200px]'
}`}
>

View File

@ -11,7 +11,6 @@ import useOpenPerpPositions from 'hooks/useOpenPerpPositions'
import HistoryTabs from './HistoryTabs'
import ManualRefresh from '@components/shared/ManualRefresh'
import useMangoAccount from 'hooks/useMangoAccount'
import SwapTriggerOrders from '@components/swap/SwapTriggerOrders'
import AccountOverview from './AccountOverview'
import AccountOrders from './AccountOrders'
@ -88,8 +87,6 @@ const TabContent = ({ activeTab }: { activeTab: string }) => {
return <PerpPositions />
case 'trade:orders':
return <AccountOrders />
case 'trade:trigger-orders':
return <SwapTriggerOrders />
case 'trade:unsettled':
return (
<UnsettledTrades

View File

@ -171,12 +171,15 @@ const ModifyTvOrderModal = ({
/>
</div>
<div className="mb-6">
<MaxSizeButton
minOrderDecimals={minOrderDecimals}
tickDecimals={tickDecimals}
useMargin={savedCheckboxSettings.margin}
large
/>
<div className="mb-2 mt-3 flex items-center justify-between">
<p className="text-th-fgd-3">{t('trade:size')}</p>
<MaxSizeButton
minOrderDecimals={minOrderDecimals}
tickDecimals={tickDecimals}
useMargin={savedCheckboxSettings.margin}
large
/>
</div>
<NumberFormat
name="size"
id="size"

View File

@ -0,0 +1,30 @@
import { useMemo } from 'react'
const TradePriceDifference = ({
currentPrice,
newPrice,
}: {
currentPrice: number | undefined
newPrice: number | undefined
}) => {
const priceDifference = useMemo(() => {
if (!currentPrice || !newPrice) return 0
const difference = ((newPrice - currentPrice) / currentPrice) * 100
return difference
}, [currentPrice, newPrice])
return (
<p
className={`font-mono text-xs ${
priceDifference >= 0 ? 'text-th-up' : 'text-th-down'
}`}
>
{priceDifference
? (priceDifference > 0 ? '+' : '') + priceDifference.toFixed(2)
: '0.00'}
%
</p>
)
}
export default TradePriceDifference

View File

@ -14,7 +14,7 @@ import { NUMBER_FORMAT_CLASSNAMES } from './MarketSwapForm'
import InlineNotification from '@components/shared/InlineNotification'
import useMangoAccount from 'hooks/useMangoAccount'
import { SwapFormTokenListType } from './SwapFormTokenList'
import { getInputTokenBalance } from './TriggerSwapForm'
import { getTokenBalance } from './TriggerSwapForm'
const ReduceOutputTokenInput = ({
error,
@ -38,7 +38,7 @@ const ReduceOutputTokenInput = ({
const reducingLong = useMemo(() => {
if (!inputBank || !mangoAccountAddress) return false
const inputBalance = getInputTokenBalance(inputBank)
const inputBalance = getTokenBalance(inputBank)
return inputBalance > 0
}, [inputBank, mangoAccountAddress])

View File

@ -14,7 +14,7 @@ import MarketSwapForm from './MarketSwapForm'
import Switch from '@components/forms/Switch'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { SwapFormTokenListType } from './SwapFormTokenList'
import { TriggerOrderTypes } from 'types'
import { SwapTypes } from 'types'
import TriggerSwapForm from './TriggerSwapForm'
import WalletSwapForm from './WalletSwapForm'
import TabButtons from '@components/shared/TabButtons'
@ -101,7 +101,7 @@ const SwapForm = () => {
}, [groupLoaded, query])
const handleSwapOrTrigger = useCallback(
(orderType: TriggerOrderTypes) => {
(orderType: SwapTypes) => {
set((state) => {
state.swap.swapOrTrigger = orderType
if (orderType !== 'swap' && outputBank?.name === OUTPUT_TOKEN_DEFAULT) {

View File

@ -14,7 +14,7 @@ import FormatNumericValue from '@components/shared/FormatNumericValue'
import { formatTokenSymbol } from 'utils/tokens'
import TokenLogo from '@components/shared/TokenLogo'
import Input from '@components/forms/Input'
import { getInputTokenBalance } from './TriggerSwapForm'
import { getTokenBalance } from './TriggerSwapForm'
import { walletBalanceForToken } from '@components/DepositForm'
import TokenReduceOnlyDesc from '@components/shared/TokenReduceOnlyDesc'
import PopularSwapTokens from './PopularSwapTokens'
@ -333,7 +333,7 @@ const SwapFormTokenList = ({
return t('swap:reduce-position')
} else {
if (!mangoAccountAddress || !inputBank) return ''
const uiPos = getInputTokenBalance(inputBank)
const uiPos = getTokenBalance(inputBank)
if (uiPos > 0) {
return t('swap:reduce-position-buy')
} else if (uiPos < 0) {

View File

@ -33,6 +33,56 @@ import { Disclosure, Transition } from '@headlessui/react'
import SheenLoader from '@components/shared/SheenLoader'
import { formatTokenSymbol } from 'utils/tokens'
export const handleCancelTriggerOrder = async (
id: BN,
setCancelId?: (id: string) => void,
) => {
try {
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const actions = mangoStore.getState().actions
const mangoAccount = mangoStore.getState().mangoAccount.current
if (!mangoAccount || !group) return
if (setCancelId) {
setCancelId(id.toString())
}
try {
const { signature: tx, slot } = await client.tokenConditionalSwapCancel(
group,
mangoAccount,
id,
)
notify({
title: 'Transaction confirmed',
type: 'success',
txid: tx,
noSound: true,
})
actions.fetchGroup()
await actions.reloadMangoAccount(slot)
} catch (e) {
console.error('failed to cancel swap order', e)
sentry.captureException(e)
if (isMangoError(e)) {
notify({
title: 'Transaction failed',
description: e.message,
txid: e?.txid,
type: 'error',
})
}
}
} catch (e) {
console.error('failed to cancel trigger order', e)
} finally {
if (setCancelId) {
setCancelId('')
}
}
}
export const handleCancelAll = async (
setCancelId: (id: '' | 'all') => void,
) => {
@ -162,49 +212,6 @@ const SwapOrders = () => {
sortConfig,
} = useSortableData(formattedTableData())
const handleCancel = async (id: BN) => {
try {
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const actions = mangoStore.getState().actions
const mangoAccount = mangoStore.getState().mangoAccount.current
if (!mangoAccount || !group) return
setCancelId(id.toString())
try {
const { signature: tx, slot } = await client.tokenConditionalSwapCancel(
group,
mangoAccount,
id,
)
notify({
title: 'Transaction confirmed',
type: 'success',
txid: tx,
noSound: true,
})
actions.fetchGroup()
await actions.reloadMangoAccount(slot)
} catch (e) {
console.error('failed to cancel swap order', e)
sentry.captureException(e)
if (isMangoError(e)) {
notify({
title: 'Transaction failed',
description: e.message,
txid: e?.txid,
type: 'error',
})
}
}
} catch (e) {
console.error('failed to cancel trigger order', e)
} finally {
setCancelId('')
}
}
return orders.length ? (
showTableView ? (
<Table>
@ -358,7 +365,9 @@ const SwapOrders = () => {
disabled={
cancelId === data.id.toString() || cancelId === 'all'
}
onClick={() => handleCancel(data.id)}
onClick={() =>
handleCancelTriggerOrder(data.id, setCancelId)
}
size="small"
>
{cancelId === data.id.toString() || cancelId === 'all' ? (
@ -495,7 +504,11 @@ const SwapOrders = () => {
<div className="col-span-1">
<p className="text-xs text-th-fgd-3">{t('cancel')}</p>
<LinkButton onClick={() => handleCancel(data.id)}>
<LinkButton
onClick={() =>
handleCancelTriggerOrder(data.id, setCancelId)
}
>
{cancelId === data.id.toString() ? (
<SheenLoader className="mt-1">
<div className="h-3.5 w-20 bg-th-bkg-2" />

View File

@ -27,9 +27,6 @@ import {
import { withValueLimit } from './MarketSwapForm'
import ReduceInputTokenInput from './ReduceInputTokenInput'
import ReduceOutputTokenInput from './ReduceOutputTokenInput'
import { notify } from 'utils/notifications'
import * as sentry from '@sentry/nextjs'
import { isMangoError } from 'types'
import Button, { LinkButton } from '@components/shared/Button'
import Loading from '@components/shared/Loading'
import TokenLogo from '@components/shared/TokenLogo'
@ -49,6 +46,8 @@ import Tooltip from '@components/shared/Tooltip'
import Link from 'next/link'
import useTokenPositionsFull from 'hooks/useTokenPositionsFull'
import TopBarStore from '@store/topBarStore'
import { TriggerOrderTypes, handlePlaceTriggerOrder } from 'utils/tradeForm'
import TradePriceDifference from '@components/shared/TradePriceDifference'
dayjs.extend(relativeTime)
@ -69,19 +68,14 @@ type TriggerSwapForm = {
type FormErrors = Partial<Record<keyof TriggerSwapForm, string>>
enum OrderTypes {
STOP_LOSS = 'trade:stop-loss',
TAKE_PROFIT = 'trade:take-profit',
}
const ORDER_TYPES = [OrderTypes.STOP_LOSS, OrderTypes.TAKE_PROFIT]
const ORDER_TYPES = [TriggerOrderTypes.STOP_LOSS, TriggerOrderTypes.TAKE_PROFIT]
const set = mangoStore.getState().set
export const getInputTokenBalance = (inputBank: Bank | undefined) => {
export const getTokenBalance = (bank: Bank | undefined) => {
const mangoAccount = mangoStore.getState().mangoAccount.current
if (!inputBank || !mangoAccount) return 0
const balance = mangoAccount.getTokenBalanceUi(inputBank)
if (!bank || !mangoAccount) return 0
const balance = mangoAccount.getTokenBalanceUi(bank)
return balance
}
@ -93,13 +87,13 @@ const getOutputTokenBalance = (outputBank: Bank | undefined) => {
}
const getOrderTypeMultiplier = (
orderType: OrderTypes,
orderType: TriggerOrderTypes,
flipPrices: boolean,
reducingShort: boolean,
) => {
if (orderType === OrderTypes.STOP_LOSS) {
if (orderType === TriggerOrderTypes.STOP_LOSS) {
return reducingShort ? (flipPrices ? 0.9 : 1.1) : flipPrices ? 1.1 : 0.9
} else if (orderType === OrderTypes.TAKE_PROFIT) {
} else if (orderType === TriggerOrderTypes.TAKE_PROFIT) {
return reducingShort ? (flipPrices ? 1.1 : 0.9) : flipPrices ? 0.9 : 1.1
} else {
return 1
@ -114,7 +108,6 @@ const TriggerSwapForm = ({
const { mangoAccountAddress } = useMangoAccount()
const { ipAllowed, ipCountry } = useIpAddress()
const { setShowSettingsModal } = TopBarStore()
// const [triggerPrice, setTriggerPrice] = useState('')
const [orderType, setOrderType] = useState(ORDER_TYPES[0])
const [submitting, setSubmitting] = useState(false)
const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider')
@ -185,7 +178,7 @@ const TriggerSwapForm = ({
const isReducingShort = useMemo(() => {
if (!mangoAccountAddress || !inputBank) return false
const inputBalance = getInputTokenBalance(inputBank)
const inputBalance = getTokenBalance(inputBank)
return inputBalance < 0
}, [inputBank, mangoAccountAddress])
@ -235,14 +228,6 @@ const TriggerSwapForm = ({
}
}, [flipPrices, orderType, isReducingShort])
const triggerPriceDifference = useMemo(() => {
if (!quotePrice) return 0
const triggerDifference = triggerPrice
? ((parseFloat(triggerPrice) - quotePrice) / quotePrice) * 100
: 0
return triggerDifference
}, [quotePrice, triggerPrice])
const handleTokenSelect = (type: SwapFormTokenListType) => {
setShowTokenSelect(type)
setFormErrors({})
@ -284,7 +269,7 @@ const TriggerSwapForm = ({
'triggerPrice',
]
const triggerPriceNumber = parseFloat(form.triggerPrice)
const inputTokenBalance = getInputTokenBalance(inputBank)
const inputTokenBalance = getTokenBalance(inputBank)
const shouldFlip = flipPrices !== isReducingShort
for (const key of requiredFields) {
const value = form[key] as string
@ -292,24 +277,20 @@ const TriggerSwapForm = ({
invalidFields[key] = t('settings:error-required-field')
}
}
if (orderType === OrderTypes.STOP_LOSS) {
if (orderType === TriggerOrderTypes.STOP_LOSS) {
if (shouldFlip && triggerPriceNumber <= quotePrice) {
invalidFields.triggerPrice =
'Trigger price must be above oracle price'
invalidFields.triggerPrice = t('trade:error-trigger-above')
}
if (!shouldFlip && triggerPriceNumber >= quotePrice) {
invalidFields.triggerPrice =
'Trigger price must be below oracle price'
invalidFields.triggerPrice = t('trade:error-trigger-below')
}
}
if (orderType === OrderTypes.TAKE_PROFIT) {
if (orderType === TriggerOrderTypes.TAKE_PROFIT) {
if (shouldFlip && triggerPriceNumber >= quotePrice) {
invalidFields.triggerPrice =
'Trigger price must be below oracle price'
invalidFields.triggerPrice = t('trade:error-trigger-below')
}
if (!shouldFlip && triggerPriceNumber <= quotePrice) {
invalidFields.triggerPrice =
'Trigger price must be above oracle price'
invalidFields.triggerPrice = t('trade:error-trigger-above')
}
}
if (form.amountIn > Math.abs(inputTokenBalance)) {
@ -457,123 +438,6 @@ const TriggerSwapForm = ({
[amountInFormValue, flipPrices, setFormErrors],
)
const handlePlaceStopLoss = useCallback(async () => {
const invalidFields = isFormValid({
amountIn: amountInAsDecimal.toNumber(),
triggerPrice,
})
if (Object.keys(invalidFields).length) {
return
}
try {
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const actions = mangoStore.getState().actions
const mangoAccount = mangoStore.getState().mangoAccount.current
const inputBank = mangoStore.getState().swap.inputBank
const outputBank = mangoStore.getState().swap.outputBank
if (!mangoAccount || !group || !inputBank || !outputBank || !triggerPrice)
return
setSubmitting(true)
const amountIn = amountInAsDecimal.toNumber()
const isReduceLong = !isReducingShort
try {
let tx
if (orderType === OrderTypes.STOP_LOSS) {
if (isReduceLong) {
tx = await client.tcsStopLossOnDeposit(
group,
mangoAccount,
inputBank,
outputBank,
parseFloat(triggerPrice),
flipPrices,
amountIn,
null,
null,
)
} else {
tx = await client.tcsStopLossOnBorrow(
group,
mangoAccount,
outputBank,
inputBank,
parseFloat(triggerPrice),
!flipPrices,
amountIn,
null,
true,
null,
)
}
}
if (orderType === OrderTypes.TAKE_PROFIT) {
if (isReduceLong) {
tx = await client.tcsTakeProfitOnDeposit(
group,
mangoAccount,
inputBank,
outputBank,
parseFloat(triggerPrice),
flipPrices,
amountIn,
null,
null,
)
} else {
tx = await client.tcsTakeProfitOnBorrow(
group,
mangoAccount,
outputBank,
inputBank,
parseFloat(triggerPrice),
!flipPrices,
amountIn,
null,
true,
null,
)
}
}
notify({
title: 'Transaction confirmed',
type: 'success',
txid: tx?.signature,
noSound: true,
})
actions.fetchGroup()
await actions.reloadMangoAccount(tx?.slot)
} catch (e) {
console.error('onSwap error: ', e)
sentry.captureException(e)
if (isMangoError(e)) {
notify({
title: 'Transaction failed',
description: e.message,
txid: e?.txid,
type: 'error',
})
}
}
} catch (e) {
console.error('Swap error:', e)
} finally {
setSubmitting(false)
}
}, [
flipPrices,
orderType,
quotePrice,
triggerPrice,
amountInAsDecimal,
amountOutFormValue,
isReducingShort,
])
const orderDescription = useMemo(() => {
if (
!amountInFormValue ||
@ -611,7 +475,7 @@ const TriggerSwapForm = ({
// xor of two flip flags
const shouldFlip = flipPrices !== isReducingShort
const orderTypeString =
orderType === OrderTypes.STOP_LOSS
orderType === TriggerOrderTypes.STOP_LOSS
? shouldFlip
? t('trade:rises-to')
: t('trade:falls-to')
@ -675,7 +539,7 @@ const TriggerSwapForm = ({
const handleOrderTypeChange = useCallback(
(type: string) => {
setFormErrors({})
const newType = type as OrderTypes
const newType = type as TriggerOrderTypes
setOrderType(newType)
const triggerMultiplier = getOrderTypeMultiplier(
newType,
@ -700,7 +564,27 @@ const TriggerSwapForm = ({
[flipPrices, quotePrice, setFormErrors, isReducingShort],
)
const onClick = !connected ? connect : handlePlaceStopLoss
const handleOrder = () => {
const invalidFields = isFormValid({
amountIn: amountInAsDecimal.toNumber(),
triggerPrice,
})
if (Object.keys(invalidFields).length) {
return
}
handlePlaceTriggerOrder(
inputBank,
outputBank,
amountInAsDecimal.toNumber(),
triggerPrice,
orderType,
isReducingShort,
flipPrices,
setSubmitting,
)
}
const onClick = !connected ? connect : handleOrder
return (
<>
@ -789,17 +673,10 @@ const TriggerSwapForm = ({
<div className="col-span-1">
<div className="mb-2 flex items-end justify-between">
<p className="text-th-fgd-2">{t('trade:trigger-price')}</p>
<p
className={`font-mono text-xs ${
triggerPriceDifference >= 0 ? 'text-th-up' : 'text-th-down'
}`}
>
{triggerPriceDifference
? (triggerPriceDifference > 0 ? '+' : '') +
triggerPriceDifference.toFixed(2)
: '0.00'}
%
</p>
<TradePriceDifference
currentPrice={quotePrice}
newPrice={parseFloat(triggerPrice)}
/>
</div>
<div className="flex items-center">
<div className="relative w-full">

View File

@ -29,7 +29,12 @@ import * as sentry from '@sentry/nextjs'
import { notify } from 'utils/notifications'
import SpotSlider from './SpotSlider'
import { calculateLimitPriceForMarketOrder } from 'utils/tradeForm'
import {
OrderTypes,
TriggerOrderTypes,
calculateLimitPriceForMarketOrder,
handlePlaceTriggerOrder,
} from 'utils/tradeForm'
import Image from 'next/legacy/image'
import { QuestionMarkCircleIcon } from '@heroicons/react/20/solid'
import Loading from '@components/shared/Loading'
@ -66,6 +71,10 @@ import SecondaryConnectButton from '@components/shared/SecondaryConnectButton'
import useRemainingBorrowsInPeriod from 'hooks/useRemainingBorrowsInPeriod'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import Select from '@components/forms/Select'
import TriggerOrderMaxButton from './TriggerOrderMaxButton'
import TradePriceDifference from '@components/shared/TradePriceDifference'
import { getTokenBalance } from '@components/swap/TriggerSwapForm'
dayjs.extend(relativeTime)
@ -88,12 +97,22 @@ export const DEFAULT_CHECKBOX_SETTINGS = {
margin: true,
}
type TradeForm = {
baseSize: number
orderType: OrderTypes | TriggerOrderTypes
price: string | undefined
side: 'buy' | 'sell'
}
type FormErrors = Partial<Record<keyof TradeForm, string>>
const AdvancedTradeForm = () => {
const { t } = useTranslation(['common', 'trade'])
const { t } = useTranslation(['common', 'settings', 'swap', 'trade'])
const { mangoAccount } = useMangoAccount()
const tradeForm = mangoStore((s) => s.tradeForm)
const themeData = mangoStore((s) => s.themeData)
const [placingOrder, setPlacingOrder] = useState(false)
const [formErrors, setFormErrors] = useState<FormErrors>({})
const [tradeFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider')
const [savedCheckboxSettings, setSavedCheckboxSettings] =
useLocalStorageState(TRADE_CHECKBOXES_KEY, DEFAULT_CHECKBOX_SETTINGS)
@ -108,6 +127,7 @@ const AdvancedTradeForm = () => {
price: oraclePrice,
baseLogoURI,
baseSymbol,
quoteBank,
quoteLogoURI,
quoteSymbol,
serumOrPerpMarket,
@ -115,11 +135,22 @@ const AdvancedTradeForm = () => {
const { remainingBorrowsInPeriod, timeToNextPeriod } =
useRemainingBorrowsInPeriod()
const setTradeType = useCallback((tradeType: 'Limit' | 'Market') => {
set((s) => {
s.tradeForm.tradeType = tradeType
})
}, [])
const baseBank = useMemo(() => {
const group = mangoStore.getState().group
if (!group || !selectedMarket || selectedMarket instanceof PerpMarket)
return
const bank = group.getFirstBankByTokenIndex(selectedMarket.baseTokenIndex)
return bank
}, [selectedMarket])
const setTradeType = useCallback(
(tradeType: OrderTypes | TriggerOrderTypes) => {
set((s) => {
s.tradeForm.tradeType = tradeType
})
},
[],
)
const handlePriceChange = useCallback(
(e: NumberFormatValues, info: SourceInfo) => {
@ -132,6 +163,7 @@ const AdvancedTradeForm = () => {
).toString()
}
})
setFormErrors({})
},
[],
)
@ -152,6 +184,7 @@ const AdvancedTradeForm = () => {
s.tradeForm.quoteSize = ''
}
})
setFormErrors({})
},
[oraclePrice],
)
@ -172,6 +205,7 @@ const AdvancedTradeForm = () => {
s.tradeForm.baseSize = ''
}
})
setFormErrors({})
},
[oraclePrice],
)
@ -232,6 +266,7 @@ const AdvancedTradeForm = () => {
set((s) => {
s.tradeForm.side = side
})
setFormErrors({})
}, [])
const handleSetMargin = useCallback(
@ -248,6 +283,8 @@ const AdvancedTradeForm = () => {
if (
!group ||
!mangoAccount ||
!baseBank ||
!quoteBank ||
!tradePrice ||
!(selectedMarket instanceof Serum3Market)
) {
@ -255,11 +292,8 @@ const AdvancedTradeForm = () => {
}
const isBuySide = side === 'buy'
const tokenIndex =
selectedMarket[isBuySide ? 'quoteTokenIndex' : 'baseTokenIndex']
const balance = mangoAccount.getTokenBalanceUi(
group.getFirstBankByTokenIndex(tokenIndex),
)
const balanceBank = isBuySide ? quoteBank : baseBank
const balance = mangoAccount.getTokenBalanceUi(balanceBank)
const max = Math.max(balance, 0)
const sizeToCompare = isBuySide ? quoteSize : baseSize
@ -292,6 +326,8 @@ const AdvancedTradeForm = () => {
})
},
[
baseBank,
quoteBank,
mangoAccount,
oraclePrice,
savedCheckboxSettings,
@ -316,25 +352,19 @@ const AdvancedTradeForm = () => {
}, [serumOrPerpMarket])
const isMarketEnabled = useMemo(() => {
const group = mangoStore.getState().group
const { group } = mangoStore.getState()
if (!selectedMarket || !group) return false
if (selectedMarket instanceof PerpMarket) {
return selectedMarket.oracleLastUpdatedSlot !== 0
} else if (selectedMarket instanceof Serum3Market) {
const baseBank = group.getFirstBankByTokenIndex(
selectedMarket.baseTokenIndex,
)
const quoteBank = group.getFirstBankByTokenIndex(
selectedMarket.quoteTokenIndex,
)
return (
baseBank.oracleLastUpdatedSlot !== 0 &&
(quoteBank.name == 'USDC'
baseBank?.oracleLastUpdatedSlot !== 0 &&
(quoteBank?.name == 'USDC'
? true
: quoteBank.oracleLastUpdatedSlot !== 0)
: quoteBank?.oracleLastUpdatedSlot !== 0)
)
}
}, [selectedMarket])
}, [baseBank, quoteBank, selectedMarket])
/*
* Updates the limit price on page load
@ -376,12 +406,56 @@ const AdvancedTradeForm = () => {
}
}, [oraclePrice, selectedMarket, tickDecimals, tradeForm])
const handlePlaceOrder = useCallback(async () => {
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const isTriggerOrder =
tradeForm.tradeType === TriggerOrderTypes.STOP_LOSS ||
tradeForm.tradeType === TriggerOrderTypes.TAKE_PROFIT
// default to correct side for trigger orders
useEffect(() => {
if (isTriggerOrder) {
const balance = getTokenBalance(baseBank)
set((state) => {
if (balance > 0) {
state.tradeForm.side = 'sell'
} else {
state.tradeForm.side = 'buy'
}
})
}
}, [isTriggerOrder])
// set default trigger price
useEffect(() => {
if (isTriggerOrder) {
let triggerPrice = oraclePrice
if (tradeForm.tradeType === TriggerOrderTypes.STOP_LOSS) {
if (tradeForm.side === 'buy') {
triggerPrice = oraclePrice * 1.1
} else {
triggerPrice = oraclePrice * 0.9
}
} else {
if (tradeForm.side === 'buy') {
triggerPrice = oraclePrice * 0.9
} else {
triggerPrice = oraclePrice * 1.1
}
}
set((state) => {
state.tradeForm.price = floorToDecimal(
triggerPrice,
tickDecimals,
).toFixed()
})
}
}, [isTriggerOrder, tickDecimals, tradeForm.side, tradeForm.tradeType])
const handleStandardOrder = useCallback(async () => {
const { client } = mangoStore.getState()
const { group } = mangoStore.getState()
const mangoAccount = mangoStore.getState().mangoAccount.current
const tradeForm = mangoStore.getState().tradeForm
const actions = mangoStore.getState().actions
const { tradeForm } = mangoStore.getState()
const { actions } = mangoStore.getState()
const selectedMarket = mangoStore.getState().selectedMarket.current
if (!group || !mangoAccount) return
@ -486,32 +560,68 @@ const AdvancedTradeForm = () => {
}
}, [])
const handleTriggerOrder = useCallback(() => {
const mangoAccount = mangoStore.getState().mangoAccount.current
const { baseSize, price, side, tradeType } = mangoStore.getState().tradeForm
const invalidFields = isFormValid({
baseSize: parseFloat(baseSize),
price: price,
orderType: tradeType,
side,
})
if (Object.keys(invalidFields).length) {
return
}
if (!mangoAccount || !baseBank || !price) return
const isReducingShort = mangoAccount.getTokenBalanceUi(baseBank) < 0
const orderType = tradeType as TriggerOrderTypes
handlePlaceTriggerOrder(
baseBank,
quoteBank,
Number(baseSize),
price,
orderType,
isReducingShort,
false,
setPlacingOrder,
)
}, [baseBank, quoteBank, setPlacingOrder])
const handleSubmit = useCallback(
(e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
isTriggerOrder ? handleTriggerOrder() : handleStandardOrder()
},
[isTriggerOrder],
)
const sideNames = useMemo(() => {
return selectedMarket instanceof PerpMarket
? [t('trade:long'), t('trade:short')]
: [t('buy'), t('sell')]
}, [selectedMarket, t])
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
handlePlaceOrder()
}
const balanceBank = useMemo(() => {
const { group } = mangoStore.getState()
if (
!group ||
!selectedMarket ||
selectedMarket instanceof PerpMarket ||
!savedCheckboxSettings.margin
!savedCheckboxSettings.margin ||
isTriggerOrder
)
return
if (tradeForm.side === 'buy') {
return group.getFirstBankByTokenIndex(selectedMarket.quoteTokenIndex)
return quoteBank
} else {
return group.getFirstBankByTokenIndex(selectedMarket.baseTokenIndex)
return baseBank
}
}, [savedCheckboxSettings, selectedMarket, tradeForm.side])
}, [
baseBank,
quoteBank,
savedCheckboxSettings,
selectedMarket,
tradeForm.side,
isTriggerOrder,
])
// check if the borrowed amount exceeds the net borrow limit in the current period
const borrowExceedsLimitInPeriod = useMemo(() => {
@ -525,11 +635,77 @@ const AdvancedTradeForm = () => {
return borrowAmountNotional > remainingBorrowsInPeriod
}, [balanceBank, mangoAccount, remainingBorrowsInPeriod, tradeForm])
const disabled =
(connected && (!tradeForm.baseSize || !tradeForm.price)) ||
!serumOrPerpMarket ||
parseFloat(tradeForm.baseSize) < serumOrPerpMarket.minOrderSize ||
!isMarketEnabled
const orderTypes = useMemo(() => {
const orderTypesArray = Object.values(OrderTypes)
if (!selectedMarket || selectedMarket instanceof PerpMarket)
return orderTypesArray
const baseBalance = floorToDecimal(
getTokenBalance(baseBank),
minOrderDecimals,
).toNumber()
const triggerOrderTypesArray = Object.values(TriggerOrderTypes)
return Math.abs(baseBalance) > 0
? [...orderTypesArray, ...triggerOrderTypesArray]
: orderTypesArray
}, [baseBank, minOrderDecimals, selectedMarket])
const isFormValid = useCallback(
(form: TradeForm) => {
const { baseSize, price, orderType, side } = form
const invalidFields: FormErrors = {}
setFormErrors({})
const requiredFields: (keyof TradeForm)[] = ['baseSize', 'price']
const priceNumber = price ? parseFloat(price) : 0
const baseTokenBalance = getTokenBalance(baseBank)
const isReducingShort = baseTokenBalance < 0
for (const key of requiredFields) {
const value = form[key] as string
if (!value) {
invalidFields[key] = t('settings:error-required-field')
}
}
if (orderType === TriggerOrderTypes.STOP_LOSS) {
if (isReducingShort && priceNumber <= oraclePrice) {
invalidFields.price = t('trade:error-trigger-above')
}
if (!isReducingShort && priceNumber >= oraclePrice) {
invalidFields.price = t('trade:error-trigger-below')
}
}
if (orderType === TriggerOrderTypes.TAKE_PROFIT) {
if (isReducingShort && priceNumber >= oraclePrice) {
invalidFields.price = t('trade:error-trigger-below')
}
if (!isReducingShort && priceNumber <= oraclePrice) {
invalidFields.price = t('trade:error-trigger-above')
}
}
if (side === 'buy' && !isReducingShort && isTriggerOrder) {
invalidFields.baseSize = t('trade:error-no-short')
}
if (side === 'sell' && isReducingShort && isTriggerOrder) {
invalidFields.baseSize = t('trade:error-no-long')
}
if (baseSize > Math.abs(baseTokenBalance) && isTriggerOrder) {
invalidFields.baseSize = t('swap:insufficient-balance', {
symbol: baseBank?.name,
})
}
if (baseSize < minOrderSize) {
invalidFields.baseSize = t('trade:min-order-size-error', {
minSize: minOrderSize,
symbol: baseSymbol,
})
}
if (Object.keys(invalidFields).length) {
setFormErrors(invalidFields)
}
return invalidFields
},
[baseBank, isTriggerOrder, minOrderSize, oraclePrice, setFormErrors],
)
const disabled = !serumOrPerpMarket || !isMarketEnabled
return (
<div>
@ -547,11 +723,30 @@ const AdvancedTradeForm = () => {
</div>
<div className="mt-1 px-2 md:mt-3 md:px-4">
<p className="mb-2 text-xs">{t('trade:order-type')}</p>
<ButtonGroup
activeValue={tradeForm.tradeType}
onChange={(tab: 'Limit' | 'Market') => setTradeType(tab)}
values={['Limit', 'Market']}
/>
{selectedMarket instanceof PerpMarket ? (
<ButtonGroup
activeValue={tradeForm.tradeType}
onChange={(tab: OrderTypes | TriggerOrderTypes) =>
setTradeType(tab)
}
values={orderTypes}
/>
) : (
<Select
value={t(tradeForm.tradeType)}
onChange={(type: OrderTypes | TriggerOrderTypes) =>
setTradeType(type)
}
>
{orderTypes.map((type) => (
<Select.Option key={type} value={type}>
<div className="flex w-full items-center justify-between">
{t(type)}
</div>
</Select.Option>
))}
</Select>
)}
</div>
{tradeForm.tradeType === 'Market' &&
selectedMarket instanceof Serum3Market ? (
@ -560,12 +755,20 @@ const AdvancedTradeForm = () => {
<>
<form onSubmit={(e) => handleSubmit(e)}>
<div className="mt-3 px-3 md:px-4">
{tradeForm.tradeType === 'Limit' ? (
{tradeForm.tradeType === 'Limit' || isTriggerOrder ? (
<>
<div className="mb-2 mt-3 flex items-center justify-between">
<p className="text-xs text-th-fgd-3">
{t('trade:limit-price')}
{isTriggerOrder
? t('trade:trigger-price')
: t('trade:limit-price')}
</p>
{tradeForm.price ? (
<TradePriceDifference
currentPrice={oraclePrice}
newPrice={parseFloat(tradeForm.price)}
/>
) : null}
</div>
<div className="relative">
{quoteLogoURI ? (
@ -597,13 +800,33 @@ const AdvancedTradeForm = () => {
/>
<div className={INPUT_SUFFIX_CLASSNAMES}>{quoteSymbol}</div>
</div>
{formErrors.price ? (
<div className="mt-1">
<InlineNotification
type="error"
desc={formErrors.price}
hideBorder
hidePadding
/>
</div>
) : null}
</>
) : null}
<MaxSizeButton
minOrderDecimals={minOrderDecimals}
tickDecimals={tickDecimals}
useMargin={savedCheckboxSettings.margin}
/>
<div className="mb-2 mt-3 flex items-center justify-between">
<p className="text-xs text-th-fgd-3">{t('trade:size')}</p>
{!isTriggerOrder ? (
<MaxSizeButton
minOrderDecimals={minOrderDecimals}
tickDecimals={tickDecimals}
useMargin={savedCheckboxSettings.margin}
/>
) : (
<TriggerOrderMaxButton
minOrderDecimals={minOrderDecimals}
tickDecimals={tickDecimals}
/>
)}
</div>
<div className="flex flex-col">
<div className="relative">
<NumberFormat
@ -664,16 +887,11 @@ const AdvancedTradeForm = () => {
/>
<div className={INPUT_SUFFIX_CLASSNAMES}>{quoteSymbol}</div>
</div>
{minOrderSize &&
tradeForm.baseSize &&
parseFloat(tradeForm.baseSize) < minOrderSize ? (
{formErrors.baseSize ? (
<div className="mt-1">
<InlineNotification
type="error"
desc={t('trade:min-order-size-error', {
minSize: minOrderSize,
symbol: baseSymbol,
})}
desc={formErrors.baseSize}
hideBorder
hidePadding
/>
@ -689,12 +907,14 @@ const AdvancedTradeForm = () => {
tickDecimals={tickDecimals}
step={tradeForm.side === 'buy' ? tickSize : minOrderSize}
useMargin={savedCheckboxSettings.margin}
isTriggerOrder
/>
) : (
<SpotButtonGroup
minOrderDecimals={minOrderDecimals}
tickDecimals={tickDecimals}
useMargin={savedCheckboxSettings.margin}
isTriggerOrder
/>
)
) : tradeFormSizeUi === 'slider' ? (
@ -746,7 +966,8 @@ const AdvancedTradeForm = () => {
</div>
</div>
) : null}
{selectedMarket instanceof Serum3Market ? (
{isTriggerOrder ? null : selectedMarket instanceof
Serum3Market ? (
<div className="mt-4" id="trade-step-eight">
<Tooltip
className="hidden md:block"
@ -838,7 +1059,7 @@ const AdvancedTradeForm = () => {
</form>
{tradeForm.tradeType === 'Market' &&
selectedMarket instanceof PerpMarket ? (
<div className="mb-4 px-3 md:px-4">
<div className="mb-4 px-4">
<InlineNotification
type="warning"
desc={t('trade:price-expect')}
@ -860,6 +1081,44 @@ const AdvancedTradeForm = () => {
/>
</div>
) : null}
{isTriggerOrder ? (
<div className="mb-4 px-4">
<InlineNotification
desc={
<div>
<span className="mr-1">{t('swap:trigger-beta')}</span>
<Tooltip
content={
<ul className="ml-4 list-disc space-y-2">
<li>
Trigger orders on long-tail assets could be
susceptible to oracle manipulation.
</li>
<li>
Trigger orders rely on a sufficient amount of well
collateralized liquidators.
</li>
<li>
The slippage on existing orders could be
higher/lower than what&apos;s estimated.
</li>
<li>
The amount of tokens used to fill your order can
vary and depends on the final execution price.
</li>
</ul>
}
>
<span className="tooltip-underline whitespace-nowrap">
{t('swap:important-info')}
</span>
</Tooltip>
</div>
}
type="info"
/>
</div>
) : null}
<TradeSummary balanceBank={balanceBank} mangoAccount={mangoAccount} />
</>
)}

View File

@ -115,20 +115,15 @@ const MaxSizeButton = ({
}, [perpMax, spotMax, selectedMarket, price, side, tradeType])
return (
<div className="mb-2 mt-3 flex items-center justify-between">
<p className={`${large ? 'text-sm' : 'text-xs'} text-th-fgd-3`}>
{t('trade:size')}
</p>
<FadeInFadeOut show={!!price && !isUnownedAccount && connected}>
<MaxAmountButton
className={large ? 'text-sm' : 'text-xs'}
decimals={minOrderDecimals}
label={t('max')}
onClick={handleMax}
value={maxAmount}
/>
</FadeInFadeOut>
</div>
<FadeInFadeOut show={!!price && !isUnownedAccount && connected}>
<MaxAmountButton
className={large ? 'text-sm' : 'text-xs'}
decimals={minOrderDecimals}
label={t('max')}
onClick={handleMax}
value={maxAmount}
/>
</FadeInFadeOut>
)
}

View File

@ -39,6 +39,7 @@ import isEqual from 'lodash/isEqual'
import { useViewport } from 'hooks/useViewport'
import TokenLogo from '@components/shared/TokenLogo'
import MarketLogos from './MarketLogos'
import { OrderTypes } from 'utils/tradeForm'
const sizeCompacter = Intl.NumberFormat('en', {
maximumFractionDigits: 6,
@ -698,12 +699,6 @@ const OrderbookRow = ({
return floorToDecimal(sizeToShow, decimals)
}, [minOrderSizeDecimals, price, size, sizeInBase, tickSizeDecimals])
// const formattedSize = useMemo(() => {
// return minOrderSize && !isNaN(size)
// ? floorToDecimal(size, getDecimalCount(minOrderSize))
// : new Decimal(size ?? -1)
// }, [size, minOrderSize, sizeInBase, tickSize])
const formattedPrice = useMemo(() => {
return tickSizeDecimals && !isNaN(price)
? floorToDecimal(price, tickSizeDecimals)
@ -714,7 +709,7 @@ const OrderbookRow = ({
const set = mangoStore.getState().set
set((state) => {
state.tradeForm.price = formattedPrice.toFixed()
state.tradeForm.tradeType = 'Limit'
state.tradeForm.tradeType = OrderTypes.LIMIT
if (state.tradeForm.baseSize) {
const quoteSize = floorToDecimal(
formattedPrice.mul(new Decimal(state.tradeForm.baseSize)),

View File

@ -151,6 +151,7 @@ const PerpPositions = () => {
</Th>
<Th className="text-right">{t('trade:unrealized-pnl')}</Th>
<Th className="text-right">ROE</Th>
<Th className="text-right">{t('funding')}</Th>
{!isUnownedAccount ? (
<Th>
{openPerpPositions?.length > 1 ? (
@ -192,6 +193,8 @@ const PerpPositions = () => {
position.cumulativePnlOverPositionLifetimeUi(market)
const unrealizedPnl = position.getUnRealizedPnlUi(market)
const realizedPnl = position.getRealizedPnlUi()
const positionFunding =
position.getCumulativeFundingUi(market)
const roe =
(unrealizedPnl / (Math.abs(basePosition) * avgEntryPrice)) *
100
@ -320,6 +323,19 @@ const PerpPositions = () => {
<FormatNumericValue value={roe} decimals={2} />%
</span>
</Td>
<Td className="text-right font-mono">
<span
className={
positionFunding >= 0 ? 'text-th-up' : 'text-th-down'
}
>
<FormatNumericValue
value={positionFunding}
decimals={2}
isUsd
/>
</span>
</Td>
{!isUnownedAccount ? (
<Td>
<div className="flex items-center justify-end space-x-4">
@ -450,6 +466,7 @@ const PerpPositions = () => {
group,
mangoAccount,
)
const positionFunding = position.getCumulativeFundingUi(market)
const unsettledPnl = position.getUnsettledPnlUi(market)
const notional = Math.abs(floorBasePosition) * market._uiPrice
return (
@ -661,6 +678,24 @@ const PerpPositions = () => {
<FormatNumericValue value={roe} decimals={2} />%
</p>
</div>
<div className="col-span-1">
<p className="text-xs text-th-fgd-3">
{t('funding')}
</p>
<p
className={`font-mono ${
positionFunding >= 0
? 'text-th-up'
: 'text-th-down'
}`}
>
<FormatNumericValue
value={positionFunding}
decimals={2}
isUsd
/>
</p>
</div>
<div className="col-span-2 mt-3 flex space-x-3">
<Button
className="w-full text-xs sm:text-sm"

View File

@ -2,24 +2,58 @@ import ButtonGroup from '@components/forms/ButtonGroup'
import mangoStore from '@store/mangoStore'
import useMangoAccount from 'hooks/useMangoAccount'
import useSelectedMarket from 'hooks/useSelectedMarket'
import { useCallback, useState } from 'react'
import { useCallback, useMemo, useState } from 'react'
import { floorToDecimal } from 'utils/numbers'
import { useSpotMarketMax } from './SpotSlider'
import { PerpMarket } from '@blockworks-foundation/mango-v4'
import Decimal from 'decimal.js'
const SpotButtonGroup = ({
minOrderDecimals,
tickDecimals,
useMargin,
isTriggerOrder,
}: {
minOrderDecimals: number
tickDecimals: number
useMargin: boolean
isTriggerOrder: boolean
}) => {
const side = mangoStore((s) => s.tradeForm.side)
const { side } = mangoStore((s) => s.tradeForm)
const { selectedMarket } = useSelectedMarket()
const { mangoAccount } = useMangoAccount()
const [sizePercentage, setSizePercentage] = useState('')
const max = useSpotMarketMax(mangoAccount, selectedMarket, side, useMargin)
const standardOrderMax = useSpotMarketMax(
mangoAccount,
selectedMarket,
side,
useMargin,
)
const max = useMemo(() => {
if (!isTriggerOrder) return standardOrderMax
const mangoAccount = mangoStore.getState().mangoAccount.current
const { group } = mangoStore.getState()
if (
!group ||
!mangoAccount ||
!selectedMarket ||
selectedMarket instanceof PerpMarket
)
return 0
const positionBank = group.getFirstBankByTokenIndex(
selectedMarket.baseTokenIndex,
)
let max = 0
const balance = mangoAccount.getTokenBalanceUi(positionBank)
const roundedBalance = floorToDecimal(balance, minOrderDecimals).toNumber()
if (side === 'buy') {
max = roundedBalance < 0 ? roundedBalance : 0
} else {
max = roundedBalance > 0 ? roundedBalance : 0
}
return Math.abs(max)
}, [isTriggerOrder, selectedMarket, side, standardOrderMax])
const handleSizePercentage = useCallback(
(percentage: string) => {
@ -28,30 +62,46 @@ const SpotButtonGroup = ({
const size = max * (Number(percentage) / 100)
set((s) => {
if (s.tradeForm.side === 'buy') {
s.tradeForm.quoteSize = floorToDecimal(size, tickDecimals).toString()
if (Number(s.tradeForm.price)) {
s.tradeForm.baseSize = floorToDecimal(
size / Number(s.tradeForm.price),
minOrderDecimals,
).toString()
} else {
s.tradeForm.baseSize = ''
const price = Number(s.tradeForm.price)
if (isTriggerOrder) {
const baseSize = floorToDecimal(size, minOrderDecimals)
s.tradeForm.baseSize = baseSize.toFixed()
if (price) {
const quoteSize = floorToDecimal(
new Decimal(size).mul(price),
tickDecimals,
)
s.tradeForm.quoteSize = quoteSize.toFixed()
}
} else if (s.tradeForm.side === 'sell') {
s.tradeForm.baseSize = floorToDecimal(size, tickDecimals).toString()
if (Number(s.tradeForm.price)) {
} else {
if (s.tradeForm.side === 'buy') {
s.tradeForm.quoteSize = floorToDecimal(
size * Number(s.tradeForm.price),
size,
tickDecimals,
).toString()
if (price) {
s.tradeForm.baseSize = floorToDecimal(
size / Number(s.tradeForm.price),
minOrderDecimals,
).toString()
} else {
s.tradeForm.baseSize = ''
}
} else if (s.tradeForm.side === 'sell') {
s.tradeForm.baseSize = floorToDecimal(size, tickDecimals).toString()
if (Number(s.tradeForm.price)) {
s.tradeForm.quoteSize = floorToDecimal(
size * Number(s.tradeForm.price),
tickDecimals,
).toString()
}
}
}
})
},
[minOrderDecimals, tickDecimals, max],
[minOrderDecimals, tickDecimals, max, isTriggerOrder],
)
return (

View File

@ -1,4 +1,8 @@
import { MangoAccount, Serum3Market } from '@blockworks-foundation/mango-v4'
import {
MangoAccount,
PerpMarket,
Serum3Market,
} from '@blockworks-foundation/mango-v4'
import LeverageSlider from '@components/shared/LeverageSlider'
import mangoStore from '@store/mangoStore'
import useMangoAccount from 'hooks/useMangoAccount'
@ -60,17 +64,48 @@ const SpotSlider = ({
tickDecimals,
step,
useMargin,
isTriggerOrder,
}: {
minOrderDecimals: number
tickDecimals: number
step: number
useMargin: boolean
isTriggerOrder: boolean
}) => {
const side = mangoStore((s) => s.tradeForm.side)
const { baseSize, quoteSize, side } = mangoStore((s) => s.tradeForm)
const { selectedMarket, price: marketPrice } = useSelectedMarket()
const { mangoAccount } = useMangoAccount()
const tradeForm = mangoStore((s) => s.tradeForm)
const max = useSpotMarketMax(mangoAccount, selectedMarket, side, useMargin)
const standardOrderMax = useSpotMarketMax(
mangoAccount,
selectedMarket,
side,
useMargin,
)
const max = useMemo(() => {
if (!isTriggerOrder) return standardOrderMax
const mangoAccount = mangoStore.getState().mangoAccount.current
const { group } = mangoStore.getState()
if (
!group ||
!mangoAccount ||
!selectedMarket ||
selectedMarket instanceof PerpMarket
)
return 0
const positionBank = group.getFirstBankByTokenIndex(
selectedMarket.baseTokenIndex,
)
let max = 0
const balance = mangoAccount.getTokenBalanceUi(positionBank)
const roundedBalance = floorToDecimal(balance, minOrderDecimals).toNumber()
if (side === 'buy') {
max = roundedBalance < 0 ? roundedBalance : 0
} else {
max = roundedBalance > 0 ? roundedBalance : 0
}
return Math.abs(max)
}, [isTriggerOrder, selectedMarket, side, standardOrderMax])
const handleSlide = useCallback(
(val: string) => {
@ -81,41 +116,50 @@ const SpotSlider = ({
s.tradeForm.tradeType === 'Market'
? marketPrice
: Number(s.tradeForm.price)
if (s.tradeForm.side === 'buy') {
if (Number(price)) {
const baseSize = floorToDecimal(
parseFloat(val) / price,
minOrderDecimals,
)
const quoteSize = floorToDecimal(baseSize.mul(price), tickDecimals)
s.tradeForm.baseSize = baseSize.toFixed()
s.tradeForm.quoteSize = quoteSize.toFixed()
} else {
s.tradeForm.baseSize = ''
s.tradeForm.quoteSize = val
}
} else if (s.tradeForm.side === 'sell') {
s.tradeForm.baseSize = val
if (Number(price)) {
s.tradeForm.quoteSize = floorToDecimal(
parseFloat(val) * price,
tickDecimals,
).toFixed()
if (isTriggerOrder) {
const baseSize = floorToDecimal(parseFloat(val), minOrderDecimals)
const quoteSize = floorToDecimal(baseSize.mul(price), tickDecimals)
s.tradeForm.baseSize = baseSize.toFixed()
s.tradeForm.quoteSize = quoteSize.toFixed()
} else {
if (s.tradeForm.side === 'buy') {
if (Number(price)) {
const baseSize = floorToDecimal(
parseFloat(val) / price,
minOrderDecimals,
)
const quoteSize = floorToDecimal(
baseSize.mul(price),
tickDecimals,
)
s.tradeForm.baseSize = baseSize.toFixed()
s.tradeForm.quoteSize = quoteSize.toFixed()
} else {
s.tradeForm.baseSize = ''
s.tradeForm.quoteSize = val
}
} else if (s.tradeForm.side === 'sell') {
s.tradeForm.baseSize = val
if (Number(price)) {
s.tradeForm.quoteSize = floorToDecimal(
parseFloat(val) * price,
tickDecimals,
).toFixed()
}
}
}
})
},
[marketPrice, minOrderDecimals, tickDecimals],
[marketPrice, minOrderDecimals, tickDecimals, isTriggerOrder],
)
return (
<div className="w-full px-3 md:px-4">
<LeverageSlider
amount={
tradeForm.side === 'buy'
? parseFloat(tradeForm.quoteSize)
: parseFloat(tradeForm.baseSize)
side === 'buy' && !isTriggerOrder
? parseFloat(quoteSize)
: parseFloat(baseSize)
}
leverageMax={max}
onChange={handleSlide}

View File

@ -1,6 +1,5 @@
import { useEffect, useMemo, useState } from 'react'
import TabButtons from '@components/shared/TabButtons'
import OpenOrders from './OpenOrders'
import SwapTradeBalances from '../shared/BalancesTable'
import UnsettledTrades from './UnsettledTrades'
import mangoStore from '@store/mangoStore'
@ -12,6 +11,7 @@ import useUnsettledPerpPositions from 'hooks/useUnsettledPerpPositions'
import TradeHistory from './TradeHistory'
import useOpenPerpPositions from 'hooks/useOpenPerpPositions'
import ManualRefresh from '@components/shared/ManualRefresh'
import AccountOrders from '@components/account/AccountOrders'
const TradeInfoTabs = () => {
const [selectedTab, setSelectedTab] = useState('balances')
@ -30,13 +30,22 @@ const TradeInfoTabs = () => {
}, [selectedMarketName])
const tabsWithCount: [string, number][] = useMemo(() => {
const mangoAccount = mangoStore.getState().mangoAccount.current
const unsettledTradeCount =
Object.values(unsettledSpotBalances).flat().length +
unsettledPerpPositions?.length
const stopOrdersCount =
mangoAccount?.tokenConditionalSwaps.filter((tcs) => tcs.hasData)
?.length || 0
return [
['balances', 0],
['trade:positions', openPerpPositions.length],
['trade:orders', Object.values(openOrders).flat().length],
[
'trade:orders',
Object.values(openOrders).flat().length + stopOrdersCount,
],
['trade:unsettled', unsettledTradeCount],
['trade-history', 0],
]
@ -79,7 +88,7 @@ const TabContent = ({ selectedTab }: { selectedTab: string }) => {
case 'balances':
return <SwapTradeBalances />
case 'trade:orders':
return <OpenOrders />
return <AccountOrders />
case 'trade:unsettled':
return (
<UnsettledTrades

View File

@ -22,7 +22,10 @@ import {
} from 'utils/numbers'
import { formatTokenSymbol } from 'utils/tokens'
import useOpenPerpPositions from 'hooks/useOpenPerpPositions'
import { calculateEstPriceForBaseSize } from 'utils/tradeForm'
import {
TriggerOrderTypes,
calculateEstPriceForBaseSize,
} from 'utils/tradeForm'
const TradeSummary = ({
balanceBank,
@ -174,6 +177,10 @@ const TradeSummary = ({
)
}, [quoteBank, tradeForm])
const isTriggerOrder =
tradeForm.tradeType === TriggerOrderTypes.STOP_LOSS ||
tradeForm.tradeType === TriggerOrderTypes.TAKE_PROFIT
return (
<div className="space-y-2 px-3 md:px-4">
<div className="flex justify-between text-xs">
@ -277,7 +284,7 @@ const TradeSummary = ({
</p>
</div>
) : null}
{selectedMarket instanceof Serum3Market ? (
{selectedMarket instanceof Serum3Market && !isTriggerOrder ? (
<div className="flex justify-between text-xs">
<p>{t('common:route')}</p>
<p className="text-th-fgd-2">Openbook</p>

View File

@ -27,11 +27,16 @@ import {
PerpOrder,
Serum3Market,
Serum3Side,
TokenConditionalSwap,
} from '@blockworks-foundation/mango-v4'
import { Order } from '@project-serum/serum/lib/market'
import { PublicKey } from '@solana/web3.js'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { formatNumericValue, getDecimalCount } from 'utils/numbers'
import {
floorToDecimal,
formatNumericValue,
getDecimalCount,
} from 'utils/numbers'
import { BN } from '@coral-xyz/anchor'
import Datafeed from 'apis/datafeed'
// import PerpDatafeed from 'apis/mngo/datafeed'
@ -43,6 +48,7 @@ import ModifyTvOrderModal from '@components/modals/ModifyTvOrderModal'
import { findSerum3MarketPkInOpenOrders } from './OpenOrders'
import { Transition } from '@headlessui/react'
import useThemeWrapper from 'hooks/useThemeWrapper'
import { handleCancelTriggerOrder } from '@components/swap/SwapTriggerOrders'
export interface ChartContainerProps {
container: ChartingLibraryWidgetOptions['container']
@ -70,6 +76,15 @@ function hexToRgb(hex: string) {
: null
}
const getTriggerOrders = () => {
const mangoAccount = mangoStore.getState().mangoAccount.current
if (!mangoAccount) return []
const triggerOrders = mangoAccount.tokenConditionalSwaps.filter(
(tcs) => tcs.hasData,
)
return triggerOrders
}
const TradingViewChart = () => {
const { t } = useTranslation(['common', 'tv-chart', 'trade'])
const { theme } = useThemeWrapper()
@ -146,9 +161,10 @@ const TradingViewChart = () => {
tvWidgetRef.current.activeChart().resolution(),
() => {
if (showOrderLinesLocalStorage) {
const openOrders = mangoStore.getState().mangoAccount.openOrders
const { openOrders } = mangoStore.getState().mangoAccount
deleteLines()
drawLinesForMarket(openOrders)
const triggerOrders = getTriggerOrders()
drawLinesForMarket(openOrders, triggerOrders)
}
return
},
@ -297,6 +313,7 @@ const TradingViewChart = () => {
order.size,
minOrderDecimals,
)
const sideColor = isLong ? COLORS.UP[theme] : COLORS.DOWN[theme]
if (!tvWidgetRef?.current?.chart()) return
return (
tvWidgetRef.current
@ -363,20 +380,126 @@ const TradingViewChart = () => {
// ? `${order.orderType} Order #: ${order.orderId}`
// : `Order #: ${order.orderId}`
// )
.setBodyTextColor(isLong ? COLORS.UP[theme] : COLORS.DOWN[theme])
.setQuantityTextColor(isLong ? COLORS.UP[theme] : COLORS.DOWN[theme])
.setBodyTextColor(sideColor)
.setQuantityTextColor(sideColor)
.setCancelButtonIconColor(COLORS.FGD4[theme])
.setBodyBorderColor(isLong ? COLORS.UP[theme] : COLORS.DOWN[theme])
.setQuantityBorderColor(
isLong ? COLORS.UP[theme] : COLORS.DOWN[theme],
)
.setCancelButtonBorderColor(
isLong ? COLORS.UP[theme] : COLORS.DOWN[theme],
)
.setBodyBorderColor(sideColor)
.setQuantityBorderColor(sideColor)
.setCancelButtonBorderColor(sideColor)
.setBodyBackgroundColor(COLORS.BKG1[theme])
.setQuantityBackgroundColor(COLORS.BKG1[theme])
.setCancelButtonBackgroundColor(COLORS.BKG1[theme])
.setLineColor(isLong ? COLORS.UP[theme] : COLORS.DOWN[theme])
.setLineColor(sideColor)
.setLineLength(3)
.setLineWidth(1)
.setLineStyle(1)
)
},
[
cancelPerpOrder,
cancelSpotOrder,
selectedMarketName,
t,
theme,
getOrderDecimals,
],
)
const drawTriggerOrderLine = useCallback(
(order: TokenConditionalSwap) => {
const { group } = mangoStore.getState()
const selectedMarket = mangoStore.getState().selectedMarket.current
if (!group) return
const buyBank = group.getFirstBankByTokenIndex(order.buyTokenIndex)
const sellBank = group.getFirstBankByTokenIndex(order.sellTokenIndex)
const maxBuy = floorToDecimal(
order.getMaxBuyUi(group),
buyBank.mintDecimals,
).toNumber()
const maxSell = floorToDecimal(
order.getMaxSellUi(group),
sellBank.mintDecimals,
).toNumber()
let side: string
let orderSizeUi: number
if (maxBuy === 0 || maxBuy > maxSell) {
orderSizeUi = maxSell
side = 'sell'
} else {
orderSizeUi = maxBuy
side = 'buy'
}
const price = order.getThresholdPriceUi(group)
const isReducingShort = side.toLowerCase() === 'buy'
let orderType
if (selectedMarket && selectedMarket instanceof Serum3Market) {
const baseBank =
selectedMarket.baseTokenIndex === buyBank.tokenIndex
? buyBank
: sellBank
const quoteBank =
selectedMarket.quoteTokenIndex === buyBank.tokenIndex
? buyBank
: sellBank
const currentPrice = baseBank.uiPrice / quoteBank.uiPrice
orderType =
(isReducingShort && price > currentPrice) ||
(!isReducingShort && price < currentPrice)
? t('trade:stop-loss')
: t('trade:take-profit')
}
const [
// minOrderDecimals,
tickSizeDecimals,
] = getOrderDecimals()
const sideColor = isReducingShort ? COLORS.UP[theme] : COLORS.DOWN[theme]
if (!tvWidgetRef?.current?.chart()) return
return (
tvWidgetRef.current
.chart()
.createOrderLine({ disableUndo: false })
.onMove(function (this: IOrderLineAdapter) {
tvWidgetRef.current?.showNoticeDialog({
title: 'Edit trigger order',
body: 'Editing trigger orders is coming soon',
callback: () => this.setPrice(price),
})
})
.onCancel(function () {
tvWidgetRef.current?.showConfirmDialog({
title: t('tv-chart:cancel-order'),
body: t('tv-chart:cancel-order-details', {
marketName: selectedMarketName,
orderSize: orderSizeUi,
orderSide: side.toUpperCase(),
orderPrice: formatNumericValue(price, tickSizeDecimals),
}),
callback: (res) => {
if (res) {
handleCancelTriggerOrder(order.id)
}
},
})
})
.setPrice(price)
.setQuantity(orderSizeUi.toString())
.setText(orderType ? orderType.toUpperCase() : side.toUpperCase())
// .setTooltip(
// order.perpTrigger?.clientOrderId
// ? `${order.orderType} Order #: ${order.orderId}`
// : `Order #: ${order.orderId}`
// )
.setBodyTextColor(sideColor)
.setQuantityTextColor(sideColor)
.setCancelButtonIconColor(COLORS.FGD4[theme])
.setBodyBorderColor(sideColor)
.setQuantityBorderColor(sideColor)
.setCancelButtonBorderColor(sideColor)
.setBodyBackgroundColor(COLORS.BKG1[theme])
.setQuantityBackgroundColor(COLORS.BKG1[theme])
.setCancelButtonBackgroundColor(COLORS.BKG1[theme])
.setLineColor(sideColor)
.setLineLength(3)
.setLineWidth(1)
.setLineStyle(1)
@ -393,15 +516,18 @@ const TradingViewChart = () => {
)
const drawLinesForMarket = useCallback(
(openOrders: Record<string, Order[] | PerpOrder[]>) => {
(
openOrders: Record<string, Order[] | PerpOrder[]>,
triggerOrders: TokenConditionalSwap[],
) => {
const set = mangoStore.getState().set
const newOrderLines = new Map()
const oOrders = Object.entries(openOrders).map(([marketPk, orders]) => ({
orders,
marketPk,
}))
const selectedMarket = mangoStore.getState().selectedMarket.current
if (oOrders?.length) {
const selectedMarket = mangoStore.getState().selectedMarket.current
const selectedMarketPk =
selectedMarket instanceof Serum3Market
? selectedMarket?.serumMarketExternal.toString()
@ -414,6 +540,24 @@ const TradingViewChart = () => {
}
}
}
if (triggerOrders.length && selectedMarket instanceof Serum3Market) {
const { baseTokenIndex, quoteTokenIndex } = selectedMarket
for (const triggerOrder of triggerOrders) {
const { buyTokenIndex, sellTokenIndex } = triggerOrder
if (
(baseTokenIndex === buyTokenIndex ||
quoteTokenIndex === buyTokenIndex) &&
(baseTokenIndex === sellTokenIndex ||
quoteTokenIndex === sellTokenIndex)
) {
newOrderLines.set(
triggerOrder.id.toString(),
drawTriggerOrderLine(triggerOrder),
)
}
}
}
set((state) => {
state.tradingView.orderLines = newOrderLines
})
@ -429,7 +573,8 @@ const TradingViewChart = () => {
el.style.color = COLORS.FGD4[theme]
} else {
const openOrders = mangoStore.getState().mangoAccount.openOrders
drawLinesForMarket(openOrders)
const triggerOrders = getTriggerOrders()
drawLinesForMarket(openOrders, triggerOrders)
el.style.color = COLORS.ACTIVE[theme]
}
},
@ -438,9 +583,10 @@ const TradingViewChart = () => {
const closeModifyOrderModal = useCallback(() => {
const openOrders = mangoStore.getState().mangoAccount.openOrders
const triggerOrders = getTriggerOrders()
setOrderToModify(null)
deleteLines()
drawLinesForMarket(openOrders)
drawLinesForMarket(openOrders, triggerOrders)
}, [deleteLines, drawLinesForMarket])
const toggleTradeExecutions = useCallback(
@ -674,9 +820,14 @@ const TradingViewChart = () => {
let subscription
if (chartReady && tvWidgetRef?.current) {
subscription = mangoStore.subscribe(
(state) => state.mangoAccount.openOrders,
(openOrders) => {
(state) => state.mangoAccount,
(account) => {
if (showOrderLines) {
const openOrders = account.openOrders
const triggerOrders =
account.current?.tokenConditionalSwaps.filter(
(tcs) => tcs.hasData,
) || []
const orderLines = mangoStore.getState().tradingView.orderLines
tvWidgetRef.current?.onChartReady(() => {
let matchingOrderLines = 0
@ -697,6 +848,11 @@ const TradingViewChart = () => {
}
}
})
triggerOrders?.forEach((order) => {
if (order.id.toString() == key) {
matchingOrderLines += 1
}
})
}
const selectedMarket =
@ -706,11 +862,29 @@ const TradingViewChart = () => {
? selectedMarket?.serumMarketExternal.toString()
: selectedMarket?.publicKey.toString()
let ordersForMarket = 0
oOrders?.forEach(({ marketPk, orders }) => {
if (marketPk === selectedMarketPk) {
openOrdersForMarket = orders.length
ordersForMarket = orders.length
}
})
let triggerOrdersForMarket = 0
triggerOrders.forEach((order) => {
if (selectedMarket instanceof Serum3Market) {
const { baseTokenIndex, quoteTokenIndex } = selectedMarket
const { buyTokenIndex, sellTokenIndex } = order
if (
(baseTokenIndex === buyTokenIndex ||
quoteTokenIndex === buyTokenIndex) &&
(baseTokenIndex === sellTokenIndex ||
quoteTokenIndex === sellTokenIndex)
) {
triggerOrdersForMarket += 1
}
}
})
openOrdersForMarket = ordersForMarket + triggerOrdersForMarket
tvWidgetRef.current?.activeChart().dataReady(() => {
if (
@ -718,7 +892,9 @@ const TradingViewChart = () => {
orderLines?.size !== matchingOrderLines
) {
deleteLines()
drawLinesForMarket(openOrders)
// might need to change
const triggerOrders = getTriggerOrders()
drawLinesForMarket(openOrders, triggerOrders)
}
})
})

View File

@ -0,0 +1,74 @@
import { PerpMarket } from '@blockworks-foundation/mango-v4'
import MaxAmountButton from '@components/shared/MaxAmountButton'
import { FadeInFadeOut } from '@components/shared/Transitions'
import { useWallet } from '@solana/wallet-adapter-react'
import mangoStore from '@store/mangoStore'
import useSelectedMarket from 'hooks/useSelectedMarket'
import useUnownedAccount from 'hooks/useUnownedAccount'
import { useTranslation } from 'next-i18next'
import { useCallback, useMemo } from 'react'
import { floorToDecimal } from 'utils/numbers'
const TriggerOrderMaxButton = ({
minOrderDecimals,
tickDecimals,
large,
}: {
minOrderDecimals: number
tickDecimals: number
large?: boolean
}) => {
const { t } = useTranslation(['common', 'trade'])
const { selectedMarket } = useSelectedMarket()
const { price, side } = mangoStore((s) => s.tradeForm)
const { isUnownedAccount } = useUnownedAccount()
const { connected } = useWallet()
const positionBank = useMemo(() => {
const { group } = mangoStore.getState()
if (!group || !selectedMarket || selectedMarket instanceof PerpMarket)
return
const bank = group.getFirstBankByTokenIndex(selectedMarket.baseTokenIndex)
return bank
}, [selectedMarket])
const max = useMemo(() => {
const mangoAccount = mangoStore.getState().mangoAccount.current
if (!positionBank || !mangoAccount) return 0
let max = 0
const balance = mangoAccount.getTokenBalanceUi(positionBank)
const roundedBalance = floorToDecimal(balance, minOrderDecimals).toNumber()
if (side === 'buy') {
max = roundedBalance < 0 ? roundedBalance : 0
} else {
max = roundedBalance > 0 ? roundedBalance : 0
}
return max
}, [positionBank, side, minOrderDecimals])
const handleMax = useCallback(() => {
const set = mangoStore.getState().set
const baseSize = floorToDecimal(Math.abs(max), minOrderDecimals)
const quoteSize = price
? floorToDecimal(baseSize.mul(price), tickDecimals)
: 0
set((state) => {
state.tradeForm.baseSize = baseSize.toFixed()
state.tradeForm.quoteSize = quoteSize.toFixed()
})
}, [minOrderDecimals, price, tickDecimals])
return (
<FadeInFadeOut show={!!price && !isUnownedAccount && connected}>
<MaxAmountButton
className={large ? 'text-sm' : 'text-xs'}
decimals={minOrderDecimals}
label={t('trade:position')}
onClick={handleMax}
value={max}
/>
</FadeInFadeOut>
)
}
export default TriggerOrderMaxButton

View File

@ -23,7 +23,7 @@
"dependencies": {
"@blockworks-foundation/mango-feeds": "0.1.7",
"@blockworks-foundation/mango-mints-redemption": "^0.0.10",
"@blockworks-foundation/mango-v4": "^0.19.41",
"@blockworks-foundation/mango-v4": "0.19.43",
"@blockworks-foundation/mango-v4-settings": "0.2.16",
"@blockworks-foundation/mangolana": "0.0.1-beta.15",
"@headlessui/react": "1.6.6",

View File

@ -32,6 +32,7 @@ import DashboardSuggestedValues from '@components/modals/DashboardSuggestedValue
import { USDC_MINT } from 'utils/constants'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import useBanks from 'hooks/useBanks'
dayjs.extend(relativeTime)
@ -55,9 +56,34 @@ export async function getStaticProps({ locale }: { locale: string }) {
const Dashboard: NextPage = () => {
const { group } = useMangoGroup()
const connection = mangoStore((s) => s.connection)
const { banks } = useBanks()
const [isOpenSuggestionModal, setIsOpenSuggestionModal] = useState(false)
const [priceImpacts, setPriceImapcts] = useState<PriceImpact[]>([])
const [stickyIndex, setStickyIndex] = useState(-1)
const handleScroll = useCallback(() => {
for (let i = 0; i < banks.length; i++) {
const element = document.getElementById(`parent-item-${i}`)
if (element) {
const rect = element.getBoundingClientRect()
if (rect.top <= 0) {
setStickyIndex(i)
}
}
}
}, [banks])
useEffect(() => {
if (banks.length) {
window.addEventListener('scroll', handleScroll)
return () => {
window.removeEventListener('scroll', handleScroll)
}
}
}, [banks])
useEffect(() => {
const handleGetPriceImapcts = async () => {
@ -114,28 +140,35 @@ const Dashboard: NextPage = () => {
</div>
<h3 className="mb-3 mt-6 text-base text-th-fgd-3">Banks</h3>
<div className="border-b border-th-bkg-3">
{Array.from(group.banksMapByMint)
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([mintAddress, banks]) =>
banks.map((bank) => {
const mintInfo = group.mintInfosMapByMint.get(
bank.mint.toString(),
)
{banks
.sort((a, b) => a.name.localeCompare(b.name))
.map((bank, i) => {
const mintInfo = group.mintInfosMapByMint.get(
bank.mint.toString(),
)
const formattedBankValues = getFormattedBankValues(
group,
bank,
)
const formattedBankValues = getFormattedBankValues(
group,
bank,
)
return (
<Disclosure key={bank.publicKey.toString()}>
{({ open }) => (
<>
return (
<Disclosure key={bank.publicKey.toString()}>
{({ open }) => (
<>
<div
className={`w-full border-t border-th-bkg-3 p-4 md:hover:bg-th-bkg-4 ${
open
? i === stickyIndex
? 'sticky top-0 bg-th-bkg-4'
: 'bg-th-bkg-4'
: ''
}`}
id={`parent-item-${i}`}
>
<Disclosure.Button
className="flex w-full items-center justify-between"
aria-label="panel"
className={`flex w-full items-center justify-between border-t border-th-bkg-3 p-4 md:hover:bg-th-bkg-4 ${
open ? 'bg-th-bkg-4' : ''
}`}
>
<div className="flex items-center">
<TokenLogo bank={bank} />
@ -149,243 +182,245 @@ const Dashboard: NextPage = () => {
} h-5 w-5 text-th-fgd-3`}
/>
</Disclosure.Button>
<Disclosure.Panel>
<KeyValuePair
label="Mint"
value={
<ExplorerLink address={mintAddress} />
}
/>
<KeyValuePair
label="Bank"
value={
<ExplorerLink
address={formattedBankValues.publicKey.toString()}
anchorData
/>
}
/>
<KeyValuePair
label="MintInfo"
value={
<ExplorerLink
address={mintInfo!.publicKey.toString()}
anchorData
/>
}
/>
<KeyValuePair
label="Vault"
value={
<ExplorerLink
address={formattedBankValues.vault}
anchorData
/>
}
/>
<KeyValuePair
label="Oracle"
value={
bank.oracleProvider ==
OracleProvider.Switchboard ? (
<a
href={`https://app.switchboard.xyz/solana/mainnet-beta/feed/${bank.oracle.toString()}`}
className={`flex items-center break-all text-th-fgd-2 hover:text-th-fgd-3`}
target="_blank"
rel="noreferrer"
>
{bank.oracle.toString()}
<ArrowTopRightOnSquareIcon className="ml-2 h-5 w-5 whitespace-nowrap" />
</a>
) : (
<ExplorerLink
address={formattedBankValues.oracle}
/>
)
}
/>
<KeyValuePair
label="Token Index"
value={formattedBankValues.tokenIndex}
/>
<KeyValuePair
label="Mint Decimals"
value={formattedBankValues.mintDecimals}
/>
<KeyValuePair
label="Oracle Price"
value={`$${bank.uiPrice}`}
/>
<KeyValuePair
label="Stable Price"
value={`$${formattedBankValues.stablePrice}`}
/>
<KeyValuePair
label="Last stable price updated"
value={
formattedBankValues.lastStablePriceUpdated
}
/>
<KeyValuePair
label="Stable Price: delay interval"
value={`${formattedBankValues.stablePriceModel.delayIntervalSeconds}s`}
/>
<KeyValuePair
label="Stable Price: growth limits"
value={`${formattedBankValues.stablePriceGrowthLimitsDelay}% delay / ${formattedBankValues.stablePriceGrowthLimitsStable}% stable`}
/>
<VaultData bank={bank} />
<KeyValuePair
label="Loan Fee Rate"
value={`${formattedBankValues.loanFeeRate} bps`}
/>
<KeyValuePair
label="Loan origination fee rate"
value={`${formattedBankValues.loanOriginationFeeRate} bps`}
/>
<KeyValuePair
label="Collected fees native"
value={`${formattedBankValues.collectedFeesNative} ($${formattedBankValues.collectedFeesNativePrice})`}
/>
<KeyValuePair
label="Dust"
value={formattedBankValues.dust}
/>
<KeyValuePair
label="Deposits"
value={`${formattedBankValues.deposits} ($${formattedBankValues.depositsPrice})`}
/>
<KeyValuePair
label="Borrows"
value={`${formattedBankValues.borrows} ($${formattedBankValues.borrowsPrice})`}
/>
<KeyValuePair
label="Reduce Only"
value={`${
bank.reduceOnly
} (Are deposits reduce only - ${bank.areDepositsReduceOnly()}, Are borrows reduce only - ${bank.areBorrowsReduceOnly()})`}
/>
<KeyValuePair
label="Avg Utilization"
value={`${formattedBankValues.avgUtilization}%`}
/>
<KeyValuePair
label="Maint Asset/Liab Weight"
value={`${formattedBankValues.maintAssetWeight} /
${formattedBankValues.maintLiabWeight}`}
/>
<KeyValuePair
label="Init Asset/Liab Weight"
value={`${formattedBankValues.initAssetWeight} /
${formattedBankValues.initLiabWeight}`}
/>
<KeyValuePair
label="Scaled Init Asset/Liab Weight"
value={`${formattedBankValues.scaledInitAssetWeight} / ${formattedBankValues.scaledInitLiabWeight}`}
/>
<KeyValuePair
label="Deposit weight scale start quote"
value={`$${formattedBankValues.depositWeightScaleStartQuote}`}
/>
<KeyValuePair
label="Borrow weight scale start quote"
value={`$${formattedBankValues.borrowWeightScaleStartQuote}`}
/>
<KeyValuePair
label="Rate params"
value={
<span className="text-right">
{`${formattedBankValues.rate0}% @ ${formattedBankValues.util0}% util, `}
{`${formattedBankValues.rate1}% @ ${formattedBankValues.util1}% util, `}
{`${formattedBankValues.maxRate}% @ 100% util`}
</span>
}
/>
<KeyValuePair
label="Adjustment factor"
value={`${formattedBankValues.adjustmentFactor}%`}
/>
<KeyValuePair
label="Deposit rate"
value={`${formattedBankValues.depositRate}%`}
/>
<KeyValuePair
label="Borrow rate"
value={`${formattedBankValues.borrowRate}%`}
/>
<KeyValuePair
label="Last index update"
value={formattedBankValues.lastIndexUpdate}
/>
<KeyValuePair
label="Last rates updated"
value={formattedBankValues.lastRatesUpdate}
/>
<KeyValuePair
label="Oracle: Conf Filter"
value={`${
formattedBankValues.oracleConfFilter
}% (Last known confidence ${bank._oracleLastKnownDeviation
?.div(bank.price)
.mul(I80F48.fromNumber(100))
.toNumber()
.toFixed(2)}%)`}
/>
<KeyValuePair
label="Oracle: Max Staleness"
value={`${bank.oracleConfig.maxStalenessSlots} slots (Last updated slot ${bank._oracleLastUpdatedSlot})`}
/>
<KeyValuePair
label="Group Insurance Fund"
value={`${mintInfo!.groupInsuranceFund}`}
/>
<KeyValuePair
label="Min vault to deposits ratio"
value={`${formattedBankValues.minVaultToDepositsRatio}%`}
/>
<KeyValuePair
label={`Net borrows in window (next window starts ${dayjs().to(
dayjs().add(
bank.getTimeToNextBorrowLimitWindowStartsTs(),
'second',
),
)})`}
value={`$${formattedBankValues.minVaultToDepositsRatio} / $${formattedBankValues.netBorrowLimitPerWindowQuote}`}
/>
<KeyValuePair
label="Liquidation fee"
value={`${formattedBankValues.liquidationFee}%`}
/>
{bank.mint.toBase58() !== USDC_MINT && (
<div className="mb-4 mt-2 flex">
<Button
className=" ml-auto"
onClick={() =>
setIsOpenSuggestionModal(true)
}
</div>
<Disclosure.Panel>
<KeyValuePair
label="Mint"
value={
<ExplorerLink
address={bank.mint.toString()}
/>
}
/>
<KeyValuePair
label="Bank"
value={
<ExplorerLink
address={formattedBankValues.publicKey.toString()}
anchorData
/>
}
/>
<KeyValuePair
label="MintInfo"
value={
<ExplorerLink
address={mintInfo!.publicKey.toString()}
anchorData
/>
}
/>
<KeyValuePair
label="Vault"
value={
<ExplorerLink
address={formattedBankValues.vault}
anchorData
/>
}
/>
<KeyValuePair
label="Oracle"
value={
bank.oracleProvider ==
OracleProvider.Switchboard ? (
<a
href={`https://app.switchboard.xyz/solana/mainnet-beta/feed/${bank.oracle.toString()}`}
className={`flex items-center break-all text-th-fgd-2 hover:text-th-fgd-3`}
target="_blank"
rel="noreferrer"
>
Check suggested values
{isOpenSuggestionModal && (
<DashboardSuggestedValues
priceImpacts={priceImpacts}
group={group}
bank={bank}
isOpen={isOpenSuggestionModal}
onClose={() =>
setIsOpenSuggestionModal(false)
}
></DashboardSuggestedValues>
)}
</Button>
</div>
)}
</Disclosure.Panel>
</>
)}
</Disclosure>
)
}),
)}
{bank.oracle.toString()}
<ArrowTopRightOnSquareIcon className="ml-2 h-5 w-5 whitespace-nowrap" />
</a>
) : (
<ExplorerLink
address={formattedBankValues.oracle}
/>
)
}
/>
<KeyValuePair
label="Token Index"
value={formattedBankValues.tokenIndex}
/>
<KeyValuePair
label="Mint Decimals"
value={formattedBankValues.mintDecimals}
/>
<KeyValuePair
label="Oracle Price"
value={`$${bank.uiPrice}`}
/>
<KeyValuePair
label="Stable Price"
value={`$${formattedBankValues.stablePrice}`}
/>
<KeyValuePair
label="Last stable price updated"
value={
formattedBankValues.lastStablePriceUpdated
}
/>
<KeyValuePair
label="Stable Price: delay interval"
value={`${formattedBankValues.stablePriceModel.delayIntervalSeconds}s`}
/>
<KeyValuePair
label="Stable Price: growth limits"
value={`${formattedBankValues.stablePriceGrowthLimitsDelay}% delay / ${formattedBankValues.stablePriceGrowthLimitsStable}% stable`}
/>
<VaultData bank={bank} />
<KeyValuePair
label="Loan Fee Rate"
value={`${formattedBankValues.loanFeeRate} bps`}
/>
<KeyValuePair
label="Loan origination fee rate"
value={`${formattedBankValues.loanOriginationFeeRate} bps`}
/>
<KeyValuePair
label="Collected fees native"
value={`${formattedBankValues.collectedFeesNative} ($${formattedBankValues.collectedFeesNativePrice})`}
/>
<KeyValuePair
label="Dust"
value={formattedBankValues.dust}
/>
<KeyValuePair
label="Deposits"
value={`${formattedBankValues.deposits} ($${formattedBankValues.depositsPrice})`}
/>
<KeyValuePair
label="Borrows"
value={`${formattedBankValues.borrows} ($${formattedBankValues.borrowsPrice})`}
/>
<KeyValuePair
label="Reduce Only"
value={`${
bank.reduceOnly
} (Are deposits reduce only - ${bank.areDepositsReduceOnly()}, Are borrows reduce only - ${bank.areBorrowsReduceOnly()})`}
/>
<KeyValuePair
label="Avg Utilization"
value={`${formattedBankValues.avgUtilization}%`}
/>
<KeyValuePair
label="Maint Asset/Liab Weight"
value={`${formattedBankValues.maintAssetWeight} /
${formattedBankValues.maintLiabWeight}`}
/>
<KeyValuePair
label="Init Asset/Liab Weight"
value={`${formattedBankValues.initAssetWeight} /
${formattedBankValues.initLiabWeight}`}
/>
<KeyValuePair
label="Scaled Init Asset/Liab Weight"
value={`${formattedBankValues.scaledInitAssetWeight} / ${formattedBankValues.scaledInitLiabWeight}`}
/>
<KeyValuePair
label="Deposit weight scale start quote"
value={`$${formattedBankValues.depositWeightScaleStartQuote}`}
/>
<KeyValuePair
label="Borrow weight scale start quote"
value={`$${formattedBankValues.borrowWeightScaleStartQuote}`}
/>
<KeyValuePair
label="Rate params"
value={
<span className="text-right">
{`${formattedBankValues.rate0}% @ ${formattedBankValues.util0}% util, `}
{`${formattedBankValues.rate1}% @ ${formattedBankValues.util1}% util, `}
{`${formattedBankValues.maxRate}% @ 100% util`}
</span>
}
/>
<KeyValuePair
label="Adjustment factor"
value={`${formattedBankValues.adjustmentFactor}%`}
/>
<KeyValuePair
label="Deposit rate"
value={`${formattedBankValues.depositRate}%`}
/>
<KeyValuePair
label="Borrow rate"
value={`${formattedBankValues.borrowRate}%`}
/>
<KeyValuePair
label="Last index update"
value={formattedBankValues.lastIndexUpdate}
/>
<KeyValuePair
label="Last rates updated"
value={formattedBankValues.lastRatesUpdate}
/>
<KeyValuePair
label="Oracle: Conf Filter"
value={`${
formattedBankValues.oracleConfFilter
}% (Last known confidence ${bank._oracleLastKnownDeviation
?.div(bank.price)
.mul(I80F48.fromNumber(100))
.toNumber()
.toFixed(2)}%)`}
/>
<KeyValuePair
label="Oracle: Max Staleness"
value={`${bank.oracleConfig.maxStalenessSlots} slots (Last updated slot ${bank._oracleLastUpdatedSlot})`}
/>
<KeyValuePair
label="Group Insurance Fund"
value={`${mintInfo!.groupInsuranceFund}`}
/>
<KeyValuePair
label="Min vault to deposits ratio"
value={`${formattedBankValues.minVaultToDepositsRatio}%`}
/>
<KeyValuePair
label={`Net borrows in window (next window starts ${dayjs().to(
dayjs().add(
bank.getTimeToNextBorrowLimitWindowStartsTs(),
'second',
),
)})`}
value={`$${formattedBankValues.minVaultToDepositsRatio} / $${formattedBankValues.netBorrowLimitPerWindowQuote}`}
/>
<KeyValuePair
label="Liquidation fee"
value={`${formattedBankValues.liquidationFee}%`}
/>
{bank.mint.toBase58() !== USDC_MINT && (
<div className="mb-4 mt-2 flex">
<Button
className=" ml-auto"
onClick={() =>
setIsOpenSuggestionModal(true)
}
>
Check suggested values
{isOpenSuggestionModal && (
<DashboardSuggestedValues
priceImpacts={priceImpacts}
group={group}
bank={bank}
isOpen={isOpenSuggestionModal}
onClose={() =>
setIsOpenSuggestionModal(false)
}
></DashboardSuggestedValues>
)}
</Button>
</div>
)}
</Disclosure.Panel>
</>
)}
</Disclosure>
)
})}
</div>
<h3 className="mb-3 mt-6 text-base text-th-fgd-3">

View File

@ -53,7 +53,7 @@
"tooltip-favorite-swap-add": "Add pair to favorites",
"tooltip-favorite-swap-remove": "Remove pair from favorites",
"tooltip-wallet-swap": "When enabled, you'll swap tokens in your wallet. When disabled, you'll swap tokens in your Mango Account.",
"trigger-beta": "Trigger orders are in beta. Use with caution.",
"trigger-beta": "Trigger orders are in beta",
"use-margin": "Allow Margin",
"wallet-swap": "Wallet Swap",
"warning-no-collateral": "You have no free collateral",

View File

@ -24,6 +24,10 @@
"current-price": "Current Price",
"depth": "Depth",
"edit-order": "Edit Order",
"error-no-long": "No long position to reduce",
"error-no-short": "No borrow position to reduce",
"error-trigger-above": "Trigger price must be above oracle price",
"error-trigger-below": "Trigger price must be below oracle price",
"est-liq-price": "Est. Liq. Price",
"est-slippage": "Est. Slippage",
"falls-to": "falls below",
@ -73,6 +77,7 @@
"orders": "Orders",
"place-order": "Place {{side}} Order",
"placing-order": "Placing Order",
"position": "Position",
"positions": "Positions",
"post": "Post",
"preview-sound": "Preview Sound",

View File

@ -53,7 +53,7 @@
"tooltip-favorite-swap-add": "Add pair to favorites",
"tooltip-favorite-swap-remove": "Remove pair from favorites",
"tooltip-wallet-swap": "When enabled, you'll swap tokens in your wallet. When disabled, you'll swap tokens in your Mango Account.",
"trigger-beta": "Trigger orders are in beta. Use with caution.",
"trigger-beta": "Trigger orders are in beta",
"use-margin": "Allow Margin",
"wallet-swap": "Wallet Swap",
"warning-no-collateral": "You have no free collateral",

View File

@ -24,6 +24,10 @@
"current-price": "Current Price",
"depth": "Depth",
"edit-order": "Edit Order",
"error-no-long": "No long position to reduce",
"error-no-short": "No borrow position to reduce",
"error-trigger-above": "Trigger price must be above oracle price",
"error-trigger-below": "Trigger price must be below oracle price",
"est-liq-price": "Est. Liq. Price",
"est-slippage": "Est. Slippage",
"falls-to": "falls below",
@ -73,6 +77,7 @@
"orders": "Orders",
"place-order": "Place {{side}} Order",
"placing-order": "Placing Order",
"position": "Position",
"positions": "Positions",
"post": "Post",
"preview-sound": "Preview Sound",

View File

@ -53,7 +53,7 @@
"tooltip-favorite-swap-add": "Add pair to favorites",
"tooltip-favorite-swap-remove": "Remove pair from favorites",
"tooltip-wallet-swap": "When enabled, you'll swap tokens in your wallet. When disabled, you'll swap tokens in your Mango Account.",
"trigger-beta": "Trigger orders are in beta. Use with caution.",
"trigger-beta": "Trigger orders are in beta",
"use-margin": "Allow Margin",
"wallet-swap": "Wallet Swap",
"warning-no-collateral": "You have no free collateral",

View File

@ -24,6 +24,10 @@
"current-price": "Current Price",
"depth": "Depth",
"edit-order": "Edit Order",
"error-no-long": "No long position to reduce",
"error-no-short": "No borrow position to reduce",
"error-trigger-above": "Trigger price must be above oracle price",
"error-trigger-below": "Trigger price must be below oracle price",
"est-liq-price": "Est. Liq. Price",
"est-slippage": "Est. Slippage",
"falls-to": "falls below",
@ -73,6 +77,7 @@
"orders": "Orders",
"place-order": "Place {{side}} Order",
"placing-order": "Placing Order",
"position": "Position",
"positions": "Positions",
"post": "Post",
"preview-sound": "Preview Sound",

View File

@ -1,5 +1,5 @@
{
"account-stats": "Account Stats",
"account-stats": "帐户统计",
"assets": "资产",
"assets-liabilities": "资产和债务",
"collateral-value": "质押品价值",
@ -16,7 +16,7 @@
"maint-health": "维持健康度",
"maint-health-contribution": "维持健康贡献",
"maint-health-contributions": "维持健康度",
"more-account-stats": "More Account Stats",
"more-account-stats": "更多帐户统计",
"no-data": "无数据可显示",
"no-pnl-history": "无盈亏历史",
"pnl-chart": "盈亏图表",

View File

@ -19,7 +19,7 @@
"all": "全部",
"amount": "数量",
"amount-owed": "欠款",
"asked-sign-transaction": "You'll be asked to sign a transaction",
"asked-sign-transaction": "你会被要求签署交易",
"asset-liability-weight": "资产/债务权重",
"asset-liability-weight-desc": "资产权重在账户健康计算中对质押品价值进行扣减。资产权重越低,资产对质押品的影响越小。债务权重恰恰相反(在健康计算中增加债务价值)。",
"asset-weight": "资产权重",
@ -42,7 +42,7 @@
"close-account": "关户",
"close-account-desc": "你确定吗? 关户就无法恢复",
"closing-account": "正在关闭帐户...",
"close-borrow": "Close {{token}} Borrow",
"close-borrow": "结清{{token}}借贷",
"collateral-value": "质押品价值",
"confirm": "确认",
"connect": "连接",
@ -59,7 +59,7 @@
"date": "日期",
"date-from": "从",
"date-to": "至",
"degraded": "Degraded",
"degraded": "退化的",
"delegate": "委托",
"delegate-account": "委托帐户",
"delegate-account-info": "帐户委托给: {{delegate}}",
@ -78,11 +78,11 @@
"edit": "编辑",
"edit-account": "编辑帐户标签",
"edit-profile-image": "切换头像",
"enable-notifications": "Enable Notifications",
"error-borrow-exceeds-limit": "Maximum borrow for the current period is {{remaining}}. New period starts {{resetTime}}",
"error-repay-insufficient-funds": "Not enough {{token}} in your wallet to repay this amount",
"enable-notifications": "开启通知",
"error-borrow-exceeds-limit": "目前期间的最大借贷为{{remaining}}.新期间从{{resetTime}}开始",
"error-repay-insufficient-funds": "钱包里的{{token}}不足归还",
"error-token-positions-full": "你帐户的币位已占满",
"explore": "Explore",
"explore": "探索",
"explorer": "浏览器",
"fee": "费用",
"feedback-survey": "反馈调查",
@ -123,9 +123,9 @@
"new-account-success": "您的新帐户准备好了😎",
"new-version": "新版本出来了",
"no": "不",
"offchain-services": "Offchain Services",
"offchain-services": "区块联外的服务",
"operational": "运行中",
"optional": "可选",
"operational": "Operational",
"outstanding-balance": "未结余额",
"overview": "摘要",
"perp": "合约",
@ -145,7 +145,7 @@
"repayment-amount": "还贷额",
"risks": "风险",
"rolling-change": "24小时变化",
"rpc-ping": "与RPC节点的ping时间",
"rpc-ping": "RPC Ping",
"route": "路线",
"save": "存",
"select": "选择",
@ -156,7 +156,7 @@
"select-withdraw-token": "选提出币种",
"sell": "卖",
"settings": "设置",
"severly-degraded": "Severly Degraded",
"severly-degraded": "退化严重",
"show-more": "显示更多",
"solana-tps": "Solana TPS",
"soon": "等一下",
@ -170,12 +170,12 @@
"token": "币种",
"token-collateral-multiplier": "{{token}}质押品乘数",
"tokens": "币种",
"tooltip-available": "The amount of {{token}} available to borrow",
"tooltip-available": "可借的{{token}}数量",
"tooltip-borrow-fee": "借贷费用。",
"tooltip-borrow-rate": "您将为借入余额支付的可变利率",
"tooltip-collateral-value": "您可以交易或借入的美元金额",
"tooltip-collateral-weight": "The multiplier applied to the notional value of {{token}} collateral",
"tooltip-interest-rates": "The interest rates for depositing (green/left) and borrowing (red/right)",
"tooltip-collateral-weight": "适用于 {{token}} 抵押品名义价值的乘数",
"tooltip-interest-rates": "存款利率(绿/左)和借贷利率(红/右)",
"total": "总计",
"total-borrow-value": "总借贷价值",
"total-borrows": "总借贷",
@ -205,7 +205,7 @@
"vote": "投票",
"yes": "是",
"you": "你",
"using-ledger": "Using Ledger",
"sign-to-in-app-notifications": "Sign to in app notifications"
}
"using-ledger": "正在使用Ledger",
"sign-to-in-app-notifications": "登入APP通知"
}

View File

@ -1,8 +1,8 @@
{
"collateral-weight": "Collateral Weight",
"gainers": "Gainers",
"losers": "Losers",
"no-gainers": "No Gainers...",
"no-losers": "No Losers...",
"recently-listed": "Recently Listed"
"collateral-weight": "质押品权重",
"gainers": "赢家",
"losers": "输家",
"no-gainers": "无赢家...",
"no-losers": "无输家...",
"recently-listed": "最近上架"
}

View File

@ -5,7 +5,7 @@
"base-bank": "基准Bank",
"base-mint": "基准铸币",
"base-token": "基准币种",
"before-listing-1": "你必须存入10,0000 MNGO以上",
"before-listing-1": "你必须存入100000 MNGO以上",
"before-listing-2": "新币种需要有",
"before-listing-3": "是否有足够相对USDC与SOL的流动性",
"before-listing-4": "新币种由DAO确认。过程需要3天的时间。",

View File

@ -4,7 +4,7 @@
"empty-state-title": "这里没什么",
"notifications": "通知",
"sign-message": "签署讯息",
"sign-using-ledger": "Sign with Ledger",
"sign-using-ledger": "以Ledger登入",
"unauth-desc": "连接钱包而受到通知",
"unauth-title": "通知收件匣"
}

View File

@ -23,13 +23,13 @@
"chart-right": "图表右",
"chinese": "简体中文",
"chinese-traditional": "繁体中文",
"close-instructions": "Close Instructions",
"close-perp": "Close Perp Positions",
"close-instructions": "关闭帐户指示",
"close-perp": "平仓",
"close-perp-desc": "To close unused positions, close the position/s, cancel any open orders and settle any unsettled funding.",
"close-spot-oo": "Close Spot Open Orders",
"close-spot-oo": "取消现货未结订单",
"close-spot-oo-desc": "To close unused markets, cancel your open orders and settle any unsettled balances.",
"close-token-positions-desc": "Closing tokens positions can affect your account health. The balance will be withdrawn to your wallet on closing.",
"close-unused": "Close Unused",
"close-unused": "关闭位用过的币位",
"close-unused-desc": "Closing unused slots will refund SOL to your account. Each slot type has different requirements for closure (hover over the buttons for details)",
"connect-notifications": "连接钱包来切换通知设定",
"custom": "自定",
@ -84,7 +84,7 @@
"percentage": "百分比",
"percentage-of-max": "最多之{{size}}%",
"perp-open-orders": "合约未结订单",
"perp-positions": "合约持仓",
"perp-markets": "合约持仓",
"preferences": "Preferences",
"preferred-explorer": "首选探索器",
"privacy": "Privacy",
@ -109,7 +109,7 @@
"solscan": "Solscan",
"sounds": "声响",
"spanish": "Español",
"spot-markets": "现货未结订单",
"spot-markets": "现货市场",
"swap-success": "换币/交易成功",
"swap-trade-size-selector": "换币/交易大小选择器",
"telemetry": "Telemetry",
@ -122,12 +122,12 @@
"tooltip-hot-key-percentage-size": "将大小设置为最大杠杆的百分比。",
"tooltip-hot-key-price": "将价格设置为预言机价格变化的百分比。",
"tooltip-orderbook-bandwidth-saving": "使用链下服务进行挂单簿更新,将数据使用量减少约 1000 倍。如果未结订单在挂单簿中不正确显示,请禁用此选项。",
"tooltip-perp-open-orders": "你可以有未结订单的合约市场数量。新帐户的最多为{{max}}",
"tooltip-perp-markets": "你可以有持仓的合约市场数量。新帐户的最多为{{max}}",
"tooltip-perp-open-orders": "包括所有试场的未结订单的综合数量。新帐户的最多为{{max}}",
"tooltip-perp-markets": "你有持仓的合约市场数量。新帐户的最多为{{max}}",
"tooltip-privacy-mode": "Masks the USD values of your account",
"tooltip-private-account": "Your account won't show on leaderboards and it won't be viewable via URL",
"tooltip-spot-markets": "你能有余额的现货市场数量。新帐户的最多为{{max}}",
"tooltip-send-telemetry": "Send anonymous usage statistics to help improve the app",
"tooltip-spot-markets": "你可以有未结订单的现货市场数量。新帐户的最多为{{max}}",
"tooltip-token-accounts": "你帐户可以持有的币种数量。新帐户的最多为{{max}}",
"top-left": "左上",
"top-right": "右上",

View File

@ -5,12 +5,12 @@
"hourly": "小时",
"largest-perp-positions": "最大持仓",
"perp-details": "{{market}}统计",
"perp-open-interest": "合约未平仓",
"perp-open-interest": "合约未平仓",
"perp-volume": "合约交易量",
"pnl-liquidation-fee": "正数盈亏清算费用",
"settle-pnl-factor": "结清盈亏因素",
"show-as-apr": "Show as APR",
"show-cumulative": "Show Cumulative",
"show-as-apr": "为APR显示",
"show-cumulative": "显示累积",
"six-hourly": "6-小时",
"tooltip-base-liquidation-fee": "当清算者接管持仓时,被清算者将支付此费用。",
"tooltip-funding-limits": "资金费用的最高与最低(一天)。",

View File

@ -12,7 +12,7 @@
"health-impact": "健康影响",
"hide-fees": "隐藏费用",
"hide-swap-history": "隐藏换币纪录",
"important-info": "Important Info",
"important-info": "重要资料",
"input-reduce-only-warning": "{{symbol}}处于仅减少模式。您可以将余额换成另一个币种",
"insufficient-balance": "{{symbol}}余额不够",
"insufficient-collateral": "质押品不够",
@ -34,9 +34,9 @@
"rate": "汇率",
"receive": "你将获取",
"received": "获取",
"reduce-position": "Reduce Position",
"reduce-position-buy": "Reduce long by buying",
"reduce-position-sell": "Reduce short by selling",
"reduce-position": "减少持仓",
"reduce-position-buy": "买而减少做多持仓",
"reduce-position-sell": "卖而减少做空持仓",
"review-swap": "检视换币",
"route-info": "路线细节",
"show-fees": "显示费用",
@ -55,7 +55,7 @@
"tooltip-favorite-swap-add": "Add pair to favorites",
"tooltip-favorite-swap-remove": "Remove pair from favorites",
"tooltip-wallet-swap": "When enabled, you'll swap tokens in your wallet. When disabled, you'll swap tokens in your Mango Account.",
"trigger-beta": "Trigger orders are in beta. Use with caution.",
"trigger-beta": "触发订单还在开发中。请小心一点。",
"use-margin": "允许杠杆",
"wallet-swap": "Wallet Swap",
"warning-no-collateral": "你没有可用质押品",

View File

@ -30,7 +30,7 @@
"oracle-confidence": "预言机可信度",
"oracle-staleness": "预言机不新鲜性",
"parameters": "参数",
"rate-curve": "Interest Rate Curve",
"rate-curve": "利率曲线",
"token-stats": "币种细节",
"token-fees-collected": "收取的币种费用",
"token-not-found": "查不到币种",

View File

@ -11,19 +11,23 @@
"cancel-all": "取消全部",
"cancel-order": "取消订单",
"cancel-order-error": "取消订单失败",
"close-all": "Close All",
"close-all-positions": "Close All Positions",
"close-all": "全部平仓",
"close-all-positions": "全部平仓",
"close-confirm": "市场平仓 {{config_name}}",
"close-position": "平仓",
"connect-orders": "连接以查看未结订单",
"connect-positions": "连接以查看持仓",
"connect-trade-history": "连接以查看交易历史",
"connect-trigger-orders": "Connect to view your trigger orders",
"connect-trigger-orders": "连接以查看触发订单",
"connect-unsettled": "连接以查看未结清余额",
"copy-and-share": "将图片复制到剪贴版",
"current-price": "目前价格",
"depth": "深度",
"edit-order": "编辑订单",
"error-no-long": "No long position to reduce",
"error-no-short": "No borrow position to reduce",
"error-trigger-above": "Trigger price must be above oracle price",
"error-trigger-below": "Trigger price must be below oracle price",
"est-liq-price": "预计清算价格",
"est-slippage": "预计下滑",
"falls-to": "下降至",
@ -54,15 +58,15 @@
"min-order-size-error": "订单的最小数量是 {{minSize}} {{symbol}}",
"more-details": "看细节",
"no-balances": "无余额",
"no-borrows": "Borrow Disabled",
"no-markets-found": "No markets found...",
"no-borrows": "借贷已停用",
"no-markets-found": "无市场",
"no-orders": "无订单",
"no-positions": "无持仓",
"no-trigger-orders": "No trigger orders",
"no-trigger-orders": "无触发订单",
"no-unsettled": "无未结清余额",
"notional": "名义",
"notional-volume": "名义交易量($)",
"open-interest": "未平仓",
"open-interest": "未平仓",
"oracle": "预言机",
"oracle-not-updated": "此预言机最近没更新。",
"oracle-not-updated-warning": "有该币种持仓的账户,操作将会失败。",
@ -73,10 +77,11 @@
"orders": "订单",
"place-order": "下{{side}}单",
"placing-order": "正在下单",
"position": "Position",
"positions": "持仓",
"post": "Post",
"preview-sound": "声音预览",
"price-expect": "您收到的价格可能与您预期有差异并且无法保证完全执行。为了您的安全最大滑点保持为2.5%。超过2.5%滑点的部分不会被平仓。",
"price-expect": "The execution price may be worse than you expect and full execution is not guaranteed. Your order will be filled up to a max slippage of 2.5%",
"price-provided-by": "预言机来自",
"quote": "计价",
"realized-pnl": "已实现的盈亏",
@ -93,7 +98,6 @@
"show-asks": "显示要价",
"show-bids": "显示出价",
"side": "方向",
"trigger-order-desc-with-borrow": "{{action}} {{amount}} {{symbol}} if the oracle price {{orderType}} {{triggerPrice}} {{priceUnit}}. You'll borrow ~{{borrowAmount}} {{quoteSymbol}} to execute this trade",
"size": "数量",
"spread": "差价",
"stable-price": "稳定价格",
@ -111,11 +115,11 @@
"tooltip-insured": "如果发生破产,{{tokenOrMarket}}损失是否可以从保险基金中归还",
"tooltip-ioc": "IOC交易若不吃单就会被取消。任何无法立刻成交的部分将被取消",
"tooltip-post": "Post交易若不挂单就会被取消。",
"tooltip-size-base-quote": "Show size in {{token}}",
"tooltip-private-counterparty": "Counterparty has Private Account enabled",
"tooltip-size-base-quote": "以{{token}}显示数量",
"tooltip-private-counterparty": "交易对手已启用私人帐户",
"tooltip-slippage": "当前价格与您的交易将执行的价格之间的差值的估计",
"tooltip-stable-price": "稳定价格用于一个安全机制。此机制可以限制用户在预言机价格快速波动时下风险高的订单",
"tooltip-view-counterparty": "View counterparty {{pk}}",
"tooltip-view-counterparty": "查看交易对手 {{pk}}",
"tooltip-volume-alert": "交易量警报设定",
"total-pnl": "总盈亏",
"trade-sounds-tooltip": "为每笔新交易播放警报声音",
@ -124,10 +128,11 @@
"trigger-order": "触发订单",
"trigger-order-desc": "{{amount}} {{symbol}} 若预言机价格{{orderType}} {{triggerPrice}} {{priceUnit}}",
"trigger-orders": "触发订单",
"trigger-order-desc-with-borrow": "{{action}} {{amount}} {{symbol}} if the oracle price {{orderType}} {{triggerPrice}} {{priceUnit}}. You' ll borrow ~{{borrowAmount}} {{quoteSymbol}} to execute this trade",
"tweet-position": "分享至Twitter",
"unrealized-pnl": "未实现盈亏",
"unsettled": "未结清",
"view-counterparty": "View counterparty {{pk}}",
"view-counterparty": "查看交易对手 {{pk}}",
"volume-alert": "交易量警报",
"volume-alert-desc": "交易量超过警报设定时播放声音"
}

View File

@ -1,5 +1,5 @@
{
"account-stats": "Account Stats",
"account-stats": "帳戶統計",
"assets": "資產",
"assets-liabilities": "資產和債務",
"collateral-value": "質押品價值",
@ -16,7 +16,7 @@
"maint-health": "維持健康度",
"maint-health-contribution": "維持健康貢獻",
"maint-health-contributions": "維持健康度",
"more-account-stats": "More Account Stats",
"more-account-stats": "更多帳戶統計",
"no-data": "無數據可顯示",
"no-pnl-history": "無盈虧歷史",
"pnl-chart": "盈虧圖表",

View File

@ -19,7 +19,7 @@
"all": "全部",
"amount": "數量",
"amount-owed": "欠款",
"asked-sign-transaction": "You'll be asked to sign a transaction",
"asked-sign-transaction": "你會被要求簽署交易",
"asset-liability-weight": "資產/債務權重",
"asset-liability-weight-desc": "資產權重在賬戶健康計算中對質押品價值進行扣減。資產權重越低,資產對質押品的影響越小。債務權重恰恰相反(在健康計算中增加債務價值)。",
"asset-weight": "資產權重",
@ -42,7 +42,7 @@
"close-account": "關戶",
"close-account-desc": "你確定嗎? 關戶就無法恢復",
"closing-account": "正在關閉帳戶...",
"close-borrow": "Close {{token}} Borrow",
"close-borrow": "結清{{token}}借貸",
"collateral-value": "質押品價值",
"confirm": "確認",
"connect": "連接",
@ -59,7 +59,7 @@
"date": "日期",
"date-from": "從",
"date-to": "至",
"degraded": "Degraded",
"degraded": "退化的",
"delegate": "委託",
"delegate-account": "委託帳戶",
"delegate-account-info": "帳戶委託給: {{delegate}}",
@ -78,11 +78,11 @@
"edit": "編輯",
"edit-account": "編輯帳戶標籤",
"edit-profile-image": "切換頭像",
"enable-notifications": "Enable Notifications",
"error-borrow-exceeds-limit": "Maximum borrow for the current period is {{remaining}}. New period starts {{resetTime}}",
"error-repay-insufficient-funds": "Not enough {{token}} in your wallet to repay this amount",
"enable-notifications": "開啟通知",
"error-borrow-exceeds-limit": "目前期間的最大借貸為{{remaining}}.新期間從{{resetTime}}開始",
"error-repay-insufficient-funds": "錢包裡的{{token}}不足歸還",
"error-token-positions-full": "你帳戶的幣位已占滿",
"explore": "Explore",
"explore": "探索",
"explorer": "瀏覽器",
"fee": "費用",
"feedback-survey": "反饋調查",
@ -123,8 +123,8 @@
"new-account-success": "您的新帳戶準備好了😎",
"new-version": "新版本出來了",
"no": "不",
"offchain-services": "Offchain Services",
"operational": "Operational",
"offchain-services": "區塊聯外的服務",
"operational": "運行中",
"optional": "可選",
"outstanding-balance": "未結餘額",
"overview": "摘要",
@ -145,7 +145,7 @@
"repayment-amount": "還貸額",
"risks": "風險",
"rolling-change": "24小時變化",
"rpc-ping": "與RPC節點的ping時間",
"rpc-ping": "RPC Ping",
"route": "路線",
"save": "存",
"select": "選擇",
@ -156,7 +156,7 @@
"select-withdraw-token": "選提出幣種",
"sell": "賣",
"settings": "設置",
"severly-degraded": "Severly Degraded",
"severly-degraded": "退化嚴重",
"show-more": "顯示更多",
"solana-tps": "Solana TPS",
"soon": "等一下",
@ -170,12 +170,12 @@
"token": "幣種",
"token-collateral-multiplier": "{{token}}質押品乘數",
"tokens": "幣種",
"tooltip-available": "The amount of {{token}} available to borrow",
"tooltip-available": "可借的{{token}}數量",
"tooltip-borrow-fee": "借貸費用。",
"tooltip-borrow-rate": "您將為借入餘額支付的可變利率",
"tooltip-collateral-value": "您可以交易或借入的美元金額",
"tooltip-collateral-weight": "The multiplier applied to the notional value of {{token}} collateral",
"tooltip-interest-rates": "The interest rates for depositing (green/left) and borrowing (red/right)",
"tooltip-collateral-weight": "適用於 {{token}} 抵押品名義價值的乘數",
"tooltip-interest-rates": "存款利率(綠/左)和借貸利率(紅/右)",
"total": "總計",
"total-borrow-value": "總借貸價值",
"total-borrows": "總借貸",
@ -205,6 +205,6 @@
"vote": "投票",
"yes": "是",
"you": "你",
"using-ledger": "Using Ledger",
"sign-to-in-app-notifications": "Sign to in app notifications"
"using-ledger": "正在使用Ledger",
"sign-to-in-app-notifications": "登入APP通知"
}

View File

@ -1,8 +1,8 @@
{
"collateral-weight": "Collateral Weight",
"gainers": "Gainers",
"losers": "Losers",
"no-gainers": "No Gainers...",
"no-losers": "No Losers...",
"recently-listed": "Recently Listed"
"collateral-weight": "質押品權重",
"gainers": "贏家",
"losers": "輸家",
"no-gainers": "無贏家...",
"no-losers": "無輸家...",
"recently-listed": "最近上架"
}

View File

@ -5,7 +5,7 @@
"base-bank": "基準Bank",
"base-mint": "基準鑄幣",
"base-token": "基準幣種",
"before-listing-1": "你必須存入10,0000 MNGO以上",
"before-listing-1": "你必須存入100000 MNGO以上",
"before-listing-2": "新幣種需要有",
"before-listing-3": "是否有足夠相對USDC與SOL的流動性",
"before-listing-4": "新幣種由DAO確認。過程需要3天的時間。",

View File

@ -4,7 +4,7 @@
"empty-state-title": "這裡沒什麼",
"notifications": "通知",
"sign-message": "簽署訊息",
"sign-using-ledger": "Sign with Ledger",
"sign-using-ledger": "以Ledger登入",
"unauth-desc": "連接錢包而受到通知",
"unauth-title": "通知收件匣"
}

View File

@ -23,13 +23,13 @@
"chart-right": "圖表右",
"chinese": "简体中文",
"chinese-traditional": "繁體中文",
"close-instructions": "Close Instructions",
"close-perp": "Close Perp Positions",
"close-instructions": "關閉帳戶指示",
"close-perp": "平倉",
"close-perp-desc": "To close unused positions, close the position/s, cancel any open orders and settle any unsettled funding.",
"close-spot-oo": "Close Spot Open Orders",
"close-spot-oo": "取消現貨未結訂單",
"close-spot-oo-desc": "To close unused markets, cancel your open orders and settle any unsettled balances.",
"close-token-positions-desc": "Closing tokens positions can affect your account health. The balance will be withdrawn to your wallet on closing.",
"close-unused": "Close Unused",
"close-unused": "關閉位用過的幣位",
"close-unused-desc": "Closing unused slots will refund SOL to your account. Each slot type has different requirements for closure (hover over the buttons for details)",
"connect-notifications": "連接錢包來切換通知設定",
"custom": "自定",
@ -84,7 +84,7 @@
"percentage": "百分比",
"percentage-of-max": "最多之{{size}}%",
"perp-open-orders": "合約未結訂單",
"perp-positions": "合約持倉",
"perp-markets": "合約市場",
"preferences": "Preferences",
"preferred-explorer": "首選探索器",
"privacy": "Privacy",
@ -109,7 +109,7 @@
"solscan": "Solscan",
"sounds": "聲響",
"spanish": "Español",
"spot-markets": "現貨未結訂單",
"spot-markets": "現貨市場",
"swap-success": "換幣/交易成功",
"swap-trade-size-selector": "換幣/交易大小選擇器",
"telemetry": "Telemetry",
@ -122,12 +122,12 @@
"tooltip-hot-key-percentage-size": "將大小設置為最大槓桿的百分比。",
"tooltip-hot-key-price": "將價格設置為預言機價格變化的百分比。",
"tooltip-orderbook-bandwidth-saving": "使用鏈下服務進行掛單簿更新,將數據使用量減少約 1000 倍。如果未結訂單在掛單簿中不正確顯示,請禁用此選項。",
"tooltip-perp-open-orders": "你可以有未結訂單的合約市場數量。新帳戶的最多為{{max}}",
"tooltip-perp-markets": "你可以有持倉的合約市場數量。新帳戶的最多為{{max}}",
"tooltip-perp-open-orders": "包括所有試場的未結訂單的綜合數量。新帳戶的最多為{{max}}",
"tooltip-perp-markets": "你有持倉的合約市場數量。新帳戶的最多為{{max}}",
"tooltip-privacy-mode": "Masks the USD values of your account",
"tooltip-private-account": "Your account won't show on leaderboards and it won't be viewable via URL",
"tooltip-spot-markets": "你能有餘額的現貨市場數量。新帳戶的最多為{{max}}",
"tooltip-send-telemetry": "Send anonymous usage statistics to help improve the app",
"tooltip-spot-markets": "你可以有未結訂單的現貨市場數量。新帳戶的最多為{{max}}",
"tooltip-token-accounts": "你帳戶可以持有的幣種數量。新帳戶的最多為{{max}}",
"top-left": "左上",
"top-right": "右上",

View File

@ -5,12 +5,12 @@
"hourly": "小時",
"largest-perp-positions": "最大持倉",
"perp-details": "{{market}}統計",
"perp-open-interest": "合約未平倉",
"perp-open-interest": "合約未平倉",
"perp-volume": "合約交易量",
"pnl-liquidation-fee": "正數盈虧清算費用",
"settle-pnl-factor": "結清盈虧因素",
"show-as-apr": "Show as APR",
"show-cumulative": "Show Cumulative",
"show-as-apr": "為APR顯示",
"show-cumulative": "顯示累積",
"six-hourly": "6-小時",
"tooltip-base-liquidation-fee": "當清算者接管持倉時,被清算者將支付此費用。",
"tooltip-funding-limits": "資金費用的最高與最低(一天)。",

View File

@ -12,7 +12,7 @@
"health-impact": "健康影響",
"hide-fees": "隱藏費用",
"hide-swap-history": "隱藏換幣紀錄",
"important-info": "Important Info",
"important-info": "重要資料",
"input-reduce-only-warning": "{{symbol}}處於僅減少模式。您可以將餘額換成另一個幣種",
"insufficient-balance": "{{symbol}}餘額不夠",
"insufficient-collateral": "質押品不夠",
@ -34,9 +34,9 @@
"rate": "匯率",
"receive": "你將獲取",
"received": "獲取",
"reduce-position": "Reduce Position",
"reduce-position-buy": "Reduce long by buying",
"reduce-position-sell": "Reduce short by selling",
"reduce-position": "減少持倉",
"reduce-position-buy": "買而減少做多持倉",
"reduce-position-sell": "賣而減少做空持倉",
"review-swap": "檢視換幣",
"route-info": "路線細節",
"show-fees": "顯示費用",
@ -55,7 +55,7 @@
"tooltip-favorite-swap-add": "Add pair to favorites",
"tooltip-favorite-swap-remove": "Remove pair from favorites",
"tooltip-wallet-swap": "When enabled, you'll swap tokens in your wallet. When disabled, you'll swap tokens in your Mango Account.",
"trigger-beta": "Trigger orders are in beta. Use with caution.",
"trigger-beta": "觸發訂單還在開發中。請小心一點。",
"use-margin": "允許槓桿",
"wallet-swap": "Wallet Swap",
"warning-no-collateral": "你沒有可用質押品",

View File

@ -30,7 +30,7 @@
"oracle-confidence": "預言機可信度",
"oracle-staleness": "預言機不新鮮性",
"parameters": "參數",
"rate-curve": "Interest Rate Curve",
"rate-curve": "利率曲線",
"token-stats": "幣種細節",
"token-fees-collected": "收取的幣種費用",
"token-not-found": "查不到幣種",

View File

@ -11,19 +11,23 @@
"cancel-all": "取消全部",
"cancel-order": "取消訂單",
"cancel-order-error": "取消訂單失敗",
"close-all": "Close All",
"close-all-positions": "Close All Positions",
"close-all": "全部平倉",
"close-all-positions": "全部平倉",
"close-confirm": "市場平倉 {{config_name}}",
"close-position": "平倉",
"connect-orders": "連接以查看未結訂單",
"connect-positions": "連接以查看持倉",
"connect-trade-history": "連接以查看交易歷史",
"connect-trigger-orders": "Connect to view your trigger orders",
"connect-trigger-orders": "連接以查看觸發訂單",
"connect-unsettled": "連接以查看未結清餘額",
"copy-and-share": "將圖片複製到剪貼版",
"current-price": "目前價格",
"depth": "深度",
"edit-order": "編輯訂單",
"error-no-long": "No long position to reduce",
"error-no-short": "No borrow position to reduce",
"error-trigger-above": "Trigger price must be above oracle price",
"error-trigger-below": "Trigger price must be below oracle price",
"est-liq-price": "預計清算價格",
"est-slippage": "預計下滑",
"falls-to": "下降至",
@ -54,15 +58,15 @@
"min-order-size-error": "訂單的最小數量是 {{minSize}} {{symbol}}",
"more-details": "看細節",
"no-balances": "無餘額",
"no-borrows": "Borrow Disabled",
"no-markets-found": "No markets found...",
"no-borrows": "借貸已停用",
"no-markets-found": "無市場",
"no-orders": "無訂單",
"no-positions": "無持倉",
"no-trigger-orders": "No trigger orders",
"no-trigger-orders": "無觸發訂單",
"no-unsettled": "無未結清餘額",
"notional": "名義",
"notional-volume": "名義交易量($)",
"open-interest": "未平倉",
"open-interest": "未平倉",
"oracle": "預言機",
"oracle-not-updated": "此預言機最近沒更新。",
"oracle-not-updated-warning": "有該幣種持倉的賬戶,操作將會失敗。",
@ -73,10 +77,11 @@
"orders": "訂單",
"place-order": "下{{side}}單",
"placing-order": "正在下單",
"position": "Position",
"positions": "持倉",
"post": "Post",
"preview-sound": "聲音預覽",
"price-expect": "您收到的價格可能與您預期有差異,並且無法保證完全執行。為了您的安全,最大滑點保持為 2.5%。超過 2.5%滑點的部分不會被平倉。",
"price-expect": "The execution price may be worse than you expect and full execution is not guaranteed. Your order will be filled up to a max slippage of 2.5%",
"price-provided-by": "預言機來自",
"quote": "計價",
"realized-pnl": "已實現的盈虧",
@ -110,11 +115,11 @@
"tooltip-insured": "如果發生破產,{{tokenOrMarket}}損失是否可以從保險基金中歸還",
"tooltip-ioc": "IOC交易若不吃單就會被取消。任何無法立刻成交的部分將被取消",
"tooltip-post": "Post交易若不掛單就會被取消。",
"tooltip-size-base-quote": "Show size in {{token}}",
"tooltip-private-counterparty": "Counterparty has Private Account enabled",
"tooltip-size-base-quote": "以{{token}}顯示數量",
"tooltip-private-counterparty": "交易對手已啟用私人帳戶",
"tooltip-slippage": "當前價格與您的交易將執行的價格之間的差值的估計",
"tooltip-stable-price": "穩定價格用於一個安全機制。此機制可以限制用戶在預言機價格快速波動時下風險高的訂單",
"tooltip-view-counterparty": "View counterparty {{pk}}",
"tooltip-view-counterparty": "查看交易對手 {{pk}}",
"tooltip-volume-alert": "交易量警報設定",
"total-pnl": "總盈虧",
"trade-sounds-tooltip": "為每筆新交易播放警報聲音",
@ -127,7 +132,7 @@
"tweet-position": "分享至Twitter",
"unrealized-pnl": "未實現盈虧",
"unsettled": "未結清",
"view-counterparty": "View counterparty {{pk}}",
"view-counterparty": "查看交易對手 {{pk}}",
"volume-alert": "交易量警報",
"volume-alert-desc": "交易量超過警報設定時播放聲音"
}

View File

@ -59,7 +59,6 @@ import {
SpotBalances,
SpotTradeHistory,
SwapHistoryItem,
TotalInterestDataItem,
TradeForm,
TokenStatsItem,
NFT,
@ -69,7 +68,7 @@ import {
ThemeData,
PositionStat,
OrderbookTooltip,
TriggerOrderTypes,
SwapTypes,
} from 'types'
import spotBalancesUpdater from './spotBalancesUpdater'
import { PerpMarket } from '@blockworks-foundation/mango-v4/'
@ -88,6 +87,7 @@ import mapValues from 'lodash/mapValues'
import groupBy from 'lodash/groupBy'
import sampleSize from 'lodash/sampleSize'
import { fetchTokenStatsData, processTokenStatsData } from 'apis/mngo'
import { OrderTypes, TriggerOrderTypes } from 'utils/tradeForm'
const GROUP = new PublicKey('78b8f4cGCwmZ9ysPFMWLaLTkkaYnUjwMJYStWe5RTSSX')
@ -146,7 +146,7 @@ export const DEFAULT_TRADE_FORM: TradeForm = {
price: undefined,
baseSize: '',
quoteSize: '',
tradeType: 'Limit',
tradeType: OrderTypes.LIMIT,
triggerPrice: '',
postOnly: false,
ioc: false,
@ -241,7 +241,7 @@ export type MangoStore = {
amountIn: string
amountOut: string
flipPrices: boolean
swapOrTrigger: TriggerOrderTypes
swapOrTrigger: SwapTypes
triggerPrice: string
}
set: (x: (x: MangoStore) => void) => void

View File

@ -11,6 +11,7 @@ import { JsonMetadata } from '@metaplex-foundation/js'
import { Event } from '@project-serum/serum/lib/queue'
import { PublicKey } from '@solana/web3.js'
import { formatTradeHistory } from 'hooks/useTradeHistory'
import { OrderTypes, TriggerOrderTypes } from 'utils/tradeForm'
export type EmptyObject = { [K in keyof never]?: never }
export interface OrderbookL2 {
@ -441,7 +442,7 @@ export interface TradeForm {
price: string | undefined
baseSize: string
quoteSize: string
tradeType: 'Market' | 'Limit'
tradeType: OrderTypes | TriggerOrderTypes
triggerPrice?: string
postOnly: boolean
ioc: boolean
@ -553,4 +554,4 @@ export interface FilledOrder {
quantity: number
}
export type TriggerOrderTypes = 'swap' | 'trade:trigger-order'
export type SwapTypes = 'swap' | 'trade:trigger-order'

View File

@ -1,4 +1,8 @@
import { OrderbookL2 } from 'types'
import mangoStore from '@store/mangoStore'
import { OrderbookL2, isMangoError } from 'types'
import { notify } from './notifications'
import * as sentry from '@sentry/nextjs'
import { Bank } from '@blockworks-foundation/mango-v4'
export const calculateLimitPriceForMarketOrder = (
orderBook: OrderbookL2,
@ -83,3 +87,124 @@ export const calculateSlippage = (
}
return 0
}
export const handlePlaceTriggerOrder = async (
positionBank: Bank | undefined,
quoteBank: Bank | undefined,
amountIn: number,
triggerPrice: string,
orderType: TriggerOrderTypes,
isReducingShort: boolean,
flipPrices: boolean,
setSubmitting: (s: boolean) => void,
) => {
try {
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const actions = mangoStore.getState().actions
const mangoAccount = mangoStore.getState().mangoAccount.current
// const inputBank = mangoStore.getState().swap.inputBank
// const outputBank = mangoStore.getState().swap.outputBank
if (!mangoAccount || !group || !positionBank || !quoteBank || !triggerPrice)
return
setSubmitting(true)
// const amountIn = amountInAsDecimal.toNumber()
const isReduceLong = !isReducingShort
try {
let tx
if (orderType === TriggerOrderTypes.STOP_LOSS) {
if (isReduceLong) {
tx = await client.tcsStopLossOnDeposit(
group,
mangoAccount,
positionBank,
quoteBank,
parseFloat(triggerPrice),
flipPrices,
amountIn,
null,
null,
)
} else {
tx = await client.tcsStopLossOnBorrow(
group,
mangoAccount,
quoteBank,
positionBank,
parseFloat(triggerPrice),
!flipPrices,
amountIn,
null,
true,
null,
)
}
}
if (orderType === TriggerOrderTypes.TAKE_PROFIT) {
if (isReduceLong) {
tx = await client.tcsTakeProfitOnDeposit(
group,
mangoAccount,
positionBank,
quoteBank,
parseFloat(triggerPrice),
flipPrices,
amountIn,
null,
null,
)
} else {
tx = await client.tcsTakeProfitOnBorrow(
group,
mangoAccount,
quoteBank,
positionBank,
parseFloat(triggerPrice),
!flipPrices,
amountIn,
null,
true,
null,
)
}
}
notify({
title: 'Transaction confirmed',
type: 'success',
txid: tx?.signature,
noSound: true,
})
actions.fetchGroup()
await actions.reloadMangoAccount(tx?.slot)
} catch (e) {
console.error('onSwap error: ', e)
sentry.captureException(e)
if (isMangoError(e)) {
notify({
title: 'Transaction failed',
description: e.message,
txid: e?.txid,
type: 'error',
})
}
}
} catch (e) {
console.error('Swap error:', e)
} finally {
setSubmitting(false)
}
}
export enum OrderTypes {
LIMIT = 'Limit',
MARKET = 'Market',
}
export enum TriggerOrderTypes {
STOP_LOSS = 'trade:stop-loss',
TAKE_PROFIT = 'trade:take-profit',
}

View File

@ -50,10 +50,10 @@
bn.js "^5.2.1"
eslint-config-prettier "^9.0.0"
"@blockworks-foundation/mango-v4@^0.19.41":
version "0.19.41"
resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.19.41.tgz#d4f88aebe5509cacf5569e6b3b7665bc9053c33f"
integrity sha512-q3rXSgql8lcYtCbtvIb5VPHjkcQUEbnmMQ4p6D81X+wVE3sBKNOpXiq2uQNEVJRBsWKzz/Ea1W9jM6siq7bpWA==
"@blockworks-foundation/mango-v4@0.19.43":
version "0.19.43"
resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.19.43.tgz#82c2df9c63204e31c19473672329cba43ea40fbc"
integrity sha512-son/flq/mvngtBppKbxXfNIq/Z4L7mp6zOP5k8cD11rVlZ3wmb3jfBkKqocgL3IqnRIWj3cL9R6dM37tqaFZzw==
dependencies:
"@blockworks-foundation/mango-v4-settings" "^0.2.16"
"@coral-xyz/anchor" "^0.28.1-beta.2"