diff --git a/src/app/components/OrderInput.tsx b/src/app/components/OrderInput.tsx
index 0754912f..5d0535a7 100644
--- a/src/app/components/OrderInput.tsx
+++ b/src/app/components/OrderInput.tsx
@@ -1,5 +1,8 @@
-import { useEffect, useState, ChangeEvent } from "react";
+import { useEffect, useState, useRef, ChangeEvent, useCallback } from "react";
import { AiOutlineInfoCircle } from "react-icons/ai";
+import Tippy from "@tippyjs/react";
+import "tippy.js/dist/tippy.css";
+import "tippy.js/dist/svg-arrow.css";
import {
getPrecision,
@@ -361,6 +364,7 @@ function SubmitButton() {
side,
type,
token1,
+ token2,
quote,
quoteDescription,
quoteError,
@@ -373,7 +377,9 @@ function SubmitButton() {
const hasQuoteError = quoteError !== undefined;
const isLimitOrder = type === OrderType.LIMIT;
const isBuyOrder = side === OrderSide.BUY;
+ const isZeroAmount = token1.amount === 0 || token2.amount === 0;
const disabled =
+ isZeroAmount ||
!hasQuote ||
hasQuoteError ||
!isConnected ||
@@ -442,13 +448,71 @@ function SubmitButton() {
}
function UserInputContainer() {
- const { side, type } = useAppSelector((state) => state.orderInput);
+ const dispatch = useAppDispatch();
+ const { side, type, token1, token2 } = useAppSelector(
+ (state) => state.orderInput
+ );
+ const balanceToken1 =
+ useAppSelector((state) => selectBalanceByAddress(state, token1.address)) ||
+ 0;
+ const balanceToken2 =
+ useAppSelector((state) => selectBalanceByAddress(state, token2.address)) ||
+ 0;
+ const bestBuy = useAppSelector((state) => state.orderBook.bestBuy) || 0;
+ const bestSell = useAppSelector((state) => state.orderBook.bestSell) || 0;
const isMarketOrder = type === "MARKET";
const isLimitOrder = type === "LIMIT";
const isBuyOrder = side === "BUY";
const isSellOrder = side === "SELL";
+ const sliderCallback = useCallback(
+ (newPercentage: number) => {
+ const isXRDToken = isBuyOrder
+ ? token2.symbol === "XRD"
+ : token1.symbol === "XRD";
+ let balance = isBuyOrder ? balanceToken2 : balanceToken1;
+
+ if (newPercentage === 100 && isXRDToken) {
+ balance = Math.max(balance - XRD_FEE_ALLOWANCE, 0);
+ }
+
+ const amount = Calculator.divide(
+ Calculator.multiply(balance, newPercentage),
+ 100
+ );
+
+ const specifiedToken = isBuyOrder
+ ? SpecifiedToken.TOKEN_2
+ : SpecifiedToken.TOKEN_1;
+
+ dispatch(
+ orderInputSlice.actions.setTokenAmount({
+ amount,
+ bestBuy,
+ bestSell,
+ balanceToken1,
+ balanceToken2,
+ specifiedToken,
+ })
+ );
+ },
+ [
+ isBuyOrder,
+ token1.symbol,
+ token2.symbol,
+ balanceToken1,
+ balanceToken2,
+ bestBuy,
+ bestSell,
+ dispatch,
+ ]
+ );
+
+ useEffect(() => {
+ sliderCallback(0);
+ }, [isBuyOrder, isSellOrder, isMarketOrder, isLimitOrder, sliderCallback]);
+
return (
{isMarketOrder && (
@@ -457,20 +521,36 @@ function UserInputContainer() {
userAction={UserAction.UPDATE_PRICE}
disabled={true}
/>
-
{isSellOrder && ( // specify "Quantity"
)}
{isBuyOrder && ( // specify "Total"
)}
+
+ sliderCallback(newPercentage)
+ }
+ isLimitOrder={isLimitOrder}
+ isBuyOrder={isBuyOrder}
+ isSellOrder={isSellOrder}
+ />
>
)}
{isLimitOrder && (
<>
-
+
+ sliderCallback(newPercentage)
+ }
+ isLimitOrder={isLimitOrder}
+ isBuyOrder={isBuyOrder}
+ isSellOrder={isSellOrder}
+ />
{isLimitOrder && }
>
@@ -487,6 +567,7 @@ function CurrencyInputGroupSettings(
): CurrencyInputGroupConfig {
const t = useTranslations();
const dispatch = useAppDispatch();
+
const {
side,
type,
@@ -796,10 +877,197 @@ function InputTooltip({ message }: { message: string }) {
}
// TODO(dcts): implement percentage slider in future PR
-function PercentageSlider() {
- return <>>;
+interface PercentageSliderProps {
+ initialPercentage: number;
+ callbackOnPercentageUpdate: (newPercentage: number) => void;
+ isLimitOrder: boolean;
+ isBuyOrder: boolean;
+ isSellOrder: boolean;
}
+const PercentageSlider: React.FC = ({
+ initialPercentage,
+ callbackOnPercentageUpdate,
+ isLimitOrder,
+ isBuyOrder,
+ isSellOrder,
+}) => {
+ const [percentage, setPercentage] = useState(initialPercentage);
+ const [toolTipVisible, setToolTipVisible] = useState(false);
+ const sliderRef = useRef(null);
+ const { token1, token2 } = useAppSelector((state) => state.orderInput);
+
+ const inputToken1 = useAppSelector((state) => state.orderInput.token1.amount);
+ const inputToken2 = useAppSelector((state) => state.orderInput.token2.amount);
+
+ const balanceToken1 =
+ useAppSelector((state) => selectBalanceByAddress(state, token1.address)) ||
+ 0;
+ const balanceToken2 =
+ useAppSelector((state) => selectBalanceByAddress(state, token2.address)) ||
+ 0;
+
+ const handleChange = (e: React.ChangeEvent) => {
+ const newPercentage = parseInt(e.target.value, 10);
+ setPercentage(newPercentage);
+ callbackOnPercentageUpdate(newPercentage);
+ };
+
+ useEffect(() => {
+ if (!sliderRef.current) {
+ return;
+ }
+
+ sliderRef.current.value = "0";
+ sliderRef.current.style.backgroundSize = `0% 100%`;
+ setPercentage(0);
+
+ if (inputToken1 > 0 && isLimitOrder && isBuyOrder) {
+ return;
+ } else if (inputToken2 > 0 && isLimitOrder && isSellOrder) {
+ return;
+ } else if (balanceToken2 && inputToken2 > 0) {
+ const newPercentage = Calculator.multiply(
+ Calculator.divide(inputToken2, balanceToken2),
+ 100
+ );
+ setPercentage(newPercentage);
+ sliderRef.current.style.backgroundSize = `${newPercentage}% 100%`;
+ } else if (balanceToken1 && inputToken1 > 0) {
+ const newPercentage = Calculator.multiply(
+ Calculator.divide(inputToken1, balanceToken1),
+ 100
+ );
+ setPercentage(newPercentage);
+ sliderRef.current.style.backgroundSize = `${newPercentage}% 100%`;
+ }
+ }, [
+ inputToken1,
+ balanceToken1,
+ inputToken2,
+ balanceToken2,
+ isLimitOrder,
+ isBuyOrder,
+ isSellOrder,
+ ]);
+
+ const handleClickOnLabel = (newPercentage: number) => {
+ setPercentage(newPercentage);
+ callbackOnPercentageUpdate(newPercentage);
+ };
+
+ return (
+ <>
+
+
+
setToolTipVisible(true)}
+ onMouseUp={() => setToolTipVisible(false)}
+ onMouseEnter={() => setToolTipVisible(true)}
+ onMouseLeave={() => setToolTipVisible(false)}
+ />
+
{Math.round(percentage)}%}
+ visible={toolTipVisible}
+ onClickOutside={() => setToolTipVisible(false)}
+ arrow={false}
+ theme="custom"
+ placement="top"
+ >
+
+
+
+
+
+
+ {Array(5)
+ .fill(0)
+ .map((_, index) => (
+
+ ))}
+
+
+
+
+
+ handleClickOnLabel(0)}
+ >
+ 0%
+
+ handleClickOnLabel(25)}
+ >
+ 25%
+
+ handleClickOnLabel(50)}
+ >
+ 50%
+
+ handleClickOnLabel(75)}
+ >
+ 75%
+
+ handleClickOnLabel(100)}
+ >
+ 100%
+
+
+
+
+
+ >
+ );
+};
+
// Mimics IMask with improved onAccept, triggered only by user input to avoid rerender bugs.
function CustomNumericIMask({
value,
diff --git a/src/app/state/locales/en/errors.json b/src/app/state/locales/en/errors.json
index 7ed58ff1..2ff0e430 100644
--- a/src/app/state/locales/en/errors.json
+++ b/src/app/state/locales/en/errors.json
@@ -1,7 +1,6 @@
{
"UNSPECIFIED_PRICE": "Price must be specified",
"NONZERO_PRICE": "Price must be greater than 0",
- "NONZERO_AMOUNT": "Amount must be greater than 0",
"HIGH_PRICE": "Price is significantly higher than best sell",
"LOW_PRICE": "Price is significantly lower than best buy",
"EXCESSIVE_DECIMALS": "Too many decimal places",
diff --git a/src/app/state/locales/pt/errors.json b/src/app/state/locales/pt/errors.json
index dbd78f14..dda644fc 100644
--- a/src/app/state/locales/pt/errors.json
+++ b/src/app/state/locales/pt/errors.json
@@ -1,7 +1,6 @@
{
"UNSPECIFIED_PRICE": "Preço deve ser especificado",
"NONZERO_PRICE": "Preço deve ser maior que 0",
- "NONZERO_AMOUNT": "Quantidade deve ser maior que 0",
"HIGH_PRICE": "Preço está significativamente maior que a melhor oferta de venda",
"LOW_PRICE": "Preço está significativamente menor que a melhor oferta de compra",
"EXCESSIVE_DECIMALS": "Excesso de casas decimais",
diff --git a/src/app/state/orderInputSlice.test.ts b/src/app/state/orderInputSlice.test.ts
index 1d3d07f3..a8a919bd 100644
--- a/src/app/state/orderInputSlice.test.ts
+++ b/src/app/state/orderInputSlice.test.ts
@@ -66,32 +66,6 @@ describe("OrderInputSlice", () => {
);
});
- it("Validation works for zero token1 amount", () => {
- store.dispatch(
- orderInputSlice.actions.setTokenAmount({
- amount: 0,
- specifiedToken: SpecifiedToken.TOKEN_1,
- })
- );
- expect(store.getState().orderInput.validationToken1.valid).toBe(false);
- expect(store.getState().orderInput.validationToken1.message).toBe(
- ErrorMessage.NONZERO_AMOUNT
- );
- });
-
- it("Validation works for zero token2 amount", () => {
- store.dispatch(
- orderInputSlice.actions.setTokenAmount({
- amount: 0,
- specifiedToken: SpecifiedToken.TOKEN_2,
- })
- );
- expect(store.getState().orderInput.validationToken2.valid).toBe(false);
- expect(store.getState().orderInput.validationToken2.message).toBe(
- ErrorMessage.NONZERO_AMOUNT
- );
- });
-
it("Validation works for insufficient balance for token1 on sell order ", () => {
store.dispatch(orderInputSlice.actions.setSide(OrderSide.SELL));
store.dispatch(
diff --git a/src/app/state/orderInputSlice.ts b/src/app/state/orderInputSlice.ts
index 5f5ceeb0..28f7751f 100644
--- a/src/app/state/orderInputSlice.ts
+++ b/src/app/state/orderInputSlice.ts
@@ -40,7 +40,6 @@ export enum SpecifiedToken {
export enum ErrorMessage {
NONZERO_PRICE = "NONZERO_PRICE",
- NONZERO_AMOUNT = "NONZERO_AMOUNT",
INSUFFICIENT_FUNDS = "INSUFFICIENT_FUNDS",
COULD_NOT_GET_QUOTE = "COULD_NOT_GET_QUOTE",
INSUFFICIENT_LIQUDITIY = "INSUFFICIENT_LIQUDITIY",
@@ -422,21 +421,6 @@ export const orderInputSlice = createSlice({
state.price = bestPrice;
}
- // Set zero amount error
- if (amount === 0) {
- if (specifiedToken === SpecifiedToken.TOKEN_1) {
- state.validationToken1 = {
- valid: false,
- message: ErrorMessage.NONZERO_AMOUNT,
- };
- } else if (specifiedToken === SpecifiedToken.TOKEN_2) {
- state.validationToken2 = {
- valid: false,
- message: ErrorMessage.NONZERO_AMOUNT,
- };
- }
- }
-
// Set insufficient balance for specifiedToken
if (
specifiedToken === SpecifiedToken.TOKEN_1 &&
diff --git a/src/app/styles/globals.css b/src/app/styles/globals.css
index d2b40260..dbb2c799 100644
--- a/src/app/styles/globals.css
+++ b/src/app/styles/globals.css
@@ -239,3 +239,58 @@ input[type="number"]::-ms-clear {
.account-history table thead tr th:last-child {
padding-right: 16px;
}
+
+/* pseudo-classes for the percentage slider */
+input[type="range"]::-webit-slider-thumb {
+ appearance: none;
+ height: 14px;
+ width: 14px;
+ border-radius: 50%;
+ background-color: #474d52;
+ color: #474d52;
+ border: 3.5px #ffffff solid;
+ position: relative;
+ z-index: 2;
+ cursor: pointer;
+}
+
+input[type="range"]::-moz-range-thumb {
+ appearance: none;
+ height: 14px;
+ width: 14px;
+ background-color: #474d52;
+ border-radius: 50%;
+ cursor: pointer;
+}
+
+input[type="range"]::-ms-thumb {
+ appearance: none;
+ height: 14px;
+ width: 14px;
+ background-color: #474d52;
+ border-radius: 50%;
+ cursor: pointer;
+}
+
+input[type="range"]::-webkit-slider-runnable-track {
+ -webkit-appearance: none;
+ box-shadow: none;
+ border: none;
+ width: full;
+ cursor: pointer;
+ z-index: 99;
+}
+
+input[type="range"]::-moz-range-track {
+ appearance: none;
+ box-shadow: none;
+ border: none;
+ cursor: pointer;
+}
+
+input[type="range"]::-ms-track {
+ appearance: none;
+ box-shadow: none;
+ border: none;
+ cursor: pointer;
+}
diff --git a/tailwind.config.js b/tailwind.config.js
index 1da2c7e7..eb11f539 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -33,6 +33,10 @@ module.exports = {
"dexter-grey-dark": "#141414",
"dexter-grey-light": "#191B1D",
"content-dark": "#212A09",
+ "slider-grey": "#474d52",
+ },
+ fontSize: {
+ xxs: "0.6rem",
},
keyframes: {
blueLight: {