2023-07-17 18:18:01 -07:00
|
|
|
import { PublicKey } from '@solana/web3.js'
|
2022-09-13 23:24:26 -07:00
|
|
|
import mangoStore from '@store/mangoStore'
|
2022-09-20 11:14:31 -07:00
|
|
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
2023-07-17 18:18:01 -07:00
|
|
|
import { Market } from '@project-serum/serum'
|
2022-09-13 23:24:26 -07:00
|
|
|
import useLocalStorageState from 'hooks/useLocalStorageState'
|
2023-02-08 18:08:17 -08:00
|
|
|
import {
|
|
|
|
floorToDecimal,
|
|
|
|
formatNumericValue,
|
|
|
|
getDecimalCount,
|
|
|
|
} from 'utils/numbers'
|
2023-07-12 07:37:18 -07:00
|
|
|
import {
|
|
|
|
ANIMATION_SETTINGS_KEY,
|
2023-07-17 18:24:13 -07:00
|
|
|
// USE_ORDERBOOK_FEED_KEY,
|
2023-07-12 07:37:18 -07:00
|
|
|
} from 'utils/constants'
|
2022-09-13 23:24:26 -07:00
|
|
|
import { useTranslation } from 'next-i18next'
|
|
|
|
import Decimal from 'decimal.js'
|
2022-09-18 05:53:28 -07:00
|
|
|
import Tooltip from '@components/shared/Tooltip'
|
|
|
|
import GroupSize from './GroupSize'
|
2023-08-07 07:04:20 -07:00
|
|
|
// import { useViewport } from 'hooks/useViewport'
|
2023-07-17 18:18:01 -07:00
|
|
|
import { BookSide, Serum3Market } from '@blockworks-foundation/mango-v4'
|
2022-11-20 12:20:27 -08:00
|
|
|
import useSelectedMarket from 'hooks/useSelectedMarket'
|
2022-11-24 18:39:14 -08:00
|
|
|
import { INITIAL_ANIMATION_SETTINGS } from '@components/settings/AnimationSettings'
|
2023-04-22 10:35:57 -07:00
|
|
|
import { OrderbookFeed } from '@blockworks-foundation/mango-feeds'
|
2023-08-07 07:04:20 -07:00
|
|
|
// import { breakpoints } from 'utils/theme'
|
2023-07-17 18:18:01 -07:00
|
|
|
import {
|
|
|
|
decodeBook,
|
|
|
|
decodeBookL2,
|
|
|
|
getCumulativeOrderbookSide,
|
|
|
|
getMarket,
|
|
|
|
groupBy,
|
|
|
|
updatePerpMarketOnGroup,
|
|
|
|
} from 'utils/orderbook'
|
|
|
|
import { OrderbookData, OrderbookL2 } from 'types'
|
2023-08-10 23:30:38 -07:00
|
|
|
import isEqual from 'lodash/isEqual'
|
2023-08-17 03:56:38 -07:00
|
|
|
import { useViewport } from 'hooks/useViewport'
|
|
|
|
import { breakpoints } from 'utils/theme'
|
2022-09-13 23:24:26 -07:00
|
|
|
|
2023-04-25 17:58:33 -07:00
|
|
|
const sizeCompacter = Intl.NumberFormat('en', {
|
|
|
|
maximumFractionDigits: 6,
|
|
|
|
notation: 'compact',
|
|
|
|
})
|
|
|
|
|
|
|
|
const SHOW_EXPONENTIAL_THRESHOLD = 0.00001
|
|
|
|
|
2023-07-17 18:18:01 -07:00
|
|
|
const Orderbook = () => {
|
2022-10-03 03:38:05 -07:00
|
|
|
const { t } = useTranslation(['common', 'trade'])
|
2023-07-12 07:37:18 -07:00
|
|
|
const { serumOrPerpMarket: market } = useSelectedMarket()
|
2023-01-25 14:28:22 -08:00
|
|
|
const connection = mangoStore((s) => s.connection)
|
2022-09-13 23:24:26 -07:00
|
|
|
|
2023-02-21 21:11:56 -08:00
|
|
|
const [tickSize, setTickSize] = useState(0)
|
2023-07-17 18:18:01 -07:00
|
|
|
const [grouping, setGrouping] = useState(0.01)
|
2023-07-12 11:01:29 -07:00
|
|
|
const [useOrderbookFeed, setUseOrderbookFeed] = useState(false)
|
2023-08-07 07:04:20 -07:00
|
|
|
const orderbookElRef = useRef<HTMLDivElement>(null)
|
|
|
|
const [isScrolled, setIsScrolled] = useState(false)
|
2023-07-12 11:01:29 -07:00
|
|
|
// const [useOrderbookFeed, setUseOrderbookFeed] = useState(
|
|
|
|
// localStorage.getItem(USE_ORDERBOOK_FEED_KEY) !== null
|
|
|
|
// ? localStorage.getItem(USE_ORDERBOOK_FEED_KEY) === 'true'
|
|
|
|
// : true
|
|
|
|
// )
|
2023-08-17 03:56:38 -07:00
|
|
|
const { width } = useViewport()
|
|
|
|
const isMobile = width ? width < breakpoints.md : false
|
2023-07-17 18:18:01 -07:00
|
|
|
const [orderbookData, setOrderbookData] = useState<OrderbookData | null>(null)
|
|
|
|
const currentOrderbookData = useRef<OrderbookL2>()
|
2022-09-13 23:24:26 -07:00
|
|
|
|
2023-04-18 19:01:58 -07:00
|
|
|
const depth = useMemo(() => {
|
2023-08-17 03:56:38 -07:00
|
|
|
return isMobile ? 12 : 30
|
|
|
|
}, [isMobile])
|
2022-09-20 11:14:31 -07:00
|
|
|
|
2023-07-02 22:55:25 -07:00
|
|
|
const depthArray: number[] = useMemo(() => {
|
|
|
|
return Array(depth).fill(0)
|
|
|
|
}, [depth])
|
2023-02-19 21:43:35 -08:00
|
|
|
|
2023-08-07 07:04:20 -07:00
|
|
|
const verticallyCenterOrderbook = useCallback(() => {
|
|
|
|
const element = orderbookElRef.current
|
|
|
|
if (element) {
|
2023-08-07 07:41:14 -07:00
|
|
|
if (
|
|
|
|
element.parentElement &&
|
|
|
|
element.scrollHeight > element.parentElement.offsetHeight
|
|
|
|
) {
|
2023-08-07 07:04:20 -07:00
|
|
|
element.scrollTop =
|
|
|
|
(element.scrollHeight - element.scrollHeight) / 2 +
|
2023-08-07 07:41:14 -07:00
|
|
|
(element.scrollHeight - element.parentElement.offsetHeight) / 2 +
|
|
|
|
60
|
2023-08-07 07:04:20 -07:00
|
|
|
} else {
|
|
|
|
element.scrollTop = (element.scrollHeight - element.offsetHeight) / 2
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
window.addEventListener('resize', verticallyCenterOrderbook)
|
|
|
|
}, [verticallyCenterOrderbook])
|
|
|
|
|
|
|
|
const handleScroll = useCallback(() => {
|
|
|
|
setIsScrolled(true)
|
|
|
|
}, [])
|
|
|
|
|
2023-07-02 22:55:25 -07:00
|
|
|
const orderbookFeed = useRef<OrderbookFeed | null>(null)
|
2023-02-19 21:43:35 -08:00
|
|
|
|
2023-07-02 22:55:25 -07:00
|
|
|
useEffect(() => {
|
|
|
|
if (market && market.tickSize !== tickSize) {
|
|
|
|
setTickSize(market.tickSize)
|
|
|
|
setGrouping(market.tickSize)
|
|
|
|
}
|
|
|
|
}, [market, tickSize])
|
2022-09-13 23:24:26 -07:00
|
|
|
|
2023-02-25 11:58:34 -08:00
|
|
|
const bidAccountAddress = useMemo(() => {
|
|
|
|
if (!market) return ''
|
|
|
|
const bidsPk =
|
|
|
|
market instanceof Market ? market['_decoded'].bids : market.bids
|
|
|
|
return bidsPk.toString()
|
|
|
|
}, [market])
|
|
|
|
|
|
|
|
const askAccountAddress = useMemo(() => {
|
|
|
|
if (!market) return ''
|
|
|
|
const asksPk =
|
|
|
|
market instanceof Market ? market['_decoded'].asks : market.asks
|
|
|
|
return asksPk.toString()
|
|
|
|
}, [market])
|
|
|
|
|
2023-07-17 18:18:01 -07:00
|
|
|
useEffect(
|
|
|
|
() =>
|
|
|
|
mangoStore.subscribe(
|
|
|
|
(state) => state.selectedMarket.orderbook,
|
|
|
|
(newOrderbook) => {
|
|
|
|
if (
|
|
|
|
newOrderbook &&
|
|
|
|
market &&
|
|
|
|
!isEqual(currentOrderbookData.current, newOrderbook)
|
|
|
|
) {
|
|
|
|
// check if user has open orders so we can highlight them on orderbook
|
|
|
|
const openOrders = mangoStore.getState().mangoAccount.openOrders
|
|
|
|
const marketPk = market.publicKey.toString()
|
|
|
|
const bids2 = mangoStore.getState().selectedMarket.bidsAccount
|
|
|
|
const asks2 = mangoStore.getState().selectedMarket.asksAccount
|
|
|
|
const mangoAccount = mangoStore.getState().mangoAccount.current
|
|
|
|
let usersOpenOrderPrices: number[] = []
|
|
|
|
if (
|
|
|
|
mangoAccount &&
|
|
|
|
bids2 &&
|
|
|
|
asks2 &&
|
|
|
|
bids2 instanceof BookSide &&
|
|
|
|
asks2 instanceof BookSide
|
|
|
|
) {
|
|
|
|
usersOpenOrderPrices = [...bids2.items(), ...asks2.items()]
|
|
|
|
.filter((order) => order.owner.equals(mangoAccount.publicKey))
|
|
|
|
.map((order) => order.price)
|
|
|
|
} else {
|
|
|
|
usersOpenOrderPrices =
|
|
|
|
marketPk && openOrders[marketPk]?.length
|
|
|
|
? openOrders[marketPk]?.map((order) => order.price)
|
|
|
|
: []
|
|
|
|
}
|
|
|
|
|
|
|
|
// updated orderbook data
|
|
|
|
const bids =
|
|
|
|
groupBy(newOrderbook?.bids, market, grouping, true) || []
|
|
|
|
const asks =
|
|
|
|
groupBy(newOrderbook?.asks, market, grouping, false) || []
|
|
|
|
|
|
|
|
const sum = (total: number, [, size]: number[], index: number) =>
|
|
|
|
index < depth ? total + size : total
|
|
|
|
const totalSize = bids.reduce(sum, 0) + asks.reduce(sum, 0)
|
|
|
|
|
|
|
|
const maxSize =
|
|
|
|
Math.max(
|
|
|
|
...bids.map((b: number[]) => {
|
|
|
|
return b[1]
|
2023-07-21 11:47:53 -07:00
|
|
|
}),
|
2023-07-17 18:18:01 -07:00
|
|
|
) +
|
|
|
|
Math.max(
|
|
|
|
...asks.map((a: number[]) => {
|
|
|
|
return a[1]
|
2023-07-21 11:47:53 -07:00
|
|
|
}),
|
2023-07-17 18:18:01 -07:00
|
|
|
)
|
|
|
|
const isGrouped = grouping !== market.tickSize
|
|
|
|
const bidsToDisplay = getCumulativeOrderbookSide(
|
|
|
|
bids,
|
|
|
|
totalSize,
|
|
|
|
maxSize,
|
|
|
|
depth,
|
|
|
|
usersOpenOrderPrices,
|
|
|
|
grouping,
|
2023-07-21 11:47:53 -07:00
|
|
|
isGrouped,
|
2023-07-17 18:18:01 -07:00
|
|
|
)
|
|
|
|
const asksToDisplay = getCumulativeOrderbookSide(
|
|
|
|
asks,
|
|
|
|
totalSize,
|
|
|
|
maxSize,
|
|
|
|
depth,
|
|
|
|
usersOpenOrderPrices,
|
|
|
|
grouping,
|
2023-07-21 11:47:53 -07:00
|
|
|
isGrouped,
|
2023-07-17 18:18:01 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
currentOrderbookData.current = newOrderbook
|
|
|
|
if (bidsToDisplay[0] || asksToDisplay[0]) {
|
|
|
|
const bid = bidsToDisplay[0]?.price
|
|
|
|
const ask = asksToDisplay[0]?.price
|
|
|
|
let spread = 0,
|
|
|
|
spreadPercentage = 0
|
|
|
|
if (bid && ask) {
|
|
|
|
spread = parseFloat(
|
2023-07-21 11:47:53 -07:00
|
|
|
(ask - bid).toFixed(getDecimalCount(market.tickSize)),
|
2023-07-17 18:18:01 -07:00
|
|
|
)
|
|
|
|
spreadPercentage = (spread / ask) * 100
|
|
|
|
}
|
|
|
|
|
|
|
|
setOrderbookData({
|
|
|
|
bids: bidsToDisplay,
|
|
|
|
asks: asksToDisplay.reverse(),
|
|
|
|
spread,
|
|
|
|
spreadPercentage,
|
|
|
|
})
|
2023-08-07 07:04:20 -07:00
|
|
|
if (!isScrolled) {
|
|
|
|
verticallyCenterOrderbook()
|
|
|
|
}
|
2023-07-17 18:18:01 -07:00
|
|
|
} else {
|
|
|
|
setOrderbookData(null)
|
|
|
|
}
|
|
|
|
}
|
2023-07-21 11:47:53 -07:00
|
|
|
},
|
2023-07-17 18:18:01 -07:00
|
|
|
),
|
2023-07-21 11:47:53 -07:00
|
|
|
[depth, grouping, market],
|
2023-07-17 18:18:01 -07:00
|
|
|
)
|
|
|
|
|
2023-03-25 11:19:57 -07:00
|
|
|
// subscribe to the bids and asks orderbook accounts
|
2022-09-13 23:24:26 -07:00
|
|
|
useEffect(() => {
|
|
|
|
const set = mangoStore.getState().set
|
2022-10-10 19:16:13 -07:00
|
|
|
const client = mangoStore.getState().client
|
2023-02-25 11:58:34 -08:00
|
|
|
const group = mangoStore.getState().group
|
2023-03-25 11:47:37 -07:00
|
|
|
const market = getMarket()
|
|
|
|
if (!group || !market) return
|
2022-12-05 18:53:59 -08:00
|
|
|
|
2023-04-22 10:35:57 -07:00
|
|
|
if (useOrderbookFeed) {
|
2023-05-10 02:53:55 -07:00
|
|
|
if (!orderbookFeed.current) {
|
|
|
|
orderbookFeed.current = new OrderbookFeed(
|
|
|
|
`wss://api.mngo.cloud/orderbook/v1/`,
|
|
|
|
{
|
|
|
|
reconnectionIntervalMs: 5_000,
|
|
|
|
reconnectionMaxAttempts: 6,
|
2023-07-21 11:47:53 -07:00
|
|
|
},
|
2023-05-10 02:53:55 -07:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-04-22 10:35:57 -07:00
|
|
|
let hasConnected = false
|
2023-05-10 02:53:55 -07:00
|
|
|
orderbookFeed.current.onConnect(() => {
|
|
|
|
if (!orderbookFeed.current) return
|
2023-04-22 10:35:57 -07:00
|
|
|
console.log('[OrderbookFeed] connected')
|
|
|
|
hasConnected = true
|
2023-05-10 02:53:55 -07:00
|
|
|
orderbookFeed.current.subscribe({
|
2023-04-22 10:35:57 -07:00
|
|
|
marketId: market.publicKey.toBase58(),
|
2022-11-15 20:12:51 -08:00
|
|
|
})
|
2023-04-22 10:35:57 -07:00
|
|
|
})
|
|
|
|
|
2023-05-10 02:53:55 -07:00
|
|
|
orderbookFeed.current.onDisconnect((reconnectionAttemptsExhausted) => {
|
2023-04-22 10:35:57 -07:00
|
|
|
// fallback to rpc if we couldn't reconnect or if we never connected
|
|
|
|
if (reconnectionAttemptsExhausted || !hasConnected) {
|
|
|
|
console.warn('[OrderbookFeed] disconnected')
|
|
|
|
setUseOrderbookFeed(false)
|
|
|
|
} else {
|
|
|
|
console.log('[OrderbookFeed] reconnecting...')
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
let lastWriteVersion = 0
|
2023-05-10 02:53:55 -07:00
|
|
|
orderbookFeed.current.onL2Update((update) => {
|
2023-04-22 10:35:57 -07:00
|
|
|
const selectedMarket = mangoStore.getState().selectedMarket
|
2023-05-07 04:53:22 -07:00
|
|
|
if (!useOrderbookFeed || !selectedMarket || !selectedMarket.current)
|
|
|
|
return
|
2023-06-22 11:29:40 -07:00
|
|
|
const selectedMarketKey =
|
|
|
|
selectedMarket.current instanceof Serum3Market
|
|
|
|
? selectedMarket.current['serumMarketExternal']
|
|
|
|
: selectedMarket.current.publicKey
|
|
|
|
if (update.market != selectedMarketKey.toBase58()) return
|
2023-04-22 10:35:57 -07:00
|
|
|
|
|
|
|
// ensure updates are applied in the correct order by checking slot and writeVersion
|
|
|
|
const lastSeenSlot =
|
|
|
|
update.side == 'bid'
|
|
|
|
? mangoStore.getState().selectedMarket.lastSeenSlot.bids
|
|
|
|
: mangoStore.getState().selectedMarket.lastSeenSlot.asks
|
|
|
|
if (update.slot < lastSeenSlot) return
|
|
|
|
if (
|
|
|
|
update.slot == lastSeenSlot &&
|
|
|
|
update.writeVersion < lastWriteVersion
|
|
|
|
)
|
|
|
|
return
|
|
|
|
lastWriteVersion = update.writeVersion
|
|
|
|
|
|
|
|
const bookside =
|
|
|
|
update.side == 'bid'
|
|
|
|
? selectedMarket.orderbook.bids
|
|
|
|
: selectedMarket.orderbook.asks
|
|
|
|
const new_bookside = Array.from(bookside)
|
|
|
|
|
|
|
|
for (const diff of update.update) {
|
|
|
|
// find existing level for each update
|
|
|
|
const levelIndex = new_bookside.findIndex(
|
2023-07-21 11:47:53 -07:00
|
|
|
(level) => level && level.length && level[0] === diff[0],
|
2023-04-22 10:35:57 -07:00
|
|
|
)
|
|
|
|
if (diff[1] > 0) {
|
|
|
|
// level being added or updated
|
|
|
|
if (levelIndex !== -1) {
|
|
|
|
new_bookside[levelIndex] = diff
|
|
|
|
} else {
|
|
|
|
// add new level and resort
|
|
|
|
new_bookside.push(diff)
|
|
|
|
new_bookside.sort((a, b) => {
|
|
|
|
return update.side == 'bid' ? b[0] - a[0] : a[0] - b[0]
|
|
|
|
})
|
2023-02-21 18:24:46 -08:00
|
|
|
}
|
2023-04-22 10:35:57 -07:00
|
|
|
} else {
|
|
|
|
// level being removed if zero size
|
|
|
|
if (levelIndex !== -1) {
|
|
|
|
new_bookside.splice(levelIndex, 1)
|
|
|
|
} else {
|
2023-05-10 02:53:55 -07:00
|
|
|
console.warn('[OrderbookFeed] tried to remove missing level')
|
2023-04-22 10:35:57 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
set((state) => {
|
|
|
|
if (update.side == 'bid') {
|
|
|
|
state.selectedMarket.bidsAccount = undefined
|
|
|
|
state.selectedMarket.orderbook.bids = new_bookside
|
|
|
|
state.selectedMarket.lastSeenSlot.bids = update.slot
|
|
|
|
} else {
|
|
|
|
state.selectedMarket.asksAccount = undefined
|
|
|
|
state.selectedMarket.orderbook.asks = new_bookside
|
|
|
|
state.selectedMarket.lastSeenSlot.asks = update.slot
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
2023-05-10 02:53:55 -07:00
|
|
|
orderbookFeed.current.onL2Checkpoint((checkpoint) => {
|
2023-05-07 04:53:22 -07:00
|
|
|
if (
|
|
|
|
!useOrderbookFeed ||
|
|
|
|
checkpoint.market !== market.publicKey.toBase58()
|
|
|
|
)
|
|
|
|
return
|
2023-04-22 10:35:57 -07:00
|
|
|
set((state) => {
|
|
|
|
state.selectedMarket.lastSeenSlot.bids = checkpoint.slot
|
|
|
|
state.selectedMarket.lastSeenSlot.asks = checkpoint.slot
|
|
|
|
state.selectedMarket.bidsAccount = undefined
|
|
|
|
state.selectedMarket.asksAccount = undefined
|
|
|
|
state.selectedMarket.orderbook.bids = checkpoint.bids
|
|
|
|
state.selectedMarket.orderbook.asks = checkpoint.asks
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
return () => {
|
2023-05-10 02:53:55 -07:00
|
|
|
if (!orderbookFeed.current) return
|
|
|
|
console.log(
|
2023-07-21 11:47:53 -07:00
|
|
|
`[OrderbookFeed] unsubscribe ${market.publicKey.toBase58()}`,
|
2023-05-10 02:53:55 -07:00
|
|
|
)
|
|
|
|
orderbookFeed.current.unsubscribe(market.publicKey.toBase58())
|
2023-04-22 10:35:57 -07:00
|
|
|
}
|
|
|
|
} else {
|
2023-05-10 02:53:55 -07:00
|
|
|
console.log(`[OrderbookRPC] subscribe ${market.publicKey.toBase58()}`)
|
2023-04-22 10:35:57 -07:00
|
|
|
|
|
|
|
let bidSubscriptionId: number | undefined = undefined
|
|
|
|
let askSubscriptionId: number | undefined = undefined
|
|
|
|
const bidsPk = new PublicKey(bidAccountAddress)
|
|
|
|
if (bidsPk) {
|
|
|
|
connection
|
|
|
|
.getAccountInfoAndContext(bidsPk)
|
|
|
|
.then(({ context, value: info }) => {
|
|
|
|
if (!info) return
|
|
|
|
const decodedBook = decodeBook(client, market, info, 'bids')
|
2022-11-15 20:12:51 -08:00
|
|
|
set((state) => {
|
2023-04-22 10:35:57 -07:00
|
|
|
state.selectedMarket.lastSeenSlot.bids = context.slot
|
2022-11-30 07:46:20 -08:00
|
|
|
state.selectedMarket.bidsAccount = decodedBook
|
|
|
|
state.selectedMarket.orderbook.bids = decodeBookL2(decodedBook)
|
2022-11-15 20:12:51 -08:00
|
|
|
})
|
2023-02-21 13:07:47 -08:00
|
|
|
})
|
2023-04-22 10:35:57 -07:00
|
|
|
bidSubscriptionId = connection.onAccountChange(
|
|
|
|
bidsPk,
|
|
|
|
(info, context) => {
|
|
|
|
const lastSeenSlot =
|
|
|
|
mangoStore.getState().selectedMarket.lastSeenSlot.bids
|
|
|
|
if (context.slot > lastSeenSlot) {
|
|
|
|
const market = getMarket()
|
|
|
|
if (!market) return
|
|
|
|
const decodedBook = decodeBook(client, market, info, 'bids')
|
|
|
|
if (decodedBook instanceof BookSide) {
|
|
|
|
updatePerpMarketOnGroup(decodedBook, 'bids')
|
|
|
|
}
|
|
|
|
set((state) => {
|
|
|
|
state.selectedMarket.bidsAccount = decodedBook
|
|
|
|
state.selectedMarket.orderbook.bids = decodeBookL2(decodedBook)
|
|
|
|
state.selectedMarket.lastSeenSlot.bids = context.slot
|
|
|
|
})
|
2023-02-21 18:24:46 -08:00
|
|
|
}
|
2023-04-22 10:35:57 -07:00
|
|
|
},
|
2023-07-21 11:47:53 -07:00
|
|
|
'processed',
|
2023-04-22 10:35:57 -07:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
const asksPk = new PublicKey(askAccountAddress)
|
|
|
|
if (asksPk) {
|
|
|
|
connection
|
|
|
|
.getAccountInfoAndContext(asksPk)
|
|
|
|
.then(({ context, value: info }) => {
|
|
|
|
if (!info) return
|
|
|
|
const decodedBook = decodeBook(client, market, info, 'asks')
|
2022-11-15 20:12:51 -08:00
|
|
|
set((state) => {
|
2022-11-30 07:46:20 -08:00
|
|
|
state.selectedMarket.asksAccount = decodedBook
|
|
|
|
state.selectedMarket.orderbook.asks = decodeBookL2(decodedBook)
|
2023-02-21 13:07:47 -08:00
|
|
|
state.selectedMarket.lastSeenSlot.asks = context.slot
|
2022-11-15 20:12:51 -08:00
|
|
|
})
|
2023-04-22 10:35:57 -07:00
|
|
|
})
|
|
|
|
askSubscriptionId = connection.onAccountChange(
|
|
|
|
asksPk,
|
|
|
|
(info, context) => {
|
|
|
|
const lastSeenSlot =
|
|
|
|
mangoStore.getState().selectedMarket.lastSeenSlot.asks
|
|
|
|
if (context.slot > lastSeenSlot) {
|
|
|
|
const market = getMarket()
|
|
|
|
if (!market) return
|
|
|
|
const decodedBook = decodeBook(client, market, info, 'asks')
|
|
|
|
if (decodedBook instanceof BookSide) {
|
|
|
|
updatePerpMarketOnGroup(decodedBook, 'asks')
|
|
|
|
}
|
|
|
|
set((state) => {
|
|
|
|
state.selectedMarket.asksAccount = decodedBook
|
|
|
|
state.selectedMarket.orderbook.asks = decodeBookL2(decodedBook)
|
|
|
|
state.selectedMarket.lastSeenSlot.asks = context.slot
|
|
|
|
})
|
|
|
|
}
|
|
|
|
},
|
2023-07-21 11:47:53 -07:00
|
|
|
'processed',
|
2023-04-22 10:35:57 -07:00
|
|
|
)
|
2022-11-15 20:12:51 -08:00
|
|
|
}
|
2023-04-22 10:35:57 -07:00
|
|
|
return () => {
|
2023-05-10 02:53:55 -07:00
|
|
|
console.log(`[OrderbookRPC] unsubscribe ${market.publicKey.toBase58()}`)
|
2023-04-22 10:35:57 -07:00
|
|
|
if (typeof bidSubscriptionId !== 'undefined') {
|
|
|
|
connection.removeAccountChangeListener(bidSubscriptionId)
|
|
|
|
}
|
|
|
|
if (typeof askSubscriptionId !== 'undefined') {
|
|
|
|
connection.removeAccountChangeListener(askSubscriptionId)
|
|
|
|
}
|
2022-11-15 20:12:51 -08:00
|
|
|
}
|
2022-09-13 23:24:26 -07:00
|
|
|
}
|
2023-04-22 10:35:57 -07:00
|
|
|
}, [bidAccountAddress, askAccountAddress, connection, useOrderbookFeed])
|
2022-09-13 23:24:26 -07:00
|
|
|
|
2023-05-10 02:53:55 -07:00
|
|
|
useEffect(() => {
|
|
|
|
const market = getMarket()
|
|
|
|
if (!orderbookFeed.current || !market) return
|
|
|
|
console.log(`[OrderbookFeed] subscribe ${market.publicKey.toBase58()}`)
|
|
|
|
orderbookFeed.current.subscribe({
|
|
|
|
marketId: market.publicKey.toBase58(),
|
|
|
|
})
|
|
|
|
}, [bidAccountAddress])
|
|
|
|
|
2022-09-20 11:14:31 -07:00
|
|
|
const onGroupSizeChange = useCallback((groupSize: number) => {
|
2022-09-18 05:53:28 -07:00
|
|
|
setGrouping(groupSize)
|
2022-09-20 11:14:31 -07:00
|
|
|
}, [])
|
2022-09-18 05:53:28 -07:00
|
|
|
|
2022-09-13 23:24:26 -07:00
|
|
|
return (
|
2023-08-07 07:04:20 -07:00
|
|
|
<div className="flex h-full flex-col">
|
2023-08-17 16:47:11 -07:00
|
|
|
<div className="flex h-10 items-center justify-between border-b border-th-bkg-3 px-4">
|
2022-10-10 19:16:13 -07:00
|
|
|
{market ? (
|
2023-04-18 19:01:58 -07:00
|
|
|
<>
|
2023-07-24 15:52:09 -07:00
|
|
|
<p className="text-xs">{t('trade:grouping')}:</p>
|
2023-04-18 19:01:58 -07:00
|
|
|
<div id="trade-step-four">
|
|
|
|
<Tooltip
|
|
|
|
className="hidden md:block"
|
|
|
|
content={t('trade:grouping')}
|
|
|
|
placement="left"
|
|
|
|
delay={100}
|
|
|
|
>
|
|
|
|
<GroupSize
|
|
|
|
tickSize={market.tickSize}
|
|
|
|
onChange={onGroupSizeChange}
|
|
|
|
value={grouping}
|
|
|
|
/>
|
|
|
|
</Tooltip>
|
|
|
|
</div>
|
|
|
|
</>
|
2022-09-25 22:32:48 -07:00
|
|
|
) : null}
|
2022-09-20 13:05:50 -07:00
|
|
|
</div>
|
2023-07-12 07:37:18 -07:00
|
|
|
<div className="grid grid-cols-2 px-2 py-0.5 text-xxs text-th-fgd-4">
|
|
|
|
<div className="col-span-1">{t('price')}</div>
|
|
|
|
<div className="col-span-1 text-right">{t('trade:size')}</div>
|
2022-09-20 13:05:50 -07:00
|
|
|
</div>
|
2023-08-07 07:04:20 -07:00
|
|
|
<div
|
|
|
|
className="hide-scroll relative h-full overflow-y-scroll"
|
|
|
|
ref={orderbookElRef}
|
|
|
|
onScroll={handleScroll}
|
|
|
|
>
|
2023-07-12 07:37:18 -07:00
|
|
|
{depthArray.map((_x, idx) => {
|
|
|
|
let index = idx
|
|
|
|
if (orderbookData?.asks) {
|
|
|
|
const lengthDiff = depthArray.length - orderbookData.asks.length
|
|
|
|
if (lengthDiff > 0) {
|
|
|
|
index = index < lengthDiff ? -1 : Math.abs(lengthDiff - index)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return (
|
|
|
|
<div className="h-[20px]" key={idx}>
|
|
|
|
{!!orderbookData?.asks[index] && market ? (
|
|
|
|
<MemoizedOrderbookRow
|
|
|
|
minOrderSize={market.minOrderSize}
|
|
|
|
tickSize={market.tickSize}
|
|
|
|
hasOpenOrder={orderbookData?.asks[index].isUsersOrder}
|
|
|
|
key={orderbookData?.asks[index].price}
|
|
|
|
price={orderbookData?.asks[index].price}
|
|
|
|
size={orderbookData?.asks[index].size}
|
|
|
|
side="sell"
|
|
|
|
sizePercent={orderbookData?.asks[index].sizePercent}
|
2023-07-25 03:43:19 -07:00
|
|
|
averagePrice={orderbookData?.asks[index].averagePrice}
|
|
|
|
cumulativeValue={orderbookData?.asks[index].cumulativeValue}
|
|
|
|
cumulativeSize={orderbookData?.asks[index].cumulativeSize}
|
2023-07-12 07:37:18 -07:00
|
|
|
cumulativeSizePercent={
|
|
|
|
orderbookData?.asks[index].cumulativeSizePercent
|
|
|
|
}
|
|
|
|
grouping={grouping}
|
|
|
|
/>
|
|
|
|
) : null}
|
2022-09-20 13:05:50 -07:00
|
|
|
</div>
|
2023-07-12 07:37:18 -07:00
|
|
|
)
|
|
|
|
})}
|
|
|
|
<div
|
2023-08-17 16:47:11 -07:00
|
|
|
className="my-1 grid grid-cols-2 border-y border-th-bkg-3 px-4 py-1 text-xs text-th-fgd-4"
|
2023-07-12 07:37:18 -07:00
|
|
|
id="trade-step-nine"
|
|
|
|
>
|
|
|
|
<div className="col-span-1 flex justify-between">
|
|
|
|
<div className="text-xxs">{t('trade:spread')}</div>
|
|
|
|
<div className="font-mono">
|
|
|
|
{orderbookData?.spreadPercentage.toFixed(2)}%
|
2022-09-20 13:05:50 -07:00
|
|
|
</div>
|
|
|
|
</div>
|
2023-07-12 07:37:18 -07:00
|
|
|
<div className="col-span-1 text-right font-mono">
|
|
|
|
{orderbookData?.spread
|
|
|
|
? orderbookData.spread < SHOW_EXPONENTIAL_THRESHOLD
|
|
|
|
? orderbookData.spread.toExponential()
|
|
|
|
: formatNumericValue(
|
|
|
|
orderbookData.spread,
|
2023-07-21 11:47:53 -07:00
|
|
|
market ? getDecimalCount(market.tickSize) : undefined,
|
2023-07-12 07:37:18 -07:00
|
|
|
)
|
|
|
|
: null}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{depthArray.map((_x, index) => (
|
|
|
|
<div className="h-[20px]" key={index}>
|
|
|
|
{!!orderbookData?.bids[index] && market ? (
|
|
|
|
<MemoizedOrderbookRow
|
|
|
|
minOrderSize={market.minOrderSize}
|
|
|
|
tickSize={market.tickSize}
|
|
|
|
hasOpenOrder={orderbookData?.bids[index].isUsersOrder}
|
|
|
|
price={orderbookData?.bids[index].price}
|
|
|
|
size={orderbookData?.bids[index].size}
|
|
|
|
side="buy"
|
|
|
|
sizePercent={orderbookData?.bids[index].sizePercent}
|
2023-07-25 03:43:19 -07:00
|
|
|
averagePrice={orderbookData?.bids[index].averagePrice}
|
|
|
|
cumulativeValue={orderbookData?.bids[index].cumulativeValue}
|
|
|
|
cumulativeSize={orderbookData?.bids[index].cumulativeSize}
|
2023-07-12 07:37:18 -07:00
|
|
|
cumulativeSizePercent={
|
|
|
|
orderbookData?.bids[index].cumulativeSizePercent
|
|
|
|
}
|
|
|
|
grouping={grouping}
|
|
|
|
/>
|
|
|
|
) : null}
|
|
|
|
</div>
|
|
|
|
))}
|
2022-09-13 23:24:26 -07:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
const OrderbookRow = ({
|
|
|
|
side,
|
|
|
|
price,
|
|
|
|
size,
|
|
|
|
sizePercent,
|
|
|
|
// invert,
|
2022-11-01 11:13:02 -07:00
|
|
|
hasOpenOrder,
|
2022-09-19 16:26:30 -07:00
|
|
|
minOrderSize,
|
2023-07-25 03:43:19 -07:00
|
|
|
averagePrice,
|
|
|
|
cumulativeValue,
|
|
|
|
cumulativeSize,
|
2022-09-29 13:00:36 -07:00
|
|
|
cumulativeSizePercent,
|
2022-09-19 16:26:30 -07:00
|
|
|
tickSize,
|
2022-09-13 23:24:26 -07:00
|
|
|
grouping,
|
|
|
|
}: {
|
|
|
|
side: 'buy' | 'sell'
|
|
|
|
price: number
|
|
|
|
size: number
|
|
|
|
sizePercent: number
|
2023-07-25 03:43:19 -07:00
|
|
|
averagePrice: number
|
|
|
|
cumulativeValue: number
|
|
|
|
cumulativeSize: number
|
2022-09-29 13:00:36 -07:00
|
|
|
cumulativeSizePercent: number
|
2022-11-01 11:13:02 -07:00
|
|
|
hasOpenOrder: boolean
|
2022-09-13 23:24:26 -07:00
|
|
|
// invert: boolean
|
|
|
|
grouping: number
|
2022-09-19 16:26:30 -07:00
|
|
|
minOrderSize: number
|
|
|
|
tickSize: number
|
2022-09-13 23:24:26 -07:00
|
|
|
}) => {
|
|
|
|
const element = useRef<HTMLDivElement>(null)
|
2022-11-22 21:38:31 -08:00
|
|
|
const [animationSettings] = useLocalStorageState(
|
|
|
|
ANIMATION_SETTINGS_KEY,
|
2023-07-21 11:47:53 -07:00
|
|
|
INITIAL_ANIMATION_SETTINGS,
|
2022-11-22 21:38:31 -08:00
|
|
|
)
|
2022-09-13 23:24:26 -07:00
|
|
|
const flashClassName = side === 'sell' ? 'red-flash' : 'green-flash'
|
|
|
|
|
|
|
|
useEffect(() => {
|
2022-11-22 21:38:31 -08:00
|
|
|
animationSettings['orderbook-flash'].active &&
|
2022-09-13 23:24:26 -07:00
|
|
|
!element.current?.classList.contains(`${flashClassName}`) &&
|
|
|
|
element.current?.classList.add(`${flashClassName}`)
|
|
|
|
const id = setTimeout(
|
|
|
|
() =>
|
|
|
|
element.current?.classList.contains(`${flashClassName}`) &&
|
|
|
|
element.current?.classList.remove(`${flashClassName}`),
|
2023-07-21 11:47:53 -07:00
|
|
|
500,
|
2022-09-13 23:24:26 -07:00
|
|
|
)
|
|
|
|
return () => clearTimeout(id)
|
|
|
|
}, [price, size])
|
|
|
|
|
2022-09-26 12:56:06 -07:00
|
|
|
const formattedSize = useMemo(() => {
|
|
|
|
return minOrderSize && !isNaN(size)
|
2022-09-19 16:26:30 -07:00
|
|
|
? floorToDecimal(size, getDecimalCount(minOrderSize))
|
2023-04-22 10:35:57 -07:00
|
|
|
: new Decimal(size ?? -1)
|
2022-09-26 12:56:06 -07:00
|
|
|
}, [size, minOrderSize])
|
2022-09-13 23:24:26 -07:00
|
|
|
|
2022-09-26 12:56:06 -07:00
|
|
|
const formattedPrice = useMemo(() => {
|
|
|
|
return tickSize && !isNaN(price)
|
2022-09-19 16:26:30 -07:00
|
|
|
? floorToDecimal(price, getDecimalCount(tickSize))
|
2022-09-13 23:24:26 -07:00
|
|
|
: new Decimal(price)
|
2022-09-26 12:56:06 -07:00
|
|
|
}, [price, tickSize])
|
2022-09-13 23:24:26 -07:00
|
|
|
|
2022-09-26 12:56:06 -07:00
|
|
|
const handlePriceClick = useCallback(() => {
|
|
|
|
const set = mangoStore.getState().set
|
|
|
|
set((state) => {
|
|
|
|
state.tradeForm.price = formattedPrice.toFixed()
|
2023-07-01 03:28:31 -07:00
|
|
|
state.tradeForm.tradeType = 'Limit'
|
|
|
|
if (state.tradeForm.baseSize) {
|
2022-12-13 19:53:06 -08:00
|
|
|
const quoteSize = floorToDecimal(
|
2023-01-12 13:02:23 -08:00
|
|
|
formattedPrice.mul(new Decimal(state.tradeForm.baseSize)),
|
2023-07-21 11:47:53 -07:00
|
|
|
getDecimalCount(tickSize),
|
2022-12-13 19:53:06 -08:00
|
|
|
)
|
|
|
|
state.tradeForm.quoteSize = quoteSize.toFixed()
|
|
|
|
}
|
2022-09-26 12:56:06 -07:00
|
|
|
})
|
2023-01-12 13:02:23 -08:00
|
|
|
}, [formattedPrice, tickSize])
|
2022-09-13 23:24:26 -07:00
|
|
|
|
2022-12-08 15:56:36 -08:00
|
|
|
const handleSizeClick = useCallback(() => {
|
|
|
|
const set = mangoStore.getState().set
|
|
|
|
set((state) => {
|
|
|
|
state.tradeForm.baseSize = formattedSize.toString()
|
2023-01-12 13:02:23 -08:00
|
|
|
if (formattedSize && state.tradeForm.price) {
|
2022-12-13 19:53:06 -08:00
|
|
|
const quoteSize = floorToDecimal(
|
2023-01-12 13:02:23 -08:00
|
|
|
formattedSize.mul(new Decimal(state.tradeForm.price)),
|
2023-07-21 11:47:53 -07:00
|
|
|
getDecimalCount(tickSize),
|
2022-12-13 19:53:06 -08:00
|
|
|
)
|
|
|
|
state.tradeForm.quoteSize = quoteSize.toString()
|
|
|
|
}
|
2022-12-08 15:56:36 -08:00
|
|
|
})
|
2023-01-12 13:02:23 -08:00
|
|
|
}, [formattedSize, tickSize])
|
2022-09-13 23:24:26 -07:00
|
|
|
|
|
|
|
const groupingDecimalCount = useMemo(
|
|
|
|
() => getDecimalCount(grouping),
|
2023-07-21 11:47:53 -07:00
|
|
|
[grouping],
|
2022-09-13 23:24:26 -07:00
|
|
|
)
|
|
|
|
const minOrderSizeDecimals = useMemo(
|
2022-09-19 16:26:30 -07:00
|
|
|
() => getDecimalCount(minOrderSize),
|
2023-07-21 11:47:53 -07:00
|
|
|
[minOrderSize],
|
2022-09-13 23:24:26 -07:00
|
|
|
)
|
|
|
|
|
2023-07-25 03:43:19 -07:00
|
|
|
const handleMouseOver = useCallback(() => {
|
|
|
|
const { set } = mangoStore.getState()
|
|
|
|
if (averagePrice && cumulativeSize && cumulativeValue) {
|
|
|
|
set((state) => {
|
|
|
|
state.orderbookTooltip = {
|
|
|
|
averagePrice,
|
|
|
|
cumulativeSize,
|
|
|
|
cumulativeValue,
|
|
|
|
side,
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}, [averagePrice, cumulativeSize, cumulativeValue])
|
|
|
|
|
|
|
|
const handleMouseLeave = useCallback(() => {
|
|
|
|
const { set } = mangoStore.getState()
|
|
|
|
set((state) => {
|
|
|
|
state.orderbookTooltip = undefined
|
|
|
|
})
|
|
|
|
}, [])
|
|
|
|
|
2022-09-19 16:26:30 -07:00
|
|
|
if (!minOrderSize) return null
|
2022-09-13 23:24:26 -07:00
|
|
|
|
|
|
|
return (
|
|
|
|
<div
|
2023-07-12 07:37:18 -07:00
|
|
|
className={`relative flex h-[20px] cursor-pointer justify-between border-b border-b-th-bkg-1 text-sm`}
|
2022-09-13 23:24:26 -07:00
|
|
|
ref={element}
|
2023-07-25 03:43:19 -07:00
|
|
|
onMouseOver={handleMouseOver}
|
|
|
|
onMouseLeave={handleMouseLeave}
|
2022-09-13 23:24:26 -07:00
|
|
|
>
|
|
|
|
<>
|
2022-12-08 15:56:36 -08:00
|
|
|
<div className="flex h-full w-full items-center justify-between text-th-fgd-3 hover:bg-th-bkg-2">
|
|
|
|
<div
|
2023-07-12 07:37:18 -07:00
|
|
|
className={`z-10 flex h-full w-full items-center pl-2 hover:underline`}
|
|
|
|
onClick={handlePriceClick}
|
|
|
|
>
|
|
|
|
<span className="w-full font-mono text-xs">
|
|
|
|
{price < SHOW_EXPONENTIAL_THRESHOLD
|
|
|
|
? formattedPrice.toExponential()
|
|
|
|
: formattedPrice.toFixed(groupingDecimalCount)}
|
|
|
|
</span>
|
|
|
|
</div>
|
|
|
|
<div
|
|
|
|
className="flex h-full w-full items-center justify-start pr-2 hover:underline"
|
2022-12-08 15:56:36 -08:00
|
|
|
onClick={handleSizeClick}
|
|
|
|
>
|
2022-09-18 16:01:57 -07:00
|
|
|
<div
|
|
|
|
style={{ fontFeatureSettings: 'zero 1' }}
|
2022-09-29 13:00:36 -07:00
|
|
|
className={`z-10 w-full text-right font-mono text-xs ${
|
2022-11-30 19:32:32 -08:00
|
|
|
hasOpenOrder ? 'text-th-active' : ''
|
2022-09-18 16:01:57 -07:00
|
|
|
}`}
|
|
|
|
>
|
2023-04-25 17:58:33 -07:00
|
|
|
{size >= 1000000
|
|
|
|
? sizeCompacter.format(size)
|
|
|
|
: formattedSize.toFixed(minOrderSizeDecimals)}
|
2022-09-18 16:01:57 -07:00
|
|
|
</div>
|
2022-09-13 23:24:26 -07:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<Line
|
2023-07-20 03:40:27 -07:00
|
|
|
className={`absolute left-0 opacity-30 brightness-125 ${
|
2022-11-30 19:32:32 -08:00
|
|
|
side === 'buy' ? `bg-th-up-muted` : `bg-th-down-muted`
|
2022-09-13 23:24:26 -07:00
|
|
|
}`}
|
2022-09-29 13:00:36 -07:00
|
|
|
data-width={Math.max(sizePercent, 0.5) + '%'}
|
|
|
|
/>
|
|
|
|
<Line
|
2023-07-20 03:40:27 -07:00
|
|
|
className={`absolute left-0 opacity-40 ${
|
2022-11-30 19:32:32 -08:00
|
|
|
side === 'buy' ? `bg-th-up` : `bg-th-down`
|
2022-09-29 13:00:36 -07:00
|
|
|
}`}
|
2022-09-29 13:06:04 -07:00
|
|
|
data-width={
|
|
|
|
Math.max((cumulativeSizePercent / 100) * sizePercent, 0.1) + '%'
|
|
|
|
}
|
2022-09-13 23:24:26 -07:00
|
|
|
/>
|
|
|
|
</>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-09-19 16:26:30 -07:00
|
|
|
const MemoizedOrderbookRow = React.memo(OrderbookRow)
|
|
|
|
|
2022-11-20 15:35:59 -08:00
|
|
|
const Line = (props: {
|
|
|
|
className: string
|
|
|
|
invert?: boolean
|
|
|
|
'data-width': string
|
|
|
|
}) => {
|
2022-09-13 23:24:26 -07:00
|
|
|
return (
|
|
|
|
<div
|
|
|
|
className={`${props.className}`}
|
|
|
|
style={{
|
|
|
|
textAlign: props.invert ? 'left' : 'right',
|
|
|
|
height: '100%',
|
|
|
|
width: `${props['data-width'] ? props['data-width'] : ''}`,
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
export default Orderbook
|