From 27afcbc63dfffcdae0401ddf66c1c5abf65d31fa Mon Sep 17 00:00:00 2001 From: Connor Barr Date: Thu, 6 Jun 2024 21:35:37 +0100 Subject: [PATCH] feat: added review order modal --- packages/web/components/input/limit-input.tsx | 4 +- .../web/components/place-limit-tool/index.tsx | 253 ++++++++++-------- .../web/hooks/limit-orders/use-place-limit.ts | 105 +++++--- packages/web/modals/review-limit-order.tsx | 161 +++++++++++ 4 files changed, 366 insertions(+), 157 deletions(-) create mode 100644 packages/web/modals/review-limit-order.tsx diff --git a/packages/web/components/input/limit-input.tsx b/packages/web/components/input/limit-input.tsx index b963bdb0dc..423b480727 100644 --- a/packages/web/components/input/limit-input.tsx +++ b/packages/web/components/input/limit-input.tsx @@ -4,6 +4,7 @@ import { FC, useCallback, useEffect, useMemo, useState } from "react"; import { Icon } from "~/components/assets"; import { DynamicSizeInput } from "~/components/input/dynamic-size-input"; +import { useCoinPrice } from "~/hooks/queries/assets/use-coin-price"; import { formatPretty } from "~/utils/formatter"; export interface LimitInputProps { @@ -43,6 +44,7 @@ export const LimitInput: FC = ({ }) => { const [fiatAmount, setFiatAmount] = useState(""); const [focused, setFocused] = useState(FocusedInput.FIAT); + const { price: basePrice } = useCoinPrice(baseAsset); const swapFocus = useCallback(() => { switch (focused) { @@ -99,7 +101,7 @@ export const LimitInput: FC = ({ const tokenValue = new Dec(value)?.quo(price); setTokenAmountSafe(tokenValue.toString()); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [price, fiatAmount, setTokenAmountSafe]); + }, [price, fiatAmount, setTokenAmountSafe, basePrice]); const FiatInput = useMemo(() => { const isFocused = focused === FocusedInput.FIAT; diff --git a/packages/web/components/place-limit-tool/index.tsx b/packages/web/components/place-limit-tool/index.tsx index 05686e9ac3..c01f495522 100644 --- a/packages/web/components/place-limit-tool/index.tsx +++ b/packages/web/components/place-limit-tool/index.tsx @@ -12,6 +12,7 @@ import { Button } from "~/components/ui/button"; import { useTranslation } from "~/hooks"; import { OrderDirection, usePlaceLimit } from "~/hooks/limit-orders"; import { useOrderbookPool } from "~/hooks/limit-orders/use-orderbook-pool"; +import { ReviewLimitOrderModal } from "~/modals/review-limit-order"; import { useStore } from "~/stores"; import { formatPretty } from "~/utils/formatter"; @@ -30,6 +31,7 @@ export const PlaceLimitTool: FunctionComponent = observer( ({ orderDirection = OrderDirection.Bid }) => { const { accountStore } = useStore(); const { t } = useTranslation(); + const [reviewOpen, setReviewOpen] = useState(false); const [baseDenom, setBaseDenom] = useState("ION"); const quoteDenom = "OSMO"; @@ -51,135 +53,148 @@ export const PlaceLimitTool: FunctionComponent = observer( const isSwapToolLoading = false; return ( -
- setBaseDenom(newDenom)} - disabled={false} - orderDirection={orderDirection} - /> -
-
-
- - {t("swap.maxButtonErrorNoBalance")} -
- } - disabled={!swapState.inAmountInput.notEnoughBalanceForMax} - > - - + + +
-
-
-
- +
+
+ +
-
-
-
-
- {`When ${swapState.baseDenom} price is at `} - {`$${formatPretty(swapState.priceState.price)}`} +
+
+
+ {`When ${swapState.baseDenom} price is at `} + {`$${formatPretty(swapState.priceState.price)}`} +
-
-
-
- {`${swapState.priceState.percentAdjusted - .mul(new Dec(100)) - .round() - .abs()}% `} - - {orderDirection === OrderDirection.Bid ? "below" : "above"}{" "} - current price - +
+
+ {`${swapState.priceState.percentAdjusted + .mul(new Dec(100)) + .round() + .abs()}% `} + + {orderDirection === OrderDirection.Bid ? "below" : "above"}{" "} + current price + +
+
+ {useMemo( + () => + percentAdjustmentOptions.map(({ label, value }) => ( + + )), + [swapState.priceState, orderDirection] + )} +
-
- {useMemo( - () => - percentAdjustmentOptions.map(({ label, value }) => ( - - )), - [swapState.priceState, orderDirection] +
+
- -
+ setReviewOpen(false)} + orderType="limit" + /> + ); } ); diff --git a/packages/web/hooks/limit-orders/use-place-limit.ts b/packages/web/hooks/limit-orders/use-place-limit.ts index 5d331772d0..5cc2db9dac 100644 --- a/packages/web/hooks/limit-orders/use-place-limit.ts +++ b/packages/web/hooks/limit-orders/use-place-limit.ts @@ -1,13 +1,12 @@ import { CoinPretty, Dec } from "@keplr-wallet/unit"; import { priceToTick } from "@osmosis-labs/math"; +import { DEFAULT_VS_CURRENCY } from "@osmosis-labs/server"; import { useCallback, useMemo, useState } from "react"; +import { mulPrice } from "~/hooks/queries/assets/use-coin-fiat-value"; +import { useCoinPrice } from "~/hooks/queries/assets/use-coin-price"; import { useBalances } from "~/hooks/queries/cosmos/use-balances"; -import { - useSwapAmountInput, - useSwapAsset, - useSwapAssets, -} from "~/hooks/use-swap"; +import { useSwapAmountInput, useSwapAssets } from "~/hooks/use-swap"; import { useStore } from "~/stores"; export enum OrderDirection { @@ -26,6 +25,8 @@ export interface UsePlaceLimitParams { orderbookContractAddress: string; } +export type PlaceLimitState = ReturnType; + // TODO: adjust as necessary const CLAIM_BOUNTY = "0.01"; @@ -45,6 +46,10 @@ export const usePlaceLimit = ({ useQueryParams, useOtherCurrencies, }); + + const quoteAsset = swapAssets.toAsset; + const baseAsset = swapAssets.fromAsset; + const priceState = useLimitPrice(); const inAmountInput = useSwapAmountInput({ swapAssets, @@ -53,61 +58,86 @@ export const usePlaceLimit = ({ }); const account = accountStore.getWallet(osmosisChainId); + const paymentAmount = useMemo( + () => + orderDirection === OrderDirection.Ask + ? inAmountInput.amount ?? new CoinPretty(baseAsset, "0") + : new CoinPretty( + quoteAsset, + inAmountInput.amount?.toCoin().amount ?? "0" + ).mul(priceState.price), + [ + orderDirection, + inAmountInput.amount, + priceState.price, + baseAsset, + quoteAsset, + ] + ); + + const { price: basePrice } = useCoinPrice( + new CoinPretty(baseAsset, new Dec(1)) + ); + const { price: quotePrice } = useCoinPrice( + new CoinPretty(quoteAsset, new Dec(1)) + ); + + const paymentFiatValue = useMemo( + () => + mulPrice( + paymentAmount, + orderDirection === OrderDirection.Bid ? quotePrice : basePrice, + DEFAULT_VS_CURRENCY + ), + [paymentAmount, orderDirection, basePrice, quotePrice] + ); + const placeLimit = useCallback(async () => { - const quantity = inAmountInput.amount?.toCoin().amount ?? "0'"; + const quantity = inAmountInput.amount?.toCoin().amount ?? "0"; if (quantity === "0") { return; } const paymentDenom = orderDirection === OrderDirection.Bid - ? swapAssets.toAsset.coinMinimalDenom - : swapAssets.fromAsset.coinMinimalDenom; - const paymentAmount = - orderDirection === OrderDirection.Ask - ? quantity - : new Dec(quantity).mul(priceState.price).truncate().toString(); + ? quoteAsset.coinMinimalDenom + : baseAsset.coinMinimalDenom; const tickId = priceToTick(priceState.price); const msg = { place_limit: { tick_id: parseInt(tickId.toString()), order_direction: orderDirection, - quantity: paymentAmount, + quantity: paymentAmount?.toCoin().amount ?? "0", claim_bounty: CLAIM_BOUNTY, }, }; - await account?.cosmwasm.sendExecuteContractMsg( - "executeWasm", - orderbookContractAddress, - msg, - [ - { - amount: paymentAmount, - denom: paymentDenom, - }, - ] - ); + try { + await account?.cosmwasm.sendExecuteContractMsg( + "executeWasm", + orderbookContractAddress, + msg, + [ + { + amount: paymentAmount.toCoin().amount ?? "0", + denom: paymentDenom, + }, + ] + ); + } catch (error) { + console.error("Error attempting to broadcast place limit tx", error); + } }, [ orderbookContractAddress, account, - swapAssets, + quoteAsset, + baseAsset, orderDirection, inAmountInput, priceState, + paymentAmount, ]); - const { asset: quoteAsset } = useSwapAsset({ - minDenomOrSymbol: quoteDenom, - existingAssets: swapAssets.selectableAssets, - }); - console.log("QUOTE", quoteAsset); - - const { asset: baseAsset } = useSwapAsset({ - minDenomOrSymbol: baseDenom, - existingAssets: swapAssets.selectableAssets, - }); - const { data: balances, isFetched: isBalancesFetched } = useBalances({ address: account?.address ?? "", queryOptions: { @@ -141,7 +171,6 @@ export const usePlaceLimit = ({ .lt(inAmountInput.amount?.toDec() ?? new Dec(0)); return { - ...swapAssets, quoteDenom, baseDenom, baseAsset, @@ -153,6 +182,8 @@ export const usePlaceLimit = ({ quoteTokenBalance, isBalancesFetched, insufficientFunds, + paymentAmount, + paymentFiatValue, }; }; diff --git a/packages/web/modals/review-limit-order.tsx b/packages/web/modals/review-limit-order.tsx new file mode 100644 index 0000000000..c3021f3c62 --- /dev/null +++ b/packages/web/modals/review-limit-order.tsx @@ -0,0 +1,161 @@ +import { Dec, PricePretty } from "@keplr-wallet/unit"; +import { DEFAULT_VS_CURRENCY } from "@osmosis-labs/server"; +import Image from "next/image"; +import { useMemo } from "react"; + +import { Icon } from "~/components/assets"; +import { Button } from "~/components/buttons"; +import { OrderDirection, PlaceLimitState } from "~/hooks/limit-orders"; +import { ModalBase } from "~/modals/base"; +import { formatPretty } from "~/utils/formatter"; + +export interface ReviewLimitOrderModalProps { + isOpen: boolean; + onRequestClose: () => void; + placeLimitState: PlaceLimitState; + orderDirection: OrderDirection; + orderType: "limit" | "market"; +} + +export const ReviewLimitOrderModal: React.FC = ({ + isOpen, + onRequestClose, + placeLimitState, + orderDirection, + orderType, +}) => { + const fee = useMemo(() => new PricePretty(DEFAULT_VS_CURRENCY, 0.14), []); + const total = useMemo(() => { + if ( + placeLimitState.paymentFiatValue && + !placeLimitState.paymentFiatValue?.toDec().isZero() + ) { + return placeLimitState.paymentFiatValue!.sub(fee); + } + return new PricePretty(DEFAULT_VS_CURRENCY, 0); + }, [placeLimitState.paymentFiatValue, fee]); + return ( + +
+
+ {placeLimitState.baseAsset.coinDenom} +
+
+ + ≈{" "} + {placeLimitState.inAmountInput.amount + ? formatPretty(placeLimitState.inAmountInput.amount) + : "0"} + + + at ${formatPretty(placeLimitState.priceState.price)} + +
+
+
+
+ Amount + + ≈{" "} + {placeLimitState.inAmountInput.fiatValue + ? formatPretty(placeLimitState.paymentFiatValue!) + : "$0"} + +
+
+ Total Estimated Fees + ≈ {formatPretty(fee)} +
+
+
+ Total + ≈ {formatPretty(total)} +
+
+ + {orderDirection === OrderDirection.Ask + ? "Receive Asset" + : "Pay With"} + + + {placeLimitState.quoteAsset.coinDenom}{" "} + {placeLimitState.quoteAsset.coinDenom} + +
+
+ Order Type + + {orderType === "limit" ? "Limit order" : "Market order"} + +
+
+ Limit price +
+
+ {!placeLimitState.priceState.percentAdjusted.isZero() && ( + + )} +
+ {formatPretty( + placeLimitState.priceState.percentAdjusted + .mul(new Dec(100)) + .abs() + )} + % +
+
+
${formatPretty(placeLimitState.priceState.price)}
+
+
+
+ More details + Show +
+
+ + Disclaimer lorem ipsum.{" "} + Learn more. + +
+
+ + +
+
+
+ ); +};