Skip to content

Commit

Permalink
Merge branch 'connor/place-limit' into connor/maker-fee
Browse files Browse the repository at this point in the history
  • Loading branch information
crnbarr93 committed Jun 12, 2024
2 parents cdfc3ef + 00ded19 commit 85404c1
Show file tree
Hide file tree
Showing 22 changed files with 431 additions and 71 deletions.
117 changes: 117 additions & 0 deletions packages/web/components/swap-tool/order-type-selector.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Menu as="div" className="relative inline-block">
<Menu.Button className="flex items-center gap-2 rounded-[48px] bg-osmoverse-825 py-3 px-4">
<p className="font-semibold text-wosmongton-200">
{type === "market" ? "Market" : "Limit"}
</p>
<div className="flex h-6 w-6 items-center justify-center">
<Icon id="chevron-down" className="h-[7px] w-3 text-wosmongton-200" />
</div>
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-50 mt-3 flex w-[280px] origin-top-right flex-col rounded-xl bg-osmoverse-800">
<div className="flex items-center border-b border-osmoverse-700 py-2 px-4">
<p className="text-subtitle1 font-semibold">Order Type</p>
</div>
<div className="flex flex-col gap-2 p-2">
{uiTradeTypes.map(({ id, title, description, icon }) => {
const isSelected = type === id;

return (
<Menu.Item key={title}>
{({ active }) => (
<button
onClick={() => setType(id)}
className={classNames(
"flex gap-3 rounded-lg py-2 px-3 transition-colors",
{ "bg-osmoverse-700": active || isSelected }
)}
>
<div className="flex h-6 w-6 items-center justify-center">
<Icon
id={icon}
className={classNames(
"h-6 w-6 text-osmoverse-400 transition-colors",
{
"text-white-full": active || isSelected,
}
)}
/>
</div>
<div className="flex flex-col gap-1 text-left">
<p>{title}</p>
<small className="text-sm leading-5 text-osmoverse-300">
{description}
</small>
</div>
</button>
)}
</Menu.Item>
);
})}
</div>
</Menu.Items>
</Transition>
</Menu>
);
}
23 changes: 10 additions & 13 deletions packages/web/components/swap-tool/swap-tool-tabs.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import classNames from "classnames";
import { FunctionComponent } from "react";

import { theme } from "~/tailwind.config";

export enum SwapToolTab {
SWAP = "swap",
BUY = "buy",
Expand All @@ -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],
},
];

Expand All @@ -44,22 +39,24 @@ export const SwapToolTabs: FunctionComponent<SwapToolTabsProps> = ({
activeTab,
}) => {
return (
<div className="flex items-center">
<div className="flex w-max items-center rounded-3xl border border-osmoverse-700">
{tabs.map((tab) => {
const isActive = activeTab === tab.value;
return (
<button
key={`swap-tab-${tab.value}`}
onClick={() => setTab(tab.value)}
className={classNames("px-3 py-2", {
"!pl-0": tab.value === SwapToolTab.BUY,
"text-osmoverse-500": !isActive,
className={classNames("rounded-3xl px-4 py-3", {
"bg-osmoverse-700": isActive,
})}
style={{
color: isActive ? tab.color : undefined,
}}
>
<h6 className="leading-6">{tab.label}</h6>
<p
className={classNames("font-semibold", {
"text-wosmongton-100": !isActive,
})}
>
{tab.label}
</p>
</button>
);
})}
Expand Down
6 changes: 5 additions & 1 deletion packages/web/components/trade-tool/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -23,7 +24,10 @@ export const TradeTool: FunctionComponent<TradeToolProps> = () => {
return (
<ClientOnly>
<div className="relative flex flex-col gap-6 overflow-hidden md:gap-6 md:px-3 md:pb-4 md:pt-4">
<SwapToolTabs activeTab={tab} setTab={setTab} />
<div className="flex w-full items-center justify-between">
<SwapToolTabs activeTab={tab} setTab={setTab} />
{tab !== SwapToolTab.SWAP && <OrderTypeSelector />}
</div>
{useMemo(() => {
switch (tab) {
case SwapToolTab.BUY:
Expand Down
105 changes: 65 additions & 40 deletions packages/web/hooks/limit-orders/use-place-limit.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -58,59 +58,87 @@ 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))
);
const { price: quoteAssetPrice } = useCoinPrice(
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,
},
};
Expand All @@ -121,7 +149,7 @@ export const usePlaceLimit = ({
msg,
[
{
amount: paymentAmount.toCoin().amount ?? "0",
amount: quantity,
denom: paymentDenom,
},
]
Expand All @@ -132,12 +160,10 @@ export const usePlaceLimit = ({
}, [
orderbookContractAddress,
account,
quoteAsset,
baseAsset,
orderDirection,
inAmountInput,
priceState,
paymentAmount,
quoteAssetPrice,
paymentTokenValue,
]);

const { data: balances, isFetched: isBalancesFetched } = useBalances({
Expand Down Expand Up @@ -184,7 +210,6 @@ export const usePlaceLimit = ({
quoteTokenBalance,
isBalancesFetched,
insufficientFunds,
paymentAmount,
quoteAssetPrice,
baseAssetPrice,
paymentFiatValue,
Expand Down
14 changes: 13 additions & 1 deletion packages/web/localizations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
14 changes: 13 additions & 1 deletion packages/web/localizations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
Loading

0 comments on commit 85404c1

Please sign in to comment.