improve typing to fix errors with recent trades and history

This commit is contained in:
tjs 2023-02-23 16:44:54 -05:00
parent f80144640f
commit 09852aa007
11 changed files with 200 additions and 169 deletions

View File

@ -12,6 +12,7 @@
"@next/next/no-img-element": 0,
"react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
"react-hooks/exhaustive-deps": "warn", // Checks effect dependencies
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": [
2,
{

View File

@ -8,16 +8,12 @@ export const socketUrl = `wss://public-api.birdeye.so/socket?x-api-key=${NEXT_PU
// Make requests to CryptoCompare API
export async function makeApiRequest(path: string) {
try {
const response = await fetch(`${API_URL}${path}`, {
headers: {
'X-API-KEY': NEXT_PUBLIC_BIRDEYE_API_KEY,
},
})
return response.json()
} catch (error: any) {
throw new Error(`CryptoCompare request error: ${error.status}`)
}
const response = await fetch(`${API_URL}${path}`, {
headers: {
'X-API-KEY': NEXT_PUBLIC_BIRDEYE_API_KEY,
},
})
return response.json()
}
const RESOLUTION_MAPPING: Record<string, string> = {

View File

@ -0,0 +1,42 @@
import React, { Component, ErrorInfo, ReactNode } from 'react'
interface Props {
children?: ReactNode
}
interface State {
error: any
hasError: boolean
}
class ErrorBoundary extends Component<Props, State> {
public state: State = {
error: null,
hasError: false,
}
public static getDerivedStateFromError(e: Error): State {
// Update state so the next render will show the fallback UI.
return { hasError: true, error: e }
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Uncaught error:', error, errorInfo)
}
public render() {
const error = this.state.error
if (this.state.hasError) {
return (
<div>
<p>Error. Please refresh</p>
<div>{`${error}`}</div>
</div>
)
}
return this.props.children
}
}
export default ErrorBoundary

View File

@ -1,16 +1,15 @@
import React, { FunctionComponent } from 'react'
import { useTranslation } from 'next-i18next'
import { PerpOrderSide } from '@blockworks-foundation/mango-v4'
type SideBadgeProps = {
side: string | PerpOrderSide
side: string
}
const SideBadge: FunctionComponent<SideBadgeProps> = ({ side }) => {
const { t } = useTranslation('common')
if (side !== 'buy' && side !== 'sell') {
return <div>Unknown</div>
}
const isBid =
typeof side === 'string' ? ['buy', 'long'].includes(side) : 'bid' in side
const isBid = side === 'buy'
return (
<div
@ -21,7 +20,7 @@ const SideBadge: FunctionComponent<SideBadgeProps> = ({ side }) => {
}
uppercase md:-my-0.5 md:px-1.5 md:py-0.5 md:text-xs`}
>
{typeof side === 'string' ? t(side) : 'bid' in side ? 'Long' : 'Short'}
{isBid ? 'Buy' : 'Sell'}
</div>
)
}

View File

