multi-order cancel (#391)

* wip

* cancel all orders for market

* disable button when loading

* fix

* fix

---------

Co-authored-by: Adrian Brzeziński <a.brzezinski94@gmail.com>
This commit is contained in:
saml33 2024-02-20 23:38:44 +11:00 committed by GitHub
parent 99d609ce0b
commit 2910d04086
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 234 additions and 81 deletions

View File

@ -14,7 +14,7 @@ const MobileTradeAdvancedPage = () => {
const [activeTab, setActiveTab] = useState('trade:book')
const [showChart, setShowChart] = useState(false)
return (
<div className="grid grid-cols-2 sm:grid-cols-3">
<div className="grid grid-cols-2 pb-20 sm:grid-cols-3">
<div className="col-span-2 sm:col-span-3">
<FavoriteMarketsBar />
</div>

View File

@ -42,7 +42,7 @@ import useFilledOrders from 'hooks/useFilledOrders'
import { useViewport } from 'hooks/useViewport'
import { useTranslation } from 'next-i18next'
import Link from 'next/link'
import { ChangeEvent, useCallback, useState } from 'react'
import { ChangeEvent, Fragment, useCallback, useState } from 'react'
import { isMangoError } from 'types'
import { notify } from 'utils/notifications'
import { getDecimalCount } from 'utils/numbers'
@ -52,6 +52,7 @@ import PerpSideBadge from './PerpSideBadge'
import TableMarketName from './TableMarketName'
import { useSortableData } from 'hooks/useSortableData'
import { BN } from '@project-serum/anchor'
import NukeIcon from '@components/icons/NukeIcon'
type TableData = {
expiryTimestamp: number | undefined
@ -95,6 +96,7 @@ const OpenOrders = ({
const { t } = useTranslation(['common', 'trade'])
const openOrders = mangoStore((s) => s.mangoAccount.openOrders)
const [cancelId, setCancelId] = useState<string>('')
const [cancelAllMarket, setCancelAllMarket] = useState<string | number>('')
const [modifyOrderId, setModifyOrderId] = useState<string | undefined>(
undefined,
)
@ -109,6 +111,48 @@ const OpenOrders = ({
const { selectedMarket } = useSelectedMarket()
const { filledOrders, fetchingFilledOrders } = useFilledOrders()
const handleCancelAllForSpotMarket = useCallback(
async (o: Order) => {
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const mangoAccount = mangoStore.getState().mangoAccount.current
if (!group || !mangoAccount) return
const marketPk = findSerum3MarketPkInOpenOrders(o)
if (!marketPk) return
const market = group.getSerum3MarketByExternalMarket(
new PublicKey(marketPk),
)
setCancelAllMarket(market.name)
try {
const { signature: tx } = await client.serum3CancelAllOrders(
group,
mangoAccount,
market.serumMarketExternal,
)
const actions = mangoStore.getState().actions
await actions.fetchOpenOrders(true)
notify({
type: 'success',
title: 'Transaction successful',
txid: tx,
})
} catch (e) {
console.error('Error canceling', e)
if (isMangoError(e)) {
notify({
title: t('trade:cancel-order-error'),
description: e.message,
txid: e.txid,
type: 'error',
})
}
} finally {
setCancelAllMarket('')
}
},
[t],
)
const handleCancelSerumOrder = useCallback(
async (o: Order) => {
const client = mangoStore.getState().client
@ -222,7 +266,7 @@ const OpenOrders = ({
cancelEditOrderForm()
}
},
[t, modifiedOrderSize, modifiedOrderPrice],
[modifiedOrderSize, modifiedOrderPrice],
)
const handleCancelPerpOrder = useCallback(
@ -263,6 +307,44 @@ const OpenOrders = ({
[t],
)
const handleCancelAllPerpOrders = useCallback(
async (o: PerpOrder) => {
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const mangoAccount = mangoStore.getState().mangoAccount.current
const actions = mangoStore.getState().actions
if (!group || !mangoAccount) return
setCancelAllMarket(o.perpMarketIndex)
try {
const { signature: tx } = await client.perpCancelAllOrders(
group,
mangoAccount,
o.perpMarketIndex,
100,
)
await actions.fetchOpenOrders(true)
notify({
type: 'success',
title: 'Transaction successful',
txid: tx,
})
} catch (e) {
console.error('Error canceling', e)
if (isMangoError(e)) {
notify({
title: t('trade:cancel-order-error'),
description: e.message,
txid: e.txid,
type: 'error',
})
}
} finally {
setCancelAllMarket('')
}
},
[t],
)
const showEditOrderForm = (order: Order | PerpOrder, tickSize: number) => {
setModifyOrderId(order.orderId.toString())
setModifiedOrderSize(order.size.toString())
@ -377,6 +459,13 @@ const OpenOrders = ({
sortConfig,
} = useSortableData(formattedTableData())
const ordersByMarket: { [key: string]: number } = {}
// count the number of orders for each market
tableData.forEach((data) => {
const { market } = data
ordersByMarket[market.name] = (ordersByMarket[market.name] || 0) + 1
})
return mangoAccountAddress && tableData.length ? (
showTableView ? (
<Table>
@ -431,12 +520,12 @@ const OpenOrders = ({
</div>
</Th>
{!isUnownedAccount ? (
<Th className="w-[14.28%] text-right" />
<Th className="w-[16.67%] text-right" />
) : null}
</TrHead>
</thead>
<tbody>
{tableData.map((data) => {
{tableData.map((data, index) => {
const {
expiryTimestamp,
filledQuantity,
@ -450,23 +539,33 @@ const OpenOrders = ({
tickSize,
value,
} = data
const isFirstOrderForMarket =
ordersByMarket[market.name] > 1 &&
index ===
tableData.findIndex((item) => item.market.name === market.name)
const loadingCancel =
cancelAllMarket === market.name ||
(order instanceof PerpOrder &&
cancelAllMarket === order.perpMarketIndex)
return (
<TrBody
key={`${side}${size}${price}${orderId.toString()}`}
className="my-1 p-2"
>
<Td className="w-[14.28%]">
<Td className="w-[16.67%]">
<TableMarketName market={market} side={side} />
</Td>
{modifyOrderId !== orderId.toString() ? (
<>
<Td className="w-[14.28%] text-right font-mono">
<Td className="w-[16.67%] text-right font-mono">
<FormatNumericValue
value={size}
decimals={getDecimalCount(minOrderSize)}
/>
</Td>
<Td className="w-[14.28%] whitespace-nowrap text-right font-mono">
<Td className="w-[16.67%] whitespace-nowrap text-right font-mono">
<FormatNumericValue
value={price}
decimals={getDecimalCount(tickSize)}
@ -475,9 +574,9 @@ const OpenOrders = ({
</>
) : (
<>
<Td className="w-[14.28%]">
<Td className="w-[16.67%]">
<input
className="h-8 w-full rounded-l-none rounded-r-none border-b-2 border-l-0 border-r-0 border-t-0 border-th-bkg-4 bg-transparent px-0 text-right font-mono text-sm hover:border-th-fgd-3 focus:border-th-fgd-3 focus:outline-none"
className="h-8 w-full rounded-none border-x-0 border-b-2 border-t-0 border-th-bkg-4 bg-transparent px-0 text-right font-mono text-sm hover:border-th-fgd-3 focus:border-th-fgd-3 focus:outline-none"
type="text"
value={modifiedOrderSize}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
@ -485,10 +584,10 @@ const OpenOrders = ({
}
/>
</Td>
<Td className="w-[14.28%]">
<Td className="w-[16.67%]">
<input
autoFocus
className="h-8 w-full rounded-l-none rounded-r-none border-b-2 border-l-0 border-r-0 border-t-0 border-th-bkg-4 bg-transparent px-0 text-right font-mono text-sm hover:border-th-fgd-3 focus:border-th-fgd-3 focus:outline-none"
className="h-8 w-full rounded-none border-x-0 border-b-2 border-t-0 border-th-bkg-4 bg-transparent px-0 text-right font-mono text-sm hover:border-th-fgd-3 focus:border-th-fgd-3 focus:outline-none"
type="text"
value={modifiedOrderPrice}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
@ -498,9 +597,9 @@ const OpenOrders = ({
</Td>
</>
)}
<Td className="w-[14.28%] text-right font-mono">
<Td className="w-[16.67%] text-right font-mono">
{fetchingFilledOrders ? (
<div className="items flex justify-end">
<div className="flex justify-end">
<SheenLoader className="flex justify-end">
<div className="h-4 w-8 bg-th-bkg-2" />
</SheenLoader>
@ -512,7 +611,7 @@ const OpenOrders = ({
/>
)}
</Td>
<Td className="w-[14.28%] text-right font-mono">
<Td className="w-[16.67%] text-right font-mono">
<FormatNumericValue value={value} isUsd />
{expiryTimestamp ? (
<div className="h-min text-xxs leading-tight text-th-fgd-4">{`Expires ${new Date(
@ -521,56 +620,85 @@ const OpenOrders = ({
) : null}
</Td>
{!isUnownedAccount ? (
<Td className="w-[14.28%]">
<div className="flex justify-end space-x-2">
{modifyOrderId !== orderId.toString() ? (
<>
<IconButton
onClick={() => showEditOrderForm(order, tickSize)}
size="small"
>
<PencilIcon className="h-4 w-4" />
</IconButton>
<Tooltip content={t('cancel')}>
<IconButton
disabled={cancelId === orderId.toString()}
onClick={() =>
order instanceof PerpOrder
? handleCancelPerpOrder(order)
: handleCancelSerumOrder(order)
}
size="small"
>
{cancelId === orderId.toString() ? (
<Loading className="h-4 w-4" />
<>
<Td className="w-[16.67%]">
<div className="flex w-full justify-end">
<div className="flex justify-start space-x-2">
{modifyOrderId !== orderId.toString() ? (
<>
<IconButton
onClick={() =>
showEditOrderForm(order, tickSize)
}
size="small"
>
<PencilIcon className="h-4 w-4" />
</IconButton>
<Tooltip content={t('cancel')}>
<IconButton
disabled={cancelId === orderId.toString()}
onClick={() =>
order instanceof PerpOrder
? handleCancelPerpOrder(order)
: handleCancelSerumOrder(order)
}
size="small"
>
{cancelId === orderId.toString() ||
loadingCancel ? (
<Loading className="h-4 w-4" />
) : (
<TrashIcon className="h-4 w-4" />
)}
</IconButton>
</Tooltip>
{isFirstOrderForMarket ? (
<Tooltip
content={t('trade:cancel-all-orders', {
market: market.name,
})}
>
<IconButton
disabled={loadingCancel}
onClick={() =>
order instanceof PerpOrder
? handleCancelAllPerpOrders(order)
: handleCancelAllForSpotMarket(order)
}
size="small"
>
<NukeIcon className="h-4 w-4" />
</IconButton>
</Tooltip>
) : (
<TrashIcon className="h-4 w-4" />
<div className="h-8 w-8" />
)}
</IconButton>
</Tooltip>
</>
) : (
<>
<IconButton
onClick={() => modifyOrder(order)}
size="small"
>
{loadingModifyOrder ? (
<Loading className="h-4 w-4" />
) : (
<CheckIcon className="h-4 w-4" />
)}
</IconButton>
<IconButton
onClick={cancelEditOrderForm}
size="small"
>
<XMarkIcon className="h-4 w-4" />
</IconButton>
</>
)}
</div>
</Td>
</>
) : (
<>
<IconButton
onClick={() => modifyOrder(order)}
size="small"
>
{loadingModifyOrder ? (
<Loading className="h-4 w-4" />
) : (
<CheckIcon className="h-4 w-4" />
)}
</IconButton>
<IconButton
onClick={cancelEditOrderForm}
size="small"
>
<XMarkIcon className="h-4 w-4" />
</IconButton>
<div className="h-8 w-8" />
</>
)}
</div>
</div>
</Td>
</>
) : null}
</TrBody>
)
@ -579,7 +707,7 @@ const OpenOrders = ({
</Table>
) : (
<div>
{tableData.map((data) => {
{tableData.map((data, index) => {
const {
expiryTimestamp,
market,
@ -603,6 +731,14 @@ const OpenOrders = ({
externalMarket.quoteTokenIndex,
)
}
const isFirstOrderForMarket =
ordersByMarket[market.name] > 1 &&
index ===
tableData.findIndex((item) => item.market.name === market.name)
const loadingCancel =
cancelAllMarket === market.name ||
(order instanceof PerpOrder &&
cancelAllMarket === order.perpMarketIndex)
return (
<div
className="flex items-center justify-between border-b border-th-bkg-3 p-4"
@ -676,7 +812,7 @@ const OpenOrders = ({
<div>
<p className="text-xs">{t('trade:size')}</p>
<Input
className="w-full rounded-l-none rounded-r-none border-b-2 border-l-0 border-r-0 border-t-0 border-th-bkg-4 bg-transparent px-0 text-right font-mono hover:border-th-fgd-3 focus:border-th-fgd-3 focus:outline-none"
className="w-full rounded-none border-x-0 border-b-2 border-t-0 border-th-bkg-4 bg-transparent px-0 text-right font-mono hover:border-th-fgd-3 focus:border-th-fgd-3 focus:outline-none"
type="text"
value={modifiedOrderSize}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
@ -688,7 +824,7 @@ const OpenOrders = ({
<p className="text-xs">{t('price')}</p>
<Input
autoFocus
className="w-full rounded-l-none rounded-r-none border-b-2 border-l-0 border-r-0 border-t-0 border-th-bkg-4 bg-transparent px-0 text-right font-mono hover:border-th-fgd-3 focus:border-th-fgd-3 focus:outline-none"
className="w-full rounded-none border-x-0 border-b-2 border-t-0 border-th-bkg-4 bg-transparent px-0 text-right font-mono hover:border-th-fgd-3 focus:border-th-fgd-3 focus:outline-none"
type="text"
value={modifiedOrderPrice}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
@ -720,12 +856,31 @@ const OpenOrders = ({
}
size="small"
>
{cancelId === orderId.toString() ? (
{cancelId === orderId.toString() || loadingCancel ? (
<Loading className="h-4 w-4" />
) : (
<TrashIcon className="h-4 w-4" />
)}
</IconButton>
{isFirstOrderForMarket ? (
<Tooltip
content={t('trade:cancel-all-orders', {
market: market.name,
})}
>
<IconButton
disabled={loadingCancel}
onClick={() =>
order instanceof PerpOrder
? handleCancelAllPerpOrders(order)
: handleCancelAllForSpotMarket(order)
}
size="small"
>
<NukeIcon className="h-4 w-4" />
</IconButton>
</Tooltip>
) : null}
</>
) : (
<>

View File

@ -24,7 +24,7 @@
"dependencies": {
"@blockworks-foundation/mango-feeds": "0.1.7",
"@blockworks-foundation/mango-mints-redemption": "^0.0.10",
"@blockworks-foundation/mango-v4": "0.21.29",
"@blockworks-foundation/mango-v4": "0.21.31",
"@blockworks-foundation/mango-v4-settings": "0.14.15",
"@blockworks-foundation/mangolana": "0.0.14",
"@headlessui/react": "1.6.6",

View File

@ -9,6 +9,7 @@
"book": "Book",
"buys": "Buys",
"cancel-all": "Cancel All",
"cancel-all-orders": "Cancel all {{market}} orders",
"cancel-order": "Cancel Order",
"cancel-order-error": "Failed to cancel order",
"close-all": "Close All",

View File

@ -9,6 +9,7 @@
"book": "Book",
"buys": "Buys",
"cancel-all": "Cancel All",
"cancel-all-orders": "Cancel all {{market}} orders",
"cancel-order": "Cancel Order",
"cancel-order-error": "Failed to cancel order",
"close-all": "Close All",

View File

@ -9,6 +9,7 @@
"book": "Livro",
"buys": "Compras",
"cancel-all": "Cancelar Todos",
"cancel-all-orders": "Cancel all {{market}} orders",
"cancel-order": "Cancelar Pedido",
"cancel-order-error": "Falha ao cancelar pedido",
"close-all": "Fechar Todos",

View File

@ -9,6 +9,7 @@
"book": "Book",
"buys": "Buys",
"cancel-all": "Cancel All",
"cancel-all-orders": "Cancel all {{market}} orders",
"cancel-order": "Cancel Order",
"cancel-order-error": "Failed to cancel order",
"close-all": "Close All",

View File

@ -9,6 +9,7 @@
"book": "单薄",
"buys": "买",
"cancel-all": "取消全部",
"cancel-all-orders": "Cancel all {{market}} orders",
"cancel-order": "取消订单",
"cancel-order-error": "取消订单失败",
"close-all": "全部平仓",

View File

@ -9,6 +9,7 @@
"book": "單薄",
"buys": "買",
"cancel-all": "取消全部",
"cancel-all-orders": "Cancel all {{market}} orders",
"cancel-order": "取消訂單",
"cancel-order-error": "取消訂單失敗",
"close-all": "全部平倉",

View File

@ -350,20 +350,12 @@
bn.js "^5.2.1"
eslint-config-prettier "^9.0.0"
"@blockworks-foundation/mango-v4-settings@0.4.10":
version "0.4.10"
resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4-settings/-/mango-v4-settings-0.4.10.tgz#7a30db53c81abea56c2dd83e928259246e3b4e7d"
integrity sha512-J4RhfhcNmBn0CeqjN4mlaou+vPW9NGpWlbkx1+ZczsJw6r5tIkQ82MDqKMKvyIgn457krRVdJ9+gLmHEln4QfA==
"@blockworks-foundation/mango-v4@0.21.31":
version "0.21.31"
resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.21.31.tgz#b57511a66bc4a69d30e6e23f8053acf9fb21552f"
integrity sha512-KYGRvbqagsiTOnvu2yirdRuirXl2ei//b1VkCXtXJ2TCQE6lPyvMe24HziXzWbn9rymAnSohooKCvODQXDGFug==
dependencies:
bn.js "^5.2.1"
eslint-config-prettier "^9.0.0"
"@blockworks-foundation/mango-v4@0.21.29":
version "0.21.29"
resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.21.29.tgz#4ff5f6c7efed7daa6f34ac2779a3fd5785a5566e"
integrity sha512-gu+U55g52ttLu+44z66+5NDhLYaCGujjUjQm1/jVHbE4iXwgQoBCxY/QD6a0rZVR9tJYs+epoCL3KK9q0AUqCg==
dependencies:
"@blockworks-foundation/mango-v4-settings" "0.4.10"
"@blockworks-foundation/mango-v4-settings" "0.14.15"
"@blockworks-foundation/mangolana" "0.0.14"
"@coral-xyz/anchor" "^0.28.1-beta.2"
"@project-serum/serum" "0.13.65"