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"