Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ECO-2313] Fix input fields on market and pools pages #308

Merged
merged 7 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,44 +1,72 @@
import React from "react";
import { Input } from "components/inputs/input";
import { type InputProps } from "components/inputs/input/types";
import Big from "big.js";
import React, { useEffect, useState } from "react";
import { isNumberInConstruction, countDigitsAfterDecimal, sanitizeNumber } from "@sdk/utils";

const NUMBERS = new Set("0123456789");
const intToStr = (value: bigint, decimals?: number) =>
(Number(value) / 10 ** (decimals ?? 0)).toString();

export const InputNumeric = <E extends React.ElementType = "input">({
const strToInt = (value: string, decimals?: number) => {
if (isNaN(parseFloat(value))) {
return 0n;
}
const res = Big(value.toString()).mul(Big(10 ** (decimals ?? 0)));
if (res < Big(1)) {
return 0n;
}
return BigInt(res.toString());
};

export const InputNumeric = ({
onUserInput,
decimals,
value,
onSubmit,
...props
}: InputProps<E> & { onUserInput: (value: string) => void }): JSX.Element => {
const [input, setInput] = React.useState("");

const onChangeText = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value.replace(/,/g, ".").replace(/^0+/, "0");

let hasDecimal = false;
let s = "";
for (const char of value) {
if (char === ".") {
if (!hasDecimal) {
s += char;
} else {
hasDecimal = true;
}
} else if (NUMBERS.has(char)) {
s += char;
}
}: {
className?: string;
onUserInput?: (value: bigint) => void;
onSubmit?: (value: bigint) => void;
decimals?: number;
disabled?: boolean;
value: bigint;
}) => {
const [input, setInput] = useState(intToStr(value, decimals));

useEffect(() => {
if (strToInt(input, decimals) != value) {
setInput(intToStr(value, decimals));
}
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [value, decimals]);

const onChangeText = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = sanitizeNumber(e.target.value);

if (!isNumberInConstruction(value)) {
return;
}

if (s === "" || !isNaN(Number(s))) {
setInput(s);
onUserInput(s);
const decimalsInValue = countDigitsAfterDecimal(value);
if (typeof decimals === "number" && decimalsInValue > decimals) {
return;
}

setInput(value);
if (onUserInput) {
onUserInput(strToInt(value, decimals));
}
};

return (
<Input
inputMode="decimal"
pattern="^\d*\.?\d*$"
onChange={(event) => onChangeText(event)}
<input
type="text"
onChange={(e) => onChangeText(e)}
value={input}
onKeyDown={(e) => {
if (e.key === "Enter" && onSubmit) {
onSubmit(strToInt(input, decimals));
}
}}
{...props}
/>
);
Expand Down
CRBl69 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
type PropsWithChildren,
useEffect,
useState,
useCallback,
useMemo,
type MouseEventHandler,
} from "react";
Expand All @@ -26,6 +25,7 @@ import { toCoinTypes } from "@sdk/markets/utils";
import { Flex, FlexGap } from "@containers";
import Popup from "components/popup";
import { Text } from "components/text";
import { InputNumeric } from "components/inputs";

const SmallButton = ({
emoji,
Expand Down Expand Up @@ -79,8 +79,7 @@ const inputAndOutputStyles = `
border-transparent !p-0 text-white
`;

const APT_DISPLAY_DECIMALS = 4;
const EMOJICOIN_DISPLAY_DECIMALS = 1;
const OUTPUT_DISPLAY_DECIMALS = 4;
const SWAP_GAS_COST = 52500n;

export default function SwapComponent({
Expand All @@ -103,7 +102,7 @@ export default function SwapComponent({
const [inputAmount, setInputAmount] = useState(
toActualCoinDecimals({ num: presetInputAmountIsValid ? presetInputAmount! : "1" })
);
const [outputAmount, setOutputAmount] = useState("0");
const [outputAmount, setOutputAmount] = useState(0n);
const [previous, setPrevious] = useState(inputAmount);
const [isLoading, setIsLoading] = useState(false);
const [isSell, setIsSell] = useState(!(searchParams.get("sell") === null));
Expand All @@ -125,15 +124,18 @@ export default function SwapComponent({

const swapResult = useSimulateSwap({
marketAddress,
inputAmount: inputAmount === "" ? "0" : inputAmount,
inputAmount: inputAmount.toString(),
isSell,
numSwaps,
});

const outputAmountString = toDisplayCoinDecimals({
num: isLoading ? previous : outputAmount,
decimals: OUTPUT_DISPLAY_DECIMALS,
});

const { ref, replay } = useScramble({
text: Number(isLoading ? previous : outputAmount).toFixed(
isSell ? APT_DISPLAY_DECIMALS : EMOJICOIN_DISPLAY_DECIMALS
),
text: new Intl.NumberFormat().format(Number(outputAmountString)),
overdrive: false,
overflow: true,
speed: isLoading ? 0.4 : 1000,
Expand All @@ -151,52 +153,19 @@ export default function SwapComponent({
setIsLoading(true);
return;
}
const swapResultDisplay = toDisplayNumber(swapResult, isSell ? "apt" : "emoji");
setPrevious(swapResultDisplay);
setOutputAmount(swapResultDisplay);
setPrevious(swapResult);
setOutputAmount(swapResult);
setIsLoading(false);
replay();
}, [swapResult, replay, isSell]);

const toDisplayNumber = (value: bigint | number | string, type: "apt" | "emoji" = "apt") => {
const badString = typeof value === "string" && (value === "" || isNaN(parseInt(value)));
if (!value || badString) {
return "0";
}
// We use the APT display decimal amount here to avoid early truncation.
return toDisplayCoinDecimals({
num: value,
decimals: type === "apt" ? APT_DISPLAY_DECIMALS : EMOJICOIN_DISPLAY_DECIMALS,
}).toString();
};

const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.value === "") {
setInputAmount("");
}
if (isNaN(parseFloat(e.target.value))) {
e.stopPropagation();
return;
}
setInputAmount(toActualCoinDecimals({ num: e.target.value }));
};

const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && submit) {
submit();
}
},
[submit]
);

const sufficientBalance = useMemo(() => {
if (!account || (isSell && !emojicoinBalance) || (!isSell && !aptBalance)) return false;
if (account) {
if (isSell) {
return emojicoinBalance >= BigInt(inputAmount);
return emojicoinBalance >= inputAmount;
}
return aptBalance >= BigInt(inputAmount);
return aptBalance >= inputAmount;
}
}, [account, aptBalance, emojicoinBalance, isSell, inputAmount]);

Expand All @@ -205,7 +174,7 @@ export default function SwapComponent({
const coinBalance = isSell ? emojicoinBalance : aptBalance;
const balance = toDisplayCoinDecimals({
num: coinBalance,
decimals: !isSell ? APT_DISPLAY_DECIMALS : EMOJICOIN_DISPLAY_DECIMALS,
decimals: 4,
});
return (
<AnimatePresence>
Expand Down Expand Up @@ -235,30 +204,40 @@ export default function SwapComponent({
<SmallButton
emoji="🤢"
description="Sell 50%"
onClick={() => setInputAmount(String(emojicoinBalance / 2n))}
onClick={() => {
setInputAmount(emojicoinBalance / 2n);
}}
/>
<SmallButton
emoji="🤮"
description="Sell 100%"
onClick={() => setInputAmount(String(emojicoinBalance))}
onClick={() => {
setInputAmount(emojicoinBalance);
}}
/>
</>
) : (
<>
<SmallButton
emoji="🌒"
description="Buy 25%"
onClick={() => setInputAmount(String(availableAptBalance / 4n))}
onClick={() => {
setInputAmount(availableAptBalance / 4n);
}}
/>
<SmallButton
emoji="🌓"
description="Buy 50%"
onClick={() => setInputAmount(String(availableAptBalance / 2n))}
onClick={() => {
setInputAmount(availableAptBalance / 2n);
}}
/>
<SmallButton
emoji="🌕"
description="Buy 100%"
onClick={() => setInputAmount(String(availableAptBalance))}
onClick={() => {
setInputAmount(availableAptBalance);
}}
/>
</>
)}
Expand All @@ -272,25 +251,23 @@ export default function SwapComponent({
{isSell ? t("You sell") : t("You pay")}
{balanceLabel}
</div>
<input
<InputNumeric
className={inputAndOutputStyles + " bg-transparent leading-[32px]"}
value={
inputAmount === ""
? ""
: Number(toDisplayNumber(inputAmount, isSell ? "emoji" : "apt"))
}
step={0.01}
onChange={handleInput}
onKeyDown={handleKeyDown}
type="number"
></input>
value={inputAmount}
onUserInput={(v) => setInputAmount(v)}
onSubmit={() => (submit ? submit() : {})}
decimals={8}
/>
</Column>
{isSell ? <EmojiInputLabel emoji={emojicoin} /> : <AptosInputLabel />}
</InnerWrapper>

<FlipInputsArrow
onClick={() => {
setInputAmount(toActualCoinDecimals({ num: outputAmount }));
setInputAmount(outputAmount);
// This is done as to not display an old value if the swap simulation fails.
setOutputAmount(0n);
setPrevious(0n);
setIsSell((v) => !v);
}}
/>
Expand Down
Loading
Loading