From 85840eb05593ecfe32feaecdfec00ff301830363 Mon Sep 17 00:00:00 2001 From: abdulhaseeb13mar Date: Thu, 14 Sep 2023 01:02:00 +0500 Subject: [PATCH 01/39] feat: reusable input component --- src/app/common/input.tsx | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/app/common/input.tsx diff --git a/src/app/common/input.tsx b/src/app/common/input.tsx new file mode 100644 index 00000000..587305f6 --- /dev/null +++ b/src/app/common/input.tsx @@ -0,0 +1,33 @@ +import React, { ReactNode } from "react"; + +type Props = Omit & { + parentClasses?: string; + inputClasses?: string; + endAdornmentClasses?: string; + endAdornment?: ReactNode; + isError?: boolean; +}; + +export const Input = ({ + parentClasses = "", + inputClasses = "", + endAdornment, + isError = false, + endAdornmentClasses = "", + ...inputProps +}: Props) => { + return ( +
+ + {endAdornment && ( +
{endAdornment}
+ )} +
+ ); +}; From 56578aa6be2b17bb25d64ef14ef29f712fc73b0d Mon Sep 17 00:00:00 2001 From: abdulhaseeb13mar Date: Thu, 14 Sep 2023 01:02:17 +0500 Subject: [PATCH 02/39] feat: reusable token avatar component --- src/app/common/tokenAvatar.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/app/common/tokenAvatar.tsx diff --git a/src/app/common/tokenAvatar.tsx b/src/app/common/tokenAvatar.tsx new file mode 100644 index 00000000..9d7d03e6 --- /dev/null +++ b/src/app/common/tokenAvatar.tsx @@ -0,0 +1,11 @@ +import React from "react"; + +export const TokenAvatar = ({ url }: { url: string }) => { + return ( +
+
+ token1 +
+
+ ); +}; From 4021278219e5ae280cd3f45ad55ce34ce5eb3b4d Mon Sep 17 00:00:00 2001 From: abdulhaseeb13mar Date: Thu, 14 Sep 2023 01:02:55 +0500 Subject: [PATCH 03/39] style: setting base style for html elements --- src/app/styles/globals.css | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/app/styles/globals.css b/src/app/styles/globals.css index b56373fa..65d814d8 100644 --- a/src/app/styles/globals.css +++ b/src/app/styles/globals.css @@ -2,6 +2,21 @@ @tailwind components; @tailwind utilities; +@layer base { + img { + @apply !my-0; + } + p { + @apply !my-0; + } + input { + @apply !bg-base-300; + } + input:focus { + @apply !outline-none; + } +} + :root { --radix-connect-button-height: 40px; } @@ -24,3 +39,12 @@ input[type="number"]::-webkit-outer-spin-button { input[type="number"]::-ms-clear { display: none; } + +.tab-active { + @apply !border-accent; + @apply tab-border-2; +} + +.btn-active { + @apply !bg-secondary; +} From dc77e9a0dfb40aa6c4f71ce8d9b9cf746aa65d1e Mon Sep 17 00:00:00 2001 From: abdulhaseeb13mar Date: Thu, 14 Sep 2023 17:43:13 +0500 Subject: [PATCH 04/39] package: install @tailwindcss/container-queries --- package-lock.json | 9 +++++++++ package.json | 1 + tailwind.config.js | 6 +++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index e8f9d24e..16e970c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@heroicons/react": "^2.0.18", "@radixdlt/radix-dapp-toolkit": "^0.7.1", "@reduxjs/toolkit": "^1.9.5", + "@tailwindcss/container-queries": "^0.1.1", "@types/node": "20.3.3", "@types/react": "18.2.14", "@types/react-dom": "18.2.6", @@ -1711,6 +1712,14 @@ "tslib": "^2.4.0" } }, + "node_modules/@tailwindcss/container-queries": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/container-queries/-/container-queries-0.1.1.tgz", + "integrity": "sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==", + "peerDependencies": { + "tailwindcss": ">=3.2.0" + } + }, "node_modules/@tailwindcss/typography": { "version": "0.5.9", "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.9.tgz", diff --git a/package.json b/package.json index 32a7917e..c65c0c51 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@heroicons/react": "^2.0.18", "@radixdlt/radix-dapp-toolkit": "^0.7.1", "@reduxjs/toolkit": "^1.9.5", + "@tailwindcss/container-queries": "^0.1.1", "@types/node": "20.3.3", "@types/react": "18.2.14", "@types/react-dom": "18.2.6", diff --git a/tailwind.config.js b/tailwind.config.js index 9459a28b..3851aaef 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -70,5 +70,9 @@ module.exports = { prefix: "", // prefix for daisyUI classnames (components, modifiers and responsive class names. Not colors) logs: true, // Shows info about daisyUI version and used config in the console when building your CSS }, - plugins: [require("daisyui"), require("@tailwindcss/typography")], + plugins: [ + require("daisyui"), + require("@tailwindcss/typography"), + require("@tailwindcss/container-queries"), + ], }; From eec05ae7cb8bbe8757571bac1631baa80c7c0a4e Mon Sep 17 00:00:00 2001 From: abdulhaseeb13mar Date: Thu, 14 Sep 2023 22:00:53 +0500 Subject: [PATCH 05/39] order input component polished and code revamp --- src/app/components/OrderInput.tsx | 371 +++++++++++++++++------------- src/app/page.tsx | 2 +- 2 files changed, 209 insertions(+), 164 deletions(-) diff --git a/src/app/components/OrderInput.tsx b/src/app/components/OrderInput.tsx index 018d35d1..5a648280 100644 --- a/src/app/components/OrderInput.tsx +++ b/src/app/components/OrderInput.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { useAppDispatch, useAppSelector } from "../hooks"; import { @@ -16,6 +16,38 @@ import { } from "../redux/orderInputSlice"; import { fetchBalances } from "../redux/pairSelectorSlice"; import { displayAmount } from "../utils"; +import { TokenAvatar } from "common/tokenAvatar"; +import { Input } from "common/input"; + +function SingleGroupButton({ + isActive, + onClick, + avatarUrl, + text, + wrapperClass, +}: { + isActive: boolean; + onClick: () => void; + avatarUrl?: string; + text: string; + wrapperClass?: string; +}) { + return ( +
+ {avatarUrl && } +

+ {text} +

+
+ ); +} function OrderTypeTabs() { const activeTab = useAppSelector((state) => state.orderInput.tab); @@ -24,23 +56,24 @@ function OrderTypeTabs() { function tabClass(isActive: boolean) { return ( - "flex-1 tab tab-bordered no-underline" + (isActive ? " tab-active" : "") + "flex-1 tab tab-bordered border-base-300 no-underline h-full text-base font-semibold pb-3" + + (isActive ? " tab-active" : "") ); } return ( -
- + ); } @@ -82,21 +115,24 @@ function DirectionToggle() { const activeSide = useAppSelector((state) => state.orderInput.side); const dispatch = useAppDispatch(); return ( -
- - +
+

Direction

+
+ { + dispatch(orderInputSlice.actions.setSide(OrderSide.BUY)); + }} + /> + { + dispatch(orderInputSlice.actions.setSide(OrderSide.SELL)); + }} + /> +
); } @@ -109,29 +145,26 @@ function AssetToggle() { const dispatch = useAppDispatch(); return ( -
- - +
+

Asset

+
+ { + dispatch(orderInputSlice.actions.setToken1Selected(true)); + }} + /> + { + dispatch(orderInputSlice.actions.setToken1Selected(false)); + }} + /> +
); } @@ -142,73 +175,94 @@ function PositionSizeInput() { const validationResult = useAppSelector(validatePositionSize); const dispatch = useAppDispatch(); + const [customPercentage, setCustomPercentage] = useState(0); + const [isFirstInteraction, setIsFirstInteraction] = useState(true); + const customPercentInputRef = useRef(null); const handleButtonClick = (percent: number) => { + setCustomPercentage(percent); dispatch(setSizePercent(percent)); if (customPercentInputRef.current) { customPercentInputRef.current.value = ""; } }; + const handleOnChange = (event: React.ChangeEvent) => { + if (isFirstInteraction) setIsFirstInteraction(false); + if (customPercentInputRef.current) { + customPercentInputRef.current.value = ""; + } + const size = Number(event.target.value); + dispatch(orderInputSlice.actions.setSize(size)); + }; + + const handleOnPercentChange = ( + event: React.ChangeEvent + ) => { + if (Number(event.target.value) > 100) return; + const size = Number(event.target.value); + setCustomPercentage(size); + dispatch(setSizePercent(size)); + }; + return (
- -
- ) => { - if (customPercentInputRef.current) { - customPercentInputRef.current.value = ""; - } - const size = Number(event.target.value); - dispatch(orderInputSlice.actions.setSize(size)); - }} - > - -
- Selected Token Icon - {selectedToken.symbol} -
-
+

Position Size

+ + + {selectedToken.symbol} +
+ } + /> -
- - - - - +
+
+ handleButtonClick(25)} + /> + handleButtonClick(50)} + /> +
+
+ handleButtonClick(75)} + /> + handleButtonClick(100)} + /> +
+
+ ) => { - const size = Number(event.target.value); - dispatch(setSizePercent(size)); - }} - > + inputClasses="!w-14" + endAdornmentClasses="px-0 pr-2" + parentClasses="mb-2" + placeholder="0.00" + endAdornment={%} + onChange={handleOnPercentChange} + />
); @@ -219,8 +273,8 @@ function slippagePercentage(slippage: number): number { return slippage * 100; } +// TODO: decimal numbers with dots (1.3) don't work (but 1,3 does) function slippageFromPercentage(percentage: string): number { - // TODO: decimal numbers with dots (1.3) don't work (but 1,3 does) return Number(percentage) / 100; } @@ -228,29 +282,26 @@ function MarketOrderInput() { const slippage = useAppSelector((state) => state.orderInput.slippage); const validationResult = useAppSelector(validateSlippageInput); const dispatch = useAppDispatch(); + const [isFirstInteraction, setIsFirstInteraction] = useState(true); + + const handleOnChange = (event: React.ChangeEvent) => { + if (isFirstInteraction) setIsFirstInteraction(false); + const slippage = slippageFromPercentage(event.target.value); + dispatch(orderInputSlice.actions.setSlippage(slippage)); + }; return ( <>
-
- ) => { - const slippage = slippageFromPercentage(event.target.value); - dispatch(orderInputSlice.actions.setSlippage(slippage)); - }} - > -
- % -
-
+ %} + isError={!isFirstInteraction && !validationResult.valid} + />
@@ -398,18 +445,16 @@ function SubmitButton() { export function OrderInput() { // updates quote when any of the listed dependencies changes const dispatch = useAppDispatch(); + const { + token1Selected, + side, + size, + price, + preventImmediateExecution, + slippage, + tab, + } = useAppSelector((state) => state.orderInput); const pairAddress = useAppSelector((state) => state.pairSelector.address); - const token1Selected = useAppSelector( - (state) => state.orderInput.token1Selected - ); - const side = useAppSelector((state) => state.orderInput.side); - const size = useAppSelector((state) => state.orderInput.size); - const price = useAppSelector((state) => state.orderInput.price); - const preventImmediateExecution = useAppSelector( - (state) => state.orderInput.preventImmediateExecution - ); - const slippage = useAppSelector((state) => state.orderInput.slippage); - const tab = useAppSelector((state) => state.orderInput.tab); const validationResult = useAppSelector(validateOrderInput); @@ -435,12 +480,12 @@ export function OrderInput() { ]); return ( -
+ <> -
+
@@ -450,6 +495,6 @@ export function OrderInput() { -
+ ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index fad4fa4a..7873a6ef 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -19,7 +19,7 @@ export default function Home() {
-
+
From 2a9e02c3878864f77c78e27d2f0d002b374503e8 Mon Sep 17 00:00:00 2001 From: SmashingBumpkin <66125276+SmashingBumpkin@users.noreply.github.com> Date: Wed, 20 Sep 2023 11:04:25 +0200 Subject: [PATCH 06/39] Corrected setPercentage behaviour --- src/app/redux/orderInputSlice.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/redux/orderInputSlice.ts b/src/app/redux/orderInputSlice.ts index a3f10454..863de447 100644 --- a/src/app/redux/orderInputSlice.ts +++ b/src/app/redux/orderInputSlice.ts @@ -149,14 +149,15 @@ export const setSizePercent = createAsyncThunk< } else { // for limit buy orders we can just calculate based on balance and price if (selectToken1Selected(state)) { - balance = (proportion * unselectedBalance) / state.orderInput.price; + balance = unselectedBalance / state.orderInput.price; } else { - balance = proportion * unselectedBalance * state.orderInput.price; + balance = unselectedBalance * state.orderInput.price; } } } else { //for sell orders the calculation is very simple - balance = getSelectedToken(state).balance || 0 * proportion; + balance = getSelectedToken(state).balance || 0; + balance = balance * proportion; //for market sell orders the order quote is retrieved to check liquidity. if (state.orderInput.tab === OrderTab.MARKET) { const quote = await adex.getExchangeOrderQuote( From 6a206bf99635fd8b872cc971a8bb34c212594c75 Mon Sep 17 00:00:00 2001 From: abdulhaseeb13mar Date: Thu, 21 Sep 2023 23:01:14 +0500 Subject: [PATCH 07/39] feat: merket order input functionality --- src/app/components/OrderInput.tsx | 431 +++++++++++++++------------- src/app/components/PairSelector.tsx | 67 +++-- src/app/page.tsx | 2 +- src/app/styles/globals.css | 3 +- 4 files changed, 279 insertions(+), 224 deletions(-) diff --git a/src/app/components/OrderInput.tsx b/src/app/components/OrderInput.tsx index 5a648280..f4d6959c 100644 --- a/src/app/components/OrderInput.tsx +++ b/src/app/components/OrderInput.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from "react"; +import React, { useEffect, useMemo, useRef } from "react"; import { useAppDispatch, useAppSelector } from "../hooks"; import { @@ -10,12 +10,9 @@ import { validateOrderInput, validatePositionSize, validatePriceInput, - validateSlippageInput, submitOrder, - setSizePercent, } from "../redux/orderInputSlice"; import { fetchBalances } from "../redux/pairSelectorSlice"; -import { displayAmount } from "../utils"; import { TokenAvatar } from "common/tokenAvatar"; import { Input } from "common/input"; @@ -49,146 +46,123 @@ function SingleGroupButton({ ); } -function OrderTypeTabs() { - const activeTab = useAppSelector((state) => state.orderInput.tab); - const actions = orderInputSlice.actions; - const dispatch = useAppDispatch(); - - function tabClass(isActive: boolean) { - return ( - "flex-1 tab tab-bordered border-base-300 no-underline h-full text-base font-semibold pb-3" + - (isActive ? " tab-active" : "") - ); - } - return ( -
-
dispatch(actions.setActiveTab(OrderTab.MARKET))} - > - Market -
-
dispatch(actions.setActiveTab(OrderTab.LIMIT))} - > - Limit -
-
- ); -} - -function AvailableBalances() { - const token1 = useAppSelector((state) => state.pairSelector.token1); - const token2 = useAppSelector((state) => state.pairSelector.token2); - return ( -
-
-
Available balances:
-
-
-
-
{displayAmount(token1.balance || 0)}
- Token 1 Icon - {token1.symbol} -
- -
-
{displayAmount(token2.balance || 0)}
- Token 2 Icon - {token2.symbol} -
-
-
- ); -} +// function AvailableBalances() { +// const token1 = useAppSelector((state) => state.pairSelector.token1); +// const token2 = useAppSelector((state) => state.pairSelector.token2); +// return ( +//
+//
+//
Available balances:
+//
+//
+//
+//
{displayAmount(token1.balance || 0)}
+// Token 1 Icon +// {token1.symbol} +//
+ +//
+//
{displayAmount(token2.balance || 0)}
+// Token 2 Icon +// {token2.symbol} +//
+//
+//
+// ); +// } function DirectionToggle() { const activeSide = useAppSelector((state) => state.orderInput.side); const dispatch = useAppDispatch(); + const isBuyActive = activeSide === OrderSide.BUY; + const isSellActive = activeSide === OrderSide.SELL; return ( -
-

Direction

-
- { - dispatch(orderInputSlice.actions.setSide(OrderSide.BUY)); - }} - /> - { - dispatch(orderInputSlice.actions.setSide(OrderSide.SELL)); - }} - /> -
+
+ { + dispatch(orderInputSlice.actions.setSide(OrderSide.BUY)); + }} + wrapperClass={ + "w-1/2 max-w-none border-none " + + (isBuyActive ? "!bg-green-800/10" : "") + } + /> + { + dispatch(orderInputSlice.actions.setSide(OrderSide.SELL)); + }} + wrapperClass={ + "w-1/2 max-w-none border-none " + + (isSellActive ? "!bg-red-800/10" : "") + } + />
); } -function AssetToggle() { - const pairToken1 = useAppSelector((state) => state.pairSelector.token1); - const pairToken2 = useAppSelector((state) => state.pairSelector.token2); - const selectedToken = useAppSelector(getSelectedToken); - - const dispatch = useAppDispatch(); - - return ( -
-

Asset

-
- { - dispatch(orderInputSlice.actions.setToken1Selected(true)); - }} - /> - { - dispatch(orderInputSlice.actions.setToken1Selected(false)); - }} - /> -
-
- ); -} +// function AssetToggle() { +// const pairToken1 = useAppSelector((state) => state.pairSelector.token1); +// const pairToken2 = useAppSelector((state) => state.pairSelector.token2); +// const selectedToken = useAppSelector(getSelectedToken); + +// const dispatch = useAppDispatch(); + +// return ( +//
+//

