Commit e6676154 authored by Noah Zinsmeister's avatar Noah Zinsmeister Committed by GitHub

support proposed new FoT-capable methods (#866)

* support new FoT-capable methods

short-circuit modal footer rendering if no trade

improve clarity of tx modal flow

* update router address + ABI
parent 4ab61fae
......@@ -10,8 +10,8 @@ import { ButtonPrimary } from '../Button'
import { AutoColumn, ColumnCenter } from '../Column'
import Circle from '../../assets/images/blue-loader.svg'
import { useActiveWeb3React } from '../../hooks'
import { getEtherscanLink } from '../../utils'
import { useActiveWeb3React } from '../../hooks'
const Wrapper = styled.div`
width: 100%;
......@@ -42,7 +42,6 @@ interface ConfirmationModalProps {
topContent: () => React.ReactChild
bottomContent: () => React.ReactChild
attemptingTxn: boolean
pendingConfirmation: boolean
pendingText: string
title?: string
}
......@@ -50,33 +49,22 @@ interface ConfirmationModalProps {
export default function ConfirmationModal({
isOpen,
onDismiss,
hash,
topContent,
bottomContent,
attemptingTxn,
pendingConfirmation,
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}>
{!attemptingTxn ? (
<Wrapper>
<Section>
<RowBetween>
<Text fontWeight={500} fontSize={20}>
{title}
</Text>
<CloseIcon onClick={onDismiss} />
</RowBetween>
{topContent()}
</Section>
<BottomSection gap="12px">{bottomContent()}</BottomSection>
</Wrapper>
) : (
<Wrapper>
<Section>
<RowBetween>
......@@ -84,22 +72,23 @@ export default function ConfirmationModal({
<CloseIcon onClick={onDismiss} />
</RowBetween>
<ConfirmedIcon>
{pendingConfirmation ? (
<CustomLightSpinner src={Circle} alt="loader" size={'90px'} />
) : (
{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}>
{!pendingConfirmation ? 'Transaction Submitted' : 'Waiting For Confirmation'}
{transactionBroadcast ? 'Transaction Submitted' : 'Waiting For Confirmation'}
</Text>
<AutoColumn gap="12px" justify={'center'}>
<Text fontWeight={600} fontSize={14} color="" textAlign="center">
{pendingText}
</Text>
</AutoColumn>
{!pendingConfirmation && (
{transactionBroadcast ? (
<>
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>
<Text fontWeight={500} fontSize={14} color={theme.primary1}>
......@@ -112,9 +101,7 @@ export default function ConfirmationModal({
</Text>
</ButtonPrimary>
</>
)}
{pendingConfirmation && (
) : (
<Text fontSize={12} color="#565A69" textAlign="center">
Confirm this transaction in your wallet
</Text>
......@@ -122,7 +109,25 @@ export default function ConfirmationModal({
</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>
)
}
......@@ -37,6 +37,11 @@ export default function SwapModalFooter({
confirmText: string
}) {
const theme = useContext(ThemeContext)
if (!trade) {
return null
}
return (
<>
<AutoColumn gap="0px">
......
......@@ -11,29 +11,30 @@ import TokenLogo from '../TokenLogo'
import { TruncatedText } from './styleds'
export default function SwapModalHeader({
formattedAmounts,
tokens,
formattedAmounts,
slippageAdjustedAmounts,
priceImpactSeverity,
independentField
}: {
formattedAmounts?: { [field in Field]?: string }
tokens?: { [field in Field]?: Token }
slippageAdjustedAmounts?: { [field in Field]?: TokenAmount }
tokens: { [field in Field]?: Token }
formattedAmounts: { [field in Field]?: string }
slippageAdjustedAmounts: { [field in Field]?: TokenAmount }
priceImpactSeverity: number
independentField: Field
}) {
const theme = useContext(ThemeContext)
return (
<AutoColumn gap={'md'} style={{ marginTop: '20px' }}>
<RowBetween align="flex-end">
<TruncatedText fontSize={24} fontWeight={500}>
{!!formattedAmounts[Field.INPUT] && formattedAmounts[Field.INPUT]}
{formattedAmounts[Field.INPUT]}
</TruncatedText>
<RowFixed gap="4px">
<TokenLogo address={tokens[Field.INPUT]?.address} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{tokens[Field.INPUT]?.symbol || ''}
{tokens[Field.INPUT]?.symbol}
</Text>
</RowFixed>
</RowBetween>
......@@ -42,12 +43,12 @@ export default function SwapModalHeader({
</RowFixed>
<RowBetween align="flex-end">
<TruncatedText fontSize={24} fontWeight={500} color={priceImpactSeverity > 2 ? theme.red1 : ''}>
{!!formattedAmounts[Field.OUTPUT] && formattedAmounts[Field.OUTPUT]}
{formattedAmounts[Field.OUTPUT]}
</TruncatedText>
<RowFixed gap="4px">
<TokenLogo address={tokens[Field.OUTPUT]?.address} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{tokens[Field.OUTPUT]?.symbol || ''}
{tokens[Field.OUTPUT]?.symbol}
</Text>
</RowFixed>
</RowBetween>
......@@ -56,7 +57,7 @@ export default function SwapModalHeader({
<TYPE.italic textAlign="left" style={{ width: '100%' }}>
{`Output is estimated. You will receive at least `}
<b>
{slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} {tokens[Field.OUTPUT]?.symbol}{' '}
{slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} {tokens[Field.OUTPUT]?.symbol}
</b>
{' or the transaction will revert.'}
</TYPE.italic>
......
......@@ -2,7 +2,7 @@ import { ChainId, JSBI, Percent, Token, WETH, Pair, TokenAmount } from '@uniswap
import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors'
export const ROUTER_ADDRESS = '0xf164fC0Ec4E93095b804a4795bBe1e041497b92a'
export const ROUTER_ADDRESS = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'
// used to construct intermediary pairs for trading
export const BASES_TO_CHECK_TRADES_AGAINST: { readonly [chainId in ChainId]: Token[] } = {
......
......@@ -90,11 +90,13 @@ export function useSwapCallback(
chainId as ChainId
)
let estimate, method: Function, args: Array<string | string[] | number>, value: BigNumber | null
// let estimate: Function, method: Function,
let methodNames: string[],
args: Array<string | string[] | number>,
value: BigNumber | null = null
switch (swapType) {
case SwapType.EXACT_TOKENS_FOR_TOKENS:
estimate = routerContract.estimateGas.swapExactTokensForTokens
method = routerContract.swapExactTokensForTokens
methodNames = ['swapExactTokensForTokens', 'swapExactTokensForTokensSupportingFeeOnTransferTokens']
args = [
slippageAdjustedInput.raw.toString(),
slippageAdjustedOutput.raw.toString(),
......@@ -102,11 +104,9 @@ export function useSwapCallback(
recipient,
deadlineFromNow
]
value = null
break
case SwapType.TOKENS_FOR_EXACT_TOKENS:
estimate = routerContract.estimateGas.swapTokensForExactTokens
method = routerContract.swapTokensForExactTokens
methodNames = ['swapTokensForExactTokens']
args = [
slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(),
......@@ -114,17 +114,14 @@ export function useSwapCallback(
recipient,
deadlineFromNow
]
value = null
break
case SwapType.EXACT_ETH_FOR_TOKENS:
estimate = routerContract.estimateGas.swapExactETHForTokens
method = routerContract.swapExactETHForTokens
methodNames = ['swapExactETHForTokens', 'swapExactETHForTokensSupportingFeeOnTransferTokens']
args = [slippageAdjustedOutput.raw.toString(), path, recipient, deadlineFromNow]
value = BigNumber.from(slippageAdjustedInput.raw.toString())
break
case SwapType.TOKENS_FOR_EXACT_ETH:
estimate = routerContract.estimateGas.swapTokensForExactETH
method = routerContract.swapTokensForExactETH
methodNames = ['swapTokensForExactETH']
args = [
slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(),
......@@ -132,11 +129,9 @@ export function useSwapCallback(
recipient,
deadlineFromNow
]
value = null
break
case SwapType.EXACT_TOKENS_FOR_ETH:
estimate = routerContract.estimateGas.swapExactTokensForETH
method = routerContract.swapExactTokensForETH
methodNames = ['swapExactTokensForETH', 'swapExactTokensForETHSupportingFeeOnTransferTokens']
args = [
slippageAdjustedInput.raw.toString(),
slippageAdjustedOutput.raw.toString(),
......@@ -144,24 +139,57 @@ export function useSwapCallback(
recipient,
deadlineFromNow
]
value = null
break
case SwapType.ETH_FOR_EXACT_TOKENS:
estimate = routerContract.estimateGas.swapETHForExactTokens
method = routerContract.swapETHForExactTokens
methodNames = ['swapETHForExactTokens']
args = [slippageAdjustedOutput.raw.toString(), path, recipient, deadlineFromNow]
value = BigNumber.from(slippageAdjustedInput.raw.toString())
break
}
return estimate(...args, value ? { value } : {})
.then(estimatedGasLimit =>
method(...args, {
...(value ? { value } : {}),
gasLimit: calculateGasMargin(estimatedGasLimit)
const safeGasEstimates = await Promise.all(
methodNames.map(methodName =>
routerContract.estimateGas[methodName](...args, value ? { value } : {})
.then(calculateGasMargin)
.catch(error => {
console.error(`estimateGas failed for ${methodName}`, error)
})
)
.then(response => {
)
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 (methodNames.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 (methodNames.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.')
}
} else {
const methodName = methodNames[indexOfSuccessfulEstimation]
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
return routerContract[methodName](...args, {
gasLimit: safeGasEstimate,
...(value ? { value } : {})
})
.then((response: any) => {
if (recipient === account) {
addTransaction(response, {
summary:
......@@ -192,10 +220,18 @@ export function useSwapCallback(
return response.hash
})
.catch(error => {
console.error(`Swap or gas estimate failed`, error)
.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 {
console.error(`swap failed for ${methodName}`, error)
throw Error('An error occurred while swapping. Please contact support.')
}
})
}
}
}, [account, allowedSlippage, addTransaction, chainId, deadline, inputAllowance, library, trade, ensName, recipient])
}
......@@ -36,7 +36,7 @@ import { useApproveCallback, ApprovalState } from '../../hooks/useApproveCallbac
import { useWalletModalToggle } from '../../state/application/hooks'
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
export default function AddLiquidity({ match: { params }, history }: RouteComponentProps<{ tokens: string }>) {
export default function AddLiquidity({ match: { params } }: RouteComponentProps<{ tokens: string }>) {
useDefaultsFromURLMatchParams(params)
const { account, chainId, library } = useActiveWeb3React()
......@@ -64,13 +64,12 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
const isValid = !error
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showAdvanced, setShowAdvanced] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirm
const [pendingConfirmation, setPendingConfirmation] = useState<boolean>(true) // waiting for user confirmation
// txn values
const [showAdvanced, setShowAdvanced] = useState<boolean>(false) // toggling slippage, deadline, etc. on and off
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>('')
// tx parameters
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
......@@ -114,8 +113,6 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
const addTransaction = useTransactionAdder()
async function onAdd() {
setAttemptingTxn(true)
const router = getRouterContract(chainId, library, account)
const amountsMin = {
......@@ -155,12 +152,15 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
value = null
}
setAttemptingTxn(true)
await estimate(...args, value ? { value } : {})
.then(estimatedGasLimit =>
method(...args, {
...(value ? { value } : {}),
gasLimit: calculateGasMargin(estimatedGasLimit)
}).then(response => {
setAttemptingTxn(false)
addTransaction(response, {
summary:
'Add ' +
......@@ -174,7 +174,6 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
})
setTxHash(response.hash)
setPendingConfirmation(false)
ReactGA.event({
category: 'Liquidity',
......@@ -183,12 +182,12 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
})
})
)
.catch((e: Error) => {
console.error(e)
setPendingConfirmation(true)
.catch(error => {
setAttemptingTxn(false)
setShowConfirm(false)
setShowAdvanced(false)
// we only care if the error is something _other_ than the user rejected the tx
if (error?.code !== 4001) {
console.error(error)
}
})
}
......@@ -311,17 +310,15 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
<ConfirmationModal
isOpen={showConfirm}
onDismiss={() => {
if (attemptingTxn) {
history.push('/pool')
return
}
setPendingConfirmation(true)
setAttemptingTxn(false)
setShowConfirm(false)
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.TOKEN_A, '')
}
setTxHash('')
}}
attemptingTxn={attemptingTxn}
pendingConfirmation={pendingConfirmation}
hash={txHash ? txHash : ''}
hash={txHash}
topContent={() => modalHeader()}
bottomContent={modalBottom}
pendingText={pendingText}
......
......@@ -34,8 +34,9 @@ import { useDerivedBurnInfo, useBurnState } from '../../state/burn/hooks'
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
import { Field } from '../../state/burn/actions'
import { useWalletModalToggle } from '../../state/application/hooks'
import { BigNumber } from '@ethersproject/bignumber'
export default function RemoveLiquidity({ match: { params }, history }: RouteComponentProps<{ tokens: string }>) {
export default function RemoveLiquidity({ match: { params } }: RouteComponentProps<{ tokens: string }>) {
useDefaultsFromURLMatchParams(params)
const { account, chainId, library } = useActiveWeb3React()
......@@ -51,14 +52,13 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
const isValid = !error
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showAdvanced, setShowAdvanced] = useState<boolean>(false)
const [showDetailed, setShowDetailed] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirm
const [pendingConfirmation, setPendingConfirmation] = useState(true) // waiting for
// txn values
const [showDetailed, setShowDetailed] = useState<boolean>(false) // toggling detailed view
const [showAdvanced, setShowAdvanced] = useState<boolean>(false) // toggling slippage, deadline, etc. on and off
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>('')
// tx parameters
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
......@@ -144,17 +144,9 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
})
}
function resetModalState() {
setSignatureData(null)
setAttemptingTxn(false)
setPendingConfirmation(true)
}
// tx sending
const addTransaction = useTransactionAdder()
async function onRemove() {
setAttemptingTxn(true)
const router = getRouterContract(chainId, library, account)
const amountsMin = {
......@@ -167,13 +159,12 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline
let estimate, method: Function, args: Array<string | string[] | number | boolean>
let methodNames: string[], args: Array<string | string[] | number | boolean>
// we have approval, use normal remove liquidity
if (approval === ApprovalState.APPROVED) {
// removeLiquidityETH
if (oneTokenIsETH) {
estimate = router.estimateGas.removeLiquidityETH
method = router.removeLiquidityETH
methodNames = ['removeLiquidityETH', 'removeLiquidityETHSupportingFeeOnTransferTokens']
args = [
tokens[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].address,
parsedAmounts[Field.LIQUIDITY].raw.toString(),
......@@ -185,8 +176,7 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
}
// removeLiquidity
else {
estimate = router.estimateGas.removeLiquidity
method = router.removeLiquidity
methodNames = ['removeLiquidity']
args = [
tokens[Field.TOKEN_A].address,
tokens[Field.TOKEN_B].address,
......@@ -202,8 +192,7 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
else if (signatureData !== null) {
// removeLiquidityETHWithPermit
if (oneTokenIsETH) {
estimate = router.estimateGas.removeLiquidityETHWithPermit
method = router.removeLiquidityETHWithPermit
methodNames = ['removeLiquidityETHWithPermit', 'removeLiquidityETHWithPermitSupportingFeeOnTransferTokens']
args = [
tokens[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].address,
parsedAmounts[Field.LIQUIDITY].raw.toString(),
......@@ -219,8 +208,7 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
}
// removeLiquidityETHWithPermit
else {
estimate = router.estimateGas.removeLiquidityWithPermit
method = router.removeLiquidityWithPermit
methodNames = ['removeLiquidityWithPermit']
args = [
tokens[Field.TOKEN_A].address,
tokens[Field.TOKEN_B].address,
......@@ -236,14 +224,37 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
]
}
} else {
console.error('Attempting to confirm without approval or a signature.')
console.error('Attempting to confirm without approval or a signature. Please contact support.')
}
await estimate(...args)
.then(estimatedGasLimit =>
method(...args, {
gasLimit: calculateGasMargin(estimatedGasLimit)
}).then(response => {
const safeGasEstimates = await Promise.all(
methodNames.map(methodName =>
router.estimateGas[methodName](...args)
.then(calculateGasMargin)
.catch(error => {
console.error(`estimateGas failed for ${methodName}`, error)
})
)
)
const indexOfSuccessfulEstimation = safeGasEstimates.findIndex(safeGasEstimate =>
BigNumber.isBigNumber(safeGasEstimate)
)
// all estimations failed...
if (indexOfSuccessfulEstimation === -1) {
console.error('This transaction would fail. Please contact support.')
} else {
const methodName = methodNames[indexOfSuccessfulEstimation]
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
setAttemptingTxn(true)
await router[methodName](...args, {
gasLimit: safeGasEstimate
})
.then(response => {
setAttemptingTxn(false)
addTransaction(response, {
summary:
'Remove ' +
......@@ -257,7 +268,6 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
})
setTxHash(response.hash)
setPendingConfirmation(false)
ReactGA.event({
category: 'Liquidity',
......@@ -265,14 +275,15 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
label: [tokens[Field.TOKEN_A]?.symbol, tokens[Field.TOKEN_B]?.symbol].join('/')
})
})
)
.catch(e => {
console.error(e)
resetModalState()
setShowConfirm(false)
setShowAdvanced(false)
.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)
}
})
}
}
function modalHeader() {
return (
......@@ -398,16 +409,15 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
<ConfirmationModal
isOpen={showConfirm}
onDismiss={() => {
if (attemptingTxn) {
history.push('/pool')
return
}
resetModalState()
setShowConfirm(false)
setShowAdvanced(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}
pendingConfirmation={pendingConfirmation}
hash={txHash ? txHash : ''}
topContent={modalHeader}
bottomContent={modalBottom}
......
......@@ -74,13 +74,12 @@ export default function Send({ location: { search } }: RouteComponentProps) {
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showAdvanced, setShowAdvanced] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirmed
const [pendingConfirmation, setPendingConfirmation] = useState<boolean>(true) // waiting for user confirmation
// txn values
const [showAdvanced, setShowAdvanced] = useState<boolean>(false) // toggling slippage, deadline, etc. on and off
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>('')
// tx parameters
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
......@@ -137,17 +136,6 @@ export default function Send({ location: { search } }: RouteComponentProps) {
const atMaxAmountInput: boolean =
!!maxAmountInput && !!parsedAmounts[Field.INPUT] ? maxAmountInput.equalTo(parsedAmounts[Field.INPUT]) : undefined
// reset modal state when closed
function resetModal() {
// clear input if txn submitted
if (!pendingConfirmation) {
onUserInput(Field.INPUT, '')
}
setPendingConfirmation(true)
setAttemptingTxn(false)
setShowAdvanced(false)
}
const swapCallback = useSwapCallback(bestTrade, allowedSlippage, deadline, recipient)
function onSwap() {
......@@ -156,9 +144,10 @@ export default function Send({ location: { search } }: RouteComponentProps) {
}
setAttemptingTxn(true)
swapCallback().then(hash => {
swapCallback()
.then(hash => {
setAttemptingTxn(false)
setTxHash(hash)
setPendingConfirmation(false)
ReactGA.event({
category: 'Send',
......@@ -166,6 +155,13 @@ export default function Send({ location: { search } }: RouteComponentProps) {
label: [bestTrade.inputAmount.token.symbol, bestTrade.outputAmount.token.symbol].join(';')
})
})
.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)
}
})
}
const sendCallback = useSendCallback(parsedAmounts?.[Field.INPUT], recipient)
......@@ -173,16 +169,19 @@ export default function Send({ location: { search } }: RouteComponentProps) {
async function onSend() {
setAttemptingTxn(true)
sendCallback()
.then(hash => {
setAttemptingTxn(false)
setTxHash(hash)
ReactGA.event({ category: 'Send', action: 'Send', label: tokens[Field.INPUT]?.symbol })
setPendingConfirmation(false)
})
.catch(() => {
resetModal()
setShowConfirm(false)
.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)
}
})
}
......@@ -306,11 +305,13 @@ export default function Send({ location: { search } }: RouteComponentProps) {
isOpen={showConfirm}
title={sendingWithSwap ? 'Confirm swap and send' : 'Confirm Send'}
onDismiss={() => {
resetModal()
setShowConfirm(false)
if (txHash) {
onUserInput(Field.INPUT, '')
}
setTxHash('')
}}
attemptingTxn={attemptingTxn}
pendingConfirmation={pendingConfirmation}
hash={txHash}
topContent={modalHeader}
bottomContent={modalBottom}
......
......@@ -55,13 +55,12 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showAdvanced, setShowAdvanced] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirmed
const [pendingConfirmation, setPendingConfirmation] = useState<boolean>(true) // waiting for user confirmation
// txn values
const [showAdvanced, setShowAdvanced] = useState<boolean>(false) // toggling slippage, deadline, etc. on and off
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>('')
// tx parameters
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
......@@ -114,17 +113,6 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(bestTrade, allowedSlippage)
// reset modal state when closed
function resetModal() {
// clear input if txn submitted
if (!pendingConfirmation) {
onUserInput(Field.INPUT, '')
}
setPendingConfirmation(true)
setAttemptingTxn(false)
setShowAdvanced(false)
}
// the callback to execute the swap
const swapCallback = useSwapCallback(bestTrade, allowedSlippage, deadline)
......@@ -136,9 +124,10 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
}
setAttemptingTxn(true)
swapCallback().then(hash => {
swapCallback()
.then(hash => {
setAttemptingTxn(false)
setTxHash(hash)
setPendingConfirmation(false)
ReactGA.event({
category: 'Swap',
......@@ -146,6 +135,13 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
label: [bestTrade.inputAmount.token.symbol, bestTrade.outputAmount.token.symbol].join('/')
})
})
.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)
}
})
}
// errors
......@@ -157,11 +153,11 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
function modalHeader() {
return (
<SwapModalHeader
independentField={independentField}
priceImpactSeverity={priceImpactSeverity}
tokens={tokens}
formattedAmounts={formattedAmounts}
slippageAdjustedAmounts={slippageAdjustedAmounts}
priceImpactSeverity={priceImpactSeverity}
independentField={independentField}
/>
)
}
......@@ -197,11 +193,14 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
isOpen={showConfirm}
title="Confirm Swap"
onDismiss={() => {
resetModal()
setShowConfirm(false)
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.INPUT, '')
}
setTxHash('')
}}
attemptingTxn={attemptingTxn}
pendingConfirmation={pendingConfirmation}
hash={txHash}
topContent={modalHeader}
bottomContent={modalBottom}
......
......@@ -4,7 +4,7 @@ import { AddressZero } from '@ethersproject/constants'
import { JsonRpcSigner, Web3Provider } from '@ethersproject/providers'
import { BigNumber } from '@ethersproject/bignumber'
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { abi as IUniswapV2Router01ABI } from '@uniswap/v2-periphery/build/IUniswapV2Router01.json'
import { abi as IUniswapV2Router02ABI } from '@uniswap/v2-periphery/build/IUniswapV2Router02.json'
import { ROUTER_ADDRESS } from '../constants'
import { ChainId, JSBI, Percent, TokenAmount } from '@uniswap/sdk'
......@@ -88,8 +88,8 @@ export function getContract(address: string, ABI: any, library: Web3Provider, ac
}
// account is optional
export function getRouterContract(chainId: number, library: Web3Provider, account?: string) {
return getContract(ROUTER_ADDRESS, IUniswapV2Router01ABI, library, account)
export function getRouterContract(_: number, library: Web3Provider, account?: string) {
return getContract(ROUTER_ADDRESS, IUniswapV2Router02ABI, library, account)
}
// account is optional
......
......@@ -2813,10 +2813,10 @@
resolved "https://registry.yarnpkg.com/@uniswap/v2-core/-/v2-core-1.0.0.tgz#e0fab91a7d53e8cafb5326ae4ca18351116b0844"
integrity sha512-BJiXrBGnN8mti7saW49MXwxDBRFiWemGetE58q8zgfnPPzQKq55ADltEILqOt6VFZ22kVeVKbF8gVd8aY3l7pA==
"@uniswap/v2-periphery@1.0.0-beta.0":
version "1.0.0-beta.0"
resolved "https://registry.yarnpkg.com/@uniswap/v2-periphery/-/v2-periphery-1.0.0-beta.0.tgz#53ccbd5f6a3e43fd37f04660625873dc7d5c57b2"
integrity sha512-r0Iuk7L3gZzbmlZeNhMdLmG0fOqfHCoxmWqYQ9OX4r3w0lJ6dyErJ8kVdTy8PtairkuHkXWeH3OdyzzzBtyRpw==
"@uniswap/v2-periphery@^1.1.0-beta.0":
version "1.1.0-beta.0"
resolved "https://registry.yarnpkg.com/@uniswap/v2-periphery/-/v2-periphery-1.1.0-beta.0.tgz#20a4ccfca22f1a45402303aedb5717b6918ebe6d"
integrity sha512-6dkwAMKza8nzqYiXEr2D86dgW3TTavUvCR0w2Tu33bAbM8Ah43LKAzH7oKKPRT5VJQaMi1jtkGs1E8JPor1n5g==
dependencies:
"@uniswap/lib" "1.1.1"
"@uniswap/v2-core" "1.0.0"
......
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