test placing orders and swap orders table

This commit is contained in:
saml33 2023-07-31 16:05:26 +10:00
parent 840991961f
commit 7e600cfd20
4 changed files with 453 additions and 49 deletions

View File

@ -13,7 +13,6 @@ import NumberFormat, {
} from 'react-number-format'
import Decimal from 'decimal.js'
import mangoStore from '@store/mangoStore'
import useDebounce from '../shared/useDebounce'
import { useTranslation } from 'next-i18next'
import { SIZE_INPUT_UI_KEY } from '../../utils/constants'
import useLocalStorageState from 'hooks/useLocalStorageState'
@ -24,6 +23,12 @@ import { floorToDecimal } from 'utils/numbers'
import { withValueLimit } from './MarketSwapForm'
import SellTokenInput from './SellTokenInput'
import BuyTokenInput from './BuyTokenInput'
import { notify } from 'utils/notifications'
import * as sentry from '@sentry/nextjs'
import { isMangoError } from 'types'
import Button from '@components/shared/Button'
import { useWallet } from '@solana/wallet-adapter-react'
import Loading from '@components/shared/Loading'
type LimitSwapFormProps = {
setShowTokenSelect: Dispatch<SetStateAction<'input' | 'output' | undefined>>
@ -35,9 +40,11 @@ const set = mangoStore.getState().set
const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => {
const { t } = useTranslation(['common', 'swap', 'trade'])
const { connected } = useWallet()
const [animateSwitchArrow, setAnimateSwitchArrow] = useState(0)
const [orderType, setOrderType] = useState(ORDER_TYPES[0])
const [triggerPrice, setTriggerPrice] = useState('')
const [submitting, setSubmitting] = useState(false)
const [swapFormSizeUi] = useLocalStorageState(SIZE_INPUT_UI_KEY, 'slider')
const {
@ -48,20 +55,28 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => {
amountOut: amountOutFormValue,
limitPrice,
} = mangoStore((s) => s.swap)
const [debouncedAmountIn] = useDebounce(amountInFormValue, 300)
const [debouncedAmountOut] = useDebounce(amountOutFormValue, 300)
const amountInAsDecimal: Decimal | null = useMemo(() => {
return Number(debouncedAmountIn)
? new Decimal(debouncedAmountIn)
return Number(amountInFormValue)
? new Decimal(amountInFormValue)
: new Decimal(0)
}, [debouncedAmountIn])
}, [amountInFormValue])
const amountOutAsDecimal: Decimal | null = useMemo(() => {
return Number(debouncedAmountOut)
? new Decimal(debouncedAmountOut)
return Number(amountOutFormValue)
? new Decimal(amountOutFormValue)
: new Decimal(0)
}, [debouncedAmountOut])
}, [amountOutFormValue])
const [baseBank, quoteBank] = useMemo(() => {
if (inputBank && inputBank.name === 'USDC') {
return [outputBank, inputBank]
} else if (outputBank && outputBank.name === 'USDC') {
return [inputBank, outputBank]
} else if (inputBank && inputBank.name === 'SOL') {
return [outputBank, inputBank]
} else return [inputBank, outputBank]
}, [inputBank, outputBank])
const setAmountInFormValue = useCallback((amountIn: string) => {
set((s) => {
@ -87,6 +102,16 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => {
})
}, [])
useEffect(() => {
if (!baseBank || !quoteBank) return
const initialLimitPrice = baseBank.uiPrice / quoteBank.uiPrice
if (!limitPrice) {
set((s) => {
s.swap.limitPrice = initialLimitPrice.toString()
})
}
}, [baseBank, limitPrice, quoteBank])
/*
If the use margin setting is toggled, clear the form values
*/
@ -100,14 +125,26 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => {
if (info.source !== 'event') return
setAmountInFormValue(e.value)
if (parseFloat(e.value) > 0 && limitPrice && outputBank) {
const amount = floorToDecimal(
parseFloat(e.value) / parseFloat(limitPrice),
outputBank.mintDecimals,
)
const amount =
outputBank.name === quoteBank?.name
? floorToDecimal(
parseFloat(e.value) * parseFloat(limitPrice),
outputBank.mintDecimals,
)
: floorToDecimal(
parseFloat(e.value) / parseFloat(limitPrice),
outputBank.mintDecimals,
)
setAmountOutFormValue(amount.toString())
}
},
[limitPrice, outputBank, setAmountInFormValue, setAmountOutFormValue],
[
limitPrice,
outputBank,
quoteBank,
setAmountInFormValue,
setAmountOutFormValue,
],
)
const handleAmountOutChange = useCallback(
@ -115,14 +152,26 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => {
if (info.source !== 'event') return
setAmountOutFormValue(e.value)
if (parseFloat(e.value) > 0 && limitPrice && inputBank) {
const amount = floorToDecimal(
parseFloat(e.value) * parseFloat(limitPrice),
inputBank.mintDecimals,
)
const amount =
outputBank?.name === quoteBank?.name
? floorToDecimal(
parseFloat(e.value) / parseFloat(limitPrice),
inputBank.mintDecimals,
)
: floorToDecimal(
parseFloat(e.value) * parseFloat(limitPrice),
inputBank.mintDecimals,
)
setAmountInFormValue(amount.toString())
}
},
[inputBank, limitPrice, setAmountInFormValue, setAmountOutFormValue],
[
inputBank,
outputBank,
limitPrice,
setAmountInFormValue,
setAmountOutFormValue,
],
)
const handleAmountInUi = useCallback(
@ -149,7 +198,7 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => {
outputBank
) {
const amount = floorToDecimal(
parseFloat(amountInFormValue) / parseFloat(e.value),
parseFloat(amountInFormValue) * parseFloat(e.value),
outputBank.mintDecimals,
)
setAmountOutFormValue(amount.toString())
@ -166,20 +215,101 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => {
[setTriggerPrice],
)
const handleSwitchTokens = useCallback(() => {
if (amountInAsDecimal?.gt(0) && amountOutAsDecimal.gte(0)) {
setAmountInFormValue(amountOutAsDecimal.toString())
const handleLimitSwap = useCallback(async () => {
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 && orderType !== 'trade:limit') ||
(!limitPrice && orderType !== 'trade:stop-market')
)
return
setSubmitting(true)
const orderPrice =
orderType === 'trade:limit'
? parseFloat(limitPrice!)
: parseFloat(triggerPrice)
const stopLimitPrice =
orderType !== 'trade:stop-market' ? parseFloat(limitPrice!) : 0
try {
const tx = await client.tokenConditionalSwapStopLoss(
group,
mangoAccount,
inputBank.mint,
orderPrice,
outputBank.mint,
stopLimitPrice,
amountInAsDecimal.toNumber(),
null,
null,
)
notify({
title: 'Transaction confirmed',
type: 'success',
txid: tx,
noSound: true,
})
actions.fetchGroup()
await actions.reloadMangoAccount()
} 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)
}
}, [orderType, limitPrice, triggerPrice, amountInAsDecimal])
const handleSwitchTokens = useCallback(() => {
if (amountInAsDecimal?.gt(0) && limitPrice) {
const amountOut =
outputBank?.name !== quoteBank?.name
? amountInAsDecimal.mul(limitPrice)
: amountInAsDecimal.div(limitPrice)
setAmountOutFormValue(amountOut.toString())
}
const inputBank = mangoStore.getState().swap.inputBank
const outputBank = mangoStore.getState().swap.outputBank
set((s) => {
s.swap.inputBank = outputBank
s.swap.outputBank = inputBank
// s.swap.limitPrice = ''
})
setAnimateSwitchArrow(
(prevanimateSwitchArrow) => prevanimateSwitchArrow + 1,
)
}, [setAmountInFormValue, amountOutAsDecimal, amountInAsDecimal])
}, [
setAmountInFormValue,
amountOutAsDecimal,
amountInAsDecimal,
limitPrice,
inputBank,
outputBank,
quoteBank,
])
const limitOrderDisabled =
!connected || !amountInFormValue || !amountOutFormValue
return (
<>
@ -231,7 +361,13 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => {
) : null}
{orderType !== 'trade:stop-market' ? (
<div className="col-span-1">
<p className="mb-2 text-th-fgd-2">{t('trade:limit-price')}</p>
<p className="mb-2 text-th-fgd-2">
{t('trade:limit-price')}
<span className="text-xs text-th-fgd-3">
{' '}
({quoteBank?.name})
</span>
</p>
<NumberFormat
inputMode="decimal"
thousandSeparator=","
@ -283,6 +419,14 @@ const LimitSwapForm = ({ setShowTokenSelect }: LimitSwapFormProps) => {
useMargin={useMargin}
/>
)}
<Button
onClick={handleLimitSwap}
className="mt-6 mb-4 flex w-full items-center justify-center text-base"
disabled={limitOrderDisabled}
size="large"
>
{submitting ? <Loading /> : t('swap:place-limit-order')}
</Button>
</>
)
}

