From 0f91af1df26ef9456e5745557276fe8ad9118b50 Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Thu, 6 Aug 2020 18:18:43 -0500 Subject: [PATCH] improvement(swap): Better swap errors for FoT (#1015) * move the gas estimation stuff into its own hook and report errors from the gas estimation * fix linter errors * show the swap callback error separately * rename some variables * use a manually specified key for gas estimates * flip price... thought i did this already * only show swap callback error if approval state is approved * some clean up to the swap components * stop proactively looking for gas estimates * improve some retry stuff, show errors inline * add another retry test * latest ethers * fix integration tests * simplify modal and fix jitter on open in mobile * refactor confirmation modal into pieces before creating the error content * finish refactoring of transaction confirmation modal * show error state in the transaction confirmation modal * fix lint errors * error not always relevant * fix lint errors, remove action item * move a lot of code into ConfirmSwapModal.tsx * show accept changes flow, not styled * Adjust styles for slippage error states * Add styles for updated price prompt * Add input/output highlighting * lint errors * fix link to wallets in modal * use total supply instead of reserves for `noLiquidity` (fixes #701) * bump the walletconnect version to the fixed alpha Co-authored-by: Callil Capuozzo --- package.json | 20 +- src/components/AccountDetails/index.tsx | 9 +- src/components/Button/index.tsx | 2 + src/components/ConfirmationModal/index.tsx | 133 -------- src/components/Modal/index.tsx | 129 ++----- src/components/Popups/PopupItem.tsx | 4 +- .../{TxnPopup.tsx => TransactionPopup.tsx} | 20 +- src/components/Popups/index.tsx | 25 +- .../TransactionConfirmationModal/index.tsx | 195 +++++++++++ .../tsconfig.json | 4 + src/components/WalletModal/index.tsx | 4 +- src/components/swap/AdvancedSwapDetails.tsx | 30 +- .../swap/AdvancedSwapDetailsDropdown.tsx | 6 +- src/components/swap/ConfirmSwapModal.tsx | 109 ++++++ src/components/swap/FormattedPriceImpact.tsx | 5 +- src/components/swap/SwapModalFooter.tsx | 82 ++--- src/components/swap/SwapModalHeader.tsx | 105 ++++-- .../swap/confirmPriceImpactWithoutFee.ts | 6 +- .../swap/{styleds.ts => styleds.tsx} | 71 ++-- src/components/swap/tsconfig.json | 4 + src/connectors/NetworkConnector.ts | 94 ++++- src/constants/index.ts | 112 +++--- src/data/V1.ts | 2 +- src/hooks/useLast.ts | 30 +- src/hooks/useSwapCallback.ts | 274 +++++++++------ src/hooks/useWrapCallback.ts | 6 +- src/pages/AddLiquidity/PoolPriceBar.tsx | 12 +- src/pages/AddLiquidity/index.tsx | 33 +- src/pages/RemoveLiquidity/index.tsx | 40 ++- src/pages/Swap/index.tsx | 200 ++++++----- src/state/mint/hooks.ts | 21 +- src/state/multicall/updater.tsx | 99 ++++-- src/state/swap/hooks.ts | 16 +- src/theme/index.tsx | 2 +- src/utils/index.ts | 2 +- src/utils/isZero.ts | 7 + src/utils/retry.test.ts | 33 +- src/utils/retry.ts | 56 ++- yarn.lock | 322 +++++------------- 39 files changed, 1336 insertions(+), 988 deletions(-) delete mode 100644 src/components/ConfirmationModal/index.tsx rename src/components/Popups/{TxnPopup.tsx => TransactionPopup.tsx} (76%) create mode 100644 src/components/TransactionConfirmationModal/index.tsx create mode 100644 src/components/TransactionConfirmationModal/tsconfig.json create mode 100644 src/components/swap/ConfirmSwapModal.tsx rename src/components/swap/{styleds.ts => styleds.tsx} (59%) create mode 100644 src/components/swap/tsconfig.json create mode 100644 src/utils/isZero.ts diff --git a/package.json b/package.json index 1691aa0c06a..c09a9a86063 100644 --- a/package.json +++ b/package.json @@ -4,17 +4,7 @@ "homepage": ".", "private": true, "devDependencies": { - "@ethersproject/address": "5.0.0-beta.134", - "@ethersproject/bignumber": "5.0.0-beta.138", - "@ethersproject/constants": "5.0.0-beta.133", - "@ethersproject/contracts": "5.0.0-beta.151", - "@ethersproject/experimental": "5.0.0-beta.141", - "@ethersproject/networks": "5.0.0-beta.136", - "@ethersproject/providers": "5.0.0-beta.162", - "@ethersproject/solidity": "5.0.2", - "@ethersproject/strings": "5.0.0-beta.136", - "@ethersproject/units": "5.0.0-beta.132", - "@ethersproject/wallet": "5.0.0-beta.141", + "@ethersproject/experimental": "^5.0.1", "@popperjs/core": "^2.4.4", "@reach/dialog": "^0.10.3", "@reach/portal": "^0.10.3", @@ -52,6 +42,7 @@ "eslint-plugin-prettier": "^3.1.3", "eslint-plugin-react": "^7.19.0", "eslint-plugin-react-hooks": "^4.0.0", + "ethers": "^5.0.7", "i18next": "^15.0.9", "i18next-browser-languagedetector": "^3.0.1", "i18next-xhr-backend": "^2.0.1", @@ -60,7 +51,6 @@ "lodash.flatmap": "^4.5.0", "polished": "^3.3.2", "prettier": "^1.17.0", - "qrcode.react": "^0.9.3", "qs": "^6.9.4", "react": "^16.13.1", "react-device-detect": "^1.6.2", @@ -80,8 +70,10 @@ "serve": "^11.3.0", "start-server-and-test": "^1.11.0", "styled-components": "^4.2.0", - "typescript": "^3.8.3", - "use-media": "^1.4.0" + "typescript": "^3.8.3" + }, + "resolutions": { + "@walletconnect/web3-provider": "1.1.1-alpha.0" }, "scripts": { "start": "react-scripts start", diff --git a/src/components/AccountDetails/index.tsx b/src/components/AccountDetails/index.tsx index d64df155333..82caecdd2ff 100644 --- a/src/components/AccountDetails/index.tsx +++ b/src/components/AccountDetails/index.tsx @@ -251,26 +251,26 @@ export default function AccountDetails({ } else if (connector === walletconnect) { return ( - {''} + {'wallet ) } else if (connector === walletlink) { return ( - {''} + {'coinbase ) } else if (connector === fortmatic) { return ( - {''} + {'fortmatic ) } else if (connector === portis) { return ( <> - {''} + {'portis { portis.portis.showPortis() @@ -382,7 +382,6 @@ export default function AccountDetails({ )} - {/* {formatConnectorName()} */} diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 2a14966df1d..6d1e8ce3556 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -27,6 +27,8 @@ const Base = styled(RebassButton)<{ flex-wrap: nowrap; align-items: center; cursor: pointer; + position: relative; + z-index: 1; &:disabled { cursor: auto; } diff --git a/src/components/ConfirmationModal/index.tsx b/src/components/ConfirmationModal/index.tsx deleted file mode 100644 index b17e48398cc..00000000000 --- a/src/components/ConfirmationModal/index.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import React, { useContext } from 'react' -import styled, { ThemeContext } from 'styled-components' -import Modal from '../Modal' -import { ExternalLink } from '../../theme' -import { Text } from 'rebass' -import { CloseIcon, Spinner } from '../../theme/components' -import { RowBetween } from '../Row' -import { ArrowUpCircle } from 'react-feather' -import { ButtonPrimary } from '../Button' -import { AutoColumn, ColumnCenter } from '../Column' -import Circle from '../../assets/images/blue-loader.svg' - -import { getEtherscanLink } from '../../utils' -import { useActiveWeb3React } from '../../hooks' - -const Wrapper = styled.div` - width: 100%; -` -const Section = styled(AutoColumn)` - padding: 24px; -` - -const BottomSection = styled(Section)` - background-color: ${({ theme }) => theme.bg2}; - border-bottom-left-radius: 20px; - border-bottom-right-radius: 20px; -` - -const ConfirmedIcon = styled(ColumnCenter)` - padding: 60px 0; -` - -const CustomLightSpinner = styled(Spinner)<{ size: string }>` - height: ${({ size }) => size}; - width: ${({ size }) => size}; -` - -interface ConfirmationModalProps { - isOpen: boolean - onDismiss: () => void - hash: string - topContent: () => React.ReactChild - bottomContent: () => React.ReactChild - attemptingTxn: boolean - pendingText: string - title?: string -} - -export default function ConfirmationModal({ - isOpen, - onDismiss, - topContent, - bottomContent, - attemptingTxn, - hash, - pendingText, - title = '' -}: ConfirmationModalProps) { - const { chainId } = useActiveWeb3React() - const theme = useContext(ThemeContext) - - const transactionBroadcast = !!hash - - // waiting for user to confirm/reject tx _or_ showing info on a tx that has been broadcast - if (attemptingTxn || transactionBroadcast) { - return ( - - -
- -
- - - - {transactionBroadcast ? ( - - ) : ( - - )} - - - - {transactionBroadcast ? 'Transaction Submitted' : 'Waiting For Confirmation'} - - - - {pendingText} - - - - {transactionBroadcast ? ( - <> - - - View on Etherscan - - - - - Close - - - - ) : ( - - Confirm this transaction in your wallet - - )} - -
-
-
- ) - } - - // confirmation screen - return ( - - -
- - - {title} - - - - {topContent()} -
- {bottomContent()} -
-
- ) -} diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx index b76ac1b508d..bf214756607 100644 --- a/src/components/Modal/index.tsx +++ b/src/components/Modal/index.tsx @@ -1,8 +1,6 @@ import React from 'react' import styled, { css } from 'styled-components' import { animated, useTransition, useSpring } from 'react-spring' -import { Spring } from 'react-spring/renderprops' - import { DialogOverlay, DialogContent } from '@reach/dialog' import { isMobile } from 'react-device-detect' import '@reach/dialog/styles.css' @@ -11,39 +9,25 @@ import { useGesture } from 'react-use-gesture' const AnimatedDialogOverlay = animated(DialogOverlay) // eslint-disable-next-line @typescript-eslint/no-unused-vars -const StyledDialogOverlay = styled(({ mobile, ...rest }) => )<{ mobile: boolean }>` +const StyledDialogOverlay = styled(AnimatedDialogOverlay)` &[data-reach-dialog-overlay] { z-index: 2; - display: flex; - align-items: center; - justify-content: center; background-color: transparent; overflow: hidden; - ${({ mobile }) => - mobile && - css` - align-items: flex-end; - `} + display: flex; + align-items: center; + justify-content: center; - &::after { - content: ''; - background-color: ${({ theme }) => theme.modalBG}; - opacity: 0.5; - top: 0; - left: 0; - bottom: 0; - right: 0; - position: fixed; - z-index: -1; - } + background-color: ${({ theme }) => theme.modalBG}; } ` +const AnimatedDialogContent = animated(DialogContent) // destructure to not pass custom props to Dialog DOM element // eslint-disable-next-line @typescript-eslint/no-unused-vars const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...rest }) => ( - + )).attrs({ 'aria-label': 'dialog' })` @@ -55,6 +39,8 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...r padding: 0px; width: 50vw; + align-self: ${({ mobile }) => (mobile ? 'flex-end' : 'center')}; + max-width: 420px; ${({ maxHeight }) => maxHeight && @@ -102,7 +88,7 @@ export default function Modal({ initialFocusRef = null, children }: ModalProps) { - const transitions = useTransition(isOpen, null, { + const fadeTransition = useTransition(isOpen, null, { config: { duration: 200 }, from: { opacity: 0 }, enter: { opacity: 1 }, @@ -115,80 +101,37 @@ export default function Modal({ set({ y: state.down ? state.movement[1] : 0 }) - if (state.velocity > 3 && state.direction[1] > 0) { + if (state.movement[1] > 300 || (state.velocity > 3 && state.direction[1] > 0)) { onDismiss() } } }) - if (isMobile) { - return ( - <> - {transitions.map( - ({ item, key, props }) => - item && ( - + {fadeTransition.map( + ({ item, key, props }) => + item && ( + + `translateY(${y > 0 ? y : 0}px)`) } + } + : {})} + aria-label="dialog content" + minHeight={minHeight} + maxHeight={maxHeight} + mobile={isMobile} > {/* prevents the automatic focusing of inputs on mobile by the reach dialog */} - {initialFocusRef ? null :
} - - {props => ( - `translateY(${y > 0 ? y : 0}px)`) - }} - > - - - )} - - - ) - )} - - ) - } else { - return ( - <> - {transitions.map( - ({ item, key, props }) => - item && ( - - - - ) - )} - - ) - } + {!initialFocusRef && isMobile ?
: null} + {children} + + + ) + )} + + ) } diff --git a/src/components/Popups/PopupItem.tsx b/src/components/Popups/PopupItem.tsx index 6d6f5137377..f7f0436052a 100644 --- a/src/components/Popups/PopupItem.tsx +++ b/src/components/Popups/PopupItem.tsx @@ -5,7 +5,7 @@ import useInterval from '../../hooks/useInterval' import { PopupContent } from '../../state/application/actions' import { useRemovePopup } from '../../state/application/hooks' import ListUpdatePopup from './ListUpdatePopup' -import TxnPopup from './TxnPopup' +import TransactionPopup from './TransactionPopup' export const StyledClose = styled(X)` position: absolute; @@ -68,7 +68,7 @@ export default function PopupItem({ content, popKey }: { content: PopupContent; const { txn: { hash, success, summary } } = content - popupContent = + popupContent = } else if ('listUpdate' in content) { const { listUpdate: { listUrl, oldList, newList, auto } diff --git a/src/components/Popups/TxnPopup.tsx b/src/components/Popups/TransactionPopup.tsx similarity index 76% rename from src/components/Popups/TxnPopup.tsx rename to src/components/Popups/TransactionPopup.tsx index 4cde7d290a1..53495a8da9b 100644 --- a/src/components/Popups/TxnPopup.tsx +++ b/src/components/Popups/TransactionPopup.tsx @@ -1,6 +1,6 @@ import React, { useContext } from 'react' import { AlertCircle, CheckCircle } from 'react-feather' -import { ThemeContext } from 'styled-components' +import styled, { ThemeContext } from 'styled-components' import { useActiveWeb3React } from '../../hooks' import { TYPE } from '../../theme' import { ExternalLink } from '../../theme/components' @@ -8,13 +8,25 @@ import { getEtherscanLink } from '../../utils' import { AutoColumn } from '../Column' import { AutoRow } from '../Row' -export default function TxnPopup({ hash, success, summary }: { hash: string; success?: boolean; summary?: string }) { +const RowNoFlex = styled(AutoRow)` + flex-wrap: nowrap; +` + +export default function TransactionPopup({ + hash, + success, + summary +}: { + hash: string + success?: boolean + summary?: string +}) { const { chainId } = useActiveWeb3React() const theme = useContext(ThemeContext) return ( - +
{success ? : }
@@ -22,6 +34,6 @@ export default function TxnPopup({ hash, success, summary }: { hash: string; suc {summary ?? 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)} View on Etherscan -
+ ) } diff --git a/src/components/Popups/index.tsx b/src/components/Popups/index.tsx index 58f68f82607..25528ccfde7 100644 --- a/src/components/Popups/index.tsx +++ b/src/components/Popups/index.tsx @@ -1,6 +1,5 @@ import React from 'react' import styled from 'styled-components' -import { useMediaLayout } from 'use-media' import { useActivePopups } from '../../state/application/hooks' import { AutoColumn } from '../Column' import PopupItem from './PopupItem' @@ -11,6 +10,11 @@ const MobilePopupWrapper = styled.div<{ height: string | number }>` height: ${({ height }) => height}; margin: ${({ height }) => (height ? '0 auto;' : 0)}; margin-bottom: ${({ height }) => (height ? '20px' : 0)}}; + + display: none; + ${({ theme }) => theme.mediaWidth.upToSmall` + display: block; + `}; ` const MobilePopupInner = styled.div` @@ -26,8 +30,8 @@ const MobilePopupInner = styled.div` ` const FixedPopupColumn = styled(AutoColumn)` - position: absolute; - top: 112px; + position: fixed; + top: 64px; right: 1rem; max-width: 355px !important; width: 100%; @@ -41,21 +45,13 @@ export default function Popups() { // get all popups const activePopups = useActivePopups() - // switch view settings on mobile - const isMobile = useMediaLayout({ maxWidth: '600px' }) - - if (!isMobile) { - return ( + return ( + <> {activePopups.map(item => ( ))} - ) - } - //mobile - else - return ( 0 ? 'fit-content' : 0}> {activePopups // reverse so new items up front @@ -66,5 +62,6 @@ export default function Popups() { ))} - ) + + ) } diff --git a/src/components/TransactionConfirmationModal/index.tsx b/src/components/TransactionConfirmationModal/index.tsx new file mode 100644 index 00000000000..dcfb77f6f0f --- /dev/null +++ b/src/components/TransactionConfirmationModal/index.tsx @@ -0,0 +1,195 @@ +import { ChainId } from '@uniswap/sdk' +import React, { useContext } from 'react' +import styled, { ThemeContext } from 'styled-components' +import Modal from '../Modal' +import { ExternalLink } from '../../theme' +import { Text } from 'rebass' +import { CloseIcon, Spinner } from '../../theme/components' +import { RowBetween } from '../Row' +import { AlertTriangle, ArrowUpCircle } from 'react-feather' +import { ButtonPrimary } from '../Button' +import { AutoColumn, ColumnCenter } from '../Column' +import Circle from '../../assets/images/blue-loader.svg' + +import { getEtherscanLink } from '../../utils' +import { useActiveWeb3React } from '../../hooks' + +const Wrapper = styled.div` + width: 100%; +` +const Section = styled(AutoColumn)` + padding: 24px; +` + +const BottomSection = styled(Section)` + background-color: ${({ theme }) => theme.bg2}; + border-bottom-left-radius: 20px; + border-bottom-right-radius: 20px; +` + +const ConfirmedIcon = styled(ColumnCenter)` + padding: 60px 0; +` + +const CustomLightSpinner = styled(Spinner)<{ size: string }>` + height: ${({ size }) => size}; + width: ${({ size }) => size}; +` + +function ConfirmationPendingContent({ onDismiss, pendingText }: { onDismiss: () => void; pendingText: string }) { + return ( + +
+ +
+ + + + + + + + Waiting For Confirmation + + + + {pendingText} + + + + Confirm this transaction in your wallet + + +
+
+ ) +} + +function TransactionSubmittedContent({ + onDismiss, + chainId, + hash +}: { + onDismiss: () => void + hash: string | undefined + chainId: ChainId +}) { + const theme = useContext(ThemeContext) + + return ( + +
+ +
+ + + + + + + + Transaction Submitted + + + + + View on Etherscan + + + + + Close + + + +
+
+ ) +} + +export function ConfirmationModalContent({ + title, + bottomContent, + onDismiss, + topContent +}: { + title: string + onDismiss: () => void + topContent: () => React.ReactNode + bottomContent: () => React.ReactNode +}) { + return ( + +
+ + + {title} + + + + {topContent()} +
+ {bottomContent()} +
+ ) +} + +export function TransactionErrorContent({ message, onDismiss }: { message: string; onDismiss: () => void }) { + const theme = useContext(ThemeContext) + return ( + +
+ + + Error + + + + + + + {message} + + +
+ + Dismiss + +
+ ) +} + +interface ConfirmationModalProps { + isOpen: boolean + onDismiss: () => void + hash: string | undefined + content: () => React.ReactNode + attemptingTxn: boolean + pendingText: string +} + +export default function TransactionConfirmationModal({ + isOpen, + onDismiss, + attemptingTxn, + hash, + pendingText, + content +}: ConfirmationModalProps) { + const { chainId } = useActiveWeb3React() + + if (!chainId) return null + + // confirmation screen + return ( + + {attemptingTxn ? ( + + ) : hash ? ( + + ) : ( + content() + )} + + ) +} diff --git a/src/components/TransactionConfirmationModal/tsconfig.json b/src/components/TransactionConfirmationModal/tsconfig.json new file mode 100644 index 00000000000..638227fff6d --- /dev/null +++ b/src/components/TransactionConfirmationModal/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tsconfig.strict.json", + "include": ["**/*"] +} \ No newline at end of file diff --git a/src/components/WalletModal/index.tsx b/src/components/WalletModal/index.tsx index 6b21dee3ebd..b6b2356bba2 100644 --- a/src/components/WalletModal/index.tsx +++ b/src/components/WalletModal/index.tsx @@ -349,9 +349,7 @@ export default function WalletModal({ {walletView !== WALLET_VIEWS.PENDING && ( New to Ethereum?  {' '} - - Learn more about wallets - + Learn more about wallets )} diff --git a/src/components/swap/AdvancedSwapDetails.tsx b/src/components/swap/AdvancedSwapDetails.tsx index f4847c26012..c7a2e8d150a 100644 --- a/src/components/swap/AdvancedSwapDetails.tsx +++ b/src/components/swap/AdvancedSwapDetails.tsx @@ -73,23 +73,27 @@ export function AdvancedSwapDetails({ trade }: AdvancedSwapDetailsProps) { const [allowedSlippage] = useUserSlippageTolerance() - const showRoute = trade?.route?.path?.length > 2 + const showRoute = Boolean(trade && trade.route.path.length > 2) return ( - {trade && } - {showRoute && ( + {trade && ( <> - - - - - Route - - - - - + + {showRoute && ( + <> + + + + + Route + + + + + + + )} )} diff --git a/src/components/swap/AdvancedSwapDetailsDropdown.tsx b/src/components/swap/AdvancedSwapDetailsDropdown.tsx index e3da41b3e87..4529ba34003 100644 --- a/src/components/swap/AdvancedSwapDetailsDropdown.tsx +++ b/src/components/swap/AdvancedSwapDetailsDropdown.tsx @@ -1,6 +1,6 @@ import React from 'react' import styled from 'styled-components' -import useLast from '../../hooks/useLast' +import { useLastTruthy } from '../../hooks/useLast' import { AdvancedSwapDetails, AdvancedSwapDetailsProps } from './AdvancedSwapDetails' const AdvancedDetailsFooter = styled.div<{ show: boolean }>` @@ -20,11 +20,11 @@ const AdvancedDetailsFooter = styled.div<{ show: boolean }>` ` export default function AdvancedSwapDetailsDropdown({ trade, ...rest }: AdvancedSwapDetailsProps) { - const lastTrade = useLast(trade) + const lastTrade = useLastTruthy(trade) return ( - + ) } diff --git a/src/components/swap/ConfirmSwapModal.tsx b/src/components/swap/ConfirmSwapModal.tsx new file mode 100644 index 00000000000..24ed3f3d664 --- /dev/null +++ b/src/components/swap/ConfirmSwapModal.tsx @@ -0,0 +1,109 @@ +import { currencyEquals, Trade } from '@uniswap/sdk' +import React, { useCallback, useMemo } from 'react' +import TransactionConfirmationModal, { + ConfirmationModalContent, + TransactionErrorContent +} from '../TransactionConfirmationModal' +import SwapModalFooter from './SwapModalFooter' +import SwapModalHeader from './SwapModalHeader' + +/** + * Returns true if the trade requires a confirmation of details before we can submit it + * @param tradeA trade A + * @param tradeB trade B + */ +function tradeMeaningfullyDiffers(tradeA: Trade, tradeB: Trade): boolean { + return ( + tradeA.tradeType !== tradeB.tradeType || + !currencyEquals(tradeA.inputAmount.currency, tradeB.inputAmount.currency) || + !tradeA.inputAmount.equalTo(tradeB.inputAmount) || + !currencyEquals(tradeA.outputAmount.currency, tradeB.outputAmount.currency) || + !tradeA.outputAmount.equalTo(tradeB.outputAmount) + ) +} + +export default function ConfirmSwapModal({ + trade, + originalTrade, + onAcceptChanges, + allowedSlippage, + onConfirm, + onDismiss, + recipient, + swapErrorMessage, + isOpen, + attemptingTxn, + txHash +}: { + isOpen: boolean + trade: Trade | undefined + originalTrade: Trade | undefined + attemptingTxn: boolean + txHash: string | undefined + recipient: string | null + allowedSlippage: number + onAcceptChanges: () => void + onConfirm: () => void + swapErrorMessage: string | undefined + onDismiss: () => void +}) { + const showAcceptChanges = useMemo( + () => Boolean(trade && originalTrade && tradeMeaningfullyDiffers(trade, originalTrade)), + [originalTrade, trade] + ) + + const modalHeader = useCallback(() => { + return trade ? ( + + ) : null + }, [allowedSlippage, onAcceptChanges, recipient, showAcceptChanges, trade]) + + const modalBottom = useCallback(() => { + return trade ? ( + + ) : null + }, [allowedSlippage, onConfirm, showAcceptChanges, swapErrorMessage, trade]) + + // text to show while loading + const pendingText = `Swapping ${trade?.inputAmount?.toSignificant(6)} ${ + trade?.inputAmount?.currency?.symbol + } for ${trade?.outputAmount?.toSignificant(6)} ${trade?.outputAmount?.currency?.symbol}` + + const confirmationContent = useCallback( + () => + swapErrorMessage ? ( + + ) : ( + + ), + [onDismiss, modalBottom, modalHeader, swapErrorMessage] + ) + + return ( + + ) +} diff --git a/src/components/swap/FormattedPriceImpact.tsx b/src/components/swap/FormattedPriceImpact.tsx index c79da60a08b..c67727e06e2 100644 --- a/src/components/swap/FormattedPriceImpact.tsx +++ b/src/components/swap/FormattedPriceImpact.tsx @@ -4,10 +4,13 @@ import { ONE_BIPS } from '../../constants' import { warningSeverity } from '../../utils/prices' import { ErrorText } from './styleds' +/** + * Formatted version of price impact text with warning colors + */ export default function FormattedPriceImpact({ priceImpact }: { priceImpact?: Percent }) { return ( - {priceImpact?.lessThan(ONE_BIPS) ? '<0.01%' : `${priceImpact?.toFixed(2)}%` ?? '-'} + {priceImpact ? (priceImpact.lessThan(ONE_BIPS) ? '<0.01%' : `${priceImpact.toFixed(2)}%`) : '-'} ) } diff --git a/src/components/swap/SwapModalFooter.tsx b/src/components/swap/SwapModalFooter.tsx index 55412f4d644..369ff88b03c 100644 --- a/src/components/swap/SwapModalFooter.tsx +++ b/src/components/swap/SwapModalFooter.tsx @@ -1,46 +1,44 @@ -import { CurrencyAmount, Percent, Trade, TradeType } from '@uniswap/sdk' -import React, { useContext } from 'react' +import { Trade, TradeType } from '@uniswap/sdk' +import React, { useContext, useMemo, useState } from 'react' import { Repeat } from 'react-feather' import { Text } from 'rebass' import { ThemeContext } from 'styled-components' import { Field } from '../../state/swap/actions' import { TYPE } from '../../theme' -import { formatExecutionPrice } from '../../utils/prices' +import { + computeSlippageAdjustedAmounts, + computeTradePriceBreakdown, + formatExecutionPrice, + warningSeverity +} from '../../utils/prices' import { ButtonError } from '../Button' import { AutoColumn } from '../Column' import QuestionHelper from '../QuestionHelper' import { AutoRow, RowBetween, RowFixed } from '../Row' import FormattedPriceImpact from './FormattedPriceImpact' -import { StyledBalanceMaxMini } from './styleds' +import { StyledBalanceMaxMini, SwapCallbackError } from './styleds' export default function SwapModalFooter({ trade, - showInverted, - setShowInverted, - severity, - slippageAdjustedAmounts, - onSwap, - parsedAmounts, - realizedLPFee, - priceImpactWithoutFee, - confirmText + onConfirm, + allowedSlippage, + swapErrorMessage, + disabledConfirm }: { - trade?: Trade - showInverted: boolean - setShowInverted: (inverted: boolean) => void - severity: number - slippageAdjustedAmounts?: { [field in Field]?: CurrencyAmount } - onSwap: () => any - parsedAmounts?: { [field in Field]?: CurrencyAmount } - realizedLPFee?: CurrencyAmount - priceImpactWithoutFee?: Percent - confirmText: string + trade: Trade + allowedSlippage: number + onConfirm: () => void + swapErrorMessage: string | undefined + disabledConfirm: boolean }) { + const [showInverted, setShowInverted] = useState(false) const theme = useContext(ThemeContext) - - if (!trade) { - return null - } + const slippageAdjustedAmounts = useMemo(() => computeSlippageAdjustedAmounts(trade, allowedSlippage), [ + allowedSlippage, + trade + ]) + const { priceImpactWithoutFee, realizedLPFee } = useMemo(() => computeTradePriceBreakdown(trade), [trade]) + const severity = warningSeverity(priceImpactWithoutFee) return ( <> @@ -71,23 +69,21 @@ export default function SwapModalFooter({ - {trade?.tradeType === TradeType.EXACT_INPUT ? 'Minimum received' : 'Maximum sold'} + {trade.tradeType === TradeType.EXACT_INPUT ? 'Minimum received' : 'Maximum sold'} - {trade?.tradeType === TradeType.EXACT_INPUT + {trade.tradeType === TradeType.EXACT_INPUT ? slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(4) ?? '-' : slippageAdjustedAmounts[Field.INPUT]?.toSignificant(4) ?? '-'} - {parsedAmounts[Field.OUTPUT] && parsedAmounts[Field.INPUT] && ( - - {trade?.tradeType === TradeType.EXACT_INPUT - ? parsedAmounts[Field.OUTPUT]?.currency?.symbol - : parsedAmounts[Field.INPUT]?.currency?.symbol} - - )} + + {trade.tradeType === TradeType.EXACT_INPUT + ? trade.outputAmount.currency.symbol + : trade.inputAmount.currency.symbol} + @@ -107,17 +103,25 @@ export default function SwapModalFooter({ - {realizedLPFee ? realizedLPFee?.toSignificant(6) + ' ' + trade?.inputAmount?.currency?.symbol : '-'} + {realizedLPFee ? realizedLPFee?.toSignificant(6) + ' ' + trade.inputAmount.currency.symbol : '-'} - 2} style={{ margin: '10px 0 0 0' }} id="confirm-swap-or-send"> + 2} + style={{ margin: '10px 0 0 0' }} + id="confirm-swap-or-send" + > - {confirmText} + {severity > 2 ? 'Swap Anyway' : 'Confirm Swap'} + + {swapErrorMessage ? : null} ) diff --git a/src/components/swap/SwapModalHeader.tsx b/src/components/swap/SwapModalHeader.tsx index 57c52941364..e82e51ad08e 100644 --- a/src/components/swap/SwapModalHeader.tsx +++ b/src/components/swap/SwapModalHeader.tsx @@ -1,66 +1,107 @@ -import { Currency, CurrencyAmount } from '@uniswap/sdk' -import React, { useContext } from 'react' -import { ArrowDown } from 'react-feather' +import { Trade, TradeType } from '@uniswap/sdk' +import React, { useContext, useMemo } from 'react' +import { ArrowDown, AlertTriangle } from 'react-feather' import { Text } from 'rebass' import { ThemeContext } from 'styled-components' import { Field } from '../../state/swap/actions' import { TYPE } from '../../theme' +import { ButtonPrimary } from '../Button' import { isAddress, shortenAddress } from '../../utils' +import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices' import { AutoColumn } from '../Column' -import { RowBetween, RowFixed } from '../Row' import CurrencyLogo from '../CurrencyLogo' -import { TruncatedText } from './styleds' +import { RowBetween, RowFixed } from '../Row' +import { TruncatedText, SwapShowAcceptChanges } from './styleds' export default function SwapModalHeader({ - currencies, - formattedAmounts, - slippageAdjustedAmounts, - priceImpactSeverity, - independentField, - recipient + trade, + allowedSlippage, + recipient, + showAcceptChanges, + onAcceptChanges }: { - currencies: { [field in Field]?: Currency } - formattedAmounts: { [field in Field]?: string } - slippageAdjustedAmounts: { [field in Field]?: CurrencyAmount } - priceImpactSeverity: number - independentField: Field + trade: Trade + allowedSlippage: number recipient: string | null + showAcceptChanges: boolean + onAcceptChanges: () => void }) { + const slippageAdjustedAmounts = useMemo(() => computeSlippageAdjustedAmounts(trade, allowedSlippage), [ + trade, + allowedSlippage + ]) + const { priceImpactWithoutFee } = useMemo(() => computeTradePriceBreakdown(trade), [trade]) + const priceImpactSeverity = warningSeverity(priceImpactWithoutFee) + const theme = useContext(ThemeContext) return ( - - {formattedAmounts[Field.INPUT]} - - - + + + + {trade.inputAmount.toSignificant(6)} + + + - {currencies[Field.INPUT]?.symbol} + {trade.inputAmount.currency.symbol} - + - 2 ? theme.red1 : ''}> - {formattedAmounts[Field.OUTPUT]} - - - + + + 2 + ? theme.red1 + : showAcceptChanges && trade.tradeType === TradeType.EXACT_INPUT + ? theme.primary1 + : '' + } + > + {trade.outputAmount.toSignificant(6)} + + + - {currencies[Field.OUTPUT]?.symbol} + {trade.outputAmount.currency.symbol} + {showAcceptChanges ? ( + + + + + Price Updated + + + Accept + + + + ) : null} - {independentField === Field.INPUT ? ( + {trade.tradeType === TradeType.EXACT_INPUT ? ( {`Output is estimated. You will receive at least `} - {slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} {currencies[Field.OUTPUT]?.symbol} + {slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} {trade.outputAmount.currency.symbol} {' or the transaction will revert.'} @@ -68,7 +109,7 @@ export default function SwapModalHeader({ {`Input is estimated. You will sell at most `} - {slippageAdjustedAmounts[Field.INPUT]?.toSignificant(6)} {currencies[Field.INPUT]?.symbol} + {slippageAdjustedAmounts[Field.INPUT]?.toSignificant(6)} {trade.inputAmount.currency.symbol} {' or the transaction will revert.'} diff --git a/src/components/swap/confirmPriceImpactWithoutFee.ts b/src/components/swap/confirmPriceImpactWithoutFee.ts index 982e19eec97..1b955c33862 100644 --- a/src/components/swap/confirmPriceImpactWithoutFee.ts +++ b/src/components/swap/confirmPriceImpactWithoutFee.ts @@ -1,7 +1,11 @@ -// gathers additional user consent for a high price impact import { Percent } from '@uniswap/sdk' import { ALLOWED_PRICE_IMPACT_HIGH, PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN } from '../../constants' +/** + * Given the price impact, get user confirmation. + * + * @param priceImpactWithoutFee price impact of the trade without the fee. + */ export default function confirmPriceImpactWithoutFee(priceImpactWithoutFee: Percent): boolean { if (!priceImpactWithoutFee.lessThan(PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN)) { return ( diff --git a/src/components/swap/styleds.ts b/src/components/swap/styleds.tsx similarity index 59% rename from src/components/swap/styleds.ts rename to src/components/swap/styleds.tsx index 612dd02f5fc..bfc98be6898 100644 --- a/src/components/swap/styleds.ts +++ b/src/components/swap/styleds.tsx @@ -1,8 +1,9 @@ +import { transparentize } from 'polished' +import React from 'react' +import { AlertTriangle } from 'react-feather' import styled, { css } from 'styled-components' -import { AutoColumn } from '../Column' import { Text } from 'rebass' - -import NumericalInput from '../NumericalInput' +import { AutoColumn } from '../Column' export const Wrapper = styled.div` position: relative; @@ -30,7 +31,6 @@ export const SectionBreak = styled.div` export const BottomGrouping = styled.div` margin-top: 12px; - position: relative; ` export const ErrorText = styled(Text)<{ severity?: 0 | 1 | 2 | 3 | 4 }>` @@ -44,21 +44,6 @@ export const ErrorText = styled(Text)<{ severity?: 0 | 1 | 2 | 3 | 4 }>` : theme.green1}; ` -export const InputGroup = styled(AutoColumn)` - position: relative; - padding: 40px 0 20px 0; -` - -export const StyledNumerical = styled(NumericalInput)` - text-align: center; - font-size: 48px; - font-weight: 500px; - width: 100%; - - ::placeholder { - color: ${({ theme }) => theme.text4}; - } -` export const StyledBalanceMaxMini = styled.button` height: 22px; width: 22px; @@ -112,3 +97,51 @@ export const Dots = styled.span` } } ` + +const SwapCallbackErrorInner = styled.div` + background-color: ${({ theme }) => transparentize(0.9, theme.red1)}; + border-radius: 1rem; + display: flex; + align-items: center; + font-size: 0.825rem; + width: 100%; + padding: 3rem 1.25rem 1rem 1rem; + margin-top: -2rem; + color: ${({ theme }) => theme.red1}; + z-index: -1; + p { + padding: 0; + margin: 0; + font-weight: 500; + } +` + +const SwapCallbackErrorInnerAlertTriangle = styled.div` + background-color: ${({ theme }) => transparentize(0.9, theme.red1)}; + display: flex; + align-items: center; + justify-content: center; + margin-right: 12px; + border-radius: 12px; + min-width: 48px; + height: 48px; +` + +export function SwapCallbackError({ error }: { error: string }) { + return ( + + + + +

{error}

+
+ ) +} + +export const SwapShowAcceptChanges = styled(AutoColumn)` + background-color: ${({ theme }) => transparentize(0.9, theme.primary1)}; + color: ${({ theme }) => theme.primary1}; + padding: 0.5rem; + border-radius: 12px; + margin-top: 8px; +` diff --git a/src/components/swap/tsconfig.json b/src/components/swap/tsconfig.json new file mode 100644 index 00000000000..638227fff6d --- /dev/null +++ b/src/components/swap/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tsconfig.strict.json", + "include": ["**/*"] +} \ No newline at end of file diff --git a/src/connectors/NetworkConnector.ts b/src/connectors/NetworkConnector.ts index 415affcf59b..8e27145996b 100644 --- a/src/connectors/NetworkConnector.ts +++ b/src/connectors/NetworkConnector.ts @@ -22,19 +22,83 @@ class RequestError extends Error { } } +interface BatchItem { + request: { jsonrpc: '2.0'; id: number; method: string; params: unknown } + resolve: (result: any) => void + reject: (error: Error) => void +} + class MiniRpcProvider implements AsyncSendable { public readonly isMetaMask: false = false public readonly chainId: number public readonly url: string public readonly host: string public readonly path: string + public readonly batchWaitTimeMs: number - constructor(chainId: number, url: string) { + private nextId = 1 + private batchTimeoutId: ReturnType | null = null + private batch: BatchItem[] = [] + + constructor(chainId: number, url: string, batchWaitTimeMs?: number) { this.chainId = chainId this.url = url const parsed = new URL(url) this.host = parsed.host this.path = parsed.pathname + // how long to wait to batch calls + this.batchWaitTimeMs = batchWaitTimeMs ?? 50 + } + + public readonly clearBatch = async () => { + console.debug('Clearing batch', this.batch) + const batch = this.batch + this.batch = [] + this.batchTimeoutId = null + let response: Response + try { + response = await fetch(this.url, { + method: 'POST', + headers: { 'content-type': 'application/json', accept: 'application/json' }, + body: JSON.stringify(batch.map(item => item.request)) + }) + } catch (error) { + batch.forEach(({ reject }) => reject(new Error('Failed to send batch call'))) + return + } + + if (!response.ok) { + batch.forEach(({ reject }) => reject(new RequestError(`${response.status}: ${response.statusText}`, -32000))) + return + } + + let json + try { + json = await response.json() + } catch (error) { + batch.forEach(({ reject }) => reject(new Error('Failed to parse JSON response'))) + return + } + const byKey = batch.reduce<{ [id: number]: BatchItem }>((memo, current) => { + memo[current.request.id] = current + return memo + }, {}) + for (const result of json) { + const { + resolve, + reject, + request: { method } + } = byKey[result.id] + if (resolve && reject) { + if ('error' in result) { + reject(new RequestError(result?.error?.message, result?.error?.code, result?.error?.data)) + } else if ('result' in result) { + resolve(result.result) + } else { + reject(new RequestError(`Received unexpected JSON-RPC response to ${method} request.`, -32000, result)) + } + } + } } public readonly sendAsync = ( @@ -56,24 +120,20 @@ class MiniRpcProvider implements AsyncSendable { if (method === 'eth_chainId') { return `0x${this.chainId.toString(16)}` } - const response = await fetch(this.url, { - method: 'POST', - body: JSON.stringify({ - jsonrpc: '2.0', - id: 1, - method, - params + const promise = new Promise((resolve, reject) => { + this.batch.push({ + request: { + jsonrpc: '2.0', + id: this.nextId++, + method, + params + }, + resolve, + reject }) }) - if (!response.ok) throw new RequestError(`${response.status}: ${response.statusText}`, -32000) - const body = await response.json() - if ('error' in body) { - throw new RequestError(body?.error?.message, body?.error?.code, body?.error?.data) - } else if ('result' in body) { - return body.result - } else { - throw new RequestError(`Received unexpected JSON-RPC response to ${method} request.`, -32000, body) - } + this.batchTimeoutId = this.batchTimeoutId ?? setTimeout(this.clearBatch, this.batchWaitTimeMs) + return promise } } diff --git a/src/constants/index.ts b/src/constants/index.ts index 0420b4f1aa3..aa5470c37cc 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,3 +1,4 @@ +import { AbstractConnector } from '@web3-react/abstract-connector' import { ChainId, JSBI, Percent, Token, WETH } from '@uniswap/sdk' import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors' @@ -52,7 +53,19 @@ export const PINNED_PAIRS: { readonly [chainId in ChainId]?: [Token, Token][] } ] } -const TESTNET_CAPABLE_WALLETS = { +export interface WalletInfo { + connector?: AbstractConnector + name: string + iconName: string + description: string + href: string | null + color: string + primary?: true + mobile?: true + mobileOnly?: true +} + +export const SUPPORTED_WALLETS: { [key: string]: WalletInfo } = { INJECTED: { connector: injected, name: 'Injected', @@ -69,62 +82,53 @@ const TESTNET_CAPABLE_WALLETS = { description: 'Easy-to-use browser extension.', href: null, color: '#E8831D' + }, + WALLET_CONNECT: { + connector: walletconnect, + name: 'WalletConnect', + iconName: 'walletConnectIcon.svg', + description: 'Connect to Trust Wallet, Rainbow Wallet and more...', + href: null, + color: '#4196FC', + mobile: true + }, + WALLET_LINK: { + connector: walletlink, + name: 'Coinbase Wallet', + iconName: 'coinbaseWalletIcon.svg', + description: 'Use Coinbase Wallet app on mobile device', + href: null, + color: '#315CF5' + }, + COINBASE_LINK: { + name: 'Open in Coinbase Wallet', + iconName: 'coinbaseWalletIcon.svg', + description: 'Open in Coinbase Wallet app.', + href: 'https://go.cb-w.com/mtUDhEZPy1', + color: '#315CF5', + mobile: true, + mobileOnly: true + }, + FORTMATIC: { + connector: fortmatic, + name: 'Fortmatic', + iconName: 'fortmaticIcon.png', + description: 'Login using Fortmatic hosted wallet', + href: null, + color: '#6748FF', + mobile: true + }, + Portis: { + connector: portis, + name: 'Portis', + iconName: 'portisIcon.png', + description: 'Login using Portis hosted wallet', + href: null, + color: '#4A6C9B', + mobile: true } } -export const SUPPORTED_WALLETS = - process.env.REACT_APP_CHAIN_ID !== '1' - ? TESTNET_CAPABLE_WALLETS - : { - ...TESTNET_CAPABLE_WALLETS, - ...{ - WALLET_CONNECT: { - connector: walletconnect, - name: 'WalletConnect', - iconName: 'walletConnectIcon.svg', - description: 'Connect to Trust Wallet, Rainbow Wallet and more...', - href: null, - color: '#4196FC', - mobile: true - }, - WALLET_LINK: { - connector: walletlink, - name: 'Coinbase Wallet', - iconName: 'coinbaseWalletIcon.svg', - description: 'Use Coinbase Wallet app on mobile device', - href: null, - color: '#315CF5' - }, - COINBASE_LINK: { - name: 'Open in Coinbase Wallet', - iconName: 'coinbaseWalletIcon.svg', - description: 'Open in Coinbase Wallet app.', - href: 'https://go.cb-w.com/mtUDhEZPy1', - color: '#315CF5', - mobile: true, - mobileOnly: true - }, - FORTMATIC: { - connector: fortmatic, - name: 'Fortmatic', - iconName: 'fortmaticIcon.png', - description: 'Login using Fortmatic hosted wallet', - href: null, - color: '#6748FF', - mobile: true - }, - Portis: { - connector: portis, - name: 'Portis', - iconName: 'portisIcon.png', - description: 'Login using Portis hosted wallet', - href: null, - color: '#4A6C9B', - mobile: true - } - } - } - export const NetworkContextName = 'NETWORK' // default allowed slippage, in bips diff --git a/src/data/V1.ts b/src/data/V1.ts index 73ec3931a22..67f7b78ccf8 100644 --- a/src/data/V1.ts +++ b/src/data/V1.ts @@ -131,7 +131,7 @@ export function useV1Trade( ? new Trade(route, exactAmount, isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT) : undefined } catch (error) { - console.error('Failed to create V1 trade', error) + console.debug('Failed to create V1 trade', error) } return v1Trade } diff --git a/src/hooks/useLast.ts b/src/hooks/useLast.ts index 9703044eb39..6260997b831 100644 --- a/src/hooks/useLast.ts +++ b/src/hooks/useLast.ts @@ -1,13 +1,33 @@ import { useEffect, useState } from 'react' /** - * Returns the last truthy value of type T + * Returns the last value of type T that passes a filter function * @param value changing value + * @param filterFn function that determines whether a given value should be considered for the last value */ -export default function useLast(value: T | undefined | null): T | null | undefined { - const [last, setLast] = useState(value) +export default function useLast( + value: T | undefined | null, + filterFn?: (value: T | null | undefined) => boolean +): T | null | undefined { + const [last, setLast] = useState(filterFn && filterFn(value) ? value : undefined) useEffect(() => { - setLast(last => value ?? last) - }, [value]) + setLast(last => { + const shouldUse: boolean = filterFn ? filterFn(value) : true + if (shouldUse) return value + return last + }) + }, [filterFn, value]) return last } + +function isDefined(x: T | null | undefined): x is T { + return x !== null && x !== undefined +} + +/** + * Returns the last truthy value of type T + * @param value changing value + */ +export function useLastTruthy(value: T | undefined | null): T | null | undefined { + return useLast(value, isDefined) +} diff --git a/src/hooks/useSwapCallback.ts b/src/hooks/useSwapCallback.ts index 2a1431329c8..e8b39cf082f 100644 --- a/src/hooks/useSwapCallback.ts +++ b/src/hooks/useSwapCallback.ts @@ -1,137 +1,207 @@ import { BigNumber } from '@ethersproject/bignumber' import { Contract } from '@ethersproject/contracts' -import { JSBI, Percent, Router, Trade, TradeType } from '@uniswap/sdk' +import { JSBI, Percent, Router, SwapParameters, Trade, TradeType } from '@uniswap/sdk' import { useMemo } from 'react' import { BIPS_BASE, DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../constants' import { getTradeVersion, useV1TradeExchangeAddress } from '../data/V1' import { useTransactionAdder } from '../state/transactions/hooks' import { calculateGasMargin, getRouterContract, isAddress, shortenAddress } from '../utils' +import isZero from '../utils/isZero' import v1SwapArguments from '../utils/v1SwapArguments' import { useActiveWeb3React } from './index' import { useV1ExchangeContract } from './useContract' import useENS from './useENS' import { Version } from './useToggledVersion' -function isZero(hexNumber: string) { - return /^0x0*$/.test(hexNumber) +export enum SwapCallbackState { + INVALID, + LOADING, + VALID } -// returns a function that will execute a swap, if the parameters are all valid -// and the user has approved the slippage adjusted input amount for the trade -export function useSwapCallback( +interface SwapCall { + contract: Contract + parameters: SwapParameters +} + +interface SuccessfulCall { + call: SwapCall + gasEstimate: BigNumber +} + +interface FailedCall { + call: SwapCall + error: Error +} + +type EstimatedSwapCall = SuccessfulCall | FailedCall + +/** + * Returns the swap calls that can be used to make the trade + * @param trade trade to execute + * @param allowedSlippage user allowed slippage + * @param deadline the deadline for the trade + * @param recipientAddressOrName + */ +function useSwapCallArguments( trade: Trade | undefined, // trade to execute, required allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // in bips deadline: number = DEFAULT_DEADLINE_FROM_NOW, // in seconds from now recipientAddressOrName: string | null // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender -): null | (() => Promise) { +): SwapCall[] { const { account, chainId, library } = useActiveWeb3React() - const addTransaction = useTransactionAdder() const { address: recipientAddress } = useENS(recipientAddressOrName) const recipient = recipientAddressOrName === null ? account : recipientAddress - const tradeVersion = getTradeVersion(trade) const v1Exchange = useV1ExchangeContract(useV1TradeExchangeAddress(trade), true) return useMemo(() => { - if (!trade || !recipient || !library || !account || !tradeVersion || !chainId) return null + const tradeVersion = getTradeVersion(trade) + if (!trade || !recipient || !library || !account || !tradeVersion || !chainId) return [] - return async function onSwap() { - const contract: Contract | null = - tradeVersion === Version.v2 ? getRouterContract(chainId, library, account) : v1Exchange - if (!contract) { - throw new Error('Failed to get a swap contract') - } + const contract: Contract | null = + tradeVersion === Version.v2 ? getRouterContract(chainId, library, account) : v1Exchange + if (!contract) { + return [] + } + + const swapMethods = [] - const swapMethods = [] + switch (tradeVersion) { + case Version.v2: + swapMethods.push( + Router.swapCallParameters(trade, { + feeOnTransfer: false, + allowedSlippage: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE), + recipient, + ttl: deadline + }) + ) - switch (tradeVersion) { - case Version.v2: + if (trade.tradeType === TradeType.EXACT_INPUT) { swapMethods.push( Router.swapCallParameters(trade, { - feeOnTransfer: false, + feeOnTransfer: true, allowedSlippage: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE), recipient, ttl: deadline }) ) + } + break + case Version.v1: + swapMethods.push( + v1SwapArguments(trade, { + allowedSlippage: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE), + recipient, + ttl: deadline + }) + ) + break + } + return swapMethods.map(parameters => ({ parameters, contract })) + }, [account, allowedSlippage, chainId, deadline, library, recipient, trade, v1Exchange]) +} - if (trade.tradeType === TradeType.EXACT_INPUT) { - swapMethods.push( - Router.swapCallParameters(trade, { - feeOnTransfer: true, - allowedSlippage: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE), - recipient, - ttl: deadline - }) - ) - } - break - case Version.v1: - swapMethods.push( - v1SwapArguments(trade, { - allowedSlippage: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE), - recipient, - ttl: deadline - }) - ) - break +const DEFAULT_FAILED_SWAP_ERROR = 'Unexpected error. Please try again or contact support.' + +// returns a function that will execute a swap, if the parameters are all valid +// and the user has approved the slippage adjusted input amount for the trade +export function useSwapCallback( + trade: Trade | undefined, // trade to execute, required + allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // in bips + deadline: number = DEFAULT_DEADLINE_FROM_NOW, // in seconds from now + recipientAddressOrName: string | null // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender +): { state: SwapCallbackState; callback: null | (() => Promise); error: string | null } { + const { account, chainId, library } = useActiveWeb3React() + + const swapCalls = useSwapCallArguments(trade, allowedSlippage, deadline, recipientAddressOrName) + + const addTransaction = useTransactionAdder() + + const { address: recipientAddress } = useENS(recipientAddressOrName) + const recipient = recipientAddressOrName === null ? account : recipientAddress + + return useMemo(() => { + if (!trade || !library || !account || !chainId) { + return { state: SwapCallbackState.INVALID, callback: null, error: 'Missing dependencies' } + } + if (!recipient) { + if (recipientAddressOrName !== null) { + return { state: SwapCallbackState.INVALID, callback: null, error: 'Invalid recipient' } + } else { + return { state: SwapCallbackState.LOADING, callback: null, error: null } } + } - const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all( - swapMethods.map(({ args, methodName, value }) => - contract.estimateGas[methodName](...args, value && !isZero(value) ? { value } : {}) - .then(calculateGasMargin) - .catch(error => { - console.error(`estimateGas failed for ${methodName}`, error) - return undefined - }) + const tradeVersion = getTradeVersion(trade) + + return { + state: SwapCallbackState.VALID, + callback: async function onSwap(): Promise { + const estimatedCalls: EstimatedSwapCall[] = await Promise.all( + swapCalls.map(call => { + const { + parameters: { methodName, args, value }, + contract + } = call + const options = !value || isZero(value) ? {} : { value } + + return contract.estimateGas[methodName](...args, options) + .then(gasEstimate => { + return { + call, + gasEstimate + } + }) + .catch(gasError => { + console.debug('Gas estimate failed, trying eth_call to extract error', call) + + return contract.callStatic[methodName](...args, options) + .then(result => { + console.debug('Unexpected successful call after failed estimate gas', call, gasError, result) + return { call, error: new Error(DEFAULT_FAILED_SWAP_ERROR) } + }) + .catch(callError => { + console.debug('Call threw error', call, callError) + let errorMessage: string = DEFAULT_FAILED_SWAP_ERROR + switch (callError.reason) { + case 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT': + case 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT': + errorMessage = + 'This transaction will not succeed either due to price movement or fee on transfer. Try increasing your slippage tolerance.' + break + } + return { call, error: new Error(errorMessage) } + }) + }) + }) ) - ) - - // we expect failures from left to right, so throw if we see failures - // from right to left - for (let i = 0; i < safeGasEstimates.length - 1; i++) { - // if the FoT method fails, but the regular method does not, we should not - // use the regular method. this probably means something is wrong with the fot token. - if (BigNumber.isBigNumber(safeGasEstimates[i]) && !BigNumber.isBigNumber(safeGasEstimates[i + 1])) { - throw new Error( - 'An error occurred. Please try raising your slippage. If that does not work, contact support.' - ) - } - } - const indexOfSuccessfulEstimation = safeGasEstimates.findIndex(safeGasEstimate => - BigNumber.isBigNumber(safeGasEstimate) - ) - - // all estimations failed... - if (indexOfSuccessfulEstimation === -1) { - // if only 1 method exists, either: - // a) the token is doing something weird not related to FoT (e.g. enforcing a whitelist) - // b) the token is FoT and the user specified an exact output, which is not allowed - if (swapMethods.length === 1) { - throw Error( - `An error occurred. If either of the tokens you're swapping take a fee on transfer, you must specify an exact input amount.` - ) - } - // if 2 methods exists, either: - // a) the token is doing something weird not related to FoT (e.g. enforcing a whitelist) - // b) the token is FoT and is taking more than the specified slippage - else if (swapMethods.length === 2) { - throw Error( - `An error occurred. If either of the tokens you're swapping take a fee on transfer, you must specify a slippage tolerance higher than the fee.` - ) - } else { - throw Error('This transaction would fail. Please contact support.') + // a successful estimation is a bignumber gas estimate and the next call is also a bignumber gas estimate + const successfulEstimation = estimatedCalls.find( + (el, ix, list): el is SuccessfulCall => + 'gasEstimate' in el && (ix === list.length - 1 || 'gasEstimate' in list[ix + 1]) + ) + + if (!successfulEstimation) { + const errorCalls = estimatedCalls.filter((call): call is FailedCall => 'error' in call) + if (errorCalls.length > 0) throw errorCalls[errorCalls.length - 1].error + throw new Error(DEFAULT_FAILED_SWAP_ERROR) } - } else { - const { methodName, args, value } = swapMethods[indexOfSuccessfulEstimation] - const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation] + + const { + call: { + contract, + parameters: { methodName, args, value } + }, + gasEstimate + } = successfulEstimation return contract[methodName](...args, { - gasLimit: safeGasEstimate, - ...(value && !isZero(value) ? { value } : {}) + gasLimit: calculateGasMargin(gasEstimate), + ...(value && !isZero(value) ? { value, from: account } : { from: account }) }) .then((response: any) => { const inputSymbol = trade.inputAmount.currency.symbol @@ -161,27 +231,15 @@ export function useSwapCallback( .catch((error: any) => { // if the user rejected the tx, pass this along if (error?.code === 4001) { - throw error - } - // otherwise, the error was unexpected and we need to convey that - else { + throw new Error('Transaction rejected.') + } else { + // otherwise, the error was unexpected and we need to convey that console.error(`Swap failed`, error, methodName, args, value) - throw Error('An error occurred while swapping. Please contact support.') + throw new Error(DEFAULT_FAILED_SWAP_ERROR) } }) - } + }, + error: null } - }, [ - trade, - recipient, - library, - account, - tradeVersion, - chainId, - allowedSlippage, - v1Exchange, - deadline, - recipientAddressOrName, - addTransaction - ]) + }, [trade, library, account, chainId, recipient, recipientAddressOrName, swapCalls, addTransaction]) } diff --git a/src/hooks/useWrapCallback.ts b/src/hooks/useWrapCallback.ts index 43211c38e47..46a4f8e2d33 100644 --- a/src/hooks/useWrapCallback.ts +++ b/src/hooks/useWrapCallback.ts @@ -23,7 +23,7 @@ export default function useWrapCallback( inputCurrency: Currency | undefined, outputCurrency: Currency | undefined, typedValue: string | undefined -): { wrapType: WrapType; execute?: undefined | (() => Promise); error?: string } { +): { wrapType: WrapType; execute?: undefined | (() => Promise); inputError?: string } { const { chainId, account } = useActiveWeb3React() const wethContract = useWETHContract() const balance = useCurrencyBalance(account ?? undefined, inputCurrency) @@ -50,7 +50,7 @@ export default function useWrapCallback( } } : undefined, - error: sufficientBalance ? undefined : 'Insufficient ETH balance' + inputError: sufficientBalance ? undefined : 'Insufficient ETH balance' } } else if (currencyEquals(WETH[chainId], inputCurrency) && outputCurrency === ETHER) { return { @@ -66,7 +66,7 @@ export default function useWrapCallback( } } : undefined, - error: sufficientBalance ? undefined : 'Insufficient WETH balance' + inputError: sufficientBalance ? undefined : 'Insufficient WETH balance' } } else { return NOT_APPLICABLE diff --git a/src/pages/AddLiquidity/PoolPriceBar.tsx b/src/pages/AddLiquidity/PoolPriceBar.tsx index 04593a82da5..9d2f920be3c 100644 --- a/src/pages/AddLiquidity/PoolPriceBar.tsx +++ b/src/pages/AddLiquidity/PoolPriceBar.tsx @@ -1,4 +1,4 @@ -import { Currency, Fraction, Percent } from '@uniswap/sdk' +import { Currency, Percent, Price } from '@uniswap/sdk' import React, { useContext } from 'react' import { Text } from 'rebass' import { ThemeContext } from 'styled-components' @@ -8,7 +8,7 @@ import { ONE_BIPS } from '../../constants' import { Field } from '../../state/mint/actions' import { TYPE } from '../../theme' -export const PoolPriceBar = ({ +export function PoolPriceBar({ currencies, noLiquidity, poolTokenPercentage, @@ -17,20 +17,20 @@ export const PoolPriceBar = ({ currencies: { [field in Field]?: Currency } noLiquidity?: boolean poolTokenPercentage?: Percent - price?: Fraction -}) => { + price?: Price +}) { const theme = useContext(ThemeContext) return ( - {price?.toSignificant(6) ?? '0'} + {price?.toSignificant(6) ?? '-'} {currencies[Field.CURRENCY_B]?.symbol} per {currencies[Field.CURRENCY_A]?.symbol} - {price?.invert().toSignificant(6) ?? '0'} + {price?.invert()?.toSignificant(6) ?? '-'} {currencies[Field.CURRENCY_A]?.symbol} per {currencies[Field.CURRENCY_B]?.symbol} diff --git a/src/pages/AddLiquidity/index.tsx b/src/pages/AddLiquidity/index.tsx index c481b2b3b4f..bc53c5092e8 100644 --- a/src/pages/AddLiquidity/index.tsx +++ b/src/pages/AddLiquidity/index.tsx @@ -10,7 +10,7 @@ import { ThemeContext } from 'styled-components' import { ButtonError, ButtonLight, ButtonPrimary } from '../../components/Button' import { BlueCard, GreyCard, LightCard } from '../../components/Card' import { AutoColumn, ColumnCenter } from '../../components/Column' -import ConfirmationModal from '../../components/ConfirmationModal' +import TransactionConfirmationModal, { ConfirmationModalContent } from '../../components/TransactionConfirmationModal' import CurrencyInputPanel from '../../components/CurrencyInputPanel' import DoubleCurrencyLogo from '../../components/DoubleLogo' import { AddRemoveTabs } from '../../components/NavigationTabs' @@ -294,27 +294,34 @@ export default function AddLiquidity({ [currencyIdA, history, currencyIdB] ) + const handleDismissConfirmation = useCallback(() => { + setShowConfirm(false) + // if there was a tx hash, we want to clear the input + if (txHash) { + onFieldAInput('') + } + setTxHash('') + }, [onFieldAInput, txHash]) + return ( <> - { - setShowConfirm(false) - // if there was a tx hash, we want to clear the input - if (txHash) { - onFieldAInput('') - } - setTxHash('') - }} + onDismiss={handleDismissConfirmation} attemptingTxn={attemptingTxn} hash={txHash} - topContent={modalHeader} - bottomContent={modalBottom} + content={() => ( + + )} pendingText={pendingText} - title={noLiquidity ? 'You are creating a pool' : 'You will receive'} /> {noLiquidity && ( diff --git a/src/pages/RemoveLiquidity/index.tsx b/src/pages/RemoveLiquidity/index.tsx index abfe3c06b8f..ef4729967ee 100644 --- a/src/pages/RemoveLiquidity/index.tsx +++ b/src/pages/RemoveLiquidity/index.tsx @@ -11,7 +11,7 @@ import { ThemeContext } from 'styled-components' import { ButtonPrimary, ButtonLight, ButtonError, ButtonConfirmed } from '../../components/Button' import { LightCard } from '../../components/Card' import { AutoColumn, ColumnCenter } from '../../components/Column' -import ConfirmationModal from '../../components/ConfirmationModal' +import TransactionConfirmationModal, { ConfirmationModalContent } from '../../components/TransactionConfirmationModal' import CurrencyInputPanel from '../../components/CurrencyInputPanel' import DoubleCurrencyLogo from '../../components/DoubleLogo' import { AddRemoveTabs } from '../../components/NavigationTabs' @@ -274,12 +274,13 @@ export default function RemoveLiquidity({ throw new Error('Attempting to confirm without approval or a signature. Please contact support.') } - const safeGasEstimates = await Promise.all( + const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all( methodNames.map(methodName => router.estimateGas[methodName](...args) .then(calculateGasMargin) .catch(error => { - console.error(`estimateGas failed for ${methodName}`, error) + console.error(`estimateGas failed`, methodName, args, error) + return undefined }) ) ) @@ -447,28 +448,35 @@ export default function RemoveLiquidity({ [currencyIdA, currencyIdB, history] ) + const handleDismissConfirmation = useCallback(() => { + setShowConfirm(false) + setSignatureData(null) // important that we clear signature data to avoid bad sigs + // if there was a tx hash, we want to clear the input + if (txHash) { + onUserInput(Field.LIQUIDITY_PERCENT, '0') + } + setTxHash('') + }, [onUserInput, txHash]) + return ( <> - { - setShowConfirm(false) - setSignatureData(null) // important that we clear signature data to avoid bad sigs - // if there was a tx hash, we want to clear the input - if (txHash) { - onUserInput(Field.LIQUIDITY_PERCENT, '0') - } - setTxHash('') - }} + onDismiss={handleDismissConfirmation} attemptingTxn={attemptingTxn} hash={txHash ? txHash : ''} - topContent={modalHeader} - bottomContent={modalBottom} + content={() => ( + + )} pendingText={pendingText} - title="You will receive" /> diff --git a/src/pages/Swap/index.tsx b/src/pages/Swap/index.tsx index 5d5dbdc5665..8068fbc251f 100644 --- a/src/pages/Swap/index.tsx +++ b/src/pages/Swap/index.tsx @@ -1,4 +1,4 @@ -import { CurrencyAmount, JSBI } from '@uniswap/sdk' +import { CurrencyAmount, JSBI, Trade } from '@uniswap/sdk' import React, { useCallback, useContext, useEffect, useState } from 'react' import { ArrowDown } from 'react-feather' import ReactGA from 'react-ga' @@ -8,16 +8,14 @@ import AddressInputPanel from '../../components/AddressInputPanel' import { ButtonError, ButtonLight, ButtonPrimary } from '../../components/Button' import Card, { GreyCard } from '../../components/Card' import { AutoColumn } from '../../components/Column' -import ConfirmationModal from '../../components/ConfirmationModal' +import ConfirmSwapModal from '../../components/swap/ConfirmSwapModal' import CurrencyInputPanel from '../../components/CurrencyInputPanel' import { SwapPoolTabs } from '../../components/NavigationTabs' import { AutoRow, RowBetween } from '../../components/Row' import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown' import BetterTradeLink from '../../components/swap/BetterTradeLink' import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee' -import { ArrowWrapper, BottomGrouping, Dots, Wrapper } from '../../components/swap/styleds' -import SwapModalFooter from '../../components/swap/SwapModalFooter' -import SwapModalHeader from '../../components/swap/SwapModalHeader' +import { ArrowWrapper, BottomGrouping, Dots, SwapCallbackError, Wrapper } from '../../components/swap/styleds' import TradePrice from '../../components/swap/TradePrice' import { TokenWarningCards } from '../../components/TokenWarningCard' @@ -39,13 +37,13 @@ import { } from '../../state/swap/hooks' import { useExpertModeManager, + useTokenWarningDismissal, useUserDeadline, - useUserSlippageTolerance, - useTokenWarningDismissal + useUserSlippageTolerance } from '../../state/user/hooks' import { CursorPointer, LinkStyledButton, TYPE } from '../../theme' import { maxAmountSpend } from '../../utils/maxAmountSpend' -import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices' +import { computeTradePriceBreakdown, warningSeverity } from '../../utils/prices' import AppBody from '../AppBody' import { ClickableText } from '../Pool/styleds' @@ -60,7 +58,7 @@ export default function Swap() { // for expert mode const toggleSettings = useToggleSettingsMenu() - const [expertMode] = useExpertModeManager() + const [isExpertMode] = useExpertModeManager() // get custom setting values for user const [deadline] = useUserDeadline() @@ -68,8 +66,15 @@ export default function Swap() { // swap state const { independentField, typedValue, recipient } = useSwapState() - const { v1Trade, v2Trade, currencyBalances, parsedAmount, currencies, error } = useDerivedSwapInfo() - const { wrapType, execute: onWrap, error: wrapError } = useWrapCallback( + const { + v1Trade, + v2Trade, + currencyBalances, + parsedAmount, + currencies, + inputError: swapInputError + } = useDerivedSwapInfo() + const { wrapType, execute: onWrap, inputError: wrapInputError } = useWrapCallback( currencies[Field.INPUT], currencies[Field.OUTPUT], typedValue @@ -102,7 +107,7 @@ export default function Swap() { } const { onSwitchTokens, onCurrencySelection, onUserInput, onChangeRecipient } = useSwapActionHandlers() - const isValid = !error + const isValid = !swapInputError const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT const handleTypeInput = useCallback( @@ -119,9 +124,19 @@ export default function Swap() { ) // modal and loading - const [showConfirm, setShowConfirm] = useState(false) // show confirmation modal - const [attemptingTxn, setAttemptingTxn] = useState(false) // waiting for user confirmaion/rejection - const [txHash, setTxHash] = useState('') + const [{ showConfirm, tradeToConfirm, swapErrorMessage, attemptingTxn, txHash }, setSwapState] = useState<{ + showConfirm: boolean + tradeToConfirm: Trade | undefined + attemptingTxn: boolean + swapErrorMessage: string | undefined + txHash: string | undefined + }>({ + showConfirm: false, + tradeToConfirm: undefined, + attemptingTxn: false, + swapErrorMessage: undefined, + txHash: undefined + }) const formattedAmounts = { [independentField]: typedValue, @@ -152,25 +167,27 @@ export default function Swap() { const maxAmountInput: CurrencyAmount | undefined = maxAmountSpend(currencyBalances[Field.INPUT]) const atMaxAmountInput = Boolean(maxAmountInput && parsedAmounts[Field.INPUT]?.equalTo(maxAmountInput)) - const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(trade, allowedSlippage) - // the callback to execute the swap - const swapCallback = useSwapCallback(trade, allowedSlippage, deadline, recipient) + const { callback: swapCallback, error: swapCallbackError } = useSwapCallback( + trade, + allowedSlippage, + deadline, + recipient + ) - const { priceImpactWithoutFee, realizedLPFee } = computeTradePriceBreakdown(trade) + const { priceImpactWithoutFee } = computeTradePriceBreakdown(trade) - function onSwap() { + const handleSwap = useCallback(() => { if (priceImpactWithoutFee && !confirmPriceImpactWithoutFee(priceImpactWithoutFee)) { return } if (!swapCallback) { return } - setAttemptingTxn(true) + setSwapState({ attemptingTxn: true, tradeToConfirm, showConfirm, swapErrorMessage: undefined, txHash: undefined }) swapCallback() .then(hash => { - setAttemptingTxn(false) - setTxHash(hash) + setSwapState({ attemptingTxn: false, tradeToConfirm, showConfirm, swapErrorMessage: undefined, txHash: hash }) ReactGA.event({ category: 'Swap', @@ -188,13 +205,15 @@ export default function Swap() { }) }) .catch(error => { - setAttemptingTxn(false) - // we only care if the error is something _other_ than the user rejected the tx - if (error?.code !== 4001) { - console.error(error) - } + setSwapState({ + attemptingTxn: false, + tradeToConfirm, + showConfirm, + swapErrorMessage: error.message, + txHash: undefined + }) }) - } + }, [tradeToConfirm, account, priceImpactWithoutFee, recipient, recipientAddress, showConfirm, swapCallback, trade]) // errors const [showInverted, setShowInverted] = useState(false) @@ -205,74 +224,47 @@ export default function Swap() { // show approve flow when: no error on inputs, not approved or pending, or approved in current session // never show if price impact is above threshold in non expert mode const showApproveFlow = - !error && + !swapInputError && (approval === ApprovalState.NOT_APPROVED || approval === ApprovalState.PENDING || (approvalSubmitted && approval === ApprovalState.APPROVED)) && - !(priceImpactSeverity > 3 && !expertMode) - - function modalHeader() { - return ( - - ) - } - - function modalBottom() { - return ( - 2 ? 'Swap Anyway' : 'Confirm Swap'} - showInverted={showInverted} - severity={priceImpactSeverity} - setShowInverted={setShowInverted} - onSwap={onSwap} - realizedLPFee={realizedLPFee} - parsedAmounts={parsedAmounts} - priceImpactWithoutFee={priceImpactWithoutFee} - slippageAdjustedAmounts={slippageAdjustedAmounts} - trade={trade} - /> - ) - } - - // text to show while loading - const pendingText = `Swapping ${parsedAmounts[Field.INPUT]?.toSignificant(6)} ${ - currencies[Field.INPUT]?.symbol - } for ${parsedAmounts[Field.OUTPUT]?.toSignificant(6)} ${currencies[Field.OUTPUT]?.symbol}` + !(priceImpactSeverity > 3 && !isExpertMode) const [dismissedToken0] = useTokenWarningDismissal(chainId, currencies[Field.INPUT]) const [dismissedToken1] = useTokenWarningDismissal(chainId, currencies[Field.OUTPUT]) const showWarning = (!dismissedToken0 && !!currencies[Field.INPUT]) || (!dismissedToken1 && !!currencies[Field.OUTPUT]) + const handleConfirmDismiss = useCallback(() => { + setSwapState({ showConfirm: false, tradeToConfirm, attemptingTxn, swapErrorMessage, txHash }) + // if there was a tx hash, we want to clear the input + if (txHash) { + onUserInput(Field.INPUT, '') + } + }, [attemptingTxn, onUserInput, swapErrorMessage, tradeToConfirm, txHash]) + + const handleAcceptChanges = useCallback(() => { + setSwapState({ tradeToConfirm: trade, swapErrorMessage, txHash, attemptingTxn, showConfirm }) + }, [attemptingTxn, showConfirm, swapErrorMessage, trade, txHash]) + return ( <> {showWarning && } - + - { - setShowConfirm(false) - // if there was a tx hash, we want to clear the input - if (txHash) { - onUserInput(Field.INPUT, '') - } - setTxHash('') - }} + trade={trade} + originalTrade={tradeToConfirm} + onAcceptChanges={handleAcceptChanges} attemptingTxn={attemptingTxn} - hash={txHash} - topContent={modalHeader} - bottomContent={modalBottom} - pendingText={pendingText} + txHash={txHash} + recipient={recipient} + allowedSlippage={allowedSlippage} + onConfirm={handleSwap} + swapErrorMessage={swapErrorMessage} + onDismiss={handleConfirmDismiss} /> @@ -373,8 +365,9 @@ export default function Swap() { {!account ? ( Connect Wallet ) : showWrap ? ( - - {wrapError ?? (wrapType === WrapType.WRAP ? 'Wrap' : wrapType === WrapType.UNWRAP ? 'Unwrap' : null)} + + {wrapInputError ?? + (wrapType === WrapType.WRAP ? 'Wrap' : wrapType === WrapType.UNWRAP ? 'Unwrap' : null)} ) : noRoute && userHasSpecifiedInputOutput ? ( @@ -398,15 +391,27 @@ export default function Swap() { { - expertMode ? onSwap() : setShowConfirm(true) + if (isExpertMode) { + handleSwap() + } else { + setSwapState({ + tradeToConfirm: trade, + attemptingTxn: false, + swapErrorMessage: undefined, + showConfirm: true, + txHash: undefined + }) + } }} width="48%" id="swap-button" - disabled={!isValid || approval !== ApprovalState.APPROVED || (priceImpactSeverity > 3 && !expertMode)} + disabled={ + !isValid || approval !== ApprovalState.APPROVED || (priceImpactSeverity > 3 && !isExpertMode) + } error={isValid && priceImpactSeverity > 2} > - {priceImpactSeverity > 3 && !expertMode + {priceImpactSeverity > 3 && !isExpertMode ? `Price Impact High` : `Swap${priceImpactSeverity > 2 ? ' Anyway' : ''}`} @@ -415,21 +420,32 @@ export default function Swap() { ) : ( { - expertMode ? onSwap() : setShowConfirm(true) + if (isExpertMode) { + handleSwap() + } else { + setSwapState({ + tradeToConfirm: trade, + attemptingTxn: false, + swapErrorMessage: undefined, + showConfirm: true, + txHash: undefined + }) + } }} id="swap-button" - disabled={!isValid || (priceImpactSeverity > 3 && !expertMode)} - error={isValid && priceImpactSeverity > 2} + disabled={!isValid || (priceImpactSeverity > 3 && !isExpertMode) || !!swapCallbackError} + error={isValid && priceImpactSeverity > 2 && !swapCallbackError} > - {error - ? error - : priceImpactSeverity > 3 && !expertMode + {swapInputError + ? swapInputError + : priceImpactSeverity > 3 && !isExpertMode ? `Price Impact Too High` : `Swap${priceImpactSeverity > 2 ? ' Anyway' : ''}`} )} + {isExpertMode && swapErrorMessage ? : null} {betterTradeLinkVersion && } diff --git a/src/state/mint/hooks.ts b/src/state/mint/hooks.ts index a587ff120bb..c7d5fbf121e 100644 --- a/src/state/mint/hooks.ts +++ b/src/state/mint/hooks.ts @@ -50,9 +50,10 @@ export function useDerivedMintInfo( // pair const [pairState, pair] = usePair(currencies[Field.CURRENCY_A], currencies[Field.CURRENCY_B]) + const totalSupply = useTotalSupply(pair?.liquidityToken) + const noLiquidity: boolean = - pairState === PairState.NOT_EXISTS || - Boolean(pair && JSBI.equal(pair.reserve0.raw, ZERO) && JSBI.equal(pair.reserve1.raw, ZERO)) + pairState === PairState.NOT_EXISTS || Boolean(totalSupply && JSBI.equal(totalSupply.raw, ZERO)) // balances const balances = useCurrencyBalances(account ?? undefined, [ @@ -94,16 +95,20 @@ export function useDerivedMintInfo( [Field.CURRENCY_B]: independentField === Field.CURRENCY_A ? dependentAmount : independentAmount } + const token0Price = pair?.token0Price const price = useMemo(() => { - const { [Field.CURRENCY_A]: currencyAAmount, [Field.CURRENCY_B]: currencyBAmount } = parsedAmounts - if (currencyAAmount && currencyBAmount) { - return new Price(currencyAAmount.currency, currencyBAmount.currency, currencyAAmount.raw, currencyBAmount.raw) + if (noLiquidity) { + const { [Field.CURRENCY_A]: currencyAAmount, [Field.CURRENCY_B]: currencyBAmount } = parsedAmounts + if (currencyAAmount && currencyBAmount) { + return new Price(currencyAAmount.currency, currencyBAmount.currency, currencyAAmount.raw, currencyBAmount.raw) + } + return + } else { + return token0Price } - return - }, [parsedAmounts]) + }, [noLiquidity, token0Price, parsedAmounts]) // liquidity minted - const totalSupply = useTotalSupply(pair?.liquidityToken) const liquidityMinted = useMemo(() => { const { [Field.CURRENCY_A]: currencyAAmount, [Field.CURRENCY_B]: currencyBAmount } = parsedAmounts const [tokenAmountA, tokenAmountB] = [ diff --git a/src/state/multicall/updater.tsx b/src/state/multicall/updater.tsx index e0a51571dda..a360f7652b6 100644 --- a/src/state/multicall/updater.tsx +++ b/src/state/multicall/updater.tsx @@ -1,11 +1,11 @@ import { Contract } from '@ethersproject/contracts' -import { useEffect, useMemo } from 'react' +import { useEffect, useMemo, useRef } from 'react' import { useDispatch, useSelector } from 'react-redux' import { useActiveWeb3React } from '../../hooks' import { useMulticallContract } from '../../hooks/useContract' import useDebounce from '../../hooks/useDebounce' import chunkArray from '../../utils/chunkArray' -import { retry } from '../../utils/retry' +import { CancelledError, retry, RetryableError } from '../../utils/retry' import { useBlockNumber } from '../application/hooks' import { AppDispatch, AppState } from '../index' import { @@ -30,11 +30,17 @@ async function fetchChunk( chunk: Call[], minBlockNumber: number ): Promise<{ results: string[]; blockNumber: number }> { - const [resultsBlockNumber, returnData] = await multicallContract.aggregate( - chunk.map(obj => [obj.address, obj.callData]) - ) + console.debug('Fetching chunk', multicallContract, chunk, minBlockNumber) + let resultsBlockNumber, returnData + try { + ;[resultsBlockNumber, returnData] = await multicallContract.aggregate(chunk.map(obj => [obj.address, obj.callData])) + } catch (error) { + console.debug('Failed to fetch chunk inside retry', error) + throw error + } if (resultsBlockNumber.toNumber() < minBlockNumber) { - throw new Error('Fetched for old block number') + console.debug(`Fetched results for old block number: ${resultsBlockNumber.toString()} vs. ${minBlockNumber}`) + throw new RetryableError('Fetched for old block number') } return { results: returnData, blockNumber: resultsBlockNumber.toNumber() } } @@ -112,6 +118,7 @@ export default function Updater() { const latestBlockNumber = useBlockNumber() const { chainId } = useActiveWeb3React() const multicallContract = useMulticallContract() + const cancellations = useRef<{ blockNumber: number; cancellations: (() => void)[] }>() const listeningKeys: { [callKey: string]: number } = useMemo(() => { return activeListeningKeys(debouncedListeners, chainId) @@ -134,6 +141,10 @@ export default function Updater() { const chunkedCalls = chunkArray(calls, CALL_CHUNK_SIZE) + if (cancellations.current?.blockNumber !== latestBlockNumber) { + cancellations.current?.cancellations?.forEach(c => c()) + } + dispatch( fetchingMulticallResults({ calls, @@ -142,38 +153,52 @@ export default function Updater() { }) ) - chunkedCalls.forEach((chunk, index) => - // todo: cancel retries when the block number updates - retry(() => fetchChunk(multicallContract, chunk, latestBlockNumber), { n: 10, minWait: 2500, maxWait: 5000 }) - .then(({ results: returnData, blockNumber: fetchBlockNumber }) => { - // accumulates the length of all previous indices - const firstCallKeyIndex = chunkedCalls.slice(0, index).reduce((memo, curr) => memo + curr.length, 0) - const lastCallKeyIndex = firstCallKeyIndex + returnData.length - - dispatch( - updateMulticallResults({ - chainId, - results: outdatedCallKeys - .slice(firstCallKeyIndex, lastCallKeyIndex) - .reduce<{ [callKey: string]: string | null }>((memo, callKey, i) => { - memo[callKey] = returnData[i] ?? null - return memo - }, {}), - blockNumber: fetchBlockNumber - }) - ) - }) - .catch((error: any) => { - console.error('Failed to fetch multicall chunk', chunk, chainId, error) - dispatch( - errorFetchingMulticallResults({ - calls: chunk, - chainId, - fetchingBlockNumber: latestBlockNumber - }) - ) + cancellations.current = { + blockNumber: latestBlockNumber, + cancellations: chunkedCalls.map((chunk, index) => { + const { cancel, promise } = retry(() => fetchChunk(multicallContract, chunk, latestBlockNumber), { + n: Infinity, + minWait: 2500, + maxWait: 3500 }) - ) + promise + .then(({ results: returnData, blockNumber: fetchBlockNumber }) => { + cancellations.current = { cancellations: [], blockNumber: latestBlockNumber } + + // accumulates the length of all previous indices + const firstCallKeyIndex = chunkedCalls.slice(0, index).reduce((memo, curr) => memo + curr.length, 0) + const lastCallKeyIndex = firstCallKeyIndex + returnData.length + + dispatch( + updateMulticallResults({ + chainId, + results: outdatedCallKeys + .slice(firstCallKeyIndex, lastCallKeyIndex) + .reduce<{ [callKey: string]: string | null }>((memo, callKey, i) => { + memo[callKey] = returnData[i] ?? null + return memo + }, {}), + blockNumber: fetchBlockNumber + }) + ) + }) + .catch((error: any) => { + if (error instanceof CancelledError) { + console.debug('Cancelled fetch for blockNumber', latestBlockNumber) + return + } + console.error('Failed to fetch multicall chunk', chunk, chainId, error) + dispatch( + errorFetchingMulticallResults({ + calls: chunk, + chainId, + fetchingBlockNumber: latestBlockNumber + }) + ) + }) + return cancel + }) + } }, [chainId, multicallContract, dispatch, serializedOutdatedCallKeys, latestBlockNumber]) return null diff --git a/src/state/swap/hooks.ts b/src/state/swap/hooks.ts index e8a5f1ed543..fa4d6dc43ab 100644 --- a/src/state/swap/hooks.ts +++ b/src/state/swap/hooks.ts @@ -94,7 +94,7 @@ export function useDerivedSwapInfo(): { currencyBalances: { [field in Field]?: CurrencyAmount } parsedAmount: CurrencyAmount | undefined v2Trade: Trade | undefined - error?: string + inputError?: string v1Trade: Trade | undefined } { const { account } = useActiveWeb3React() @@ -140,21 +140,21 @@ export function useDerivedSwapInfo(): { // get link to trade on v1, if a better rate exists const v1Trade = useV1Trade(isExactIn, currencies[Field.INPUT], currencies[Field.OUTPUT], parsedAmount) - let error: string | undefined + let inputError: string | undefined if (!account) { - error = 'Connect Wallet' + inputError = 'Connect Wallet' } if (!parsedAmount) { - error = error ?? 'Enter an amount' + inputError = inputError ?? 'Enter an amount' } if (!currencies[Field.INPUT] || !currencies[Field.OUTPUT]) { - error = error ?? 'Select a token' + inputError = inputError ?? 'Select a token' } if (!to) { - error = error ?? 'Enter a recipient' + inputError = inputError ?? 'Enter a recipient' } const [allowedSlippage] = useUserSlippageTolerance() @@ -177,7 +177,7 @@ export function useDerivedSwapInfo(): { ] if (balanceIn && amountIn && balanceIn.lessThan(amountIn)) { - error = 'Insufficient ' + amountIn.currency.symbol + ' balance' + inputError = 'Insufficient ' + amountIn.currency.symbol + ' balance' } return { @@ -185,7 +185,7 @@ export function useDerivedSwapInfo(): { currencyBalances, parsedAmount, v2Trade: v2Trade ?? undefined, - error, + inputError, v1Trade } } diff --git a/src/theme/index.tsx b/src/theme/index.tsx index 19c9acc2fa1..b46511a1da7 100644 --- a/src/theme/index.tsx +++ b/src/theme/index.tsx @@ -55,7 +55,7 @@ export function colors(darkMode: boolean): Colors { bg5: darkMode ? '#565A69' : '#888D9B', //specialty colors - modalBG: darkMode ? 'rgba(0,0,0,0.85)' : 'rgba(0,0,0,0.6)', + modalBG: darkMode ? 'rgba(0,0,0,42.5)' : 'rgba(0,0,0,0.3)', advancedBG: darkMode ? 'rgba(0,0,0,0.1)' : 'rgba(255,255,255,0.6)', //primary colors diff --git a/src/utils/index.ts b/src/utils/index.ts index fb04ac52a5b..48d417fac9d 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -91,7 +91,7 @@ export function getContract(address: string, ABI: any, library: Web3Provider, ac } // account is optional -export function getRouterContract(_: number, library: Web3Provider, account?: string) { +export function getRouterContract(_: number, library: Web3Provider, account?: string): Contract { return getContract(ROUTER_ADDRESS, IUniswapV2Router02ABI, library, account) } diff --git a/src/utils/isZero.ts b/src/utils/isZero.ts new file mode 100644 index 00000000000..1b84e675606 --- /dev/null +++ b/src/utils/isZero.ts @@ -0,0 +1,7 @@ +/** + * Returns true if the string value is zero in hex + * @param hexNumberString + */ +export default function isZero(hexNumberString: string) { + return /^0x0*$/.test(hexNumberString) +} diff --git a/src/utils/retry.test.ts b/src/utils/retry.test.ts index 788dc55bd57..2ea7ca39304 100644 --- a/src/utils/retry.test.ts +++ b/src/utils/retry.test.ts @@ -1,26 +1,45 @@ -import { retry } from './retry' +import { retry, RetryableError } from './retry' describe('retry', () => { - function makeFn(fails: number, result: T): () => Promise { + function makeFn(fails: number, result: T, retryable = true): () => Promise { return async () => { if (fails > 0) { fails-- - throw new Error('failure') + throw retryable ? new RetryableError('failure') : new Error('bad failure') } return result } } + it('fails for non-retryable error', async () => { + await expect(retry(makeFn(1, 'abc', false), { n: 3, maxWait: 0, minWait: 0 }).promise).rejects.toThrow( + 'bad failure' + ) + }) + it('works after one fail', async () => { - await expect(retry(makeFn(1, 'abc'), { n: 3, maxWait: 0, minWait: 0 })).resolves.toEqual('abc') + await expect(retry(makeFn(1, 'abc'), { n: 3, maxWait: 0, minWait: 0 }).promise).resolves.toEqual('abc') }) it('works after two fails', async () => { - await expect(retry(makeFn(2, 'abc'), { n: 3, maxWait: 0, minWait: 0 })).resolves.toEqual('abc') + await expect(retry(makeFn(2, 'abc'), { n: 3, maxWait: 0, minWait: 0 }).promise).resolves.toEqual('abc') }) it('throws if too many fails', async () => { - await expect(retry(makeFn(4, 'abc'), { n: 3, maxWait: 0, minWait: 0 })).rejects.toThrow('failure') + await expect(retry(makeFn(4, 'abc'), { n: 3, maxWait: 0, minWait: 0 }).promise).rejects.toThrow('failure') + }) + + it('cancel causes promise to reject', async () => { + const { promise, cancel } = retry(makeFn(2, 'abc'), { n: 3, minWait: 100, maxWait: 100 }) + cancel() + await expect(promise).rejects.toThrow('Cancelled') + }) + + it('cancel no-op after complete', async () => { + const { promise, cancel } = retry(makeFn(0, 'abc'), { n: 3, minWait: 100, maxWait: 100 }) + // defer + setTimeout(cancel, 0) + await expect(promise).resolves.toEqual('abc') }) async function checkTime(fn: () => Promise, min: number, max: number) { @@ -36,7 +55,7 @@ describe('retry', () => { for (let i = 0; i < 10; i++) { promises.push( checkTime( - () => expect(retry(makeFn(4, 'abc'), { n: 3, maxWait: 100, minWait: 50 })).rejects.toThrow('failure'), + () => expect(retry(makeFn(4, 'abc'), { n: 3, maxWait: 100, minWait: 50 }).promise).rejects.toThrow('failure'), 150, 305 ) diff --git a/src/utils/retry.ts b/src/utils/retry.ts index 9a92cb5eb07..e2d85302876 100644 --- a/src/utils/retry.ts +++ b/src/utils/retry.ts @@ -6,6 +6,20 @@ function waitRandom(min: number, max: number): Promise { return wait(min + Math.round(Math.random() * Math.max(0, max - min))) } +/** + * This error is thrown if the function is cancelled before completing + */ +export class CancelledError extends Error { + constructor() { + super('Cancelled') + } +} + +/** + * Throw this error if the function should retry + */ +export class RetryableError extends Error {} + /** * Retries the function that returns the promise until the promise successfully resolves up to n retries * @param fn function to retry @@ -13,13 +27,43 @@ function waitRandom(min: number, max: number): Promise { * @param minWait min wait between retries in ms * @param maxWait max wait between retries in ms */ -// todo: support cancelling the retry export function retry( fn: () => Promise, - { n = 3, minWait = 500, maxWait = 1000 }: { n?: number; minWait?: number; maxWait?: number } = {} -): Promise { - return fn().catch(error => { - if (n === 0) throw error - return waitRandom(minWait, maxWait).then(() => retry(fn, { n: n - 1, minWait, maxWait })) + { n, minWait, maxWait }: { n: number; minWait: number; maxWait: number } +): { promise: Promise; cancel: () => void } { + let completed = false + let rejectCancelled: (error: Error) => void + const promise = new Promise(async (resolve, reject) => { + rejectCancelled = reject + while (true) { + let result: T + try { + result = await fn() + if (!completed) { + resolve(result) + completed = true + } + break + } catch (error) { + if (completed) { + break + } + if (n <= 0 || !(error instanceof RetryableError)) { + reject(error) + completed = true + break + } + n-- + } + await waitRandom(minWait, maxWait) + } }) + return { + promise, + cancel: () => { + if (completed) return + completed = true + rejectCancelled(new CancelledError()) + } + } } diff --git a/yarn.lock b/yarn.lock index b9fcaaab681..67b7f1bb4b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1296,7 +1296,7 @@ bech32 "^1.1.3" crypto-addr-codec "^0.1.7" -"@ethersproject/abi@>=5.0.0-beta.137", "@ethersproject/abi@^5.0.0": +"@ethersproject/abi@^5.0.0": version "5.0.2" resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.0.2.tgz#7fe8f080aa1483fe32cd27bb5b8f2019266af1e2" integrity sha512-Z+5f7xOgtRLu/W2l9Ry5xF7ehh9QVQ0m1vhynmTcS7DMfHgqTd1/PDFC62aw91ZPRCRZsYdZJu8ymokC5e1JSw== @@ -1311,7 +1311,7 @@ "@ethersproject/properties" "^5.0.0" "@ethersproject/strings" "^5.0.0" -"@ethersproject/abstract-provider@>=5.0.0-beta.131", "@ethersproject/abstract-provider@>=5.0.0-beta.139", "@ethersproject/abstract-provider@^5.0.0": +"@ethersproject/abstract-provider@^5.0.0": version "5.0.2" resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.0.2.tgz#9b4e8f4870f0691463e8d5b092c95dd5275c635d" integrity sha512-U1s60+nG02x8FKNMoVNI6MG8SguWCoG9HJtwOqWZ38LBRMsDV4c0w4izKx98kcsN3wXw4U2/YAyJ9LlH7+/hkg== @@ -1324,7 +1324,7 @@ "@ethersproject/transactions" "^5.0.0" "@ethersproject/web" "^5.0.0" -"@ethersproject/abstract-signer@>=5.0.0-beta.132", "@ethersproject/abstract-signer@>=5.0.0-beta.142", "@ethersproject/abstract-signer@^5.0.0": +"@ethersproject/abstract-signer@^5.0.0": version "5.0.2" resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.0.2.tgz#5776f888fda816de1d08ddb0e74778ecb9590f69" integrity sha512-CzzXbeqKlgayE4YTnvvreGBG3n+HxakGXrxaGM6LjBZnOOIVSYi6HMFG8ZXls7UspRY4hvMrtnKEJKDCOngSBw== @@ -1335,19 +1335,7 @@ "@ethersproject/logger" "^5.0.0" "@ethersproject/properties" "^5.0.0" -"@ethersproject/address@5.0.0-beta.134": - version "5.0.0-beta.134" - resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.0.0-beta.134.tgz#9c1790c87b763dc547ac12e2dbc9fa78d0799a71" - integrity sha512-FHhUVJTUIg2pXvOOhIt8sB1cQbcwrzZKzf9CPV7JM1auli20nGoYhyMFYGK7u++GXzTMJduIkU1OwlIBupewDw== - dependencies: - "@ethersproject/bignumber" ">=5.0.0-beta.130" - "@ethersproject/bytes" ">=5.0.0-beta.129" - "@ethersproject/keccak256" ">=5.0.0-beta.127" - "@ethersproject/logger" ">=5.0.0-beta.129" - "@ethersproject/rlp" ">=5.0.0-beta.126" - bn.js "^4.4.0" - -"@ethersproject/address@>=5.0.0-beta.128", "@ethersproject/address@>=5.0.0-beta.134", "@ethersproject/address@^5.0.0": +"@ethersproject/address@^5.0.0": version "5.0.2" resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.0.2.tgz#80d0ddfb7d4bd0d32657747fa4bdd2defef2e00a" integrity sha512-+rz26RKj7ujGfQynys4V9VJRbR+wpC6eL8F22q3raWMH3152Ha31GwJPWzxE/bEA+43M/zTNVwY0R53gn53L2Q== @@ -1374,17 +1362,7 @@ "@ethersproject/bytes" "^5.0.0" "@ethersproject/properties" "^5.0.0" -"@ethersproject/bignumber@5.0.0-beta.138": - version "5.0.0-beta.138" - resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.0.0-beta.138.tgz#a635f2f9a6f1b262cc38e1c7ee561fb13d79fda4" - integrity sha512-DTlOEJw6jAFz7/qkY8p4mPGGHVwgYUUC5rk1Pbg2/gR/gHPFDim+uBY+XGavh0QSWd1i3hXKafVPre92j4fs5g== - dependencies: - "@ethersproject/bytes" ">=5.0.0-beta.129" - "@ethersproject/logger" ">=5.0.0-beta.129" - "@ethersproject/properties" ">=5.0.0-beta.131" - bn.js "^4.4.0" - -"@ethersproject/bignumber@>=5.0.0-beta.130", "@ethersproject/bignumber@>=5.0.0-beta.138", "@ethersproject/bignumber@^5.0.0": +"@ethersproject/bignumber@^5.0.0": version "5.0.5" resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.0.5.tgz#31bd7e75aad46ace345fae69b1f5bb120906af1b" integrity sha512-24ln7PV0g8ZzjcVZiLW9Wod0i+XCmK6zKkAaxw5enraTIT1p7gVOcSXFSzNQ9WYAwtiFQPvvA+TIO2oEITZNJA== @@ -1393,43 +1371,20 @@ "@ethersproject/logger" "^5.0.0" bn.js "^4.4.0" -"@ethersproject/bytes@>=5.0.0-beta.129", "@ethersproject/bytes@>=5.0.0-beta.137", "@ethersproject/bytes@^5.0.0": +"@ethersproject/bytes@^5.0.0": version "5.0.3" resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.0.3.tgz#b3769963ae0188a35713d343890a903bda20af9c" integrity sha512-AyPMAlY+Amaw4Zfp8OAivm1xYPI8mqiUYmEnSUk1CnS2NrQGHEMmFJFiOJdS3gDDpgSOFhWIjZwxKq2VZpqNTA== dependencies: "@ethersproject/logger" "^5.0.0" -"@ethersproject/constants@5.0.0-beta.133": - version "5.0.0-beta.133" - resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.0.0-beta.133.tgz#af4ccd7232f3ed73aebe066a695ede32c497a394" - integrity sha512-VCTpk3AF00mlWQw1vg+fI6qCo0qO5EVWK574t4HNBKW6X748jc9UJPryKUz9JgZ64ZQupyLM92wHilsG/YTpNQ== - dependencies: - "@ethersproject/bignumber" ">=5.0.0-beta.130" - -"@ethersproject/constants@>=5.0.0-beta.128", "@ethersproject/constants@^5.0.0": +"@ethersproject/constants@^5.0.0": version "5.0.2" resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.0.2.tgz#f7ac0b320e2bbec1a5950da075015f8bc4e8fed1" integrity sha512-nNoVlNP6bgpog7pQ2EyD1xjlaXcy1Cl4kK5v1KoskHj58EtB6TK8M8AFGi3GgHTdMldfT4eN3OsoQ/CdOTVNFA== dependencies: "@ethersproject/bignumber" "^5.0.0" -"@ethersproject/contracts@5.0.0-beta.151": - version "5.0.0-beta.151" - resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.0.0-beta.151.tgz#4cee195c01b6865e8e7d8849777427864819e931" - integrity sha512-ELmsmZ/vE/rz5ydJNlU04aXsh7sw22tzmy7vM5JXCgMm5nEFhGoRF+dRIrUFCuUV2Mxe0bALN11qGkRqFKlXRQ== - dependencies: - "@ethersproject/abi" ">=5.0.0-beta.137" - "@ethersproject/abstract-provider" ">=5.0.0-beta.131" - "@ethersproject/abstract-signer" ">=5.0.0-beta.132" - "@ethersproject/address" ">=5.0.0-beta.128" - "@ethersproject/bignumber" ">=5.0.0-beta.130" - "@ethersproject/bytes" ">=5.0.0-beta.129" - "@ethersproject/constants" ">=5.0.0-beta.128" - "@ethersproject/logger" ">=5.0.0-beta.129" - "@ethersproject/properties" ">=5.0.0-beta.131" - "@ethersproject/transactions" ">=5.0.0-beta.128" - "@ethersproject/contracts@^5.0.0": version "5.0.2" resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.0.2.tgz#f19ed8335ceeb6abb60f5d45641f0a2a62b6fbc5" @@ -1445,17 +1400,17 @@ "@ethersproject/logger" "^5.0.0" "@ethersproject/properties" "^5.0.0" -"@ethersproject/experimental@5.0.0-beta.141": - version "5.0.0-beta.141" - resolved "https://registry.yarnpkg.com/@ethersproject/experimental/-/experimental-5.0.0-beta.141.tgz#2dc7e1f1c33f818cda1799b63b2ecb9e226f46bb" - integrity sha512-SFUfN5c6Wcpq18ZZBQdpf6ie50aIkz3jco/8PPv5PFkRSIrGTP4HfobAu6A3eORd/tnvlgm1H2XWOLuRJ3WujA== +"@ethersproject/experimental@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@ethersproject/experimental/-/experimental-5.0.1.tgz#c488d43092543c49e4cb70fbaeafad0956c826e0" + integrity sha512-PAVv/i4PwO2L4E2PWgPgEGP9FOt/5qaTv7W9YDTSL7Tq2zfp41jolRBI1o7X0UdnPWUe54TiibOp4xJR65Dwpw== dependencies: "@ensdomains/address-encoder" "^0.1.2" - "@ethersproject/web" ">=5.0.0-beta.138" - ethers ">=5.0.0-beta.186" - scrypt-js "3.0.0" + "@ethersproject/web" "^5.0.0" + ethers "^5.0.0" + scrypt-js "3.0.1" -"@ethersproject/hash@>=5.0.0-beta.128", "@ethersproject/hash@>=5.0.0-beta.133", "@ethersproject/hash@^5.0.0": +"@ethersproject/hash@^5.0.0": version "5.0.2" resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.0.2.tgz#6d69558786961836d530b8b4a8714eac5388aec7" integrity sha512-dWGvNwmVRX2bxoQQ3ciMw46Vzl1nqfL+5R8+2ZxsRXD3Cjgw1dL2mdjJF7xMMWPvPdrlhKXWSK0gb8VLwHZ8Cw== @@ -1465,7 +1420,7 @@ "@ethersproject/logger" "^5.0.0" "@ethersproject/strings" "^5.0.0" -"@ethersproject/hdnode@>=5.0.0-beta.139", "@ethersproject/hdnode@^5.0.0": +"@ethersproject/hdnode@^5.0.0": version "5.0.2" resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.0.2.tgz#c4f2152590a64822d0c0feb90f09cc247af657e0" integrity sha512-QAUI5tfseTFqv00Vnbwzofqse81wN9TaL+x5GufTHIHJXgVdguxU+l39E3VYDCmO+eVAA6RCn5dJgeyra+PU2g== @@ -1483,7 +1438,7 @@ "@ethersproject/transactions" "^5.0.0" "@ethersproject/wordlists" "^5.0.0" -"@ethersproject/json-wallets@>=5.0.0-beta.138", "@ethersproject/json-wallets@^5.0.0": +"@ethersproject/json-wallets@^5.0.0": version "5.0.3" resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.0.3.tgz#072021fe79f69c9ca1300f780abd9b9d0c8ea42e" integrity sha512-VfDXn5ylugkfiM6SrvQfhX9oAHVU5dsNpRw8PjjTCn4k5E2JuVRO5A8sibkYXDhcBmRISZIWqclIxka6FI/chg== @@ -1502,7 +1457,7 @@ aes-js "3.0.0" scrypt-js "3.0.1" -"@ethersproject/keccak256@>=5.0.0-beta.127", "@ethersproject/keccak256@>=5.0.0-beta.131", "@ethersproject/keccak256@^5.0.0", "@ethersproject/keccak256@^5.0.0-beta.130": +"@ethersproject/keccak256@^5.0.0", "@ethersproject/keccak256@^5.0.0-beta.130": version "5.0.2" resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.0.2.tgz#7ed4a95bb45ee502cf4532223833740a83602797" integrity sha512-MbroXutc0gPNYIrUjS4Aw0lDuXabdzI7+l7elRWr1G6G+W0v00e/3gbikWkCReGtt2Jnt4lQSgnflhDwQGcIhA== @@ -1510,19 +1465,12 @@ "@ethersproject/bytes" "^5.0.0" js-sha3 "0.5.7" -"@ethersproject/logger@>=5.0.0-beta.129", "@ethersproject/logger@>=5.0.0-beta.137", "@ethersproject/logger@^5.0.0": +"@ethersproject/logger@^5.0.0": version "5.0.4" resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.0.4.tgz#09fa4765b5691233e3afb6617cb38a700f9dd2e4" integrity sha512-alA2LiAy1LdQ/L1SA9ajUC7MvGAEQLsICEfKK4erX5qhkXE1LwLSPIzobtOWFsMHf2yrXGKBLnnpuVHprI3sAw== -"@ethersproject/networks@5.0.0-beta.136": - version "5.0.0-beta.136" - resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.0.0-beta.136.tgz#8d6fdae297c0ce7ebe1893e601c4a57f7e38dc7a" - integrity sha512-skMDix0LVOhpfCItbg6Z1fXLK6vAtUkzAKaslDxVczEPUvjQ0kiJ5ceurmL+ROOO1owURGxUac5BrIarbO7Zgw== - dependencies: - "@ethersproject/logger" ">=5.0.0-beta.129" - -"@ethersproject/networks@>=5.0.0-beta.129", "@ethersproject/networks@^5.0.0": +"@ethersproject/networks@^5.0.0": version "5.0.2" resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.0.2.tgz#a49e82cf071e3618e87e3c5d69fdbcf54dc6766c" integrity sha512-T7HVd62D4izNU2tDHf6xUDo7k4JOGX4Lk7vDmVcDKrepSWwL2OmGWrqSlkRe2a1Dnz4+1VPE6fb6+KsmSRe82g== @@ -1537,35 +1485,13 @@ "@ethersproject/bytes" "^5.0.0" "@ethersproject/sha2" "^5.0.0" -"@ethersproject/properties@>=5.0.0-beta.131", "@ethersproject/properties@>=5.0.0-beta.140", "@ethersproject/properties@^5.0.0": +"@ethersproject/properties@^5.0.0": version "5.0.2" resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.0.2.tgz#2facb62d2f2d968c7b3d0befa5bcc884cc565d3b" integrity sha512-FxAisPGAOACQjMJzewl9OJG6lsGCPTm5vpUMtfeoxzAlAb2lv+kHzQPUh9h4jfAILzE8AR1jgXMzRmlhwyra1Q== dependencies: "@ethersproject/logger" "^5.0.0" -"@ethersproject/providers@5.0.0-beta.162": - version "5.0.0-beta.162" - resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.0.0-beta.162.tgz#cb4efbeea2c776d0ce97712e05ffaa3e0a8df215" - integrity sha512-mXT5pQLOmRkXP5pza6TuV9RitaI50b1O2r0og8VzUIHcjO9bq4yppVbWs0Zcxn4KQAiIrAd2xXbYE3q2KdfUYQ== - dependencies: - "@ethersproject/abstract-provider" ">=5.0.0-beta.131" - "@ethersproject/abstract-signer" ">=5.0.0-beta.132" - "@ethersproject/address" ">=5.0.0-beta.128" - "@ethersproject/bignumber" ">=5.0.0-beta.130" - "@ethersproject/bytes" ">=5.0.0-beta.129" - "@ethersproject/constants" ">=5.0.0-beta.128" - "@ethersproject/hash" ">=5.0.0-beta.128" - "@ethersproject/logger" ">=5.0.0-beta.129" - "@ethersproject/networks" ">=5.0.0-beta.129" - "@ethersproject/properties" ">=5.0.0-beta.131" - "@ethersproject/random" ">=5.0.0-beta.128" - "@ethersproject/rlp" ">=5.0.0-beta.126" - "@ethersproject/strings" ">=5.0.0-beta.130" - "@ethersproject/transactions" ">=5.0.0-beta.128" - "@ethersproject/web" ">=5.0.0-beta.129" - ws "7.2.3" - "@ethersproject/providers@^5.0.0": version "5.0.5" resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.0.5.tgz#fa28498ce9683d1d99f6cb11e1a7fe8d4886e0ce" @@ -1588,7 +1514,7 @@ "@ethersproject/web" "^5.0.0" ws "7.2.3" -"@ethersproject/random@>=5.0.0-beta.128", "@ethersproject/random@>=5.0.0-beta.135", "@ethersproject/random@^5.0.0": +"@ethersproject/random@^5.0.0": version "5.0.2" resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.0.2.tgz#bb58aca69a85e8de506686117f050d03dac69023" integrity sha512-kLeS+6bwz37WR2zbe69gudyoGVoUzljQO0LhifnATsZ7rW0JZ9Zgt0h5aXY7tqFDo9TvdqeCwUFdp1t3T5Fkhg== @@ -1596,7 +1522,7 @@ "@ethersproject/bytes" "^5.0.0" "@ethersproject/logger" "^5.0.0" -"@ethersproject/rlp@>=5.0.0-beta.126", "@ethersproject/rlp@^5.0.0": +"@ethersproject/rlp@^5.0.0": version "5.0.2" resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.0.2.tgz#d6b550a2ac5e484f15f0f63337e522004d2e78cd" integrity sha512-oE0M5jqQ67fi2SuMcrpoewOpEuoXaD8M9JeR9md1bXRMvDYgKXUtDHs22oevpEOdnO2DPIRabp6MVHa4aDuWmw== @@ -1613,7 +1539,7 @@ "@ethersproject/logger" "^5.0.0" hash.js "1.1.3" -"@ethersproject/signing-key@>=5.0.0-beta.135", "@ethersproject/signing-key@^5.0.0": +"@ethersproject/signing-key@^5.0.0": version "5.0.3" resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.0.3.tgz#adb84360e147bfd336cb2fe114100120732dc10a" integrity sha512-5QPZaBRGCLzfVMbFb3LcVjNR0UbTXnwDHASnQYfbzwUOnFYHKxHsrcbl/5ONGoppgi8yXgOocKqlPCFycJJVWQ== @@ -1623,7 +1549,7 @@ "@ethersproject/properties" "^5.0.0" elliptic "6.5.3" -"@ethersproject/solidity@5.0.2", "@ethersproject/solidity@^5.0.0": +"@ethersproject/solidity@^5.0.0": version "5.0.2" resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.0.2.tgz#431cee341ec51e022bd897b93fef04521f414756" integrity sha512-RygurUe1hPW1LDYAPXy4471AklGWNnxgFWc3YUE6H11gzkit26jr6AyZH4Yyjw38eBBL6j0AOfQzMWm+NhxZ9g== @@ -1634,16 +1560,7 @@ "@ethersproject/sha2" "^5.0.0" "@ethersproject/strings" "^5.0.0" -"@ethersproject/strings@5.0.0-beta.136": - version "5.0.0-beta.136" - resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.0.0-beta.136.tgz#053cbf4f9f96a7537cbc50300597f2d707907f51" - integrity sha512-Hb9RvTrgGcOavHvtQZz+AuijB79BO3g1cfF2MeMfCU9ID4j3mbZv/olzDMS2pK9r4aERJpAS94AmlWzCgoY2LQ== - dependencies: - "@ethersproject/bytes" ">=5.0.0-beta.129" - "@ethersproject/constants" ">=5.0.0-beta.128" - "@ethersproject/logger" ">=5.0.0-beta.129" - -"@ethersproject/strings@>=5.0.0-beta.130", "@ethersproject/strings@^5.0.0": +"@ethersproject/strings@^5.0.0": version "5.0.2" resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.0.2.tgz#1753408c3c889813fd0992abd76393e3e47a2619" integrity sha512-oNa+xvSqsFU96ndzog0IBTtsRFGOqGpzrXJ7shXLBT7juVeSEyZA/sYs0DMZB5mJ9FEjHdZKxR/rTyBY91vuXg== @@ -1652,7 +1569,7 @@ "@ethersproject/constants" "^5.0.0" "@ethersproject/logger" "^5.0.0" -"@ethersproject/transactions@>=5.0.0-beta.128", "@ethersproject/transactions@>=5.0.0-beta.135", "@ethersproject/transactions@^5.0.0": +"@ethersproject/transactions@^5.0.0": version "5.0.2" resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.0.2.tgz#590ede71fc87b45be7bd46002e18ae52246a2347" integrity sha512-jZp0ZbbJlq4JLZY6qoMzNtp2HQsX6USQposi3ns0MPUdn3OdZJBDtrcO15r/2VS5t/K1e1GE5MI1HmMKlcTbbQ== @@ -1667,15 +1584,6 @@ "@ethersproject/rlp" "^5.0.0" "@ethersproject/signing-key" "^5.0.0" -"@ethersproject/units@5.0.0-beta.132": - version "5.0.0-beta.132" - resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.0.0-beta.132.tgz#54c03c821e515a09ef79a22704ad57994ee66c45" - integrity sha512-3GZDup1uTydvqaP5wpwoRF36irp6kx/gd3buPG+aoGWLPCoPjyk76OiGoxNQKfEaynOdZ7zG2lM8WevlBDJ57g== - dependencies: - "@ethersproject/bignumber" ">=5.0.0-beta.130" - "@ethersproject/constants" ">=5.0.0-beta.128" - "@ethersproject/logger" ">=5.0.0-beta.129" - "@ethersproject/units@^5.0.0": version "5.0.2" resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.0.2.tgz#de1461ff3ad2587e57bf367d056b6b72cfceda78" @@ -1685,27 +1593,6 @@ "@ethersproject/constants" "^5.0.0" "@ethersproject/logger" "^5.0.0" -"@ethersproject/wallet@5.0.0-beta.141": - version "5.0.0-beta.141" - resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.0.0-beta.141.tgz#2a4a72cf2423c6ac08c38b5faa28e72f8e9a4f03" - integrity sha512-N/69EgBOhRXYmDj91ZUrDK7V38Eb4mrC8OvUdmGEwjHVO3VIz0sH+Li1IDVRdyGSWYhoxfVRP650ObMzL9a7dQ== - dependencies: - "@ethersproject/abstract-provider" ">=5.0.0-beta.139" - "@ethersproject/abstract-signer" ">=5.0.0-beta.142" - "@ethersproject/address" ">=5.0.0-beta.134" - "@ethersproject/bignumber" ">=5.0.0-beta.138" - "@ethersproject/bytes" ">=5.0.0-beta.137" - "@ethersproject/hash" ">=5.0.0-beta.133" - "@ethersproject/hdnode" ">=5.0.0-beta.139" - "@ethersproject/json-wallets" ">=5.0.0-beta.138" - "@ethersproject/keccak256" ">=5.0.0-beta.131" - "@ethersproject/logger" ">=5.0.0-beta.137" - "@ethersproject/properties" ">=5.0.0-beta.140" - "@ethersproject/random" ">=5.0.0-beta.135" - "@ethersproject/signing-key" ">=5.0.0-beta.135" - "@ethersproject/transactions" ">=5.0.0-beta.135" - "@ethersproject/wordlists" ">=5.0.0-beta.136" - "@ethersproject/wallet@^5.0.0": version "5.0.2" resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.0.2.tgz#714ca8324c1b3b66e51b9b4e0358c882e88caf1d" @@ -1727,7 +1614,7 @@ "@ethersproject/transactions" "^5.0.0" "@ethersproject/wordlists" "^5.0.0" -"@ethersproject/web@>=5.0.0-beta.129", "@ethersproject/web@>=5.0.0-beta.138", "@ethersproject/web@^5.0.0": +"@ethersproject/web@^5.0.0": version "5.0.2" resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.0.2.tgz#6565b4c4fe2f56de9556d0e9a966c4ccc1b7b7da" integrity sha512-uAlcxdrAWB9PXZlb5NPzbOOt5/m9EJP2c6eLw15/PXPkNNohEIKvdXXOWdcQgTjZ0pcAaD/9mnJ6HXg7NbqXiw== @@ -1737,7 +1624,7 @@ "@ethersproject/properties" "^5.0.0" "@ethersproject/strings" "^5.0.0" -"@ethersproject/wordlists@>=5.0.0-beta.136", "@ethersproject/wordlists@^5.0.0": +"@ethersproject/wordlists@^5.0.0": version "5.0.2" resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.0.2.tgz#eded47314509c8608373fc2b22879ee2b71b7c7c" integrity sha512-6vKDQcjjpnfdSCr0+jNxpFH3ieKxUPkm29tQX2US7a3zT/sJU/BGlKBR7D8oOpwdE0hpkHhJyMlypRBK+A2avA== @@ -2704,92 +2591,92 @@ "@uniswap/lib" "1.1.1" "@uniswap/v2-core" "1.0.0" -"@walletconnect/client@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@walletconnect/client/-/client-1.1.0.tgz#f2454cba82da3d8c7375b2a5d9d47f34ed7348ec" - integrity sha512-pHxvUDCkD4oP3AFxYLU7yeE+qDZtcHF20b2K8/HNvyuyu3eWFX4jpHgx6FdvcIcFcAXGs5nk24zBUEO8p+axWg== +"@walletconnect/client@^1.1.1-alpha.0": + version "1.1.1-alpha.0" + resolved "https://registry.yarnpkg.com/@walletconnect/client/-/client-1.1.1-alpha.0.tgz#18362a6b05150f02adfd281ca2251539cd727606" + integrity sha512-/aOvwouwXgSMnAMypVlZB6MhIbLwEZOHF2Waa6CvcRRFYe9dA/LqI+vF/dABevg7B4R2q012ZF22NQmhZOVZsw== dependencies: - "@walletconnect/core" "^1.1.0" - "@walletconnect/iso-crypto" "^1.1.0" - "@walletconnect/types" "^1.1.0" - "@walletconnect/utils" "^1.1.0" + "@walletconnect/core" "^1.1.1-alpha.0" + "@walletconnect/iso-crypto" "^1.1.1-alpha.0" + "@walletconnect/types" "^1.1.1-alpha.0" + "@walletconnect/utils" "^1.1.1-alpha.0" -"@walletconnect/core@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-1.1.0.tgz#053f08b0ccfdfb14ccd27b7fd425d9849cedba14" - integrity sha512-Bhe4gnR6Az11u7OAOw0UDZKM6emUjIQtQ2PVdPDWke6ryC0DWMg9vTYbVPf3lDHBv5hy5eAyDst30N5E91SuYw== +"@walletconnect/core@^1.1.1-alpha.0": + version "1.1.1-alpha.0" + resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-1.1.1-alpha.0.tgz#ffc80babfe271ff7de07a1159f2e52e5a6487e34" + integrity sha512-EZf2aqB/nAouHX9T/niCcBxRTPat4B92hcHKKOhBZgKwXF4ajB0LfC1tXwhTDeQGt6PpJ1HLjtnCCJ7+/TLhJg== dependencies: - "@walletconnect/socket-transport" "^1.1.0" - "@walletconnect/types" "^1.1.0" - "@walletconnect/utils" "^1.1.0" + "@walletconnect/socket-transport" "^1.1.1-alpha.0" + "@walletconnect/types" "^1.1.1-alpha.0" + "@walletconnect/utils" "^1.1.1-alpha.0" -"@walletconnect/http-connection@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@walletconnect/http-connection/-/http-connection-1.1.0.tgz#c6650c12a07244d30f20647420cdcd8c69c6daca" - integrity sha512-ugxDW/NaSgn7rmdPZhrpJIS79gASLvzBnGHScMs8zpYDHwcFxh2DP3HTspC8o5FyMqjRlEGtNi4zSGKY6EOrkw== +"@walletconnect/http-connection@^1.1.1-alpha.0": + version "1.1.1-alpha.0" + resolved "https://registry.yarnpkg.com/@walletconnect/http-connection/-/http-connection-1.1.1-alpha.0.tgz#5d02efa2a4dd70d502bbf917adc6798ff068c2b0" + integrity sha512-IBAwBu9xCmnDMRNiHqaRHgbNibGf4tqtY5BfzU2I49Awmbk//H8TmZ4pDRlXe6/ADWkB2CFcsDZpdjRAkfAWvg== dependencies: - "@walletconnect/types" "^1.1.0" - "@walletconnect/utils" "^1.1.0" + "@walletconnect/types" "^1.1.1-alpha.0" + "@walletconnect/utils" "^1.1.1-alpha.0" xhr2-cookies "1.1.0" -"@walletconnect/iso-crypto@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@walletconnect/iso-crypto/-/iso-crypto-1.1.0.tgz#a8235049c1b239adcf9fc6a6c38b7e9ad13004a6" - integrity sha512-ttWLj4rTy2NGQnSAKnAar1LSrsJuCQ2JnQUl8hsgc9oTwXKgnRvtxGy2Kajoih/tNKnK959Ilj4WI2HaSJ9G1g== +"@walletconnect/iso-crypto@^1.1.1-alpha.0": + version "1.1.1-alpha.0" + resolved "https://registry.yarnpkg.com/@walletconnect/iso-crypto/-/iso-crypto-1.1.1-alpha.0.tgz#f18a336e310341427603b201f973bb8ee13800cd" + integrity sha512-A8U57SgskexAF9TBisfYvtXc+rhjSEakF/hOJxY/vyGlITN4fS9fp/qmz+8dqSOTO4vmTj69dXHowaAhKj+PpQ== dependencies: - "@walletconnect/types" "^1.1.0" - "@walletconnect/utils" "^1.1.0" + "@walletconnect/types" "^1.1.1-alpha.0" + "@walletconnect/utils" "^1.1.1-alpha.0" eccrypto-js "5.2.0" -"@walletconnect/mobile-registry@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@walletconnect/mobile-registry/-/mobile-registry-1.1.0.tgz#72173a4fcee61f4f8819f6d9fc7cfbf824ed3548" - integrity sha512-OOHQa4NeK2lbfI9WD2d+hTHGwSDzBLoTCeofdLNO2ibaTltQ6S+WNDAVuho6U8CkUTzs5cHPFgLJ6nxYZ8sr/g== +"@walletconnect/mobile-registry@^1.1.1-alpha.0": + version "1.1.1-alpha.0" + resolved "https://registry.yarnpkg.com/@walletconnect/mobile-registry/-/mobile-registry-1.1.1-alpha.0.tgz#65e6708df784e838d05fbe03459eb30a7fedc0ba" + integrity sha512-ncX2+XOEYu6OoIXrcLJiy2mrrMTJDuXgcJUpv5Ghwl9CbLczaiq7AlVDNPjMxZJFQj4aalR/idz6RKJbPBA6SQ== -"@walletconnect/qrcode-modal@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@walletconnect/qrcode-modal/-/qrcode-modal-1.1.0.tgz#4cd0c2c2c713be3f49ef00293a1b23a079d4c7b7" - integrity sha512-vYsu1MBE0D+kx1+xdXmaCs7JqhhWPw8orKk9Br64YIPF5pv/48i+Yi/m28/0myJm54YPlVcgzTnuf8PzAH7jgA== +"@walletconnect/qrcode-modal@^1.1.1-alpha.0": + version "1.1.1-alpha.0" + resolved "https://registry.yarnpkg.com/@walletconnect/qrcode-modal/-/qrcode-modal-1.1.1-alpha.0.tgz#42eea4e8b091ded92bb47ba497cd3772196b7a3a" + integrity sha512-bD7LFVdTzlAb2GFaOR2ymbPVkta8Ezct39VPkteip41KKyta468sZN2AzFTY/wPbTBn+YYwhOliOZhj+Gm9U3A== dependencies: - "@walletconnect/mobile-registry" "^1.1.0" - "@walletconnect/types" "^1.1.0" - "@walletconnect/utils" "^1.1.0" + "@walletconnect/mobile-registry" "^1.1.1-alpha.0" + "@walletconnect/types" "^1.1.1-alpha.0" + "@walletconnect/utils" "^1.1.1-alpha.0" preact "10.4.1" qrcode "1.4.4" -"@walletconnect/socket-transport@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@walletconnect/socket-transport/-/socket-transport-1.1.0.tgz#d80b5e6b3b904f131961259ca16de816ae2b003b" - integrity sha512-plo5WHjL3RTDENH7MTgs7D/ePGHfSuc/HLzkVGvgZSOtoPlRR916nSZNeL4bStYF1ZRJCrds10x36C0DlZjpQg== +"@walletconnect/socket-transport@^1.1.1-alpha.0": + version "1.1.1-alpha.0" + resolved "https://registry.yarnpkg.com/@walletconnect/socket-transport/-/socket-transport-1.1.1-alpha.0.tgz#6a1f4abb537f891a1e6c282e0a2a8d1410270554" + integrity sha512-epS/zNL4GQclYZ3dDiumR0krwYEpHHGC+LsaNxkrSHTh/URuqfmf6QqCOdcjTU6qW5G1cliHC9Kk0UKcC+VeDA== dependencies: - "@walletconnect/types" "^1.1.0" + "@walletconnect/types" "^1.1.1-alpha.0" ws "7.3.0" -"@walletconnect/types@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-1.1.0.tgz#1e4efbf033ad89910cbb86f1f381cd5fe7e764fd" - integrity sha512-cgDEuYHZZTiaXFRwQs3Zhhar+l2T58/YjhWrfZTMKWuc77geIbF7682i9lE9bNEQqQvQ76jjKxJfSLGjCu++sA== +"@walletconnect/types@^1.1.1-alpha.0": + version "1.1.1-alpha.0" + resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-1.1.1-alpha.0.tgz#9fbb8356aa347240f4356abea4aaa8b0137396d0" + integrity sha512-ro6yJ53kTG8aibKyoUv79CFMujkZk6W5FNHCk6SJdIkPec03XICrt9cqLUaPDt0wx+FM5z94ZHAg46Wzqkh5NA== -"@walletconnect/utils@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-1.1.0.tgz#7b0bcf5c77e8079ac055013537a9620244db2da9" - integrity sha512-y5v8PCmd/2kASOncYaz5QJiAzwBRT5MK398PmIkImX9tNEeBh00ifeQGZKkCGi6JYXbde0UC5jsGTGkH8hdxeg== +"@walletconnect/utils@^1.1.1-alpha.0": + version "1.1.1-alpha.0" + resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-1.1.1-alpha.0.tgz#62a274290263dcca45102d1cb0cea617827c1849" + integrity sha512-35NbpD7JeyzKzh/UPM+TYuxJgudeIr1LuCWdbhGX9KyIoeYxXPVJiCu7RkX5cXI0Fh1dGghQx7++4O3xkqLVcg== dependencies: - "@walletconnect/types" "^1.1.0" + "@walletconnect/types" "^1.1.1-alpha.0" detect-browser "5.1.0" enc-utils "2.1.0" js-sha3 "0.8.0" -"@walletconnect/web3-provider@^1.0.11": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@walletconnect/web3-provider/-/web3-provider-1.1.0.tgz#c8a30c4121d3ade159022b10d3a18ecd804c8993" - integrity sha512-1DaYG+aK2pjCBKXrB0c2JKeFk27ObUsu09LlZN1VvIi1+zvHftaubNsSGViLmrq25w72yPle/SDjhgmxvKVMQQ== - dependencies: - "@walletconnect/client" "^1.1.0" - "@walletconnect/http-connection" "^1.1.0" - "@walletconnect/qrcode-modal" "^1.1.0" - "@walletconnect/types" "^1.1.0" - "@walletconnect/utils" "^1.1.0" +"@walletconnect/web3-provider@1.1.1-alpha.0", "@walletconnect/web3-provider@^1.0.11": + version "1.1.1-alpha.0" + resolved "https://registry.yarnpkg.com/@walletconnect/web3-provider/-/web3-provider-1.1.1-alpha.0.tgz#b8ca2158da4974b692f57e4f939b5128d8e4696f" + integrity sha512-1AoTeCOtK8u2jIH+0NsvisPv2TySZLWHwWu0BIb72wzvzJeG3uD383/stHX8mBOI6a0aPoyDEYzA2R4c/O0vWQ== + dependencies: + "@walletconnect/client" "^1.1.1-alpha.0" + "@walletconnect/http-connection" "^1.1.1-alpha.0" + "@walletconnect/qrcode-modal" "^1.1.1-alpha.0" + "@walletconnect/types" "^1.1.1-alpha.0" + "@walletconnect/utils" "^1.1.1-alpha.0" web3-provider-engine "15.0.12" "@web3-react/abstract-connector@^6.0.7": @@ -6937,7 +6824,7 @@ ethereumjs-vm@^2.1.0, ethereumjs-vm@^2.3.4, ethereumjs-vm@^2.6.0: rustbn.js "~0.2.0" safe-buffer "^5.1.1" -ethers@>=5.0.0-beta.186: +ethers@^5.0.0, ethers@^5.0.7: version "5.0.7" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.0.7.tgz#41c3d774e0a57bfde12b0198885789fb41a14976" integrity sha512-1Zu9s+z4BgsDAZcGIYACJdWBB6mVtCCmUonj68Njul7STcSdgwOyj0sCAxCUr2Nsmsamckr4E12q3ecvZPGAUw== @@ -12063,7 +11950,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.4" -prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -12159,19 +12046,6 @@ q@^1.1.2: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= -qr.js@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f" - integrity sha1-ys6GOG9ZoNuAUPqQ2baw6IoeNk8= - -qrcode.react@^0.9.3: - version "0.9.3" - resolved "https://registry.yarnpkg.com/qrcode.react/-/qrcode.react-0.9.3.tgz#91de1287912bdc5ccfb3b091737b828d6ced60c5" - integrity sha512-gGd30Ez7cmrKxyN2M3nueaNLk/f9J7NDRgaD5fVgxGpPLsYGWMn9UQ+XnDpv95cfszTQTdaf4QGLNMf3xU0hmw== - dependencies: - prop-types "^15.6.0" - qr.js "0.0.0" - qrcode@1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.4.4.tgz#f0c43568a7e7510a55efc3b88d9602f71963ea83" @@ -13243,11 +13117,6 @@ schema-utils@^2.5.0, schema-utils@^2.6.0, schema-utils@^2.6.1, schema-utils@^2.6 ajv "^6.12.2" ajv-keywords "^3.4.1" -scrypt-js@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.0.tgz#52361c1f272eeaab09ec1f806ea82078bca58b15" - integrity sha512-7CC7aufwukEvqdmllR0ny0QaSg0+S22xKXrXz3ZahaV6J+fgD2YAtrjtImuoDWog17/Ty9Q4HBmnXEXJ3JkfQA== - scrypt-js@3.0.1, scrypt-js@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.1.tgz#d314a57c2aef69d1ad98a138a21fe9eafa9ee312" @@ -14647,11 +14516,6 @@ use-callback-ref@^1.2.1, use-callback-ref@^1.2.3: resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.4.tgz#d86d1577bfd0b955b6e04aaf5971025f406bea3c" integrity sha512-rXpsyvOnqdScyied4Uglsp14qzag1JIemLeTWGKbwpotWht57hbP78aNT+Q4wdFKQfQibbUX4fb6Qb4y11aVOQ== -use-media@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/use-media/-/use-media-1.4.0.tgz#e777bf1f382a7aacabbd1f9ce3da2b62e58b2a98" - integrity sha512-XsgyUAf3nhzZmEfhc5MqLHwyaPjs78bgytpVJ/xDl0TF4Bptf3vEpBNBBT/EIKOmsOc8UbuECq3mrP3mt1QANA== - use-sidecar@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.0.2.tgz#e72f582a75842f7de4ef8becd6235a4720ad8af6"