@ -254,6 +254,7 @@ const OpenOrders = () => {
let tickSize: number
let minOrderSize: number
let expiryTimestamp: number | undefined
let side: string
if (o instanceof PerpOrder) {
market = group.getPerpMarketByMarketIndex(o.perpMarketIndex)
tickSize = market.tickSize
@ -262,6 +263,7 @@ const OpenOrders = () => {
o.expiryTimestamp === U64_MAX_BN
? 0
: o.expiryTimestamp.toNumber()
side = 'bid' in o.side ? 'buy' : 'sell'
} else {
market = group.getSerum3MarketByExternalMarket(
new PublicKey(marketPk)
@ -271,6 +273,7 @@ const OpenOrders = () => {
)
tickSize = serumMarket.tickSize
minOrderSize = serumMarket.minOrderSize
side = o.side
}
return (
<TrBody
@ -281,7 +284,7 @@ const OpenOrders = () => {
<TableMarketName market={market} />
</Td>
<Td className="w-[16.67%] text-right">
<SideBadge side={o.side} />
<SideBadge side={side} />
</Td>
{modifyOrderId !== o.orderId.toString() ? (
<>
@ -398,10 +401,12 @@ const OpenOrders = () => {
let market: PerpMarket | Serum3Market
let tickSize: number
let minOrderSize: number
let side: string
if (o instanceof PerpOrder) {
market = group.getPerpMarketByMarketIndex(o.perpMarketIndex)
tickSize = market.tickSize
minOrderSize = market.minOrderSize
side = 'bid' in o.side ? 'buy' : 'sell'
} else {
market = group.getSerum3MarketByExternalMarket(
new PublicKey(marketPk)
@ -411,6 +416,7 @@ const OpenOrders = () => {
)
tickSize = serumMarket.tickSize
minOrderSize = serumMarket.minOrderSize
side = o.side
}
return (
<div
@ -421,7 +427,7 @@ const OpenOrders = () => {
<TableMarketName market={market} />
{modifyOrderId !== o.orderId.toString() ? (
<div className="mt-1 flex items-center space-x-1">
<SideBadge side={o.side} />
<SideBadge side={side} />
<p className="text-th-fgd-4">
<span className="font-mono text-th-fgd-3">
<FormatNumericValue

View File

@ -2,7 +2,6 @@ import useInterval from '@components/shared/useInterval'
import mangoStore from '@store/mangoStore'
import { useEffect, useMemo, useState } from 'react'
import { formatNumericValue, getDecimalCount } from 'utils/numbers'
import { ChartTradeType } from 'types'
import { useTranslation } from 'next-i18next'
import useSelectedMarket from 'hooks/useSelectedMarket'
import { Howl } from 'howler'
@ -16,13 +15,16 @@ import TradeVolumeAlertModal, {
DEFAULT_VOLUME_ALERT_SETTINGS,
} from '@components/modals/TradeVolumeAlertModal'
import dayjs from 'dayjs'
import { PerpMarket } from '@blockworks-foundation/mango-v4'
import { isPerpFillEvent } from './TradeHistory'
import ErrorBoundary from '@components/ErrorBoundary'
const volumeAlertSound = new Howl({
src: ['/sounds/trade-buy.mp3'],
volume: 0.8,
})
type Test = { buys: number; sells: number }
const RecentTrades = () => {
const { t } = useTranslation(['common', 'trade'])
const fills = mangoStore((s) => s.selectedMarket.fills)
@ -49,56 +51,35 @@ const RecentTrades = () => {
if (!fills.length) return
const latesetFill = fills[0]
if (!latestFillId) {
const fillId =
selectedMarket instanceof PerpMarket
? latesetFill.takerClientOrderId
: latesetFill.orderId
const fillId = isPerpFillEvent(latesetFill)
? latesetFill.takerClientOrderId
: latesetFill.orderId
setLatestFillId(fillId.toString())
}
}, [fills])
useInterval(() => {
if (!soundSettings['recent-trades'] || !quoteBank || !fills.length) return
const latesetFill = fills[0]
const fillId =
selectedMarket instanceof PerpMarket
? latesetFill.takerClientOrderId
: latesetFill.orderId
if (!soundSettings['recent-trades'] || !quoteBank || !latesetFill) return
const fillId = isPerpFillEvent(latesetFill)
? latesetFill.takerClientOrderId
: latesetFill.orderId
setLatestFillId(fillId.toString())
const fillsLimitIndex = fills.findIndex((f) => {
const id =
selectedMarket instanceof PerpMarket ? f.takerClientOrderId : f.orderId
const id = isPerpFillEvent(f) ? f.takerClientOrderId : f.orderId
return id.toString() === fillId.toString()
})
const newFillsVolumeValue = fills
.slice(0, fillsLimitIndex)
.reduce((a, c) => a + c.size * c.price, 0)
.reduce((a, c) => {
const size = isPerpFillEvent(c) ? c.quantity : c.size
return a + size * c.price
}, 0)
if (newFillsVolumeValue * quoteBank.uiPrice > Number(alertSettings.value)) {
volumeAlertSound.play()
}
}, alertSettings.seconds * 1000)
// const fetchRecentTrades = useCallback(async () => {
// if (!market) return
// try {
// const response = await fetch(
// `https://event-history-api-candles.herokuapp.com/trades/address/${market.publicKey}`
// )
// const parsedResp = await response.json()
// const newTrades = parsedResp.data
// if (!newTrades) return null
// if (newTrades.length && trades.length === 0) {
// setTrades(newTrades)
// } else if (newTrades?.length && !isEqual(newTrades[0], trades[0])) {
// setTrades(newTrades)
// }
// } catch (e) {
// console.error('Unable to fetch recent trades', e)
// }
// }, [market, trades])
useEffect(() => {
// if (CLUSTER === 'mainnet-beta') {
// fetchRecentTrades()
@ -113,19 +94,28 @@ const RecentTrades = () => {
// }
const actions = mangoStore.getState().actions
actions.loadMarketFills()
}, 5000)
}, 6000)
const [buyRatio, sellRatio] = useMemo(() => {
if (!fills.length) return [0, 0]
const vol = fills.reduce(
(a: { buys: number; sells: number }, c: any) => {
if (c.side === 'buy' || c.takerSide === 0) {
a.buys = a.buys + c.size
(acc: Test, fill) => {
let side
let size
if (isPerpFillEvent(fill)) {
side = fill.takerSide === 0 ? 'buy' : 'sell'
size = fill.quantity
} else {
a.sells = a.sells + c.size
side = fill.side
size = fill.size
}
return a
if (side === 'buy') {
acc.buys = acc.buys + size
} else {
acc.sells = acc.sells + size
}
return acc
},
{ buys: 0, sells: 0 }
)
@ -134,7 +124,7 @@ const RecentTrades = () => {
}, [fills])
return (
<>
<ErrorBoundary>
<div className="thin-scroll h-full overflow-y-scroll">
<div className="flex items-center justify-between border-b border-th-bkg-3 py-1 px-2">
<Tooltip
@ -179,9 +169,19 @@ const RecentTrades = () => {
</thead>
<tbody>
{!!fills.length &&
fills.map((trade: ChartTradeType, i: number) => {
const side =
trade.side || (trade.takerSide === 0 ? 'bid' : 'ask')
fills.map((trade, i: number) => {
let side
let size
let time
if (isPerpFillEvent(trade)) {
side = trade.takerSide === 0 ? 'bid' : 'ask'
size = trade.quantity
time = trade.timestamp.toString()
} else {
side = trade.side
size = trade.size
time = ''
}
const formattedPrice =
market?.tickSize && trade.price
@ -192,12 +192,12 @@ const RecentTrades = () => {
: trade?.price || 0
const formattedSize =
market?.minOrderSize && trade.size
market?.minOrderSize && size
? formatNumericValue(
trade.size,
size,
getDecimalCount(market.minOrderSize)
)
: trade?.size || 0
: size || 0
return (
<tr className="font-mono text-xs" key={i}>
@ -212,12 +212,8 @@ const RecentTrades = () => {
</td>
<td className="pb-1.5 text-right">{formattedSize}</td>
<td className="pb-1.5 text-right text-th-fgd-4">
{trade.time
? new Date(trade.time).toLocaleTimeString()
: trade.timestamp
? dayjs(trade.timestamp.toNumber() * 1000).format(
'hh:mma'
)
{time
? dayjs(Number(time) * 1000).format('hh:mma')
: '-'}
</td>
</tr>
@ -233,7 +229,7 @@ const RecentTrades = () => {
onClose={() => setShowVolumeAlertModal(false)}
/>
) : null}
</>
</ErrorBoundary>
)
}

View File

@ -33,15 +33,6 @@ import { abbreviateAddress } from 'utils/formatting'
import { breakpoints } from 'utils/theme'
import TableMarketName from './TableMarketName'
const byTimestamp = (a: any, b: any) => {
return (
new Date(b.loadTimestamp || b.timestamp * 1000).getTime() -
new Date(a.loadTimestamp || a.timestamp * 1000).getTime()
)
}
const reverseSide = (side: string) => (side === 'long' ? 'short' : 'long')
type PerpFillEvent = ParsedFillEvent
const parsePerpEvent = (mangoAccountAddress: string, event: PerpFillEvent) => {
@ -49,8 +40,8 @@ const parsePerpEvent = (mangoAccountAddress: string, event: PerpFillEvent) => {
const orderId = maker ? event.makerOrderId : event.takerOrderId
const value = event.quantity * event.price
const feeRate = maker ? event.makerFee : event.takerFee
const takerSide = event.takerSide === 0 ? 'long' : 'short'
const side = maker ? reverseSide(takerSide) : takerSide
const takerSide = event.takerSide === 0 ? 'buy' : 'sell'
const side = maker ? (takerSide === 'buy' ? 'sell' : 'buy') : takerSide
return {
...event,
@ -86,15 +77,21 @@ const isApiSpotTradeHistory = (
else return false
}
const isSerumFillEvent = (
t: PerpFillEvent | SerumEvent | SpotTradeHistory | PerpTradeHistory
type CombinedTradeHistoryTypes =
| SpotTradeHistory
| PerpTradeHistory
| PerpFillEvent
| SerumEvent
export const isSerumFillEvent = (
t: CombinedTradeHistoryTypes
): t is SerumEvent => {
if ('eventFlags' in t) return true
else return false
}
const isPerpFillEvent = (
t: PerpFillEvent | SerumEvent | SpotTradeHistory | PerpTradeHistory
export const isPerpFillEvent = (
t: CombinedTradeHistoryTypes
): t is PerpFillEvent => {
if ('takerSide' in t) return true
else return false
@ -104,7 +101,7 @@ const parseApiTradeHistory = (
mangoAccountAddress: string,
trade: SpotTradeHistory | PerpTradeHistory
) => {
let side
let side: 'buy' | 'sell'
let size
let feeCost
let liquidity
@ -115,16 +112,12 @@ const parseApiTradeHistory = (
liquidity = trade.maker ? 'Maker' : 'Taker'
} else {
liquidity =
trade.taker && trade.taker.toString() === mangoAccountAddress
? 'Taker'
: 'Maker'
const sideObj: any = {}
trade.taker && trade.taker === mangoAccountAddress ? 'Taker' : 'Maker'
if (liquidity == 'Taker') {
sideObj[trade.taker_side] = 1
side = trade.taker_side == 'bid' ? 'buy' : 'sell'
} else {
sideObj[trade.taker_side == 'bid' ? 'ask' : 'bid'] = 1
side = trade.taker_side == 'bid' ? 'sell' : 'buy'
}
side = sideObj
size = trade.quantity
const feeRate =
trade.maker === mangoAccountAddress ? trade.maker_fee : trade.taker_fee
@ -140,44 +133,40 @@ const parseApiTradeHistory = (
}
}
type CombinedTradeHistoryTypes =
| PerpFillEvent
| SerumEvent
| SpotTradeHistory
| PerpTradeHistory
const formatTradeHistory = (
group: Group,
selectedMarket: Serum3Market | PerpMarket | undefined,
selectedMarket: Serum3Market | PerpMarket,
mangoAccountAddress: string,
tradeHistory: Array<CombinedTradeHistoryTypes>
) => {
return tradeHistory
.flat()
.map((event) => {
let trade
if (isSerumFillEvent(event)) {
trade = parseSerumEvent(event)
} else if (isPerpFillEvent(event)) {
trade = parsePerpEvent(mangoAccountAddress, event)
} else {
trade = parseApiTradeHistory(mangoAccountAddress, event)
}
let market
return tradeHistory.flat().map((event) => {
let trade
let market = selectedMarket
let time = 'Recent'
if (isSerumFillEvent(event)) {
trade = parseSerumEvent(event)
} else if (isPerpFillEvent(event)) {
trade = parsePerpEvent(mangoAccountAddress, event)
market = selectedMarket
time = trade.timestamp.toString()
} else {
trade = parseApiTradeHistory(mangoAccountAddress, event)
time = trade.block_datetime
if ('market' in trade) {
market = group.getSerum3MarketByExternalMarket(
new PublicKey(trade.market)
)
} else if ('perp_market' in trade) {
market = group.getPerpMarketByMarketIndex(trade.market_index)
} else {
market = selectedMarket
}
}
return { ...trade, market }
})
.sort(byTimestamp)
return {
...trade,
market,
time,
}
})
}
const TradeHistory = () => {
@ -187,7 +176,9 @@ const TradeHistory = () => {
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
const actions = mangoStore((s) => s.actions)
const fills = mangoStore((s) => s.selectedMarket.fills)
const tradeHistory = mangoStore((s) => s.mangoAccount.tradeHistory.data)
const tradeHistoryFromApi = mangoStore(
(s) => s.mangoAccount.tradeHistory.data
)
const loadingTradeHistory = mangoStore(
(s) => s.mangoAccount.tradeHistory.loading
)
@ -213,14 +204,14 @@ const TradeHistory = () => {
}
}, [mangoAccount, selectedMarket])
const eventQueueFillsForAccount = useMemo(() => {
const eventQueueFillsForOwner = useMemo(() => {
if (!selectedMarket || !openOrderOwner) return []
return fills.filter((fill: any) => {
if (fill.openOrders) {
return fills.filter((fill) => {
if (isSerumFillEvent(fill)) {
// handles serum event queue for spot trades
return openOrderOwner ? fill.openOrders.equals(openOrderOwner) : false
} else if (fill.taker) {
} else if (isPerpFillEvent(fill)) {
// handles mango event queue for perp trades
return (
fill.taker.equals(openOrderOwner) || fill.maker.equals(openOrderOwner)
@ -231,27 +222,27 @@ const TradeHistory = () => {
const combinedTradeHistory = useMemo(() => {
const group = mangoStore.getState().group
if (!group) return []
let newFills = []
if (eventQueueFillsForAccount?.length) {
newFills = eventQueueFillsForAccount.filter((fill) => {
return !tradeHistory.find((t) => {
if ('order_id' in t) {
return t.order_id === fill.orderId?.toString()
} else {
return t.seq_num === fill.seqNum?.toNumber()
if (!group || !selectedMarket) return []
let newFills: (SerumEvent | PerpFillEvent)[] = []
if (eventQueueFillsForOwner?.length) {
newFills = eventQueueFillsForOwner.filter((fill) => {
return !tradeHistoryFromApi.find((t) => {
if ('order_id' in t && isSerumFillEvent(fill)) {
return t.order_id === fill.orderId.toString()
} else if ('seq_num' in t && isPerpFillEvent(fill)) {
return t.seq_num === fill.seqNum.toNumber()
}
})
})
}
return formatTradeHistory(group, selectedMarket, mangoAccountAddress, [
...newFills,
...tradeHistory,
...tradeHistoryFromApi,
])
}, [
eventQueueFillsForAccount,
eventQueueFillsForOwner,
mangoAccountAddress,
tradeHistory,
tradeHistoryFromApi,
selectedMarket,
])
@ -285,10 +276,10 @@ const TradeHistory = () => {
</TrHead>
</thead>
<tbody>
{combinedTradeHistory.map((trade: any, index: number) => {
{combinedTradeHistory.map((trade, index: number) => {
return (
<TrBody
key={`${trade.signature || trade.marketIndex}${index}`}
key={`${trade.side}${trade.size}${trade.price}${trade.time}${index}`}
className="my-1 p-2"
>
<Td className="">
@ -317,17 +308,14 @@ const TradeHistory = () => {
</p>
</Td>
<Td className="whitespace-nowrap text-right">
{trade.block_datetime ? (
<TableDateDisplay
date={trade.block_datetime}
showSeconds
/>
{trade.time ? (
<TableDateDisplay date={trade.time} showSeconds />
) : (
'Recent'
)}
</Td>
<Td className="xl:!pl-0">
{trade.market.name.includes('PERP') ? (
{'taker' in trade ? (
<div className="flex justify-end">
<Tooltip
content={`View Counterparty ${abbreviateAddress(
@ -363,11 +351,11 @@ const TradeHistory = () => {
</div>
) : (
<div>
{combinedTradeHistory.map((trade: any, index: number) => {
{combinedTradeHistory.map((trade, index: number) => {
return (
<div
className="flex items-center justify-between border-b border-th-bkg-3 p-4"
key={`${trade.marketIndex}${index}`}
key={`${trade.price}${trade.size}${trade.side}${trade.time}${index}`}
>
<div>
<TableMarketName market={selectedMarket} />
@ -387,11 +375,8 @@ const TradeHistory = () => {
<div className="flex items-center space-x-2.5">
<div className="flex flex-col items-end">
<span className="mb-0.5 flex items-center space-x-1.5">
{trade.block_datetime ? (
<TableDateDisplay
date={trade.block_datetime}
showSeconds
/>
{trade.time ? (
<TableDateDisplay date={trade.time} showSeconds />
) : (
'Recent'
)}
@ -404,7 +389,7 @@ const TradeHistory = () => {
/>
</p>
</div>
{trade.market.name.includes('PERP') ? (
{'taker' in trade ? (
<a
className=""
target="_blank"

View File

@ -18,7 +18,7 @@
"postinstall": "tar -xzC public -f vendor/charting_library.tgz;tar -xzC public -f vendor/datafeeds.tgz"
},
"dependencies": {
"@blockworks-foundation/mango-v4": "^0.5.22",
"@blockworks-foundation/mango-v4": "^0.5.23",
"@headlessui/react": "1.6.6",
"@heroicons/react": "2.0.10",
"@project-serum/anchor": "0.25.0",

View File

@ -17,7 +17,7 @@ import {
PerpOrder,
PerpPosition,
BookSide,
FillEvent,
ParsedFillEvent,
} from '@blockworks-foundation/mango-v4'
import EmptyWallet from '../utils/wallet'
@ -42,6 +42,7 @@ import {
import {
OrderbookL2,
PerpTradeHistory,
SerumEvent,
SpotBalances,
SpotTradeHistory,
} from 'types'
@ -300,7 +301,7 @@ export type MangoStore = {
selectedMarket: {
name: string
current: Serum3Market | PerpMarket | undefined
fills: (FillEvent | any)[]
fills: (ParsedFillEvent | SerumEvent)[]
bidsAccount: BookSide | Orderbook | undefined
asksAccount: BookSide | Orderbook | undefined
orderbook: OrderbookL2
@ -1087,13 +1088,18 @@ const mangoStore = create<MangoStore>()(
perpMarket = selectedMarket
}
let loadedFills: any[] = []
let loadedFills: (ParsedFillEvent | SerumEvent)[] = []
if (serumMarket) {
loadedFills = await serumMarket.loadFills(connection, 10000)
loadedFills = loadedFills.filter((f) => !f?.eventFlags?.maker)
const serumFills = (await serumMarket.loadFills(
connection,
10000
)) as SerumEvent[]
loadedFills = serumFills.filter((f) => !f?.eventFlags?.maker)
} else if (perpMarket) {
loadedFills = await perpMarket.loadFills(client)
loadedFills = loadedFills.reverse()
const perpFills = (await perpMarket.loadFills(
client
)) as unknown as ParsedFillEvent[]
loadedFills = perpFills.reverse()
}
set((state) => {
state.selectedMarket.fills = loadedFills

View File

@ -42,7 +42,7 @@ export interface SpotTradeHistory {
instruction_num: number
size: number
price: number
side: string
side: 'buy' | 'sell'
fee_cost: number
open_orders_owner: string
base_symbol: string
@ -60,7 +60,7 @@ export interface PerpTradeHistory {
taker_order_id: string
taker_client_order_id: string
taker_fee: number
taker_side: string
taker_side: 'bid' | 'ask'
perp_market: string
market_index: number
price: number

View File

@ -29,10 +29,10 @@
dependencies:
regenerator-runtime "^0.13.11"
"@blockworks-foundation/mango-v4@^0.5.22":
version "0.5.22"
resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.5.22.tgz#fb7f39ae1c57d117583e080a150695fb3fcdae8c"
integrity sha512-r043k5NHKZ0uAlNqcMny5ZSjBmBhnrBf2lRqBuruwFZ0nJ0WXLXO9EZB5L3Ymb2fY81ldUcxBSznU5QljQqEPQ==
"@blockworks-foundation/mango-v4@^0.5.23":
version "0.5.23"
resolved "https://registry.yarnpkg.com/@blockworks-foundation/mango-v4/-/mango-v4-0.5.23.tgz#f0216c75ce1e10d0187a73265134f42fb94f8c9a"
integrity sha512-HUqYzNMoCd6M3Esm9dB2p4MYbFWgy3KSvnFrRFi0xFOQ4ncc4UA5sLxp1ah5I3lvNVgs8mEmnNh8JQ/cPtrZRg==
dependencies:
"@coral-xyz/anchor" "^0.26.0"
"@project-serum/serum" "0.13.65"