Asset

+//
+// { +// dispatch(orderInputSlice.actions.setToken1Selected(true)); +// }} +// /> +// { +// dispatch(orderInputSlice.actions.setToken1Selected(false)); +// }} +// /> +//
+//
+// ); +// } function PositionSizeInput() { - const size = useAppSelector((state) => state.orderInput.size); + const { size, quote, side } = useAppSelector((state) => state.orderInput); const selectedToken = useAppSelector(getSelectedToken); const validationResult = useAppSelector(validatePositionSize); + const selectToken2 = useAppSelector((state) => state.pairSelector.token2); const dispatch = useAppDispatch(); - const [customPercentage, setCustomPercentage] = useState(0); - const [isFirstInteraction, setIsFirstInteraction] = useState(true); + // const [customPercentage, setCustomPercentage] = useState(0); const customPercentInputRef = useRef(null); - const handleButtonClick = (percent: number) => { - setCustomPercentage(percent); - dispatch(setSizePercent(percent)); - if (customPercentInputRef.current) { - customPercentInputRef.current.value = ""; - } - }; + // const handleButtonClick = (percent: number) => { + // setCustomPercentage(percent); + // dispatch(setSizePercent(percent)); + // if (customPercentInputRef.current) { + // customPercentInputRef.current.value = ""; + // } + // }; const handleOnChange = (event: React.ChangeEvent) => { - if (isFirstInteraction) setIsFirstInteraction(false); if (customPercentInputRef.current) { customPercentInputRef.current.value = ""; } @@ -196,23 +170,28 @@ function PositionSizeInput() { dispatch(orderInputSlice.actions.setSize(size)); }; - const handleOnPercentChange = ( - event: React.ChangeEvent - ) => { - if (Number(event.target.value) > 100) return; - const size = Number(event.target.value); - setCustomPercentage(size); - dispatch(setSizePercent(size)); - }; + // const handleOnPercentChange = ( + // event: React.ChangeEvent + // ) => { + // if (Number(event.target.value) > 100) return; + // const size = Number(event.target.value); + // setCustomPercentage(size); + // dispatch(setSizePercent(size)); + // }; + + const isBuy = useMemo(() => side === OrderSide.BUY, [side]); + const isSell = useMemo(() => side === OrderSide.SELL, [side]); return ( -
-

Position Size

+
+

+ {side.toUpperCase()} Amount: +

@@ -221,12 +200,61 @@ function PositionSizeInput() { } /> -
+

+ {isBuy ? "You will give:" : "You will get:"} +

+ + + {selectToken2.symbol} +
+ } + /> + + +
+ +
+ Total fee: {quote?.totalFeesXrd?.toFixed(8) ?? 0}{" "} + {selectToken2.symbol} +
+
+
+
Exchange Fee:
+
{quote?.exchangeFeesXrd} XRD
+
+
+
Platform Fee:
+
{quote?.platformFeesXrd} XRD
+
+
+
Liquidity Fee:
+
{quote?.liquidityFeesXrd} XRD
+
+
+
+ {/*
%} onChange={handleOnPercentChange} /> -
+
*/}
); } // TODO: test if floating point numbers are handled correctly -function slippagePercentage(slippage: number): number { - return slippage * 100; -} +// function slippagePercentage(slippage: number): number { +// return slippage * 100; +// } // TODO: decimal numbers with dots (1.3) don't work (but 1,3 does) -function slippageFromPercentage(percentage: string): number { - return Number(percentage) / 100; -} +// function slippageFromPercentage(percentage: string): number { +// return Number(percentage) / 100; +// } function MarketOrderInput() { - const slippage = useAppSelector((state) => state.orderInput.slippage); - const validationResult = useAppSelector(validateSlippageInput); - const dispatch = useAppDispatch(); - const [isFirstInteraction, setIsFirstInteraction] = useState(true); - - const handleOnChange = (event: React.ChangeEvent) => { - if (isFirstInteraction) setIsFirstInteraction(false); - const slippage = slippageFromPercentage(event.target.value); - dispatch(orderInputSlice.actions.setSlippage(slippage)); - }; + // const slippage = useAppSelector((state) => state.orderInput.slippage); + // const validationResult = useAppSelector(validateSlippageInput); + // const dispatch = useAppDispatch(); + + // const handleOnChange = (event: React.ChangeEvent) => { + // const slippage = slippageFromPercentage(event.target.value); + // dispatch(orderInputSlice.actions.setSlippage(slippage)); + // }; return ( <> -
+ {/*
@@ -300,15 +326,14 @@ function MarketOrderInput() { value={slippagePercentage(slippage)} onChange={handleOnChange} endAdornment={%} - isError={!isFirstInteraction && !validationResult.valid} + isError={!validationResult.valid} /> -
- +
*/} ); @@ -320,11 +345,9 @@ function LimitOrderInput() { const validationResult = useAppSelector(validatePriceInput); const bestBuyPrice = useAppSelector((state) => state.orderBook.bestBuy); const bestSellPrice = useAppSelector((state) => state.orderBook.bestSell); - const [isFirstInteraction, setIsFirstInteraction] = useState(true); const dispatch = useAppDispatch(); const handleOnChange = (event: React.ChangeEvent) => { - if (isFirstInteraction) setIsFirstInteraction(false); const price = Number(event.target.value); dispatch(orderInputSlice.actions.setPrice(price)); }; @@ -332,12 +355,12 @@ function LimitOrderInput() { return ( <>
-

Price

+

Amount

@@ -385,31 +408,31 @@ function LimitOrderInput() { ); } -function Description() { - const description = useAppSelector((state) => state.orderInput.description); - const quote = useAppSelector((state) => state.orderInput.quote); - return ( -
-
- - {quote?.message} -
-

{description}

-
- - - {displayAmount(quote?.platformFeesXrd || 0, 7)} XRD - -
-
- - - {displayAmount(quote?.totalFeesXrd || 0, 7)} XRD - -
-
- ); -} +// function Description() { +// const description = useAppSelector((state) => state.orderInput.description); +// const quote = useAppSelector((state) => state.orderInput.quote); +// return ( +//
+//
+// +// {quote?.message} +//
+//

{description}

+//
+// +// +// {displayAmount(quote?.platformFeesXrd || 0, 7)} XRD +// +//
+//
+// +// +// {displayAmount(quote?.totalFeesXrd || 0, 7)} XRD +// +//
+//
+// ); +// } function SubmitButton() { const symbol = useAppSelector(getSelectedToken).symbol; @@ -429,7 +452,7 @@ function SubmitButton() { symbol; return ( -
+
- - ))} - -
+ <> +
+ +
    + {pairSelector.pairsList.map((pair, index) => ( +
  • + +
  • + ))} +
+
+ + ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 7873a6ef..fad4fa4a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -19,7 +19,7 @@ export default function Home() {
-
+
diff --git a/src/app/styles/globals.css b/src/app/styles/globals.css index 65d814d8..50d62624 100644 --- a/src/app/styles/globals.css +++ b/src/app/styles/globals.css @@ -42,9 +42,8 @@ input[type="number"]::-ms-clear { .tab-active { @apply !border-accent; - @apply tab-border-2; } .btn-active { - @apply !bg-secondary; + @apply bg-secondary; } From bbdaed92d224d8837536c60864ff654a49d28107 Mon Sep 17 00:00:00 2001 From: abdulhaseeb13mar Date: Sat, 23 Sep 2023 17:12:18 +0500 Subject: [PATCH 08/39] revamp: limit order initial functionality --- src/app/components/OrderInput.tsx | 296 ++++++++++-------------------- 1 file changed, 97 insertions(+), 199 deletions(-) diff --git a/src/app/components/OrderInput.tsx b/src/app/components/OrderInput.tsx index f4d6959c..b70cc9b3 100644 --- a/src/app/components/OrderInput.tsx +++ b/src/app/components/OrderInput.tsx @@ -46,39 +46,6 @@ function SingleGroupButton({ ); } -// function AvailableBalances() { -// const token1 = useAppSelector((state) => state.pairSelector.token1); -// const token2 = useAppSelector((state) => state.pairSelector.token2); -// return ( -//
-//
-//
Available balances:
-//
-//
-//
-//
{displayAmount(token1.balance || 0)}
-// Token 1 Icon -// {token1.symbol} -//
- -//
-//
{displayAmount(token2.balance || 0)}
-// Token 2 Icon -// {token2.symbol} -//
-//
-//
-// ); -// } - function DirectionToggle() { const activeSide = useAppSelector((state) => state.orderInput.side); const dispatch = useAppDispatch(); @@ -112,55 +79,20 @@ function DirectionToggle() { ); } -// function AssetToggle() { -// const pairToken1 = useAppSelector((state) => state.pairSelector.token1); -// const pairToken2 = useAppSelector((state) => state.pairSelector.token2); -// const selectedToken = useAppSelector(getSelectedToken); - -// const dispatch = useAppDispatch(); - -// return ( -//
-//

Asset

-//
-// { -// dispatch(orderInputSlice.actions.setToken1Selected(true)); -// }} -// /> -// { -// dispatch(orderInputSlice.actions.setToken1Selected(false)); -// }} -// /> -//
-//
-// ); -// } - function PositionSizeInput() { - const { size, quote, side } = useAppSelector((state) => state.orderInput); - const selectedToken = useAppSelector(getSelectedToken); + const { size, quote, side, tab, price } = useAppSelector( + (state) => state.orderInput + ); + const { + token1: { balance: balance1 }, + token2: { balance: balance2 }, + } = useAppSelector((state) => state.pairSelector); const validationResult = useAppSelector(validatePositionSize); - const selectToken2 = useAppSelector((state) => state.pairSelector.token2); + const { token2, token1 } = useAppSelector((state) => state.pairSelector); const dispatch = useAppDispatch(); - // const [customPercentage, setCustomPercentage] = useState(0); const customPercentInputRef = useRef(null); - // const handleButtonClick = (percent: number) => { - // setCustomPercentage(percent); - // dispatch(setSizePercent(percent)); - // if (customPercentInputRef.current) { - // customPercentInputRef.current.value = ""; - // } - // }; const handleOnChange = (event: React.ChangeEvent) => { if (customPercentInputRef.current) { @@ -181,12 +113,31 @@ function PositionSizeInput() { const isBuy = useMemo(() => side === OrderSide.BUY, [side]); const isSell = useMemo(() => side === OrderSide.SELL, [side]); + const feeToken = useMemo( + () => (isBuy ? token1 : token2), + [isBuy, token1, token2] + ); + + const token2Amount = useMemo( + () => + tab === OrderTab.MARKET + ? (quote?.avgPrice ?? 0) * size + : (price ?? 0) * size, + [price, quote?.avgPrice, size, tab] + ); return (
-

- {side.toUpperCase()} Amount: -

+
+

+ {side.toUpperCase()} Amount: +

+ {isSell && ( +

+ Balance: {balance1} +

+ )} +
- - {selectedToken.symbol} + + {token1.symbol}
} /> @@ -208,19 +159,25 @@ function PositionSizeInput() { -

- {isBuy ? "You will give:" : "You will get:"} -

+
+

+ {isBuy ? "You will give:" : "You will get:"} +

+ {isBuy && ( +

+ Balance: {balance2} +

+ )} +
- - {selectToken2.symbol} + + {token2.symbol}
} /> @@ -233,65 +190,32 @@ function PositionSizeInput() { -
+
- Total fee: {quote?.totalFeesXrd?.toFixed(8) ?? 0}{" "} - {selectToken2.symbol} + Total fee: {quote?.totalFees ?? 0} {feeToken.symbol}
Exchange Fee:
-
{quote?.exchangeFeesXrd} XRD
+
+ {quote?.exchangeFees} {feeToken.symbol} +
Platform Fee:
-
{quote?.platformFeesXrd} XRD
+
+ {quote?.platformFees} {feeToken.symbol} +
Liquidity Fee:
-
{quote?.liquidityFeesXrd} XRD
+
+ {quote?.liquidityFees} {feeToken.symbol} +
- {/*
-
-
- handleButtonClick(25)} - /> - handleButtonClick(50)} - /> -
-
- handleButtonClick(75)} - /> - handleButtonClick(100)} - /> -
-
- %} - onChange={handleOnPercentChange} - /> -
*/}
); } @@ -340,7 +264,7 @@ function MarketOrderInput() { } function LimitOrderInput() { - const price = useAppSelector((state) => state.orderInput.price); + const { price, side } = useAppSelector((state) => state.orderInput); const priceToken = useAppSelector((state) => state.pairSelector.token2); const validationResult = useAppSelector(validatePriceInput); const bestBuyPrice = useAppSelector((state) => state.orderBook.bestBuy); @@ -354,8 +278,35 @@ function LimitOrderInput() { return ( <> -
-

Amount

+
+
+

+ {side.toUpperCase()} Price +

+

+ dispatch( + orderInputSlice.actions.setPrice( + side === OrderSide.BUY + ? bestBuyPrice || 0 + : bestSellPrice || 0 + ) + ) + } + > + Best Price:{" "} + + {side === OrderSide.BUY ? bestBuyPrice : bestSellPrice}{" "} + {priceToken.symbol} + +

+
-
- - dispatch(orderInputSlice.actions.setPrice(bestBuyPrice || 0)) - } - wrapperClass="max-w-[30ch]" - /> - - dispatch(orderInputSlice.actions.setPrice(bestSellPrice || 0)) - } - wrapperClass="max-w-[30ch]" - /> -
- -
- - dispatch(orderInputSlice.actions.togglePreventImmediateExecution()) - } - /> - Prevent immediate execution -
); } -// function Description() { -// const description = useAppSelector((state) => state.orderInput.description); -// const quote = useAppSelector((state) => state.orderInput.quote); -// return ( -//
-//
-// -// {quote?.message} -//
-//

{description}

