From f1a7eea1f52504b5952871824a7585423a4a0b43 Mon Sep 17 00:00:00 2001 From: Evgeny Boxer Date: Mon, 27 Jul 2020 17:22:41 +1000 Subject: [PATCH] wip --- .../CreateOrderCard/CreateOrderCard.tsx | 274 +++++++++++------- .../components/OrderBookCard/myOrders.js | 8 + src/shared/translations/en.json | 1 + src/typings/synthetix-js.d.ts | 4 + 4 files changed, 190 insertions(+), 97 deletions(-) diff --git a/src/pages/Trade/components/CreateOrderCard/CreateOrderCard.tsx b/src/pages/Trade/components/CreateOrderCard/CreateOrderCard.tsx index 10c47b9e..0c358243 100644 --- a/src/pages/Trade/components/CreateOrderCard/CreateOrderCard.tsx +++ b/src/pages/Trade/components/CreateOrderCard/CreateOrderCard.tsx @@ -1,8 +1,7 @@ -import React, { FC, useState, useEffect, useCallback } from 'react'; +import React, { FC, useState, useEffect, useCallback, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import styled from 'styled-components'; import { useTranslation } from 'react-i18next'; -import isEmpty from 'lodash/isEmpty'; import snxJSConnector from 'utils/snxJSConnector'; @@ -11,8 +10,8 @@ import { ReactComponent as ReverseArrow } from 'assets/images/reverse-arrow.svg' import Card from 'components/Card'; import NumericInputWithCurrency from 'components/Input/NumericInputWithCurrency'; -import { getWalletInfo } from 'ducks/wallet/walletDetails'; -import { getSynthsWalletBalances } from 'ducks/wallet/walletBalances'; +import { getWalletInfo, getIsWalletConnected } from 'ducks/wallet/walletDetails'; +import { getWalletBalancesMap } from 'ducks/wallet/walletBalances'; import { getSynthPair, getAvailableSynthsMap } from 'ducks/synths'; import { getRatesExchangeRates, getEthRate } from 'ducks/rates'; import { RootState } from 'ducks/types'; @@ -39,6 +38,7 @@ import { bytesFormatter, bigNumberFormatter, secondsToTime, + getAddress, } from 'utils/formatters'; import { Button } from 'components/Button'; @@ -52,20 +52,22 @@ import { } from 'shared/commonStyles'; import NetworkInfo from './NetworkInfo'; -import { bigNumberify } from 'ethers/utils'; import { INPUT_SIZES } from 'components/Input/constants'; +import { getCurrencyKeyBalance, getCurrencyKeyUSDBalanceBN } from 'utils/balances'; +import { APPROVAL_EVENTS } from 'constants/events'; const INPUT_DEFAULT_VALUE = ''; const mapStateToProps = (state: RootState) => ({ synthPair: getSynthPair(state), walletInfo: getWalletInfo(state), - synthsWalletBalances: getSynthsWalletBalances(state), + walletBalancesMap: getWalletBalancesMap(state), exchangeRates: getRatesExchangeRates(state), gasInfo: getGasInfo(state), ethRate: getEthRate(state), transactions: getTransactions(state), synthsMap: getAvailableSynthsMap(state), + isWalletConnected: getIsWalletConnected(state), }); const mapDispatchToProps = { @@ -85,7 +87,7 @@ type OrderType = 'limit' | 'market'; const CreateOrderCard: FC = ({ synthPair, walletInfo: { currentWallet, walletType }, - synthsWalletBalances, + walletBalancesMap, exchangeRates, gasInfo, ethRate, @@ -94,9 +96,10 @@ const CreateOrderCard: FC = ({ updateTransaction, transactions, synthsMap, + isWalletConnected, }) => { const { t } = useTranslation(); - const [orderType, setOrderType] = useState('market'); + const [orderType, setOrderType] = useState('limit'); const [baseAmount, setBaseAmount] = useState(INPUT_DEFAULT_VALUE); const [quoteAmount, setQuoteAmount] = useState(INPUT_DEFAULT_VALUE); const [limitPrice, setLimitPrice] = useState(INPUT_DEFAULT_VALUE); @@ -112,6 +115,8 @@ const CreateOrderCard: FC = ({ const [feeReclamationError, setFeeReclamationError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const [hasMarketClosed, setHasMarketClosed] = useState(false); + const [hasAllowance, setAllowance] = useState(false); + const [isAllowing, setIsAllowing] = useState(false); const resetInputAmounts = () => { setBaseAmount(INPUT_DEFAULT_VALUE); @@ -135,7 +140,6 @@ const CreateOrderCard: FC = ({ setPair(synthPair); } resetInputAmounts(); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [synthPair.base.name, synthPair.quote.name, synthPair.reversed]); @@ -155,7 +159,6 @@ const CreateOrderCard: FC = ({ } }; getFeeRateForExchange(); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [base.name, quote.name]); useEffect(() => { @@ -181,13 +184,17 @@ const CreateOrderCard: FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [base.name, quote.name]); - const baseBalance = - (synthsWalletBalances && synthsWalletBalances.find((synth) => synth.name === base.name)) || 0; - const quoteBalance = - (synthsWalletBalances && synthsWalletBalances.find((synth) => synth.name === quote.name)) || 0; - console.log(baseBalance); - const rate = getExchangeRatesForCurrencies(exchangeRates, quote.name, base.name); - const inverseRate = getExchangeRatesForCurrencies(exchangeRates, base.name, quote.name); + const baseBalance = getCurrencyKeyBalance(walletBalancesMap, base.name) || 0; + + const quoteBalance = getCurrencyKeyBalance(walletBalancesMap, quote.name) || 0; + const quoteBalanceBN = getCurrencyKeyUSDBalanceBN(walletBalancesMap, quote.name) || 0; + + const rate = useMemo(() => getExchangeRatesForCurrencies(exchangeRates, quote.name, base.name), [ + quote.name, + base.name, + exchangeRates, + ]); + const inverseRate = 1 / rate; const buttonDisabled = !baseAmount || @@ -196,16 +203,15 @@ const CreateOrderCard: FC = ({ isSubmitting || feeReclamationError != null; - const isEmptyQuoteBalance = !quoteBalance || !quoteBalance.balance; + const isEmptyQuoteBalance = !quoteBalance; useEffect(() => { setInputError(null); if (!quoteAmount || !baseAmount) return; - if (currentWallet && quoteAmount > quoteBalance.balance) { + if (currentWallet && quoteAmount > quoteBalance) { setInputError(t('common.errors.amount-exceeds-balance')); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [quoteAmount, baseAmount, currentWallet, baseBalance, quoteBalance]); + }, [t, quoteAmount, baseAmount, currentWallet, baseBalance, quoteBalance]); const getMaxSecsLeftInWaitingPeriod = useCallback(async () => { if (!currentWallet) return; @@ -230,8 +236,7 @@ const CreateOrderCard: FC = ({ console.log(e); setFeeReclamationError(null); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [quote.name, currentWallet, quoteAmount]); + }, [t, quote.name, currentWallet]); useEffect(() => { getMaxSecsLeftInWaitingPeriod(); @@ -246,7 +251,7 @@ const CreateOrderCard: FC = ({ if (!quoteAmount || !quoteBalance || hasSetGasLimit) return; const amountToExchange = tradeAllBalance - ? quoteBalance.balanceBN + ? quoteBalanceBN : utils.parseEther(quoteAmount.toString()); const gasEstimate = await Synthetix.contract.estimate.exchange( @@ -265,8 +270,66 @@ const CreateOrderCard: FC = ({ const setMaxBalance = () => { if (!isEmptyQuoteBalance) { setTradeAllBalance(true); - setBaseAmount(`${Number(quoteBalance.balance) * rate}`); - setQuoteAmount(quoteBalance.balance); + setBaseAmount(`${Number(quoteBalance) * rate}`); + setQuoteAmount(quoteBalance); + } + }; + + useEffect(() => { + resetInputAmounts(); + if (isLimitOrder) { + setLimitPrice(`${inverseRate}`); + } + }, [isLimitOrder, inverseRate]); + + useEffect(() => { + const { snxJS, limitOrdersContract } = snxJSConnector; + // @ts-ignore + const synthContract = snxJS[quote.name]; + + const getAllowance = async () => { + const allowance = await synthContract.allowance(currentWallet, limitOrdersContract.address); + setAllowance(!!Number(allowance)); + }; + + const registerAllowanceListener = () => { + synthContract.contract.on(APPROVAL_EVENTS.APPROVAL, (owner: string, spender: string) => { + if (owner === currentWallet && spender === getAddress(limitOrdersContract.address)) { + setAllowance(true); + setIsAllowing(false); + } + }); + }; + if (isWalletConnected && synthContract) { + getAllowance(); + registerAllowanceListener(); + } + return () => { + if (synthContract) { + synthContract.contract.removeAllListeners(APPROVAL_EVENTS.APPROVAL); + } + }; + }, [currentWallet, isWalletConnected, quote.name]); + + const handleAllowance = async () => { + const { snxJS, limitOrdersContract } = snxJSConnector; + // @ts-ignore + const synthContract = snxJS[quote.name]; + + try { + setIsAllowing(true); + const maxInt = `0x${'f'.repeat(64)}`; + const gasEstimate = await synthContract.contract.estimate.approve( + limitOrdersContract.address, + maxInt + ); + await synthContract.approve(limitOrdersContract.address, maxInt, { + gasLimit: normalizeGasLimit(Number(gasEstimate)), + gasPrice: gasInfo.gasPrice * GWEI_UNIT, + }); + } catch (e) { + console.log(e); + setIsAllowing(false); } }; @@ -281,9 +344,35 @@ const CreateOrderCard: FC = ({ const transactionId = transactions.length; setTxErrorMessage(null); setIsSubmitting(true); + try { + const usdRate = getExchangeRatesForCurrencies(exchangeRates, base.name, SYNTHS_MAP.sUSD); + + const txProps = { + id: transactionId, + date: new Date(), + base: base.name, + quote: quote.name, + fromAmount: quoteAmount, + toAmount: baseAmount, + price: + orderType === 'market' + ? base.name === SYNTHS_MAP.sUSD + ? rate + : inverseRate + : limitPrice, + amount: formatCurrency(baseAmount), + priceUSD: + base.name === SYNTHS_MAP.sUSD + ? getExchangeRatesForCurrencies(exchangeRates, quote.name, SYNTHS_MAP.sUSD) + : usdRate, + totalUSD: formatCurrency(Number(baseAmount) * usdRate), + status: TRANSACTION_STATUS.WAITING, + orderType, + }; + const amountToExchange = tradeAllBalance - ? quoteBalance.balanceBN + ? quoteBalanceBN : utils.parseEther(quoteAmount.toString()); if (orderType === 'market') { @@ -296,28 +385,7 @@ const CreateOrderCard: FC = ({ setGasLimit(rectifiedGasLimit); - createTransaction({ - id: transactionId, - date: new Date(), - base: base.name, - quote: quote.name, - fromAmount: quoteAmount, - toAmount: baseAmount, - price: - base.name === SYNTHS_MAP.sUSD - ? getExchangeRatesForCurrencies(exchangeRates, quote.name, base.name) - : getExchangeRatesForCurrencies(exchangeRates, base.name, quote.name), - amount: formatCurrency(baseAmount), - priceUSD: - base.name === SYNTHS_MAP.sUSD - ? getExchangeRatesForCurrencies(exchangeRates, quote.name, SYNTHS_MAP.sUSD) - : getExchangeRatesForCurrencies(exchangeRates, base.name, SYNTHS_MAP.sUSD), - totalUSD: formatCurrency( - Number(baseAmount) * - getExchangeRatesForCurrencies(exchangeRates, base.name, SYNTHS_MAP.sUSD) - ), - status: TRANSACTION_STATUS.WAITING, - }); + createTransaction(txProps); const tx = await Synthetix.exchange( bytesFormatter(quote.name), @@ -331,42 +399,36 @@ const CreateOrderCard: FC = ({ updateTransaction({ status: TRANSACTION_STATUS.PENDING, ...tx }, transactionId); } else { - console.log( + const executionFee = utils.parseEther('0'); + const weiDeposit = gasInfo.gasSpeed.fast; + + const gasEstimate = await limitOrdersContractWithSigner.estimate.newOrder( bytesFormatter(quote.name), - quoteAmount.toString(), + amountToExchange, bytesFormatter(base.name), - quoteAmount.toString(), - '1', + utils.parseEther(`${Number(limitPrice) * Number(quoteAmount)}`), + executionFee, { - value: '1', + value: weiDeposit, } ); - // add typings - /* - { - newOrder: ( - sourceCurrencyKey: string, - sourceAmount: string, - destinationCurrencyKey: string, - minDestinationAmount: string, - executionFee: string, - gas: { value: string } - ) => Promise; - } - */ + + createTransaction(txProps); + const tx = await limitOrdersContractWithSigner.newOrder( bytesFormatter(quote.name), amountToExchange, bytesFormatter(base.name), - limitPrice, - bigNumberify(1), + utils.parseEther(`${Number(limitPrice) * Number(quoteAmount)}`), + executionFee, { - value: bigNumberify(1), + value: gasInfo.gasSpeed.fast, gasPrice: gasInfo.gasPrice * GWEI_UNIT, - gasLimit: 500000, + gasLimit: normalizeGasLimit(Number(gasEstimate)), } ); - console.log(tx); + + updateTransaction({ status: TRANSACTION_STATUS.PENDING, ...tx }, transactionId); } setIsSubmitting(false); } catch (e) { @@ -384,7 +446,6 @@ const CreateOrderCard: FC = ({ setIsSubmitting(false); } }; - return ( @@ -409,8 +470,8 @@ const CreateOrderCard: FC = ({ > {t('common.wallet.balance-currency', { balance: quoteBalance - ? formatCurrency(quoteBalance.balance) - : !isEmpty(synthsWalletBalances) + ? formatCurrency(quoteBalance) + : walletBalancesMap != null ? 0 : EMPTY_VALUE, })} @@ -419,8 +480,8 @@ const CreateOrderCard: FC = ({ } onChange={(_, value) => { setTradeAllBalance(false); - setBaseAmount(`${Number(value) * rate}`); setQuoteAmount(value); + setBaseAmount(`${Number(value) * (isMarketOrder ? rate : 1 / Number(limitPrice))}`); }} errorMessage={inputError} /> @@ -437,7 +498,7 @@ const CreateOrderCard: FC = ({ disabled={isEmptyQuoteBalance} key={`button-fraction-${id}`} onClick={() => { - const balance = quoteBalance.balance; + const balance = quoteBalance; const isWholeBalance = fraction === 100; const amount = isWholeBalance ? balance : (balance * fraction) / 100; setTradeAllBalance(isWholeBalance); @@ -450,6 +511,23 @@ const CreateOrderCard: FC = ({ ))} )} + {isLimitOrder && ( + <> + + {t('common.price-label')}} + onChange={(_, value) => { + setLimitPrice(value); + if (baseAmount) { + setBaseAmount(`${Number(quoteAmount) * (1 / Number(value))}`); + } + }} + /> + + + )} = ({ <> {t('trade.trade-card.buy-input-label')}: - - - + {isLimitOrder && ( + + + + )} = ({ > {t('common.wallet.balance-currency', { balance: baseBalance - ? formatCurrency(baseBalance.balance) - : !isEmpty(synthsWalletBalances) + ? formatCurrency(baseBalance) + : walletBalancesMap != null ? 0 : EMPTY_VALUE, })} @@ -478,25 +558,13 @@ const CreateOrderCard: FC = ({ } onChange={(_, value) => { setTradeAllBalance(false); - setQuoteAmount(`${Number(value) * inverseRate}`); setBaseAmount(value); + setQuoteAmount( + `${Number(value) * (isMarketOrder ? inverseRate : Number(limitPrice))}` + ); }} /> - {isLimitOrder && ( - <> - - {t('common.price-label')}} - onChange={(_, value) => { - setLimitPrice(value); - }} - /> - - - )} = ({ ) : synthsMap[base.name].isFrozen ? ( {t('trade.trade-card.frozen-synth')} + ) : isLimitOrder ? ( + hasAllowance ? ( + + {t('trade.trade-card.confirm-trade-button')} + + ) : ( + + {!isAllowing + ? t('common.enable-wallet-access.label') + : t('common.enable-wallet-access.progress-label')} + + ) ) : ( {t('trade.trade-card.confirm-trade-button')} diff --git a/src/pages/Trade/components/OrderBookCard/myOrders.js b/src/pages/Trade/components/OrderBookCard/myOrders.js index 2f37967c..9fa01e81 100644 --- a/src/pages/Trade/components/OrderBookCard/myOrders.js +++ b/src/pages/Trade/components/OrderBookCard/myOrders.js @@ -71,6 +71,14 @@ const MyOrders = ({ transactions, networkId, synthsMap }) => { ), sortable: true, }, + { + Header: <>{t('trade.order-book-card.table.type')}, + accessor: 'orderType', + Cell: (cellProps) => ( + {cellProps.cell.value} + ), + sortable: true, + }, { Header: <>{t('trade.order-book-card.table.buying')}, accessor: 'toAmount', diff --git a/src/shared/translations/en.json b/src/shared/translations/en.json index dee64546..77dee5e5 100644 --- a/src/shared/translations/en.json +++ b/src/shared/translations/en.json @@ -448,6 +448,7 @@ "table": { "date": "Date | time", "pair": "Pair", + "type": "Type", "buying": "Buying", "selling": "Selling", "price": "Price", diff --git a/src/typings/synthetix-js.d.ts b/src/typings/synthetix-js.d.ts index 2072c17c..637b84e0 100644 --- a/src/typings/synthetix-js.d.ts +++ b/src/typings/synthetix-js.d.ts @@ -70,5 +70,9 @@ declare module 'synthetix-js' { } ) => Promise; }; + sUSD: { + contract: any; + approve: any; + }; } }