Commit 0f91af1d authored by Moody Salem's avatar Moody Salem Committed by GitHub

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: default avatarCallil Capuozzo <callil.capuozzo@gmail.com>
parent 10ef0451
...@@ -4,17 +4,7 @@ ...@@ -4,17 +4,7 @@
"homepage": ".", "homepage": ".",
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"@ethersproject/address": "5.0.0-beta.134", "@ethersproject/experimental": "^5.0.1",
"@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",
"@popperjs/core": "^2.4.4", "@popperjs/core": "^2.4.4",
"@reach/dialog": "^0.10.3", "@reach/dialog": "^0.10.3",
"@reach/portal": "^0.10.3", "@reach/portal": "^0.10.3",
...@@ -52,6 +42,7 @@ ...@@ -52,6 +42,7 @@
"eslint-plugin-prettier": "^3.1.3", "eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.19.0", "eslint-plugin-react": "^7.19.0",
"eslint-plugin-react-hooks": "^4.0.0", "eslint-plugin-react-hooks": "^4.0.0",
"ethers": "^5.0.7",
"i18next": "^15.0.9", "i18next": "^15.0.9",
"i18next-browser-languagedetector": "^3.0.1", "i18next-browser-languagedetector": "^3.0.1",
"i18next-xhr-backend": "^2.0.1", "i18next-xhr-backend": "^2.0.1",
...@@ -60,7 +51,6 @@ ...@@ -60,7 +51,6 @@
"lodash.flatmap": "^4.5.0", "lodash.flatmap": "^4.5.0",
"polished": "^3.3.2", "polished": "^3.3.2",
"prettier": "^1.17.0", "prettier": "^1.17.0",
"qrcode.react": "^0.9.3",
"qs": "^6.9.4", "qs": "^6.9.4",
"react": "^16.13.1", "react": "^16.13.1",
"react-device-detect": "^1.6.2", "react-device-detect": "^1.6.2",
...@@ -80,8 +70,10 @@ ...@@ -80,8 +70,10 @@
"serve": "^11.3.0", "serve": "^11.3.0",
"start-server-and-test": "^1.11.0", "start-server-and-test": "^1.11.0",
"styled-components": "^4.2.0", "styled-components": "^4.2.0",
"typescript": "^3.8.3", "typescript": "^3.8.3"
"use-media": "^1.4.0" },
"resolutions": {
"@walletconnect/web3-provider": "1.1.1-alpha.0"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
......
...@@ -251,26 +251,26 @@ export default function AccountDetails({ ...@@ -251,26 +251,26 @@ export default function AccountDetails({
} else if (connector === walletconnect) { } else if (connector === walletconnect) {
return ( return (
<IconWrapper size={16}> <IconWrapper size={16}>
<img src={WalletConnectIcon} alt={''} /> <img src={WalletConnectIcon} alt={'wallet connect logo'} />
</IconWrapper> </IconWrapper>
) )
} else if (connector === walletlink) { } else if (connector === walletlink) {
return ( return (
<IconWrapper size={16}> <IconWrapper size={16}>
<img src={CoinbaseWalletIcon} alt={''} /> <img src={CoinbaseWalletIcon} alt={'coinbase wallet logo'} />
</IconWrapper> </IconWrapper>
) )
} else if (connector === fortmatic) { } else if (connector === fortmatic) {
return ( return (
<IconWrapper size={16}> <IconWrapper size={16}>
<img src={FortmaticIcon} alt={''} /> <img src={FortmaticIcon} alt={'fortmatic logo'} />
</IconWrapper> </IconWrapper>
) )
} else if (connector === portis) { } else if (connector === portis) {
return ( return (
<> <>
<IconWrapper size={16}> <IconWrapper size={16}>
<img src={PortisIcon} alt={''} /> <img src={PortisIcon} alt={'portis logo'} />
<MainWalletAction <MainWalletAction
onClick={() => { onClick={() => {
portis.portis.showPortis() portis.portis.showPortis()
...@@ -382,7 +382,6 @@ export default function AccountDetails({ ...@@ -382,7 +382,6 @@ export default function AccountDetails({
</AccountControl> </AccountControl>
</> </>
)} )}
{/* {formatConnectorName()} */}
</AccountGroupingRow> </AccountGroupingRow>
</InfoCard> </InfoCard>
</YourAccount> </YourAccount>
......
...@@ -27,6 +27,8 @@ const Base = styled(RebassButton)<{ ...@@ -27,6 +27,8 @@ const Base = styled(RebassButton)<{
flex-wrap: nowrap; flex-wrap: nowrap;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
position: relative;
z-index: 1;
&:disabled { &:disabled {
cursor: auto; cursor: auto;
} }
......
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>
)
}
import React from 'react' import React from 'react'
import styled, { css } from 'styled-components' import styled, { css } from 'styled-components'
import { animated, useTransition, useSpring } from 'react-spring' import { animated, useTransition, useSpring } from 'react-spring'
import { Spring } from 'react-spring/renderprops'
import { DialogOverlay, DialogContent } from '@reach/dialog' import { DialogOverlay, DialogContent } from '@reach/dialog'
import { isMobile } from 'react-device-detect' import { isMobile } from 'react-device-detect'
import '@reach/dialog/styles.css' import '@reach/dialog/styles.css'
...@@ -11,39 +9,25 @@ import { useGesture } from 'react-use-gesture' ...@@ -11,39 +9,25 @@ import { useGesture } from 'react-use-gesture'
const AnimatedDialogOverlay = animated(DialogOverlay) const AnimatedDialogOverlay = animated(DialogOverlay)
// eslint-disable-next-line @typescript-eslint/no-unused-vars // 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] { &[data-reach-dialog-overlay] {
z-index: 2; z-index: 2;
display: flex;
align-items: center;
justify-content: center;
background-color: transparent; background-color: transparent;
overflow: hidden; overflow: hidden;
${({ mobile }) => display: flex;
mobile && align-items: center;
css` justify-content: center;
align-items: flex-end;
`}
&::after { background-color: ${({ theme }) => theme.modalBG};
content: '';
background-color: ${({ theme }) => theme.modalBG};
opacity: 0.5;
top: 0;
left: 0;
bottom: 0;
right: 0;
position: fixed;
z-index: -1;
}
} }
` `
const AnimatedDialogContent = animated(DialogContent)
// destructure to not pass custom props to Dialog DOM element // destructure to not pass custom props to Dialog DOM element
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...rest }) => ( const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...rest }) => (
<DialogContent {...rest} /> <AnimatedDialogContent {...rest} />
)).attrs({ )).attrs({
'aria-label': 'dialog' 'aria-label': 'dialog'
})` })`
...@@ -55,6 +39,8 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...r ...@@ -55,6 +39,8 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...r
padding: 0px; padding: 0px;
width: 50vw; width: 50vw;
align-self: ${({ mobile }) => (mobile ? 'flex-end' : 'center')};
max-width: 420px; max-width: 420px;
${({ maxHeight }) => ${({ maxHeight }) =>
maxHeight && maxHeight &&
...@@ -102,7 +88,7 @@ export default function Modal({ ...@@ -102,7 +88,7 @@ export default function Modal({
initialFocusRef = null, initialFocusRef = null,
children children
}: ModalProps) { }: ModalProps) {
const transitions = useTransition(isOpen, null, { const fadeTransition = useTransition(isOpen, null, {
config: { duration: 200 }, config: { duration: 200 },
from: { opacity: 0 }, from: { opacity: 0 },
enter: { opacity: 1 }, enter: { opacity: 1 },
...@@ -115,80 +101,37 @@ export default function Modal({ ...@@ -115,80 +101,37 @@ export default function Modal({
set({ set({
y: state.down ? state.movement[1] : 0 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() onDismiss()
} }
} }
}) })
if (isMobile) { return (
return ( <>
<> {fadeTransition.map(
{transitions.map( ({ item, key, props }) =>
({ item, key, props }) => item && (
item && ( <StyledDialogOverlay key={key} style={props} onDismiss={onDismiss} initialFocusRef={initialFocusRef}>
<StyledDialogOverlay <StyledDialogContent
key={key} {...(isMobile
style={props} ? {
onDismiss={onDismiss} ...bind(),
initialFocusRef={initialFocusRef} style: { transform: y.interpolate(y => `translateY(${y > 0 ? y : 0}px)`) }
mobile={true} }
: {})}
aria-label="dialog content"
minHeight={minHeight}
maxHeight={maxHeight}
mobile={isMobile}
> >
{/* prevents the automatic focusing of inputs on mobile by the reach dialog */} {/* prevents the automatic focusing of inputs on mobile by the reach dialog */}
{initialFocusRef ? null : <div tabIndex={1} />} {!initialFocusRef && isMobile ? <div tabIndex={1} /> : null}
<Spring // animation for entrance and exit {children}
from={{ </StyledDialogContent>
transform: isOpen ? 'translateY(200px)' : 'translateY(100px)' </StyledDialogOverlay>
}} )
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>
)
)}
</>
)
}
} }
...@@ -5,7 +5,7 @@ import useInterval from '../../hooks/useInterval' ...@@ -5,7 +5,7 @@ import useInterval from '../../hooks/useInterval'
import { PopupContent } from '../../state/application/actions' import { PopupContent } from '../../state/application/actions'
import { useRemovePopup } from '../../state/application/hooks' import { useRemovePopup } from '../../state/application/hooks'
import ListUpdatePopup from './ListUpdatePopup' import ListUpdatePopup from './ListUpdatePopup'
import TxnPopup from './TxnPopup' import TransactionPopup from './TransactionPopup'
export const StyledClose = styled(X)` export const StyledClose = styled(X)`
position: absolute; position: absolute;
...@@ -68,7 +68,7 @@ export default function PopupItem({ content, popKey }: { content: PopupContent; ...@@ -68,7 +68,7 @@ export default function PopupItem({ content, popKey }: { content: PopupContent;
const { const {
txn: { hash, success, summary } txn: { hash, success, summary }
} = content } = content
popupContent = <TxnPopup hash={hash} success={success} summary={summary} /> popupContent = <TransactionPopup hash={hash} success={success} summary={summary} />
} else if ('listUpdate' in content) { } else if ('listUpdate' in content) {
const { const {
listUpdate: { listUrl, oldList, newList, auto } listUpdate: { listUrl, oldList, newList, auto }
......
import React, { useContext } from 'react' import React, { useContext } from 'react'
import { AlertCircle, CheckCircle } from 'react-feather' import { AlertCircle, CheckCircle } from 'react-feather'
import { ThemeContext } from 'styled-components' import styled, { ThemeContext } from 'styled-components'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { TYPE } from '../../theme' import { TYPE } from '../../theme'
import { ExternalLink } from '../../theme/components' import { ExternalLink } from '../../theme/components'
...@@ -8,13 +8,25 @@ import { getEtherscanLink } from '../../utils' ...@@ -8,13 +8,25 @@ import { getEtherscanLink } from '../../utils'
import { AutoColumn } from '../Column' import { AutoColumn } from '../Column'
import { AutoRow } from '../Row' 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 { chainId } = useActiveWeb3React()
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
return ( return (
<AutoRow> <RowNoFlex>
<div style={{ paddingRight: 16 }}> <div style={{ paddingRight: 16 }}>
{success ? <CheckCircle color={theme.green1} size={24} /> : <AlertCircle color={theme.red1} size={24} />} {success ? <CheckCircle color={theme.green1} size={24} /> : <AlertCircle color={theme.red1} size={24} />}
</div> </div>
...@@ -22,6 +34,6 @@ export default function TxnPopup({ hash, success, summary }: { hash: string; suc ...@@ -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> <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> <ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>View on Etherscan</ExternalLink>
</AutoColumn> </AutoColumn>
</AutoRow> </RowNoFlex>
) )
} }
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { useMediaLayout } from 'use-media'
import { useActivePopups } from '../../state/application/hooks' import { useActivePopups } from '../../state/application/hooks'
import { AutoColumn } from '../Column' import { AutoColumn } from '../Column'
import PopupItem from './PopupItem' import PopupItem from './PopupItem'
...@@ -11,6 +10,11 @@ const MobilePopupWrapper = styled.div<{ height: string | number }>` ...@@ -11,6 +10,11 @@ const MobilePopupWrapper = styled.div<{ height: string | number }>`
height: ${({ height }) => height}; height: ${({ height }) => height};
margin: ${({ height }) => (height ? '0 auto;' : 0)}; margin: ${({ height }) => (height ? '0 auto;' : 0)};
margin-bottom: ${({ height }) => (height ? '20px' : 0)}}; margin-bottom: ${({ height }) => (height ? '20px' : 0)}};
display: none;
${({ theme }) => theme.mediaWidth.upToSmall`
display: block;
`};
` `
const MobilePopupInner = styled.div` const MobilePopupInner = styled.div`
...@@ -26,8 +30,8 @@ const MobilePopupInner = styled.div` ...@@ -26,8 +30,8 @@ const MobilePopupInner = styled.div`
` `
const FixedPopupColumn = styled(AutoColumn)` const FixedPopupColumn = styled(AutoColumn)`
position: absolute; position: fixed;
top: 112px; top: 64px;
right: 1rem; right: 1rem;
max-width: 355px !important; max-width: 355px !important;
width: 100%; width: 100%;
...@@ -41,21 +45,13 @@ export default function Popups() { ...@@ -41,21 +45,13 @@ export default function Popups() {
// get all popups // get all popups
const activePopups = useActivePopups() const activePopups = useActivePopups()
// switch view settings on mobile return (
const isMobile = useMediaLayout({ maxWidth: '600px' }) <>
if (!isMobile) {
return (
<FixedPopupColumn gap="20px"> <FixedPopupColumn gap="20px">
{activePopups.map(item => ( {activePopups.map(item => (
<PopupItem key={item.key} content={item.content} popKey={item.key} /> <PopupItem key={item.key} content={item.content} popKey={item.key} />
))} ))}
</FixedPopupColumn> </FixedPopupColumn>
)
}
//mobile
else
return (
<MobilePopupWrapper height={activePopups?.length > 0 ? 'fit-content' : 0}> <MobilePopupWrapper height={activePopups?.length > 0 ? 'fit-content' : 0}>
<MobilePopupInner> <MobilePopupInner>
{activePopups // reverse so new items up front {activePopups // reverse so new items up front
...@@ -66,5 +62,6 @@ export default function Popups() { ...@@ -66,5 +62,6 @@ export default function Popups() {
))} ))}
</MobilePopupInner> </MobilePopupInner>
</MobilePopupWrapper> </MobilePopupWrapper>
) </>
)
} }
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>
)
}
{
"extends": "../../../tsconfig.strict.json",
"include": ["**/*"]
}
\ No newline at end of file
...@@ -349,9 +349,7 @@ export default function WalletModal({ ...@@ -349,9 +349,7 @@ export default function WalletModal({
{walletView !== WALLET_VIEWS.PENDING && ( {walletView !== WALLET_VIEWS.PENDING && (
<Blurb> <Blurb>
<span>New to Ethereum? &nbsp;</span>{' '} <span>New to Ethereum? &nbsp;</span>{' '}
<ExternalLink href="https://ethereum.org/use/#3-what-is-a-wallet-and-which-one-should-i-use"> <ExternalLink href="https://ethereum.org/wallets/">Learn more about wallets</ExternalLink>
Learn more about wallets
</ExternalLink>
</Blurb> </Blurb>
)} )}
</ContentWrapper> </ContentWrapper>
......
...@@ -73,23 +73,27 @@ export function AdvancedSwapDetails({ trade }: AdvancedSwapDetailsProps) { ...@@ -73,23 +73,27 @@ export function AdvancedSwapDetails({ trade }: AdvancedSwapDetailsProps) {
const [allowedSlippage] = useUserSlippageTolerance() const [allowedSlippage] = useUserSlippageTolerance()
const showRoute = trade?.route?.path?.length > 2 const showRoute = Boolean(trade && trade.route.path.length > 2)
return ( return (
<AutoColumn gap="md"> <AutoColumn gap="md">
{trade && <TradeSummary trade={trade} allowedSlippage={allowedSlippage} />} {trade && (
{showRoute && (
<> <>
<SectionBreak /> <TradeSummary trade={trade} allowedSlippage={allowedSlippage} />
<AutoColumn style={{ padding: '0 24px' }}> {showRoute && (
<RowFixed> <>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}> <SectionBreak />
Route <AutoColumn style={{ padding: '0 24px' }}>
</TYPE.black> <RowFixed>
<QuestionHelper text="Routing through these tokens resulted in the best price for your trade." /> <TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
</RowFixed> Route
<SwapRoute trade={trade} /> </TYPE.black>
</AutoColumn> <QuestionHelper text="Routing through these tokens resulted in the best price for your trade." />
</RowFixed>
<SwapRoute trade={trade} />
</AutoColumn>
</>
)}
</> </>
)} )}
</AutoColumn> </AutoColumn>
......
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import useLast from '../../hooks/useLast' import { useLastTruthy } from '../../hooks/useLast'
import { AdvancedSwapDetails, AdvancedSwapDetailsProps } from './AdvancedSwapDetails' import { AdvancedSwapDetails, AdvancedSwapDetailsProps } from './AdvancedSwapDetails'
const AdvancedDetailsFooter = styled.div<{ show: boolean }>` const AdvancedDetailsFooter = styled.div<{ show: boolean }>`
...@@ -20,11 +20,11 @@ const AdvancedDetailsFooter = styled.div<{ show: boolean }>` ...@@ -20,11 +20,11 @@ const AdvancedDetailsFooter = styled.div<{ show: boolean }>`
` `
export default function AdvancedSwapDetailsDropdown({ trade, ...rest }: AdvancedSwapDetailsProps) { export default function AdvancedSwapDetailsDropdown({ trade, ...rest }: AdvancedSwapDetailsProps) {
const lastTrade = useLast(trade) const lastTrade = useLastTruthy(trade)
return ( return (
<AdvancedDetailsFooter show={Boolean(trade)}> <AdvancedDetailsFooter show={Boolean(trade)}>
<AdvancedSwapDetails {...rest} trade={trade ?? lastTrade} /> <AdvancedSwapDetails {...rest} trade={trade ?? lastTrade ?? undefined} />
</AdvancedDetailsFooter> </AdvancedDetailsFooter>
) )
} }
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' ...@@ -4,10 +4,13 @@ import { ONE_BIPS } from '../../constants'
import { warningSeverity } from '../../utils/prices' import { warningSeverity } from '../../utils/prices'
import { ErrorText } from './styleds' import { ErrorText } from './styleds'
/**
* Formatted version of price impact text with warning colors
*/
export default function FormattedPriceImpact({ priceImpact }: { priceImpact?: Percent }) { export default function FormattedPriceImpact({ priceImpact }: { priceImpact?: Percent }) {
return ( return (
<ErrorText fontWeight={500} fontSize={14} severity={warningSeverity(priceImpact)}> <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> </ErrorText>
) )
} }
import { CurrencyAmount, Percent, Trade, TradeType } from '@uniswap/sdk' import { Trade, TradeType } from '@uniswap/sdk'
import React, { useContext } from 'react' import React, { useContext, useMemo, useState } from 'react'
import { Repeat } from 'react-feather' import { Repeat } from 'react-feather'
import { Text } from 'rebass' import { Text } from 'rebass'
import { ThemeContext } from 'styled-components' import { ThemeContext } from 'styled-components'
import { Field } from '../../state/swap/actions' import { Field } from '../../state/swap/actions'
import { TYPE } from '../../theme' import { TYPE } from '../../theme'
import { formatExecutionPrice } from '../../utils/prices' import {
computeSlippageAdjustedAmounts,
computeTradePriceBreakdown,
formatExecutionPrice,
warningSeverity
} from '../../utils/prices'
import { ButtonError } from '../Button' import { ButtonError } from '../Button'
import { AutoColumn } from '../Column' import { AutoColumn } from '../Column'
import QuestionHelper from '../QuestionHelper' import QuestionHelper from '../QuestionHelper'
import { AutoRow, RowBetween, RowFixed } from '../Row' import { AutoRow, RowBetween, RowFixed } from '../Row'
import FormattedPriceImpact from './FormattedPriceImpact' import FormattedPriceImpact from './FormattedPriceImpact'
import { StyledBalanceMaxMini } from './styleds' import { StyledBalanceMaxMini, SwapCallbackError } from './styleds'
export default function SwapModalFooter({ export default function SwapModalFooter({
trade, trade,
showInverted, onConfirm,
setShowInverted, allowedSlippage,
severity, swapErrorMessage,
slippageAdjustedAmounts, disabledConfirm
onSwap,
parsedAmounts,
realizedLPFee,
priceImpactWithoutFee,
confirmText
}: { }: {
trade?: Trade trade: Trade
showInverted: boolean allowedSlippage: number
setShowInverted: (inverted: boolean) => void onConfirm: () => void
severity: number swapErrorMessage: string | undefined
slippageAdjustedAmounts?: { [field in Field]?: CurrencyAmount } disabledConfirm: boolean
onSwap: () => any
parsedAmounts?: { [field in Field]?: CurrencyAmount }
realizedLPFee?: CurrencyAmount
priceImpactWithoutFee?: Percent
confirmText: string
}) { }) {
const [showInverted, setShowInverted] = useState<boolean>(false)
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
const slippageAdjustedAmounts = useMemo(() => computeSlippageAdjustedAmounts(trade, allowedSlippage), [
if (!trade) { allowedSlippage,
return null trade
} ])
const { priceImpactWithoutFee, realizedLPFee } = useMemo(() => computeTradePriceBreakdown(trade), [trade])
const severity = warningSeverity(priceImpactWithoutFee)
return ( return (
<> <>
...@@ -71,23 +69,21 @@ export default function SwapModalFooter({ ...@@ -71,23 +69,21 @@ export default function SwapModalFooter({
<RowBetween> <RowBetween>
<RowFixed> <RowFixed>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}> <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> </TYPE.black>
<QuestionHelper text="Your transaction will revert if there is a large, unfavorable price movement before it is confirmed." /> <QuestionHelper text="Your transaction will revert if there is a large, unfavorable price movement before it is confirmed." />
</RowFixed> </RowFixed>
<RowFixed> <RowFixed>
<TYPE.black fontSize={14}> <TYPE.black fontSize={14}>
{trade?.tradeType === TradeType.EXACT_INPUT {trade.tradeType === TradeType.EXACT_INPUT
? slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(4) ?? '-' ? slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(4) ?? '-'
: slippageAdjustedAmounts[Field.INPUT]?.toSignificant(4) ?? '-'} : slippageAdjustedAmounts[Field.INPUT]?.toSignificant(4) ?? '-'}
</TYPE.black> </TYPE.black>
{parsedAmounts[Field.OUTPUT] && parsedAmounts[Field.INPUT] && ( <TYPE.black fontSize={14} marginLeft={'4px'}>
<TYPE.black fontSize={14} marginLeft={'4px'}> {trade.tradeType === TradeType.EXACT_INPUT
{trade?.tradeType === TradeType.EXACT_INPUT ? trade.outputAmount.currency.symbol
? parsedAmounts[Field.OUTPUT]?.currency?.symbol : trade.inputAmount.currency.symbol}
: parsedAmounts[Field.INPUT]?.currency?.symbol} </TYPE.black>
</TYPE.black>
)}
</RowFixed> </RowFixed>
</RowBetween> </RowBetween>
<RowBetween> <RowBetween>
...@@ -107,17 +103,25 @@ export default function SwapModalFooter({ ...@@ -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." /> <QuestionHelper text="A portion of each trade (0.30%) goes to liquidity providers as a protocol incentive." />
</RowFixed> </RowFixed>
<TYPE.black fontSize={14}> <TYPE.black fontSize={14}>
{realizedLPFee ? realizedLPFee?.toSignificant(6) + ' ' + trade?.inputAmount?.currency?.symbol : '-'} {realizedLPFee ? realizedLPFee?.toSignificant(6) + ' ' + trade.inputAmount.currency.symbol : '-'}
</TYPE.black> </TYPE.black>
</RowBetween> </RowBetween>
</AutoColumn> </AutoColumn>
<AutoRow> <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}> <Text fontSize={20} fontWeight={500}>
{confirmText} {severity > 2 ? 'Swap Anyway' : 'Confirm Swap'}
</Text> </Text>
</ButtonError> </ButtonError>
{swapErrorMessage ? <SwapCallbackError error={swapErrorMessage} /> : null}
</AutoRow> </AutoRow>
</> </>
) )
......
import { Currency, CurrencyAmount } from '@uniswap/sdk' import { Trade, TradeType } from '@uniswap/sdk'
import React, { useContext } from 'react' import React, { useContext, useMemo } from 'react'
import { ArrowDown } from 'react-feather' import { ArrowDown, AlertTriangle } from 'react-feather'
import { Text } from 'rebass' import { Text } from 'rebass'
import { ThemeContext } from 'styled-components' import { ThemeContext } from 'styled-components'
import { Field } from '../../state/swap/actions' import { Field } from '../../state/swap/actions'
import { TYPE } from '../../theme' import { TYPE } from '../../theme'
import { ButtonPrimary } from '../Button'
import { isAddress, shortenAddress } from '../../utils' import { isAddress, shortenAddress } from '../../utils'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
import { AutoColumn } from '../Column' import { AutoColumn } from '../Column'
import { RowBetween, RowFixed } from '../Row'
import CurrencyLogo from '../CurrencyLogo' import CurrencyLogo from '../CurrencyLogo'
import { TruncatedText } from './styleds' import { RowBetween, RowFixed } from '../Row'
import { TruncatedText, SwapShowAcceptChanges } from './styleds'
export default function SwapModalHeader({ export default function SwapModalHeader({
currencies, trade,
formattedAmounts, allowedSlippage,
slippageAdjustedAmounts, recipient,
priceImpactSeverity, showAcceptChanges,
independentField, onAcceptChanges
recipient
}: { }: {
currencies: { [field in Field]?: Currency } trade: Trade
formattedAmounts: { [field in Field]?: string } allowedSlippage: number
slippageAdjustedAmounts: { [field in Field]?: CurrencyAmount }
priceImpactSeverity: number
independentField: Field
recipient: string | null 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) const theme = useContext(ThemeContext)
return ( return (
<AutoColumn gap={'md'} style={{ marginTop: '20px' }}> <AutoColumn gap={'md'} style={{ marginTop: '20px' }}>
<RowBetween align="flex-end"> <RowBetween align="flex-end">
<TruncatedText fontSize={24} fontWeight={500}> <RowFixed gap={'0px'}>
{formattedAmounts[Field.INPUT]} <CurrencyLogo currency={trade.inputAmount.currency} size={'24px'} style={{ marginRight: '12px' }} />
</TruncatedText> <TruncatedText
<RowFixed gap="4px"> fontSize={24}
<CurrencyLogo currency={currencies[Field.INPUT]} size={'24px'} /> 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' }}> <Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{currencies[Field.INPUT]?.symbol} {trade.inputAmount.currency.symbol}
</Text> </Text>
</RowFixed> </RowFixed>
</RowBetween> </RowBetween>
<RowFixed> <RowFixed>
<ArrowDown size="16" color={theme.text2} /> <ArrowDown size="16" color={theme.text2} style={{ marginLeft: '4px', minWidth: '16px' }} />
</RowFixed> </RowFixed>
<RowBetween align="flex-end"> <RowBetween align="flex-end">
<TruncatedText fontSize={24} fontWeight={500} color={priceImpactSeverity > 2 ? theme.red1 : ''}> <RowFixed gap={'0px'}>
{formattedAmounts[Field.OUTPUT]} <CurrencyLogo currency={trade.outputAmount.currency} size={'24px'} style={{ marginRight: '12px' }} />
</TruncatedText> <TruncatedText
<RowFixed gap="4px"> fontSize={24}
<CurrencyLogo currency={currencies[Field.OUTPUT]} size={'24px'} /> 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' }}> <Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{currencies[Field.OUTPUT]?.symbol} {trade.outputAmount.currency.symbol}
</Text> </Text>
</RowFixed> </RowFixed>
</RowBetween> </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' }}> <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%' }}> <TYPE.italic textAlign="left" style={{ width: '100%' }}>
{`Output is estimated. You will receive at least `} {`Output is estimated. You will receive at least `}
<b> <b>
{slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} {currencies[Field.OUTPUT]?.symbol} {slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} {trade.outputAmount.currency.symbol}
</b> </b>
{' or the transaction will revert.'} {' or the transaction will revert.'}
</TYPE.italic> </TYPE.italic>
...@@ -68,7 +109,7 @@ export default function SwapModalHeader({ ...@@ -68,7 +109,7 @@ export default function SwapModalHeader({
<TYPE.italic textAlign="left" style={{ width: '100%' }}> <TYPE.italic textAlign="left" style={{ width: '100%' }}>
{`Input is estimated. You will sell at most `} {`Input is estimated. You will sell at most `}
<b> <b>
{slippageAdjustedAmounts[Field.INPUT]?.toSignificant(6)} {currencies[Field.INPUT]?.symbol} {slippageAdjustedAmounts[Field.INPUT]?.toSignificant(6)} {trade.inputAmount.currency.symbol}
</b> </b>
{' or the transaction will revert.'} {' or the transaction will revert.'}
</TYPE.italic> </TYPE.italic>
......
// gathers additional user consent for a high price impact
import { Percent } from '@uniswap/sdk' import { Percent } from '@uniswap/sdk'
import { ALLOWED_PRICE_IMPACT_HIGH, PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN } from '../../constants' 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 { export default function confirmPriceImpactWithoutFee(priceImpactWithoutFee: Percent): boolean {
if (!priceImpactWithoutFee.lessThan(PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN)) { if (!priceImpactWithoutFee.lessThan(PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN)) {
return ( return (
......
import { transparentize } from 'polished'
import React from 'react'
import { AlertTriangle } from 'react-feather'
import styled, { css } from 'styled-components' import styled, { css } from 'styled-components'
import { AutoColumn } from '../Column'
import { Text } from 'rebass' import { Text } from 'rebass'
import { AutoColumn } from '../Column'
import NumericalInput from '../NumericalInput'
export const Wrapper = styled.div` export const Wrapper = styled.div`
position: relative; position: relative;
...@@ -30,7 +31,6 @@ export const SectionBreak = styled.div` ...@@ -30,7 +31,6 @@ export const SectionBreak = styled.div`
export const BottomGrouping = styled.div` export const BottomGrouping = styled.div`
margin-top: 12px; margin-top: 12px;
position: relative;
` `
export const ErrorText = styled(Text)<{ severity?: 0 | 1 | 2 | 3 | 4 }>` 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 }>` ...@@ -44,21 +44,6 @@ export const ErrorText = styled(Text)<{ severity?: 0 | 1 | 2 | 3 | 4 }>`
: theme.green1}; : 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` export const StyledBalanceMaxMini = styled.button`
height: 22px; height: 22px;
width: 22px; width: 22px;
...@@ -112,3 +97,51 @@ export const Dots = styled.span` ...@@ -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;
`
{
"extends": "../../../tsconfig.strict.json",
"include": ["**/*"]
}
\ No newline at end of file
...@@ -22,19 +22,83 @@ class RequestError extends Error { ...@@ -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 { class MiniRpcProvider implements AsyncSendable {
public readonly isMetaMask: false = false public readonly isMetaMask: false = false
public readonly chainId: number public readonly chainId: number
public readonly url: string public readonly url: string
public readonly host: string public readonly host: string
public readonly path: 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.chainId = chainId
this.url = url this.url = url
const parsed = new URL(url) const parsed = new URL(url)
this.host = parsed.host this.host = parsed.host
this.path = parsed.pathname 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 = ( public readonly sendAsync = (
...@@ -56,24 +120,20 @@ class MiniRpcProvider implements AsyncSendable { ...@@ -56,24 +120,20 @@ class MiniRpcProvider implements AsyncSendable {
if (method === 'eth_chainId') { if (method === 'eth_chainId') {
return `0x${this.chainId.toString(16)}` return `0x${this.chainId.toString(16)}`
} }
const response = await fetch(this.url, { const promise = new Promise((resolve, reject) => {
method: 'POST', this.batch.push({
body: JSON.stringify({ request: {
jsonrpc: '2.0', jsonrpc: '2.0',
id: 1, id: this.nextId++,
method, method,
params params
},
resolve,
reject
}) })
}) })
if (!response.ok) throw new RequestError(`${response.status}: ${response.statusText}`, -32000) this.batchTimeoutId = this.batchTimeoutId ?? setTimeout(this.clearBatch, this.batchWaitTimeMs)
const body = await response.json() return promise
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)
}
} }
} }
......
import { AbstractConnector } from '@web3-react/abstract-connector'
import { ChainId, JSBI, Percent, Token, WETH } from '@uniswap/sdk' import { ChainId, JSBI, Percent, Token, WETH } from '@uniswap/sdk'
import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors' import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors'
...@@ -52,7 +53,19 @@ export const PINNED_PAIRS: { readonly [chainId in ChainId]?: [Token, Token][] } ...@@ -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: { INJECTED: {
connector: injected, connector: injected,
name: 'Injected', name: 'Injected',
...@@ -69,62 +82,53 @@ const TESTNET_CAPABLE_WALLETS = { ...@@ -69,62 +82,53 @@ const TESTNET_CAPABLE_WALLETS = {
description: 'Easy-to-use browser extension.', description: 'Easy-to-use browser extension.',
href: null, href: null,
color: '#E8831D' 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' export const NetworkContextName = 'NETWORK'
// default allowed slippage, in bips // default allowed slippage, in bips
......
...@@ -131,7 +131,7 @@ export function useV1Trade( ...@@ -131,7 +131,7 @@ export function useV1Trade(
? new Trade(route, exactAmount, isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT) ? new Trade(route, exactAmount, isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT)
: undefined : undefined
} catch (error) { } catch (error) {
console.error('Failed to create V1 trade', error) console.debug('Failed to create V1 trade', error)
} }
return v1Trade return v1Trade
} }
......
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
/** /**
* Returns the last truthy value of type T * Returns the last value of type T that passes a filter function
* @param value changing value * @param 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): T | null | undefined { export default function useLast<T>(
const [last, setLast] = useState<T | null | undefined>(value) 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(() => { useEffect(() => {
setLast(last => value ?? last) setLast(last => {
}, [value]) const shouldUse: boolean = filterFn ? filterFn(value) : true
if (shouldUse) return value
return last
})
}, [filterFn, value])
return last 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 function useLastTruthy<T>(value: T | undefined | null): T | null | undefined {
return useLast(value, isDefined)
}
This diff is collapsed.
...@@ -23,7 +23,7 @@ export default function useWrapCallback( ...@@ -23,7 +23,7 @@ export default function useWrapCallback(
inputCurrency: Currency | undefined, inputCurrency: Currency | undefined,
outputCurrency: Currency | undefined, outputCurrency: Currency | undefined,
typedValue: string | 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 { chainId, account } = useActiveWeb3React()
const wethContract = useWETHContract() const wethContract = useWETHContract()
const balance = useCurrencyBalance(account ?? undefined, inputCurrency) const balance = useCurrencyBalance(account ?? undefined, inputCurrency)
...@@ -50,7 +50,7 @@ export default function useWrapCallback( ...@@ -50,7 +50,7 @@ export default function useWrapCallback(
} }
} }
: undefined, : undefined,
error: sufficientBalance ? undefined : 'Insufficient ETH balance' inputError: sufficientBalance ? undefined : 'Insufficient ETH balance'
} }
} else if (currencyEquals(WETH[chainId], inputCurrency) && outputCurrency === ETHER) { } else if (currencyEquals(WETH[chainId], inputCurrency) && outputCurrency === ETHER) {
return { return {
...@@ -66,7 +66,7 @@ export default function useWrapCallback( ...@@ -66,7 +66,7 @@ export default function useWrapCallback(
} }
} }
: undefined, : undefined,
error: sufficientBalance ? undefined : 'Insufficient WETH balance' inputError: sufficientBalance ? undefined : 'Insufficient WETH balance'
} }
} else { } else {
return NOT_APPLICABLE return NOT_APPLICABLE
......
import { Currency, Fraction, Percent } from '@uniswap/sdk' import { Currency, Percent, Price } from '@uniswap/sdk'
import React, { useContext } from 'react' import React, { useContext } from 'react'
import { Text } from 'rebass' import { Text } from 'rebass'
import { ThemeContext } from 'styled-components' import { ThemeContext } from 'styled-components'
...@@ -8,7 +8,7 @@ import { ONE_BIPS } from '../../constants' ...@@ -8,7 +8,7 @@ import { ONE_BIPS } from '../../constants'
import { Field } from '../../state/mint/actions' import { Field } from '../../state/mint/actions'
import { TYPE } from '../../theme' import { TYPE } from '../../theme'
export const PoolPriceBar = ({ export function PoolPriceBar({
currencies, currencies,
noLiquidity, noLiquidity,
poolTokenPercentage, poolTokenPercentage,
...@@ -17,20 +17,20 @@ export const PoolPriceBar = ({ ...@@ -17,20 +17,20 @@ export const PoolPriceBar = ({
currencies: { [field in Field]?: Currency } currencies: { [field in Field]?: Currency }
noLiquidity?: boolean noLiquidity?: boolean
poolTokenPercentage?: Percent poolTokenPercentage?: Percent
price?: Fraction price?: Price
}) => { }) {
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
return ( return (
<AutoColumn gap="md"> <AutoColumn gap="md">
<AutoRow justify="space-around" gap="4px"> <AutoRow justify="space-around" gap="4px">
<AutoColumn justify="center"> <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}> <Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
{currencies[Field.CURRENCY_B]?.symbol} per {currencies[Field.CURRENCY_A]?.symbol} {currencies[Field.CURRENCY_B]?.symbol} per {currencies[Field.CURRENCY_A]?.symbol}
</Text> </Text>
</AutoColumn> </AutoColumn>
<AutoColumn justify="center"> <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}> <Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
{currencies[Field.CURRENCY_A]?.symbol} per {currencies[Field.CURRENCY_B]?.symbol} {currencies[Field.CURRENCY_A]?.symbol} per {currencies[Field.CURRENCY_B]?.symbol}
</Text> </Text>
......
...@@ -10,7 +10,7 @@ import { ThemeContext } from 'styled-components' ...@@ -10,7 +10,7 @@ import { ThemeContext } from 'styled-components'
import { ButtonError, ButtonLight, ButtonPrimary } from '../../components/Button' import { ButtonError, ButtonLight, ButtonPrimary } from '../../components/Button'
import { BlueCard, GreyCard, LightCard } from '../../components/Card' import { BlueCard, GreyCard, LightCard } from '../../components/Card'
import { AutoColumn, ColumnCenter } from '../../components/Column' import { AutoColumn, ColumnCenter } from '../../components/Column'
import ConfirmationModal from '../../components/ConfirmationModal' import TransactionConfirmationModal, { ConfirmationModalContent } from '../../components/TransactionConfirmationModal'
import CurrencyInputPanel from '../../components/CurrencyInputPanel' import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import DoubleCurrencyLogo from '../../components/DoubleLogo' import DoubleCurrencyLogo from '../../components/DoubleLogo'
import { AddRemoveTabs } from '../../components/NavigationTabs' import { AddRemoveTabs } from '../../components/NavigationTabs'
...@@ -294,27 +294,34 @@ export default function AddLiquidity({ ...@@ -294,27 +294,34 @@ export default function AddLiquidity({
[currencyIdA, history, currencyIdB] [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 ( return (
<> <>
<AppBody> <AppBody>
<AddRemoveTabs adding={true} /> <AddRemoveTabs adding={true} />
<Wrapper> <Wrapper>
<ConfirmationModal <TransactionConfirmationModal
isOpen={showConfirm} isOpen={showConfirm}
onDismiss={() => { onDismiss={handleDismissConfirmation}
setShowConfirm(false)
// if there was a tx hash, we want to clear the input
if (txHash) {
onFieldAInput('')
}
setTxHash('')
}}
attemptingTxn={attemptingTxn} attemptingTxn={attemptingTxn}
hash={txHash} hash={txHash}
topContent={modalHeader} content={() => (
bottomContent={modalBottom} <ConfirmationModalContent
title={noLiquidity ? 'You are creating a pool' : 'You will receive'}
onDismiss={handleDismissConfirmation}
topContent={modalHeader}
bottomContent={modalBottom}
/>
)}
pendingText={pendingText} pendingText={pendingText}
title={noLiquidity ? 'You are creating a pool' : 'You will receive'}
/> />
<AutoColumn gap="20px"> <AutoColumn gap="20px">
{noLiquidity && ( {noLiquidity && (
......
...@@ -11,7 +11,7 @@ import { ThemeContext } from 'styled-components' ...@@ -11,7 +11,7 @@ import { ThemeContext } from 'styled-components'
import { ButtonPrimary, ButtonLight, ButtonError, ButtonConfirmed } from '../../components/Button' import { ButtonPrimary, ButtonLight, ButtonError, ButtonConfirmed } from '../../components/Button'
import { LightCard } from '../../components/Card' import { LightCard } from '../../components/Card'
import { AutoColumn, ColumnCenter } from '../../components/Column' import { AutoColumn, ColumnCenter } from '../../components/Column'
import ConfirmationModal from '../../components/ConfirmationModal' import TransactionConfirmationModal, { ConfirmationModalContent } from '../../components/TransactionConfirmationModal'
import CurrencyInputPanel from '../../components/CurrencyInputPanel' import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import DoubleCurrencyLogo from '../../components/DoubleLogo' import DoubleCurrencyLogo from '../../components/DoubleLogo'
import { AddRemoveTabs } from '../../components/NavigationTabs' import { AddRemoveTabs } from '../../components/NavigationTabs'
...@@ -274,12 +274,13 @@ export default function RemoveLiquidity({ ...@@ -274,12 +274,13 @@ export default function RemoveLiquidity({
throw new Error('Attempting to confirm without approval or a signature. Please contact support.') 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 => methodNames.map(methodName =>
router.estimateGas[methodName](...args) router.estimateGas[methodName](...args)
.then(calculateGasMargin) .then(calculateGasMargin)
.catch(error => { .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({ ...@@ -447,28 +448,35 @@ export default function RemoveLiquidity({
[currencyIdA, currencyIdB, history] [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 ( return (
<> <>
<AppBody> <AppBody>
<AddRemoveTabs adding={false} /> <AddRemoveTabs adding={false} />
<Wrapper> <Wrapper>
<ConfirmationModal <TransactionConfirmationModal
isOpen={showConfirm} isOpen={showConfirm}
onDismiss={() => { onDismiss={handleDismissConfirmation}
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('')
}}
attemptingTxn={attemptingTxn} attemptingTxn={attemptingTxn}
hash={txHash ? txHash : ''} hash={txHash ? txHash : ''}
topContent={modalHeader} content={() => (
bottomContent={modalBottom} <ConfirmationModalContent
title={'You will receive'}
onDismiss={handleDismissConfirmation}
topContent={modalHeader}
bottomContent={modalBottom}
/>
)}
pendingText={pendingText} pendingText={pendingText}
title="You will receive"
/> />
<AutoColumn gap="md"> <AutoColumn gap="md">
<LightCard> <LightCard>
......
This diff is collapsed.
...@@ -50,9 +50,10 @@ export function useDerivedMintInfo( ...@@ -50,9 +50,10 @@ export function useDerivedMintInfo(
// pair // pair
const [pairState, pair] = usePair(currencies[Field.CURRENCY_A], currencies[Field.CURRENCY_B]) const [pairState, pair] = usePair(currencies[Field.CURRENCY_A], currencies[Field.CURRENCY_B])
const totalSupply = useTotalSupply(pair?.liquidityToken)
const noLiquidity: boolean = const noLiquidity: boolean =
pairState === PairState.NOT_EXISTS || pairState === PairState.NOT_EXISTS || Boolean(totalSupply && JSBI.equal(totalSupply.raw, ZERO))
Boolean(pair && JSBI.equal(pair.reserve0.raw, ZERO) && JSBI.equal(pair.reserve1.raw, ZERO))
// balances // balances
const balances = useCurrencyBalances(account ?? undefined, [ const balances = useCurrencyBalances(account ?? undefined, [
...@@ -94,16 +95,20 @@ export function useDerivedMintInfo( ...@@ -94,16 +95,20 @@ export function useDerivedMintInfo(
[Field.CURRENCY_B]: independentField === Field.CURRENCY_A ? dependentAmount : independentAmount [Field.CURRENCY_B]: independentField === Field.CURRENCY_A ? dependentAmount : independentAmount
} }
const token0Price = pair?.token0Price
const price = useMemo(() => { const price = useMemo(() => {
const { [Field.CURRENCY_A]: currencyAAmount, [Field.CURRENCY_B]: currencyBAmount } = parsedAmounts if (noLiquidity) {
if (currencyAAmount && currencyBAmount) { const { [Field.CURRENCY_A]: currencyAAmount, [Field.CURRENCY_B]: currencyBAmount } = parsedAmounts
return new Price(currencyAAmount.currency, currencyBAmount.currency, currencyAAmount.raw, currencyBAmount.raw) if (currencyAAmount && currencyBAmount) {
return new Price(currencyAAmount.currency, currencyBAmount.currency, currencyAAmount.raw, currencyBAmount.raw)
}
return
} else {
return token0Price
} }
return }, [noLiquidity, token0Price, parsedAmounts])
}, [parsedAmounts])
// liquidity minted // liquidity minted
const totalSupply = useTotalSupply(pair?.liquidityToken)
const liquidityMinted = useMemo(() => { const liquidityMinted = useMemo(() => {
const { [Field.CURRENCY_A]: currencyAAmount, [Field.CURRENCY_B]: currencyBAmount } = parsedAmounts const { [Field.CURRENCY_A]: currencyAAmount, [Field.CURRENCY_B]: currencyBAmount } = parsedAmounts
const [tokenAmountA, tokenAmountB] = [ const [tokenAmountA, tokenAmountB] = [
......
import { Contract } from '@ethersproject/contracts' import { Contract } from '@ethersproject/contracts'
import { useEffect, useMemo } from 'react' import { useEffect, useMemo, useRef } from 'react'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { useMulticallContract } from '../../hooks/useContract' import { useMulticallContract } from '../../hooks/useContract'
import useDebounce from '../../hooks/useDebounce' import useDebounce from '../../hooks/useDebounce'
import chunkArray from '../../utils/chunkArray' import chunkArray from '../../utils/chunkArray'
import { retry } from '../../utils/retry' import { CancelledError, retry, RetryableError } from '../../utils/retry'
import { useBlockNumber } from '../application/hooks' import { useBlockNumber } from '../application/hooks'
import { AppDispatch, AppState } from '../index' import { AppDispatch, AppState } from '../index'
import { import {
...@@ -30,11 +30,17 @@ async function fetchChunk( ...@@ -30,11 +30,17 @@ async function fetchChunk(
chunk: Call[], chunk: Call[],
minBlockNumber: number minBlockNumber: number
): Promise<{ results: string[]; blockNumber: number }> { ): Promise<{ results: string[]; blockNumber: number }> {
const [resultsBlockNumber, returnData] = await multicallContract.aggregate( console.debug('Fetching chunk', multicallContract, chunk, minBlockNumber)
chunk.map(obj => [obj.address, obj.callData]) 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) { 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() } return { results: returnData, blockNumber: resultsBlockNumber.toNumber() }
} }
...@@ -112,6 +118,7 @@ export default function Updater() { ...@@ -112,6 +118,7 @@ export default function Updater() {
const latestBlockNumber = useBlockNumber() const latestBlockNumber = useBlockNumber()
const { chainId } = useActiveWeb3React() const { chainId } = useActiveWeb3React()
const multicallContract = useMulticallContract() const multicallContract = useMulticallContract()
const cancellations = useRef<{ blockNumber: number; cancellations: (() => void)[] }>()
const listeningKeys: { [callKey: string]: number } = useMemo(() => { const listeningKeys: { [callKey: string]: number } = useMemo(() => {
return activeListeningKeys(debouncedListeners, chainId) return activeListeningKeys(debouncedListeners, chainId)
...@@ -134,6 +141,10 @@ export default function Updater() { ...@@ -134,6 +141,10 @@ export default function Updater() {
const chunkedCalls = chunkArray(calls, CALL_CHUNK_SIZE) const chunkedCalls = chunkArray(calls, CALL_CHUNK_SIZE)
if (cancellations.current?.blockNumber !== latestBlockNumber) {
cancellations.current?.cancellations?.forEach(c => c())
}
dispatch( dispatch(
fetchingMulticallResults({ fetchingMulticallResults({
calls, calls,
...@@ -142,38 +153,52 @@ export default function Updater() { ...@@ -142,38 +153,52 @@ export default function Updater() {
}) })
) )
chunkedCalls.forEach((chunk, index) => cancellations.current = {
// todo: cancel retries when the block number updates blockNumber: latestBlockNumber,
retry(() => fetchChunk(multicallContract, chunk, latestBlockNumber), { n: 10, minWait: 2500, maxWait: 5000 }) cancellations: chunkedCalls.map((chunk, index) => {
.then(({ results: returnData, blockNumber: fetchBlockNumber }) => { const { cancel, promise } = retry(() => fetchChunk(multicallContract, chunk, latestBlockNumber), {
// accumulates the length of all previous indices n: Infinity,
const firstCallKeyIndex = chunkedCalls.slice(0, index).reduce<number>((memo, curr) => memo + curr.length, 0) minWait: 2500,
const lastCallKeyIndex = firstCallKeyIndex + returnData.length maxWait: 3500
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
})
)
}) })
) promise
.then(({ results: returnData, blockNumber: fetchBlockNumber }) => {
cancellations.current = { cancellations: [], blockNumber: latestBlockNumber }
// accumulates the length of all previous indices
const firstCallKeyIndex = chunkedCalls.slice(0, index).reduce<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]) }, [chainId, multicallContract, dispatch, serializedOutdatedCallKeys, latestBlockNumber])
return null return null
......
...@@ -94,7 +94,7 @@ export function useDerivedSwapInfo(): { ...@@ -94,7 +94,7 @@ export function useDerivedSwapInfo(): {
currencyBalances: { [field in Field]?: CurrencyAmount } currencyBalances: { [field in Field]?: CurrencyAmount }
parsedAmount: CurrencyAmount | undefined parsedAmount: CurrencyAmount | undefined
v2Trade: Trade | undefined v2Trade: Trade | undefined
error?: string inputError?: string
v1Trade: Trade | undefined v1Trade: Trade | undefined
} { } {
const { account } = useActiveWeb3React() const { account } = useActiveWeb3React()
...@@ -140,21 +140,21 @@ export function useDerivedSwapInfo(): { ...@@ -140,21 +140,21 @@ export function useDerivedSwapInfo(): {
// get link to trade on v1, if a better rate exists // get link to trade on v1, if a better rate exists
const v1Trade = useV1Trade(isExactIn, currencies[Field.INPUT], currencies[Field.OUTPUT], parsedAmount) const v1Trade = useV1Trade(isExactIn, currencies[Field.INPUT], currencies[Field.OUTPUT], parsedAmount)
let error: string | undefined let inputError: string | undefined
if (!account) { if (!account) {
error = 'Connect Wallet' inputError = 'Connect Wallet'
} }
if (!parsedAmount) { if (!parsedAmount) {
error = error ?? 'Enter an amount' inputError = inputError ?? 'Enter an amount'
} }
if (!currencies[Field.INPUT] || !currencies[Field.OUTPUT]) { if (!currencies[Field.INPUT] || !currencies[Field.OUTPUT]) {
error = error ?? 'Select a token' inputError = inputError ?? 'Select a token'
} }
if (!to) { if (!to) {
error = error ?? 'Enter a recipient' inputError = inputError ?? 'Enter a recipient'
} }
const [allowedSlippage] = useUserSlippageTolerance() const [allowedSlippage] = useUserSlippageTolerance()
...@@ -177,7 +177,7 @@ export function useDerivedSwapInfo(): { ...@@ -177,7 +177,7 @@ export function useDerivedSwapInfo(): {
] ]
if (balanceIn && amountIn && balanceIn.lessThan(amountIn)) { if (balanceIn && amountIn && balanceIn.lessThan(amountIn)) {
error = 'Insufficient ' + amountIn.currency.symbol + ' balance' inputError = 'Insufficient ' + amountIn.currency.symbol + ' balance'
} }
return { return {
...@@ -185,7 +185,7 @@ export function useDerivedSwapInfo(): { ...@@ -185,7 +185,7 @@ export function useDerivedSwapInfo(): {
currencyBalances, currencyBalances,
parsedAmount, parsedAmount,
v2Trade: v2Trade ?? undefined, v2Trade: v2Trade ?? undefined,
error, inputError,
v1Trade v1Trade
} }
} }
......
...@@ -55,7 +55,7 @@ export function colors(darkMode: boolean): Colors { ...@@ -55,7 +55,7 @@ export function colors(darkMode: boolean): Colors {
bg5: darkMode ? '#565A69' : '#888D9B', bg5: darkMode ? '#565A69' : '#888D9B',
//specialty colors //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)', advancedBG: darkMode ? 'rgba(0,0,0,0.1)' : 'rgba(255,255,255,0.6)',
//primary colors //primary colors
......
...@@ -91,7 +91,7 @@ export function getContract(address: string, ABI: any, library: Web3Provider, ac ...@@ -91,7 +91,7 @@ export function getContract(address: string, ABI: any, library: Web3Provider, ac
} }
// account is optional // 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) return getContract(ROUTER_ADDRESS, IUniswapV2Router02ABI, library, account)
} }
......
/**
* Returns true if the string value is zero in hex
* @param hexNumberString
*/
export default function isZero(hexNumberString: string) {
return /^0x0*$/.test(hexNumberString)
}
import { retry } from './retry' import { retry, RetryableError } from './retry'
describe('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 () => { return async () => {
if (fails > 0) { if (fails > 0) {
fails-- fails--
throw new Error('failure') throw retryable ? new RetryableError('failure') : new Error('bad failure')
} }
return result 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 () => { 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 () => { 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 () => { 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) { async function checkTime(fn: () => Promise<any>, min: number, max: number) {
...@@ -36,7 +55,7 @@ describe('retry', () => { ...@@ -36,7 +55,7 @@ describe('retry', () => {
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
promises.push( promises.push(
checkTime( 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, 150,
305 305
) )
......
...@@ -6,6 +6,20 @@ function waitRandom(min: number, max: number): Promise<void> { ...@@ -6,6 +6,20 @@ function waitRandom(min: number, max: number): Promise<void> {
return wait(min + Math.round(Math.random() * Math.max(0, max - min))) 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 * Retries the function that returns the promise until the promise successfully resolves up to n retries
* @param fn function to retry * @param fn function to retry
...@@ -13,13 +27,43 @@ function waitRandom(min: number, max: number): Promise<void> { ...@@ -13,13 +27,43 @@ function waitRandom(min: number, max: number): Promise<void> {
* @param minWait min wait between retries in ms * @param minWait min wait between retries in ms
* @param maxWait max wait between retries in ms * @param maxWait max wait between retries in ms
*/ */
// todo: support cancelling the retry
export function retry<T>( export function retry<T>(
fn: () => Promise<T>, fn: () => Promise<T>,
{ n = 3, minWait = 500, maxWait = 1000 }: { n?: number; minWait?: number; maxWait?: number } = {} { n, minWait, maxWait }: { n: number; minWait: number; maxWait: number }
): Promise<T> { ): { promise: Promise<T>; cancel: () => void } {
return fn().catch(error => { let completed = false
if (n === 0) throw error let rejectCancelled: (error: Error) => void
return waitRandom(minWait, maxWait).then(() => retry(fn, { n: n - 1, minWait, maxWait })) 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())
}
}
} }
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment