optimize rerenders

This commit is contained in:
Tyler Shipe 2021-04-07 11:44:22 -04:00
parent a0c0768473
commit 35be5f1276
13 changed files with 318 additions and 106 deletions

View File

@ -54,13 +54,10 @@ const calculatePNL = (tradeHistory, prices, mangoGroup) => {
}
export default function MarginInfo() {
// Connection hook
console.log('loading margin info')
const { connection } = useConnection()
// Wallet hook
// Get our account info
const { marginAccount, mangoGroup } = useMarginAccount()
// Working state
// Hold the margin account info
const [mAccountInfo, setMAccountInfo] = useState<
| {
label: string
@ -74,8 +71,6 @@ export default function MarginInfo() {
const { tradeHistory } = useTradeHistory()
useEffect(() => {
console.log('marginInfo useEffect')
if (mangoGroup) {
mangoGroup.getPrices(connection).then((prices) => {
const collateralRatio = marginAccount

View File

@ -19,6 +19,30 @@ const Line = styled.div<any>`
props['data-bgcolor'] && `background-color: ${props['data-bgcolor']};`}
`
function getCumulativeOrderbookSide(
orders,
totalSize,
depth,
backwards = false
) {
let cumulative = orders
.slice(0, depth)
.reduce((cumulative, [price, size], i) => {
const cumulativeSize = (cumulative[i - 1]?.cumulativeSize || 0) + size
cumulative.push({
price,
size,
cumulativeSize,
sizePercent: Math.round((cumulativeSize / (totalSize || 1)) * 100),
})
return cumulative
}, [])
if (backwards) {
cumulative = cumulative.reverse()
}
return cumulative
}
export default function Orderbook({ depth = 7 }) {
const markPrice = useMarkPrice()
const [orderbook] = useOrderbook()
@ -42,8 +66,18 @@ export default function Orderbook({ depth = 7 }) {
index < depth ? total + size : total
const totalSize = bids.reduce(sum, 0) + asks.reduce(sum, 0)
const bidsToDisplay = getCumulativeOrderbookSide(bids, totalSize, false)
const asksToDisplay = getCumulativeOrderbookSide(asks, totalSize, true)
const bidsToDisplay = getCumulativeOrderbookSide(
bids,
totalSize,
depth,
false
)
const asksToDisplay = getCumulativeOrderbookSide(
asks,
totalSize,
depth,
true
)
currentOrderbookData.current = {
bids: orderbook?.bids,
@ -61,25 +95,6 @@ export default function Orderbook({ depth = 7 }) {
}
}, [orderbook])
function getCumulativeOrderbookSide(orders, totalSize, backwards = false) {
let cumulative = orders
.slice(0, depth)
.reduce((cumulative, [price, size], i) => {
const cumulativeSize = (cumulative[i - 1]?.cumulativeSize || 0) + size
cumulative.push({
price,
size,
cumulativeSize,
sizePercent: Math.round((cumulativeSize / (totalSize || 1)) * 100),
})
return cumulative
}, [])
if (backwards) {
cumulative = cumulative.reverse()
}
return cumulative
}
return (
<>
<ElementTitle>Orderbook</ElementTitle>

View File

@ -8,20 +8,25 @@ import useMarket from '../hooks/useMarket'
import useInterval from '../hooks/useInterval'
import ChartApi from '../utils/chartDataConnector'
import { ElementTitle } from './styles'
import { isEqual } from '../utils/index'
export default function PublicTrades() {
const { baseCurrency, quoteCurrency, market, marketAddress } = useMarket()
const [trades, setTrades] = useState([])
useInterval(async () => {
const trades = await ChartApi.getRecentTrades(marketAddress)
setTrades(trades)
const newTrades = await ChartApi.getRecentTrades(marketAddress)
if (trades.length === 0) {
setTrades(newTrades)
} else if (!isEqual(newTrades[0], trades[0], Object.keys(newTrades[0]))) {
setTrades(newTrades)
}
}, 5000)
return (
<FloatingElement>
<ElementTitle>Recent Market Trades</ElementTitle>
<div css={xw`grid grid-cols-3 text-gray-500 font-light mb-2`}>
<div css={xw`grid grid-cols-3 text-gray-500 mb-2`}>
<div>Price ({quoteCurrency}) </div>
<div css={xw`text-right`}>Size ({baseCurrency})</div>
<div css={xw`text-right`}>Time</div>

View File

@ -0,0 +1,85 @@
import { useCallback, useEffect, useRef } from 'react'
import xw from 'xwind'
import { getDecimalCount } from '../utils'
import { ChartTradeType } from '../@types/types'
import FloatingElement from './FloatingElement'
import useMarket from '../hooks/useMarket'
import useInterval from '../hooks/useInterval'
import ChartApi from '../utils/chartDataConnector'
import { ElementTitle } from './styles'
import useSerumStore from '../stores/useSerumStore'
export default function PublicTrades() {
const { baseCurrency, quoteCurrency, market, marketAddress } = useMarket()
const setSerumStore = useSerumStore((s) => s.set)
const fetchTrades = useCallback(async () => {
const trades = await ChartApi.getRecentTrades(marketAddress)
console.log('trades in interval', trades)
setSerumStore((state) => {
state.chartApiTrades = trades
})
}, [marketAddress])
// fetch trades on load
useEffect(() => {
fetchTrades()
}, [])
// refresh trades on interval
useInterval(async () => {
fetchTrades()
}, 5000)
const tradesRef = useRef(useSerumStore.getState().chartApiTrades)
const trades = tradesRef.current
useEffect(
() =>
useSerumStore.subscribe(
(trades) => (tradesRef.current = trades as any[]),
(state) => state.chartApiTrades
),
[]
)
return (
<FloatingElement>
<ElementTitle>Recent Market Trades</ElementTitle>
<div css={xw`grid grid-cols-3 text-gray-500 mb-2`}>
<div>Price ({quoteCurrency}) </div>
<div css={xw`text-right`}>Size ({baseCurrency})</div>
<div css={xw`text-right`}>Time</div>
</div>
{!!trades.length && (
<div>
{trades.map((trade: ChartTradeType, i: number) => (
<div key={i} css={xw`mb-2 font-light grid grid-cols-3`}>
<div
style={{
color: trade.side === 'buy' ? '#AFD803' : '#E54033',
}}
>
{market?.tickSize && !isNaN(trade.price)
? Number(trade.price).toFixed(
getDecimalCount(market.tickSize)
)
: trade.price}
</div>
<div css={xw`text-right`}>
{market?.minOrderSize && !isNaN(trade.size)
? Number(trade.size).toFixed(
getDecimalCount(market.minOrderSize)
)
: trade.size}
</div>
<div css={xw`text-right text-gray-500`}>
{trade.time && new Date(trade.time).toLocaleTimeString()}
</div>
</div>
))}
</div>
)}
</FloatingElement>
)
}

View File

@ -36,7 +36,7 @@ export default function TradeForm({
ref: ({ size, price }: { size?: number; price?: number }) => void
) => void
}) {
// console.log('reloading trade form')
console.log('reloading trade form')
const [side, setSide] = useState<'buy' | 'sell'>('buy')
const { baseCurrency, quoteCurrency, market } = useMarket()

View File

@ -1,6 +1,4 @@
import styled from '@emotion/styled'
import xw from 'xwind'
export const ElementTitle = styled.div(xw`
flex justify-center mb-4 text-lg tracking-tight
`)
export const ElementTitle = styled.div(xw`flex justify-center mb-4 text-lg`)

View File

@ -4,10 +4,12 @@ import { IDS } from '@blockworks-foundation/mango-client'
import useMangoStore from '../stores/useMangoStore'
const useConnection = () => {
// console.log('loading useConnection')
const setSolanaStore = useMangoStore((s) => s.set)
const { cluster, current: connection, endpoint } = useMangoStore(
(s) => s.connection
)
const setSolanaStore = useMangoStore((s) => s.set)
const sendConnection = useMemo(() => new Connection(endpoint, 'recent'), [
endpoint,
@ -29,17 +31,17 @@ const useConnection = () => {
return () => {
connection.removeAccountChangeListener(id)
}
}, [connection])
}, [endpoint])
useEffect(() => {
const id = connection.onSlotChange(() => null)
return () => {
connection.removeSlotChangeListener(id)
}
}, [connection])
}, [endpoint])
const programId = IDS[cluster].mango_program_id
const dexProgramId = IDS[cluster]?.dex_program_id
const programId = useMemo(() => IDS[cluster].mango_program_id, [cluster])
const dexProgramId = useMemo(() => IDS[cluster]?.dex_program_id, [cluster])
return { connection, dexProgramId, cluster, programId, sendConnection }
}

View File

@ -1,49 +1,168 @@
import { useEffect } from 'react'
import { Market } from '@project-serum/serum'
import { PublicKey, AccountInfo } from '@solana/web3.js'
import useConnection from './useConnection'
import useMangoStore from '../stores/useMangoStore'
import { PublicKey } from '@solana/web3.js'
import useMarketList from './useMarketList'
// function useOrderBookSubscribe(market) {
// const setMangoStore = useMangoStore((s) => s.set)
// const { connection } = useConnection()
// const accountPkAsString = account ? account.toString() : null
// useInterval(async () => {
// if (!account) return
// const info = await connection.getAccountInfo(account)
// console.log('fetching account info on interval', accountPkAsString)
// setMangoStore((state) => {
// state.accountInfos[accountPkAsString] = info
// })
// }, 60000)
// useEffect(() => {
// if (!account) return
// let previousInfo: AccountInfo<Buffer> | null = null
// const subscriptionId = connection.onAccountChange(account, (info) => {
// if (
// !previousInfo ||
// !previousInfo.data.equals(info.data) ||
// previousInfo.lamports !== info.lamports
// ) {
// previousInfo = info
// setMangoStore((state) => {
// state.accountInfos[accountPkAsString] = previousInfo
// })
// }
// })
// return () => {
// connection.removeAccountChangeListener(subscriptionId)
// }
// }, [account, connection])
// }
const marketAddressSelector = (s) => s.selectedMarket.address
const mangoGroupMarketsSelector = (s) => s.selectedMangoGroup.markets
const useHydrateStore = () => {
const setMangoStore = useMangoStore((s) => s.set)
const selectedMarket = useMangoStore((s) => s.selectedMarket)
const selectedMarketAddress = useMangoStore(marketAddressSelector)
const marketsForSelectedMangoGroup = useMangoStore(mangoGroupMarketsSelector)
const { connection, dexProgramId } = useConnection()
const { marketList } = useMarketList()
// load selected market
useEffect(() => {
console.log(
'useEffect loading market',
selectedMarket.address,
dexProgramId
)
console.log('useEffect loading market', selectedMarketAddress, dexProgramId)
Market.load(
connection,
new PublicKey(selectedMarket.address),
new PublicKey(selectedMarketAddress),
{},
new PublicKey(dexProgramId)
)
.then((market) => {
.then(async (market) => {
// @ts-ignore
const bidAcccount = market._decoded.bids
const bidInfo = await connection.getAccountInfo(bidAcccount)
// @ts-ignore
const askAccount = market._decoded.asks
const askInfo = await connection.getAccountInfo(askAccount)
setMangoStore((state) => {
state.market.current = market
// @ts-ignore
state.accountInfos[market._decoded.bids.toString()] = null
// @ts-ignore
state.accountInfos[market._decoded.asks.toString()] = null
state.accountInfos[askAccount.toString()] = askInfo
state.accountInfos[bidAcccount.toString()] = bidInfo
})
})
.catch(
(e) => {
console.error('failed to load market', e)
}
// TODO
// notify({
// message: 'Error loading market',
// description: e.message,
// type: 'error',
// }),
)
}, [selectedMarket])
}, [selectedMarketAddress])
return null
// load all markets for mangoGroup
useEffect(() => {
console.log('loading all markets for mangoGroup')
Promise.all(
marketList.map((mkt) => {
return Market.load(connection, mkt.address, {}, mkt.programId)
})
).then((markets) => {
setMangoStore((state) => {
markets.forEach((market) => {
state.selectedMangoGroup.markets[market.publicKey.toString()] = market
// @ts-ignore
state.accountInfos[market._decoded.bids.toString()] = null
// @ts-ignore
state.accountInfos[market._decoded.asks.toString()] = null
})
})
})
}, [marketList])
// hydrate orderbook for all markets in mango group
useEffect(() => {
const subscriptionIds = Object.entries(marketsForSelectedMangoGroup).map(
([, market]) => {
let previousBidInfo: AccountInfo<Buffer> | null = null
let previousAskInfo: AccountInfo<Buffer> | null = null
return [
connection.onAccountChange(
// @ts-ignore
market._decoded.bids,
(info) => {
if (
!previousBidInfo ||
!previousBidInfo.data.equals(info.data) ||
previousBidInfo.lamports !== info.lamports
) {
previousBidInfo = info
setMangoStore((state) => {
// @ts-ignore
const pkString = market._decoded.bids.toString()
state.accountInfos[pkString] = previousBidInfo
})
}
}
),
connection.onAccountChange(
// @ts-ignore
market._decoded.asks,
(info) => {
if (
!previousAskInfo ||
!previousAskInfo.data.equals(info.data) ||
previousAskInfo.lamports !== info.lamports
) {
previousAskInfo = info
setMangoStore((state) => {
// @ts-ignore
const pkString = market._decoded.bids.toString()
state.accountInfos[pkString] = previousAskInfo
})
}
}
),
]
}
)
console.log('subscription ids', subscriptionIds)
return () => {
for (const id of subscriptionIds.flat()) {
connection.removeAccountChangeListener(id)
}
}
}, [marketsForSelectedMangoGroup])
}
export default useHydrateStore

View File

@ -3,7 +3,7 @@ import { PublicKey } from '@solana/web3.js'
import { IDS } from '@blockworks-foundation/mango-client'
import useMangoStore from '../stores/useMangoStore'
import useConnection from './useConnection'
// import useInterval from './useInterval'
import useInterval from './useInterval'
import useWallet from './useWallet'
const useMarginAccount = () => {
@ -46,7 +46,6 @@ const useMarginAccount = () => {
const fetchMarginAccounts = useCallback(() => {
if (!mangoClient || !mangoGroup || !connected || !wallet.publicKey) return
console.log('fetching margin accounts from: ', wallet.publicKey.toString())
mangoClient
.getMarginAccountsForOwner(
@ -80,10 +79,10 @@ const useMarginAccount = () => {
fetchMarginAccounts()
}, [connected, fetchMarginAccounts])
// useInterval(() => {
// fetchMarginAccounts()
// fetchMangoGroup()
// }, 20000)
useInterval(() => {
fetchMarginAccounts()
// fetchMangoGroup()
}, 2000)
return {
mangoClient,

32
hooks/useOpenOrders.tsx Normal file
View File

@ -0,0 +1,32 @@
import { useEffect, useRef } from 'react'
import useConnection from './useConnection'
import useMarket from './useMarket'
import useMarginAccount from './useMarginAcccount'
export function useOpenOrders() {
const { market } = useMarket()
const { marginAccount, mangoGroup } = useMarginAccount()
const { bidOrderbook, askOrderbook } = useOrderbookAccounts()
const { cluster } = useConnection()
if (!market || !mangoGroup || !marginAccount) return null
const marketIndex = mangoGroup.getMarketIndex(market)
const openOrdersAccount = marginAccount.openOrdersAccounts[marketIndex]
if (!openOrdersAccount || !bidOrderbook || !askOrderbook) {
return null
}
const spotMarketFromIDs = Object.entries(IDS[cluster].spot_markets).find(
([symbol, address]) => {
return market.address.toString() === address
}
)
const marketName = spotMarketFromIDs ? spotMarketFromIDs[0] : ''
return market
.filterForOpenOrders(bidOrderbook, askOrderbook, [openOrdersAccount])
.map((order) => ({ ...order, marketName, market }))
}

View File

@ -1,53 +1,9 @@
import { useEffect, useMemo } from 'react'
import { PublicKey, AccountInfo } from '@solana/web3.js'
import { Orderbook } from '@project-serum/serum'
import useMarket from './useMarket'
import useInterval from './useInterval'
import useMangoStore from '../stores/useMangoStore'
import useConnection from './useConnection'
function useAccountInfo(account: PublicKey) {
const setSolanaStore = useMangoStore((s) => s.set)
const { connection } = useConnection()
const accountPkAsString = account ? account.toString() : null
useInterval(async () => {
if (!account) return
const info = await connection.getAccountInfo(account)
console.log('fetching account info on interval', accountPkAsString)
setSolanaStore((state) => {
state.accountInfos[accountPkAsString] = info
})
}, 60000)
useEffect(() => {
if (!account) return
let previousInfo: AccountInfo<Buffer> | null = null
const subscriptionId = connection.onAccountChange(account, (info) => {
if (
!previousInfo ||
!previousInfo.data.equals(info.data) ||
previousInfo.lamports !== info.lamports
) {
previousInfo = info
setSolanaStore((state) => {
state.accountInfos[accountPkAsString] = previousInfo
})
}
})
return () => {
connection.removeAccountChangeListener(subscriptionId)
}
}, [account, connection])
}
export function useAccountData(publicKey) {
useAccountInfo(publicKey)
const account = publicKey ? publicKey.toString() : null
const accountInfo = useMangoStore((s) => s.accountInfos[account])
return accountInfo && Buffer.from(accountInfo.data)

View File

@ -56,6 +56,9 @@ interface MangoStore extends State {
selectedMangoGroup: {
name: string
current: MangoGroup | null
markets: {
[key: string]: Market
}
}
selectedMarginAccount: {
current: MarginAccount | null
@ -82,6 +85,7 @@ const useMangoStore = create<MangoStore>(
selectedMangoGroup: {
name: DEFAULT_MANGO_GROUP,
current: null,
markets: {},
},
selectedMarket: {
name: Object.entries(

View File

@ -9,6 +9,7 @@ interface SerumStore extends State {
asks: any[]
}
fills: any[]
chartApiTrades: any[]
set: (x: any) => void
}
@ -18,6 +19,7 @@ const useSerumStore = create<SerumStore>((set) => ({
asks: [],
},
fills: [],
chartApiTrades: [], // TODO remove transient updates
set: (fn) => set(produce(fn)),
}))