Commit 7934777f authored by eddie's avatar eddie Committed by GitHub

feat: permit2 flow updates (#6538)

* test: swap flow cypress tests

* fix: use default parameter

* feat: use Swap Component on TDP

* feat: auto nav for TDP tokens

* chore: merge

* chore: merge

* chore: merge

* chore: merge

* fix: remove extra inputCurrency URL parsing logic

* fix: undo last change

* fix: pass expected chain id to swap component

* fix: search for default tokens on unconnected networks if needed

* test: e2e test for l2 token

* fix: delete irrelevant tests

* fix: address comments

* fix: lint error

* test: update TDP e2e tests

* fix: use pageChainId for filter

* fix: rename chainId

* fix: typecheck

* fix: chainId bug

* fix: chainId required fixes

* fix: bad merge in e2e test

* fix: remove unused test util

* fix: remove unnecessary variable

* fix: token defaults

* fix: address comments

* fix: address comments and fix tests

* fix: e2e test formatting, remove Maybe<>

* fix: remove unused variable

* fix: use feature flag for swap component on TDP

* fix: back button

* feat: copy review screen UI from widgetg

* fix: modal padding

* feat: add final detail row

* fix: remove widget comment

* fix: update unit tests

* fix: code style consistency

* fix: remove padding from AutoColumn

* fix: update snapshots

* fix: use semantic gaps

* fix: more px and gaps

* fix: design feedbacks

* fix: button radius in summary modal

* fix: design nits

* feat: update design of summary modal

* fix: font weight and vertical spacing

* fix: update snapshots

* fix: css nits

* fix: modal flicker when refetching trade

* wip: move approval to summary modal

* wip: not working

* feat: working

* fix: fix flow

* feat: simplify states and build new modal UI

* feat: todos and differs fix

* feat: update tx status modal

* feat: split up approve and permit

* feat: error state

* feat: update success and error states

* feat: undo changes to TxConfirmationModal

* feat: remove step indicators when only one step

* feat: move content into PendingModalContent component

* fix: lint

* fix: correct modal state when moving between steps

* fix: comments

* fix: code style improvements

* feat: require trade to be defined

* fix: remove extra props from ThemedTexts

* fix: one more trans

* fix: remove unused export

* feat: remove undefined checks and other fixes

* fix: update test

* fix: add missing dollar sign

* fix: remove null check and update test

* fix: remove max width from detail row value

* fix: remove isOpen prop

* fix: isopen

* feat: refactor approval flow into a hook

* fix: tradeMeaningfullyDiffers improvement and prepareFlow fix

* fix: address  comments

* feat: add comments explaining async state

* fix: nits

* fix: address comments

* feat: permit2 e2e tests (#6541)

* wip: move approval to summary modal

* wip: not working

* feat: working

* fix: fix flow

* feat: simplify states and build new modal UI

* feat: todos and differs fix

* feat: update tx status modal

* feat: split up approve and permit

* feat: error state

* feat: update success and error states

* feat: undo changes to TxConfirmationModal

* feat: re-order functions

* wip: move approval to summary modal

* wip: not working

* feat: update permit2 e2e tests

* feat: tests passing

* fix: swap test

* fix: bad merge

* chore: merge

* fix: update tests for new modal

* fix: testid fix

* fix: test updates

* fix: reduce nesting

* test: remove line from test for debugging

* fix: update tests

* fix: more nesting in test

* fix: update test

* fix: reorganize test code
parent 2415a1e3
......@@ -104,7 +104,7 @@ describe('mini-portfolio activity history', () => {
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.get(getTestSelector('dismiss-tx-confirmation')).click()
cy.get(getTestSelector('confirmation-close-icon')).click()
// Check activity history tab.
cy.get(getTestSelector('web3-status-connected')).click()
......
This diff is collapsed.
......@@ -19,9 +19,9 @@ describe('Swap errors', () => {
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.contains('Transaction rejected').should('exist')
cy.contains('Dismiss').click()
cy.contains('Transaction rejected').should('not.exist')
cy.contains('Confirmation failed').should('exist')
cy.get('body').click('topRight')
cy.contains('Confirmation failed').should('not.exist')
})
})
......@@ -43,7 +43,7 @@ describe('Swap errors', () => {
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.get(getTestSelector('dismiss-tx-confirmation')).click()
cy.get(getTestSelector('confirmation-close-icon')).click()
// The pending transaction indicator should reflect the state.
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
......@@ -88,7 +88,8 @@ describe('Swap errors', () => {
cy.get('#swap-currency-output .token-amount-input').should('not.have.value', '')
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.get(getTestSelector('dismiss-tx-confirmation')).click()
cy.contains('Confirm Swap').should('exist')
cy.get(getTestSelector('confirmation-close-icon')).click()
cy.get('#swap-currency-input .token-amount-input')
.clear()
......@@ -97,7 +98,8 @@ describe('Swap errors', () => {
cy.get('#swap-currency-output .token-amount-input').should('not.have.value', '')
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.get(getTestSelector('dismiss-tx-confirmation')).click()
cy.contains('Confirm Swap').should('exist')
cy.get(getTestSelector('confirmation-close-icon')).click()
// The pending transaction indicator should reflect the state.
cy.get(getTestSelector('web3-status-connected')).should('contain', '2 Pending')
......
......@@ -48,7 +48,7 @@ describe('Swap', () => {
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.get(getTestSelector('dismiss-tx-confirmation')).click()
cy.get(getTestSelector('confirmation-close-icon')).click()
// The pending transaction indicator should reflect the state.
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
......
......@@ -7,6 +7,8 @@ import { Z_INDEX } from 'theme/zIndex'
import { isMobile } from '../../utils/userAgent'
export const MODAL_TRANSITION_DURATION = 200
const AnimatedDialogOverlay = animated(DialogOverlay)
const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ $scrollOverlay?: boolean }>`
......@@ -103,7 +105,7 @@ export default function Modal({
hideBorder = false,
}: ModalProps) {
const fadeTransition = useTransition(isOpen, {
config: { duration: 200 },
config: { duration: MODAL_TRANSITION_DURATION },
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
......
import styled, { keyframes, useTheme } from 'styled-components/macro'
const Wrapper = styled.div`
const Wrapper = styled.div<{ size?: string }>`
height: 90px;
width: 90px;
`
......@@ -38,11 +38,11 @@ const PolyLine = styled.polyline`
animation: ${dashCheck} 0.9s 0.35s ease-in-out forwards;
`
export default function AnimatedConfirmation() {
export default function AnimatedConfirmation({ className }: { className?: string }) {
const theme = useTheme()
return (
<Wrapper className="w4rAnimated_checkmark">
<Wrapper className={className}>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 130.2 130.2">
<Circle
className="path circle"
......
......@@ -6,16 +6,15 @@ import { getChainInfo } from 'constants/chainInfo'
import { SupportedChainId, SupportedL2ChainId } from 'constants/chains'
import useCurrencyLogoURIs from 'lib/hooks/useCurrencyLogoURIs'
import { ReactNode, useCallback, useState } from 'react'
import { AlertCircle, AlertTriangle, ArrowUpCircle, CheckCircle } from 'react-feather'
import { Text } from 'rebass'
import { AlertCircle, ArrowUpCircle, CheckCircle } from 'react-feather'
import { useIsTransactionConfirmed, useTransaction } from 'state/transactions/hooks'
import styled, { useTheme } from 'styled-components/macro'
import { isL2ChainId } from 'utils/chains'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
import Circle from '../../assets/images/blue-loader.svg'
import { ExternalLink, ThemedText } from '../../theme'
import { CloseIcon, CustomLightSpinner } from '../../theme'
import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink'
import { TransactionSummary } from '../AccountDetails/TransactionSummary'
import { ButtonLight, ButtonPrimary } from '../Button'
import { AutoColumn, ColumnCenter } from '../Column'
......@@ -187,7 +186,7 @@ export function ConfirmationModalContent({
<Row justify="center" marginLeft="24px">
<ThemedText.SubHeader>{title}</ThemedText.SubHeader>
</Row>
<CloseIcon onClick={onDismiss} data-cy="confirmation-close-icon" />
<CloseIcon onClick={onDismiss} data-testid="confirmation-close-icon" />
</Row>
{topContent()}
</AutoColumn>
......@@ -196,31 +195,6 @@ export function ConfirmationModalContent({
)
}
export function TransactionErrorContent({ message, onDismiss }: { message: ReactNode; onDismiss: () => void }) {
const theme = useTheme()
return (
<Wrapper>
<AutoColumn>
<RowBetween>
<Text fontWeight={600} fontSize={16}>
<Trans>Error</Trans>
</Text>
<CloseIcon onClick={onDismiss} />
</RowBetween>
<AutoColumn style={{ marginTop: 20, padding: '2rem 0' }} gap="24px" justify="center">
<AlertTriangle color={theme.accentCritical} style={{ strokeWidth: 1 }} size={90} />
<ThemedText.MediumHeader textAlign="center">{message}</ThemedText.MediumHeader>
</AutoColumn>
</AutoColumn>
<BottomSection gap="12px">
<ButtonPrimary onClick={onDismiss}>
<Trans>Dismiss</Trans>
</ButtonPrimary>
</BottomSection>
</Wrapper>
)
}
function L2Content({
onDismiss,
chainId,
......@@ -325,7 +299,7 @@ interface ConfirmationModalProps {
isOpen: boolean
onDismiss: () => void
hash?: string
content: () => ReactNode
reviewContent: () => ReactNode
attemptingTxn: boolean
pendingText: ReactNode
currencyToAdd?: Currency
......@@ -337,7 +311,7 @@ export default function TransactionConfirmationModal({
attemptingTxn,
hash,
pendingText,
content,
reviewContent,
currencyToAdd,
}: ConfirmationModalProps) {
const { chainId } = useWeb3React()
......@@ -359,7 +333,7 @@ export default function TransactionConfirmationModal({
currencyToAdd={currencyToAdd}
/>
) : (
content()
reviewContent()
)}
</Modal>
)
......
This diff is collapsed.
import { t, Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core'
import { ButtonPrimary } from 'components/Button'
import { ColumnCenter } from 'components/Column'
import Loader from 'components/Icons/LoadingSpinner'
import CurrencyLogo from 'components/Logo/CurrencyLogo'
import QuestionHelper from 'components/QuestionHelper'
import Row from 'components/Row'
import AnimatedConfirmation from 'components/TransactionConfirmationModal/AnimatedConfirmation'
import { ReactNode } from 'react'
import { AlertTriangle } from 'react-feather'
import { useIsTransactionConfirmed } from 'state/transactions/hooks'
import styled, { DefaultTheme, useTheme } from 'styled-components/macro'
import { ThemedText } from 'theme/components/text'
import { ConfirmModalState } from './ConfirmSwapModal'
const Container = styled(ColumnCenter)`
margin: 48px 0 28px;
`
const LogoContainer = styled.div`
position: relative;
display: flex;
border-radius: 50%;
overflow: visible;
`
const LogoLayer = styled.div`
z-index: 2;
`
const StepCircle = styled.div<{ active: boolean }>`
height: 10px;
width: 10px;
border-radius: 50%;
background-color: ${({ theme, active }) => (active ? theme.accentAction : theme.textTertiary)};
outline: 3px solid ${({ theme, active }) => (active ? theme.accentActionSoft : theme.accentTextLightTertiary)};
`
const SizedAnimatedConfirmation = styled(AnimatedConfirmation)`
height: 48px;
width: 48px;
`
// TODO: switch to LoaderV2 with updated API to support changing color and size.
const LoadingIndicator = styled(Loader)`
width: calc(100% + 8px);
height: calc(100% + 8px);
top: -4px;
left: -4px;
position: absolute;
`
// This component is used for all steps after ConfirmModalState.REVIEWING
export type PendingConfirmModalState = Extract<
ConfirmModalState,
ConfirmModalState.APPROVING_TOKEN | ConfirmModalState.PERMITTING | ConfirmModalState.PENDING_CONFIRMATION
>
interface PendingModalStep {
title: ReactNode
subtitle?: ReactNode
label?: ReactNode
tooltipText?: ReactNode
logo?: ReactNode
button?: ReactNode
}
interface PendingModalContentProps {
steps: PendingConfirmModalState[]
currentStep: PendingConfirmModalState
approvalCurrency?: Currency
hideStepIndicators?: boolean
txHash?: string
}
function CurrencyLoader({ currency }: { currency?: Currency }) {
const theme = useTheme()
return (
<LogoContainer>
<LogoLayer>
<CurrencyLogo currency={currency} size="48px" />
</LogoLayer>
<LoadingIndicator stroke={theme.textTertiary} />
</LogoContainer>
)
}
function getContent(
step: PendingConfirmModalState,
approvalCurrency: Currency | undefined,
confirmed: boolean,
theme: DefaultTheme
): PendingModalStep {
switch (step) {
case ConfirmModalState.APPROVING_TOKEN:
return {
title: t`Approve permit`,
subtitle: t`Proceed in wallet`,
label: t`Why are permits required?`,
tooltipText: t`Permit2 allows token approvals to be shared and managed across different applications.`,
logo: <CurrencyLoader currency={approvalCurrency} />,
}
case ConfirmModalState.PERMITTING:
return {
title: t`Approve ${approvalCurrency?.symbol ?? 'token'}`,
subtitle: t`Proceed in wallet`,
label: t`Why are approvals required?`,
tooltipText: t`This provides the Uniswap protocol access to your token for trading. For security, this will expire after 30 days.`,
logo: <CurrencyLoader currency={approvalCurrency} />,
}
case ConfirmModalState.PENDING_CONFIRMATION:
return {
title: t`Confirm Swap`,
subtitle: t`Proceed in wallet`,
logo: confirmed ? <SizedAnimatedConfirmation /> : <Loader stroke={theme.textTertiary} size="48px" />,
}
}
}
export function PendingModalContent({
steps,
currentStep,
approvalCurrency,
txHash,
hideStepIndicators,
}: PendingModalContentProps) {
const theme = useTheme()
const confirmed = useIsTransactionConfirmed(txHash)
const { logo, title, subtitle, label, tooltipText, button } = getContent(
currentStep,
approvalCurrency,
confirmed,
theme
)
return (
<Container gap="lg">
{logo}
{/* TODO: implement animations between title/subtitles of each step. */}
<ColumnCenter gap="md">
<ThemedText.HeadlineSmall data-testid="PendingModalContent-title">{title}</ThemedText.HeadlineSmall>
{subtitle && (
<ThemedText.LabelSmall data-testid="PendingModalContent-subtitle">{subtitle}</ThemedText.LabelSmall>
)}
<Row justify="center">
{label && (
<ThemedText.Caption color="textSecondary" data-testid="PendingModalContent-label">
{label}
</ThemedText.Caption>
)}
{tooltipText && <QuestionHelper text={tooltipText} />}
</Row>
</ColumnCenter>
{button && (
<Row justify="center" data-testid="PendingModalContent-button">
{button}
</Row>
)}
{!hideStepIndicators && (
<Row gap="14px" justify="center">
{steps.map((_, i) => {
return <StepCircle key={i} active={steps.indexOf(currentStep) === i} />
})}
</Row>
)}
</Container>
)
}
export enum PendingModalError {
TOKEN_APPROVAL_ERROR,
PERMIT_ERROR,
CONFIRMATION_ERROR,
}
interface ErrorModalContentProps {
errorType: PendingModalError
onRetry: () => void
}
function getErrorContent(errorType: PendingModalError) {
switch (errorType) {
case PendingModalError.TOKEN_APPROVAL_ERROR:
return {
title: t`Token approval failed`,
label: t`Why are approvals required?`,
tooltipText: t`This provides the Uniswap protocol access to your token for trading. For security, this will expire after 30 days.`,
}
case PendingModalError.PERMIT_ERROR:
return {
title: t`Permit approval failed`,
label: t`Why are permits required?`,
tooltipText: t`Permit2 allows token approvals to be shared and managed across different applications.`,
}
case PendingModalError.CONFIRMATION_ERROR:
return {
title: t`Confirmation failed`,
}
}
}
export function ErrorModalContent({ errorType, onRetry }: ErrorModalContentProps) {
const theme = useTheme()
const { title, label, tooltipText } = getErrorContent(errorType)
return (
<Container gap="lg">
<AlertTriangle strokeWidth={1} color={theme.accentFailure} size="48px" />
<ColumnCenter gap="md">
<ThemedText.HeadlineSmall>{title}</ThemedText.HeadlineSmall>
<Row justify="center">
{label && <ThemedText.Caption color="textSecondary">{label}</ThemedText.Caption>}
{tooltipText && <QuestionHelper text={tooltipText} />}
</Row>
</ColumnCenter>
<Row justify="center">
<ButtonPrimary marginX="24px" onClick={onRetry} data-testid="pending-modal-content-retry">
<Trans>Retry</Trans>
</ButtonPrimary>
</Row>
</Container>
)
}
import { CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'
import { useMemo } from 'react'
import { InterfaceTrade } from 'state/routing/types'
export function useMaxAmountIn(trade: InterfaceTrade | undefined, allowedSlippage: Percent) {
return useMemo(() => {
const maximumAmountIn = trade?.maximumAmountIn(allowedSlippage)
return maximumAmountIn?.currency.isToken ? (maximumAmountIn as CurrencyAmount<Token>) : undefined
}, [allowedSlippage, trade])
}
......@@ -25,6 +25,10 @@ interface AllowanceRequired {
token: Token
isApprovalLoading: boolean
approveAndPermit: () => Promise<void>
approve: () => Promise<void>
permit: () => Promise<void>
needsPermit2Approval: boolean
needsSignature: boolean
}
export type Allowance =
......@@ -101,24 +105,64 @@ export default function usePermit2Allowance(amount?: CurrencyAmount<Token>, spen
}
}, [addTransaction, shouldRequestApproval, shouldRequestSignature, updatePermitAllowance, updateTokenAllowance])
const approve = useCallback(async () => {
if (shouldRequestApproval) {
const { response, info } = await updateTokenAllowance()
addTransaction(response, info)
}
}, [addTransaction, shouldRequestApproval, updateTokenAllowance])
const permit = useCallback(async () => {
if (shouldRequestSignature) {
await updatePermitAllowance()
}
}, [shouldRequestSignature, updatePermitAllowance])
return useMemo(() => {
if (token) {
if (!tokenAllowance || !permitAllowance) {
return { state: AllowanceState.LOADING }
} else if (!(isPermitted || isSigned)) {
return { token, state: AllowanceState.REQUIRED, isApprovalLoading: false, approveAndPermit }
} else if (shouldRequestSignature) {
return {
token,
state: AllowanceState.REQUIRED,
isApprovalLoading: false,
approveAndPermit,
approve,
permit,
needsPermit2Approval: !isApproved,
needsSignature: shouldRequestSignature,
}
} else if (!isApproved) {
return { token, state: AllowanceState.REQUIRED, isApprovalLoading, approveAndPermit }
return {
token,
state: AllowanceState.REQUIRED,
isApprovalLoading,
approveAndPermit,
approve,
permit,
needsPermit2Approval: true,
needsSignature: shouldRequestSignature,
}
}
}
return { token, state: AllowanceState.ALLOWED, permitSignature: !isPermitted && isSigned ? signature : undefined }
return {
token,
state: AllowanceState.ALLOWED,
permitSignature: !isPermitted && isSigned ? signature : undefined,
needsPermit2Approval: false,
needsSignature: false,
}
}, [
approve,
approveAndPermit,
isApprovalLoading,
isApproved,
isPermitted,
isSigned,
permit,
permitAllowance,
shouldRequestSignature,
signature,
token,
tokenAllowance,
......
......@@ -8,6 +8,7 @@ import { useContract } from 'hooks/useContract'
import { useSingleCallResult } from 'lib/hooks/multicall'
import ms from 'ms.macro'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { didUserReject } from 'utils/swapErrorToUserReadableMessage'
const PERMIT_EXPIRATION = ms`30d`
const PERMIT_SIG_EXPIRATION = ms`30m`
......@@ -55,7 +56,6 @@ export function useUpdatePermitAllowance(
onPermitSignature: (signature: PermitSignature) => void
) {
const { account, chainId, provider } = useWeb3React()
return useCallback(async () => {
try {
if (!chainId) throw new Error('missing chainId')
......@@ -81,8 +81,10 @@ export function useUpdatePermitAllowance(
onPermitSignature?.({ ...permit, signature })
return
} catch (e: unknown) {
const message = didUserReject(e) ? 'User rejected signature' : e instanceof Error ? e.message : e
const symbol = token?.symbol ?? 'Token'
throw new Error(`${symbol} permit allowance failed: ${e instanceof Error ? e.message : e}`)
const error = new Error(`${symbol} permit allowance failed: ${message}`)
throw error
}
}, [account, chainId, nonce, onPermitSignature, provider, spender, token])
}
......@@ -5,6 +5,7 @@ import { useTokenContract } from 'hooks/useContract'
import { useSingleCallResult } from 'lib/hooks/multicall'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { ApproveTransactionInfo, TransactionType } from 'state/transactions/types'
import { didUserReject } from 'utils/swapErrorToUserReadableMessage'
export function useTokenAllowance(
token?: Token,
......@@ -58,8 +59,9 @@ export function useUpdateTokenAllowance(
},
}
} catch (e: unknown) {
const message = didUserReject(e) ? 'User rejected' : e instanceof Error ? e.message : e
const symbol = amount?.currency.symbol ?? 'Token'
throw new Error(`${symbol} token allowance failed: ${e instanceof Error ? e.message : e}`)
throw new Error(`${symbol} token allowance failed: ${message}`)
}
}, [amount, contract, spender])
}
......@@ -576,7 +576,7 @@ function AddLiquidity() {
onDismiss={handleDismissConfirmation}
attemptingTxn={attemptingTxn}
hash={txHash}
content={() => (
reviewContent={() => (
<ConfirmationModalContent
title={<Trans>Add Liquidity</Trans>}
onDismiss={handleDismissConfirmation}
......
......@@ -330,7 +330,7 @@ export default function AddLiquidity() {
onDismiss={handleDismissConfirmation}
attemptingTxn={attemptingTxn}
hash={txHash}
content={() => (
reviewContent={() => (
<ConfirmationModalContent
title={noLiquidity ? <Trans>You are creating a pool</Trans> : <Trans>You will receive</Trans>}
onDismiss={handleDismissConfirmation}
......
......@@ -641,7 +641,7 @@ function PositionPageContent() {
onDismiss={() => setShowConfirm(false)}
attemptingTxn={collecting}
hash={collectMigrationHash ?? ''}
content={() => (
reviewContent={() => (
<ConfirmationModalContent
title={<Trans>Claim fees</Trans>}
onDismiss={() => setShowConfirm(false)}
......
......@@ -283,7 +283,7 @@ function Remove({ tokenId }: { tokenId: BigNumber }) {
onDismiss={handleDismissConfirmation}
attemptingTxn={attemptingTxn}
hash={txnHash ?? ''}
content={() => (
reviewContent={() => (
<ConfirmationModalContent
title={<Trans>Remove Liquidity</Trans>}
onDismiss={handleDismissConfirmation}
......
......@@ -449,7 +449,7 @@ function RemoveLiquidity() {
onDismiss={handleDismissConfirmation}
attemptingTxn={attemptingTxn}
hash={txHash ? txHash : ''}
content={() => (
reviewContent={() => (
<ConfirmationModalContent
title={<Trans>You will receive</Trans>}
onDismiss={handleDismissConfirmation}
......
......@@ -13,15 +13,14 @@ import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'
import { useWeb3React } from '@web3-react/core'
import { useToggleAccountDrawer } from 'components/AccountDrawer'
import { sendEvent } from 'components/analytics'
import Loader from 'components/Icons/LoadingSpinner'
import { NetworkAlert } from 'components/NetworkAlert/NetworkAlert'
import PriceImpactWarning from 'components/swap/PriceImpactWarning'
import SwapDetailsDropdown from 'components/swap/SwapDetailsDropdown'
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
import { MouseoverTooltip } from 'components/Tooltip'
import { getChainInfo } from 'constants/chainInfo'
import { isSupportedChain, SupportedChainId } from 'constants/chains'
import useENSAddress from 'hooks/useENSAddress'
import { useMaxAmountIn } from 'hooks/useMaxAmountIn'
import usePermit2Allowance, { AllowanceState } from 'hooks/usePermit2Allowance'
import usePrevious from 'hooks/usePrevious'
import { useSwapCallback } from 'hooks/useSwapCallback'
......@@ -30,13 +29,12 @@ import JSBI from 'jsbi'
import { formatSwapQuoteReceivedEventProperties } from 'lib/utils/analytics'
import { useCallback, useEffect, useMemo, useReducer, useState } from 'react'
import { ReactNode } from 'react'
import { ArrowDown, Info } from 'react-feather'
import { ArrowDown } from 'react-feather'
import { useNavigate } from 'react-router-dom'
import { Text } from 'rebass'
import { InterfaceTrade } from 'state/routing/types'
import { TradeState } from 'state/routing/types'
import styled, { useTheme } from 'styled-components/macro'
import invariant from 'tiny-invariant'
import { currencyAmountToPreciseFloat, formatTransactionAmount } from 'utils/formatNumbers'
import { didUserReject } from 'utils/swapErrorToUserReadableMessage'
import { switchChain } from 'utils/switchChain'
......@@ -335,7 +333,7 @@ export function Swap({
}, [navigate])
// modal and loading
const [{ showConfirm, tradeToConfirm, swapErrorMessage, attemptingTxn, txHash }, setSwapState] = useState<{
const [{ showConfirm, tradeToConfirm, swapErrorMessage, txHash }, setSwapState] = useState<{
showConfirm: boolean
tradeToConfirm?: InterfaceTrade
attemptingTxn: boolean
......@@ -363,10 +361,7 @@ export function Swap({
currencies[Field.INPUT] && currencies[Field.OUTPUT] && parsedAmounts[independentField]?.greaterThan(JSBI.BigInt(0))
)
const maximumAmountIn = useMemo(() => {
const maximumAmountIn = trade?.maximumAmountIn(allowedSlippage)
return maximumAmountIn?.currency.isToken ? (maximumAmountIn as CurrencyAmount<Token>) : undefined
}, [allowedSlippage, trade])
const maximumAmountIn = useMaxAmountIn(trade, allowedSlippage)
const allowance = usePermit2Allowance(
maximumAmountIn ??
(parsedAmounts[Field.INPUT]?.currency.isToken
......@@ -374,25 +369,6 @@ export function Swap({
: undefined),
isSupportedChain(chainId) ? UNIVERSAL_ROUTER_ADDRESS(chainId) : undefined
)
const isApprovalLoading = allowance.state === AllowanceState.REQUIRED && allowance.isApprovalLoading
const [isAllowancePending, setIsAllowancePending] = useState(false)
const updateAllowance = useCallback(async () => {
invariant(allowance.state === AllowanceState.REQUIRED)
setIsAllowancePending(true)
try {
await allowance.approveAndPermit()
sendAnalyticsEvent(InterfaceEventName.APPROVE_TOKEN_TXN_SUBMITTED, {
chain_id: chainId,
token_symbol: maximumAmountIn?.currency.symbol,
token_address: maximumAmountIn?.currency.address,
...trace,
})
} catch (e) {
console.error(e)
} finally {
setIsAllowancePending(false)
}
}, [allowance, chainId, maximumAmountIn?.currency.address, maximumAmountIn?.currency.symbol, trace])
const maxInputAmount: CurrencyAmount<Currency> | undefined = useMemo(
() => maxAmountSpend(currencyBalances[Field.INPUT]),
......@@ -558,10 +534,10 @@ export function Swap({
trade={trade}
originalTrade={tradeToConfirm}
onAcceptChanges={handleAcceptChanges}
attemptingTxn={attemptingTxn}
txHash={txHash}
allowedSlippage={allowedSlippage}
onConfirm={handleSwap}
allowance={allowance}
swapErrorMessage={swapErrorMessage}
onDismiss={handleConfirmDismiss}
swapQuoteReceivedDate={swapQuoteReceivedDate}
......@@ -714,41 +690,6 @@ export function Swap({
<Trans>Insufficient liquidity for this trade.</Trans>
</ThemedText.DeprecatedMain>
</GrayCard>
) : isValid && allowance.state === AllowanceState.REQUIRED ? (
<ButtonPrimary
onClick={updateAllowance}
disabled={isAllowancePending || isApprovalLoading}
style={{ gap: 14 }}
data-testid="swap-approve-button"
>
{isAllowancePending ? (
<>
<Loader size="20px" />
<Trans>Approve in your wallet</Trans>
</>
) : isApprovalLoading ? (
<>
<Loader size="20px" />
<Trans>Approval pending</Trans>
</>
) : (
<>
<div style={{ height: 20 }}>
<MouseoverTooltip
text={
<Trans>
Permission is required for Uniswap to swap each token. This will expire after one month for
your security.
</Trans>
}
>
<Info size={20} />
</MouseoverTooltip>
</div>
<Trans>Approve use of {currencies[Field.INPUT]?.symbol}</Trans>
</>
)}
</ButtonPrimary>
) : (
<ButtonError
onClick={() => {
......@@ -765,13 +706,8 @@ export function Swap({
}
}}
id="swap-button"
disabled={
!isValid ||
routeIsSyncing ||
routeIsLoading ||
priceImpactTooHigh ||
allowance.state !== AllowanceState.ALLOWED
}
data-testid="swap-button"
disabled={!isValid || routeIsSyncing || routeIsLoading || priceImpactTooHigh}
error={isValid && priceImpactSeverity > 2 && allowance.state === AllowanceState.ALLOWED}
>
<Text fontSize={20} fontWeight={600}>
......
import { Trade } from '@uniswap/router-sdk'
import { Currency, TradeType } from '@uniswap/sdk-core'
import { Percent } from '@uniswap/sdk-core'
import { InterfaceTrade } from 'state/routing/types'
/**
* Returns true if the trade requires a confirmation of details before we can submit it
* @param args either a pair of V2 trades or a pair of V3 trades
*/
export function tradeMeaningfullyDiffers(
...args: [Trade<Currency, Currency, TradeType>, Trade<Currency, Currency, TradeType>]
): boolean {
const [tradeA, tradeB] = args
export function tradeMeaningfullyDiffers(tradeA: InterfaceTrade, tradeB: InterfaceTrade, slippage: Percent): boolean {
return (
tradeA.tradeType !== tradeB.tradeType ||
!tradeA.inputAmount.currency.equals(tradeB.inputAmount.currency) ||
!tradeA.inputAmount.equalTo(tradeB.inputAmount) ||
!tradeA.outputAmount.currency.equals(tradeB.outputAmount.currency) ||
!tradeA.outputAmount.equalTo(tradeB.outputAmount)
tradeB.executionPrice.lessThan(tradeA.worstExecutionPrice(slippage))
)
}
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