diff --git a/packages/trpc/src/orderbook-router.ts b/packages/trpc/src/orderbook-router.ts index b211bec2ab..8607c31ff4 100644 --- a/packages/trpc/src/orderbook-router.ts +++ b/packages/trpc/src/orderbook-router.ts @@ -69,34 +69,53 @@ export const orderbookRouter = createTRPCRouter({ const { contractAddresses, userOsmoAddress } = input; if (contractAddresses.length === 0 || userOsmoAddress.length === 0) return []; - const promises = contractAddresses.map( - async (contractOsmoAddress: string) => { - const { quoteAsset, baseAsset } = await getOrderbookDenoms({ - orderbookAddress: contractOsmoAddress, - chainList: ctx.chainList, - assetLists: ctx.assetLists, - }); - const orders = await getOrderbookActiveOrders({ - orderbookAddress: contractOsmoAddress, - userOsmoAddress: userOsmoAddress, - chainList: ctx.chainList, - baseAsset, - quoteAsset, - }); - return orders; + + const chunk = (arr: any[], size: number) => { + const result = []; + for (let i = 0; i < arr.length; i += size) { + result.push(arr.slice(i, i + size)); } - ); - promises.push( - getOrderbookHistoricalOrders({ - userOsmoAddress: input.userOsmoAddress, - assetLists: ctx.assetLists, - chainList: ctx.chainList, - }) - ); + return result; + }; + + const chunkedAddresses = chunk(contractAddresses, 8); + + const ordersByContracts: MappedLimitOrder[] = []; + for (let i = 0; i < chunkedAddresses.length; i++) { + const contractOsmoAddresses = chunkedAddresses[i]; + const orderPromises = contractOsmoAddresses.map( + async (contractOsmoAddress: string) => { + const { quoteAsset, baseAsset } = await getOrderbookDenoms({ + orderbookAddress: contractOsmoAddress, + chainList: ctx.chainList, + assetLists: ctx.assetLists, + }); + const orders = await getOrderbookActiveOrders({ + orderbookAddress: contractOsmoAddress, + userOsmoAddress: userOsmoAddress, + chainList: ctx.chainList, + baseAsset, + quoteAsset, + }); + return orders; + } + ); + if (i === chunkedAddresses.length - 1) { + orderPromises.push( + getOrderbookHistoricalOrders({ + userOsmoAddress: input.userOsmoAddress, + assetLists: ctx.assetLists, + chainList: ctx.chainList, + }) + ); + } + const newOrders = await Promise.all(orderPromises); + ordersByContracts.push(...newOrders.flat().flat()); + } + + // const ordersByContracts = await Promise.all(promises); - const ordersByContracts = await Promise.all(promises); - const allOrders = ordersByContracts.flat(); - return allOrders.sort(defaultSortOrders); + return ordersByContracts.sort(defaultSortOrders); }, cacheKey: `all-active-orders-${input.contractAddresses.join(",")}-${ input.userOsmoAddress diff --git a/packages/web/components/complex/orders-history/index.tsx b/packages/web/components/complex/orders-history/index.tsx index 87cddb6241..902d387d4d 100644 --- a/packages/web/components/complex/orders-history/index.tsx +++ b/packages/web/components/complex/orders-history/index.tsx @@ -11,16 +11,19 @@ import React, { memo, useCallback, useMemo, useRef, useState } from "react"; import { Icon } from "~/components/assets"; import { ActionsCell } from "~/components/complex/orders-history/cells/actions"; import { OrderProgressBar } from "~/components/complex/orders-history/cells/filled-progress"; +import { OrderModal } from "~/components/complex/orders-history/order-modal"; import { Intersection } from "~/components/intersection"; import { Spinner } from "~/components/loaders"; import { GenericDisclaimer } from "~/components/tooltip/generic-disclaimer"; import { Button } from "~/components/ui/button"; import { EventName } from "~/config"; import { + Breakpoint, useAmplitudeAnalytics, useFeatureFlags, useTranslation, useWalletSelect, + useWindowSize, } from "~/hooks"; import { useOrderbookAllActiveOrders, @@ -50,6 +53,10 @@ function groupOrdersByStatus(orders: MappedLimitOrder[]) { } const headers = ["order", "amount", "price", "orderPlaced", "status"]; +const mdHeaders = ["order", "status"]; + +const gridClasses = + "grid grid-cols-[80px_4fr_2fr_2fr_2fr_150px] md:grid-cols-[2fr_1fr]"; export const OrderHistory = observer(() => { const { logEvent } = useAmplitudeAnalytics({ @@ -61,6 +68,8 @@ export const OrderHistory = observer(() => { const wallet = accountStore.getWallet(accountStore.osmosisChainId); const listRef = useRef(null); const { onOpenWalletSelect, isLoading: isWalletLoading } = useWalletSelect(); + const { isMobile } = useWindowSize(Breakpoint.md); + const [selectedOrder, setSelectedOrder] = useState(); const { orders, @@ -98,7 +107,7 @@ export const OrderHistory = observer(() => { const rowVirtualizer = useWindowVirtualizer({ count: rows.length, - estimateSize: () => 84, + estimateSize: () => (isMobile ? 60 : 84), paddingStart: -220, paddingEnd: 110, overscan: 10, @@ -132,6 +141,14 @@ export const OrderHistory = observer(() => { const showConnectWallet = !wallet?.isWalletConnected && !isWalletLoading; + const onOrderSelect = useCallback( + (order: MappedLimitOrder) => { + if (!isMobile) return; + setSelectedOrder(order); + }, + [isMobile] + ); + if (showConnectWallet) { return (
@@ -191,27 +208,35 @@ export const OrderHistory = observer(() => { return (
- +
{!isLoading && ( - + {headers.map((header) => ( - ))} - )} @@ -258,6 +283,7 @@ export const OrderHistory = observer(() => { order={order} style={style} refetch={refetch} + onOrderSelect={onOrderSelect} /> ); }) @@ -271,6 +297,12 @@ export const OrderHistory = observer(() => { } }} /> + {isMobile && ( + setSelectedOrder(undefined)} + /> + )} ); }); @@ -297,14 +329,18 @@ const TableGroupHeader = ({ if (group === "filled") { return ( - + + {group === "pending" ? t("limitOrders.orderHistoryHeaders.pending") : t("limitOrders.orderHistoryHeaders.past")} - + ); }; @@ -351,10 +387,12 @@ const TableOrderRow = memo( order, style, refetch, + onOrderSelect, }: { order: MappedLimitOrder; style: Object; refetch: () => Promise; + onOrderSelect: (order: MappedLimitOrder) => void; }) => { const { t } = useTranslation(); @@ -401,7 +439,7 @@ const TableOrderRow = memo( const hourDiff = dayjs(new Date()).diff(dayjs(placed_at), "h"); return ( - + {dayDiff > 0 ? t("limitOrders.daysAgo", { days: dayDiff.toString(), @@ -418,8 +456,12 @@ const TableOrderRow = memo( } })(); return ( - - onOrderSelect(order)} + > + - - - diff --git a/packages/web/components/complex/orders-history/order-modal.tsx b/packages/web/components/complex/orders-history/order-modal.tsx new file mode 100644 index 0000000000..08da082cdf --- /dev/null +++ b/packages/web/components/complex/orders-history/order-modal.tsx @@ -0,0 +1,359 @@ +import { CoinPretty, Dec, Int, PricePretty } from "@keplr-wallet/unit"; +import { DEFAULT_VS_CURRENCY, MappedLimitOrder } from "@osmosis-labs/server"; +import classNames from "classnames"; +import dayjs from "dayjs"; +import { observer } from "mobx-react-lite"; +import React, { memo, useCallback, useMemo, useState } from "react"; + +import { FallbackImg, Icon } from "~/components/assets"; +import { Button } from "~/components/buttons"; +import { OrderProgressBar } from "~/components/complex/orders-history/cells/filled-progress"; +import { IconButton } from "~/components/ui/button"; +import { RecapRow } from "~/components/ui/recap-row"; +import { t } from "~/hooks"; +import { ModalBase } from "~/modals"; +import { useStore } from "~/stores"; +import { theme } from "~/tailwind.config"; +import { + formatFiatPrice, + formatPretty, + getPriceExtendedFormatOptions, +} from "~/utils/formatter"; + +interface OrderModalProps { + order?: MappedLimitOrder; + onRequestClose: () => void; +} + +export const OrderModal = memo(({ order, onRequestClose }: OrderModalProps) => { + return ( + + + + ); +}); + +interface OrderDetailsProps { + order?: MappedLimitOrder; + isModal: boolean; + onRequestClose: () => void; +} + +const OrderDetails = observer( + ({ order, isModal, onRequestClose }: OrderDetailsProps) => { + const { accountStore } = useStore(); + const account = accountStore.getWallet(accountStore.osmosisChainId); + const [broadcasting, setBroadcasting] = useState(false); + + const tokenIn = useMemo(() => { + if (!order) return; + + return order.order_direction === "ask" + ? order.baseAsset + : order.quoteAsset; + }, [order]); + + const tokenOut = useMemo(() => { + if (!order) return; + + return order.order_direction === "ask" + ? order.quoteAsset + : order.baseAsset; + }, [order]); + + const formattedMonth = dayjs(order?.placed_at).format("MMMM").slice(0, 3); + + const formattedDateDayYearHourMinute = dayjs(order?.placed_at).format( + "DD, YYYY, HH:mm" + ); + + const formattedDate = `${formattedMonth} ${formattedDateDayYearHourMinute}`; + + const statusComponent = (() => { + if (!order) return; + + switch (order?.status) { + case "open": + case "partiallyFilled": + return ( +
+ Open +
+ ); + case "cancelled": +
+ Cancelled +
; + case "filled": + return Claimable; + case "fullyClaimed": + return Filled; + default: + return; + } + })(); + + const closeOrder = useCallback(async () => { + if (!order) { + console.error("Attempted to claim and close order that does not exist"); + return; + } + + if (!account) { + console.error( + "Attempted to claim and close orders without wallet connected" + ); + return; + } + + const { tick_id, order_id, orderbookAddress } = order; + const claimMsg = { + msg: { + claim_limit: { order_id, tick_id }, + }, + contractAddress: orderbookAddress, + funds: [], + }; + const cancelMsg = { + msg: { cancel_limit: { order_id, tick_id } }, + contractAddress: orderbookAddress, + funds: [], + }; + const msgs = []; + if (order.percentFilled > order.percentClaimed) { + msgs.push(claimMsg); + } + + if (order.percentFilled.lt(new Dec(1))) msgs.push(cancelMsg); + + try { + setBroadcasting(true); + await account.cosmwasm.sendMultiExecuteContractMsg( + "executeWasm", + msgs, + undefined + ); + // await refetch(); + } catch (error) { + console.error(error); + setBroadcasting(false); + onRequestClose(); + } + }, [account, order, onRequestClose]); + + const buttonText = useMemo(() => { + if ( + order?.status !== "open" && + order?.status !== "partiallyFilled" && + order?.status !== "filled" + ) { + return; + } + + if (order?.status === "filled") { + return "Claim"; + } + + return order?.status === "open" ? "Cancel" : "Claim and Close"; + }, [order]); + + const orderAmount = useMemo(() => { + return formatFiatPrice( + new PricePretty( + DEFAULT_VS_CURRENCY, + order?.order_direction === "bid" + ? order?.placed_quantity / + Number( + new Dec(10) + .pow(new Int(order?.quoteAsset?.decimals ?? 0)) + .toString() + ) + : order?.output.quo( + new Dec(10).pow(new Int(order?.quoteAsset?.decimals ?? 0)) + ) ?? new Dec(0) + ), + 2 + ); + }, [order]); + + return ( +
+ {!isModal && ( +
+ } + onClick={onRequestClose} + /> +
+ )} +
+
+
+ +
+
+
Limit Order
+
+ {formattedDate} +
+
+
+
+
+
+
+
+ +
+
+
{t("transactions.sold")}
+
+ {tokenIn?.symbol} +
+
+
+
+
+ {formatPretty( + new CoinPretty( + { + coinDecimals: tokenIn?.decimals ?? 0, + coinDenom: tokenIn?.symbol ?? "", + coinMinimalDenom: tokenIn?.coinMinimalDenom ?? "", + }, + order?.placed_quantity ?? new Dec(0) + ) + )} +
+
{orderAmount}
+
+
+
+ +
+
+
+
+ +
+
+
{t("transactions.bought")}
+
+ {tokenOut?.symbol} +
+
+
+
+
+ {/* // TODO - clean this up to match tokenConversion */} + {formatPretty( + new CoinPretty( + { + coinDecimals: tokenOut?.decimals ?? 0, + coinDenom: tokenOut?.symbol ?? "", + coinMinimalDenom: tokenOut?.coinMinimalDenom ?? "", + }, + order?.output ?? new Dec(0) + ) + )} +
+
+ {orderAmount !== "<$0.01" && "~"} + {orderAmount} +
+
+
+
+
+
+ Price} + right={ +
+ {formatPretty( + new PricePretty( + DEFAULT_VS_CURRENCY, + order?.price ?? new Dec(0) + ), + getPriceExtendedFormatOptions(order?.price ?? new Dec(0)) + )} +
+ } + /> + Status} + right={statusComponent} + /> + {(order?.status === "open" || + order?.status === "partiallyFilled") && ( + Percent Filled} + right={ + + + + } + /> + )} +
+
+ {!!order && !!buttonText && ( +
+ +
+ )} +
+ ); + } +);
+ {header !== "amount" && ( - + {t(`limitOrders.historyTable.columns.${header}`)} )} + {!isMobile && }
-
{t("limitOrders.orderHistoryHeaders.filled")}
+ + {t("limitOrders.orderHistoryHeaders.filled")} +
- {filledOrdersCount} + + {filledOrdersCount} +
-
+
+

@@ -472,7 +514,7 @@ const TableOrderRow = memo( )}

-
+
{formatFiatPrice( new PricePretty( @@ -504,7 +546,7 @@ const TableOrderRow = memo(
+

{baseAsset?.symbol} ยท {t("limitOrders.limit")} @@ -516,7 +558,7 @@ const TableOrderRow = memo(

+

{formattedTime}

{formattedDate}

@@ -526,19 +568,22 @@ const TableOrderRow = memo(
{statusComponent} {statusString}
+