diff --git a/packages/web/components/swap-tool/order-type-selector.tsx b/packages/web/components/swap-tool/order-type-selector.tsx new file mode 100644 index 0000000000..b6e07535c2 --- /dev/null +++ b/packages/web/components/swap-tool/order-type-selector.tsx @@ -0,0 +1,117 @@ +import { Menu, Transition } from "@headlessui/react"; +import classNames from "classnames"; +import { parseAsStringLiteral, useQueryState } from "nuqs"; +import React, { Fragment, useMemo } from "react"; + +import { Icon } from "~/components/assets"; +import { SpriteIconId } from "~/config"; +import { useTranslation } from "~/hooks"; + +interface UITradeType { + // id: "market" | "limit" | "recurring"; + id: "market" | "limit"; + title: string; + description: string; + icon: SpriteIconId; +} + +// const TRADE_TYPES = ["market", "limit", "recurring"] as const; +const TRADE_TYPES = ["market", "limit"] as const; + +export default function OrderTypeSelector() { + const { t } = useTranslation(); + + const [type, setType] = useQueryState( + "type", + parseAsStringLiteral(TRADE_TYPES).withDefault("market") + ); + + const uiTradeTypes: UITradeType[] = useMemo( + () => [ + { + id: "market", + title: t("place-limit.marketOrder.title"), + description: t("place-limit.marketOrder.description"), + icon: "exchange", + }, + { + id: "limit", + title: t("place-limit.limitOrder.title"), + description: t("place-limit.limitOrder.description", { denom: "BTC" }), + icon: "trade", + }, + // { + // id: "recurring", + // title: t("place-limit.recurringOrder.title"), + // description: t("place-limit.recurringOrder.description"), + // icon: "history-uncolored", + // }, + ], + [t] + ); + + return ( + + +

+ {type === "market" ? "Market" : "Limit"} +

+
+ +
+
+ + +
+

Order Type

+
+
+ {uiTradeTypes.map(({ id, title, description, icon }) => { + const isSelected = type === id; + + return ( + + {({ active }) => ( + + )} + + ); + })} +
+
+
+
+ ); +} diff --git a/packages/web/components/swap-tool/swap-tool-tabs.tsx b/packages/web/components/swap-tool/swap-tool-tabs.tsx index 775e74e23b..fdffc393fe 100644 --- a/packages/web/components/swap-tool/swap-tool-tabs.tsx +++ b/packages/web/components/swap-tool/swap-tool-tabs.tsx @@ -1,8 +1,6 @@ import classNames from "classnames"; import { FunctionComponent } from "react"; -import { theme } from "~/tailwind.config"; - export enum SwapToolTab { SWAP = "swap", BUY = "buy", @@ -18,17 +16,14 @@ const tabs = [ { label: "Buy", value: SwapToolTab.BUY, - color: theme.colors.bullish[400], }, { label: "Sell", value: SwapToolTab.SELL, - color: theme.colors.rust[400], }, { label: "Swap", value: SwapToolTab.SWAP, - color: theme.colors.ammelia[400], }, ]; @@ -44,22 +39,24 @@ export const SwapToolTabs: FunctionComponent = ({ activeTab, }) => { return ( -
+
{tabs.map((tab) => { const isActive = activeTab === tab.value; return ( ); })} diff --git a/packages/web/components/trade-tool/index.tsx b/packages/web/components/trade-tool/index.tsx index 456ede6707..c5d4367483 100644 --- a/packages/web/components/trade-tool/index.tsx +++ b/packages/web/components/trade-tool/index.tsx @@ -4,6 +4,7 @@ import { FunctionComponent, useMemo } from "react"; import ClientOnly from "~/components/client-only"; import { PlaceLimitTool } from "~/components/place-limit-tool"; import { SwapTool } from "~/components/swap-tool"; +import OrderTypeSelector from "~/components/swap-tool/order-type-selector"; import { SwapToolTab, SwapToolTabs, @@ -23,7 +24,10 @@ export const TradeTool: FunctionComponent = () => { return (
- +
+ + {tab !== SwapToolTab.SWAP && } +
{useMemo(() => { switch (tab) { case SwapToolTab.BUY: diff --git a/packages/web/hooks/limit-orders/use-place-limit.ts b/packages/web/hooks/limit-orders/use-place-limit.ts index aeb825ceee..9fec05e5c5 100644 --- a/packages/web/hooks/limit-orders/use-place-limit.ts +++ b/packages/web/hooks/limit-orders/use-place-limit.ts @@ -1,4 +1,4 @@ -import { CoinPretty, Dec } from "@keplr-wallet/unit"; +import { CoinPretty, Dec, PricePretty } from "@keplr-wallet/unit"; import { priceToTick } from "@osmosis-labs/math"; import { DEFAULT_VS_CURRENCY } from "@osmosis-labs/server"; import { useCallback, useMemo, useState } from "react"; @@ -58,23 +58,6 @@ 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: baseAssetPrice } = useCoinPrice( new CoinPretty(baseAsset, new Dec(1)) ); @@ -82,35 +65,80 @@ export const usePlaceLimit = ({ new CoinPretty(quoteAsset, new Dec(1)) ); - const paymentFiatValue = useMemo( - () => + /** + * Calculates the amount of tokens to be sent with the order. + * In the case of an Ask order the amount sent is the amount of tokens defined by the user in terms of the base asset. + * In the case of a Bid order the amount sent is the requested fiat amount divided by the current quote asset price. + * The amount is then multiplied by the number of decimal places the quote asset has. + * + * @returns The amount of tokens to be sent with the order in base asset amounts for an Ask and quote asset amounts for a Bid. + */ + const paymentTokenValue = useMemo(() => { + // The amount of tokens the user wishes to buy/sell + const baseTokenAmount = + inAmountInput.amount ?? new CoinPretty(baseAsset, new Dec(0)); + if (orderDirection === OrderDirection.Ask) { + // In the case of an Ask we just return the amount requested to sell + return baseTokenAmount; + } + + // Determine the outgoing fiat amount the user wants to buy + const outgoingFiatValue = mulPrice( - paymentAmount, - orderDirection === OrderDirection.Bid - ? quoteAssetPrice - : baseAssetPrice, + baseTokenAmount, + new PricePretty(DEFAULT_VS_CURRENCY, priceState.price), DEFAULT_VS_CURRENCY - ), - [paymentAmount, orderDirection, baseAssetPrice, quoteAssetPrice] - ); + ) ?? new PricePretty(DEFAULT_VS_CURRENCY, new Dec(0)); + + // Determine the amount of quote asset tokens to send by dividing the outgoing fiat amount by the current quote asset price + // Multiply by 10^n where n is the amount of decimals for the quote asset + const quoteTokenAmount = outgoingFiatValue! + .quo(quoteAssetPrice ?? new Dec(1)) + .toDec() + .mul(new Dec(Math.pow(10, quoteAsset.coinDecimals))); + return new CoinPretty(quoteAsset, quoteTokenAmount); + }, [ + quoteAssetPrice, + baseAsset, + orderDirection, + inAmountInput.amount, + quoteAsset, + priceState.price, + ]); + + /** + * Determines the fiat amount the user will pay for their order. + * In the case of an Ask the fiat amount is the amount of tokens the user will sell multiplied by the currently selected price. + * In the case of a Bid the fiat amount is the amount of quote asset tokens the user will send multiplied by the current price of the quote asset. + */ + const paymentFiatValue = useMemo(() => { + return orderDirection === OrderDirection.Ask + ? mulPrice( + paymentTokenValue, + new PricePretty(DEFAULT_VS_CURRENCY, priceState.price), + DEFAULT_VS_CURRENCY + ) + : mulPrice(paymentTokenValue, quoteAssetPrice, DEFAULT_VS_CURRENCY); + }, [paymentTokenValue, orderDirection, quoteAssetPrice, priceState]); const placeLimit = useCallback(async () => { - const quantity = inAmountInput.amount?.toCoin().amount ?? "0"; + const quantity = paymentTokenValue.toCoin().amount ?? "0"; if (quantity === "0") { return; } - const paymentDenom = - orderDirection === OrderDirection.Bid - ? quoteAsset.coinMinimalDenom - : baseAsset.coinMinimalDenom; + const paymentDenom = paymentTokenValue.toCoin().denom; - const tickId = priceToTick(priceState.price); + // 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)) + ); const msg = { place_limit: { tick_id: parseInt(tickId.toString()), order_direction: orderDirection, - quantity: paymentAmount?.toCoin().amount ?? "0", + quantity, claim_bounty: CLAIM_BOUNTY, }, }; @@ -121,7 +149,7 @@ export const usePlaceLimit = ({ msg, [ { - amount: paymentAmount.toCoin().amount ?? "0", + amount: quantity, denom: paymentDenom, }, ] @@ -132,12 +160,10 @@ export const usePlaceLimit = ({ }, [ orderbookContractAddress, account, - quoteAsset, - baseAsset, orderDirection, - inAmountInput, priceState, - paymentAmount, + quoteAssetPrice, + paymentTokenValue, ]); const { data: balances, isFetched: isBalancesFetched } = useBalances({ @@ -184,7 +210,6 @@ export const usePlaceLimit = ({ quoteTokenBalance, isBalancesFetched, insufficientFunds, - paymentAmount, quoteAssetPrice, baseAssetPrice, paymentFiatValue, diff --git a/packages/web/localizations/de.json b/packages/web/localizations/de.json index 5a661c2747..5ec8ce6042 100644 --- a/packages/web/localizations/de.json +++ b/packages/web/localizations/de.json @@ -1130,6 +1130,18 @@ "newer": "Neuere" }, "place-limit": { - "reviewOrder": "Bestellung überprüfen" + "reviewOrder": "Bestellung überprüfen", + "marketOrder": { + "title": "Marktauftrag", + "description": "Sofort zum besten verfügbaren Preis kaufen" + }, + "limitOrder": { + "title": "Limit-Auftrag", + "description": "Kaufen, wenn der Preis {denom} sinkt" + }, + "recurringOrder": { + "title": "Wiederkehrende Bestellung", + "description": "Kaufen Sie zum Durchschnittspreis im Laufe der Zeit" + } } } diff --git a/packages/web/localizations/en.json b/packages/web/localizations/en.json index 124adc2219..305c22e581 100644 --- a/packages/web/localizations/en.json +++ b/packages/web/localizations/en.json @@ -1130,6 +1130,18 @@ "newer": "Newer" }, "place-limit": { - "reviewOrder": "Review Order" + "reviewOrder": "Review Order", + "marketOrder": { + "title": "Market Order", + "description": "Buy immediately at best available price" + }, + "limitOrder": { + "title": "Limit Order", + "description": "Buy when {denom} price decreases" + }, + "recurringOrder": { + "title": "Recurring Order", + "description": "Buy at average price over time" + } } } diff --git a/packages/web/localizations/es.json b/packages/web/localizations/es.json index 035bbcbc44..7f4c64dbfa 100644 --- a/packages/web/localizations/es.json +++ b/packages/web/localizations/es.json @@ -1130,6 +1130,18 @@ "newer": "Más nuevo" }, "place-limit": { - "reviewOrder": "Revisar orden" + "reviewOrder": "Revisar orden", + "marketOrder": { + "title": "Orden de mercado", + "description": "Compre inmediatamente al mejor precio disponible" + }, + "limitOrder": { + "title": "Orden límite", + "description": "Compre cuando el precio {denom} baje" + }, + "recurringOrder": { + "title": "Orden recurrente", + "description": "Compre a precio promedio a lo largo del tiempo" + } } } diff --git a/packages/web/localizations/fa.json b/packages/web/localizations/fa.json index 1b48391c59..a9102dccb5 100644 --- a/packages/web/localizations/fa.json +++ b/packages/web/localizations/fa.json @@ -1130,6 +1130,18 @@ "newer": "جدیدتر" }, "place-limit": { - "reviewOrder": "سفارش بررسی" + "reviewOrder": "سفارش بررسی", + "marketOrder": { + "title": "سفارش بازار", + "description": "بلافاصله با بهترین قیمت موجود خرید کنید" + }, + "limitOrder": { + "title": "سفارش محدود", + "description": "زمانی که قیمت {denom} کاهش یابد، خرید کنید" + }, + "recurringOrder": { + "title": "سفارش تکراری", + "description": "در طول زمان با قیمت متوسط خرید کنید" + } } } diff --git a/packages/web/localizations/fr.json b/packages/web/localizations/fr.json index fb97fd0631..430133fc47 100644 --- a/packages/web/localizations/fr.json +++ b/packages/web/localizations/fr.json @@ -1130,6 +1130,18 @@ "newer": "Plus récent" }, "place-limit": { - "reviewOrder": "Réviser la commande" + "reviewOrder": "Réviser la commande", + "marketOrder": { + "title": "Ordre du marché", + "description": "Achetez immédiatement au meilleur prix disponible" + }, + "limitOrder": { + "title": "Ordre Limité", + "description": "Achetez lorsque le prix {denom} diminue" + }, + "recurringOrder": { + "title": "Commande récurrente", + "description": "Acheter au prix moyen dans le temps" + } } } diff --git a/packages/web/localizations/gu.json b/packages/web/localizations/gu.json index 886de37588..ea917e0ee2 100644 --- a/packages/web/localizations/gu.json +++ b/packages/web/localizations/gu.json @@ -1130,6 +1130,18 @@ "newer": "નવું" }, "place-limit": { - "reviewOrder": "રિવ્યૂ ઓર્ડર" + "reviewOrder": "રિવ્યૂ ઓર્ડર", + "marketOrder": { + "title": "માર્કેટ ઓર્ડર", + "description": "શ્રેષ્ઠ ઉપલબ્ધ ભાવે તરત જ ખરીદો" + }, + "limitOrder": { + "title": "મર્યાદા ઓર્ડર", + "description": "જ્યારે {denom} કિંમત ઘટે ત્યારે ખરીદો" + }, + "recurringOrder": { + "title": "રિકરિંગ ઓર્ડર", + "description": "સમય જતાં સરેરાશ કિંમતે ખરીદો" + } } } diff --git a/packages/web/localizations/hi.json b/packages/web/localizations/hi.json index f7eb250638..b75912c6dc 100644 --- a/packages/web/localizations/hi.json +++ b/packages/web/localizations/hi.json @@ -1130,6 +1130,18 @@ "newer": "नई" }, "place-limit": { - "reviewOrder": "अादेश का पुनः निरिक्षण" + "reviewOrder": "अादेश का पुनः निरिक्षण", + "marketOrder": { + "title": "बाजार आदेश", + "description": "सर्वोत्तम उपलब्ध मूल्य पर तुरंत खरीदें" + }, + "limitOrder": { + "title": "सीमा आदेश", + "description": "जब {denom} कीमत घट जाए तो खरीदें" + }, + "recurringOrder": { + "title": "आवर्ती आदेश", + "description": "समय के साथ औसत कीमत पर खरीदें" + } } } diff --git a/packages/web/localizations/ja.json b/packages/web/localizations/ja.json index c29c9d65af..2aee1b78d6 100644 --- a/packages/web/localizations/ja.json +++ b/packages/web/localizations/ja.json @@ -1130,6 +1130,18 @@ "newer": "新しい" }, "place-limit": { - "reviewOrder": "注文の確認" + "reviewOrder": "注文の確認", + "marketOrder": { + "title": "成行注文", + "description": "最安価格で今すぐご購入ください" + }, + "limitOrder": { + "title": "指値注文", + "description": "{denom}価格が下がったら購入する" + }, + "recurringOrder": { + "title": "定期注文", + "description": "時間の経過とともに平均価格で購入する" + } } } diff --git a/packages/web/localizations/ko.json b/packages/web/localizations/ko.json index 565d783c5c..23da566ffb 100644 --- a/packages/web/localizations/ko.json +++ b/packages/web/localizations/ko.json @@ -1130,6 +1130,18 @@ "newer": "최신" }, "place-limit": { - "reviewOrder": "주문 검토" + "reviewOrder": "주문 검토", + "marketOrder": { + "title": "시장가 주문", + "description": "가장 좋은 가격으로 즉시 구매하세요" + }, + "limitOrder": { + "title": "제한 주문", + "description": "{denom} 가격이 하락하면 구매하세요." + }, + "recurringOrder": { + "title": "반복 주문", + "description": "시간이 지남에 따라 평균 가격으로 구매" + } } } diff --git a/packages/web/localizations/pl.json b/packages/web/localizations/pl.json index fc68f97c28..1c8910dfe1 100644 --- a/packages/web/localizations/pl.json +++ b/packages/web/localizations/pl.json @@ -1130,6 +1130,18 @@ "newer": "Nowsza" }, "place-limit": { - "reviewOrder": "Przegląd zamówienia" + "reviewOrder": "Przegląd zamówienia", + "marketOrder": { + "title": "Porządek rynkowy", + "description": "Kup natychmiast po najlepszej dostępnej cenie" + }, + "limitOrder": { + "title": "Zamówienie z limitem", + "description": "Kupuj, gdy cena {denom} spadnie" + }, + "recurringOrder": { + "title": "Zamówienie powtarzające się", + "description": "Kupuj po średniej cenie na przestrzeni czasu" + } } } diff --git a/packages/web/localizations/pt-br.json b/packages/web/localizations/pt-br.json index 217f051359..0729901dc3 100644 --- a/packages/web/localizations/pt-br.json +++ b/packages/web/localizations/pt-br.json @@ -1130,6 +1130,18 @@ "newer": "Mais recente" }, "place-limit": { - "reviewOrder": "Revisar pedido" + "reviewOrder": "Revisar pedido", + "marketOrder": { + "title": "Ordem de mercado", + "description": "Compre imediatamente ao melhor preço disponível" + }, + "limitOrder": { + "title": "Ordem Limitada", + "description": "Compre quando o preço {denom} diminuir" + }, + "recurringOrder": { + "title": "Pedido recorrente", + "description": "Compre pelo preço médio ao longo do tempo" + } } } diff --git a/packages/web/localizations/ro.json b/packages/web/localizations/ro.json index 5d5a651d16..110e3811a3 100644 --- a/packages/web/localizations/ro.json +++ b/packages/web/localizations/ro.json @@ -1130,6 +1130,18 @@ "newer": "Mai nou" }, "place-limit": { - "reviewOrder": "Verificați comanda" + "reviewOrder": "Verificați comanda", + "marketOrder": { + "title": "Ordinul pieței", + "description": "Cumpărați imediat la cel mai bun preț disponibil" + }, + "limitOrder": { + "title": "Ordin limită", + "description": "Cumpărați când prețul {denom} scade" + }, + "recurringOrder": { + "title": "Comandă recurentă", + "description": "Cumpărați la preț mediu în timp" + } } } diff --git a/packages/web/localizations/ru.json b/packages/web/localizations/ru.json index 90066ad545..be88d16440 100644 --- a/packages/web/localizations/ru.json +++ b/packages/web/localizations/ru.json @@ -1130,6 +1130,18 @@ "newer": "Новее" }, "place-limit": { - "reviewOrder": "Просмотреть заказ" + "reviewOrder": "Просмотреть заказ", + "marketOrder": { + "title": "Рыночный ордер", + "description": "Купите немедленно по лучшей доступной цене" + }, + "limitOrder": { + "title": "Лимитный ордер", + "description": "Покупайте, когда цена {denom} снижается" + }, + "recurringOrder": { + "title": "Повторяющийся заказ", + "description": "Покупайте по средней цене с течением времени" + } } } diff --git a/packages/web/localizations/tr.json b/packages/web/localizations/tr.json index 86f9f12b41..b3e2c3247a 100644 --- a/packages/web/localizations/tr.json +++ b/packages/web/localizations/tr.json @@ -1130,6 +1130,18 @@ "newer": "Daha yeni" }, "place-limit": { - "reviewOrder": "Siparişi İncele" + "reviewOrder": "Siparişi İncele", + "marketOrder": { + "title": "Market siparişi", + "description": "Mevcut en iyi fiyatla hemen satın alın" + }, + "limitOrder": { + "title": "Limit Emri", + "description": "{denom} fiyatı düştüğünde satın alın" + }, + "recurringOrder": { + "title": "Yinelenen Sipariş", + "description": "Zaman içinde ortalama fiyattan satın alın" + } } } diff --git a/packages/web/localizations/zh-cn.json b/packages/web/localizations/zh-cn.json index 51b54bd65c..782785cb4d 100644 --- a/packages/web/localizations/zh-cn.json +++ b/packages/web/localizations/zh-cn.json @@ -1130,6 +1130,18 @@ "newer": "较新" }, "place-limit": { - "reviewOrder": "查看订单" + "reviewOrder": "查看订单", + "marketOrder": { + "title": "市价订单", + "description": "立即以最佳价格购买" + }, + "limitOrder": { + "title": "限价订单", + "description": "当{denom}价格下降时买入" + }, + "recurringOrder": { + "title": "重复订单", + "description": "按一段时间内的平均价格购买" + } } } diff --git a/packages/web/localizations/zh-hk.json b/packages/web/localizations/zh-hk.json index 102d34a3bd..f1d8435978 100644 --- a/packages/web/localizations/zh-hk.json +++ b/packages/web/localizations/zh-hk.json @@ -1130,6 +1130,18 @@ "newer": "較新" }, "place-limit": { - "reviewOrder": "查看訂單" + "reviewOrder": "查看訂單", + "marketOrder": { + "title": "市價訂單", + "description": "立即以最優惠的價格購買" + }, + "limitOrder": { + "title": "限價訂單", + "description": "當{denom}價格下跌時購買" + }, + "recurringOrder": { + "title": "重複訂單", + "description": "以一段時間內的平均價格購買" + } } } diff --git a/packages/web/localizations/zh-tw.json b/packages/web/localizations/zh-tw.json index 6a0522eccb..36ff3d6ec4 100644 --- a/packages/web/localizations/zh-tw.json +++ b/packages/web/localizations/zh-tw.json @@ -1130,6 +1130,18 @@ "newer": "較新" }, "place-limit": { - "reviewOrder": "查看訂單" + "reviewOrder": "查看訂單", + "marketOrder": { + "title": "市價訂單", + "description": "立即以最優惠的價格購買" + }, + "limitOrder": { + "title": "限價訂單", + "description": "當{denom}價格下跌時購買" + }, + "recurringOrder": { + "title": "重複訂單", + "description": "以一段時間內的平均價格購買" + } } } diff --git a/packages/web/public/icons/sprite.svg b/packages/web/public/icons/sprite.svg index d8a7b7e830..3d46401633 100644 --- a/packages/web/public/icons/sprite.svg +++ b/packages/web/public/icons/sprite.svg @@ -1230,8 +1230,21 @@ /> + + + + + + + +