From cbd723ef42bc2232e61ad13a25c0bed69cbae2e0 Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Tue, 12 Mar 2024 17:20:16 -0500 Subject: [PATCH 01/18] Fix: rm random log --- contexts/BalancesContext.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/contexts/BalancesContext.tsx b/contexts/BalancesContext.tsx index b49007b..c4fc21a 100644 --- a/contexts/BalancesContext.tsx +++ b/contexts/BalancesContext.tsx @@ -231,7 +231,5 @@ export function BalancesProvider({ children }: { children: React.ReactNode }) { [balances], ); - console.log('value.balances', value.balances); - return {children}; } From 82e5de66bb25f9dbb63daa9f146ba739cc9f1f48 Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Tue, 12 Mar 2024 17:20:49 -0500 Subject: [PATCH 02/18] Feat: orders in context state is reactive --- contexts/ProposalMarketsContext.tsx | 218 +++++++++++++++++++++++----- 1 file changed, 181 insertions(+), 37 deletions(-) diff --git a/contexts/ProposalMarketsContext.tsx b/contexts/ProposalMarketsContext.tsx index 37b0631..ae4b401 100644 --- a/contexts/ProposalMarketsContext.tsx +++ b/contexts/ProposalMarketsContext.tsx @@ -10,6 +10,7 @@ import { IDL as OPENBOOK_IDL, OPENBOOK_PROGRAM_ID, MarketAccount, + OpenBookV2Client, } from '@openbook-dex/openbook-v2'; import { MarketAccountWithKey, @@ -24,10 +25,23 @@ import { useAutocrat } from '@/contexts/AutocratContext'; import { useConditionalVault } from '@/hooks/useConditionalVault'; import { useOpenbookTwap } from '@/hooks/useOpenbookTwap'; import { useTransactionSender } from '@/hooks/useTransactionSender'; -import { getLeafNodes } from '../lib/openbook'; +import { getLeafNodes, getUsersOpenOrderPks } from '../lib/openbook'; import { debounce } from '../lib/utils'; import { useProvider } from '@/hooks/useProvider'; import { BalancesProvider } from './BalancesContext'; +import { useOpenbook } from '@/hooks/useOpenbook'; +import { QUOTE_LOTS } from '@/lib/constants'; + +export type OrderBookOrder = { + price: number; + size: number; + timestamp: BN; + owner: PublicKey; + ownerSlot: number; + side: 'bids' | 'asks'; + market: PublicKey; + clientOrderId: BN; +}; export interface ProposalInterface { markets?: Markets; @@ -42,7 +56,14 @@ export interface ProposalInterface { failSpreadString: string; lastPassSlotUpdated: number; lastFailSlotUpdated: number; - fetchOpenOrders: (owner: PublicKey) => Promise; + refreshUserOrders: ( + openBookClient: OpenBookV2Client, + proposal: Proposal, + passBids: LeafNode[], + passAsks: LeafNode[], + failBids: LeafNode[], + failAsks: LeafNode[], + ) => Promise; fetchMarketsInfo: () => Promise; placeOrderTransactions: ( amount: number, @@ -86,6 +107,7 @@ export function ProposalMarketsProvider({ fromProposal?: ProposalAccountWithKey; }) { const provider = useProvider(); + // TODO: do we need this variable when we have openbook from the autocrat hook below? const openBookProgram = new Program(OPENBOOK_IDL, OPENBOOK_PROGRAM_ID, provider); const client = useQueryClient(); const { openbook, openbookTwap, proposals } = useAutocrat(); @@ -93,6 +115,7 @@ export function ProposalMarketsProvider({ const wallet = useWallet(); const sender = useTransactionSender(); const { placeOrderTransactions, cancelAndSettleFundsTransactions } = useOpenbookTwap(); + const { program: openBookClient } = useOpenbook(); const { program: vaultProgram } = useConditionalVault(); const [loading, setLoading] = useState(false); const [markets, setMarkets] = useState(); @@ -212,40 +235,18 @@ export function ProposalMarketsProvider({ fetchMarketsInfo(); }, [proposal]); - const fetchOpenOrders = useCallback( - async (owner: PublicKey) => { - const fetchProposalOpenOrders = async () => { - if (!openbook || !proposal) { - return; - } - const passOrders = await openbook.account.openOrdersAccount.all([ - { memcmp: { offset: 8, bytes: owner.toBase58() } }, - { memcmp: { offset: 40, bytes: proposal.account.openbookPassMarket.toBase58() } }, - ]); - const failOrders = await openbook.account.openOrdersAccount.all([ - { memcmp: { offset: 8, bytes: owner.toBase58() } }, - { memcmp: { offset: 40, bytes: proposal.account.openbookFailMarket.toBase58() } }, - ]); - return passOrders - .concat(failOrders) - .sort((a, b) => (a.account.accountNum < b.account.accountNum ? 1 : -1)); - }; - - const _orders = await client.fetchQuery({ - queryKey: [`fetchProposalOpenOrders-${proposal?.publicKey}`], - queryFn: () => fetchProposalOpenOrders(), - staleTime: 1_000, - }); - setOrders(_orders ?? []); - }, - [openbook, proposal], - ); - useEffect(() => { - if (proposal && wallet.publicKey) { - fetchOpenOrders(wallet.publicKey); + if (proposal && wallet.publicKey && markets) { + refreshUserOrders( + openBookClient, + proposal, + markets.passBids, + markets.passAsks, + markets.failBids, + markets.failAsks, + ); } - }, [markets, fetchOpenOrders, proposal]); + }, [markets, proposal]); useEffect(() => { if (!markets && proposal) { @@ -341,6 +342,142 @@ export function ProposalMarketsProvider({ return undefined; }, [markets]); + const refreshUserOrders = useCallback( + async ( + client: OpenBookV2Client, + proposal: Proposal, + passBids: LeafNode[], + passAsks: LeafNode[], + failBids: LeafNode[], + failAsks: LeafNode[], + ) => { + if (wallet.publicKey) { + const passBidOrders = passBids.map((leafNode) => { + const size = leafNode.quantity.toNumber(); + const price = leafNode.key.shrn(64).toNumber() / 10_000; + return { + price, + size, + market: proposal?.account.openbookPassMarket, + owner: leafNode.owner, + ownerSlot: leafNode.ownerSlot, + side: 'bids' as const, + timestamp: leafNode.timestamp, + clientOrderId: leafNode.clientOrderId, + }; + }); + const passAskOrders = passAsks.map((leafNode) => { + const size = leafNode.quantity.toNumber(); + const price = leafNode.key.shrn(64).toNumber() / 10_000; + return { + price, + size, + market: proposal?.account.openbookPassMarket, + owner: leafNode.owner, + ownerSlot: leafNode.ownerSlot, + side: 'asks' as const, + timestamp: leafNode.timestamp, + clientOrderId: leafNode.clientOrderId, + }; + }); + const failBidOrders = failBids.map((leafNode) => { + const size = leafNode.quantity.toNumber(); + const price = leafNode.key.shrn(64).toNumber() / 10_000; + return { + price, + size, + market: proposal?.account.openbookFailMarket, + owner: leafNode.owner, + ownerSlot: leafNode.ownerSlot, + side: 'bids' as const, + timestamp: leafNode.timestamp, + clientOrderId: leafNode.clientOrderId, + }; + }); + const failAskOrders = failAsks.map((leafNode) => { + const size = leafNode.quantity.toNumber(); + const price = leafNode.key.shrn(64).toNumber() / 10_000; + return { + price, + size, + market: proposal?.account.openbookFailMarket, + owner: leafNode.owner, + ownerSlot: leafNode.ownerSlot, + side: 'asks' as const, + timestamp: leafNode.timestamp, + clientOrderId: leafNode.clientOrderId, + }; + }); + const openOrdersPks = (await getUsersOpenOrderPks(client, wallet.publicKey)).map((p) => + p.toString(), + ); + const allOrders = [...passBidOrders, ...passAskOrders, ...failBidOrders, ...failAskOrders]; + + const userOrders = allOrders + .filter((o): o is OrderBookOrder => { + return !!o.market && openOrdersPks.includes(o.owner?.toString()); + }) + .map((o) => { + const position: OpenOrdersAccountWithKey['account']['position'] = + o.side === 'bids' + ? { + asksBaseLots: new BN(0), + bidsBaseLots: new BN(o.size), + baseFreeNative: new BN(), + quoteFreeNative: new BN(), + lockedMakerFees: new BN(), + referrerRebatesAvailable: new BN(), + penaltyHeapCount: new BN(), + makerVolume: new BN(), + takerVolume: new BN(), + reserved: [], + } + : { + bidsBaseLots: new BN(0), + asksBaseLots: new BN(o.size), + baseFreeNative: new BN(), + quoteFreeNative: new BN(), + lockedMakerFees: new BN(), + referrerRebatesAvailable: new BN(), + penaltyHeapCount: new BN(), + makerVolume: new BN(), + takerVolume: new BN(), + reserved: [], + }; + const order: OpenOrdersAccountWithKey = { + publicKey: o.owner, + account: { + owner: o.owner, + accountNum: o.clientOrderId.toNumber(), + market: o.market, + position, + bump: 0, + delegate: { + key: o.owner, + }, + name: [], + openOrders: [ + { + clientId: o.clientOrderId, + id: new BN(), + isFree: 0, + lockedPrice: new BN(o.price / QUOTE_LOTS), + padding: new BN(), + sideAndTree: 0, + }, + ], + padding: [], + }, + }; + + return order; + }); + setOrders(userOrders); + } + }, + [wallet.publicKey?.toString()], + ); + const placeOrder = useCallback( async (amount: number, price: number, limitOrder?: boolean, ask?: boolean, pass?: boolean) => { if (!proposal || !markets) return; @@ -358,7 +495,14 @@ export function ProposalMarketsProvider({ const txsSent = await sender.send(placeTxs); await fetchMarketsInfo(); - await fetchOpenOrders(wallet.publicKey); + await refreshUserOrders( + openBookClient, + proposal, + markets.passBids, + markets.passAsks, + markets.failBids, + markets.failAsks, + ); return txsSent; } catch (err) { console.error(err); @@ -374,7 +518,7 @@ export function ProposalMarketsProvider({ sender, placeOrderTransactions, fetchMarketsInfo, - fetchOpenOrders, + refreshUserOrders, ], ); @@ -639,7 +783,7 @@ export function ProposalMarketsProvider({ lastFailSlotUpdated, passSpreadString, failSpreadString, - fetchOpenOrders, + refreshUserOrders, fetchMarketsInfo, placeOrderTransactions, placeOrder, From d998ae869c82b576f64712ae4e86a8f6925c6941 Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Wed, 13 Mar 2024 09:39:25 -0500 Subject: [PATCH 03/18] Feat: users orders update by WS --- components/Orders/ProposalOpenOrderRow.tsx | 14 +- components/Orders/ProposalOpenOrdersTab.tsx | 8 +- .../Orders/ProposalUnsettledOrderRow.tsx | 34 +- .../Orders/ProposalUnsettledOrdersTab.tsx | 26 +- components/Proposals/ProposalDetailCard.tsx | 33 +- components/Proposals/ProposalOrdersCard.tsx | 43 +- contexts/ProposalMarketsContext.tsx | 490 +++++++++++------- lib/openbook.ts | 73 +-- 8 files changed, 434 insertions(+), 287 deletions(-) diff --git a/components/Orders/ProposalOpenOrderRow.tsx b/components/Orders/ProposalOpenOrderRow.tsx index 82a1d66..c592167 100644 --- a/components/Orders/ProposalOpenOrderRow.tsx +++ b/components/Orders/ProposalOpenOrderRow.tsx @@ -35,7 +35,7 @@ export function ProposalOpenOrderRow({ order }: { order: OpenOrdersAccountWithKe const wallet = useWallet(); const { generateExplorerLink } = useExplorerConfiguration(); const { proposal } = useProposal(); - const { markets, fetchOpenOrders, cancelAndSettleOrder } = useProposalMarkets(); + const { markets, cancelAndSettleOrder } = useProposalMarkets(); const { settleFundsTransactions, editOrderTransactions } = useOpenbookTwap(); const { setBalanceByMint } = useBalances(); const isBidSide = isBid(order); @@ -79,7 +79,7 @@ export function ProposalOpenOrderRow({ order }: { order: OpenOrdersAccountWithKe } finally { setIsCanceling(false); } - }, [order, proposal, markets, wallet.publicKey, cancelAndSettleOrder, fetchOpenOrders, sender]); + }, [order, proposal, markets, wallet.publicKey, cancelAndSettleOrder, sender]); const handleEdit = useCallback(async () => { if (!proposal || !markets || !editingOrder) return; @@ -127,7 +127,6 @@ export function ProposalOpenOrderRow({ order }: { order: OpenOrdersAccountWithKe // }; // }); // } - await fetchOpenOrders(wallet.publicKey); setEditingOrder(undefined); } finally { setIsEditing(false); @@ -140,7 +139,6 @@ export function ProposalOpenOrderRow({ order }: { order: OpenOrdersAccountWithKe editedSize, editedPrice, editOrderTransactions, - fetchOpenOrders, sender, ]); @@ -167,6 +165,10 @@ export function ProposalOpenOrderRow({ order }: { order: OpenOrdersAccountWithKe } }, [order, proposal, settleFundsTransactions]); + const price = numeral(order.account.openOrders[0].lockedPrice * QUOTE_LOTS).format( + NUMERAL_FORMAT, + ); + return ( @@ -216,9 +218,7 @@ export function ProposalOpenOrderRow({ order }: { order: OpenOrdersAccountWithKe setEditedPrice(Number(e.target.value))} /> ) : ( diff --git a/components/Orders/ProposalOpenOrdersTab.tsx b/components/Orders/ProposalOpenOrdersTab.tsx index a9ed6c9..c121d49 100644 --- a/components/Orders/ProposalOpenOrdersTab.tsx +++ b/components/Orders/ProposalOpenOrdersTab.tsx @@ -13,9 +13,9 @@ import { useProposalMarkets } from '@/contexts/ProposalMarketsContext'; const headers = ['Order ID', 'Market', 'Status', 'Size', 'Price', 'Notional', 'Actions']; -export function ProposalOpenOrdersTab({ orders }: { orders: OpenOrdersAccountWithKey[]; }) { +export function ProposalOpenOrdersTab({ orders }: { orders: OpenOrdersAccountWithKey[] }) { const { isCranking, crankMarkets, proposal } = useProposal(); - const { markets, fetchOpenOrders } = useProposalMarkets(); + const { markets } = useProposalMarkets(); const sender = useTransactionSender(); const wallet = useWallet(); const { cancelOrderTransactions, settleFundsTransactions } = useOpenbookTwap(); @@ -45,14 +45,12 @@ export function ProposalOpenOrdersTab({ orders }: { orders: OpenOrdersAccountWit setIsCanceling(true); // Filtered undefined already await sender.send(txs); - // We already return above if the wallet doesn't have a public key - await fetchOpenOrders(wallet.publicKey!); } catch (err) { console.error(err); } finally { setIsCanceling(false); } - }, [proposal, markets, wallet.publicKey, cancelOrderTransactions, fetchOpenOrders, sender]); + }, [proposal, markets, wallet.publicKey, cancelOrderTransactions, sender]); const handleSettleAllFunds = useCallback(async () => { if (!proposal || !markets) return; diff --git a/components/Orders/ProposalUnsettledOrderRow.tsx b/components/Orders/ProposalUnsettledOrderRow.tsx index 4a89f35..dbff11e 100644 --- a/components/Orders/ProposalUnsettledOrderRow.tsx +++ b/components/Orders/ProposalUnsettledOrderRow.tsx @@ -21,9 +21,10 @@ import { useProposal } from '@/contexts/ProposalContext'; import { isBid, isPartiallyFilled, isPass } from '@/lib/openbook'; import { useBalances } from '../../contexts/BalancesContext'; import { useProposalMarkets } from '@/contexts/ProposalMarketsContext'; +import { useOpenbook } from '@/hooks/useOpenbook'; export function ProposalUnsettledOrderRow({ order }: { order: OpenOrdersAccountWithKey }) { - const { markets, fetchOpenOrders } = useProposalMarkets(); + const { markets, fetchNonOpenOrders } = useProposalMarkets(); const theme = useMantineTheme(); const sender = useTransactionSender(); const wallet = useWallet(); @@ -31,6 +32,7 @@ export function ProposalUnsettledOrderRow({ order }: { order: OpenOrdersAccountW const { generateExplorerLink } = useExplorerConfiguration(); const { proposal, crankMarkets, isCranking } = useProposal(); const { settleFundsTransactions, closeOpenOrdersAccountTransactions } = useOpenbookTwap(); + const { program: openbookClient } = useOpenbook(); const isBidSide = isBid(order); const balance = isBidSide ? order.account.position.bidsBaseLots @@ -58,23 +60,24 @@ export function ProposalUnsettledOrderRow({ order }: { order: OpenOrdersAccountW if (!txs) return; await sender.send(txs); - await fetchOpenOrders(wallet.publicKey); - const relevantMint = isBidSide - ? marketAccount.account.quoteMint - : marketAccount.account.baseMint; - setBalanceByMint(relevantMint, (oldBalance) => { - const newAmount = oldBalance.uiAmount + balance.toNumber(); - return { - ...oldBalance, - amount: newAmount.toString(), - uiAmount: newAmount, - uiAmountString: newAmount.toString(), - }; - }); + // TODO: the balance is already at 0 when this runs, so the math doesn't work right + // const relevantMint = isBidSide + // ? marketAccount.account.quoteMint + // : marketAccount.account.baseMint; + // setBalanceByMint(relevantMint, (oldBalance) => { + // const newAmount = oldBalance.uiAmount + balance.toNumber(); + // return { + // ...oldBalance, + // amount: newAmount.toString(), + // uiAmount: newAmount, + // uiAmountString: newAmount.toString(), + // }; + // }); + await fetchNonOpenOrders(wallet.publicKey, openbookClient.program, proposal, markets); } finally { setIsSettling(false); } - }, [order, proposal, settleFundsTransactions, wallet, fetchOpenOrders]); + }, [order, proposal, settleFundsTransactions, wallet]); const handleCloseAccount = useCallback(async () => { if (!proposal || !markets) return; @@ -86,7 +89,6 @@ export function ProposalUnsettledOrderRow({ order }: { order: OpenOrdersAccountW setIsClosing(true); try { await sender.send(txs); - fetchOpenOrders(wallet.publicKey); } catch (err) { console.error(err); } finally { diff --git a/components/Orders/ProposalUnsettledOrdersTab.tsx b/components/Orders/ProposalUnsettledOrdersTab.tsx index 8c801a4..12693bb 100644 --- a/components/Orders/ProposalUnsettledOrdersTab.tsx +++ b/components/Orders/ProposalUnsettledOrdersTab.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { Stack, Table, Button, Group, Text } from '@mantine/core'; import { Transaction } from '@solana/web3.js'; import { BN } from '@coral-xyz/anchor'; @@ -11,14 +11,16 @@ import { isClosableOrder, isPartiallyFilled } from '@/lib/openbook'; import { ProposalUnsettledOrderRow } from './ProposalUnsettledOrderRow'; import { useBalances } from '../../contexts/BalancesContext'; import { useProposalMarkets } from '@/contexts/ProposalMarketsContext'; +import { useAutocrat } from '@/contexts/AutocratContext'; const headers = ['Order ID', 'Market', 'Claimable', 'Actions']; export function ProposalUnsettledOrdersTab({ orders }: { orders: OpenOrdersAccountWithKey[] }) { const sender = useTransactionSender(); + const { openbook } = useAutocrat(); const wallet = useWallet(); const { proposal } = useProposal(); - const { markets, fetchOpenOrders } = useProposalMarkets(); + const { markets, fetchNonOpenOrders } = useProposalMarkets(); const { fetchBalanceByMint } = useBalances(); const { settleFundsTransactions, closeOpenOrdersAccountTransactions } = useOpenbookTwap(); @@ -31,6 +33,12 @@ export function ProposalUnsettledOrdersTab({ orders }: { orders: OpenOrdersAccou ); const ordersToClose = useMemo(() => orders.filter((order) => isClosableOrder(order)), [orders]); + useEffect(() => { + if (wallet.publicKey) { + fetchNonOpenOrders(wallet.publicKey, openbook, proposal, markets); + } + }, [wallet.publicKey?.toString(), openbook, proposal, markets]); + const handleSettleAllFunds = useCallback(async () => { if (!proposal || !markets || !wallet?.publicKey) return; @@ -56,7 +64,6 @@ export function ProposalUnsettledOrdersTab({ orders }: { orders: OpenOrdersAccou if (!txs) return; await sender.send(txs as Transaction[]); - fetchOpenOrders(wallet.publicKey); fetchBalanceByMint(markets.pass.baseMint); fetchBalanceByMint(markets.pass.quoteMint); fetchBalanceByMint(markets.fail.baseMint); @@ -64,15 +71,7 @@ export function ProposalUnsettledOrdersTab({ orders }: { orders: OpenOrdersAccou } finally { setIsSettling(false); } - }, [ - ordersToSettle, - markets, - proposal, - sender, - settleFundsTransactions, - fetchOpenOrders, - fetchBalanceByMint, - ]); + }, [ordersToSettle, markets, proposal, sender, settleFundsTransactions, fetchBalanceByMint]); const handleCloseAllOrders = useCallback(async () => { if (!proposal || !markets || !wallet?.publicKey) return; @@ -93,10 +92,9 @@ export function ProposalUnsettledOrdersTab({ orders }: { orders: OpenOrdersAccou if (!txs) return; await sender.send(txs as Transaction[]); } finally { - fetchOpenOrders(wallet.publicKey); setIsClosing(false); } - }, [ordersToClose, markets, proposal, sender, settleFundsTransactions, fetchOpenOrders]); + }, [ordersToClose, markets, proposal, sender, settleFundsTransactions]); return ( diff --git a/components/Proposals/ProposalDetailCard.tsx b/components/Proposals/ProposalDetailCard.tsx index 7ee24fa..bc786a6 100644 --- a/components/Proposals/ProposalDetailCard.tsx +++ b/components/Proposals/ProposalDetailCard.tsx @@ -55,8 +55,8 @@ export function ProposalDetailCard() { const { tokens } = useTokens(); const { proposal, finalizeProposalTransactions } = useProposal(); const { - orders, - fetchOpenOrders, + openOrders, + unsettledOrders, markets, passAsks, passBids, @@ -146,21 +146,19 @@ export function ProposalDetailCard() { }, [tokens, daoTreasury, sender, finalizeProposalTransactions, fetchProposals]); const handleCloseOrders = useCallback(async () => { - if (!proposal || !orders || !markets || !wallet.publicKey) { + if (!proposal || !openOrders || !markets || !wallet.publicKey) { return; } - const openOrders = orders.filter((order) => isOpenOrder(order, markets)); // TODO: also handle uncranked orders // const uncrankedOrders = orders.filter((order) => isCompletedOrder(order, markets)); - const unsettledOrders = orders.filter((order) => isEmptyOrder(order)); - const ordersToSettle = unsettledOrders.filter((order) => isPartiallyFilled(order)); - const ordersToClose = unsettledOrders.filter((order) => isClosableOrder(order)); + const ordersToSettle = unsettledOrders?.filter((order) => isPartiallyFilled(order)) ?? []; + const ordersToClose = unsettledOrders?.filter((order) => isClosableOrder(order)) ?? []; const cancelOpenOrdersTxs = ( await Promise.all( - openOrders.map((order) => + (openOrders ?? []).map((order) => cancelOrderTransactions( new BN(order.account.accountNum), proposal.account.openbookPassMarket.equals(order.account.market) @@ -173,7 +171,7 @@ export function ProposalDetailCard() { const settleOrdersTxs = ( await Promise.all( - openOrders.concat(ordersToSettle).map((order) => { + (openOrders ?? []).concat(ordersToSettle).map((order) => { const pass = order.account.market.equals(proposal.account.openbookPassMarket); return settleFundsTransactions( order.account.accountNum, @@ -189,7 +187,7 @@ export function ProposalDetailCard() { const closeOrdersTxs = ( await Promise.all( - openOrders + (openOrders ?? []) .concat(ordersToSettle) .concat(ordersToClose) .map((order) => closeOpenOrdersAccountTransactions(new BN(order.account.accountNum))), @@ -202,18 +200,9 @@ export function ProposalDetailCard() { [cancelOpenOrdersTxs, settleOrdersTxs, closeOrdersTxs].filter((set) => set.length !== 0), ); } finally { - fetchOpenOrders(wallet.publicKey); setIsClosing(false); } - }, [ - orders, - markets, - proposal, - sender, - wallet.publicKey, - cancelOrderTransactions, - fetchOpenOrders, - ]); + }, [openOrders, markets, proposal, sender, wallet.publicKey, cancelOrderTransactions]); const handleRedeem = useCallback(async () => { if (!markets || !proposal) return; @@ -410,12 +399,12 @@ export function ProposalDetailCard() { <> - {(orders?.length || 0) === 0 ? ( + {(openOrders?.length || 0) === 0 ? ( diff --git a/components/Proposals/ProposalOrdersCard.tsx b/components/Proposals/ProposalOrdersCard.tsx index ad59ed7..b6d1801 100644 --- a/components/Proposals/ProposalOrdersCard.tsx +++ b/components/Proposals/ProposalOrdersCard.tsx @@ -13,18 +13,35 @@ import { ProposalOpenOrdersTab } from '@/components/Orders/ProposalOpenOrdersTab import { ProposalUnsettledOrdersTab } from '@/components/Orders/ProposalUnsettledOrdersTab'; import { ProposalUncrankedOrdersTab } from '@/components/Orders/ProposalUncrankedOrdersTab'; import { useProposalMarkets } from '@/contexts/ProposalMarketsContext'; +import { useOpenbook } from '@/hooks/useOpenbook'; export function ProposalOrdersCard() { - const wallet = useWallet(); const { proposal } = useProposal(); - const { fetchOpenOrders, markets, orders } = useProposalMarkets(); + const { + markets, + openOrders, + unsettledOrders, + uncrankedOrders: unCrankedOrders, + refreshUserOpenOrders, + } = useProposalMarkets(); + const { program: openBookClient } = useOpenbook(); - if (!orders || !markets) return <>; - const openOrders = orders.filter((order) => isOpenOrder(order, markets)); - const unCrankedOrders = orders.filter((order) => isCompletedOrder(order, markets)); - const unsettledOrders = orders.filter((order) => isEmptyOrder(order)); + if (!openOrders || !markets) return <>; - return !proposal || !markets || !orders ? ( + const onRefresh = () => { + if (proposal && markets) { + refreshUserOpenOrders( + openBookClient, + proposal, + markets.passBids, + markets.passAsks, + markets.failBids, + markets.failAsks, + ); + } + }; + + return !proposal || !markets || !openOrders ? ( @@ -38,14 +55,14 @@ export function ProposalOrdersCard() { - ${totalUsdcInOrder(orders)} + ${totalUsdcInOrder(openOrders)} {' '} condUSDC | - {totalMetaInOrder(orders)} + {totalMetaInOrder(openOrders)} {' '} condMETA @@ -53,7 +70,7 @@ export function ProposalOrdersCard() { fetchOpenOrders(wallet.publicKey)} + onClick={onRefresh} > @@ -70,12 +87,10 @@ export function ProposalOrdersCard() { - + - + diff --git a/contexts/ProposalMarketsContext.tsx b/contexts/ProposalMarketsContext.tsx index ae4b401..1711f92 100644 --- a/contexts/ProposalMarketsContext.tsx +++ b/contexts/ProposalMarketsContext.tsx @@ -25,7 +25,13 @@ import { useAutocrat } from '@/contexts/AutocratContext'; import { useConditionalVault } from '@/hooks/useConditionalVault'; import { useOpenbookTwap } from '@/hooks/useOpenbookTwap'; import { useTransactionSender } from '@/hooks/useTransactionSender'; -import { getLeafNodes, getUsersOpenOrderPks } from '../lib/openbook'; +import { + _isOpenOrder, + getLeafNodes, + getUsersOpenOrderPks, + isCompletedOrder, + isEmptyOrder, +} from '../lib/openbook'; import { debounce } from '../lib/utils'; import { useProvider } from '@/hooks/useProvider'; import { BalancesProvider } from './BalancesContext'; @@ -45,7 +51,9 @@ export type OrderBookOrder = { export interface ProposalInterface { markets?: Markets; - orders?: OpenOrdersAccountWithKey[]; + openOrders?: OpenOrdersAccountWithKey[]; + uncrankedOrders?: OpenOrdersAccountWithKey[]; + unsettledOrders?: OpenOrdersAccountWithKey[]; passAsks?: any[][]; passBids?: any[][]; failAsks?: any[][]; @@ -56,7 +64,7 @@ export interface ProposalInterface { failSpreadString: string; lastPassSlotUpdated: number; lastFailSlotUpdated: number; - refreshUserOrders: ( + refreshUserOpenOrders: ( openBookClient: OpenBookV2Client, proposal: Proposal, passBids: LeafNode[], @@ -64,6 +72,12 @@ export interface ProposalInterface { failBids: LeafNode[], failAsks: LeafNode[], ) => Promise; + fetchNonOpenOrders: ( + owner: PublicKey, + openbook: Program | undefined, + proposal: Proposal | undefined, + markets: Markets | undefined, + ) => Promise; fetchMarketsInfo: () => Promise; placeOrderTransactions: ( amount: number, @@ -119,7 +133,9 @@ export function ProposalMarketsProvider({ const { program: vaultProgram } = useConditionalVault(); const [loading, setLoading] = useState(false); const [markets, setMarkets] = useState(); - const [orders, setOrders] = useState([]); + const [openOrders, setOpenOrders] = useState([]); + const [uncrankedOrders, setUncrankedOrders] = useState([]); + const [unsettledOrders, setUnsettledOrders] = useState([]); const [passBids, setPassBids] = useState([]); const [passAsks, setPassAsks] = useState([]); const [failBids, setFailBids] = useState([]); @@ -140,6 +156,54 @@ export function ProposalMarketsProvider({ [proposals, fromProposal, proposalNumber], ); + const fetchNonOpenOrders = useCallback( + async ( + owner: PublicKey, + openbook: Program | undefined, + proposal: Proposal | undefined, + markets: Markets | undefined, + ) => { + const fetchProposalClosedOrders = async () => { + if (!openbook || !proposal || !markets) { + return []; + } + const passOrders = await openbook.account.openOrdersAccount.all([ + { memcmp: { offset: 8, bytes: owner.toBase58() } }, + { memcmp: { offset: 40, bytes: proposal.account.openbookPassMarket.toBase58() } }, + ]); + const passUnsettledOrders = passOrders.filter((o) => isEmptyOrder(o)); + const passUncrankedOrders = passOrders.filter((o) => isCompletedOrder(o, markets)); + const failOrders = await openbook.account.openOrdersAccount.all([ + { memcmp: { offset: 8, bytes: owner.toBase58() } }, + { memcmp: { offset: 40, bytes: proposal.account.openbookFailMarket.toBase58() } }, + ]); + const failUnsettledOrders = failOrders.filter((o) => isEmptyOrder(o)); + const failUncrankedOrders = failOrders.filter((o) => isCompletedOrder(o, markets)); + return [passUnsettledOrders, passUncrankedOrders, failUnsettledOrders, failUncrankedOrders]; + }; + + const nonOpenOrders = await client.fetchQuery({ + queryKey: [`fetchProposalClosedOrders-${proposal?.publicKey}`], + queryFn: () => fetchProposalClosedOrders(), + staleTime: 1_000, + }); + + if (nonOpenOrders.length > 0) { + const [passUnsettledOrders, passUncrankedOrders, failUnsettledOrders, failUncrankedOrders] = + nonOpenOrders; + const unsettledOrders = [...passUnsettledOrders, ...failUnsettledOrders].sort((a, b) => + a.account.accountNum < b.account.accountNum ? 1 : -1, + ); + const uncrankedOrders = [...passUncrankedOrders, ...failUncrankedOrders].sort((a, b) => + a.account.accountNum < b.account.accountNum ? 1 : -1, + ); + setUnsettledOrders(unsettledOrders); + setUncrankedOrders(uncrankedOrders); + } + }, + [openbook, proposal], + ); + const fetchMarketsInfo = useCallback( debounce(async () => { const fetchProposalMarketsInfo = async () => { @@ -224,7 +288,9 @@ export function ProposalMarketsProvider({ queryFn: () => fetchProposalMarketsInfo(), staleTime: 10_000, }); - setMarkets(marketsInfo); + if (marketsInfo) { + setMarkets(marketsInfo); + } setLoading(false); }, 1000), [vaultProgram, openbook, openbookTwap, proposal, connection], @@ -237,7 +303,7 @@ export function ProposalMarketsProvider({ useEffect(() => { if (proposal && wallet.publicKey && markets) { - refreshUserOrders( + refreshUserOpenOrders( openBookClient, proposal, markets.passBids, @@ -342,7 +408,7 @@ export function ProposalMarketsProvider({ return undefined; }, [markets]); - const refreshUserOrders = useCallback( + const refreshUserOpenOrders = useCallback( async ( client: OpenBookV2Client, proposal: Proposal, @@ -472,7 +538,7 @@ export function ProposalMarketsProvider({ return order; }); - setOrders(userOrders); + setOpenOrders(userOrders); } }, [wallet.publicKey?.toString()], @@ -495,7 +561,7 @@ export function ProposalMarketsProvider({ const txsSent = await sender.send(placeTxs); await fetchMarketsInfo(); - await refreshUserOrders( + await refreshUserOpenOrders( openBookClient, proposal, markets.passBids, @@ -518,7 +584,7 @@ export function ProposalMarketsProvider({ sender, placeOrderTransactions, fetchMarketsInfo, - refreshUserOrders, + refreshUserOpenOrders, ], ); @@ -527,131 +593,181 @@ export function ProposalMarketsProvider({ * orderbook state for ask and bid sides. We will also use this to update the user orders(soon). * */ - const consumeOrderBookSide = ( - side: string, - updatedAccountInfo: AccountInfo, - market: PublicKey, - ctx: Context, - ): number[][] | undefined => { - try { - const isPassMarket = market === proposal?.account.openbookPassMarket; - const leafNodes = openBookProgram.coder.accounts.decode('bookSide', updatedAccountInfo.data); - const leafNodesData: AnyNode[] = leafNodes.nodes.nodes.filter((x: AnyNode) => x.tag === 2); - - const _side = leafNodesData.map((x) => { - const leafNode: LeafNode = openBookProgram.coder.types.decode( - 'LeafNode', - Buffer.from([0, ...x.data]), + const consumeOrderBookSide = useCallback( + ( + side: string, + updatedAccountInfo: AccountInfo, + market: PublicKey, + markets: Markets, + ctx: Context, + ): number[][] | undefined => { + try { + const isPassMarket = market === proposal?.account.openbookPassMarket; + const leafNodes = openBookProgram.coder.accounts.decode( + 'bookSide', + updatedAccountInfo.data, ); - const size = leafNode.quantity.toNumber(); - const price = leafNode.key.shrn(64).toNumber() / 10_000; - return { - price, - size, - market, - owner: leafNode.owner, - ownerSlot: leafNode.ownerSlot, - side: side === 'asks' ? 'asks' : 'bids', - timestamp: leafNode.timestamp, - clientOrderId: leafNode.clientOrderId, - }; - }); + const leafNodesData: AnyNode[] = leafNodes.nodes.nodes.filter((x: AnyNode) => x.tag === 2); - let sortedSide; + const leafNodeSide = leafNodesData.map((x) => { + const leafNode: LeafNode = openBookProgram.coder.types.decode( + 'LeafNode', + Buffer.from([0, ...x.data]), + ); + return leafNode; + }); + const _side = leafNodeSide.map((leafNode) => { + const size = leafNode.quantity.toNumber(); + const price = leafNode.key.shrn(64).toNumber() / 10_000; + return { + price, + size, + market, + owner: leafNode.owner, + ownerSlot: leafNode.ownerSlot, + side: side === 'asks' ? 'asks' : 'bids', + timestamp: leafNode.timestamp, + clientOrderId: leafNode.clientOrderId, + }; + }); - if (side === 'asks') { - // Ask side sort - sortedSide = _side.sort( - (a: { price: number; size: number }, b: { price: number; size: number }) => - a.price - b.price, - ); - } else { - // Bid side sort - sortedSide = _side.sort( - (a: { price: number; size: number }, b: { price: number; size: number }) => - b.price - a.price, - ); - } + let sortedSide; - // Aggregate the price levels into sum(size) - const _aggreateSide = new Map(); - sortedSide.forEach((order: { price: number; size: number }) => { - if (_aggreateSide.get(order.price) === undefined) { - _aggreateSide.set(order.price, order.size); + if (side === 'asks') { + // Ask side sort + sortedSide = _side.sort( + (a: { price: number; size: number }, b: { price: number; size: number }) => + a.price - b.price, + ); } else { - _aggreateSide.set(order.price, _aggreateSide.get(order.price) + order.size); + // Bid side sort + sortedSide = _side.sort( + (a: { price: number; size: number }, b: { price: number; size: number }) => + b.price - a.price, + ); } - }); - // Construct array for our orderbook system - let __side: any[][]; - if (_aggreateSide) { - __side = Array.from(_aggreateSide.entries()).map((_side_) => [ - _side_[0].toFixed(4), - _side_[1], - ]); - } else { - // Return default values of 0 - return [[0, 0]]; - } - // Update our values for the orderbook and order list - if (isPassMarket) { - if (side === 'asks') { - setPassAsks(__side); + + // Aggregate the price levels into sum(size) + const _aggreateSide = new Map(); + sortedSide.forEach((order: { price: number; size: number }) => { + if (_aggreateSide.get(order.price) === undefined) { + _aggreateSide.set(order.price, order.size); + } else { + _aggreateSide.set(order.price, _aggreateSide.get(order.price) + order.size); + } + }); + // Construct array for our orderbook system + let __side: any[][]; + if (_aggreateSide) { + __side = Array.from(_aggreateSide.entries()).map((_side_) => [ + _side_[0].toFixed(4), + _side_[1], + ]); } else { - setPassBids(__side); + // Return default values of 0 + return [[0, 0]]; } - setLastPassSlotUpdated(ctx.slot); - } else { - if (side === 'asks') { - setFailAsks(__side); + // Update our values for the orderbook and order list + if (isPassMarket) { + if (side === 'asks') { + if (markets) { + refreshUserOpenOrders( + openBookClient, + proposal, + markets?.passBids, + leafNodeSide, + markets?.failBids, + markets?.failAsks, + ); + } + setPassAsks(__side); + } else { + if (markets) { + refreshUserOpenOrders( + openBookClient, + proposal, + leafNodeSide, + markets?.passAsks, + markets?.failBids, + markets?.failAsks, + ); + } + setPassBids(__side); + } + setLastPassSlotUpdated(ctx.slot); } else { - setFailBids(__side); + if (side === 'asks') { + if (markets && proposal) { + refreshUserOpenOrders( + openBookClient, + proposal, + markets?.passBids, + markets?.passAsks, + markets?.failBids, + leafNodeSide, + ); + } + setFailAsks(__side); + } else { + if (markets && proposal) { + refreshUserOpenOrders( + openBookClient, + proposal, + markets?.passBids, + markets?.passAsks, + leafNodeSide, + markets?.failAsks, + ); + } + setFailBids(__side); + } + setLastFailSlotUpdated(ctx.slot); } - setLastFailSlotUpdated(ctx.slot); - } - // Check that we have books + // Check that we have books - let tobAsk: number; - let tobBid: number; + let tobAsk: number; + let tobBid: number; - // Get top of books - if (side === 'asks') { - tobAsk = Number(__side[0][0]); - // @ts-ignore - tobBid = Number(bids[0][0]); - } else { - // @ts-ignore - tobAsk = Number(asks[0][0]); - tobBid = Number(__side[0][0]); - } - // Calculate spread - const spread: number = Math.abs(tobAsk - tobBid); - // Calculate spread percent - const spreadPercent: string = ((spread / tobBid) * 100).toFixed(2); - let _spreadString: string; - // Create our string for output into the orderbook object - if (spread === tobAsk) { - _spreadString = '∞'; - } else { - _spreadString = `${spread.toFixed(2).toString()} (${spreadPercent}%)`; - } - if (isPassMarket) { - setPassSpreadString((curSpreadString) => - curSpreadString === _spreadString ? curSpreadString : _spreadString, - ); - } else { - setFailSpreadString((curSpreadString) => - curSpreadString === _spreadString ? curSpreadString : _spreadString, - ); - } + // Get top of books + if (side === 'asks') { + tobAsk = Number(__side[0][0]); + // @ts-ignore + tobBid = Number(bids[0][0]); + } else { + // @ts-ignore + tobAsk = Number(asks[0][0]); + tobBid = Number(__side[0][0]); + } + // Calculate spread + const spread: number = Math.abs(tobAsk - tobBid); + // Calculate spread percent + const spreadPercent: string = ((spread / tobBid) * 100).toFixed(2); + let _spreadString: string; + // Create our string for output into the orderbook object + if (spread === tobAsk) { + _spreadString = '∞'; + } else { + _spreadString = `${spread.toFixed(2).toString()} (${spreadPercent}%)`; + } + if (isPassMarket) { + setPassSpreadString((curSpreadString) => + curSpreadString === _spreadString ? curSpreadString : _spreadString, + ); + } else { + setFailSpreadString((curSpreadString) => + curSpreadString === _spreadString ? curSpreadString : _spreadString, + ); + } - setWsConnected((curConnected) => curConnected === false); - } catch (err) { - // console.error(err); - // TODO: Add in call to analytics / reporting - } - }; + setWsConnected((curConnected) => curConnected === false); + } catch (err) { + // console.error(err); + // TODO: Add in call to analytics / reporting + } + }, + [markets, proposal, openBookClient], + ); // this is our initial fetching of orderbook data to set the order book state on page load // subsequent updates are handled by the WS @@ -677,46 +793,55 @@ export function ProposalMarketsProvider({ } }, [orderBookObject]); - const listenOrderBooks = async () => { - if (!proposal) return; + // need to put this in a useEffect that takes all the parameters and passes them on down baby + const listenOrderBooks = async ( + proposal: Proposal, + markets: Markets, + openBookProgram: Program, + ) => { + if (!proposal || !markets) return; const _markets = [proposal?.account.openbookFailMarket, proposal?.account.openbookPassMarket]; // Setup for pass and fail markets // bubble down events from the market for the orders - _markets.forEach(async (market: PublicKey) => { - if (!wsConnected) { - // Fetch via RPC for the openbook market - const _market = await openBookProgram.account.market.fetch(market); - const sides = [ - { - pubKey: _market.asks, - side: 'asks', - }, - { - pubKey: _market.bids, - side: 'bids', - }, - ]; - // Setup Websocket subscription for the two sides - try { - const subscriptionId = sides.map((side) => - provider.connection.onAccountChange( - side.pubKey, - (updatedAccountInfo, ctx) => { - consumeOrderBookSide(side.side, updatedAccountInfo, market, ctx); - }, - 'processed', - ), - ); - return subscriptionId; - } catch (err) { - setWsConnected(false); + const susbcriptionIds = await Promise.all( + _markets.map(async (market: PublicKey) => { + if (!wsConnected) { + // Fetch via RPC for the openbook market + const _market = await openBookProgram.account.market.fetch(market); + const sides = [ + { + pubKey: _market.asks, + side: 'asks', + }, + { + pubKey: _market.bids, + side: 'bids', + }, + ]; + // Setup Websocket subscription for the two sides + // TODO pass in markets here to consume order book side + try { + const subscriptionIds = sides.map((side) => + provider.connection.onAccountChange( + side.pubKey, + (updatedAccountInfo, ctx) => { + consumeOrderBookSide(side.side, updatedAccountInfo, market, markets, ctx); + }, + 'processed', + ), + ); + return subscriptionIds; + } catch (err) { + setWsConnected(false); + } } - } - // For map handling - return null; - }); + // For map handling + return []; + }), + ); + return susbcriptionIds.flat(); }; const cancelAndSettleOrder = useCallback( @@ -740,17 +865,20 @@ export function ProposalMarketsProvider({ if (!cancelAndSettleTxs) return; const txsSent = await sender.send([...cancelAndSettleTxs]); - if (txsSent.length !== 0) { - //update order in state - const cancelledOrderIndex = orders.findIndex( - (o) => o.account.accountNum === order.account.accountNum, - ); - orders[cancelledOrderIndex].account.openOrders[0].isFree = 1; - orders[cancelledOrderIndex].account.position.baseFreeNative = new BN(0); - orders[cancelledOrderIndex].account.position.quoteFreeNative = new BN(0); - const newOrders = [...orders]; - setOrders(newOrders); - } + // if (txsSent.length !== 0) { + // //update order in state + // const cancelledOrderIndex = openOrders.findIndex( + // (o) => o.account.accountNum === order.account.accountNum, + // ); + // const cancelledOrder: OpenOrdersAccountWithKey | undefined = openOrders[cancelledOrderIndex]; + // if (cancelledOrder) { + // openOrders[cancelledOrderIndex].account.openOrders[0].isFree = 1; + // openOrders[cancelledOrderIndex].account.position.baseFreeNative = new BN(0); + // openOrders[cancelledOrderIndex].account.position.quoteFreeNative = new BN(0); + // setUnsettledOrders([...unsettledOrders, cancelledOrder]); + // setOpenOrders(openOrders.filter((o, i) => i !== cancelledOrderIndex)); + // } + // } return txsSent; } catch (err) { console.error(err); @@ -760,11 +888,20 @@ export function ProposalMarketsProvider({ ); useEffect(() => { - if (!wsConnected && proposal) { - // connect for both pass and fail market order books - listenOrderBooks(); - } - }, [wsConnected, !!proposal]); + // TODO, TEST IT!! + const handleOrderBooklistening = async () => { + if (!wsConnected && proposal && markets && openBookProgram) { + // connect for both pass and fail market order books + const subscriptionIds = await listenOrderBooks(proposal, markets, openBookProgram); + return () => { + subscriptionIds?.forEach((s) => { + connection.removeAccountChangeListener(s); + }); + }; + } + }; + handleOrderBooklistening(); + }, [wsConnected, !!proposal, !!markets, !!openBookProgram]); useEffect(() => { fetchMarketsInfo(); }, [proposal]); @@ -772,7 +909,9 @@ export function ProposalMarketsProvider({ const memoValue = useMemo( () => ({ markets, - orders, + openOrders, + uncrankedOrders, + unsettledOrders, orderBookObject, loading, passAsks, @@ -783,7 +922,8 @@ export function ProposalMarketsProvider({ lastFailSlotUpdated, passSpreadString, failSpreadString, - refreshUserOrders, + refreshUserOpenOrders, + fetchNonOpenOrders, fetchMarketsInfo, placeOrderTransactions, placeOrder, @@ -791,7 +931,9 @@ export function ProposalMarketsProvider({ }), [ markets, - orders, + openOrders, + uncrankedOrders, + unsettledOrders, loading, passAsks.length, passBids.length, diff --git a/lib/openbook.ts b/lib/openbook.ts index c773ce8..2baf953 100644 --- a/lib/openbook.ts +++ b/lib/openbook.ts @@ -1,5 +1,5 @@ import { Program, BN } from '@coral-xyz/anchor'; -import { OpenbookV2 } from '@openbook-dex/openbook-v2'; +import { OpenbookV2, OpenBookV2Client } from '@openbook-dex/openbook-v2'; import { Keypair, PublicKey, @@ -250,33 +250,30 @@ export const isClosableOrder = (order: OpenOrdersAccountWithKey): boolean => order.account.position.baseFreeNative.eq(BN_0) && order.account.position.quoteFreeNative.eq(BN_0); - export const _isOpenOrder = ( - order: OpenOrdersAccountWithKey, - market: OpenbookMarket - ): boolean => { - if (order.account.openOrders[0].isFree === 0) { - const asksFilter = market.asks.filter( - (_order: any) => _order.owner.toString() === order.publicKey.toString(), - ); - const bidsFilter = market.bids.filter( - (_order: any) => _order.owner.toString() === order.publicKey.toString(), - ); - let _order = null; - if (asksFilter.length > 0) { - // eslint-disable-next-line prefer-destructuring - _order = asksFilter[0]; - } - if (bidsFilter.length > 0) { - // eslint-disable-next-line prefer-destructuring - _order = bidsFilter[0]; - } - if (_order !== null) { - return true; - } - return false; +export const _isOpenOrder = (order: OpenOrdersAccountWithKey, market: OpenbookMarket): boolean => { + if (order.account.openOrders[0].isFree === 0) { + const asksFilter = market.asks.filter( + (_order: any) => _order.owner.toString() === order.publicKey.toString(), + ); + const bidsFilter = market.bids.filter( + (_order: any) => _order.owner.toString() === order.publicKey.toString(), + ); + let _order = null; + if (asksFilter.length > 0) { + // eslint-disable-next-line prefer-destructuring + _order = asksFilter[0]; + } + if (bidsFilter.length > 0) { + // eslint-disable-next-line prefer-destructuring + _order = bidsFilter[0]; + } + if (_order !== null) { + return true; } return false; - }; + } + return false; +}; export const isOpenOrder = (order: OpenOrdersAccountWithKey, markets: Markets): boolean => { if (order.account.openOrders[0].isFree === 0) { @@ -319,7 +316,7 @@ export const isOpenOrder = (order: OpenOrdersAccountWithKey, markets: Markets): export const _isCompletedOrder = ( order: OpenOrdersAccountWithKey, - market: OpenbookMarket + market: OpenbookMarket, ): boolean => { const isOpen = _isOpenOrder(order, market); const isEmpty = @@ -361,14 +358,10 @@ export const totalUsdcInOrder = (orders: OpenOrdersAccountWithKey[]) => { sumOrders = orders.map((order) => { if (isBidOrAsk(order)) { return ( - (( - order.account.position.bidsBaseLots.toNumber() - * order.account.openOrders[0].lockedPrice - ) / 10_000) + - (( - order.account.position.asksBaseLots.toNumber() - * order.account.openOrders[0].lockedPrice - ) / 10_000) + (order.account.position.bidsBaseLots.toNumber() * order.account.openOrders[0].lockedPrice) / + 10_000 + + (order.account.position.asksBaseLots.toNumber() * order.account.openOrders[0].lockedPrice) / + 10_000 ); } return 0; @@ -393,3 +386,13 @@ export const totalMetaInOrder = (orders: OpenOrdersAccountWithKey[]) => { const totalValueLocked = sumOrders.reduce((partialSum, amount) => partialSum + amount, 0); return numeral(totalValueLocked).format(BASE_FORMAT); }; + +export const getUsersOpenOrderPks = async ( + client: OpenBookV2Client, + userWalletPk: PublicKey, +): Promise => { + const indexerPk = client.findOpenOrdersIndexer(userWalletPk); + const indexerAcc = await client.deserializeOpenOrdersIndexerAccount(indexerPk); + const openOrdersPks = indexerAcc?.addresses; + return openOrdersPks ?? []; +}; From acda80777a81d1aa48a89a2be159662c1291bde7 Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Wed, 13 Mar 2024 09:58:15 -0500 Subject: [PATCH 04/18] Fix: few bugs --- .../Orders/ProposalUnsettledOrderRow.tsx | 41 +++++----- components/Proposals/ProposalDetailCard.tsx | 2 +- components/Proposals/ProposalOrdersCard.tsx | 14 +++- contexts/ProposalMarketsContext.tsx | 78 ++++++++++--------- 4 files changed, 79 insertions(+), 56 deletions(-) diff --git a/components/Orders/ProposalUnsettledOrderRow.tsx b/components/Orders/ProposalUnsettledOrderRow.tsx index dbff11e..ede63dd 100644 --- a/components/Orders/ProposalUnsettledOrderRow.tsx +++ b/components/Orders/ProposalUnsettledOrderRow.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useRef, useState } from 'react'; import { ActionIcon, Group, @@ -21,10 +21,11 @@ import { useProposal } from '@/contexts/ProposalContext'; import { isBid, isPartiallyFilled, isPass } from '@/lib/openbook'; import { useBalances } from '../../contexts/BalancesContext'; import { useProposalMarkets } from '@/contexts/ProposalMarketsContext'; -import { useOpenbook } from '@/hooks/useOpenbook'; +import { useQueryClient } from '@tanstack/react-query'; export function ProposalUnsettledOrderRow({ order }: { order: OpenOrdersAccountWithKey }) { - const { markets, fetchNonOpenOrders } = useProposalMarkets(); + const queryClient = useQueryClient(); + const { markets } = useProposalMarkets(); const theme = useMantineTheme(); const sender = useTransactionSender(); const wallet = useWallet(); @@ -32,11 +33,11 @@ export function ProposalUnsettledOrderRow({ order }: { order: OpenOrdersAccountW const { generateExplorerLink } = useExplorerConfiguration(); const { proposal, crankMarkets, isCranking } = useProposal(); const { settleFundsTransactions, closeOpenOrdersAccountTransactions } = useOpenbookTwap(); - const { program: openbookClient } = useOpenbook(); const isBidSide = isBid(order); const balance = isBidSide ? order.account.position.bidsBaseLots : order.account.position.asksBaseLots; + const originalBalance = useRef(balance); const [isSettling, setIsSettling] = useState(false); const [isClosing, setIsClosing] = useState(false); @@ -60,20 +61,24 @@ export function ProposalUnsettledOrderRow({ order }: { order: OpenOrdersAccountW if (!txs) return; await sender.send(txs); - // TODO: the balance is already at 0 when this runs, so the math doesn't work right - // const relevantMint = isBidSide - // ? marketAccount.account.quoteMint - // : marketAccount.account.baseMint; - // setBalanceByMint(relevantMint, (oldBalance) => { - // const newAmount = oldBalance.uiAmount + balance.toNumber(); - // return { - // ...oldBalance, - // amount: newAmount.toString(), - // uiAmount: newAmount, - // uiAmountString: newAmount.toString(), - // }; - // }); - await fetchNonOpenOrders(wallet.publicKey, openbookClient.program, proposal, markets); + const relevantMint = isBidSide + ? marketAccount.account.quoteMint + : marketAccount.account.baseMint; + setBalanceByMint(relevantMint, (oldBalance) => { + const newAmount = oldBalance.uiAmount + originalBalance.current.toNumber(); + return { + ...oldBalance, + amount: newAmount.toString(), + uiAmount: newAmount, + uiAmountString: newAmount.toString(), + }; + }); + await queryClient.refetchQueries({ + queryKey: [ + `fetchProposalClosedOrders-${proposal?.publicKey}-${wallet.publicKey.toString()}`, + ], + exact: true, + }); } finally { setIsSettling(false); } diff --git a/components/Proposals/ProposalDetailCard.tsx b/components/Proposals/ProposalDetailCard.tsx index bc786a6..374671d 100644 --- a/components/Proposals/ProposalDetailCard.tsx +++ b/components/Proposals/ProposalDetailCard.tsx @@ -40,7 +40,7 @@ import ExternalLink from '../ExternalLink'; import MarketsBalances from './MarketsBalances'; import classes from '../../app/globals.module.css'; import { useTokens } from '../../hooks/useTokens'; -import { isClosableOrder, isEmptyOrder, isOpenOrder, isPartiallyFilled } from '../../lib/openbook'; +import { isClosableOrder, isPartiallyFilled } from '../../lib/openbook'; import { useOpenbookTwap } from '../../hooks/useOpenbookTwap'; import { Proposal } from '../../lib/types'; import { ProposalCountdown } from './ProposalCountdown'; diff --git a/components/Proposals/ProposalOrdersCard.tsx b/components/Proposals/ProposalOrdersCard.tsx index b6d1801..9e2ae89 100644 --- a/components/Proposals/ProposalOrdersCard.tsx +++ b/components/Proposals/ProposalOrdersCard.tsx @@ -14,8 +14,10 @@ import { ProposalUnsettledOrdersTab } from '@/components/Orders/ProposalUnsettle import { ProposalUncrankedOrdersTab } from '@/components/Orders/ProposalUncrankedOrdersTab'; import { useProposalMarkets } from '@/contexts/ProposalMarketsContext'; import { useOpenbook } from '@/hooks/useOpenbook'; +import { useCallback } from 'react'; export function ProposalOrdersCard() { + const { publicKey: owner } = useWallet(); const { proposal } = useProposal(); const { markets, @@ -23,6 +25,7 @@ export function ProposalOrdersCard() { unsettledOrders, uncrankedOrders: unCrankedOrders, refreshUserOpenOrders, + fetchNonOpenOrders, } = useProposalMarkets(); const { program: openBookClient } = useOpenbook(); @@ -41,6 +44,15 @@ export function ProposalOrdersCard() { } }; + const onTabChange = useCallback( + (event: string | null) => { + if ((event === 'unsettled' || event === 'uncranked') && owner) { + fetchNonOpenOrders(owner, openBookClient.program, proposal, markets); + } + }, + [!!owner], + ); + return !proposal || !markets || !openOrders ? ( @@ -77,7 +89,7 @@ export function ProposalOrdersCard() { - + Open Uncranked diff --git a/contexts/ProposalMarketsContext.tsx b/contexts/ProposalMarketsContext.tsx index 1711f92..35b07de 100644 --- a/contexts/ProposalMarketsContext.tsx +++ b/contexts/ProposalMarketsContext.tsx @@ -163,29 +163,32 @@ export function ProposalMarketsProvider({ proposal: Proposal | undefined, markets: Markets | undefined, ) => { - const fetchProposalClosedOrders = async () => { - if (!openbook || !proposal || !markets) { - return []; - } - const passOrders = await openbook.account.openOrdersAccount.all([ - { memcmp: { offset: 8, bytes: owner.toBase58() } }, - { memcmp: { offset: 40, bytes: proposal.account.openbookPassMarket.toBase58() } }, - ]); - const passUnsettledOrders = passOrders.filter((o) => isEmptyOrder(o)); - const passUncrankedOrders = passOrders.filter((o) => isCompletedOrder(o, markets)); - const failOrders = await openbook.account.openOrdersAccount.all([ - { memcmp: { offset: 8, bytes: owner.toBase58() } }, - { memcmp: { offset: 40, bytes: proposal.account.openbookFailMarket.toBase58() } }, - ]); - const failUnsettledOrders = failOrders.filter((o) => isEmptyOrder(o)); - const failUncrankedOrders = failOrders.filter((o) => isCompletedOrder(o, markets)); - return [passUnsettledOrders, passUncrankedOrders, failUnsettledOrders, failUncrankedOrders]; - }; - const nonOpenOrders = await client.fetchQuery({ - queryKey: [`fetchProposalClosedOrders-${proposal?.publicKey}`], - queryFn: () => fetchProposalClosedOrders(), - staleTime: 1_000, + queryKey: [`fetchProposalClosedOrders-${proposal?.publicKey}-${owner.toString()}`], + queryFn: async () => { + if (!openbook || !proposal || !markets) { + return []; + } + const passOrders = await openbook.account.openOrdersAccount.all([ + { memcmp: { offset: 8, bytes: owner.toBase58() } }, + { memcmp: { offset: 40, bytes: proposal.account.openbookPassMarket.toBase58() } }, + ]); + const passUnsettledOrders = passOrders.filter((o) => isEmptyOrder(o)); + const passUncrankedOrders = passOrders.filter((o) => isCompletedOrder(o, markets)); + const failOrders = await openbook.account.openOrdersAccount.all([ + { memcmp: { offset: 8, bytes: owner.toBase58() } }, + { memcmp: { offset: 40, bytes: proposal.account.openbookFailMarket.toBase58() } }, + ]); + const failUnsettledOrders = failOrders.filter((o) => isEmptyOrder(o)); + const failUncrankedOrders = failOrders.filter((o) => isCompletedOrder(o, markets)); + return [ + passUnsettledOrders, + passUncrankedOrders, + failUnsettledOrders, + failUncrankedOrders, + ]; + }, + staleTime: 30_000, }); if (nonOpenOrders.length > 0) { @@ -865,20 +868,23 @@ export function ProposalMarketsProvider({ if (!cancelAndSettleTxs) return; const txsSent = await sender.send([...cancelAndSettleTxs]); - // if (txsSent.length !== 0) { - // //update order in state - // const cancelledOrderIndex = openOrders.findIndex( - // (o) => o.account.accountNum === order.account.accountNum, - // ); - // const cancelledOrder: OpenOrdersAccountWithKey | undefined = openOrders[cancelledOrderIndex]; - // if (cancelledOrder) { - // openOrders[cancelledOrderIndex].account.openOrders[0].isFree = 1; - // openOrders[cancelledOrderIndex].account.position.baseFreeNative = new BN(0); - // openOrders[cancelledOrderIndex].account.position.quoteFreeNative = new BN(0); - // setUnsettledOrders([...unsettledOrders, cancelledOrder]); - // setOpenOrders(openOrders.filter((o, i) => i !== cancelledOrderIndex)); - // } - // } + if (txsSent.length !== 0) { + //update order in state + //FYI usually the websocket event comes through and does this first, this state update is a fallback + const cancelledOrderIndex = openOrders.findIndex( + (o) => o.account.accountNum === order.account.accountNum, + ); + const cancelledOrder: OpenOrdersAccountWithKey | undefined = + openOrders[cancelledOrderIndex]; + //if this order element is undefined, it usually means the WS event happened first + if (cancelledOrder) { + openOrders[cancelledOrderIndex].account.openOrders[0].isFree = 1; + openOrders[cancelledOrderIndex].account.position.baseFreeNative = new BN(0); + openOrders[cancelledOrderIndex].account.position.quoteFreeNative = new BN(0); + setUnsettledOrders([...unsettledOrders, cancelledOrder]); + setOpenOrders(openOrders.filter((o, i) => i !== cancelledOrderIndex)); + } + } return txsSent; } catch (err) { console.error(err); From 6c58997eb443061640b36b52bc13ea5ac3701153 Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Wed, 13 Mar 2024 09:59:47 -0500 Subject: [PATCH 05/18] Fix: sorting lint --- components/Orders/ProposalUnsettledOrderRow.tsx | 2 +- components/Proposals/ProposalOrdersCard.tsx | 14 ++++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/components/Orders/ProposalUnsettledOrderRow.tsx b/components/Orders/ProposalUnsettledOrderRow.tsx index ede63dd..7dfaec2 100644 --- a/components/Orders/ProposalUnsettledOrderRow.tsx +++ b/components/Orders/ProposalUnsettledOrderRow.tsx @@ -12,6 +12,7 @@ import { import { useWallet } from '@solana/wallet-adapter-react'; import { Icon3dRotate, IconWriting, Icon12Hours, IconAssemblyOff } from '@tabler/icons-react'; import { BN } from '@coral-xyz/anchor'; +import { useQueryClient } from '@tanstack/react-query'; import { OpenOrdersAccountWithKey } from '@/lib/types'; import { useExplorerConfiguration } from '@/hooks/useExplorerConfiguration'; import { useOpenbookTwap } from '@/hooks/useOpenbookTwap'; @@ -21,7 +22,6 @@ import { useProposal } from '@/contexts/ProposalContext'; import { isBid, isPartiallyFilled, isPass } from '@/lib/openbook'; import { useBalances } from '../../contexts/BalancesContext'; import { useProposalMarkets } from '@/contexts/ProposalMarketsContext'; -import { useQueryClient } from '@tanstack/react-query'; export function ProposalUnsettledOrderRow({ order }: { order: OpenOrdersAccountWithKey }) { const queryClient = useQueryClient(); diff --git a/components/Proposals/ProposalOrdersCard.tsx b/components/Proposals/ProposalOrdersCard.tsx index 9e2ae89..5d102fc 100644 --- a/components/Proposals/ProposalOrdersCard.tsx +++ b/components/Proposals/ProposalOrdersCard.tsx @@ -1,20 +1,14 @@ import { ActionIcon, Group, Loader, Stack, Tabs, Text } from '@mantine/core'; import { useWallet } from '@solana/wallet-adapter-react'; import { IconRefresh } from '@tabler/icons-react'; -import { useProposal } from '@/contexts/ProposalContext'; -import { - isCompletedOrder, - isEmptyOrder, - isOpenOrder, - totalMetaInOrder, - totalUsdcInOrder, -} from '@/lib/openbook'; +import { useCallback } from 'react'; import { ProposalOpenOrdersTab } from '@/components/Orders/ProposalOpenOrdersTab'; -import { ProposalUnsettledOrdersTab } from '@/components/Orders/ProposalUnsettledOrdersTab'; import { ProposalUncrankedOrdersTab } from '@/components/Orders/ProposalUncrankedOrdersTab'; +import { ProposalUnsettledOrdersTab } from '@/components/Orders/ProposalUnsettledOrdersTab'; +import { useProposal } from '@/contexts/ProposalContext'; import { useProposalMarkets } from '@/contexts/ProposalMarketsContext'; import { useOpenbook } from '@/hooks/useOpenbook'; -import { useCallback } from 'react'; +import { totalMetaInOrder, totalUsdcInOrder } from '@/lib/openbook'; export function ProposalOrdersCard() { const { publicKey: owner } = useWallet(); From ad88c11ba5a808bc2180701b77068f97abddf540 Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Wed, 13 Mar 2024 10:14:06 -0500 Subject: [PATCH 06/18] Fix: build error --- contexts/ProposalContext.tsx | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/contexts/ProposalContext.tsx b/contexts/ProposalContext.tsx index 5c5f530..2070e7b 100644 --- a/contexts/ProposalContext.tsx +++ b/contexts/ProposalContext.tsx @@ -7,10 +7,7 @@ import { } from '@solana/spl-token'; import { notifications } from '@mantine/notifications'; import { useQueryClient } from '@tanstack/react-query'; -import { - Proposal, - ProposalAccountWithKey, -} from '@/lib/types'; +import { Proposal, ProposalAccountWithKey } from '@/lib/types'; import { useAutocrat } from '@/contexts/AutocratContext'; import { useConditionalVault } from '@/hooks/useConditionalVault'; import { useOpenbookTwap } from '@/hooks/useOpenbookTwap'; @@ -57,10 +54,9 @@ export function ProposalProvider({ fromProposal?: ProposalAccountWithKey; }) { const client = useQueryClient(); - const { autocratProgram, dao, daoState, daoTreasury, proposals } = - useAutocrat(); + const { autocratProgram, dao, daoState, daoTreasury, proposals } = useAutocrat(); const { connection } = useConnection(); - const { markets, fetchMarketsInfo, fetchOpenOrders } = useProposalMarkets(); + const { markets, fetchMarketsInfo } = useProposalMarkets(); const wallet = useWallet(); const sender = useTransactionSender(); const { @@ -116,12 +112,12 @@ export function ProposalProvider({ const userBasePass = getAssociatedTokenAddressSync( baseVault.conditionalOnFinalizeTokenMint, wallet.publicKey, - true + true, ); const userQuotePass = getAssociatedTokenAddressSync( quoteVault.conditionalOnFinalizeTokenMint, wallet.publicKey, - true + true, ); const metaTokenAccount = getAssociatedTokenAddressSync(metaMint, wallet.publicKey, true); @@ -270,14 +266,13 @@ export function ProposalProvider({ if (!passTxs || !failTxs) return; const txs = [...passTxs, ...failTxs].filter(Boolean); await sender.send(txs as VersionedTransaction[]); - fetchOpenOrders(wallet.publicKey); } catch (err) { console.error(err); } finally { setIsCranking(false); } }, - [markets, proposal, wallet.publicKey, sender, crankMarketTransactions, fetchOpenOrders], + [markets, proposal, wallet.publicKey, sender, crankMarketTransactions], ); return ( From f568ee34f6834acc8249a74f4340ebcba703c9eb Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Wed, 13 Mar 2024 10:16:31 -0500 Subject: [PATCH 07/18] Rm: silly comment --- contexts/ProposalMarketsContext.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/contexts/ProposalMarketsContext.tsx b/contexts/ProposalMarketsContext.tsx index 35b07de..ab4f834 100644 --- a/contexts/ProposalMarketsContext.tsx +++ b/contexts/ProposalMarketsContext.tsx @@ -796,7 +796,6 @@ export function ProposalMarketsProvider({ } }, [orderBookObject]); - // need to put this in a useEffect that takes all the parameters and passes them on down baby const listenOrderBooks = async ( proposal: Proposal, markets: Markets, From 67038aa8e97384915e7c61b5069db5e2bfc8f51e Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Wed, 13 Mar 2024 10:17:04 -0500 Subject: [PATCH 08/18] rm: more comments --- contexts/ProposalMarketsContext.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/contexts/ProposalMarketsContext.tsx b/contexts/ProposalMarketsContext.tsx index ab4f834..45e9164 100644 --- a/contexts/ProposalMarketsContext.tsx +++ b/contexts/ProposalMarketsContext.tsx @@ -823,7 +823,6 @@ export function ProposalMarketsProvider({ }, ]; // Setup Websocket subscription for the two sides - // TODO pass in markets here to consume order book side try { const subscriptionIds = sides.map((side) => provider.connection.onAccountChange( @@ -893,7 +892,6 @@ export function ProposalMarketsProvider({ ); useEffect(() => { - // TODO, TEST IT!! const handleOrderBooklistening = async () => { if (!wsConnected && proposal && markets && openBookProgram) { // connect for both pass and fail market order books From 8ee29c5837f8f1b64d79e5ea7181f72b461e1b88 Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Wed, 13 Mar 2024 10:58:13 -0500 Subject: [PATCH 09/18] Fix: balance from row settling was WRONG --- .../Orders/ProposalUnsettledOrderRow.tsx | 59 ++++++++----------- contexts/ProposalMarketsContext.tsx | 10 +--- 2 files changed, 27 insertions(+), 42 deletions(-) diff --git a/components/Orders/ProposalUnsettledOrderRow.tsx b/components/Orders/ProposalUnsettledOrderRow.tsx index 7dfaec2..b16f0fe 100644 --- a/components/Orders/ProposalUnsettledOrderRow.tsx +++ b/components/Orders/ProposalUnsettledOrderRow.tsx @@ -1,4 +1,4 @@ -import { useCallback, useRef, useState } from 'react'; +import { useCallback, useState } from 'react'; import { ActionIcon, Group, @@ -12,7 +12,6 @@ import { import { useWallet } from '@solana/wallet-adapter-react'; import { Icon3dRotate, IconWriting, Icon12Hours, IconAssemblyOff } from '@tabler/icons-react'; import { BN } from '@coral-xyz/anchor'; -import { useQueryClient } from '@tanstack/react-query'; import { OpenOrdersAccountWithKey } from '@/lib/types'; import { useExplorerConfiguration } from '@/hooks/useExplorerConfiguration'; import { useOpenbookTwap } from '@/hooks/useOpenbookTwap'; @@ -22,10 +21,10 @@ import { useProposal } from '@/contexts/ProposalContext'; import { isBid, isPartiallyFilled, isPass } from '@/lib/openbook'; import { useBalances } from '../../contexts/BalancesContext'; import { useProposalMarkets } from '@/contexts/ProposalMarketsContext'; +import { useOpenbook } from '@/hooks/useOpenbook'; export function ProposalUnsettledOrderRow({ order }: { order: OpenOrdersAccountWithKey }) { - const queryClient = useQueryClient(); - const { markets } = useProposalMarkets(); + const { markets, fetchNonOpenOrders } = useProposalMarkets(); const theme = useMantineTheme(); const sender = useTransactionSender(); const wallet = useWallet(); @@ -33,27 +32,34 @@ export function ProposalUnsettledOrderRow({ order }: { order: OpenOrdersAccountW const { generateExplorerLink } = useExplorerConfiguration(); const { proposal, crankMarkets, isCranking } = useProposal(); const { settleFundsTransactions, closeOpenOrdersAccountTransactions } = useOpenbookTwap(); + const { program: openbookClient } = useOpenbook(); const isBidSide = isBid(order); - const balance = isBidSide - ? order.account.position.bidsBaseLots - : order.account.position.asksBaseLots; - const originalBalance = useRef(balance); + const pass = proposal ? order.account.market.equals(proposal.account.openbookPassMarket) : null; const [isSettling, setIsSettling] = useState(false); const [isClosing, setIsClosing] = useState(false); + if (!markets || !proposal) { + return null; + } + + const relevantMarket = pass ? markets.pass : markets.fail; + const marketAccount = pass + ? { account: relevantMarket, publicKey: proposal.account.openbookPassMarket } + : { account: relevantMarket, publicKey: proposal.account.openbookFailMarket }; + const [baseBalance, quoteBalance] = [ + order.account.position.baseFreeNative.toNumber() / 10 ** relevantMarket.baseDecimals, + order.account.position.quoteFreeNative / 10 ** relevantMarket.quoteDecimals, + ]; + const handleSettleFunds = useCallback(async () => { if (!proposal || !markets || !wallet?.publicKey) return; setIsSettling(true); try { - const pass = order.account.market.equals(proposal.account.openbookPassMarket); - const marketAccount = pass - ? { account: markets.pass, publicKey: proposal.account.openbookPassMarket } - : { account: markets.fail, publicKey: proposal.account.openbookFailMarket }; const txs = await settleFundsTransactions( order.account.accountNum, - pass, + pass ?? false, proposal, marketAccount, ); @@ -61,11 +67,11 @@ export function ProposalUnsettledOrderRow({ order }: { order: OpenOrdersAccountW if (!txs) return; await sender.send(txs); - const relevantMint = isBidSide - ? marketAccount.account.quoteMint - : marketAccount.account.baseMint; + const [relevantMint, relevantBalance] = isBidSide + ? [marketAccount.account.quoteMint, quoteBalance] + : [marketAccount.account.baseMint, baseBalance]; setBalanceByMint(relevantMint, (oldBalance) => { - const newAmount = oldBalance.uiAmount + originalBalance.current.toNumber(); + const newAmount = (oldBalance.uiAmount ?? 0) + relevantBalance; return { ...oldBalance, amount: newAmount.toString(), @@ -73,12 +79,7 @@ export function ProposalUnsettledOrderRow({ order }: { order: OpenOrdersAccountW uiAmountString: newAmount.toString(), }; }); - await queryClient.refetchQueries({ - queryKey: [ - `fetchProposalClosedOrders-${proposal?.publicKey}-${wallet.publicKey.toString()}`, - ], - exact: true, - }); + await fetchNonOpenOrders(wallet.publicKey, openbookClient.program, proposal, markets); } finally { setIsSettling(false); } @@ -128,16 +129,8 @@ export function ProposalUnsettledOrderRow({ order }: { order: OpenOrdersAccountW - - {`${order.account.position.baseFreeNative.toNumber() / 1_000_000_000} ${ - isPass(order, proposal) ? 'pMETA' : 'fMETA' - }`} - - - {`${order.account.position.quoteFreeNative / 1_000_000} ${ - isPass(order, proposal) ? 'pUSDC' : 'fUSDC' - }`} - + {`${baseBalance} ${isPass(order, proposal) ? `pMETA` : 'fMETA'}`} + {`${quoteBalance} ${isPass(order, proposal) ? 'pUSDC' : 'fUSDC'}`} diff --git a/contexts/ProposalMarketsContext.tsx b/contexts/ProposalMarketsContext.tsx index 45e9164..ff2fab9 100644 --- a/contexts/ProposalMarketsContext.tsx +++ b/contexts/ProposalMarketsContext.tsx @@ -188,7 +188,7 @@ export function ProposalMarketsProvider({ failUncrankedOrders, ]; }, - staleTime: 30_000, + staleTime: 5_000, }); if (nonOpenOrders.length > 0) { @@ -564,14 +564,6 @@ export function ProposalMarketsProvider({ const txsSent = await sender.send(placeTxs); await fetchMarketsInfo(); - await refreshUserOpenOrders( - openBookClient, - proposal, - markets.passBids, - markets.passAsks, - markets.failBids, - markets.failAsks, - ); return txsSent; } catch (err) { console.error(err); From 8cd365bafa4d8063fc48aaf2f6dc8554e8e49965 Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Wed, 13 Mar 2024 14:02:30 -0500 Subject: [PATCH 10/18] Fix: single quote lint --- components/Orders/ProposalUnsettledOrderRow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/Orders/ProposalUnsettledOrderRow.tsx b/components/Orders/ProposalUnsettledOrderRow.tsx index b16f0fe..3735f50 100644 --- a/components/Orders/ProposalUnsettledOrderRow.tsx +++ b/components/Orders/ProposalUnsettledOrderRow.tsx @@ -129,7 +129,7 @@ export function ProposalUnsettledOrderRow({ order }: { order: OpenOrdersAccountW - {`${baseBalance} ${isPass(order, proposal) ? `pMETA` : 'fMETA'}`} + {`${baseBalance} ${isPass(order, proposal) ? 'pMETA' : 'fMETA'}`} {`${quoteBalance} ${isPass(order, proposal) ? 'pUSDC' : 'fUSDC'}`} From a2586a62fd2a4a32f3ada6d3b0abd72f625f119d Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Wed, 13 Mar 2024 15:07:21 -0500 Subject: [PATCH 11/18] Fix: usdc bugs --- components/Markets/ConditionalMarketCard.tsx | 3 ++- components/Orders/ProposalOpenOrderRow.tsx | 3 ++- components/Orders/ProposalUnsettledOrderRow.tsx | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/components/Markets/ConditionalMarketCard.tsx b/components/Markets/ConditionalMarketCard.tsx index bfa7da9..9a802f2 100644 --- a/components/Markets/ConditionalMarketCard.tsx +++ b/components/Markets/ConditionalMarketCard.tsx @@ -286,8 +286,9 @@ export function ConditionalMarketCard({ const relevantMint = isAskSide ? marketAccount.account.baseMint : marketAccount.account.quoteMint; + const balanceChange = isAskSide ? amount : _orderPrice(); setBalanceByMint(relevantMint, (oldBalance) => { - const newAmount = (oldBalance.uiAmount ?? 0) - amount; + const newAmount = (oldBalance.uiAmount ?? 0) - balanceChange; return { ...oldBalance, amount: newAmount.toString(), diff --git a/components/Orders/ProposalOpenOrderRow.tsx b/components/Orders/ProposalOpenOrderRow.tsx index c592167..a7ace90 100644 --- a/components/Orders/ProposalOpenOrderRow.tsx +++ b/components/Orders/ProposalOpenOrderRow.tsx @@ -61,11 +61,12 @@ export function ProposalOpenOrderRow({ order }: { order: OpenOrdersAccountWithKe setIsCanceling(true); const txsSent = await cancelAndSettleOrder(order, marketAccount.publicKey); if (txsSent && txsSent.length > 0) { + const balanceChange = isBidSide ? order.account.openOrders[0].lockedPrice : balance; const relevantMint = isBidSide ? marketAccount.account.quoteMint : marketAccount.account.baseMint; setBalanceByMint(relevantMint, (oldBalance) => { - const newAmount = oldBalance.uiAmount + balance.toNumber(); + const newAmount = oldBalance.uiAmount + balanceChange.toNumber(); return { ...oldBalance, amount: newAmount.toString(), diff --git a/components/Orders/ProposalUnsettledOrderRow.tsx b/components/Orders/ProposalUnsettledOrderRow.tsx index 3735f50..a86d822 100644 --- a/components/Orders/ProposalUnsettledOrderRow.tsx +++ b/components/Orders/ProposalUnsettledOrderRow.tsx @@ -70,8 +70,9 @@ export function ProposalUnsettledOrderRow({ order }: { order: OpenOrdersAccountW const [relevantMint, relevantBalance] = isBidSide ? [marketAccount.account.quoteMint, quoteBalance] : [marketAccount.account.baseMint, baseBalance]; + const balanceChange = isBidSide ? order.account.openOrders[0].lockedPrice : relevantBalance; setBalanceByMint(relevantMint, (oldBalance) => { - const newAmount = (oldBalance.uiAmount ?? 0) + relevantBalance; + const newAmount = (oldBalance.uiAmount ?? 0) + balanceChange; return { ...oldBalance, amount: newAmount.toString(), From d6727db1a5dac8adb47d0515c714dfb682d50a66 Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Wed, 13 Mar 2024 15:53:04 -0500 Subject: [PATCH 12/18] Feat: rm extra open orders setting --- contexts/ProposalMarketsContext.tsx | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/contexts/ProposalMarketsContext.tsx b/contexts/ProposalMarketsContext.tsx index ff2fab9..da4ca20 100644 --- a/contexts/ProposalMarketsContext.tsx +++ b/contexts/ProposalMarketsContext.tsx @@ -304,19 +304,6 @@ export function ProposalMarketsProvider({ fetchMarketsInfo(); }, [proposal]); - useEffect(() => { - if (proposal && wallet.publicKey && markets) { - refreshUserOpenOrders( - openBookClient, - proposal, - markets.passBids, - markets.passAsks, - markets.failBids, - markets.failAsks, - ); - } - }, [markets, proposal]); - useEffect(() => { if (!markets && proposal) { fetchMarketsInfo(); From c44d8c763c2fc94520a66b1a30f377b987960b88 Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Wed, 13 Mar 2024 16:31:08 -0500 Subject: [PATCH 13/18] Fix: price lag on conditional order --- components/Markets/ConditionalMarketCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/Markets/ConditionalMarketCard.tsx b/components/Markets/ConditionalMarketCard.tsx index 9a802f2..a3f6093 100644 --- a/components/Markets/ConditionalMarketCard.tsx +++ b/components/Markets/ConditionalMarketCard.tsx @@ -300,7 +300,7 @@ export function ConditionalMarketCard({ } finally { setIsPlacingOrder(false); } - }, [placeOrder, amount, isLimitOrder, isPassMarket, isAskSide]); + }, [placeOrder, amount, isLimitOrder, isPassMarket, isAskSide, price]); const getObservableTwap = () => { if (isPassMarket) { From 7e3884eb51fbdd5d21196b440095a9e47e9848f6 Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Wed, 13 Mar 2024 16:41:06 -0500 Subject: [PATCH 14/18] Fix: smooth out user open order transitions --- contexts/ProposalMarketsContext.tsx | 52 +++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/contexts/ProposalMarketsContext.tsx b/contexts/ProposalMarketsContext.tsx index da4ca20..3d74106 100644 --- a/contexts/ProposalMarketsContext.tsx +++ b/contexts/ProposalMarketsContext.tsx @@ -304,6 +304,19 @@ export function ProposalMarketsProvider({ fetchMarketsInfo(); }, [proposal]); + useEffect(() => { + if (proposal && wallet.publicKey && markets) { + refreshUserOpenOrders( + openBookClient, + proposal, + markets.passBids, + markets.passAsks, + markets.failBids, + markets.failAsks, + ); + } + }, [!!markets, !!proposal]); + useEffect(() => { if (!markets && proposal) { fetchMarketsInfo(); @@ -527,7 +540,8 @@ export function ProposalMarketsProvider({ }; return order; - }); + }) + .sort((a, b) => (a.account.accountNum < b.account.accountNum ? 1 : -1)); setOpenOrders(userOrders); } }, @@ -584,6 +598,7 @@ export function ProposalMarketsProvider({ ctx: Context, ): number[][] | undefined => { try { + console.log('consuming order book side', side); const isPassMarket = market === proposal?.account.openbookPassMarket; const leafNodes = openBookProgram.coder.accounts.decode( 'bookSide', @@ -650,28 +665,35 @@ export function ProposalMarketsProvider({ return [[0, 0]]; } // Update our values for the orderbook and order list + //TODO handle what happens if we get an update for a part of the order book that doesn't include the latest... + // we need to wait for both updates to come through before updating the user rows... if (isPassMarket) { if (side === 'asks') { if (markets) { + // update markets and orders + markets.passAsks = leafNodeSide; + setMarkets({ ...markets }); refreshUserOpenOrders( openBookClient, proposal, - markets?.passBids, + markets.passBids, leafNodeSide, - markets?.failBids, - markets?.failAsks, + markets.failBids, + markets.failAsks, ); } setPassAsks(__side); } else { if (markets) { + markets.passBids = leafNodeSide; + setMarkets({ ...markets }); refreshUserOpenOrders( openBookClient, proposal, leafNodeSide, - markets?.passAsks, - markets?.failBids, - markets?.failAsks, + markets.passAsks, + markets.failBids, + markets.failAsks, ); } setPassBids(__side); @@ -680,25 +702,29 @@ export function ProposalMarketsProvider({ } else { if (side === 'asks') { if (markets && proposal) { + markets.failAsks = leafNodeSide; + setMarkets({ ...markets }); refreshUserOpenOrders( openBookClient, proposal, - markets?.passBids, - markets?.passAsks, - markets?.failBids, + markets.passBids, + markets.passAsks, + markets.failBids, leafNodeSide, ); } setFailAsks(__side); } else { if (markets && proposal) { + markets.failBids = leafNodeSide; + setMarkets({ ...markets }); refreshUserOpenOrders( openBookClient, proposal, - markets?.passBids, - markets?.passAsks, + markets.passBids, + markets.passAsks, leafNodeSide, - markets?.failAsks, + markets.failAsks, ); } setFailBids(__side); From 3f221bbb5a2967fbcf10721b570937b5db8f02b4 Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Wed, 13 Mar 2024 16:49:26 -0500 Subject: [PATCH 15/18] Feat: comma in numeral for conditional market card --- components/Markets/ConditionalMarketCard.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/components/Markets/ConditionalMarketCard.tsx b/components/Markets/ConditionalMarketCard.tsx index a3f6093..244174a 100644 --- a/components/Markets/ConditionalMarketCard.tsx +++ b/components/Markets/ConditionalMarketCard.tsx @@ -100,7 +100,11 @@ export function ConditionalMarketCard({ const updateOrderValue = () => { if (!Number.isNaN(amount) && !Number.isNaN(+price)) { - const _price = parseFloat((+price * amount).toString()).toFixed(2); + const formatter = new Intl.NumberFormat('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); + const _price = formatter.format(parseFloat((+price * amount).toString())); setOrderValue(_price); } else { setOrderValue('0'); @@ -600,7 +604,7 @@ export function ConditionalMarketCard({ )} <> - Total Order Value ${orderValue} + Total Order Value {orderValue} From 358f3a72fd85e6773e092f48f5e81d4a49c04f7e Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Wed, 13 Mar 2024 22:29:11 -0500 Subject: [PATCH 16/18] Fix: bad math on settlement --- components/Orders/ProposalOpenOrderRow.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/components/Orders/ProposalOpenOrderRow.tsx b/components/Orders/ProposalOpenOrderRow.tsx index a7ace90..2725181 100644 --- a/components/Orders/ProposalOpenOrderRow.tsx +++ b/components/Orders/ProposalOpenOrderRow.tsx @@ -28,6 +28,7 @@ import { useProposal } from '@/contexts/ProposalContext'; import { isBid, isPartiallyFilled, isPass } from '@/lib/openbook'; import { useProposalMarkets } from '@/contexts/ProposalMarketsContext'; import { useBalances } from '@/contexts/BalancesContext'; +import { BN } from '@coral-xyz/anchor'; export function ProposalOpenOrderRow({ order }: { order: OpenOrdersAccountWithKey }) { const theme = useMantineTheme(); @@ -61,7 +62,10 @@ export function ProposalOpenOrderRow({ order }: { order: OpenOrdersAccountWithKe setIsCanceling(true); const txsSent = await cancelAndSettleOrder(order, marketAccount.publicKey); if (txsSent && txsSent.length > 0) { - const balanceChange = isBidSide ? order.account.openOrders[0].lockedPrice : balance; + const quoteLots = new BN(10 ** marketAccount.account.quoteDecimals); + const balanceChange = isBidSide + ? (order.account.openOrders[0].lockedPrice / quoteLots) * 100 + : balance; const relevantMint = isBidSide ? marketAccount.account.quoteMint : marketAccount.account.baseMint; From 18f0cf97a98819c437941524dca2cf716bf39caa Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Thu, 14 Mar 2024 10:07:30 -0500 Subject: [PATCH 17/18] Fix: balances resetting to 0 --- contexts/BalancesContext.tsx | 92 +++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 43 deletions(-) diff --git a/contexts/BalancesContext.tsx b/contexts/BalancesContext.tsx index c4fc21a..b944cd7 100644 --- a/contexts/BalancesContext.tsx +++ b/contexts/BalancesContext.tsx @@ -1,4 +1,4 @@ -import { createContext, useCallback, useContext, useMemo } from 'react'; +import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { useConnection, useWallet } from '@solana/wallet-adapter-react'; import { AccountInfo, PublicKey, TokenAmount } from '@solana/web3.js'; import { AccountLayout, getAssociatedTokenAddressSync } from '@solana/spl-token'; @@ -64,6 +64,7 @@ export function BalancesProvider({ children }: { children: React.ReactNode }) { const { publicKey: owner } = useWallet(); const queryClient = useQueryClient(); const { connection } = useConnection(); + const [accounts, setAccounts] = useState[]>([]); const markets = queryClient.getQueryData>(['markets']); const vaultAccounts = queryClient.getQueryData | undefined>([ 'conditionalVault', @@ -78,49 +79,54 @@ export function BalancesProvider({ children }: { children: React.ReactNode }) { [owner], ); - const accounts: SubscriptionAccount[] = useMemo(() => { - const baseDecimals = markets?.[0].baseDecimals; - const quoteDecimals = markets?.[0].quoteDecimals; - const underlyingTokenAccounts: SubscriptionAccount[] = vaultAccounts - ? [ - { - publicKey: getAta(vaultAccounts[0].underlyingTokenMint), - metaData: { - decimals: baseDecimals, - lotSize: 10 ** (baseDecimals || 0), + useEffect(() => { + if (markets && vaultAccounts) { + const baseDecimals = markets?.[0].baseDecimals; + const quoteDecimals = markets?.[0].quoteDecimals; + const underlyingTokenAccounts: SubscriptionAccount[] = vaultAccounts + ? [ + { + publicKey: getAta(vaultAccounts[0].underlyingTokenMint), + metaData: { + decimals: baseDecimals, + lotSize: 10 ** (baseDecimals || 0), + }, }, - }, - { - publicKey: getAta(vaultAccounts[1].underlyingTokenMint), - metaData: { - decimals: quoteDecimals, - lotSize: 10 ** (quoteDecimals || 0), + { + publicKey: getAta(vaultAccounts[1].underlyingTokenMint), + metaData: { + decimals: quoteDecimals, + lotSize: 10 ** (quoteDecimals || 0), + }, }, - }, - ].filter((m): m is SubscriptionAccount => !!m.publicKey) ?? [] - : []; - - const conditionalTokenAccounts = - markets - ?.flatMap((m) => [ - { - publicKey: getAta(m.baseMint), - metaData: { - decimals: baseDecimals, - lotSize: 10 ** (baseDecimals || 0), + ].filter((m): m is SubscriptionAccount => !!m.publicKey) ?? [] + : []; + + const conditionalTokenAccounts = + markets + ?.flatMap((m) => [ + { + publicKey: getAta(m.baseMint), + metaData: { + decimals: baseDecimals, + lotSize: 10 ** (baseDecimals || 0), + }, }, - }, - { - publicKey: getAta(m.quoteMint), - metaData: { - decimals: quoteDecimals, - lotSize: 10 ** (quoteDecimals || 0), + { + publicKey: getAta(m.quoteMint), + metaData: { + decimals: quoteDecimals, + lotSize: 10 ** (quoteDecimals || 0), + }, }, - }, - ]) - .filter((m): m is SubscriptionAccount => !!m.publicKey) ?? []; - return [...underlyingTokenAccounts, ...conditionalTokenAccounts]; - }, [markets, vaultAccounts, owner]); + ]) + .filter((m): m is SubscriptionAccount => !!m.publicKey) ?? []; + const newAccounts = [...underlyingTokenAccounts, ...conditionalTokenAccounts]; + if (newAccounts.length > 0) { + setAccounts(newAccounts); + } + } + }, [!!markets, !!vaultAccounts]); const accountSubscriptionCallback = ( accountInfo: AccountInfo, @@ -212,9 +218,9 @@ export function BalancesProvider({ children }: { children: React.ReactNode }) { ) { const ata = getAta(mint); if (ata) { - const balance = balances[ata.toString()].data; - if (balance) { - const newAmount = stateUpdater(balance); + const balance = balances[ata.toString()]; + if (balance.data) { + const newAmount = stateUpdater(balance.data); updateAccountState(newAmount, ata); } } From a355f708bf013e5bde6258874caaaf70ec92e405 Mon Sep 17 00:00:00 2001 From: LukasDeco Date: Thu, 14 Mar 2024 10:18:47 -0500 Subject: [PATCH 18/18] Fix: import sort --- components/Orders/ProposalOpenOrderRow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/Orders/ProposalOpenOrderRow.tsx b/components/Orders/ProposalOpenOrderRow.tsx index 2725181..abfbc12 100644 --- a/components/Orders/ProposalOpenOrderRow.tsx +++ b/components/Orders/ProposalOpenOrderRow.tsx @@ -19,6 +19,7 @@ import { IconPencilCancel, IconCheck, } from '@tabler/icons-react'; +import { BN } from '@coral-xyz/anchor'; import { OpenOrdersAccountWithKey } from '@/lib/types'; import { useExplorerConfiguration } from '@/hooks/useExplorerConfiguration'; import { useOpenbookTwap } from '@/hooks/useOpenbookTwap'; @@ -28,7 +29,6 @@ import { useProposal } from '@/contexts/ProposalContext'; import { isBid, isPartiallyFilled, isPass } from '@/lib/openbook'; import { useProposalMarkets } from '@/contexts/ProposalMarketsContext'; import { useBalances } from '@/contexts/BalancesContext'; -import { BN } from '@coral-xyz/anchor'; export function ProposalOpenOrderRow({ order }: { order: OpenOrdersAccountWithKey }) { const theme = useMantineTheme();