diff --git a/packages/react-app/src/App.js b/packages/react-app/src/App.js index 42d47f3..4b7e7a2 100644 --- a/packages/react-app/src/App.js +++ b/packages/react-app/src/App.js @@ -1,8 +1,8 @@ import React from "react" import { useEthers } from "@usedapp/core"; -import { usePools } from "./hooks"; import styles from './styles'; +import { usePools } from "./hooks"; import { fhtLogo } from './assets'; import { Exchange, Loader, WalletButton } from "./components"; diff --git a/packages/react-app/src/components/AmountIn.js b/packages/react-app/src/components/AmountIn.js new file mode 100644 index 0000000..d35be5c --- /dev/null +++ b/packages/react-app/src/components/AmountIn.js @@ -0,0 +1,67 @@ +import React, { useState, useEffect, useRef } from "react"; + +import { chevronDown } from "../assets"; +import { useOnClickOutside } from "../utils"; +import styles from "../styles"; + +const AmountIn = ({ value, onChange, currencyValue, onSelect, currencies, isSwapping }) => { + const [showList, setShowList] = useState(false); + const [activeCurrency, setActiveCurrency] = useState("Select"); + const ref = useRef() + + useOnClickOutside(ref, () => setShowList(false)) + + useEffect(() => { + if (Object.keys(currencies).includes(currencyValue)) + setActiveCurrency(currencies[currencyValue]); + else setActiveCurrency("Select"); + }, [currencies, currencyValue]); + + return ( +
+ typeof onChange === "function" && onChange(e.target.value)} + className={styles.amountInput} + /> + +
setShowList(!showList)}> + + + {showList && ( + + )} +
+
+ ); +}; + +export default AmountIn; diff --git a/packages/react-app/src/components/AmountOut.js b/packages/react-app/src/components/AmountOut.js new file mode 100644 index 0000000..6fdf1bb --- /dev/null +++ b/packages/react-app/src/components/AmountOut.js @@ -0,0 +1,67 @@ +import React, { useState, useEffect, useRef } from "react"; +import { formatUnits } from "ethers/lib/utils"; + +import { chevronDown } from "../assets"; +import { useAmountsOut, useOnClickOutside } from "../utils"; +import styles from "../styles"; + +const AmountOut = ({ fromToken, toToken, amountIn, pairContract, currencyValue, onSelect, currencies }) => { + const [showList, setShowList] = useState(false); + const [activeCurrency, setActiveCurrency] = useState("Select"); + const ref = useRef() + + const amountOut = useAmountsOut(pairContract, amountIn, fromToken, toToken) ?? 0; + + useOnClickOutside(ref, () => setShowList(false)) + + useEffect(() => { + if (Object.keys(currencies).includes(currencyValue)) { + setActiveCurrency(currencies[currencyValue]); + } else { + setActiveCurrency("Select") + } + }, [currencyValue, currencies]); + + return ( +
+ + +
setShowList(!showList)}> + + + {showList && ( + + )} +
+
+ ); +}; + +export default AmountOut; diff --git a/packages/react-app/src/components/Balance.js b/packages/react-app/src/components/Balance.js new file mode 100644 index 0000000..dacba32 --- /dev/null +++ b/packages/react-app/src/components/Balance.js @@ -0,0 +1,24 @@ +import React from "react"; +import { formatUnits, parseUnits } from "ethers/lib/utils"; + +import styles from "../styles"; + +const Balance = ({ tokenBalance }) => { + + return ( +
+

+ {tokenBalance ? ( + <> + Balance: + {formatUnits(tokenBalance ?? parseUnits("0"))} + + ) : ( + "" + )} +

+
+ ); +}; + +export default Balance; diff --git a/packages/react-app/src/components/Exchange.js b/packages/react-app/src/components/Exchange.js index 07da0ba..548218d 100644 --- a/packages/react-app/src/components/Exchange.js +++ b/packages/react-app/src/components/Exchange.js @@ -5,12 +5,163 @@ import { ERC20, useContractFunction, useEthers, useTokenAllowance, useTokenBalan import { ethers } from "ethers"; import { parseUnits } from "ethers/lib/utils"; +import { getAvailableTokens, getCounterpartTokens, findPoolByTokens, isOperationPending, getFailureMessage, getSuccessMessage } from '../utils'; import { ROUTER_ADDRESS } from "../config"; +import AmountIn from "./AmountIn"; +import AmountOut from "./AmountOut"; +import Balance from "./Balance"; +import styles from "../styles"; const Exchange = ({ pools }) => { - return ( -
Exchange
- ) -} + const { account } = useEthers(); + const [fromValue, setFromValue] = useState("0"); + const [fromToken, setFromToken] = useState(pools[0].token0Address); // initialFromToken + const [toToken, setToToken] = useState(""); + const [resetState, setResetState] = useState(false) -export default Exchange; \ No newline at end of file + const fromValueBigNumber = parseUnits(fromValue || "0"); // converse the string to bigNumber + const availableTokens = getAvailableTokens(pools); + const counterpartTokens = getCounterpartTokens(pools, fromToken); + const pairAddress = findPoolByTokens(pools, fromToken, toToken)?.address ?? ""; + + const routerContract = new Contract(ROUTER_ADDRESS, abis.router02); + const fromTokenContract = new Contract(fromToken, ERC20.abi); + const fromTokenBalance = useTokenBalance(fromToken, account); + const toTokenBalance = useTokenBalance(toToken, account); + const tokenAllowance = useTokenAllowance(fromToken, account, ROUTER_ADDRESS) || parseUnits("0"); + const approvedNeeded = fromValueBigNumber.gt(tokenAllowance); + const formValueIsGreaterThan0 = fromValueBigNumber.gt(parseUnits("0")); + const hasEnoughBalance = fromValueBigNumber.lte(fromTokenBalance ?? parseUnits("0")); + + // approve initiating a contract call (similar to use state) -> gives the state and the sender... + const { state: swapApproveState, send: swapApproveSend } = + useContractFunction(fromTokenContract, "approve", { + transactionName: "onApproveRequested", + gasLimitBufferPercentage: 10, + }); + // swap initiating a contract call (similar to use state) -> gives the state and the sender... + const { state: swapExecuteState, send: swapExecuteSend } = + useContractFunction(routerContract, "swapExactTokensForTokens", { + transactionName: "swapExactTokensForTokens", + gasLimitBufferPercentage: 10, + }); + + const isApproving = isOperationPending(swapApproveState); + const isSwapping = isOperationPending(swapExecuteState); + const canApprove = !isApproving && approvedNeeded; + const canSwap = !approvedNeeded && !isSwapping && formValueIsGreaterThan0 && hasEnoughBalance; + + const successMessage = getSuccessMessage(swapApproveState, swapExecuteState); + const failureMessage = getFailureMessage(swapApproveState, swapExecuteState); + + const onApproveRequested = () => { + swapApproveSend(ROUTER_ADDRESS, ethers.constants.MaxUint256); + }; + + // https://docs.uniswap.org/protocol/V2/reference/smart-contracts/router-02#swapexacttokensfortokens + const onSwapRequested = () => { + swapExecuteSend( + fromValueBigNumber, + 0, + [fromToken, toToken], + account, + Math.floor(Date.now() / 1000) + 60 * 20 + ).then((_) => { + setFromValue("0"); + }); + }; + + const onFromValueChange = (value) => { + const trimmedValue = value.trim(); + + try { + trimmedValue && parseUnits(value); + setFromValue(value); + } catch (e) {} + }; + + const onFromTokenChange = (value) => { + setFromToken(value); + }; + + const onToTokenChange = (value) => { + setToToken(value); + }; + + useEffect(() => { + if(failureMessage || successMessage) { + setTimeout(() => { + setResetState(true) + setFromValue("0") + setToToken("") + }, 5000) + } + }, [failureMessage, successMessage]) + + return ( +
+
+ + +
+ +
+ + +
+ + {approvedNeeded && !isSwapping ? ( + + ) : ( + + )} + + {failureMessage && !resetState ? ( +

{failureMessage}

+ ) : successMessage ? ( +

{successMessage}

+ ) : ( + "" + )} +
+ ); +}; + +export default Exchange; diff --git a/packages/react-app/src/components/Loader.js b/packages/react-app/src/components/Loader.js index a41289d..b195bc5 100644 --- a/packages/react-app/src/components/Loader.js +++ b/packages/react-app/src/components/Loader.js @@ -6,7 +6,11 @@ import {ethereumLogo2 } from "../assets"; const Loader = ({ title }) => { return (
- Ethereum Logo + Ethereum Logo

{title}

diff --git a/packages/react-app/src/components/index.js b/packages/react-app/src/components/index.js index de594b8..7d6764c 100644 --- a/packages/react-app/src/components/index.js +++ b/packages/react-app/src/components/index.js @@ -1,3 +1,6 @@ export {default as Loader} from './Loader'; export {default as Exchange} from './Exchange'; -export {default as WalletButton} from './WalletButton'; \ No newline at end of file +export {default as WalletButton} from './WalletButton'; +export { default as AmountIn } from "./AmountIn"; +export { default as AmountOut } from "./AmountOut"; +export { default as Balance } from "./Balance"; \ No newline at end of file