View File

@ -94,6 +94,7 @@ const SwapForm = () => {
const bank = group.getFirstBankByMint(new PublicKey(mintAddress))
set((s) => {
s.swap.inputBank = bank
s.swap.limitPrice = ''
})
}
setShowTokenSelect(undefined)
@ -105,6 +106,7 @@ const SwapForm = () => {
const bank = group.getFirstBankByMint(new PublicKey(mintAddress))
set((s) => {
s.swap.outputBank = bank
s.swap.limitPrice = ''
})
}
setShowTokenSelect(undefined)
@ -167,19 +169,10 @@ const SwapForm = () => {
const handleSwapOrLimit = useCallback(
(orderType: string) => {
setSwapOrLimit(orderType)
if (orderType === 'trade:limit' && outputBank) {
set((s) => {
s.swap.limitPrice = outputBank.uiPrice.toString()
})
}
},
[outputBank, set, setSwapOrLimit],
)
const handlePlaceOrder = () => {
console.log('place swap limit order')
}
const handleSetMargin = () => {
set((s) => {
s.swap.margin = !s.swap.margin
@ -190,9 +183,6 @@ const SwapForm = () => {
setSavedSwapMargin(useMargin)
}, [useMargin])
const limitOrderDisabled =
!connected || !amountInFormValue || !amountOutFormValue
return (
<ContentBox
hidePadding
@ -279,16 +269,7 @@ const SwapForm = () => {
selectedRoute ? amountOutAsDecimal.toNumber() : undefined
}
/>
) : (
<Button
onClick={handlePlaceOrder}
className="mt-6 mb-4 flex w-full items-center justify-center text-base"
disabled={limitOrderDisabled}
size="large"
>
{t('swap:place-limit-order')}
</Button>
)
) : null
) : (
<Button
disabled

View File

@ -7,6 +7,7 @@ import useMangoAccount from 'hooks/useMangoAccount'
import ManualRefresh from '@components/shared/ManualRefresh'
import { useViewport } from 'hooks/useViewport'
import { breakpoints } from 'utils/theme'
import SwapOrders from './SwapOrders'
const SwapInfoTabs = () => {
const [selectedTab, setSelectedTab] = useState('balances')
@ -18,6 +19,7 @@ const SwapInfoTabs = () => {
const tabsWithCount: [string, number][] = useMemo(() => {
return [
['balances', 0],
['orders', 0],
['swap:swap-history', 0],
]
}, [openOrders, mangoAccount])
@ -38,6 +40,7 @@ const SwapInfoTabs = () => {
/>
</div>
{selectedTab === 'balances' ? <SwapTradeBalances /> : null}
{selectedTab === 'orders' ? <SwapOrders /> : null}
{selectedTab === 'swap:swap-history' ? <SwapHistoryTable /> : null}
</div>
)

View File

@ -0,0 +1,276 @@
import { IconButton } from '@components/shared/Button'
import ConnectEmptyState from '@components/shared/ConnectEmptyState'
import {
SortableColumnHeader,
Table,
Td,
Th,
TrBody,
TrHead,
} from '@components/shared/TableElements'
import { NoSymbolIcon, TrashIcon } from '@heroicons/react/20/solid'
import { BN } from '@project-serum/anchor'
import { useWallet } from '@solana/wallet-adapter-react'
import mangoStore from '@store/mangoStore'
import useMangoAccount from 'hooks/useMangoAccount'
import useMangoGroup from 'hooks/useMangoGroup'
import { useSortableData } from 'hooks/useSortableData'
// import { useViewport } from 'hooks/useViewport'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { notify } from 'utils/notifications'
import { floorToDecimal } from 'utils/numbers'
// import { breakpoints } from 'utils/theme'
import * as sentry from '@sentry/nextjs'
import { isMangoError } from 'types'
import Loading from '@components/shared/Loading'
const SwapOrders = () => {
const { t } = useTranslation(['common', 'swap', 'trade'])
// const { width } = useViewport()
// const showTableView = width ? width > breakpoints.md : false
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
const { group } = useMangoGroup()
const { connected } = useWallet()
const [cancelId, setCancelId] = useState('')
const orders = useMemo(() => {
if (!mangoAccount) return []
return mangoAccount.tokenConditionalSwaps.filter((tcs) => tcs.hasData)
}, [mangoAccount])
const formattedTableData = useCallback(() => {
if (!group) return []
const formatted = []
for (const order of orders) {
const buyBank = group.getFirstBankByTokenIndex(order.buyTokenIndex)
const sellBank = group.getFirstBankByTokenIndex(order.sellTokenIndex)
const market = `${sellBank.name}/${buyBank.name}`
const size = floorToDecimal(
order.getMaxSellUi(group),
sellBank.mintDecimals,
).toNumber()
const triggerPrice = order.getPriceLowerLimitUi(group)
const limitPrice = order.getPriceUpperLimitUi(group)
const pricePremium = order.getPricePremium()
const orderType =
limitPrice === 0
? 'trade:stop-market'
: triggerPrice === limitPrice
? 'trade:limit'
: 'trade:stop-limit'
const data = {
...order,
buyBank,
sellBank,
market,
size,
triggerPrice,
limitPrice,
orderType,
fee: pricePremium,
}
formatted.push(data)
}
return formatted
}, [group, orders])
const {
items: tableData,
requestSort,
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 tx = await client.tokenConditionalSwapCancel(
group,
mangoAccount,
id,
)
notify({
title: 'Transaction confirmed',
type: 'success',
txid: tx,
noSound: true,
})
actions.fetchGroup()
await actions.reloadMangoAccount()
} 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 swap order', e)
} finally {
setCancelId('')
}
}
return orders.length ? (
<Table>
<thead>
<TrHead>
<Th className="text-left">
<SortableColumnHeader
sortKey="market"
sort={() => requestSort('market')}
sortConfig={sortConfig}
title={t('market')}
/>
</Th>
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="orderType"
sort={() => requestSort('orderType')}
sortConfig={sortConfig}
title={t('order-type')}
/>
</div>
</Th>
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="size"
sort={() => requestSort('size')}
sortConfig={sortConfig}
title={t('size')}
/>
</div>
</Th>
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="triggerPrice"
sort={() => requestSort('triggerPrice')}
sortConfig={sortConfig}
title={t('trigger-price')}
/>
</div>
</Th>
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="limitPrice"
sort={() => requestSort('limitPrice')}
sortConfig={sortConfig}
title={t('limit-price')}
/>
</div>
</Th>
<Th>
<div className="flex justify-end">
<SortableColumnHeader
sortKey="fee"
sort={() => requestSort('fee')}
sortConfig={sortConfig}
title={t('fee')}
/>
</div>
</Th>
<Th className="text-right">{t('cancel')}</Th>
</TrHead>
</thead>
<tbody>
{tableData.map((data, i) => {
const {
buyBank,
fee,
market,
orderType,
limitPrice,
sellBank,
size,
triggerPrice,
} = data
return (
<TrBody key={i} className="text-sm">
<Td>{market}</Td>
<Td>
<p className="text-right font-body">{t(orderType)}</p>
</Td>
<Td>
<p className="text-right">
{size}
<span className="text-th-fgd-3 font-body">
{' '}
{sellBank.name}
</span>
</p>
</Td>
<Td>
{triggerPrice !== limitPrice ? (
<p className="text-right">
{triggerPrice}
<span className="text-th-fgd-3 font-body">
{' '}
{buyBank.name}
</span>
</p>
) : (
<p className="text-right"></p>
)}
</Td>
<Td>
{limitPrice ? (
<p className="text-right">
{limitPrice}
<span className="text-th-fgd-3 font-body">
{' '}
{buyBank.name}
</span>
</p>
) : (
<p className="text-right"></p>
)}
</Td>
<Td>
<p className="text-right">{fee.toFixed(2)}%</p>
</Td>
<Td className="flex justify-end">
<IconButton onClick={() => handleCancel(data.id)} size="small">
{cancelId === data.id.toString() ? (
<Loading />
) : (
<TrashIcon className="h-4 w-4" />
)}
</IconButton>
</Td>
</TrBody>
)
})}
</tbody>
</Table>
) : mangoAccountAddress || connected ? (
<div className="flex flex-col items-center p-8">
<NoSymbolIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
<p>{t('trade:no-orders')}</p>
</div>
) : (
<div className="p-8">
<ConnectEmptyState text={t('connect-orders')} />
</div>
)
}
export default SwapOrders