mango-ui-v3/hooks/useHydrateStore.tsx

283 lines
7.4 KiB
TypeScript

import { useEffect } from 'react'
import { RBTree } from 'bintrees'
import { AccountInfo, PublicKey } from '@solana/web3.js'
import useMangoStore, {
Orderbook,
programId,
SECONDS,
} from '../stores/useMangoStore'
import useInterval from './useInterval'
import set from 'lodash/set'
import get from 'lodash/get'
import { Market, Orderbook as SpotOrderBook } from '@project-serum/serum'
import {
BookSide,
BookSideLayout,
MangoAccount,
MangoAccountLayout,
PerpMarket,
ReferrerMemory,
ReferrerMemoryLayout,
} from '@blockworks-foundation/mango-client'
import {
actionsSelector,
connectionSelector,
mangoAccountSelector,
marketConfigSelector,
marketSelector,
marketsSelector,
} from '../stores/selectors'
import { useWallet } from '@solana/wallet-adapter-react'
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
return new BookSide(
// @ts-ignore
null,
market,
BookSideLayout.decode(accInfo.data),
undefined,
100000
)
}
}
}
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)
const { wallet } = useWallet()
// Fetches mango group as soon as page loads
useEffect(() => {
actions.fetchMangoGroup()
}, [actions])
// Fetch markets info once mango group is loaded
useEffect(() => {
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.fetchMarketsInfo()
if (wallet) {
actions.fetchWalletTokens(wallet)
}
}, 120 * SECONDS)
useEffect(() => {
const ws = new WebSocket('ws://localhost:8080')
const trees: { [market: string]: any } = {}
const books: { [market: string]: Orderbook } = {}
ws.onmessage = (event) => {
const { data } = event
const instruction = JSON.parse(data)
const { market, type, side, orders } = instruction
if (type == 'l2snapshot') {
switch (side) {
case 'bids':
set(
trees,
[market, side],
new RBTree<{ price: number; size: number }>(
(nodeA, nodeB) => nodeB.price - nodeA.price
)
)
for (const [price, size] of orders) {
trees[market][side].insert({ price, size })
}
break
case 'asks':
set(
trees,
[market, side],
new RBTree<{ price: number; size: number }>(
(nodeA, nodeB) => nodeA.price - nodeB.price
)
)
for (const [price, size] of orders) {
trees[market][side].insert({ price, size })
}
break
default:
console.error('Invalid side received')
}
}
const tree = get(trees, [market, side])
if (type == 'l2update') {
for (const [price, size] of orders) {
const node = tree.find({ price, size })
if (size == 0) {
if (node) tree.remove({ price, size })
} else if (node) {
node.size = size
} else {
tree.insert({ price, size })
}
}
}
const iterator = tree.iterator()
const materialized: any[] = []
let order = iterator.next()
while (order !== null) {
materialized.push([order.price, order.size])
order = iterator.next()
}
set(books, [market], {
...get(books, [market], { bids: [], asks: [] }),
[side]: materialized,
})
const book = get(books, [marketConfig.name], {
bids: [],
asks: [],
})
setMangoStore((state) => {
state.selectedMarket.orderBook = book
})
}
return () => ws.close()
}, [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 mangoAccount =
useMangoStore.getState().selectedMangoAccount.current
if (!mangoAccount) return
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) >= 500 && context.slot > lastSeenSlot) {
const decodedMangoAccount = MangoAccountLayout.decode(info?.data)
const newMangoAccount = new MangoAccount(
mangoAccount.publicKey,
decodedMangoAccount
)
newMangoAccount.spotOpenOrdersAccounts =
mangoAccount.spotOpenOrdersAccounts
newMangoAccount.advancedOrders = mangoAccount.advancedOrders
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])
// fetch filled trades for selected market
useInterval(() => {
actions.loadMarketFills()
}, 20 * SECONDS)
useEffect(() => {
actions.loadMarketFills()
}, [selectedMarket])
}
export default useHydrateStore