283 lines
7.4 KiB
TypeScript
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
|