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 <callil.capuozzo@gmail.com>
This commit is contained in:
Moody Salem 2020-08-06 18:18:43 -05:00 committed by GitHub
parent 10ef04510a
commit 0f91af1df2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1349 additions and 1001 deletions

@ -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",

@ -251,26 +251,26 @@ export default function AccountDetails({
} else if (connector === walletconnect) {
return (
<IconWrapper size={16}>
<img src={WalletConnectIcon} alt={''} />
<img src={WalletConnectIcon} alt={'wallet connect logo'} />
</IconWrapper>
)
} else if (connector === walletlink) {
return (
<IconWrapper size={16}>
<img src={CoinbaseWalletIcon} alt={''} />
<img src={CoinbaseWalletIcon} alt={'coinbase wallet logo'} />
</IconWrapper>
)
} else if (connector === fortmatic) {
return (
<IconWrapper size={16}>
<img src={FortmaticIcon} alt={''} />
<img src={FortmaticIcon} alt={'fortmatic logo'} />
</IconWrapper>
)
} else if (connector === portis) {
return (
<>
<IconWrapper size={16}>
<img src={PortisIcon} alt={''} />
<img src={PortisIcon} alt={'portis logo'} />
<MainWalletAction
onClick={() => {
portis.portis.showPortis()
@ -382,7 +382,6 @@ export default function AccountDetails({
</AccountControl>
</>
)}
{/* {formatConnectorName()} */}
</AccountGroupingRow>
</InfoCard>
</YourAccount>

@ -27,6 +27,8 @@ const Base = styled(RebassButton)<{
flex-wrap: nowrap;
align-items: center;
cursor: pointer;
position: relative;
z-index: 1;
&:disabled {
cursor: auto;
}

@ -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 (
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90}>
<Wrapper>
<Section>
<RowBetween>
<div />
<CloseIcon onClick={onDismiss} />
</RowBetween>
<ConfirmedIcon>
{transactionBroadcast ? (
<ArrowUpCircle strokeWidth={0.5} size={90} color={theme.primary1} />
) : (
<CustomLightSpinner src={Circle} alt="loader" size={'90px'} />
)}
</ConfirmedIcon>
<AutoColumn gap="12px" justify={'center'}>
<Text fontWeight={500} fontSize={20}>
{transactionBroadcast ? 'Transaction Submitted' : 'Waiting For Confirmation'}
</Text>
<AutoColumn gap="12px" justify={'center'}>
<Text fontWeight={600} fontSize={14} color="" textAlign="center">
{pendingText}
</Text>
</AutoColumn>
{transactionBroadcast ? (
<>
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>
<Text fontWeight={500} fontSize={14} color={theme.primary1}>
View on Etherscan
</Text>
</ExternalLink>
<ButtonPrimary onClick={onDismiss} style={{ margin: '20px 0 0 0' }}>
<Text fontWeight={500} fontSize={20}>
Close
</Text>
</ButtonPrimary>
</>
) : (
<Text fontSize={12} color="#565A69" textAlign="center">
Confirm this transaction in your wallet
</Text>
)}
</AutoColumn>
</Section>
</Wrapper>
</Modal>
)
}
// confirmation screen
return (
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90}>
<Wrapper>
<Section>
<RowBetween>
<Text fontWeight={500} fontSize={20}>
{title}
</Text>
<CloseIcon onClick={onDismiss} />
</RowBetween>
{topContent()}
</Section>
<BottomSection gap="12px">{bottomContent()}</BottomSection>
</Wrapper>
</Modal>
)
}

@ -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 }) => <AnimatedDialogOverlay {...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 }) => (
<DialogContent {...rest} />
<AnimatedDialogContent {...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 && (
<StyledDialogOverlay
key={key}
style={props}
onDismiss={onDismiss}
initialFocusRef={initialFocusRef}
mobile={true}
return (
<>
{fadeTransition.map(
({ item, key, props }) =>
item && (
<StyledDialogOverlay key={key} style={props} onDismiss={onDismiss} initialFocusRef={initialFocusRef}>
<StyledDialogContent
{...(isMobile
? {
...bind(),
style: { transform: y.interpolate(y => `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 : <div tabIndex={1} />}
<Spring // animation for entrance and exit
from={{
transform: isOpen ? 'translateY(200px)' : 'translateY(100px)'
}}
to={{
transform: isOpen ? 'translateY(0px)' : 'translateY(200px)'
}}
>
{props => (
<animated.div
{...bind()}
style={{
transform: y.interpolate(y => `translateY(${y > 0 ? y : 0}px)`)
}}
>
<StyledDialogContent
aria-label="dialog content"
style={props}
hidden={true}
minHeight={minHeight}
maxHeight={maxHeight}
mobile={isMobile}
>
{children}
</StyledDialogContent>
</animated.div>
)}
</Spring>
</StyledDialogOverlay>
)
)}
</>
)
} else {
return (
<>
{transitions.map(
({ item, key, props }) =>
item && (
<StyledDialogOverlay key={key} style={props} onDismiss={onDismiss} initialFocusRef={initialFocusRef}>
<StyledDialogContent
aria-label="dialog content"
hidden={true}
minHeight={minHeight}
maxHeight={maxHeight}
isOpen={isOpen}
>
{children}
</StyledDialogContent>
</StyledDialogOverlay>
)
)}
</>
)
}
{!initialFocusRef && isMobile ? <div tabIndex={1} /> : null}
{children}
</StyledDialogContent>
</StyledDialogOverlay>
)
)}
</>
)
}

@ -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 = <TxnPopup hash={hash} success={success} summary={summary} />
popupContent = <TransactionPopup hash={hash} success={success} summary={summary} />
} else if ('listUpdate' in content) {
const {
listUpdate: { listUrl, oldList, newList, auto }

@ -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 (
<AutoRow>
<RowNoFlex>
<div style={{ paddingRight: 16 }}>
{success ? <CheckCircle color={theme.green1} size={24} /> : <AlertCircle color={theme.red1} size={24} />}
</div>
@ -22,6 +34,6 @@ export default function TxnPopup({ hash, success, summary }: { hash: string; suc
<TYPE.body fontWeight={500}>{summary ?? 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}</TYPE.body>
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>View on Etherscan</ExternalLink>
</AutoColumn>
</AutoRow>
</RowNoFlex>
)
}

@ -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 (
<>
<FixedPopupColumn gap="20px">
{activePopups.map(item => (
<PopupItem key={item.key} content={item.content} popKey={item.key} />
))}
</FixedPopupColumn>
)
}
//mobile
else
return (
<MobilePopupWrapper height={activePopups?.length > 0 ? 'fit-content' : 0}>
<MobilePopupInner>
{activePopups // reverse so new items up front
@ -66,5 +62,6 @@ export default function Popups() {
))}
</MobilePopupInner>
</MobilePopupWrapper>
)
</>
)
}

@ -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 (
<Wrapper>
<Section>
<RowBetween>
<div />
<CloseIcon onClick={onDismiss} />
</RowBetween>
<ConfirmedIcon>
<CustomLightSpinner src={Circle} alt="loader" size={'90px'} />
</ConfirmedIcon>
<AutoColumn gap="12px" justify={'center'}>
<Text fontWeight={500} fontSize={20}>
Waiting For Confirmation
</Text>
<AutoColumn gap="12px" justify={'center'}>
<Text fontWeight={600} fontSize={14} color="" textAlign="center">
{pendingText}
</Text>
</AutoColumn>
<Text fontSize={12} color="#565A69" textAlign="center">
Confirm this transaction in your wallet
</Text>
</AutoColumn>
</Section>
</Wrapper>
)
}
function TransactionSubmittedContent({
onDismiss,
chainId,
hash
}: {
onDismiss: () => void
hash: string | undefined
chainId: ChainId
}) {
const theme = useContext(ThemeContext)
return (
<Wrapper>
<Section>
<RowBetween>
<div />
<CloseIcon onClick={onDismiss} />
</RowBetween>
<ConfirmedIcon>
<ArrowUpCircle strokeWidth={0.5} size={90} color={theme.primary1} />
</ConfirmedIcon>
<AutoColumn gap="12px" justify={'center'}>
<Text fontWeight={500} fontSize={20}>
Transaction Submitted
</Text>
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>
<Text fontWeight={500} fontSize={14} color={theme.primary1}>
View on Etherscan
</Text>
</ExternalLink>
<ButtonPrimary onClick={onDismiss} style={{ margin: '20px 0 0 0' }}>
<Text fontWeight={500} fontSize={20}>
Close
</Text>
</ButtonPrimary>
</AutoColumn>
</Section>
</Wrapper>
)
}
export function ConfirmationModalContent({
title,
bottomContent,
onDismiss,
topContent
}: {
title: string
onDismiss: () => void
topContent: () => React.ReactNode
bottomContent: () => React.ReactNode
}) {
return (
<Wrapper>
<Section>
<RowBetween>
<Text fontWeight={500} fontSize={20}>
{title}
</Text>
<CloseIcon onClick={onDismiss} />
</RowBetween>
{topContent()}
</Section>
<BottomSection gap="12px">{bottomContent()}</BottomSection>
</Wrapper>
)
}
export function TransactionErrorContent({ message, onDismiss }: { message: string; onDismiss: () => void }) {
const theme = useContext(ThemeContext)
return (
<Wrapper>
<Section>
<RowBetween>
<Text fontWeight={500} fontSize={20}>
Error
</Text>
<CloseIcon onClick={onDismiss} />
</RowBetween>
<AutoColumn style={{ marginTop: 20, padding: '2rem 0' }} gap="24px" justify="center">
<AlertTriangle color={theme.red1} style={{ strokeWidth: 1.5 }} size={64} />
<Text fontWeight={500} fontSize={16} color={theme.red1} style={{ textAlign: 'center', width: '85%' }}>
{message}
</Text>
</AutoColumn>
</Section>
<BottomSection gap="12px">
<ButtonPrimary onClick={onDismiss}>Dismiss</ButtonPrimary>
</BottomSection>
</Wrapper>
)
}
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 (
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90}>
{attemptingTxn ? (
<ConfirmationPendingContent onDismiss={onDismiss} pendingText={pendingText} />
) : hash ? (
<TransactionSubmittedContent chainId={chainId} hash={hash} onDismiss={onDismiss} />
) : (
content()
)}
</Modal>
)
}

@ -0,0 +1,4 @@
{
"extends": "../../../tsconfig.strict.json",
"include": ["**/*"]
}

@ -349,9 +349,7 @@ export default function WalletModal({
{walletView !== WALLET_VIEWS.PENDING && (
<Blurb>
<span>New to Ethereum? &nbsp;</span>{' '}
<ExternalLink href="https://ethereum.org/use/#3-what-is-a-wallet-and-which-one-should-i-use">
Learn more about wallets
</ExternalLink>
<ExternalLink href="https://ethereum.org/wallets/">Learn more about wallets</ExternalLink>
</Blurb>
)}
</ContentWrapper>

@ -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 (
<AutoColumn gap="md">
{trade && <TradeSummary trade={trade} allowedSlippage={allowedSlippage} />}
{showRoute && (
{trade && (
<>
<SectionBreak />
<AutoColumn style={{ padding: '0 24px' }}>
<RowFixed>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
Route
</TYPE.black>
<QuestionHelper text="Routing through these tokens resulted in the best price for your trade." />
</RowFixed>
<SwapRoute trade={trade} />
</AutoColumn>
<TradeSummary trade={trade} allowedSlippage={allowedSlippage} />
{showRoute && (
<>
<SectionBreak />
<AutoColumn style={{ padding: '0 24px' }}>
<RowFixed>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
Route
</TYPE.black>
<QuestionHelper text="Routing through these tokens resulted in the best price for your trade." />
</RowFixed>
<SwapRoute trade={trade} />
</AutoColumn>
</>
)}
</>
)}
</AutoColumn>

@ -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 (
<AdvancedDetailsFooter show={Boolean(trade)}>
<AdvancedSwapDetails {...rest} trade={trade ?? lastTrade} />
<AdvancedSwapDetails {...rest} trade={trade ?? lastTrade ?? undefined} />
</AdvancedDetailsFooter>
)
}

@ -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 ? (
<SwapModalHeader
trade={trade}
allowedSlippage={allowedSlippage}
recipient={recipient}
showAcceptChanges={showAcceptChanges}
onAcceptChanges={onAcceptChanges}
/>
) : null
}, [allowedSlippage, onAcceptChanges, recipient, showAcceptChanges, trade])
const modalBottom = useCallback(() => {
return trade ? (
<SwapModalFooter
onConfirm={onConfirm}
trade={trade}
disabledConfirm={showAcceptChanges}
swapErrorMessage={swapErrorMessage}
allowedSlippage={allowedSlippage}
/>
) : 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 ? (
<TransactionErrorContent onDismiss={onDismiss} message={swapErrorMessage} />
) : (
<ConfirmationModalContent
title="Confirm Swap"
onDismiss={onDismiss}
topContent={modalHeader}
bottomContent={modalBottom}
/>
),
[onDismiss, modalBottom, modalHeader, swapErrorMessage]
)
return (
<TransactionConfirmationModal
isOpen={isOpen}
onDismiss={onDismiss}
attemptingTxn={attemptingTxn}
hash={txHash}
content={confirmationContent}
pendingText={pendingText}
/>
)
}

@ -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 (
<ErrorText fontWeight={500} fontSize={14} severity={warningSeverity(priceImpact)}>
{priceImpact?.lessThan(ONE_BIPS) ? '<0.01%' : `${priceImpact?.toFixed(2)}%` ?? '-'}
{priceImpact ? (priceImpact.lessThan(ONE_BIPS) ? '<0.01%' : `${priceImpact.toFixed(2)}%`) : '-'}
</ErrorText>
)
}

@ -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<boolean>(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({
<RowBetween>
<RowFixed>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
{trade?.tradeType === TradeType.EXACT_INPUT ? 'Minimum received' : 'Maximum sold'}
{trade.tradeType === TradeType.EXACT_INPUT ? 'Minimum received' : 'Maximum sold'}
</TYPE.black>
<QuestionHelper text="Your transaction will revert if there is a large, unfavorable price movement before it is confirmed." />
</RowFixed>
<RowFixed>
<TYPE.black fontSize={14}>
{trade?.tradeType === TradeType.EXACT_INPUT
{trade.tradeType === TradeType.EXACT_INPUT
? slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(4) ?? '-'
: slippageAdjustedAmounts[Field.INPUT]?.toSignificant(4) ?? '-'}
</TYPE.black>
{parsedAmounts[Field.OUTPUT] && parsedAmounts[Field.INPUT] && (
<TYPE.black fontSize={14} marginLeft={'4px'}>
{trade?.tradeType === TradeType.EXACT_INPUT
? parsedAmounts[Field.OUTPUT]?.currency?.symbol
: parsedAmounts[Field.INPUT]?.currency?.symbol}
</TYPE.black>
)}
<TYPE.black fontSize={14} marginLeft={'4px'}>
{trade.tradeType === TradeType.EXACT_INPUT
? trade.outputAmount.currency.symbol
: trade.inputAmount.currency.symbol}
</TYPE.black>
</RowFixed>
</RowBetween>
<RowBetween>
@ -107,17 +103,25 @@ export default function SwapModalFooter({
<QuestionHelper text="A portion of each trade (0.30%) goes to liquidity providers as a protocol incentive." />
</RowFixed>
<TYPE.black fontSize={14}>
{realizedLPFee ? realizedLPFee?.toSignificant(6) + ' ' + trade?.inputAmount?.currency?.symbol : '-'}
{realizedLPFee ? realizedLPFee?.toSignificant(6) + ' ' + trade.inputAmount.currency.symbol : '-'}
</TYPE.black>
</RowBetween>
</AutoColumn>
<AutoRow>
<ButtonError onClick={onSwap} error={severity > 2} style={{ margin: '10px 0 0 0' }} id="confirm-swap-or-send">
<ButtonError
onClick={onConfirm}
disabled={disabledConfirm}
error={severity > 2}
style={{ margin: '10px 0 0 0' }}
id="confirm-swap-or-send"
>
<Text fontSize={20} fontWeight={500}>
{confirmText}
{severity > 2 ? 'Swap Anyway' : 'Confirm Swap'}
</Text>
</ButtonError>
{swapErrorMessage ? <SwapCallbackError error={swapErrorMessage} /> : null}
</AutoRow>
</>
)

@ -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 (
<AutoColumn gap={'md'} style={{ marginTop: '20px' }}>
<RowBetween align="flex-end">
<TruncatedText fontSize={24} fontWeight={500}>
{formattedAmounts[Field.INPUT]}
</TruncatedText>
<RowFixed gap="4px">
<CurrencyLogo currency={currencies[Field.INPUT]} size={'24px'} />
<RowFixed gap={'0px'}>
<CurrencyLogo currency={trade.inputAmount.currency} size={'24px'} style={{ marginRight: '12px' }} />
<TruncatedText
fontSize={24}
fontWeight={500}
color={showAcceptChanges && trade.tradeType === TradeType.EXACT_OUTPUT ? theme.primary1 : ''}
>
{trade.inputAmount.toSignificant(6)}
</TruncatedText>
</RowFixed>
<RowFixed gap={'0px'}>
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{currencies[Field.INPUT]?.symbol}
{trade.inputAmount.currency.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowFixed>
<ArrowDown size="16" color={theme.text2} />
<ArrowDown size="16" color={theme.text2} style={{ marginLeft: '4px', minWidth: '16px' }} />
</RowFixed>
<RowBetween align="flex-end">
<TruncatedText fontSize={24} fontWeight={500} color={priceImpactSeverity > 2 ? theme.red1 : ''}>
{formattedAmounts[Field.OUTPUT]}
</TruncatedText>
<RowFixed gap="4px">
<CurrencyLogo currency={currencies[Field.OUTPUT]} size={'24px'} />
<RowFixed gap={'0px'}>
<CurrencyLogo currency={trade.outputAmount.currency} size={'24px'} style={{ marginRight: '12px' }} />
<TruncatedText
fontSize={24}
fontWeight={500}
color={
priceImpactSeverity > 2
? theme.red1
: showAcceptChanges && trade.tradeType === TradeType.EXACT_INPUT
? theme.primary1
: ''
}
>
{trade.outputAmount.toSignificant(6)}
</TruncatedText>
</RowFixed>
<RowFixed gap={'0px'}>
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{currencies[Field.OUTPUT]?.symbol}
{trade.outputAmount.currency.symbol}
</Text>
</RowFixed>
</RowBetween>
{showAcceptChanges ? (
<SwapShowAcceptChanges justify="flex-start" gap={'0px'}>
<RowBetween>
<RowFixed>
<AlertTriangle size={20} style={{ marginRight: '8px', minWidth: 24 }} />
<TYPE.main color={theme.primary1}> Price Updated</TYPE.main>
</RowFixed>
<ButtonPrimary
style={{ padding: '.5rem', width: 'fit-content', fontSize: '0.825rem', borderRadius: '12px' }}
onClick={onAcceptChanges}
>
Accept
</ButtonPrimary>
</RowBetween>
</SwapShowAcceptChanges>
) : null}
<AutoColumn justify="flex-start" gap="sm" style={{ padding: '12px 0 0 0px' }}>
{independentField === Field.INPUT ? (
{trade.tradeType === TradeType.EXACT_INPUT ? (
<TYPE.italic textAlign="left" style={{ width: '100%' }}>
{`Output is estimated. You will receive at least `}
<b>
{slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} {currencies[Field.OUTPUT]?.symbol}
{slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} {trade.outputAmount.currency.symbol}
</b>
{' or the transaction will revert.'}
</TYPE.italic>
@ -68,7 +109,7 @@ export default function SwapModalHeader({
<TYPE.italic textAlign="left" style={{ width: '100%' }}>
{`Input is estimated. You will sell at most `}
<b>
{slippageAdjustedAmounts[Field.INPUT]?.toSignificant(6)} {currencies[Field.INPUT]?.symbol}
{slippageAdjustedAmounts[Field.INPUT]?.toSignificant(6)} {trade.inputAmount.currency.symbol}
</b>
{' or the transaction will revert.'}
</TYPE.italic>

@ -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 (

@ -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 (
<SwapCallbackErrorInner>
<SwapCallbackErrorInnerAlertTriangle>
<AlertTriangle size={24} />
</SwapCallbackErrorInnerAlertTriangle>
<p>{error}</p>
</SwapCallbackErrorInner>
)
}
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;
`

@ -0,0 +1,4 @@
{
"extends": "../../../tsconfig.strict.json",
"include": ["**/*"]
}

@ -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<typeof setTimeout> | 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
}
}

@ -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

@ -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
}

@ -1,13 +1,33 @@
import { useEffect, useState } from 'react'
/**
* 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<T>(
value: T | undefined | null,
filterFn?: (value: T | null | undefined) => boolean
): T | null | undefined {
const [last, setLast] = useState<T | null | undefined>(filterFn && filterFn(value) ? value : undefined)
useEffect(() => {
setLast(last => {
const shouldUse: boolean = filterFn ? filterFn(value) : true
if (shouldUse) return value
return last
})
}, [filterFn, value])
return last
}
function isDefined<T>(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 default function useLast<T>(value: T | undefined | null): T | null | undefined {
const [last, setLast] = useState<T | null | undefined>(value)
useEffect(() => {
setLast(last => value ?? last)
}, [value])
return last
export function useLastTruthy<T>(value: T | undefined | null): T | null | undefined {
return useLast(value, isDefined)
}

@ -1,21 +1,111 @@
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
}
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
): SwapCall[] {
const { account, chainId, library } = useActiveWeb3React()
const { address: recipientAddress } = useENS(recipientAddressOrName)
const recipient = recipientAddressOrName === null ? account : recipientAddress
const v1Exchange = useV1ExchangeContract(useV1TradeExchangeAddress(trade), true)
return useMemo(() => {
const tradeVersion = getTradeVersion(trade)
if (!trade || !recipient || !library || !account || !tradeVersion || !chainId) return []
const contract: Contract | null =
tradeVersion === Version.v2 ? getRouterContract(chainId, library, account) : v1Exchange
if (!contract) {
return []
}
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
})
)
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
}
return swapMethods.map(parameters => ({ parameters, contract }))
}, [account, allowedSlippage, chainId, deadline, library, recipient, trade, v1Exchange])
}
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(
@ -23,115 +113,95 @@ export function useSwapCallback(
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<string>) {
): { state: SwapCallbackState; callback: null | (() => Promise<string>); 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
const tradeVersion = getTradeVersion(trade)
const v1Exchange = useV1ExchangeContract(useV1TradeExchangeAddress(trade), true)
return useMemo(() => {
if (!trade || !recipient || !library || !account || !tradeVersion || !chainId) return null
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 swapMethods = []
switch (tradeVersion) {
case Version.v2:
swapMethods.push(
Router.swapCallParameters(trade, {
feeOnTransfer: false,
allowedSlippage: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE),
recipient,
ttl: deadline
})
)
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 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
})
)
)
// 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.')
}
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 {
const { methodName, args, value } = swapMethods[indexOfSuccessfulEstimation]
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
return { state: SwapCallbackState.LOADING, callback: null, error: null }
}
}
const tradeVersion = getTradeVersion(trade)
return {
state: SwapCallbackState.VALID,
callback: async function onSwap(): Promise<string> {
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) }
})
})
})
)
// 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)
}
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])
}

@ -23,7 +23,7 @@ export default function useWrapCallback(
inputCurrency: Currency | undefined,
outputCurrency: Currency | undefined,
typedValue: string | undefined
): { wrapType: WrapType; execute?: undefined | (() => Promise<void>); error?: string } {
): { wrapType: WrapType; execute?: undefined | (() => Promise<void>); 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

@ -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 (
<AutoColumn gap="md">
<AutoRow justify="space-around" gap="4px">
<AutoColumn justify="center">
<TYPE.black>{price?.toSignificant(6) ?? '0'}</TYPE.black>
<TYPE.black>{price?.toSignificant(6) ?? '-'}</TYPE.black>
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
{currencies[Field.CURRENCY_B]?.symbol} per {currencies[Field.CURRENCY_A]?.symbol}
</Text>
</AutoColumn>
<AutoColumn justify="center">
<TYPE.black>{price?.invert().toSignificant(6) ?? '0'}</TYPE.black>
<TYPE.black>{price?.invert()?.toSignificant(6) ?? '-'}</TYPE.black>
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
{currencies[Field.CURRENCY_A]?.symbol} per {currencies[Field.CURRENCY_B]?.symbol}
</Text>

@ -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 (
<>
<AppBody>
<AddRemoveTabs adding={true} />
<Wrapper>
<ConfirmationModal
<TransactionConfirmationModal
isOpen={showConfirm}
onDismiss={() => {
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={() => (
<ConfirmationModalContent
title={noLiquidity ? 'You are creating a pool' : 'You will receive'}
onDismiss={handleDismissConfirmation}
topContent={modalHeader}
bottomContent={modalBottom}
/>
)}
pendingText={pendingText}
title={noLiquidity ? 'You are creating a pool' : 'You will receive'}
/>
<AutoColumn gap="20px">
{noLiquidity && (

@ -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 (
<>
<AppBody>
<AddRemoveTabs adding={false} />
<Wrapper>
<ConfirmationModal
<TransactionConfirmationModal
isOpen={showConfirm}
onDismiss={() => {
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={() => (
<ConfirmationModalContent
title={'You will receive'}
onDismiss={handleDismissConfirmation}
topContent={modalHeader}
bottomContent={modalBottom}
/>
)}
pendingText={pendingText}
title="You will receive"
/>
<AutoColumn gap="md">
<LightCard>

@ -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<boolean>(false) // show confirmation modal
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // waiting for user confirmaion/rejection
const [txHash, setTxHash] = useState<string>('')
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<boolean>(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 (
<SwapModalHeader
currencies={currencies}
formattedAmounts={formattedAmounts}
slippageAdjustedAmounts={slippageAdjustedAmounts}
priceImpactSeverity={priceImpactSeverity}
independentField={independentField}
recipient={recipient}
/>
)
}
function modalBottom() {
return (
<SwapModalFooter
confirmText={priceImpactSeverity > 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 && <TokenWarningCards currencies={currencies} />}
<AppBody disabled={!!showWarning}>
<AppBody disabled={showWarning}>
<SwapPoolTabs active={'swap'} />
<Wrapper id="swap-page">
<ConfirmationModal
<ConfirmSwapModal
isOpen={showConfirm}
title="Confirm Swap"
onDismiss={() => {
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}
/>
<AutoColumn gap={'md'}>
@ -373,8 +365,9 @@ export default function Swap() {
{!account ? (
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
) : showWrap ? (
<ButtonPrimary disabled={Boolean(wrapError)} onClick={onWrap}>
{wrapError ?? (wrapType === WrapType.WRAP ? 'Wrap' : wrapType === WrapType.UNWRAP ? 'Unwrap' : null)}
<ButtonPrimary disabled={Boolean(wrapInputError)} onClick={onWrap}>
{wrapInputError ??
(wrapType === WrapType.WRAP ? 'Wrap' : wrapType === WrapType.UNWRAP ? 'Unwrap' : null)}
</ButtonPrimary>
) : noRoute && userHasSpecifiedInputOutput ? (
<GreyCard style={{ textAlign: 'center' }}>
@ -398,15 +391,27 @@ export default function Swap() {
</ButtonPrimary>
<ButtonError
onClick={() => {
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}
>
<Text fontSize={16} fontWeight={500}>
{priceImpactSeverity > 3 && !expertMode
{priceImpactSeverity > 3 && !isExpertMode
? `Price Impact High`
: `Swap${priceImpactSeverity > 2 ? ' Anyway' : ''}`}
</Text>
@ -415,21 +420,32 @@ export default function Swap() {
) : (
<ButtonError
onClick={() => {
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}
>
<Text fontSize={20} fontWeight={500}>
{error
? error
: priceImpactSeverity > 3 && !expertMode
{swapInputError
? swapInputError
: priceImpactSeverity > 3 && !isExpertMode
? `Price Impact Too High`
: `Swap${priceImpactSeverity > 2 ? ' Anyway' : ''}`}
</Text>
</ButtonError>
)}
{isExpertMode && swapErrorMessage ? <SwapCallbackError error={swapErrorMessage} /> : null}
{betterTradeLinkVersion && <BetterTradeLink version={betterTradeLinkVersion} />}
</BottomGrouping>
</Wrapper>

@ -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] = [

@ -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<number>((memo, curr) => memo + curr.length, 0)
const lastCallKeyIndex = firstCallKeyIndex + returnData.length
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 }
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
})
)
})
)
// accumulates the length of all previous indices
const firstCallKeyIndex = chunkedCalls.slice(0, index).reduce<number>((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

@ -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
}
}

@ -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

@ -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)
}

7
src/utils/isZero.ts Normal file

@ -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)
}

@ -1,26 +1,45 @@
import { retry } from './retry'
import { retry, RetryableError } from './retry'
describe('retry', () => {
function makeFn<T>(fails: number, result: T): () => Promise<T> {
function makeFn<T>(fails: number, result: T, retryable = true): () => Promise<T> {
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<any>, 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
)

@ -6,6 +6,20 @@ function waitRandom(min: number, max: number): Promise<void> {
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<void> {
* @param minWait min wait between retries in ms
* @param maxWait max wait between retries in ms
*/
// todo: support cancelling the retry
export function retry<T>(
fn: () => Promise<T>,
{ n = 3, minWait = 500, maxWait = 1000 }: { n?: number; minWait?: number; maxWait?: number } = {}
): Promise<T> {
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<T>; cancel: () => void } {
let completed = false
let rejectCancelled: (error: Error) => void
const promise = new Promise<T>(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())
}
}
}

320
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==
"@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.0"
"@walletconnect/http-connection" "^1.1.0"
"@walletconnect/qrcode-modal" "^1.1.0"
"@walletconnect/types" "^1.1.0"
"@walletconnect/utils" "^1.1.0"
"@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"