-//
-// -// -// {displayAmount(quote?.platformFeesXrd || 0, 7)} XRD -// -//
-//
-// -// -// {displayAmount(quote?.totalFeesXrd || 0, 7)} XRD -// -//
-//
-// ); -// } - function SubmitButton() { const symbol = useAppSelector(getSelectedToken).symbol; const tab = useAppSelector((state) => state.orderInput.tab); @@ -453,6 +349,16 @@ function SubmitButton() { return (
+
+ + dispatch(orderInputSlice.actions.togglePreventImmediateExecution()) + } + /> + Prevent immediate execution +
From 9b4a4ed70506f09ae22857633844b8b97803f767 Mon Sep 17 00:00:00 2001 From: abdulhaseeb13mar Date: Sun, 24 Sep 2023 21:32:55 +0500 Subject: [PATCH 09/39] update: hide balance,prevent default in limit only --- src/app/components/OrderInput.tsx | 46 +++++++++++++++++-------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/src/app/components/OrderInput.tsx b/src/app/components/OrderInput.tsx index b70cc9b3..57d3b428 100644 --- a/src/app/components/OrderInput.tsx +++ b/src/app/components/OrderInput.tsx @@ -83,10 +83,10 @@ function PositionSizeInput() { const { size, quote, side, tab, price } = useAppSelector( (state) => state.orderInput ); - const { - token1: { balance: balance1 }, - token2: { balance: balance2 }, - } = useAppSelector((state) => state.pairSelector); + // const { + // token1: { balance: balance1 }, + // token2: { balance: balance2 }, + // } = useAppSelector((state) => state.pairSelector); const validationResult = useAppSelector(validatePositionSize); const { token2, token1 } = useAppSelector((state) => state.pairSelector); const dispatch = useAppDispatch(); @@ -130,13 +130,13 @@ function PositionSizeInput() {

- {side.toUpperCase()} Amount: + {side === OrderSide.BUY ? "Buy" : "Sell"} Amount:

- {isSell && ( + {/* {isSell && (

Balance: {balance1}

- )} + )} */}

- {isBuy ? "You will give:" : "You will get:"} + {isBuy ? "You pay" : "You receive:"}

- {isBuy && ( + {/* {isBuy && (

Balance: {balance2}

- )} + )} */}

- {side.toUpperCase()} Price + {side === OrderSide.BUY ? "Buy" : "Sell"} Price

-

- - dispatch(orderInputSlice.actions.togglePreventImmediateExecution()) - } - /> - Prevent immediate execution -
+ {tab === OrderTab.LIMIT && ( +
+ + dispatch( + orderInputSlice.actions.togglePreventImmediateExecution() + ) + } + /> + Prevent immediate execution +
+ )} + +
+ //
+ // + // {selectedToken.symbol} + //
} />
- - {token2.symbol} + + + {unSelectedToken.symbol} +
} /> @@ -193,25 +220,25 @@ function PositionSizeInput() {
- Total fee: {quote?.totalFees ?? 0} {feeToken.symbol} + Total fee: {quote?.totalFees ?? 0} {quote?.toToken.symbol}
Exchange Fee:
- {quote?.exchangeFees} {feeToken.symbol} + {quote?.exchangeFees} {quote?.toToken.symbol}
Platform Fee:
- {quote?.platformFees} {feeToken.symbol} + {quote?.platformFees} {quote?.toToken.symbol}
Liquidity Fee:
- {quote?.liquidityFees} {feeToken.symbol} + {quote?.liquidityFees} {quote?.toToken.symbol}
@@ -313,10 +340,10 @@ function LimitOrderInput() { onChange={handleOnChange} isError={!validationResult.valid} endAdornment={ -
- - {priceToken.symbol} -
+ } />
+ } + /> + + +
+ +
+ Total fee: {quote?.totalFees ?? 0} {quote?.toToken.symbol} +
+
+
+
Exchange Fee:
+
+ {quote?.exchangeFees} {quote?.toToken.symbol} +
+
+
+
Platform Fee:
+
+ {quote?.platformFees} {quote?.toToken.symbol} +
+
+
+
Liquidity Fee:
+
+ {quote?.liquidityFees} {quote?.toToken.symbol} +
+
+
+
+
+ ); +} diff --git a/src/app/components/order_input/LimitOrderInput.tsx b/src/app/components/order_input/LimitOrderInput.tsx new file mode 100644 index 00000000..817b5518 --- /dev/null +++ b/src/app/components/order_input/LimitOrderInput.tsx @@ -0,0 +1,78 @@ +import React from "react"; + +import { Input } from "common/input"; +import { TokenWithSymbol } from "common/tokenWithSymbol"; +import { useAppDispatch, useAppSelector } from "hooks"; +import { + OrderSide, + orderInputSlice, + validatePriceInput, +} from "redux/orderInputSlice"; +import { AmountInput } from "./AmountInput"; + +export function LimitOrderInput() { + const { price, side } = useAppSelector((state) => state.orderInput); + const priceToken = useAppSelector((state) => state.pairSelector.token2); + const validationResult = useAppSelector(validatePriceInput); + const bestBuyPrice = useAppSelector((state) => state.orderBook.bestBuy); + const bestSellPrice = useAppSelector((state) => state.orderBook.bestSell); + const dispatch = useAppDispatch(); + + const handleOnChange = (event: React.ChangeEvent) => { + const price = Number(event.target.value); + dispatch(orderInputSlice.actions.setPrice(price)); + }; + + return ( + <> +
+
+

+ {side === OrderSide.BUY ? "Buy" : "Sell"} Price +

+

+ dispatch( + orderInputSlice.actions.setPrice( + side === OrderSide.BUY + ? bestBuyPrice || 0 + : bestSellPrice || 0 + ) + ) + } + > + Best Price:{" "} + + {side === OrderSide.BUY ? bestBuyPrice : bestSellPrice}{" "} + {priceToken.symbol} + +

+
+ + } + /> + +
+ + + ); +} diff --git a/src/app/components/order_input/MarketOrderInput.tsx b/src/app/components/order_input/MarketOrderInput.tsx new file mode 100644 index 00000000..71546c25 --- /dev/null +++ b/src/app/components/order_input/MarketOrderInput.tsx @@ -0,0 +1,34 @@ +import { AmountInput } from "./AmountInput"; + +export function MarketOrderInput() { + // const slippage = useAppSelector((state) => state.orderInput.slippage); + // const validationResult = useAppSelector(validateSlippageInput); + // const dispatch = useAppDispatch(); + + // const handleOnChange = (event: React.ChangeEvent) => { + // const slippage = slippageFromPercentage(event.target.value); + // dispatch(orderInputSlice.actions.setSlippage(slippage)); + // }; + return ( + <> + {/*
+ + %} + isError={!validationResult.valid} + /> + +
*/} + + + ); +} diff --git a/src/app/components/order_input/OrderInput.tsx b/src/app/components/order_input/OrderInput.tsx new file mode 100644 index 00000000..6bb51051 --- /dev/null +++ b/src/app/components/order_input/OrderInput.tsx @@ -0,0 +1,173 @@ +import { useEffect } from "react"; + +import { TokenAvatar } from "common/tokenAvatar"; +import { useAppDispatch, useAppSelector } from "hooks"; +import { + OrderSide, + OrderTab, + fetchQuote, + getSelectedToken, + orderInputSlice, + submitOrder, + validateOrderInput, +} from "redux/orderInputSlice"; +import { fetchBalances } from "redux/pairSelectorSlice"; +import { OrderTypeTabs } from "./OrderTypeTabs"; +import { MarketOrderInput } from "./MarketOrderInput"; +import { LimitOrderInput } from "./LimitOrderInput"; + +function SingleGroupButton({ + isActive, + onClick, + avatarUrl, + text, + wrapperClass, +}: { + isActive: boolean; + onClick: () => void; + avatarUrl?: string; + text: string; + wrapperClass?: string; +}) { + return ( +
+ {avatarUrl && } +

+ {text} +

+
+ ); +} + +function DirectionToggle() { + const activeSide = useAppSelector((state) => state.orderInput.side); + const dispatch = useAppDispatch(); + const isBuyActive = activeSide === OrderSide.BUY; + const isSellActive = activeSide === OrderSide.SELL; + return ( +
+ { + dispatch(orderInputSlice.actions.setSide(OrderSide.BUY)); + }} + wrapperClass={ + "w-1/2 max-w-none border-none " + (isBuyActive ? "!bg-neutral" : "") + } + /> + { + dispatch(orderInputSlice.actions.setSide(OrderSide.SELL)); + }} + wrapperClass={ + "w-1/2 max-w-none border-none " + (isSellActive ? "!bg-neutral" : "") + } + /> +
+ ); +} + +function SubmitButton() { + const symbol = useAppSelector(getSelectedToken).symbol; + const tab = useAppSelector((state) => state.orderInput.tab); + const side = useAppSelector((state) => state.orderInput.side); + const transactionInProgress = useAppSelector( + (state) => state.orderInput.transactionInProgress + ); + const transactionResult = useAppSelector( + (state) => state.orderInput.transactionResult + ); + const validationResult = useAppSelector(validateOrderInput); + const dispatch = useAppDispatch(); + const submitString = + (tab === OrderTab.LIMIT ? "LIMIT " : "") + + (side === OrderSide.BUY ? "Buy " : "Sell ") + + symbol; + + return ( +
+ {tab === OrderTab.LIMIT && ( +
+ + dispatch( + orderInputSlice.actions.togglePreventImmediateExecution() + ) + } + /> + Prevent immediate execution +
+ )} + +
{transactionResult}
+
+ ); +} + +export function OrderInput() { + const dispatch = useAppDispatch(); + const { + token1Selected, + side, + size, + price, + preventImmediateExecution, + slippage, + tab, + } = useAppSelector((state) => state.orderInput); + const pairAddress = useAppSelector((state) => state.pairSelector.address); + + const validationResult = useAppSelector(validateOrderInput); + + useEffect(() => { + dispatch(fetchBalances()); + }, [dispatch, pairAddress]); + + useEffect(() => { + if (validationResult.valid) { + dispatch(fetchQuote()); + } + }, [ + dispatch, + pairAddress, + side, + size, + price, + slippage, + tab, + token1Selected, + preventImmediateExecution, + validationResult, + ]); + + return ( + <> + +
+ +
+ {tab === OrderTab.MARKET ? : } + +
+
+ + ); +} diff --git a/src/app/components/order_input/OrderTypeTabs.tsx b/src/app/components/order_input/OrderTypeTabs.tsx new file mode 100644 index 00000000..914d608e --- /dev/null +++ b/src/app/components/order_input/OrderTypeTabs.tsx @@ -0,0 +1,31 @@ +import { useAppDispatch, useAppSelector } from "hooks"; +import { OrderTab, orderInputSlice } from "redux/orderInputSlice"; + +export function OrderTypeTabs() { + const activeTab = useAppSelector((state) => state.orderInput.tab); + const actions = orderInputSlice.actions; + const dispatch = useAppDispatch(); + + function tabClass(isActive: boolean) { + return ( + "flex-1 tab no-underline h-full text-base font-semibold py-3 tab-border-2" + + (isActive ? " tab-active tab-bordered" : "") + ); + } + return ( +
+
dispatch(actions.setActiveTab(OrderTab.MARKET))} + > + Market +
+
dispatch(actions.setActiveTab(OrderTab.LIMIT))} + > + Limit +
+
+ ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index c2df5ed0..2877232b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,7 +1,7 @@ "use client"; import { OrderBook } from "components/OrderBook"; -import { OrderInput } from "components/OrderInput"; +import { OrderInput } from "components/order_input/OrderInput"; import { PairSelector } from "components/PairSelector"; import { PriceChart } from "components/PriceChart"; import { AccountHistory } from "components/AccountHistory"; From a52a12617a74146b93cbeee234f3188152a0fe40 Mon Sep 17 00:00:00 2001 From: Evgeniia Vakarina <27793901+EvgeniiaVak@users.noreply.github.com> Date: Sun, 8 Oct 2023 13:54:33 +0400 Subject: [PATCH 16/39] Revert "run npm audit fix" This reverts commit 10266ee510cfda557fd0934c286745bdc8d925b7. --- package-lock.json | 146 ++++++++++++++++++++++++++++------------------ package.json | 2 +- 2 files changed, 90 insertions(+), 58 deletions(-) diff --git a/package-lock.json b/package-lock.json index 755c81b4..48127df0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "eslint-config-next": "13.4.7", "lightweight-charts": "^4.0.1", "next": "^13.4.19", - "postcss": "^8.4.31", + "postcss": "8.4.24", "react": "18.2.0", "react-dom": "18.2.0", "react-icons": "^4.11.0", @@ -1389,9 +1389,9 @@ } }, "node_modules/@next/env": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.4.tgz", - "integrity": "sha512-LGegJkMvRNw90WWphGJ3RMHMVplYcOfRWf2Be3td3sUa+1AaxmsYyANsA+znrGCBjXJNi4XAQlSoEfUxs/4kIQ==" + "version": "13.4.19", + "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.19.tgz", + "integrity": "sha512-FsAT5x0jF2kkhNkKkukhsyYOrRqtSxrEhfliniIq0bwWbuXLgyt3Gv0Ml+b91XwjwArmuP7NxCiGd++GGKdNMQ==" }, "node_modules/@next/eslint-plugin-next": { "version": "13.4.7", @@ -1402,9 +1402,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.4.tgz", - "integrity": "sha512-Df8SHuXgF1p+aonBMcDPEsaahNo2TCwuie7VXED4FVyECvdXfRT9unapm54NssV9tF3OQFKBFOdlje4T43VO0w==", + "version": "13.4.19", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.19.tgz", + "integrity": "sha512-vv1qrjXeGbuF2mOkhkdxMDtv9np7W4mcBtaDnHU+yJG+bBwa6rYsYSCI/9Xm5+TuF5SbZbrWO6G1NfTh1TMjvQ==", "cpu": [ "arm64" ], @@ -1417,9 +1417,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.4.tgz", - "integrity": "sha512-siPuUwO45PnNRMeZnSa8n/Lye5ZX93IJom9wQRB5DEOdFrw0JjOMu1GINB8jAEdwa7Vdyn1oJ2xGNaQpdQQ9Pw==", + "version": "13.4.19", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.19.tgz", + "integrity": "sha512-jyzO6wwYhx6F+7gD8ddZfuqO4TtpJdw3wyOduR4fxTUCm3aLw7YmHGYNjS0xRSYGAkLpBkH1E0RcelyId6lNsw==", "cpu": [ "x64" ], @@ -1432,9 +1432,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.4.tgz", - "integrity": "sha512-l/k/fvRP/zmB2jkFMfefmFkyZbDkYW0mRM/LB+tH5u9pB98WsHXC0WvDHlGCYp3CH/jlkJPL7gN8nkTQVrQ/2w==", + "version": "13.4.19", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.19.tgz", + "integrity": "sha512-vdlnIlaAEh6H+G6HrKZB9c2zJKnpPVKnA6LBwjwT2BTjxI7e0Hx30+FoWCgi50e+YO49p6oPOtesP9mXDRiiUg==", "cpu": [ "arm64" ], @@ -1447,9 +1447,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.4.tgz", - "integrity": "sha512-YYGb7SlLkI+XqfQa8VPErljb7k9nUnhhRrVaOdfJNCaQnHBcvbT7cx/UjDQLdleJcfyg1Hkn5YSSIeVfjgmkTg==", + "version": "13.4.19", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.19.tgz", + "integrity": "sha512-aU0HkH2XPgxqrbNRBFb3si9Ahu/CpaR5RPmN2s9GiM9qJCiBBlZtRTiEca+DC+xRPyCThTtWYgxjWHgU7ZkyvA==", "cpu": [ "arm64" ], @@ -1462,9 +1462,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.4.tgz", - "integrity": "sha512-uE61vyUSClnCH18YHjA8tE1prr/PBFlBFhxBZis4XBRJoR+txAky5d7gGNUIbQ8sZZ7LVkSVgm/5Fc7mwXmRAg==", + "version": "13.4.19", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.19.tgz", + "integrity": "sha512-htwOEagMa/CXNykFFeAHHvMJeqZfNQEoQvHfsA4wgg5QqGNqD5soeCer4oGlCol6NGUxknrQO6VEustcv+Md+g==", "cpu": [ "x64" ], @@ -1477,9 +1477,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.4.tgz", - "integrity": "sha512-qVEKFYML/GvJSy9CfYqAdUexA6M5AklYcQCW+8JECmkQHGoPxCf04iMh7CPR7wkHyWWK+XLt4Ja7hhsPJtSnhg==", + "version": "13.4.19", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.19.tgz", + "integrity": "sha512-4Gj4vvtbK1JH8ApWTT214b3GwUh9EKKQjY41hH/t+u55Knxi/0wesMzwQRhppK6Ddalhu0TEttbiJ+wRcoEj5Q==", "cpu": [ "x64" ], @@ -1492,9 +1492,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.4.tgz", - "integrity": "sha512-mDSQfqxAlfpeZOLPxLymZkX0hYF3juN57W6vFHTvwKlnHfmh12Pt7hPIRLYIShk8uYRsKPtMTth/EzpwRI+u8w==", + "version": "13.4.19", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.19.tgz", + "integrity": "sha512-bUfDevQK4NsIAHXs3/JNgnvEY+LRyneDN788W2NYiRIIzmILjba7LaQTfihuFawZDhRtkYCv3JDC3B4TwnmRJw==", "cpu": [ "arm64" ], @@ -1507,9 +1507,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.4.tgz", - "integrity": "sha512-aoqAT2XIekIWoriwzOmGFAvTtVY5O7JjV21giozBTP5c6uZhpvTWRbmHXbmsjZqY4HnEZQRXWkSAppsIBweKqw==", + "version": "13.4.19", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.19.tgz", + "integrity": "sha512-Y5kikILFAr81LYIFaw6j/NrOtmiM4Sf3GtOc0pn50ez2GCkr+oejYuKGcwAwq3jiTKuzF6OF4iT2INPoxRycEA==", "cpu": [ "ia32" ], @@ -1522,9 +1522,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.4.tgz", - "integrity": "sha512-cyRvlAxwlddlqeB9xtPSfNSCRy8BOa4wtMo0IuI9P7Y0XT2qpDrpFKRyZ7kUngZis59mPVla5k8X1oOJ8RxDYg==", + "version": "13.4.19", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.19.tgz", + "integrity": "sha512-YzA78jBDXMYiINdPdJJwGgPNT3YqBNNGhsthsDoWHL9p24tEJn9ViQf/ZqTbwSpX/RrkPupLfuuTH2sf73JBAw==", "cpu": [ "x64" ], @@ -1697,9 +1697,9 @@ } }, "node_modules/@swc/helpers": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", - "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.1.tgz", + "integrity": "sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==", "dependencies": { "tslib": "^2.4.0" } @@ -7111,34 +7111,35 @@ "integrity": "sha512-kPZKRs4VkdloCGQXPoP84q4sT/1Z+lYM61AXyV8wWa2hnuo5KpPBF2S3crSFnMrOgUISmEBP8Vo/ngGZX60NhA==" }, "node_modules/next": { - "version": "13.5.4", - "resolved": "https://registry.npmjs.org/next/-/next-13.5.4.tgz", - "integrity": "sha512-+93un5S779gho8y9ASQhb/bTkQF17FNQOtXLKAj3lsNgltEcF0C5PMLLncDmH+8X1EnJH1kbqAERa29nRXqhjA==", + "version": "13.4.19", + "resolved": "https://registry.npmjs.org/next/-/next-13.4.19.tgz", + "integrity": "sha512-HuPSzzAbJ1T4BD8e0bs6B9C1kWQ6gv8ykZoRWs5AQoiIuqbGHHdQO7Ljuvg05Q0Z24E2ABozHe6FxDvI6HfyAw==", "dependencies": { - "@next/env": "13.5.4", - "@swc/helpers": "0.5.2", + "@next/env": "13.4.19", + "@swc/helpers": "0.5.1", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001406", - "postcss": "8.4.31", + "postcss": "8.4.14", "styled-jsx": "5.1.1", - "watchpack": "2.4.0" + "watchpack": "2.4.0", + "zod": "3.21.4" }, "bin": { "next": "dist/bin/next" }, "engines": { - "node": ">=16.14.0" + "node": ">=16.8.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "13.5.4", - "@next/swc-darwin-x64": "13.5.4", - "@next/swc-linux-arm64-gnu": "13.5.4", - "@next/swc-linux-arm64-musl": "13.5.4", - "@next/swc-linux-x64-gnu": "13.5.4", - "@next/swc-linux-x64-musl": "13.5.4", - "@next/swc-win32-arm64-msvc": "13.5.4", - "@next/swc-win32-ia32-msvc": "13.5.4", - "@next/swc-win32-x64-msvc": "13.5.4" + "@next/swc-darwin-arm64": "13.4.19", + "@next/swc-darwin-x64": "13.4.19", + "@next/swc-linux-arm64-gnu": "13.4.19", + "@next/swc-linux-arm64-musl": "13.4.19", + "@next/swc-linux-x64-gnu": "13.4.19", + "@next/swc-linux-x64-musl": "13.4.19", + "@next/swc-win32-arm64-msvc": "13.4.19", + "@next/swc-win32-ia32-msvc": "13.4.19", + "@next/swc-win32-x64-msvc": "13.4.19" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -7155,6 +7156,37 @@ } } }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", + "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], + "dependencies": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/next/node_modules/zod": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -7617,9 +7649,9 @@ } }, "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "version": "8.4.24", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", + "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", "funding": [ { "type": "opencollective", @@ -9679,9 +9711,9 @@ } }, "node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.2.tgz", + "integrity": "sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 196039ad..854feb57 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "eslint-config-next": "13.4.7", "lightweight-charts": "^4.0.1", "next": "^13.4.19", - "postcss": "^8.4.31", + "postcss": "8.4.24", "react": "18.2.0", "react-dom": "18.2.0", "react-icons": "^4.11.0", From c5f453d695be9a3c7f138578d604e4c979c0d5af Mon Sep 17 00:00:00 2001 From: Evgeniia Vakarina <27793901+EvgeniiaVak@users.noreply.github.com> Date: Mon, 9 Oct 2023 20:53:37 +0400 Subject: [PATCH 17/39] functionality from figma for limit orders --- src/app/common/input.tsx | 41 +- src/app/common/tokenWithSymbol.tsx | 2 +- .../components/order_input/AmountInput.tsx | 241 ++++----- .../order_input/LimitOrderInput.tsx | 55 +- .../order_input/MarketOrderInput.tsx | 58 ++- src/app/components/order_input/OrderInput.tsx | 93 +--- src/app/redux/orderInputSlice.ts | 477 +++++++++--------- src/app/redux/pairSelectorSlice.ts | 6 +- src/app/subscriptions.ts | 2 + 9 files changed, 431 insertions(+), 544 deletions(-) diff --git a/src/app/common/input.tsx b/src/app/common/input.tsx index 38b85251..16076a08 100644 --- a/src/app/common/input.tsx +++ b/src/app/common/input.tsx @@ -1,36 +1,43 @@ import React, { ReactNode } from "react"; +import { ValidationResult } from "redux/orderInputSlice"; type Props = Omit & { parentClasses?: string; inputClasses?: string; endAdornmentClasses?: string; endAdornment?: ReactNode; - isError?: boolean; + validation?: ValidationResult; }; export const Input = ({ parentClasses = "", inputClasses = "", endAdornment, - isError = false, endAdornmentClasses = "", + validation, ...inputProps }: Props) => { + const isError = validation?.valid === false; return ( -
- - {endAdornment && ( -
{endAdornment}
- )} -
+ <> +
+ + {endAdornment && ( +
{endAdornment}
+ )} +
+ + ); }; diff --git a/src/app/common/tokenWithSymbol.tsx b/src/app/common/tokenWithSymbol.tsx index 2c994bec..e56e7675 100644 --- a/src/app/common/tokenWithSymbol.tsx +++ b/src/app/common/tokenWithSymbol.tsx @@ -8,7 +8,7 @@ type Props = { export const TokenWithSymbol = ({ logoUrl, symbol }: Props) => { return ( -
+
{symbol}
diff --git a/src/app/components/order_input/AmountInput.tsx b/src/app/components/order_input/AmountInput.tsx index c302feb3..962eb70a 100644 --- a/src/app/components/order_input/AmountInput.tsx +++ b/src/app/components/order_input/AmountInput.tsx @@ -1,152 +1,117 @@ -import React, { useMemo, useRef } from "react"; -import { RxChevronDown } from "react-icons/rx"; +import React from "react"; -import { Input } from "common/input"; -import { TokenAvatar } from "common/tokenAvatar"; -import { TokenWithSymbol } from "common/tokenWithSymbol"; import { useAppDispatch, useAppSelector } from "hooks"; -import { - OrderSide, - getSelectedToken, - getUnselectedToken, - orderInputSlice, - validatePositionSize, -} from "redux/orderInputSlice"; +import { OrderSide, TokenInput, orderInputSlice } from "redux/orderInputSlice"; -export function AmountInput() { - const { size, quote, side, token1Selected } = useAppSelector( - (state) => state.orderInput - ); - // const { - // token1: { balance: balance1 }, - // token2: { balance: balance2 }, - // } = useAppSelector((state) => state.pairSelector); - const validationResult = useAppSelector(validatePositionSize); - const selectedToken = useAppSelector(getSelectedToken); - const unSelectedToken = useAppSelector(getUnselectedToken); - const dispatch = useAppDispatch(); - // const [customPercentage, setCustomPercentage] = useState(0); +interface TokenInputFiledProps extends TokenInput { + onFocus: () => void; + onChange: (event: React.ChangeEvent) => void; +} - const customPercentInputRef = useRef(null); +function nullableNumberInput(event: React.ChangeEvent) { + let amount: number | ""; + if (event.target.value === "") { + amount = ""; + } else { + amount = Number(event.target.value); + } + return amount; +} - const handleOnChange = (event: React.ChangeEvent) => { - if (customPercentInputRef.current) { - customPercentInputRef.current.value = ""; - } - const size = Number(event.target.value); - dispatch(orderInputSlice.actions.setSize(size)); - }; +function TokenInputFiled(props: TokenInputFiledProps) { + const { + symbol, + iconUrl, + valid, + message, + amount, + balance, + onChange, + onFocus, + } = props; + return ( +
+ {/* balance */} +
+ Current balance: + {balance || "unknown"} +
- // const handleOnPercentChange = ( - // event: React.ChangeEvent - // ) => { - // if (Number(event.target.value) > 100) return; - // const size = Number(event.target.value); - // setCustomPercentage(size); - // dispatch(setSizePercent(size)); - // }; + {/* input */} +
+ {symbol} + {symbol} + +
- const isBuy = useMemo(() => side === OrderSide.BUY, [side]); - const isSell = useMemo(() => side === OrderSide.SELL, [side]); + {/* error message */} + +
+ ); +} - const handleTokenSwitch = () => { - dispatch(orderInputSlice.actions.setToken1Selected(!token1Selected)); - }; +export function AmountInput() { + const { token1, token2, quote, description } = useAppSelector( + (state) => state.orderInput + ); + + const dispatch = useAppDispatch(); return ( -
-
-

- {side === OrderSide.BUY ? "Buy" : "Sell"} Amount: -

- {/* {isSell && ( -

- Balance: {balance1} -

- )} */} -
- -
- - -
-
    -
  • { - e.stopPropagation(); - handleTokenSwitch(); - }} - > - -
  • -
-
- //
- // - // {selectedToken.symbol} - //
- } +
+ { + dispatch(orderInputSlice.actions.setSide(OrderSide.SELL)); + }} + onChange={(event) => { + dispatch( + orderInputSlice.actions.setAmountToken1(nullableNumberInput(event)) + ); + }} /> - -
-

- {isBuy ? "You pay" : "You receive:"} -

- {/* {isBuy && ( -

- Balance: {balance2} -

- )} */} -
- - - - {unSelectedToken.symbol} - -
- } + { + dispatch(orderInputSlice.actions.swapTokens()); + }} + > + + + + { + dispatch(orderInputSlice.actions.setSide(OrderSide.BUY)); + }} + onChange={(event) => { + dispatch( + orderInputSlice.actions.setAmountToken2(nullableNumberInput(event)) + ); + }} /> -
@@ -172,6 +137,10 @@ export function AmountInput() { {quote?.liquidityFees} {quote?.toToken.symbol}
+
+
Description:
+
{description}
+
diff --git a/src/app/components/order_input/LimitOrderInput.tsx b/src/app/components/order_input/LimitOrderInput.tsx index 817b5518..b14a3e3f 100644 --- a/src/app/components/order_input/LimitOrderInput.tsx +++ b/src/app/components/order_input/LimitOrderInput.tsx @@ -26,39 +26,36 @@ export function LimitOrderInput() { return ( <>
-
-

- {side === OrderSide.BUY ? "Buy" : "Sell"} Price -

-

- dispatch( - orderInputSlice.actions.setPrice( - side === OrderSide.BUY - ? bestBuyPrice || 0 - : bestSellPrice || 0 - ) + +

+ {side === OrderSide.BUY ? "Buy" : "Sell"} Price +

+

+ dispatch( + orderInputSlice.actions.setPrice( + side === OrderSide.BUY ? bestBuyPrice || 0 : bestSellPrice || 0 ) + ) + } + > + Best Price:{" "} + - Best Price:{" "} - - {side === OrderSide.BUY ? bestBuyPrice : bestSellPrice}{" "} - {priceToken.symbol} - -

-
+ {side === OrderSide.BUY ? bestBuyPrice : bestSellPrice}{" "} + {priceToken.symbol} + +

} /> -
- ); } diff --git a/src/app/components/order_input/MarketOrderInput.tsx b/src/app/components/order_input/MarketOrderInput.tsx index 71546c25..7d5d9718 100644 --- a/src/app/components/order_input/MarketOrderInput.tsx +++ b/src/app/components/order_input/MarketOrderInput.tsx @@ -1,34 +1,42 @@ +import { useAppDispatch, useAppSelector } from "hooks"; import { AmountInput } from "./AmountInput"; +import { orderInputSlice, validateSlippageInput } from "redux/orderInputSlice"; +import { Input } from "common/input"; + +function uiSlippageToSlippage(slippage: number) { + return slippage / 100; +} + +function slippageToUiSlippage(slippage: number) { + return slippage * 100; +} export function MarketOrderInput() { - // const slippage = useAppSelector((state) => state.orderInput.slippage); - // const validationResult = useAppSelector(validateSlippageInput); - // const dispatch = useAppDispatch(); + const slippage = useAppSelector((state) => state.orderInput.slippage); + const validationResult = useAppSelector(validateSlippageInput); + const dispatch = useAppDispatch(); - // const handleOnChange = (event: React.ChangeEvent) => { - // const slippage = slippageFromPercentage(event.target.value); - // dispatch(orderInputSlice.actions.setSlippage(slippage)); - // }; return ( <> - {/*
- - %} - isError={!validationResult.valid} - /> - -
*/} - +
+ + + { + dispatch( + orderInputSlice.actions.setSlippage( + uiSlippageToSlippage(event.target.valueAsNumber) + ) + ); + }} + endAdornment={%} + validation={validationResult} + /> +
); } diff --git a/src/app/components/order_input/OrderInput.tsx b/src/app/components/order_input/OrderInput.tsx index 6bb51051..568a745c 100644 --- a/src/app/components/order_input/OrderInput.tsx +++ b/src/app/components/order_input/OrderInput.tsx @@ -1,13 +1,11 @@ import { useEffect } from "react"; -import { TokenAvatar } from "common/tokenAvatar"; import { useAppDispatch, useAppSelector } from "hooks"; import { - OrderSide, OrderTab, fetchQuote, - getSelectedToken, orderInputSlice, + selectTargetToken, submitOrder, validateOrderInput, } from "redux/orderInputSlice"; @@ -16,69 +14,8 @@ import { OrderTypeTabs } from "./OrderTypeTabs"; import { MarketOrderInput } from "./MarketOrderInput"; import { LimitOrderInput } from "./LimitOrderInput"; -function SingleGroupButton({ - isActive, - onClick, - avatarUrl, - text, - wrapperClass, -}: { - isActive: boolean; - onClick: () => void; - avatarUrl?: string; - text: string; - wrapperClass?: string; -}) { - return ( -
- {avatarUrl && } -

- {text} -

-
- ); -} - -function DirectionToggle() { - const activeSide = useAppSelector((state) => state.orderInput.side); - const dispatch = useAppDispatch(); - const isBuyActive = activeSide === OrderSide.BUY; - const isSellActive = activeSide === OrderSide.SELL; - return ( -
- { - dispatch(orderInputSlice.actions.setSide(OrderSide.BUY)); - }} - wrapperClass={ - "w-1/2 max-w-none border-none " + (isBuyActive ? "!bg-neutral" : "") - } - /> - { - dispatch(orderInputSlice.actions.setSide(OrderSide.SELL)); - }} - wrapperClass={ - "w-1/2 max-w-none border-none " + (isSellActive ? "!bg-neutral" : "") - } - /> -
- ); -} - function SubmitButton() { - const symbol = useAppSelector(getSelectedToken).symbol; + const symbol = useAppSelector(selectTargetToken).symbol; const tab = useAppSelector((state) => state.orderInput.tab); const side = useAppSelector((state) => state.orderInput.side); const transactionInProgress = useAppSelector( @@ -89,10 +26,7 @@ function SubmitButton() { ); const validationResult = useAppSelector(validateOrderInput); const dispatch = useAppDispatch(); - const submitString = - (tab === OrderTab.LIMIT ? "LIMIT " : "") + - (side === OrderSide.BUY ? "Buy " : "Sell ") + - symbol; + const submitString = tab.toString() + " " + side.toString() + " " + symbol; return (
@@ -125,14 +59,15 @@ function SubmitButton() { export function OrderInput() { const dispatch = useAppDispatch(); const { - token1Selected, + token1, + token2, side, - size, price, preventImmediateExecution, slippage, tab, } = useAppSelector((state) => state.orderInput); + const tartgetToken = useAppSelector(selectTargetToken); const pairAddress = useAppSelector((state) => state.pairSelector.address); const validationResult = useAppSelector(validateOrderInput); @@ -142,31 +77,29 @@ export function OrderInput() { }, [dispatch, pairAddress]); useEffect(() => { - if (validationResult.valid) { + if (validationResult.valid && tartgetToken.amount !== "") { dispatch(fetchQuote()); } }, [ dispatch, pairAddress, + token1, + token2, side, - size, price, slippage, tab, - token1Selected, preventImmediateExecution, validationResult, + tartgetToken, ]); return ( <> -
- -
- {tab === OrderTab.MARKET ? : } - -
+
+ {tab === OrderTab.MARKET ? : } +
); diff --git a/src/app/redux/orderInputSlice.ts b/src/app/redux/orderInputSlice.ts index 863de447..8cfc04b9 100644 --- a/src/app/redux/orderInputSlice.ts +++ b/src/app/redux/orderInputSlice.ts @@ -1,19 +1,21 @@ -import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import type { PayloadAction } from "@reduxjs/toolkit"; +import { + createAsyncThunk, + createSelector, + createSlice, +} from "@reduxjs/toolkit"; import * as adex from "alphadex-sdk-js"; -import { RootState } from "./store"; -import { createSelector } from "@reduxjs/toolkit"; -import { getRdt, RDT } from "../subscriptions"; -import { AMOUNT_MAX_DECIMALS, fetchBalances } from "./pairSelectorSlice"; import { SdkResult } from "alphadex-sdk-js/lib/models/sdk-result"; -import * as utils from "../utils"; +import { RDT, getRdt } from "../subscriptions"; +import { displayAmount } from "../utils"; import { fetchAccountHistory } from "./accountHistorySlice"; import { selectBestBuy, selectBestSell } from "./orderBookSlice"; -import { displayAmount } from "../utils"; +import { TokenInfo, fetchBalances } from "./pairSelectorSlice"; +import { RootState } from "./store"; export enum OrderTab { - MARKET, - LIMIT, + MARKET = "MARKET", + LIMIT = "LIMIT", } export const PLATFORM_BADGE_ID = 1; //TODO: Get this data from the platform badge @@ -23,19 +25,33 @@ export const OrderSide = adex.OrderSide; export type OrderSide = adex.OrderSide; export type Quote = adex.Quote; +export interface ValidationResult { + valid: boolean; + message: string; +} + +export interface TokenInput { + address: string; + symbol: string; + iconUrl: string; + valid: boolean; + message: string; + amount: number | ""; + balance?: number; +} + export interface OrderInputState { - token1Selected: boolean; + token1: TokenInput; + token2: TokenInput; tab: OrderTab; preventImmediateExecution: boolean; side: OrderSide; - size: number; price: number; slippage: number; quote?: Quote; description?: string; transactionInProgress: boolean; transactionResult?: string; - fromSize?: number; } function adexOrderType(state: OrderInputState): adex.OrderType { @@ -53,12 +69,21 @@ function adexOrderType(state: OrderInputState): adex.OrderType { throw new Error("Invalid order type"); } +const initialTokenInput = { + address: "", + symbol: "", + iconUrl: "", + amount: 0, + valid: true, + message: "", +}; + const initialState: OrderInputState = { - token1Selected: true, + token1: initialTokenInput, + token2: initialTokenInput, tab: OrderTab.MARKET, preventImmediateExecution: false, side: adex.OrderSide.BUY, - size: 0, price: 0, slippage: 0.01, transactionInProgress: false, @@ -82,111 +107,25 @@ export const fetchQuote = createAsyncThunk< } else { slippageToSend = state.orderInput.slippage; } - if (!state.orderInput.size) { - return; + const targetToken = selectTargetToken(state); + + if (!targetToken?.amount) { + throw new Error("No amount specified when fetching quote."); } + const response = await adex.getExchangeOrderQuote( state.pairSelector.address, adexOrderType(state.orderInput), state.orderInput.side, - getSelectedToken(state).address, - state.orderInput.size, + targetToken.address, + targetToken.amount, PLATFORM_FEE, priceToSend, slippageToSend ); const quote = JSON.parse(JSON.stringify(response.data)); - return { ...quote }; -}); -export const setSizePercent = createAsyncThunk< - undefined, - number, - { state: RootState } ->("orderInput/setSizePercent", async (percentage, thunkAPI) => { - //Depending on the combination of input settings, the position size - //is set to x% of the tokens that will leave the user wallet - const state = thunkAPI.getState(); - const dispatch = thunkAPI.dispatch; - const side = state.orderInput.side; - const proportion = percentage / 100; - if (proportion <= 0) { - dispatch(orderInputSlice.actions.setSize(0)); - return; - } - if (percentage > 100) { - percentage = Math.floor(percentage / 10); - dispatch(setSizePercent(percentage)); - return; - } - let balance; - // TODO: add tests for this - if (side === OrderSide.BUY) { - const unselectedBalance = utils.roundTo( - proportion * (getUnselectedToken(state).balance || 0), - AMOUNT_MAX_DECIMALS - 1, - utils.RoundType.DOWN - ); - if (state.orderInput.tab === OrderTab.MARKET) { - //Market buy needs to get a quote to work out what will be returned - const quote = await adex.getExchangeOrderQuote( - state.pairSelector.address, - adexOrderType(state.orderInput), - adex.OrderSide.SELL, - getUnselectedToken(state).address, - unselectedBalance, - PLATFORM_FEE, - -1, - state.orderInput.slippage - ); - balance = quote.data.toAmount; - if (quote.data.fromAmount < unselectedBalance) { - //TODO: Display this message properly - console.error( - "Insufficient liquidity to execute full market order. Increase slippage or reduce position" - ); - } - } else { - // for limit buy orders we can just calculate based on balance and price - if (selectToken1Selected(state)) { - balance = unselectedBalance / state.orderInput.price; - } else { - balance = unselectedBalance * state.orderInput.price; - } - } - } else { - //for sell orders the calculation is very simple - balance = getSelectedToken(state).balance || 0; - balance = balance * proportion; - //for market sell orders the order quote is retrieved to check liquidity. - if (state.orderInput.tab === OrderTab.MARKET) { - const quote = await adex.getExchangeOrderQuote( - state.pairSelector.address, - adexOrderType(state.orderInput), - adex.OrderSide.SELL, - getSelectedToken(state).address, - balance || 0, - PLATFORM_FEE, - -1, - state.orderInput.slippage - ); - if (quote.data.fromAmount < balance) { - balance = quote.data.fromAmount; - //TODO: Display this message properly - console.error( - "Insufficient liquidity to execute full market order. Increase slippage or reduce position" - ); - } - } - } - const newSize = utils.roundTo( - balance || 0, - adex.AMOUNT_MAX_DECIMALS, - utils.RoundType.DOWN - ); - dispatch(orderInputSlice.actions.setSize(newSize)); - - return undefined; + return { ...quote }; }); export const submitOrder = createAsyncThunk< @@ -210,32 +149,21 @@ export const submitOrder = createAsyncThunk< return result; }); -const selectToken1 = (state: RootState) => state.pairSelector.token1; -const selectToken2 = (state: RootState) => state.pairSelector.token2; -const selectToken1Selected = (state: RootState) => - state.orderInput.token1Selected; - -export const getSelectedToken = createSelector( - [selectToken1, selectToken2, selectToken1Selected], - (token1, token2, token1Selected) => { - if (token1Selected) { - return token1; - } else { - return token2; - } - } -); - -export const getUnselectedToken = createSelector( - [selectToken1, selectToken2, selectToken1Selected], - (token1, token2, token1Selected) => { - if (token1Selected) { - return token2; - } else { - return token1; - } +export const selectTargetToken = (state: RootState) => { + if (state.orderInput.side === OrderSide.SELL) { + return state.orderInput.token1; + } else { + return state.orderInput.token2; } -); +}; +const selectSlippage = (state: RootState) => state.orderInput.slippage; +const selectPrice = (state: RootState) => state.orderInput.price; +const selectSide = (state: RootState) => state.orderInput.side; +const selectPriceMaxDecimals = (state: RootState) => { + return state.pairSelector.priceMaxDecimals; +}; +const selectToken1 = (state: RootState) => state.orderInput.token1; +const selectToken2 = (state: RootState) => state.orderInput.token2; export const orderInputSlice = createSlice({ name: "orderInput", @@ -246,17 +174,84 @@ export const orderInputSlice = createSlice({ setActiveTab(state, action: PayloadAction) { state.tab = action.payload; }, - setToken1Selected(state, action: PayloadAction) { - state.token1Selected = action.payload; - setFromSize(state); + updateAdex(state, action: PayloadAction) { + const serializedState: adex.StaticState = JSON.parse( + JSON.stringify(action.payload) + ); + const adexToken1 = serializedState.currentPairInfo.token1; + const adexToken2 = serializedState.currentPairInfo.token2; + if (state.token1.address !== adexToken1.address) { + state.token1 = { + address: adexToken1.address, + symbol: adexToken1.symbol, + iconUrl: adexToken1.iconUrl, + amount: "", + valid: true, + message: "", + }; + } + if (state.token2.address !== adexToken2.address) { + state.token2 = { + address: adexToken2.address, + symbol: adexToken2.symbol, + iconUrl: adexToken2.iconUrl, + amount: "", + valid: true, + message: "", + }; + } + + // set up a valid default price + if (state.price === 0) { + state.price = + serializedState.currentPairOrderbook.buys?.[0]?.price || 0; + } }, - setSize(state, action: PayloadAction) { - state.size = action.payload; - setFromSize(state); + updateBalance( + state, + action: PayloadAction<{ balance: number; token: TokenInfo }> + ) { + const { token, balance } = action.payload; + if (token.address === state.token1.address) { + state.token1 = { ...state.token1, balance }; + } else if (token.address === state.token2.address) { + state.token2 = { ...state.token2, balance }; + } + }, + setAmountToken1(state, action: PayloadAction) { + const amount = action.payload; + let token1 = { + ...state.token1, + amount, + }; + token1 = validateAmountToken1(token1); + state.token1 = token1; + + if (amount === "") { + state.token2.amount = ""; + } + }, + setAmountToken2(state, action: PayloadAction) { + const amount = action.payload; + let token2 = { + ...state.token2, + amount, + }; + + token2 = validateAmount(token2); + state.token2 = token2; + + if (amount === "") { + state.token1.amount = ""; + } + }, + swapTokens(state) { + const temp = state.token1; + state.token1 = state.token2; + state.token2 = temp; }, setSide(state, action: PayloadAction) { state.side = action.payload; - setFromSize(state); }, setPrice(state, action: PayloadAction) { state.price = action.payload; @@ -286,12 +281,44 @@ export const orderInputSlice = createSlice({ } state.quote = quote; - state.fromSize = quote.fromAmount; state.description = toDescription(quote); + if (state.tab === OrderTab.MARKET) { + if (state.side === OrderSide.SELL) { + state.token2.amount = quote.toAmount; + } else { + state.token1.amount = quote.fromAmount; + } + + if (quote.message.startsWith("Not enough liquidity")) { + if (state.side === OrderSide.SELL) { + state.token1.amount = quote.fromAmount; + state.token1.message = quote.message; + } else { + state.token2.amount = quote.toAmount; + state.token2.message = quote.message; + } + } + } else { + // limit order + if (state.side === OrderSide.SELL) { + state.token2.amount = Number(state.token1.amount) * state.price; + } else { + state.token1.amount = Number(state.token2.amount) / state.price; + } + } } ); - builder.addCase(fetchQuote.rejected, (_state, action) => { + builder.addCase(fetchQuote.rejected, (state, action) => { + if (state.side === OrderSide.SELL) { + state.token2.amount = ""; + state.token2.valid = false; + state.token2.message = "Could not get quote"; + } else { + state.token1.amount = ""; + state.token1.valid = false; + state.token1.message = "Could not get quote"; + } console.error("fetchQuote rejected:", action.error.message); }); @@ -311,21 +338,6 @@ export const orderInputSlice = createSlice({ }, }); -function setFromSize(state: OrderInputState) { - //Sets the amount of token leaving the user wallet - if (state.side === OrderSide.SELL) { - state.fromSize = state.size; - } else if (state.tab === OrderTab.LIMIT) { - if (state.token1Selected) { - state.fromSize = state.size * state.price; - } else { - state.fromSize = state.size / state.price; - } - } else { - state.fromSize = 0; //will update when quote is requested - } -} - function toDescription(quote: Quote): string { let description = ""; @@ -351,12 +363,17 @@ async function createTx(state: RootState, rdt: RDT) { const orderPrice = tab === OrderTab.LIMIT ? price : -1; const orderSlippage = tab === OrderTab.MARKET ? slippage : -1; //Adex creates the transaction manifest + const targetToken = selectTargetToken(state); + + if (!targetToken?.amount) { + throw new Error("No amount specified when creating transaction."); + } const createOrderResponse = await adex.createExchangeOrderTx( state.pairSelector.address, adexOrderType(state.orderInput), state.orderInput.side, - getSelectedToken(state).address, - state.orderInput.size, + targetToken.address, + targetToken.amount, orderPrice, orderSlippage, PLATFORM_BADGE_ID, @@ -376,20 +393,6 @@ async function createTx(state: RootState, rdt: RDT) { return submitTransactionResponse; } -export interface ValidationResult { - valid: boolean; - message: string; -} - -const selectSlippage = (state: RootState) => state.orderInput.slippage; -const selectPrice = (state: RootState) => state.orderInput.price; -const selectSize = (state: RootState) => state.orderInput.size; -const selectSide = (state: RootState) => state.orderInput.side; -const selectFromSize = (state: RootState) => state.orderInput.fromSize; -const selectPriceMaxDecimals = (state: RootState) => { - return state.pairSelector.priceMaxDecimals; -}; - export const validateSlippageInput = createSelector( [selectSlippage], (slippage) => { @@ -412,9 +415,8 @@ export const validatePriceInput = createSelector( selectBestBuy, selectBestSell, selectSide, - selectToken1Selected, ], - (price, priceMaxDecimals, bestBuy, bestSell, side, token1Selected) => { + (price, priceMaxDecimals, bestBuy, bestSell, side) => { if (price <= 0) { return { valid: false, message: "Price must be greater than 0" }; } @@ -423,97 +425,68 @@ export const validatePriceInput = createSelector( return { valid: false, message: "Too many decimal places" }; } - if ( - ((side === OrderSide.BUY && token1Selected) || - (side === OrderSide.SELL && !token1Selected)) && - bestSell - ? price > bestSell * 1.05 - : false - ) { - return { - valid: true, - message: "Price is significantly higher than best sell", - }; + if (bestSell) { + if (side === OrderSide.BUY && price > bestSell * 1.05) { + return { + valid: true, + message: "Price is significantly higher than best sell", + }; + } } - if ( - ((side === OrderSide.SELL && token1Selected) || - (side === OrderSide.BUY && !token1Selected)) && - bestBuy - ? price < bestBuy * 0.95 - : false - ) { - return { - valid: true, - message: "Price is significantly lower than best buy", - }; + if (bestBuy) { + if (side === OrderSide.SELL && price < bestBuy * 0.95) { + return { + valid: true, + message: "Price is significantly lower than best buy", + }; + } } return { valid: true, message: "" }; } ); -export const validatePositionSize = createSelector( - [ - selectSide, - selectSize, - getSelectedToken, - getUnselectedToken, - selectFromSize, - ], - (side, size, selectedToken, unSelectedToken, fromSize) => { - if (size.toString().split(".")[1]?.length > adex.AMOUNT_MAX_DECIMALS) { - return { valid: false, message: "Too many decimal places" }; - } +function validateAmount(token: TokenInput): TokenInput { + const amount = token.amount; + let valid = true; + let message = ""; + if (amount.toString().split(".")[1]?.length > adex.AMOUNT_MAX_DECIMALS) { + valid = false; + message = "Too many decimal places"; + } - if (size <= 0) { - return { valid: false, message: "Order size must be greater than 0" }; - } - if ( - (side === OrderSide.SELL && - selectedToken.balance && - size > selectedToken.balance) || - (side === OrderSide.BUY && - unSelectedToken.balance && - fromSize && - fromSize > unSelectedToken.balance) - ) { - return { valid: false, message: "Insufficient funds" }; - } + if (amount !== "" && amount <= 0) { + valid = false; + message = "Amount must be greater than 0"; + } - //Checks user isn't using all their xrd. maybe excessive - const MIN_XRD_BALANCE = 25; - if ( - (side === OrderSide.SELL && - selectedToken.symbol === "XRD" && - selectedToken.balance && - size > selectedToken.balance - MIN_XRD_BALANCE) || - (side === OrderSide.BUY && - unSelectedToken.balance && - unSelectedToken.symbol === "XRD" && - fromSize && - fromSize > unSelectedToken.balance - MIN_XRD_BALANCE) - ) { - return { - valid: true, - message: "WARNING: Leaves XRD balance dangerously low", - }; - } + return { ...token, valid, message }; +} - return { valid: true, message: "" }; +function validateAmountToken1(token1: TokenInput): TokenInput { + if ((token1.balance || 0) < (token1.amount || 0)) { + return { ...token1, valid: false, message: "Insufficient funds" }; + } else { + return validateAmount(token1); } -); +} const selectTab = (state: RootState) => state.orderInput.tab; export const validateOrderInput = createSelector( - [validatePositionSize, validatePriceInput, validateSlippageInput, selectTab], - ( - sizeValidationResult, - priceValidationResult, - slippageValidationResult, - tab - ) => { - if (!sizeValidationResult.valid) { - return sizeValidationResult; + [ + selectToken1, + selectToken2, + validatePriceInput, + validateSlippageInput, + selectTab, + ], + (token1, token2, priceValidationResult, slippageValidationResult, tab) => { + if (!token1.valid || token1.amount === undefined) { + return { valid: false, message: token1.message }; + } + + if (!token2.valid || token2.amount === undefined) { + return { valid: false, message: token2.message }; } if (tab === OrderTab.LIMIT && !priceValidationResult.valid) { diff --git a/src/app/redux/pairSelectorSlice.ts b/src/app/redux/pairSelectorSlice.ts index 12455b17..eb0d9364 100644 --- a/src/app/redux/pairSelectorSlice.ts +++ b/src/app/redux/pairSelectorSlice.ts @@ -2,6 +2,7 @@ import * as adex from "alphadex-sdk-js"; import { PayloadAction, createSlice, createAsyncThunk } from "@reduxjs/toolkit"; import { RootState } from "./store"; import { getRdt } from "../subscriptions"; +import { orderInputSlice } from "./orderInputSlice"; export const AMOUNT_MAX_DECIMALS = adex.AMOUNT_MAX_DECIMALS; @@ -40,7 +41,7 @@ export const fetchBalances = createAsyncThunk< { state: RootState; } ->("pairSelector/fetchToken1Balance", async (_arg, thunkAPI) => { +>("pairSelector/fetchBalances", async (_arg, thunkAPI) => { const dispatch = thunkAPI.dispatch; const state = thunkAPI.getState(); @@ -68,8 +69,11 @@ export const fetchBalances = createAsyncThunk< // if there are no items in response, set the balance to 0 const balance = parseFloat(response?.items[0]?.amount || "0"); dispatch(pairSelectorSlice.actions.setBalance({ balance, token })); + // TODO: store balances in one place only? + dispatch(orderInputSlice.actions.updateBalance({ balance, token })); } catch (error) { dispatch(pairSelectorSlice.actions.setBalance({ balance: 0, token })); + dispatch(orderInputSlice.actions.updateBalance({ balance: 0, token })); throw new Error("Error getting data from Radix gateway"); } } diff --git a/src/app/subscriptions.ts b/src/app/subscriptions.ts index 06e182fe..56e9e5ba 100644 --- a/src/app/subscriptions.ts +++ b/src/app/subscriptions.ts @@ -12,6 +12,7 @@ import { orderBookSlice } from "./redux/orderBookSlice"; import { updateCandles } from "./redux/priceChartSlice"; import { updatePriceInfo } from "./redux/priceInfoSlice"; import { accountHistorySlice } from "./redux/accountHistorySlice"; +import { orderInputSlice } from "redux/orderInputSlice"; import { AppStore } from "./redux/store"; export type RDT = ReturnType; @@ -59,6 +60,7 @@ export function initializeSubscriptions(store: AppStore) { store.dispatch(updateCandles(serializedState.currentPairCandlesList)); store.dispatch(updatePriceInfo(serializedState)); store.dispatch(accountHistorySlice.actions.updateAdex(serializedState)); + store.dispatch(orderInputSlice.actions.updateAdex(serializedState)); }) ); } From a0ca97594b02db0e07afb56c841ce27a29b718c1 Mon Sep 17 00:00:00 2001 From: Evgeniia Vakarina <27793901+EvgeniiaVak@users.noreply.github.com> Date: Mon, 9 Oct 2023 22:02:51 +0400 Subject: [PATCH 18/39] style updates to be closer to figma --- .../components/order_input/AmountInput.tsx | 27 ++++++++++---- .../order_input/LimitOrderInput.tsx | 5 +-- src/app/components/order_input/OrderInput.tsx | 36 +++++++------------ .../components/order_input/OrderTypeTabs.tsx | 4 +-- src/app/redux/orderInputSlice.ts | 26 ++++++++++---- 5 files changed, 57 insertions(+), 41 deletions(-) diff --git a/src/app/components/order_input/AmountInput.tsx b/src/app/components/order_input/AmountInput.tsx index 962eb70a..bacba43a 100644 --- a/src/app/components/order_input/AmountInput.tsx +++ b/src/app/components/order_input/AmountInput.tsx @@ -1,7 +1,12 @@ import React from "react"; import { useAppDispatch, useAppSelector } from "hooks"; -import { OrderSide, TokenInput, orderInputSlice } from "redux/orderInputSlice"; +import { + OrderSide, + TokenInput, + orderInputSlice, + selectTargetToken, +} from "redux/orderInputSlice"; interface TokenInputFiledProps extends TokenInput { onFocus: () => void; @@ -19,7 +24,9 @@ function nullableNumberInput(event: React.ChangeEvent) { } function TokenInputFiled(props: TokenInputFiledProps) { + const targetToken = useAppSelector(selectTargetToken); const { + address, symbol, iconUrl, valid, @@ -30,7 +37,7 @@ function TokenInputFiled(props: TokenInputFiledProps) { onFocus, } = props; return ( -
+
{/* balance */}
Current balance: @@ -40,12 +47,20 @@ function TokenInputFiled(props: TokenInputFiledProps) { {/* input */}
- {symbol} - {symbol} +
+ {symbol} + {symbol} +
+ { dispatch(orderInputSlice.actions.swapTokens()); diff --git a/src/app/components/order_input/LimitOrderInput.tsx b/src/app/components/order_input/LimitOrderInput.tsx index b14a3e3f..88dd1eb0 100644 --- a/src/app/components/order_input/LimitOrderInput.tsx +++ b/src/app/components/order_input/LimitOrderInput.tsx @@ -27,9 +27,6 @@ export function LimitOrderInput() { <>
-

- {side === OrderSide.BUY ? "Buy" : "Sell"} Price -

@@ -40,7 +37,7 @@ export function LimitOrderInput() { ) } > - Best Price:{" "} + Best {side} Price:{" "} state.orderInput.transactionResult ); - const validationResult = useAppSelector(validateOrderInput); + const transactionValidation = useAppSelector(isValidTransaction); const dispatch = useAppDispatch(); const submitString = tab.toString() + " " + side.toString() + " " + symbol; @@ -35,18 +36,14 @@ function SubmitButton() { - dispatch( - orderInputSlice.actions.togglePreventImmediateExecution() - ) - } + onClick={() => dispatch(orderInputSlice.actions.togglePostOnly())} /> - Prevent immediate execution + Post only

)} + +
+ ); +} diff --git a/src/app/components/order_input/OrderTypeTabs.tsx b/src/app/components/order_input/OrderTypeTabs.tsx index 6f426e93..f53d2a6b 100644 --- a/src/app/components/order_input/OrderTypeTabs.tsx +++ b/src/app/components/order_input/OrderTypeTabs.tsx @@ -8,8 +8,8 @@ export function OrderTypeTabs() { function tabClass(isActive: boolean) { return ( - "flex-1 tab no-underline h-full text-base font-semibold py-3 mx-4 tab-border-2" + - (isActive ? " tab-active tab-bordered" : "") + "flex-1 tab no-underline h-full py-3 mx-8" + + (isActive ? " tab-active tab-bordered !border-accent text-accent" : "") ); } return ( diff --git a/src/app/redux/orderInputSlice.ts b/src/app/redux/orderInputSlice.ts index 2ad0e180..b51d1e70 100644 --- a/src/app/redux/orderInputSlice.ts +++ b/src/app/redux/orderInputSlice.ts @@ -280,6 +280,12 @@ export const orderInputSlice = createSlice({ throw new Error("Invalid quote"); } + // clear any previous message if it was set + // cannot do in "pending" because it causes infinite loop + // for getting the quote + // TODO: do not call fetchQuote when message is changed + state.token1.message = ""; + state.quote = quote; state.description = toDescription(quote); if (state.tab === OrderTab.MARKET) { @@ -310,6 +316,11 @@ export const orderInputSlice = createSlice({ ); builder.addCase(fetchQuote.rejected, (state, action) => { + // clear any previous message if it was set + // cannot do in "pending" because it causes infinite loop + // for getting the quote + // TODO: do not call fetchQuote when message is changed + state.token1.message = ""; if (state.side === OrderSide.SELL) { state.token2.amount = ""; state.token2.valid = false; From b6ed728ff0969f051b8cf7a4b7ae43e497feae88 Mon Sep 17 00:00:00 2001 From: Evgeniia Vakarina <27793901+EvgeniiaVak@users.noreply.github.com> Date: Wed, 11 Oct 2023 13:51:54 +0400 Subject: [PATCH 21/39] fixes balances being wiped out with updates --- .../components/order_input/AmountInput.tsx | 31 +++++++--- src/app/redux/orderInputSlice.ts | 58 ++++++++++++------- src/app/redux/pairSelectorSlice.ts | 4 -- 3 files changed, 60 insertions(+), 33 deletions(-) diff --git a/src/app/components/order_input/AmountInput.tsx b/src/app/components/order_input/AmountInput.tsx index 954197b6..05275271 100644 --- a/src/app/components/order_input/AmountInput.tsx +++ b/src/app/components/order_input/AmountInput.tsx @@ -7,6 +7,7 @@ import { TokenInput, orderInputSlice, selectTargetToken, + selectBalanceByAddress, } from "redux/orderInputSlice"; interface TokenInputFiledProps extends TokenInput { @@ -28,6 +29,9 @@ function nullableNumberInput(event: React.ChangeEvent) { function TokenInputFiled(props: TokenInputFiledProps) { const orderTab = useAppSelector((state) => state.orderInput.tab); const targetToken = useAppSelector(selectTargetToken); + const balance = useAppSelector((state) => + selectBalanceByAddress(state, props.address) + ); const { address, symbol, @@ -35,16 +39,15 @@ function TokenInputFiled(props: TokenInputFiledProps) { valid, message, amount, - balance, disabled, - onChange, onFocus, + onChange, } = props; return (
{/* balance */}
- Current balance: + BALANCE: {balance || "unknown"}
@@ -114,6 +117,9 @@ function SwitchTokenPlacesButton() { export function SwapAmountInput() { const { token1, token2 } = useAppSelector((state) => state.orderInput); + const balanceToken1 = useAppSelector((state) => + selectBalanceByAddress(state, token1.address) + ); const dispatch = useAppDispatch(); @@ -125,9 +131,11 @@ export function SwapAmountInput() { dispatch(orderInputSlice.actions.setSide(OrderSide.SELL)); }} onChange={(event) => { - dispatch( - orderInputSlice.actions.setAmountToken1(nullableNumberInput(event)) - ); + const params = { + amount: nullableNumberInput(event), + balance: balanceToken1 || 0, + }; + dispatch(orderInputSlice.actions.setAmountToken1(params)); }} /> @@ -150,6 +158,9 @@ export function SwapAmountInput() { export function LimitAmountInput() { const { token1, token2 } = useAppSelector((state) => state.orderInput); const { side } = useAppSelector((state) => state.orderInput); + const balanceToken1 = useAppSelector((state) => + selectBalanceByAddress(state, token1.address) + ); const dispatch = useAppDispatch(); @@ -159,9 +170,11 @@ export function LimitAmountInput() { disabled={side === OrderSide.BUY} {...token1} onChange={(event) => { - dispatch( - orderInputSlice.actions.setAmountToken1(nullableNumberInput(event)) - ); + const params = { + amount: nullableNumberInput(event), + balance: balanceToken1 || 0, + }; + dispatch(orderInputSlice.actions.setAmountToken1(params)); }} /> diff --git a/src/app/redux/orderInputSlice.ts b/src/app/redux/orderInputSlice.ts index b51d1e70..8484b64c 100644 --- a/src/app/redux/orderInputSlice.ts +++ b/src/app/redux/orderInputSlice.ts @@ -10,7 +10,7 @@ import { RDT, getRdt } from "../subscriptions"; import { displayAmount } from "../utils"; import { fetchAccountHistory } from "./accountHistorySlice"; import { selectBestBuy, selectBestSell } from "./orderBookSlice"; -import { TokenInfo, fetchBalances } from "./pairSelectorSlice"; +import { fetchBalances } from "./pairSelectorSlice"; import { RootState } from "./store"; export enum OrderTab { @@ -37,7 +37,6 @@ export interface TokenInput { valid: boolean; message: string; amount: number | ""; - balance?: number; } export interface OrderInputState { @@ -165,6 +164,27 @@ const selectPriceMaxDecimals = (state: RootState) => { const selectToken1 = (state: RootState) => state.orderInput.token1; const selectToken2 = (state: RootState) => state.orderInput.token2; +// for getting balances out of pairSelector slice +const selectInfoToken1 = (state: RootState) => state.pairSelector.token1; +const selectInfoToken2 = (state: RootState) => state.pairSelector.token2; +export const selectBalanceByAddress = createSelector( + [ + selectInfoToken1, + selectInfoToken2, + (state: RootState, address: string) => address, + ], + + (infoToken1, infoToken2, address) => { + if (infoToken1.address === address) { + return infoToken1.balance; + } else if (infoToken2.address === address) { + return infoToken2.balance; + } else { + return 0; + } + } +); + export const orderInputSlice = createSlice({ name: "orderInput", initialState, @@ -207,25 +227,17 @@ export const orderInputSlice = createSlice({ serializedState.currentPairOrderbook.buys?.[0]?.price || 0; } }, - updateBalance( + setAmountToken1( state, - action: PayloadAction<{ balance: number; token: TokenInfo }> + action: PayloadAction<{ amount: number | ""; balance: number }> ) { - const { token, balance } = action.payload; - if (token.address === state.token1.address) { - state.token1 = { ...state.token1, balance }; - } else if (token.address === state.token2.address) { - state.token2 = { ...state.token2, balance }; - } - }, - setAmountToken1(state, action: PayloadAction) { - const amount = action.payload; - let token1 = { + const { amount, balance } = action.payload; + let token = { ...state.token1, amount, }; - token1 = validateAmountToken1(token1); - state.token1 = token1; + token = validateAmountWithBalance({ token, balance }); + state.token1 = token; if (amount === "") { state.token2.amount = ""; @@ -474,11 +486,17 @@ function validateAmount(token: TokenInput): TokenInput { return { ...token, valid, message }; } -function validateAmountToken1(token1: TokenInput): TokenInput { - if ((token1.balance || 0) < (token1.amount || 0)) { - return { ...token1, valid: false, message: "Insufficient funds" }; +function validateAmountWithBalance({ + token, + balance, +}: { + token: TokenInput; + balance: number; +}): TokenInput { + if ((balance || 0) < (token.amount || 0)) { + return { ...token, valid: false, message: "Insufficient funds" }; } else { - return validateAmount(token1); + return validateAmount(token); } } diff --git a/src/app/redux/pairSelectorSlice.ts b/src/app/redux/pairSelectorSlice.ts index eb0d9364..8bdbbb17 100644 --- a/src/app/redux/pairSelectorSlice.ts +++ b/src/app/redux/pairSelectorSlice.ts @@ -2,7 +2,6 @@ import * as adex from "alphadex-sdk-js"; import { PayloadAction, createSlice, createAsyncThunk } from "@reduxjs/toolkit"; import { RootState } from "./store"; import { getRdt } from "../subscriptions"; -import { orderInputSlice } from "./orderInputSlice"; export const AMOUNT_MAX_DECIMALS = adex.AMOUNT_MAX_DECIMALS; @@ -69,11 +68,8 @@ export const fetchBalances = createAsyncThunk< // if there are no items in response, set the balance to 0 const balance = parseFloat(response?.items[0]?.amount || "0"); dispatch(pairSelectorSlice.actions.setBalance({ balance, token })); - // TODO: store balances in one place only? - dispatch(orderInputSlice.actions.updateBalance({ balance, token })); } catch (error) { dispatch(pairSelectorSlice.actions.setBalance({ balance: 0, token })); - dispatch(orderInputSlice.actions.updateBalance({ balance: 0, token })); throw new Error("Error getting data from Radix gateway"); } } From 8223903f62ea73bcca9ae9a7bebe37b08625f367 Mon Sep 17 00:00:00 2001 From: Evgeniia Vakarina <27793901+EvgeniiaVak@users.noreply.github.com> Date: Wed, 11 Oct 2023 19:23:52 +0400 Subject: [PATCH 22/39] fixes undefined error for order history --- src/app/redux/accountHistorySlice.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/redux/accountHistorySlice.ts b/src/app/redux/accountHistorySlice.ts index dedbfe35..1fdc35e9 100644 --- a/src/app/redux/accountHistorySlice.ts +++ b/src/app/redux/accountHistorySlice.ts @@ -45,8 +45,12 @@ export const fetchAccountHistory = createAsyncThunk< } const apiResponse = await adex.getAccountOrders(account, pairAddress, 0); - const plainApiResponse = JSON.parse(JSON.stringify(apiResponse)); //TODO -> need to find a better way to handle serialization - return plainApiResponse; + const plainApiResponse: SdkResult = JSON.parse(JSON.stringify(apiResponse)); + if (plainApiResponse.status === "SUCCESS") { + return plainApiResponse; + } else { + throw new Error(plainApiResponse.message); + } }); export const cancelOrder = createAsyncThunk< From 806e74281d783f55e51a8860148de869cee7e510 Mon Sep 17 00:00:00 2001 From: Evgeniia Vakarina <27793901+EvgeniiaVak@users.noreply.github.com> Date: Wed, 11 Oct 2023 21:58:16 +0400 Subject: [PATCH 23/39] fixes order input styling --- .../components/order_input/AmountInput.tsx | 125 ++-------- .../order_input/LimitOrderInput.tsx | 235 ++++++++++++++---- .../order_input/MarketOrderInput.tsx | 127 ++++++++-- src/app/components/order_input/OrderInput.tsx | 51 +--- src/app/redux/orderInputSlice.ts | 43 ++-- src/app/utils.ts | 10 + 6 files changed, 357 insertions(+), 234 deletions(-) diff --git a/src/app/components/order_input/AmountInput.tsx b/src/app/components/order_input/AmountInput.tsx index 05275271..44db34bf 100644 --- a/src/app/components/order_input/AmountInput.tsx +++ b/src/app/components/order_input/AmountInput.tsx @@ -2,32 +2,27 @@ import React from "react"; import { useAppDispatch, useAppSelector } from "hooks"; import { - OrderSide, OrderTab, TokenInput, orderInputSlice, - selectTargetToken, selectBalanceByAddress, + selectTargetToken, } from "redux/orderInputSlice"; +export const enum PayReceive { + PAY = "YOU PAY:", + RECEIVE = "YOU RECEIVE:", +} + interface TokenInputFiledProps extends TokenInput { + payReceive: string; disabled?: boolean; onFocus?: () => void; onChange: (event: React.ChangeEvent) => void; } -function nullableNumberInput(event: React.ChangeEvent) { - let amount: number | ""; - if (event.target.value === "") { - amount = ""; - } else { - amount = Number(event.target.value); - } - return amount; -} - -function TokenInputFiled(props: TokenInputFiledProps) { - const orderTab = useAppSelector((state) => state.orderInput.tab); +export function AmountInput(props: TokenInputFiledProps) { + const tab = useAppSelector((state) => state.orderInput.tab); const targetToken = useAppSelector(selectTargetToken); const balance = useAppSelector((state) => selectBalanceByAddress(state, props.address) @@ -40,24 +35,27 @@ function TokenInputFiled(props: TokenInputFiledProps) { message, amount, disabled, + payReceive, onFocus, onChange, } = props; + return (
{/* balance */}
- BALANCE: - {balance || "unknown"} +
+ BALANCE: + {balance || "unknown"} +
+ {payReceive}
{/* input */}
{/* error message */} -
); } -function SwitchTokenPlacesButton() { +export function SwitchTokenPlacesButton() { const dispatch = useAppDispatch(); return ( ); } - -export function SwapAmountInput() { - const { token1, token2 } = useAppSelector((state) => state.orderInput); - const balanceToken1 = useAppSelector((state) => - selectBalanceByAddress(state, token1.address) - ); - - const dispatch = useAppDispatch(); - - return ( -
- { - dispatch(orderInputSlice.actions.setSide(OrderSide.SELL)); - }} - onChange={(event) => { - const params = { - amount: nullableNumberInput(event), - balance: balanceToken1 || 0, - }; - dispatch(orderInputSlice.actions.setAmountToken1(params)); - }} - /> - - - { - dispatch(orderInputSlice.actions.setSide(OrderSide.BUY)); - }} - onChange={(event) => { - dispatch( - orderInputSlice.actions.setAmountToken2(nullableNumberInput(event)) - ); - }} - /> -
- ); -} - -export function LimitAmountInput() { - const { token1, token2 } = useAppSelector((state) => state.orderInput); - const { side } = useAppSelector((state) => state.orderInput); - const balanceToken1 = useAppSelector((state) => - selectBalanceByAddress(state, token1.address) - ); - - const dispatch = useAppDispatch(); - - return ( -
- { - const params = { - amount: nullableNumberInput(event), - balance: balanceToken1 || 0, - }; - dispatch(orderInputSlice.actions.setAmountToken1(params)); - }} - /> - - - - { - dispatch( - orderInputSlice.actions.setAmountToken2(nullableNumberInput(event)) - ); - }} - /> -
- ); -} diff --git a/src/app/components/order_input/LimitOrderInput.tsx b/src/app/components/order_input/LimitOrderInput.tsx index 2993d347..57959637 100644 --- a/src/app/components/order_input/LimitOrderInput.tsx +++ b/src/app/components/order_input/LimitOrderInput.tsx @@ -1,68 +1,205 @@ -import React from "react"; +import React, { useEffect } from "react"; -import { Input } from "common/input"; -import { TokenWithSymbol } from "common/tokenWithSymbol"; import { useAppDispatch, useAppSelector } from "hooks"; import { OrderSide, + fetchQuote, + isValidQuoteInput, orderInputSlice, - validatePriceInput, + selectBalanceByAddress, } from "redux/orderInputSlice"; -import { LimitAmountInput } from "./AmountInput"; +import { + AmountInput, + PayReceive, + SwitchTokenPlacesButton, +} from "./AmountInput"; +import { numberOrEmptyInput } from "utils"; + +function NonTargetToken() { + const { token2, side } = useAppSelector((state) => state.orderInput); + const balance = useAppSelector((state) => + selectBalanceByAddress(state, token2.address) + ); + const { symbol, iconUrl, valid, message, amount } = token2; + + return ( +
+ {/* balance */} +
+
+ BALANCE: + {balance || "unknown"} +
+ + {side === OrderSide.BUY ? PayReceive.PAY : PayReceive.RECEIVE} + +
+ + {/* input */} +
+
+ {symbol} + {symbol} +
+ + {amount} +
+ + {/* error message */} + +
+ ); +} + +function PriceInput() { + const { token1: pairToken1, token2: pairToken2 } = useAppSelector( + (state) => state.pairSelector + ); + const { side, price } = useAppSelector((state) => state.orderInput); + const dispatch = useAppDispatch(); + return ( + <> +
+ {side === OrderSide.BUY ? "AT MAX PRICE:" : "AT MIN PRICE:"} +
+
+
+ 1 + {pairToken1.symbol} + = +
+
+ ) => { + const price = Number(event.target.value); + + dispatch(orderInputSlice.actions.setPrice(price)); + }} + /> + {pairToken2.symbol} +
+
+ + ); +} export function LimitOrderInput() { - const { price, side } = useAppSelector((state) => state.orderInput); - const priceToken = useAppSelector((state) => state.pairSelector.token2); - const validationResult = useAppSelector(validatePriceInput); + const { token1, price, side, tab, postOnly } = useAppSelector( + (state) => state.orderInput + ); + + const { address: pairAddress } = useAppSelector( + (state) => state.pairSelector + ); + + const balanceToken1 = useAppSelector((state) => + selectBalanceByAddress(state, token1.address) + ); + + const dispatch = useAppDispatch(); + const bestBuyPrice = useAppSelector((state) => state.orderBook.bestBuy); const bestSellPrice = useAppSelector((state) => state.orderBook.bestSell); - const dispatch = useAppDispatch(); - const handleOnChange = (event: React.ChangeEvent) => { - const price = Number(event.target.value); - dispatch(orderInputSlice.actions.setPrice(price)); - }; + const quoteValidation = useAppSelector(isValidQuoteInput); + + useEffect(() => { + if (quoteValidation.valid && token1.amount !== "") { + dispatch(fetchQuote()); + } + }, [ + dispatch, + pairAddress, + token1, + side, + price, + tab, + postOnly, + quoteValidation, + ]); return ( <>
- - {/*

- dispatch( - orderInputSlice.actions.setPrice( - side === OrderSide.BUY ? bestBuyPrice || 0 : bestSellPrice || 0 - ) - ) - } - > - Best {side} Price:{" "} - + - {side === OrderSide.BUY ? bestBuyPrice : bestSellPrice}{" "} - {priceToken.symbol} - -

*/} - {/* */} -
At price rate:
- + onChange={(event) => { + const params = { + amount: numberOrEmptyInput(event), + balance: balanceToken1 || 0, + }; + dispatch(orderInputSlice.actions.setAmountToken1(params)); + }} + /> + + + + +
+ + + +
+
+
+ BEST BUY  ={" "} + + dispatch(orderInputSlice.actions.setPrice(bestBuyPrice || 0)) + } + > + {bestBuyPrice} + +
+
+ BEST SELL ={" "} + + dispatch(orderInputSlice.actions.setPrice(bestSellPrice || 0)) + } + > + {bestSellPrice} + +
+
+ +
+ dispatch(orderInputSlice.actions.togglePostOnly())} + /> + POST ONLY +
+
); diff --git a/src/app/components/order_input/MarketOrderInput.tsx b/src/app/components/order_input/MarketOrderInput.tsx index 4cccc4ac..476cf9c4 100644 --- a/src/app/components/order_input/MarketOrderInput.tsx +++ b/src/app/components/order_input/MarketOrderInput.tsx @@ -1,7 +1,20 @@ import { useAppDispatch, useAppSelector } from "hooks"; -import { SwapAmountInput } from "./AmountInput"; -import { orderInputSlice, validateSlippageInput } from "redux/orderInputSlice"; -import { Input } from "common/input"; +import { + AmountInput, + PayReceive, + SwitchTokenPlacesButton, +} from "./AmountInput"; +import { + OrderSide, + fetchQuote, + isValidQuoteInput, + orderInputSlice, + selectBalanceByAddress, + selectTargetToken, + validateSlippageInput, +} from "redux/orderInputSlice"; +import { useEffect } from "react"; +import { numberOrEmptyInput } from "utils"; function uiSlippageToSlippage(slippage: number) { return slippage / 100; @@ -12,31 +25,105 @@ function slippageToUiSlippage(slippage: number) { } export function MarketOrderInput() { - const slippage = useAppSelector((state) => state.orderInput.slippage); - const validationResult = useAppSelector(validateSlippageInput); + const { token1, token2, side, slippage, tab } = useAppSelector( + (state) => state.orderInput + ); + const balanceToken1 = useAppSelector((state) => + selectBalanceByAddress(state, token1.address) + ); + const tartgetToken = useAppSelector(selectTargetToken); + const pairAddress = useAppSelector((state) => state.pairSelector.address); + + const quoteValidation = useAppSelector(isValidQuoteInput); + const slippageValidationResult = useAppSelector(validateSlippageInput); const dispatch = useAppDispatch(); + useEffect(() => { + if (quoteValidation.valid && tartgetToken.amount !== "") { + dispatch(fetchQuote()); + } + }, [ + dispatch, + pairAddress, + token1, + token2, + side, + slippage, + tab, + quoteValidation, + tartgetToken, + slippage, + ]); + return ( - <> -
- - - +
+ { + dispatch(orderInputSlice.actions.setSide(OrderSide.SELL)); + }} + onChange={(event) => { + const params = { + amount: numberOrEmptyInput(event), + balance: balanceToken1 || 0, + }; + dispatch(orderInputSlice.actions.setAmountToken1(params)); + }} + /> + + + { + dispatch(orderInputSlice.actions.setSide(OrderSide.BUY)); + }} onChange={(event) => { dispatch( - orderInputSlice.actions.setSlippage( - uiSlippageToSlippage(event.target.valueAsNumber) - ) + orderInputSlice.actions.setAmountToken2(numberOrEmptyInput(event)) ); }} - endAdornment={%} - validation={validationResult} />
- + + {/* slippage */} +
+
+
+ +
+ { + dispatch( + orderInputSlice.actions.setSlippage( + uiSlippageToSlippage(event.target.valueAsNumber) + ) + ); + }} + /> + % +
+
+
+ +
); } diff --git a/src/app/components/order_input/OrderInput.tsx b/src/app/components/order_input/OrderInput.tsx index 6b7f133f..8aab392a 100644 --- a/src/app/components/order_input/OrderInput.tsx +++ b/src/app/components/order_input/OrderInput.tsx @@ -4,17 +4,16 @@ import { useAppDispatch, useAppSelector } from "hooks"; import { OrderTab, fetchQuote, - orderInputSlice, - selectTargetToken, - submitOrder, isValidQuoteInput, isValidTransaction, + selectTargetToken, + submitOrder, } from "redux/orderInputSlice"; import { fetchBalances } from "redux/pairSelectorSlice"; -import { OrderTypeTabs } from "./OrderTypeTabs"; -import { MarketOrderInput } from "./MarketOrderInput"; import { LimitOrderInput } from "./LimitOrderInput"; +import { MarketOrderInput } from "./MarketOrderInput"; import { OrderSideTabs } from "./OrderSideTabs"; +import { OrderTypeTabs } from "./OrderTypeTabs"; function SubmitButton() { const symbol = useAppSelector(selectTargetToken).symbol; @@ -32,16 +31,6 @@ function SubmitButton() { return (
- {tab === OrderTab.LIMIT && ( -
- dispatch(orderInputSlice.actions.togglePostOnly())} - /> - Post only -
- )}
-
-
Description:
+
{description}
diff --git a/src/app/redux/orderInputSlice.ts b/src/app/redux/orderInputSlice.ts index 8484b64c..85c07e0e 100644 --- a/src/app/redux/orderInputSlice.ts +++ b/src/app/redux/orderInputSlice.ts @@ -149,10 +149,14 @@ export const submitOrder = createAsyncThunk< }); export const selectTargetToken = (state: RootState) => { - if (state.orderInput.side === OrderSide.SELL) { - return state.orderInput.token1; + if (state.orderInput.tab === OrderTab.MARKET) { + if (state.orderInput.side === OrderSide.SELL) { + return state.orderInput.token1; + } else { + return state.orderInput.token2; + } } else { - return state.orderInput.token2; + return state.orderInput.token1; } }; const selectSlippage = (state: RootState) => state.orderInput.slippage; @@ -283,6 +287,11 @@ export const orderInputSlice = createSlice({ // asynchronous reducers extraReducers: (builder) => { // fetchQuote + builder.addCase(fetchQuote.pending, (state) => { + state.quote = undefined; + state.description = undefined; + }); + builder.addCase( fetchQuote.fulfilled, (state, action: PayloadAction) => { @@ -292,12 +301,6 @@ export const orderInputSlice = createSlice({ throw new Error("Invalid quote"); } - // clear any previous message if it was set - // cannot do in "pending" because it causes infinite loop - // for getting the quote - // TODO: do not call fetchQuote when message is changed - state.token1.message = ""; - state.quote = quote; state.description = toDescription(quote); if (state.tab === OrderTab.MARKET) { @@ -308,31 +311,29 @@ export const orderInputSlice = createSlice({ } if (quote.message.startsWith("Not enough liquidity")) { - if (state.side === OrderSide.SELL) { - state.token1.amount = quote.fromAmount; - state.token1.message = quote.message; - } else { - state.token2.amount = quote.toAmount; - state.token2.message = quote.message; + if (state.tab === OrderTab.MARKET) { + if (state.side === OrderSide.SELL) { + state.token1.amount = quote.fromAmount; + state.token1.message = "Not enough liquidity."; + } else { + state.token2.amount = quote.toAmount; + state.token2.message = "Not enough liquidity."; + } } } } else { // limit order + // always changing the second token here because it's always the non-target token if (state.side === OrderSide.SELL) { state.token2.amount = Number(state.token1.amount) * state.price; } else { - state.token1.amount = Number(state.token2.amount) / state.price; + state.token2.amount = Number(state.token2.amount) / state.price; } } } ); builder.addCase(fetchQuote.rejected, (state, action) => { - // clear any previous message if it was set - // cannot do in "pending" because it causes infinite loop - // for getting the quote - // TODO: do not call fetchQuote when message is changed - state.token1.message = ""; if (state.side === OrderSide.SELL) { state.token2.amount = ""; state.token2.valid = false; diff --git a/src/app/utils.ts b/src/app/utils.ts index 143ccd4e..ee6d49e1 100644 --- a/src/app/utils.ts +++ b/src/app/utils.ts @@ -300,3 +300,13 @@ export const formatPercentageChange = (percChange: number | null): string => { } return "(0.00%)"; }; + +export function numberOrEmptyInput(event: React.ChangeEvent) { + let amount: number | ""; + if (event.target.value === "") { + amount = ""; + } else { + amount = Number(event.target.value); + } + return amount; +} From 4ac5545626e3a5c3487fd36bd26f29564ee28165 Mon Sep 17 00:00:00 2001 From: Evgeniia Vakarina <27793901+EvgeniiaVak@users.noreply.github.com> Date: Wed, 11 Oct 2023 22:11:25 +0400 Subject: [PATCH 24/39] fixes eslint errors --- src/app/components/order_input/OrderInput.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/components/order_input/OrderInput.tsx b/src/app/components/order_input/OrderInput.tsx index 8aab392a..c774a4e1 100644 --- a/src/app/components/order_input/OrderInput.tsx +++ b/src/app/components/order_input/OrderInput.tsx @@ -3,8 +3,6 @@ import { useEffect } from "react"; import { useAppDispatch, useAppSelector } from "hooks"; import { OrderTab, - fetchQuote, - isValidQuoteInput, isValidTransaction, selectTargetToken, submitOrder, From 0e8c22b5a4c496a51a74e834a173e652f71db637 Mon Sep 17 00:00:00 2001 From: Evgeniia Vakarina <27793901+EvgeniiaVak@users.noreply.github.com> Date: Thu, 12 Oct 2023 17:01:38 +0400 Subject: [PATCH 25/39] adds post-only tooltip --- .../components/order_input/LimitOrderInput.tsx | 17 ++++++++++++++++- src/app/redux/orderInputSlice.ts | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/app/components/order_input/LimitOrderInput.tsx b/src/app/components/order_input/LimitOrderInput.tsx index 57959637..eacc8b28 100644 --- a/src/app/components/order_input/LimitOrderInput.tsx +++ b/src/app/components/order_input/LimitOrderInput.tsx @@ -14,6 +14,12 @@ import { SwitchTokenPlacesButton, } from "./AmountInput"; import { numberOrEmptyInput } from "utils"; +import { AiOutlineInfoCircle } from "react-icons/ai"; + +const POST_ONLY_TOOLTIP = + "If your price is very close to the current market price, your limit order might fill immediately, " + + "making you pay taker fees. This option prevents your order from being executed immediately, " + + "guarantees that your order will make it to the order book and you will earn the liquidity provider fees."; function NonTargetToken() { const { token2, side } = useAppSelector((state) => state.orderInput); @@ -193,11 +199,20 @@ export function LimitOrderInput() {
dispatch(orderInputSlice.actions.togglePostOnly())} /> - POST ONLY + + POST ONLY + +
+ +
diff --git a/src/app/redux/orderInputSlice.ts b/src/app/redux/orderInputSlice.ts index 85c07e0e..1b06696a 100644 --- a/src/app/redux/orderInputSlice.ts +++ b/src/app/redux/orderInputSlice.ts @@ -81,7 +81,7 @@ const initialState: OrderInputState = { token1: initialTokenInput, token2: initialTokenInput, tab: OrderTab.MARKET, - postOnly: false, + postOnly: true, side: adex.OrderSide.BUY, price: 0, slippage: 0.01, From 73fae9dfcfea5ea40a58be2609c4557d14a96ea0 Mon Sep 17 00:00:00 2001 From: Evgeniia Vakarina <27793901+EvgeniiaVak@users.noreply.github.com> Date: Thu, 12 Oct 2023 17:10:29 +0400 Subject: [PATCH 26/39] fixes zeros not showing up --- src/app/components/order_input/AmountInput.tsx | 2 +- src/app/components/order_input/LimitOrderInput.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/order_input/AmountInput.tsx b/src/app/components/order_input/AmountInput.tsx index 44db34bf..8c36010e 100644 --- a/src/app/components/order_input/AmountInput.tsx +++ b/src/app/components/order_input/AmountInput.tsx @@ -46,7 +46,7 @@ export function AmountInput(props: TokenInputFiledProps) {
BALANCE: - {balance || "unknown"} + {balance}
{payReceive}
diff --git a/src/app/components/order_input/LimitOrderInput.tsx b/src/app/components/order_input/LimitOrderInput.tsx index eacc8b28..7be6711d 100644 --- a/src/app/components/order_input/LimitOrderInput.tsx +++ b/src/app/components/order_input/LimitOrderInput.tsx @@ -34,7 +34,7 @@ function NonTargetToken() {
BALANCE: - {balance || "unknown"} + {balance}
{side === OrderSide.BUY ? PayReceive.PAY : PayReceive.RECEIVE} From 91402d243012f7953fbc8480722fbc90b5334280 Mon Sep 17 00:00:00 2001 From: Evgeniia Vakarina <27793901+EvgeniiaVak@users.noreply.github.com> Date: Thu, 12 Oct 2023 21:20:49 +0400 Subject: [PATCH 27/39] fix validations --- package-lock.json | 8 +++--- package.json | 2 +- .../components/order_input/AmountInput.tsx | 2 +- .../order_input/LimitOrderInput.tsx | 27 ++++++++++++------ .../order_input/MarketOrderInput.tsx | 2 +- src/app/redux/orderInputSlice.ts | 28 +++++++++++++++++-- 6 files changed, 52 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 48127df0..bc269059 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@types/node": "20.3.3", "@types/react": "18.2.14", "@types/react-dom": "18.2.6", - "alphadex-sdk-js": "^0.11.0", + "alphadex-sdk-js": "^0.11.2", "autoprefixer": "10.4.14", "eslint-config-next": "13.4.7", "lightweight-charts": "^4.0.1", @@ -2332,9 +2332,9 @@ } }, "node_modules/alphadex-sdk-js": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/alphadex-sdk-js/-/alphadex-sdk-js-0.11.0.tgz", - "integrity": "sha512-xBf/NMIMto1P/rotZVfL+6LCD5HO3G3I9IIStQx8xqxMPUe3loRyNHokg+T7L9rkwGislI9UXrtpVlQEW9CHzw==", + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/alphadex-sdk-js/-/alphadex-sdk-js-0.11.2.tgz", + "integrity": "sha512-FPQzi7NG7eZ4KvtE73Iwab5uUkZ40be2ezglcuNrBQQDBOqDOlZhu4/G+S73T5uN02rKVDvz6cQiQCTIv8YDlQ==", "dependencies": { "axios": "^1.3.2", "rxjs": "^7.8.0" diff --git a/package.json b/package.json index 854feb57..fe4254ef 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "@types/node": "20.3.3", "@types/react": "18.2.14", "@types/react-dom": "18.2.6", - "alphadex-sdk-js": "^0.11.0", + "alphadex-sdk-js": "^0.11.2", "autoprefixer": "10.4.14", "eslint-config-next": "13.4.7", "lightweight-charts": "^4.0.1", diff --git a/src/app/components/order_input/AmountInput.tsx b/src/app/components/order_input/AmountInput.tsx index 8c36010e..c4429a5b 100644 --- a/src/app/components/order_input/AmountInput.tsx +++ b/src/app/components/order_input/AmountInput.tsx @@ -85,7 +85,7 @@ export function AmountInput(props: TokenInputFiledProps) {
{/* error message */} -
diff --git a/src/app/components/order_input/LimitOrderInput.tsx b/src/app/components/order_input/LimitOrderInput.tsx index 7be6711d..87bc6622 100644 --- a/src/app/components/order_input/LimitOrderInput.tsx +++ b/src/app/components/order_input/LimitOrderInput.tsx @@ -4,9 +4,9 @@ import { useAppDispatch, useAppSelector } from "hooks"; import { OrderSide, fetchQuote, - isValidQuoteInput, orderInputSlice, selectBalanceByAddress, + validatePriceInput, } from "redux/orderInputSlice"; import { AmountInput, @@ -61,8 +61,8 @@ function NonTargetToken() {
{/* error message */} -
); @@ -74,12 +74,18 @@ function PriceInput() { ); const { side, price } = useAppSelector((state) => state.orderInput); const dispatch = useAppDispatch(); + const priceValidationResult = useAppSelector(validatePriceInput); return ( <>
{side === OrderSide.BUY ? "AT MAX PRICE:" : "AT MIN PRICE:"}
-
+
1
+ {/* error message */} + ); } @@ -128,11 +140,10 @@ export function LimitOrderInput() { const bestBuyPrice = useAppSelector((state) => state.orderBook.bestBuy); const bestSellPrice = useAppSelector((state) => state.orderBook.bestSell); - - const quoteValidation = useAppSelector(isValidQuoteInput); + const priceValidationResult = useAppSelector(validatePriceInput); useEffect(() => { - if (quoteValidation.valid && token1.amount !== "") { + if (token1.valid && token1.amount !== "" && priceValidationResult.valid) { dispatch(fetchQuote()); } }, [ @@ -143,7 +154,7 @@ export function LimitOrderInput() { price, tab, postOnly, - quoteValidation, + priceValidationResult, ]); return ( diff --git a/src/app/components/order_input/MarketOrderInput.tsx b/src/app/components/order_input/MarketOrderInput.tsx index 476cf9c4..17051867 100644 --- a/src/app/components/order_input/MarketOrderInput.tsx +++ b/src/app/components/order_input/MarketOrderInput.tsx @@ -57,7 +57,7 @@ export function MarketOrderInput() { return (
-
+
@@ -240,9 +242,23 @@ export const orderInputSlice = createSlice({ ...state.token1, amount, }; - token = validateAmountWithBalance({ token, balance }); + + if (state.tab === OrderTab.MARKET) { + // token1 on market tab is always the one being sold + token = validateAmountWithBalance({ token, balance }); + } else { + // limit order + if (state.side === OrderSide.SELL) { + token = validateAmountWithBalance({ token, balance }); + } else { + token = validateAmount(token); + } + } + state.token1 = token; + // FIXME: when deleting the amount very quickly with backspace, + // state.token2.amount gets overritten with lagged quote data and stays filled in if (amount === "") { state.token2.amount = ""; } @@ -265,6 +281,14 @@ export const orderInputSlice = createSlice({ const temp = state.token1; state.token1 = state.token2; state.token2 = temp; + + // otherwise amount validation is incorrect + state.token1.amount = ""; + state.token1.valid = true; + state.token1.message = ""; + state.token2.amount = ""; + state.token2.valid = true; + state.token2.message = ""; }, setSide(state, action: PayloadAction) { state.side = action.payload; @@ -310,7 +334,7 @@ export const orderInputSlice = createSlice({ state.token1.amount = quote.fromAmount; } - if (quote.message.startsWith("Not enough liquidity")) { + if (quote.resultCode === 5 || quote.resultCode === 6) { if (state.tab === OrderTab.MARKET) { if (state.side === OrderSide.SELL) { state.token1.amount = quote.fromAmount; From fdd2da3ea0f3f3255e26b13af3aa846ccc0a6811 Mon Sep 17 00:00:00 2001 From: Evgeniia Vakarina <27793901+EvgeniiaVak@users.noreply.github.com> Date: Fri, 13 Oct 2023 16:42:49 +0400 Subject: [PATCH 28/39] fixes limit order errors --- .../order_input/LimitOrderInput.tsx | 4 +- src/app/redux/orderInputSlice.ts | 41 ++++++++++++------- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/app/components/order_input/LimitOrderInput.tsx b/src/app/components/order_input/LimitOrderInput.tsx index 87bc6622..6dc3b198 100644 --- a/src/app/components/order_input/LimitOrderInput.tsx +++ b/src/app/components/order_input/LimitOrderInput.tsx @@ -213,7 +213,9 @@ export function LimitOrderInput() { checked={postOnly} type="checkbox" className="checkbox checkbox-sm my-auto mr-2" - onClick={() => dispatch(orderInputSlice.actions.togglePostOnly())} + onChange={() => + dispatch(orderInputSlice.actions.togglePostOnly()) + } /> POST ONLY diff --git a/src/app/redux/orderInputSlice.ts b/src/app/redux/orderInputSlice.ts index 2233bd8c..d9ab8f00 100644 --- a/src/app/redux/orderInputSlice.ts +++ b/src/app/redux/orderInputSlice.ts @@ -7,7 +7,7 @@ import { import * as adex from "alphadex-sdk-js"; import { SdkResult } from "alphadex-sdk-js/lib/models/sdk-result"; import { RDT, getRdt } from "../subscriptions"; -import { displayAmount } from "../utils"; +import { RoundType, displayAmount, roundTo } from "../utils"; import { fetchAccountHistory } from "./accountHistorySlice"; import { selectBestBuy, selectBestSell } from "./orderBookSlice"; import { fetchBalances } from "./pairSelectorSlice"; @@ -327,7 +327,9 @@ export const orderInputSlice = createSlice({ state.quote = quote; state.description = toDescription(quote); + if (state.tab === OrderTab.MARKET) { + // MARKET if (state.side === OrderSide.SELL) { state.token2.amount = quote.toAmount; } else { @@ -335,23 +337,32 @@ export const orderInputSlice = createSlice({ } if (quote.resultCode === 5 || quote.resultCode === 6) { - if (state.tab === OrderTab.MARKET) { - if (state.side === OrderSide.SELL) { - state.token1.amount = quote.fromAmount; - state.token1.message = "Not enough liquidity."; - } else { - state.token2.amount = quote.toAmount; - state.token2.message = "Not enough liquidity."; - } + if (state.side === OrderSide.SELL) { + state.token1.amount = quote.fromAmount; + state.token1.message = "Not enough liquidity."; + } else { + state.token2.amount = quote.toAmount; + state.token2.message = "Not enough liquidity."; } } } else { - // limit order + // LIMIT order // always changing the second token here because it's always the non-target token if (state.side === OrderSide.SELL) { - state.token2.amount = Number(state.token1.amount) * state.price; + const amount = Number(state.token1.amount) * state.price; + state.token2.amount = roundTo( + amount, + adex.AMOUNT_MAX_DECIMALS, + RoundType.UP + ); } else { - state.token2.amount = Number(state.token2.amount) / state.price; + const amount = Number(state.token1.amount) / state.price; + // TODO: validate with balance + state.token2.amount = roundTo( + amount, + adex.AMOUNT_MAX_DECIMALS, + RoundType.UP + ); } } } @@ -397,8 +408,10 @@ function toDescription(quote: Quote): string { `to receive ${displayAmount(quote.toAmount, 8)} ${ quote.toToken.symbol }.\n`; - } else { - description += "Order will not immediately execute.\n"; + } + + if (quote.resultMessageLong) { + description += "\n" + quote.resultMessageLong; } return description; From 9ec24eaa06ec23c9de7c89c64d1eee142197ec31 Mon Sep 17 00:00:00 2001 From: Evgeniia Vakarina <27793901+EvgeniiaVak@users.noreply.github.com> Date: Fri, 13 Oct 2023 20:48:23 +0400 Subject: [PATCH 29/39] fix incorrect amount validation --- .eslintrc.json | 2 +- .../components/order_input/AmountInput.tsx | 6 +- .../order_input/LimitOrderInput.tsx | 62 ++++-- .../order_input/MarketOrderInput.tsx | 51 +++-- src/app/components/order_input/OrderInput.tsx | 42 ++-- src/app/redux/orderInputSlice.ts | 209 +++++++++--------- 6 files changed, 224 insertions(+), 148 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index aa312789..c1a15ed8 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -4,7 +4,7 @@ "parser": "@typescript-eslint/parser", "plugins": ["@typescript-eslint", "prettier"], "rules": { - "no-console": ["warn", { "allow": ["error"] }], + "no-console": ["warn", { "allow": ["error", "debug"] }], "prettier/prettier": [ "error", { diff --git a/src/app/components/order_input/AmountInput.tsx b/src/app/components/order_input/AmountInput.tsx index c4429a5b..748ca820 100644 --- a/src/app/components/order_input/AmountInput.tsx +++ b/src/app/components/order_input/AmountInput.tsx @@ -7,6 +7,7 @@ import { orderInputSlice, selectBalanceByAddress, selectTargetToken, + selectValidationByAddress, } from "redux/orderInputSlice"; export const enum PayReceive { @@ -27,12 +28,13 @@ export function AmountInput(props: TokenInputFiledProps) { const balance = useAppSelector((state) => selectBalanceByAddress(state, props.address) ); + const { valid, message } = useAppSelector((state) => + selectValidationByAddress(state, props.address) + ); const { address, symbol, iconUrl, - valid, - message, amount, disabled, payReceive, diff --git a/src/app/components/order_input/LimitOrderInput.tsx b/src/app/components/order_input/LimitOrderInput.tsx index 6dc3b198..71d16435 100644 --- a/src/app/components/order_input/LimitOrderInput.tsx +++ b/src/app/components/order_input/LimitOrderInput.tsx @@ -22,11 +22,29 @@ const POST_ONLY_TOOLTIP = "guarantees that your order will make it to the order book and you will earn the liquidity provider fees."; function NonTargetToken() { - const { token2, side } = useAppSelector((state) => state.orderInput); + const { token2, validationToken2, side } = useAppSelector( + (state) => state.orderInput + ); const balance = useAppSelector((state) => selectBalanceByAddress(state, token2.address) ); - const { symbol, iconUrl, valid, message, amount } = token2; + const { symbol, iconUrl, amount, address } = token2; + const { valid, message } = validationToken2; + const dispatch = useAppDispatch(); + + useEffect(() => { + if (side === OrderSide.SELL) { + dispatch(orderInputSlice.actions.validateAmount({ amount, address })); + } else { + dispatch( + orderInputSlice.actions.validateAmountWithBalance({ + amount, + address, + balance: balance || 0, + }) + ); + } + }, [amount, side, dispatch, balance, address]); return (
@@ -91,9 +109,9 @@ function PriceInput() { {pairToken1.symbol} - = + =
state.orderInput - ); + const { token1, validationToken1, price, side, tab, postOnly } = + useAppSelector((state) => state.orderInput); const { address: pairAddress } = useAppSelector( (state) => state.pairSelector @@ -143,13 +160,18 @@ export function LimitOrderInput() { const priceValidationResult = useAppSelector(validatePriceInput); useEffect(() => { - if (token1.valid && token1.amount !== "" && priceValidationResult.valid) { + if ( + validationToken1.valid && + token1.amount !== "" && + priceValidationResult.valid + ) { dispatch(fetchQuote()); } }, [ dispatch, pairAddress, token1, + validationToken1, side, price, tab, @@ -157,6 +179,20 @@ export function LimitOrderInput() { priceValidationResult, ]); + useEffect(() => { + if (side === OrderSide.BUY) { + dispatch(orderInputSlice.actions.validateAmount(token1)); + } else { + const tokenWithBalance = { + ...token1, + balance: balanceToken1 || 0, + }; + dispatch( + orderInputSlice.actions.validateAmountWithBalance(tokenWithBalance) + ); + } + }, [token1, balanceToken1, side, dispatch]); + return ( <>
@@ -167,11 +203,11 @@ export function LimitOrderInput() { side === OrderSide.BUY ? PayReceive.RECEIVE : PayReceive.PAY } onChange={(event) => { - const params = { - amount: numberOrEmptyInput(event), - balance: balanceToken1 || 0, - }; - dispatch(orderInputSlice.actions.setAmountToken1(params)); + dispatch( + orderInputSlice.actions.setAmountToken1( + numberOrEmptyInput(event) + ) + ); }} /> diff --git a/src/app/components/order_input/MarketOrderInput.tsx b/src/app/components/order_input/MarketOrderInput.tsx index 17051867..a12c9aeb 100644 --- a/src/app/components/order_input/MarketOrderInput.tsx +++ b/src/app/components/order_input/MarketOrderInput.tsx @@ -7,7 +7,6 @@ import { import { OrderSide, fetchQuote, - isValidQuoteInput, orderInputSlice, selectBalanceByAddress, selectTargetToken, @@ -25,21 +24,30 @@ function slippageToUiSlippage(slippage: number) { } export function MarketOrderInput() { - const { token1, token2, side, slippage, tab } = useAppSelector( - (state) => state.orderInput - ); + const { + token1, + token2, + validationToken1, + validationToken2, + side, + slippage, + tab, + } = useAppSelector((state) => state.orderInput); const balanceToken1 = useAppSelector((state) => selectBalanceByAddress(state, token1.address) ); const tartgetToken = useAppSelector(selectTargetToken); const pairAddress = useAppSelector((state) => state.pairSelector.address); - const quoteValidation = useAppSelector(isValidQuoteInput); const slippageValidationResult = useAppSelector(validateSlippageInput); const dispatch = useAppDispatch(); useEffect(() => { - if (quoteValidation.valid && tartgetToken.amount !== "") { + if ( + tartgetToken.amount !== "" && + validationToken1.valid && + validationToken2.valid + ) { dispatch(fetchQuote()); } }, [ @@ -50,11 +58,30 @@ export function MarketOrderInput() { side, slippage, tab, - quoteValidation, tartgetToken, - slippage, + validationToken1.valid, + validationToken2.valid, ]); + useEffect(() => { + dispatch( + orderInputSlice.actions.validateAmountWithBalance({ + address: token1.address, + amount: token1.amount, + balance: balanceToken1 || 0, + }) + ); + }, [token1, balanceToken1, side, dispatch]); + + useEffect(() => { + dispatch( + orderInputSlice.actions.validateAmount({ + address: token2.address, + amount: token2.amount, + }) + ); + }, [token2, dispatch]); + return (
@@ -65,11 +92,9 @@ export function MarketOrderInput() { dispatch(orderInputSlice.actions.setSide(OrderSide.SELL)); }} onChange={(event) => { - const params = { - amount: numberOrEmptyInput(event), - balance: balanceToken1 || 0, - }; - dispatch(orderInputSlice.actions.setAmountToken1(params)); + dispatch( + orderInputSlice.actions.setAmountToken1(numberOrEmptyInput(event)) + ); }} /> diff --git a/src/app/components/order_input/OrderInput.tsx b/src/app/components/order_input/OrderInput.tsx index c774a4e1..99f03515 100644 --- a/src/app/components/order_input/OrderInput.tsx +++ b/src/app/components/order_input/OrderInput.tsx @@ -3,9 +3,10 @@ import { useEffect } from "react"; import { useAppDispatch, useAppSelector } from "hooks"; import { OrderTab, - isValidTransaction, selectTargetToken, submitOrder, + validatePriceInput, + validateSlippageInput, } from "redux/orderInputSlice"; import { fetchBalances } from "redux/pairSelectorSlice"; import { LimitOrderInput } from "./LimitOrderInput"; @@ -15,23 +16,34 @@ import { OrderTypeTabs } from "./OrderTypeTabs"; function SubmitButton() { const symbol = useAppSelector(selectTargetToken).symbol; - const tab = useAppSelector((state) => state.orderInput.tab); - const side = useAppSelector((state) => state.orderInput.side); - const transactionInProgress = useAppSelector( - (state) => state.orderInput.transactionInProgress - ); - const transactionResult = useAppSelector( - (state) => state.orderInput.transactionResult - ); - const transactionValidation = useAppSelector(isValidTransaction); + const tartgetToken = useAppSelector(selectTargetToken); + + const { + tab, + side, + validationToken1, + validationToken2, + transactionInProgress, + transactionResult, + } = useAppSelector((state) => state.orderInput); + const dispatch = useAppDispatch(); const submitString = tab.toString() + " " + side.toString() + " " + symbol; + const isPriceValid = useAppSelector(validatePriceInput).valid; + const isSlippageValid = useAppSelector(validateSlippageInput).valid; + const isValidTransaction = + tartgetToken.amount !== "" && + validationToken1.valid && + validationToken2.valid && + isPriceValid && + isSlippageValid; + return (