diff --git a/example/package.json b/example/package.json index 62bbfdf1..ffefb492 100644 --- a/example/package.json +++ b/example/package.json @@ -1,6 +1,6 @@ { "name": "@project-serum/swap-ui-example", - "version": "0.1.1", + "version": "0.1.2", "homepage": ".", "private": true, "scripts": { diff --git a/package.json b/package.json index 9712dade..deaad7b0 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "dependencies": { "@project-serum/serum": "^0.13.34", "@project-serum/swap": "^0.1.0-alpha.31", - "@solana/spl-token": "^0.1.4" + "@solana/spl-token": "^0.1.4", + "big.js": "^6.1.1" }, "peerDependencies": { "@material-ui/core": "^4.11.4", @@ -49,6 +50,7 @@ "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", + "@types/big.js": "^6.1.1", "@types/bs58": "^4.0.1", "@types/jest": "^26.0.15", "@types/node": "^12.0.0", diff --git a/src/components/Swap.tsx b/src/components/Swap.tsx index 2cba7b6a..80cfec6f 100644 --- a/src/components/Swap.tsx +++ b/src/components/Swap.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { PublicKey, Keypair, @@ -32,6 +32,7 @@ import TokenDialog from "./TokenDialog"; import { SettingsButton } from "./Settings"; import { InfoLabel } from "./Info"; import { SOL_MINT, WRAPPED_SOL_MINT } from "../utils/pubkeys"; +import { Big, BigSource } from "big.js" const useStyles = makeStyles((theme) => ({ card: { @@ -161,29 +162,35 @@ export function ArrowButton() { } function SwapFromForm({ style }: { style?: any }) { - const { fromMint, setFromMint, fromAmount, setFromAmount } = useSwapContext(); + const { fromMint, setFromMint, amountfromInput, setAmountFromInput, fromAmount, setFromDecimals, setInputIsToAmount, fromDecimals } = useSwapContext(); return ( ); } function SwapToForm({ style }: { style?: any }) { - const { toMint, setToMint, toAmount, setToAmount } = useSwapContext(); + const { toMint, setToMint, amountToInput, setAmountToInput, toAmount, setToDecimals, setInputIsToAmount } = useSwapContext(); return ( ); } @@ -193,15 +200,21 @@ export function SwapTokenForm({ style, mint, setMint, + inputAmount, + setInputAmount, amount, - setAmount, + setDecimals, + setInputIsToAmount, }: { from: boolean; style?: any; mint: PublicKey; setMint: (m: PublicKey) => void; - amount: number; - setAmount: (a: number) => void; + inputAmount: string; + setInputAmount: (a: string) => void; + amount: Big; + setDecimals: (n: number) => void; + setInputIsToAmount: (b: boolean) => void; }) { const styles = useStyles(); @@ -209,18 +222,15 @@ export function SwapTokenForm({ const tokenAccount = useOwnedTokenAccount(mint); const mintAccount = useMint(mint); + //ToDo: Make balance a big type number const balance = tokenAccount && mintAccount && tokenAccount.account.amount.toNumber() / 10 ** mintAccount.decimals; - const formattedAmount = - mintAccount && amount - ? amount.toLocaleString("fullwide", { - maximumFractionDigits: mintAccount.decimals, - useGrouping: false, - }) - : amount; +const formattedAmount = Big(amount ?? 0).round(mintAccount && mintAccount.decimals || 2).toString() || ""; + +useEffect(()=> {setDecimals(mintAccount && mintAccount.decimals || 2)}); return (
@@ -233,17 +243,19 @@ export function SwapTokenForm({ {from && !!balance ? ( setAmount(balance)} + onClick={() => setInputAmount(balance.toString())} > MAX ) : null} + {(!from && "~" || "")}{(formattedAmount != "NaN") ? formattedAmount : ""}
setAmount(parseFloat(e.target.value))} + value={inputAmount} + onFocus={() => setInputIsToAmount(!from)} + onChange={(e) => { setInputAmount(e.target.value);} } InputProps={{ disableUnderline: true, classes: { @@ -360,7 +372,7 @@ export function SwapButton() { throw new Error("Quote mint not found"); } - const amount = new BN(fromAmount * 10 ** fromMintInfo.decimals); + const amount = new BN(fromAmount.times(10 ** fromMintInfo.decimals).toString()); const isSol = fromMint.equals(SOL_MINT) || toMint.equals(SOL_MINT); const wrappedSolAccount = isSol ? Keypair.generate() : undefined; @@ -512,3 +524,4 @@ function unwrapSol( ); return { tx, signers: [] }; } + diff --git a/src/context/Swap.tsx b/src/context/Swap.tsx index ed0c011e..8cc3a7b0 100644 --- a/src/context/Swap.tsx +++ b/src/context/Swap.tsx @@ -1,5 +1,5 @@ import * as assert from "assert"; -import React, { useContext, useState, useEffect } from "react"; +import React, { useContext, useState, useEffect, useRef } from "react"; import { useAsync } from "react-async-hook"; import { PublicKey } from "@solana/web3.js"; import { @@ -20,7 +20,8 @@ import { SPL_REGISTRY_SOLLET_TAG, SPL_REGISTRY_WORM_TAG, } from "./TokenList"; -import { useOwnedTokenAccount } from "../context/Token"; +import { useMint, useOwnedTokenAccount } from "../context/Token"; +import { Big, BigSource } from "big.js" const DEFAULT_SLIPPAGE_PERCENT = 0.5; @@ -29,17 +30,31 @@ export type SwapContext = { fromMint: PublicKey; setFromMint: (m: PublicKey) => void; + fromDecimals: number; + setFromDecimals: (n: number) => void; + + toDecimals: number; + setToDecimals: (n: number) => void; + // Mint being traded to. The user will receive these tokens after the swap. toMint: PublicKey; setToMint: (m: PublicKey) => void; - // Amount used for the swap. - fromAmount: number; - setFromAmount: (a: number) => void; + // Amount used for the swap in Big number to avoid precision errors during input. + fromAmount: Big; + // This is no longer set directly from the UI + // setfromAmount: (a: BigSource) => void; + + // Decoupled user input amounts stored in DOM native type + amountfromInput: string, + setAmountFromInput: (a: string) => void; + amountToInput: string, + setAmountToInput: (a: string) => void; - // *Expected* amount received from the swap. - toAmount: number; - setToAmount: (a: number) => void; + // *Expected* amount received from the swap in Big number to avoid precision errors during input + toAmount: Big; + // This is no longer set directly from the UI + // setToAmount: (a: BigSource) => void; // Function to flip what we consider to be the "to" and "from" mints. swapToFromMints: () => void; @@ -72,57 +87,83 @@ export type SwapContext = { setIsStrict: (isStrict: boolean) => void; setIsClosingNewAccounts: (b: boolean) => void; + + inputIsToAmount: boolean; + setInputIsToAmount: (b: boolean) => void; }; const _SwapContext = React.createContext(null); export function SwapContextProvider(props: any) { const [fromMint, setFromMint] = useState(props.fromMint ?? SRM_MINT); const [toMint, setToMint] = useState(props.toMint ?? USDC_MINT); - const [fromAmount, _setFromAmount] = useState(props.fromAmount ?? 0); - const [toAmount, _setToAmount] = useState(props.toAmount ?? 0); + const [fromDecimals, setFromDecimals] = useState(props.fromDecimals ?? 2); + const [toDecimals, setToDecimals] = useState(props.toDecimals ?? 2); + const [fromAmount, setFromAmount] = useState(props.fromAmount ?? Big(0)); + const [toAmount, setToAmount] = useState(props.toAmount ?? Big(0)); + const [amountfromInput, setAmountFromInput] = useState(props.inputFromAmount ?? "0"); + const [amountToInput, setAmountToInput] = useState(props.inputToAmount ?? "0"); const [isClosingNewAccounts, setIsClosingNewAccounts] = useState(false); const [isStrict, setIsStrict] = useState(false); const [slippage, setSlippage] = useState(DEFAULT_SLIPPAGE_PERCENT); const [fairOverride, setFairOverride] = useState(null); - const fair = _useSwapFair(fromMint, toMint, fairOverride); + let fair = _useSwapFair(fromMint, toMint, fairOverride); const referral = props.referral; + const [inputIsToAmount, setInputIsToAmount] = useState(false); assert.ok(slippage >= 0); useEffect(() => { - if (!fair) { - return; + if(inputIsToAmount) { + //This makes sure we do not play with the amount the user is inputting, but the other input. + setFromAmountByTo(); + } else { + setToAmountByFrom(); + } + if(toAmount.lt(0) || fromAmount.lt(0)) { + setFromAmount(Big(0)); + setToAmount(Big(0)); + setAmountFromInput("0"); + setAmountToInput("0"); + } + }, [fair, amountToInput, amountfromInput, toMint, fromMint, fromDecimals, toDecimals]); + + const setFromAmountByTo = () => { + if (fair) { + const newAmount = (Big(amountToInput || 0)); + setToAmount(newAmount); + const newFromAmount = (newAmount.times(fair).div(FEE_MULTIPLIER)).round(fromDecimals); + setFromAmount(newFromAmount); + setAmountFromInput(newFromAmount.toString()); + } + } + + const setToAmountByFrom = () => { + if (fair) { + const newAmount = (Big(amountfromInput || 0)); + setFromAmount(newAmount); + const newToAmount = (Big(FEE_MULTIPLIER).times(newAmount.div(fair))).round(toDecimals); + setToAmount(newToAmount); + setAmountToInput(newToAmount.toString()); } - setFromAmount(fromAmount); - }, [fair]); + } + const swapToFromMints = () => { const oldFrom = fromMint; const oldTo = toMint; + const oldFromAmount = fromAmount; const oldToAmount = toAmount; - _setFromAmount(oldToAmount); + const oldToDecimals = toDecimals; + const oldFromDecimals = fromDecimals; + setFromMint(oldTo); setToMint(oldFrom); - }; - - const setFromAmount = (amount: number) => { - if (fair === undefined) { - _setFromAmount(0); - _setToAmount(0); - return; - } - _setFromAmount(amount); - _setToAmount(FEE_MULTIPLIER * (amount / fair)); - }; - - const setToAmount = (amount: number) => { - if (fair === undefined) { - _setFromAmount(0); - _setToAmount(0); - return; - } - _setToAmount(amount); - _setFromAmount((amount * fair) / FEE_MULTIPLIER); + setToDecimals(oldFromDecimals); + setFromDecimals(oldToDecimals); + setFromAmount(oldToAmount); + setToAmount(oldFromAmount); + setAmountFromInput(oldToAmount.toString()); + setAmountToInput(oldFromAmount.toString()); }; return ( @@ -132,10 +173,12 @@ export function SwapContextProvider(props: any) { setFromMint, toMint, setToMint, + fromDecimals, + setFromDecimals, + toDecimals, + setToDecimals, fromAmount, - setFromAmount, toAmount, - setToAmount, swapToFromMints, slippage, setSlippage, @@ -146,6 +189,12 @@ export function SwapContextProvider(props: any) { setIsStrict, setIsClosingNewAccounts, referral, + amountfromInput, + amountToInput, + setAmountFromInput, + setAmountToInput, + inputIsToAmount, + setInputIsToAmount }} > {props.children} @@ -200,8 +249,8 @@ export function useCanSwap(): boolean { // Wallet is connected. swapClient.program.provider.wallet.publicKey !== null && // Trade amounts greater than zero. - fromAmount > 0 && - toAmount > 0 && + fromAmount.gt(0) && + toAmount.gt(0) && // Trade route exists. route !== null && // Wormhole <-> native markets must have the wormhole token as the diff --git a/yarn.lock b/yarn.lock index 8a1329fa..0d10e876 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1999,6 +1999,11 @@ dependencies: "@babel/types" "^7.3.0" +"@types/big.js@^6.1.1": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@types/big.js/-/big.js-6.1.1.tgz#c2be5e81e0cf0c1c31704e3b12f750712f647414" + integrity sha512-Zns+nT0hj96ie+GDbL5NeHxhL4wNz8QMxCHqBvxgc4x0hhgQ/o92rPwqxvPBhY3ZYnH8TJGw/8oCkjhOy2Rfzw== + "@types/bn.js@^4.11.5": version "4.11.6" resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-4.11.6.tgz#c306c70d9358aaea33cd4eda092a742b9505967c" @@ -3179,6 +3184,11 @@ big.js@^5.2.2: resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== +big.js@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-6.1.1.tgz#63b35b19dc9775c94991ee5db7694880655d5537" + integrity sha512-1vObw81a8ylZO5ePrtMay0n018TcftpTA5HFKDaSuiUDBo8biRBtjIobw60OpwuvrGk+FsxKamqN4cnmj/eXdg== + binary-extensions@^1.0.0: version "1.13.1" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"