mango-ui-v3/hooks/useHydrateStore.tsx

258 lines
7.6 KiB
TypeScript

import { useEffect } from 'react'
import { AccountInfo, PublicKey } from '@solana/web3.js'
import useMangoStore, { programId, SECONDS } from '../stores/useMangoStore'
import useInterval from './useInterval'
import { Market, Orderbook as SpotOrderBook } from '@project-serum/serum'
import {
BookSide,
BookSideLayout,
MangoAccountLayout,
PerpMarket,
ReferrerMemory,
ReferrerMemoryLayout,
} from '@blockworks-foundation/mango-client'
import {
actionsSelector,
connectionSelector,
mangoAccountSelector,
marketConfigSelector,
marketSelector,
marketsSelector,
} from '../stores/selectors'
function decodeBookL2(market, accInfo: AccountInfo<Buffer>): number[][] {
if (market && accInfo?.data) {
const depth = 40
if (market instanceof Market) {
const book = SpotOrderBook.decode(market, accInfo.data)
return book.getL2(depth).map(([price, size]) => [price, size])
} else if (market instanceof PerpMarket) {
// FIXME: Review the null being passed here
const book = new BookSide(
// @ts-ignore
null,
market,
BookSideLayout.decode(accInfo.data)
)
return book.getL2Ui(depth)
}
}
return []
}
export function decodeBook(
market,
accInfo: AccountInfo<Buffer>
): BookSide | SpotOrderBook | undefined {
if (market && accInfo?.data) {
if (market instanceof Market) {
return SpotOrderBook.decode(market, accInfo.data)
} else if (market instanceof PerpMarket) {
// FIXME: Review the null being passed here
// @ts-ignore
return new BookSide(null, market, BookSideLayout.decode(accInfo.data))
}
}
}
const useHydrateStore = () => {
const setMangoStore = useMangoStore((s) => s.set)
const actions = useMangoStore(actionsSelector)
const markets = useMangoStore(marketsSelector)
const marketConfig = useMangoStore(marketConfigSelector)
const selectedMarket = useMangoStore(marketSelector)
const connection = useMangoStore(connectionSelector)
const mangoAccount = useMangoStore(mangoAccountSelector)
// Fetches mango group as soon as page loads
useEffect(() => {
actions.fetchMangoGroup()
actions.fetchMarketsInfo()
}, [actions])
useInterval(() => {
actions.fetchMangoGroupCache()
}, 12 * SECONDS)
useInterval(() => {
if (mangoAccount) {
actions.reloadOrders()
}
}, 20 * SECONDS)
useInterval(() => {
if (mangoAccount) {
actions.reloadMangoAccount()
actions.fetchTradeHistory()
actions.updateOpenOrders()
}
}, 90 * SECONDS)
useInterval(() => {
actions.fetchMangoGroup()
actions.fetchWalletTokens()
actions.fetchMarketsInfo()
}, 120 * SECONDS)
useEffect(() => {
if (!marketConfig || !markets) return
const market = markets[marketConfig.publicKey.toString()]
if (!market) return
setMangoStore((state) => {
state.selectedMarket.current = market
state.selectedMarket.orderBook.bids = decodeBookL2(
market,
state.accountInfos[marketConfig.bidsKey.toString()]
)
state.selectedMarket.orderBook.asks = decodeBookL2(
market,
state.accountInfos[marketConfig.asksKey.toString()]
)
})
}, [marketConfig, markets, setMangoStore])
// watch selected Mango Account for changes
useEffect(() => {
if (!mangoAccount) return
const subscriptionId = connection.onAccountChange(
mangoAccount.publicKey,
(info, context) => {
if (info?.lamports === 0) return
const lastSeenSlot =
useMangoStore.getState().selectedMangoAccount.lastSlot
const mangoAccountLastUpdated = new Date(
useMangoStore.getState().selectedMangoAccount.lastUpdatedAt
)
const newUpdatedAt = new Date()
const timeDiff =
mangoAccountLastUpdated.getTime() - newUpdatedAt.getTime()
// only updated mango account if it's been more than 1 second since last update
if (Math.abs(timeDiff / 1000) >= 1 && context.slot > lastSeenSlot) {
const decodedMangoAccount = MangoAccountLayout.decode(info?.data)
const newMangoAccount = Object.assign(
mangoAccount,
decodedMangoAccount
)
setMangoStore((state) => {
state.selectedMangoAccount.lastSlot = context.slot
state.selectedMangoAccount.current = newMangoAccount
state.selectedMangoAccount.lastUpdatedAt =
newUpdatedAt.toISOString()
})
}
}
)
return () => {
connection.removeAccountChangeListener(subscriptionId)
}
}, [mangoAccount])
// fetch referrer for selected Mango Account
useEffect(() => {
if (mangoAccount) {
const fetchReferrer = async () => {
try {
const [referrerMemoryPk] = await PublicKey.findProgramAddress(
[
mangoAccount.publicKey.toBytes(),
new Buffer('ReferrerMemory', 'utf-8'),
],
programId
)
const info = await connection.getAccountInfo(referrerMemoryPk)
if (info) {
const decodedReferrer = ReferrerMemoryLayout.decode(info.data)
const referrerMemory = new ReferrerMemory(decodedReferrer)
setMangoStore((state) => {
state.referrerPk = referrerMemory.referrerMangoAccount
})
}
} catch (e) {
console.error('Unable to fetch referrer', e)
}
}
fetchReferrer()
}
}, [mangoAccount])
// hydrate orderbook with all markets in mango group
useEffect(() => {
let previousBidInfo: AccountInfo<Buffer> | null = null
let previousAskInfo: AccountInfo<Buffer> | null = null
if (!marketConfig || !selectedMarket) return
console.log('in orderbook WS useEffect')
const bidSubscriptionId = connection.onAccountChange(
marketConfig.bidsKey,
(info, context) => {
const lastSlot = useMangoStore.getState().connection.slot
if (
(!previousBidInfo ||
!previousBidInfo.data.equals(info.data) ||
previousBidInfo.lamports !== info.lamports) &&
context.slot > lastSlot
) {
previousBidInfo = info
info['parsed'] = decodeBook(selectedMarket, info)
setMangoStore((state) => {
state.accountInfos[marketConfig.bidsKey.toString()] = info
state.selectedMarket.orderBook.bids = decodeBookL2(
selectedMarket,
info
)
})
}
}
)
const askSubscriptionId = connection.onAccountChange(
marketConfig.asksKey,
(info, context) => {
const lastSlot = useMangoStore.getState().connection.slot
if (
(!previousAskInfo ||
!previousAskInfo.data.equals(info.data) ||
previousAskInfo.lamports !== info.lamports) &&
context.slot > lastSlot
) {
previousAskInfo = info
info['parsed'] = decodeBook(selectedMarket, info)
setMangoStore((state) => {
state.accountInfos[marketConfig.asksKey.toString()] = info
state.selectedMarket.orderBook.asks = decodeBookL2(
selectedMarket,
info
)
})
}
}
)
return () => {
connection.removeAccountChangeListener(bidSubscriptionId)
connection.removeAccountChangeListener(askSubscriptionId)
}
}, [marketConfig, selectedMarket, connection, setMangoStore])
// fetch filled trades for selected market
useInterval(() => {
actions.loadMarketFills()
}, 20 * SECONDS)
useEffect(() => {
actions.loadMarketFills()
}, [selectedMarket])
}
export default useHydrateStore