From d9bbbd5da1643ce73260a54606e5ac48cd9212da Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Sat, 29 Jun 2024 11:54:25 +0100 Subject: [PATCH 1/4] feat: added claimable orders --- .../server/src/queries/osmosis/orderbooks.ts | 3 +- packages/trpc/src/orderbook-router.ts | 215 +++++++++++------- .../orders-history/cells/filled-progress.tsx | 6 +- .../complex/orders-history/index.tsx | 12 +- .../web/hooks/limit-orders/use-orderbook.ts | 60 ++++- .../web/hooks/limit-orders/use-place-limit.ts | 6 +- packages/web/hooks/use-feature-flags.ts | 2 +- 7 files changed, 208 insertions(+), 96 deletions(-) diff --git a/packages/server/src/queries/osmosis/orderbooks.ts b/packages/server/src/queries/osmosis/orderbooks.ts index 197ecee4c5..ad273245de 100644 --- a/packages/server/src/queries/osmosis/orderbooks.ts +++ b/packages/server/src/queries/osmosis/orderbooks.ts @@ -29,6 +29,7 @@ export interface LimitOrder { etas: string; claim_bounty?: string; placed_quantity: string; + placed_at: string; } interface OrderbookActiveOrdersResponse { @@ -111,7 +112,7 @@ export const queryOrderbookTickUnrealizedCancelsById = createNodeQuery< >({ path: ({ tickIds, orderbookAddress }) => { const msg = JSON.stringify({ - tick_unrealized_cancels_by_id: { + get_unrealized_cancels: { tick_ids: tickIds, }, }); diff --git a/packages/trpc/src/orderbook-router.ts b/packages/trpc/src/orderbook-router.ts index 5345699fc8..8678ad241c 100644 --- a/packages/trpc/src/orderbook-router.ts +++ b/packages/trpc/src/orderbook-router.ts @@ -16,7 +16,7 @@ import { getAssetFromAssetList } from "@osmosis-labs/utils"; import { z } from "zod"; import { createTRPCRouter, publicProcedure } from "./api"; -import { OsmoAddressSchema } from "./parameter-types"; +import { OsmoAddressSchema, UserOsmoAddressSchema } from "./parameter-types"; const GetInfiniteLimitOrdersInputSchema = CursorPaginationSchema.merge( z.object({ @@ -33,7 +33,7 @@ export type OrderStatus = export type MappedLimitOrder = Omit< LimitOrder, - "quantity" | "placed_quantity" + "quantity" | "placed_quantity" | "placed_at" > & { quantity: number; placed_quantity: number; @@ -46,18 +46,29 @@ export type MappedLimitOrder = Omit< output: number; quoteAsset: ReturnType; baseAsset: ReturnType; + placed_at: number; }; -function mapOrderStatus(order: LimitOrder): OrderStatus { +function mapOrderStatus(order: LimitOrder, percentFilled: Dec): OrderStatus { const quantInt = parseInt(order.quantity); const placedQuantInt = parseInt(order.placed_quantity); - if (quantInt === 0) return "filled"; + if (quantInt === 0 || percentFilled.equals(new Dec(1))) return "filled"; if (quantInt === placedQuantInt) return "open"; if (quantInt < placedQuantInt) return "partiallyFilled"; return "open"; } +function defaultSortOrders( + orderA: MappedLimitOrder, + orderB: MappedLimitOrder +): number { + if (orderA.status === orderB.status) { + return orderA.placed_at - orderB.placed_at; + } + return orderA.status === "filled" ? 1 : -1; +} + async function getTickInfoAndTransformOrders( orderbookAddress: string, orders: LimitOrder[], @@ -83,88 +94,79 @@ async function getTickInfoAndTransformOrders( unrealizedCancels: unrealizedTickCancels.find((c) => c.tick_id === tick_id), })); - return orders - .map((o) => { - const { tickState, unrealizedCancels } = fullTickState.find( - ({ tickId }) => tickId === o.tick_id - ) ?? { tickState: undefined, unrealizedCancels: undefined }; - - const [tokenInAsset, tokenOutAsset] = - o.order_direction === "bid" - ? [quoteAsset, baseAsset] - : [baseAsset, quoteAsset]; + return orders.map((o) => { + const { tickState, unrealizedCancels } = fullTickState.find( + ({ tickId }) => tickId === o.tick_id + ) ?? { tickState: undefined, unrealizedCancels: undefined }; - const quantityMin = parseInt(o.quantity); - const placedQuantityMin = parseInt(o.placed_quantity); - const placedQuantity = - placedQuantityMin / 10 ** (tokenInAsset?.decimals ?? 0); - const quantity = quantityMin / 10 ** (tokenInAsset?.decimals ?? 0); + const [tokenInAsset, tokenOutAsset] = + o.order_direction === "bid" + ? [quoteAsset, baseAsset] + : [baseAsset, quoteAsset]; - const percentClaimed = new Dec( - (placedQuantityMin - quantityMin) / placedQuantityMin - ); - const [tickEtas, tickCumulativeCancelled, tickUnrealizedCancelled] = - o.order_direction === "bid" - ? [ - parseInt( - tickState?.bid_values.effective_total_amount_swapped ?? "0" - ), - parseInt( - tickState?.bid_values.cumulative_realized_cancels ?? "0" - ), - parseInt( - unrealizedCancels?.unrealized_cancels.bid_unrealized_cancels ?? - "0" - ), - ] - : [ - parseInt( - tickState?.ask_values.effective_total_amount_swapped ?? "0" - ), - parseInt( - tickState?.ask_values.cumulative_realized_cancels ?? "0" - ), - parseInt( - unrealizedCancels?.unrealized_cancels.ask_unrealized_cancels ?? - "0" - ), - ]; - const tickTotalEtas = - tickEtas + (tickUnrealizedCancelled - tickCumulativeCancelled); - const totalFilled = Math.max( - tickTotalEtas - (parseInt(o.etas) - (placedQuantityMin - quantityMin)), - 0 - ); - const percentFilled = new Dec(totalFilled / placedQuantityMin); - const price = tickToPrice(new Int(o.tick_id)); - const status = mapOrderStatus(o); + const quantityMin = parseInt(o.quantity); + const placedQuantityMin = parseInt(o.placed_quantity); + const placedQuantity = + placedQuantityMin / 10 ** (tokenInAsset?.decimals ?? 0); + const quantity = quantityMin / 10 ** (tokenInAsset?.decimals ?? 0); - const outputMin = - o.order_direction === "bid" - ? new Dec(placedQuantityMin).quo(price) - : new Dec(placedQuantityMin).mul(price); - const output = parseInt( - outputMin - .quo(new Dec(10 ** (tokenOutAsset?.decimals ?? 0))) - .truncate() - .toString() - ); - return { - ...o, - price, - quantity, - placed_quantity: placedQuantity, - percentClaimed, - totalFilled, - percentFilled, - orderbookAddress, - status, - output, - quoteAsset, - baseAsset, - }; - }) - .sort((a, b) => a.order_id - b.order_id); + const percentClaimed = new Dec( + (placedQuantityMin - quantityMin) / placedQuantityMin + ); + const [tickEtas, tickUnrealizedCancelled] = + o.order_direction === "bid" + ? [ + parseInt( + tickState?.bid_values.effective_total_amount_swapped ?? "0" + ), + parseInt( + unrealizedCancels?.unrealized_cancels.bid_unrealized_cancels ?? + "0" + ), + ] + : [ + parseInt( + tickState?.ask_values.effective_total_amount_swapped ?? "0" + ), + parseInt( + unrealizedCancels?.unrealized_cancels.ask_unrealized_cancels ?? + "0" + ), + ]; + const tickTotalEtas = tickEtas + tickUnrealizedCancelled; + const totalFilled = Math.max( + tickTotalEtas - (parseInt(o.etas) - (placedQuantityMin - quantityMin)), + 0 + ); + const percentFilled = new Dec(Math.min(totalFilled / placedQuantityMin, 1)); + const price = tickToPrice(new Int(o.tick_id)); + const status = mapOrderStatus(o, percentFilled); + const outputMin = + o.order_direction === "bid" + ? new Dec(placedQuantityMin).quo(price) + : new Dec(placedQuantityMin).mul(price); + const output = parseInt( + outputMin + .quo(new Dec(10 ** (tokenOutAsset?.decimals ?? 0))) + .truncate() + .toString() + ); + return { + ...o, + price, + quantity, + placed_quantity: placedQuantity, + percentClaimed, + totalFilled, + percentFilled, + orderbookAddress, + status, + output, + quoteAsset, + baseAsset, + placed_at: parseInt(o.placed_at), + }; + }); } export const orderbookRouter = createTRPCRouter({ @@ -268,7 +270,7 @@ export const orderbookRouter = createTRPCRouter({ quoteAsset, baseAsset ); - return mappedOrders; + return mappedOrders.sort(defaultSortOrders); } ); const ordersByContracts = await Promise.all(promises); @@ -304,4 +306,49 @@ export const orderbookRouter = createTRPCRouter({ }); return spotPrice; }), + getClaimableOrders: publicProcedure + .input( + z + .object({ contractAddresses: z.array(z.string().startsWith("osmo")) }) + .required() + .and(UserOsmoAddressSchema.required()) + ) + .query(async ({ input, ctx }) => { + const { contractAddresses, userOsmoAddress } = input; + const promises = contractAddresses.map( + async (contractOsmoAddress: string) => { + const resp = await getOrderbookActiveOrders({ + orderbookAddress: contractOsmoAddress, + userOsmoAddress: userOsmoAddress, + chainList: ctx.chainList, + }); + + if (resp.orders.length === 0) return []; + const { base_denom } = await getOrderbookDenoms({ + orderbookAddress: contractOsmoAddress, + chainList: ctx.chainList, + }); + // TODO: Use actual quote denom here + const quoteAsset = getAssetFromAssetList({ + assetLists: ctx.assetLists, + sourceDenom: "uusdc", + }); + const baseAsset = getAssetFromAssetList({ + assetLists: ctx.assetLists, + sourceDenom: base_denom, + }); + const mappedOrders = await getTickInfoAndTransformOrders( + contractOsmoAddress, + resp.orders, + ctx.chainList, + quoteAsset, + baseAsset + ); + return mappedOrders.filter((o) => o.percentFilled.gte(new Dec(1))); + } + ); + const ordersByContracts = await Promise.all(promises); + const allOrders = ordersByContracts.flatMap((p) => p); + return allOrders; + }), }); diff --git a/packages/web/components/complex/orders-history/cells/filled-progress.tsx b/packages/web/components/complex/orders-history/cells/filled-progress.tsx index e7c0f74d03..4c2e7c8fe8 100644 --- a/packages/web/components/complex/orders-history/cells/filled-progress.tsx +++ b/packages/web/components/complex/orders-history/cells/filled-progress.tsx @@ -1,4 +1,4 @@ -import { Dec } from "@keplr-wallet/unit"; +import { Dec, Int } from "@keplr-wallet/unit"; import { MappedLimitOrder } from "@osmosis-labs/trpc"; import React, { useMemo } from "react"; @@ -16,9 +16,9 @@ export const OrderProgressBar: React.FC = ({ const roundedAmountFilled = useMemo(() => { if (percentFilled.lt(new Dec(1)) && !percentFilled.isZero()) { - return new Dec(1); + return new Int(1); } - return percentFilled.round(); + return percentFilled.round().mul(new Int(100)); }, [percentFilled]); const progressSegments = useMemo( diff --git a/packages/web/components/complex/orders-history/index.tsx b/packages/web/components/complex/orders-history/index.tsx index 00cb474ea7..ea7a506574 100644 --- a/packages/web/components/complex/orders-history/index.tsx +++ b/packages/web/components/complex/orders-history/index.tsx @@ -14,6 +14,7 @@ import { Spinner } from "~/components/loaders"; import { DisplayableLimitOrder, useOrderbookAllActiveOrders, + useOrderbookClaimableOrders, } from "~/hooks/limit-orders/use-orderbook"; import { useStore } from "~/stores"; @@ -34,6 +35,10 @@ export const OrderHistory = observer(() => { getCoreRowModel: getCoreRowModel(), }); + const { count, claimAllOrders } = useOrderbookClaimableOrders({ + userAddress: wallet?.address ?? "", + }); + const filledOrders = useMemo( () => table @@ -130,7 +135,7 @@ export const OrderHistory = observer(() => {
Filled orders to claim
- 1 + {count}
@@ -142,7 +147,10 @@ export const OrderHistory = observer(() => { />
- diff --git a/packages/web/hooks/limit-orders/use-orderbook.ts b/packages/web/hooks/limit-orders/use-orderbook.ts index b10e1213d9..c4d4ec9770 100644 --- a/packages/web/hooks/limit-orders/use-orderbook.ts +++ b/packages/web/hooks/limit-orders/use-orderbook.ts @@ -1,8 +1,9 @@ import { Dec } from "@keplr-wallet/unit"; +import { CoinPrimitive } from "@osmosis-labs/keplr-stores"; import { Asset } from "@osmosis-labs/server"; import { MappedLimitOrder } from "@osmosis-labs/trpc"; import { getAssetFromAssetList, makeMinimalAsset } from "@osmosis-labs/utils"; -import { useMemo } from "react"; +import { useCallback, useMemo } from "react"; import { AssetLists } from "~/config/generated/asset-lists"; import { useSwapAsset } from "~/hooks/use-swap"; @@ -303,6 +304,63 @@ export const useOrderbookAllActiveOrders = ({ }; }; +export const useOrderbookClaimableOrders = ({ + userAddress, +}: { + userAddress: string; +}) => { + const { orderbooks } = useOrderbooks(); + const { accountStore } = useStore(); + const account = accountStore.getWallet(accountStore.osmosisChainId); + const addresses = orderbooks.map(({ contractAddress }) => contractAddress); + const { + data: orders, + isLoading, + isFetching, + } = api.edge.orderbooks.getClaimableOrders.useQuery({ + contractAddresses: addresses, + userOsmoAddress: userAddress, + }); + + const claimAllOrders = useCallback(async () => { + if (!account || !orders) return; + const msgs = addresses + .map((contractAddress) => { + const ordersForAddress = orders.filter( + (o) => o.orderbookAddress === contractAddress + ); + if (ordersForAddress.length === 0) return; + + const msg = { + batch_claim: { + orders: ordersForAddress.map((o) => [o.tick_id, o.order_id]), + }, + }; + return { + contractAddress, + msg, + funds: [], + }; + }) + .filter(Boolean) as { + contractAddress: string; + msg: object; + funds: CoinPrimitive[]; + }[]; + + if (msgs.length > 0) { + await account?.cosmwasm.sendMultiExecuteContractMsg("executeWasm", msgs); + } + }, [orders, account, addresses]); + + return { + orders: orders ?? [], + count: orders?.length ?? 0, + isLoading: isLoading || isFetching, + claimAllOrders, + }; +}; + /** * Hook to fetch the current spot price of a given pair from an orderbook. * diff --git a/packages/web/hooks/limit-orders/use-place-limit.ts b/packages/web/hooks/limit-orders/use-place-limit.ts index 159e623f9f..3145d16202 100644 --- a/packages/web/hooks/limit-orders/use-place-limit.ts +++ b/packages/web/hooks/limit-orders/use-place-limit.ts @@ -145,9 +145,8 @@ export const usePlaceLimit = ({ const paymentDenom = paymentTokenValue.toCoin().denom; // The requested price must account for the ratio between the quote and base asset as the base asset may not be a stablecoin. // To account for this we divide by the quote asset price. - const tickId = priceToTick( - priceState.price.quo(quoteAssetPrice?.toDec() ?? new Dec(1)) - ); + //TODO: Adjust for quote asset price quoteAssetPrice?.toDec() + const tickId = priceToTick(priceState.price.quo(new Dec(1))); const msg = { place_limit: { tick_id: parseInt(tickId.toString()), @@ -176,7 +175,6 @@ export const usePlaceLimit = ({ account, orderDirection, priceState, - quoteAssetPrice, paymentTokenValue, ]); diff --git a/packages/web/hooks/use-feature-flags.ts b/packages/web/hooks/use-feature-flags.ts index 267bf979ba..0703cabce6 100644 --- a/packages/web/hooks/use-feature-flags.ts +++ b/packages/web/hooks/use-feature-flags.ts @@ -45,7 +45,7 @@ const defaultFlags: Record = { sidebarOsmoChangeAndChart: true, multiBridgeProviders: true, earnPage: false, - transactionsPage: false, + transactionsPage: true, sidecarRouter: true, legacyRouter: true, tfmRouter: true, From 94c960bf9dee14c18cb5e051c189aae2db423984 Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Sat, 29 Jun 2024 12:06:36 +0100 Subject: [PATCH 2/4] chore: disabled transaction page feature flag --- packages/web/hooks/use-feature-flags.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/hooks/use-feature-flags.ts b/packages/web/hooks/use-feature-flags.ts index 0703cabce6..267bf979ba 100644 --- a/packages/web/hooks/use-feature-flags.ts +++ b/packages/web/hooks/use-feature-flags.ts @@ -45,7 +45,7 @@ const defaultFlags: Record = { sidebarOsmoChangeAndChart: true, multiBridgeProviders: true, earnPage: false, - transactionsPage: true, + transactionsPage: false, sidecarRouter: true, legacyRouter: true, tfmRouter: true, From fc7b5bf6ebde6ce047c8d57708d6356cfa0af52d Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Sat, 29 Jun 2024 12:17:32 +0100 Subject: [PATCH 3/4] feat: wired up placed at info for active orders --- .../components/complex/orders-history/columns.tsx | 14 +++++++++++--- packages/web/hooks/use-feature-flags.ts | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/web/components/complex/orders-history/columns.tsx b/packages/web/components/complex/orders-history/columns.tsx index 95e28dab78..4744695ddd 100644 --- a/packages/web/components/complex/orders-history/columns.tsx +++ b/packages/web/components/complex/orders-history/columns.tsx @@ -2,6 +2,7 @@ import { PricePretty } from "@keplr-wallet/unit"; import { DEFAULT_VS_CURRENCY } from "@osmosis-labs/server"; import { createColumnHelper } from "@tanstack/react-table"; import classNames from "classnames"; +import dayjs from "dayjs"; import Image from "next/image"; import { Icon } from "~/components/assets"; @@ -124,11 +125,18 @@ export const tableColumns = [ header: () => { return Order Placed; }, - cell: () => { + cell: ({ + row: { + original: { placed_at }, + }, + }) => { + const placedAt = dayjs(placed_at / 1000000); + const formattedTime = placedAt.format("h:mm A"); + const formattedDate = placedAt.format("MMM D"); return (
-

2:14 PM

-

Apr 1st

+

{formattedTime}

+

{formattedDate}

); }, diff --git a/packages/web/hooks/use-feature-flags.ts b/packages/web/hooks/use-feature-flags.ts index 267bf979ba..0703cabce6 100644 --- a/packages/web/hooks/use-feature-flags.ts +++ b/packages/web/hooks/use-feature-flags.ts @@ -45,7 +45,7 @@ const defaultFlags: Record = { sidebarOsmoChangeAndChart: true, multiBridgeProviders: true, earnPage: false, - transactionsPage: false, + transactionsPage: true, sidecarRouter: true, legacyRouter: true, tfmRouter: true, From ac6596eaf403c27a5b75d26a41c795c08b5e054f Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Sat, 29 Jun 2024 12:32:33 +0100 Subject: [PATCH 4/4] chore: disabled transactions page --- packages/web/hooks/use-feature-flags.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/hooks/use-feature-flags.ts b/packages/web/hooks/use-feature-flags.ts index 0703cabce6..267bf979ba 100644 --- a/packages/web/hooks/use-feature-flags.ts +++ b/packages/web/hooks/use-feature-flags.ts @@ -45,7 +45,7 @@ const defaultFlags: Record = { sidebarOsmoChangeAndChart: true, multiBridgeProviders: true, earnPage: false, - transactionsPage: true, + transactionsPage: false, sidecarRouter: true, legacyRouter: true, tfmRouter: true,