fix: reverted swap state (#7044)

* fix: wait for transaction status in ConfirmSwapModal

* fix: use actual swap status in ConfirmSwapModal

* feat: add test

* fix: shared hook

* fix: fix test

* fix: dont create Activity instance
This commit is contained in:
eddie 2023-07-28 12:48:12 -07:00 committed by GitHub
parent ace81ecc84
commit 02883aca13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 77 additions and 26 deletions

@ -1,12 +1,10 @@
import { BigNumber } from '@ethersproject/bignumber'
import { ChainId } from '@uniswap/sdk-core'
import { CurrencyAmount } from '@uniswap/sdk-core'
import { DEFAULT_DEADLINE_FROM_NOW } from '../../../src/constants/misc'
import { UNI, USDC_MAINNET } from '../../../src/constants/tokens'
import { DAI, USDC_MAINNET } from '../../../src/constants/tokens'
import { getBalance, getTestSelector } from '../../utils'
const UNI_MAINNET = UNI[ChainId.MAINNET]
describe('Swap errors', () => {
it('wallet rejection', () => {
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' })
@ -64,10 +62,19 @@ describe('Swap errors', () => {
})
})
it.skip('slippage failure', () => {
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${UNI_MAINNET.address}`, { ethereum: 'hardhat' })
cy.hardhat({ automine: false })
getBalance(USDC_MAINNET).then((initialBalance) => {
it('slippage failure', () => {
cy.visit(`/swap?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`, { ethereum: 'hardhat' })
cy.hardhat({ automine: false }).then(async (hardhat) => {
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDC_MAINNET, 500e6))
await hardhat.mine()
await Promise.all([
hardhat.approval.setTokenAllowanceForPermit2({ owner: hardhat.wallet, token: USDC_MAINNET }),
hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: USDC_MAINNET }),
])
await hardhat.mine()
})
getBalance(DAI).then((initialBalance) => {
// Gas estimation fails for this transaction (that would normally fail), so we stub it.
cy.hardhat().then((hardhat) => {
const send = cy.stub(hardhat.provider, 'send').log(false)
@ -90,18 +97,25 @@ describe('Swap errors', () => {
cy.contains('Confirm swap').click()
cy.wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
cy.contains('Swap submitted')
if (i === 0) {
cy.get(getTestSelector('confirmation-close-icon')).click()
}
}
cy.get(getTestSelector('web3-status-connected')).should('contain', '2 Pending')
// Mine transactions
cy.hardhat().then((hardhat) => hardhat.mine())
cy.wait('@eth_getTransactionReceipt')
// Verify transaction did not occur
cy.contains('Swap failed')
// Verify only 1 transaction occurred
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
cy.get(getTestSelector('popups')).contains('Swapped')
cy.get(getTestSelector('popups')).contains('Swap failed')
getBalance(UNI_MAINNET).should('eq', initialBalance)
getBalance(DAI).then((currentDaiBalance) => {
expect(currentDaiBalance).to.be.closeTo(initialBalance + 200, 1)
})
})
})
})

@ -138,17 +138,21 @@ function parseMigrateCreateV3(
return { descriptor, currencies: [baseCurrency, quoteCurrency] }
}
export function getTransactionStatus(details: TransactionDetails): TransactionStatus {
return !details.receipt
? TransactionStatus.Pending
: details.receipt.status === 1 || details.receipt?.status === undefined
? TransactionStatus.Confirmed
: TransactionStatus.Failed
}
export function transactionToActivity(
details: TransactionDetails,
chainId: ChainId,
tokens: ChainTokenMap
): Activity | undefined {
try {
const status = !details.receipt
? TransactionStatus.Pending
: details.receipt.status === 1 || details.receipt?.status === undefined
? TransactionStatus.Confirmed
: TransactionStatus.Failed
const status = getTransactionStatus(details)
const defaultFields = {
hash: details.hash,

@ -14,6 +14,7 @@ import Modal, { MODAL_TRANSITION_DURATION } from 'components/Modal'
import { RowFixed } from 'components/Row'
import { getChainInfo } from 'constants/chainInfo'
import { USDT as USDT_MAINNET } from 'constants/tokens'
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import { useMaxAmountIn } from 'hooks/useMaxAmountIn'
import { Allowance, AllowanceState } from 'hooks/usePermit2Allowance'
import usePrevious from 'hooks/usePrevious'
@ -24,7 +25,7 @@ import { getPriceUpdateBasisPoints } from 'lib/utils/analytics'
import { useCallback, useEffect, useState } from 'react'
import { InterfaceTrade, TradeFillType } from 'state/routing/types'
import { Field } from 'state/swap/actions'
import { useIsTransactionConfirmed } from 'state/transactions/hooks'
import { useIsTransactionConfirmed, useSwapTransactionStatus } from 'state/transactions/hooks'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import invariant from 'tiny-invariant'
@ -297,7 +298,13 @@ export default function ConfirmSwapModal({
doesTradeDiffer: Boolean(doesTradeDiffer),
})
const swapFailed = Boolean(swapError) && !didUserReject(swapError)
const swapStatus = useSwapTransactionStatus(swapResult)
// Swap was reverted onchain.
const swapReverted = swapStatus === TransactionStatus.Failed
// Swap failed locally and was not broadcast to the blockchain.
const localSwapFailure = Boolean(swapError) && !didUserReject(swapError)
const swapFailed = localSwapFailure || swapReverted
useEffect(() => {
// Reset the modal state if the user rejected the swap.
if (swapError && !swapFailed) {

@ -1,6 +1,7 @@
import { BigNumber } from '@ethersproject/bignumber'
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import { TradeFillType } from 'state/routing/types'
import { useIsTransactionConfirmed } from 'state/transactions/hooks'
import { useSwapTransactionStatus } from 'state/transactions/hooks'
import { TEST_TRADE_EXACT_INPUT } from 'test-utils/constants'
import { mocked } from 'test-utils/mocked'
import { render, screen } from 'test-utils/render'
@ -14,7 +15,7 @@ jest.mock('state/transactions/hooks')
describe('PendingModalContent', () => {
beforeEach(() => {
jest.clearAllMocks()
mocked(useIsTransactionConfirmed).mockReturnValue(false)
mocked(useSwapTransactionStatus).mockReturnValue(TransactionStatus.Pending)
})
it('renders null for invalid content', () => {
@ -128,7 +129,7 @@ describe('PendingModalContent', () => {
})
it('renders the success icon instead of the given logo when confirmed and successful', () => {
mocked(useIsTransactionConfirmed).mockReturnValue(true)
mocked(useSwapTransactionStatus).mockReturnValue(TransactionStatus.Confirmed)
render(
<PendingModalContent
@ -138,6 +139,20 @@ describe('PendingModalContent', () => {
ConfirmModalState.PENDING_CONFIRMATION,
]}
currentStep={ConfirmModalState.PENDING_CONFIRMATION}
swapResult={{
type: TradeFillType.Classic,
response: {
hash: '',
confirmations: 0,
from: '',
wait: jest.fn(),
nonce: 0,
gasLimit: BigNumber.from(0),
data: '',
value: BigNumber.from(0),
chainId: 0,
},
}}
/>
)
expect(screen.queryByTestId('pending-modal-failure-icon')).toBeNull()

@ -5,6 +5,7 @@ import { OrderContent } from 'components/AccountDrawer/MiniPortfolio/Activity/Of
import { ColumnCenter } from 'components/Column'
import Column from 'components/Column'
import Row from 'components/Row'
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import { SwapResult } from 'hooks/useSwapCallback'
import { useUnmountingAnimation } from 'hooks/useUnmountingAnimation'
import { UniswapXOrderStatus } from 'lib/hooks/orders/types'
@ -12,7 +13,7 @@ import { ReactNode, useRef } from 'react'
import { InterfaceTrade, TradeFillType } from 'state/routing/types'
import { useOrder } from 'state/signatures/hooks'
import { UniswapXOrderDetails } from 'state/signatures/types'
import { useIsTransactionConfirmed } from 'state/transactions/hooks'
import { useIsTransactionConfirmed, useSwapTransactionStatus } from 'state/transactions/hooks'
import styled, { css, keyframes } from 'styled-components/macro'
import { ExternalLink } from 'theme'
import { ThemedText } from 'theme/components/text'
@ -223,14 +224,14 @@ export function PendingModalContent({
}: PendingModalContentProps) {
const { chainId } = useWeb3React()
const classicSwapConfirmed = useIsTransactionConfirmed(
swapResult?.type === TradeFillType.Classic ? swapResult.response.hash : undefined
)
const swapStatus = useSwapTransactionStatus(swapResult)
const classicSwapConfirmed = swapStatus === TransactionStatus.Confirmed
const wrapConfirmed = useIsTransactionConfirmed(wrapTxHash)
// TODO(UniswapX): Support UniswapX status here too
const uniswapXSwapConfirmed = Boolean(swapResult)
const swapConfirmed = TradeFillType.Classic ? classicSwapConfirmed : uniswapXSwapConfirmed
const swapConfirmed = swapResult?.type === TradeFillType.Classic ? classicSwapConfirmed : uniswapXSwapConfirmed
const swapPending = swapResult !== undefined && !swapConfirmed
const wrapPending = wrapTxHash != undefined && !wrapConfirmed

@ -2,8 +2,12 @@ import { BigNumber } from '@ethersproject/bignumber'
import type { TransactionResponse } from '@ethersproject/providers'
import { ChainId, SUPPORTED_CHAINS, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { getTransactionStatus } from 'components/AccountDrawer/MiniPortfolio/Activity/parseLocal'
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import { SwapResult } from 'hooks/useSwapCallback'
import { useCallback, useMemo } from 'react'
import { useAppDispatch, useAppSelector } from 'state/hooks'
import { TradeFillType } from 'state/routing/types'
import { addTransaction, removeTransaction } from './reducer'
import { TransactionDetails, TransactionInfo, TransactionType } from './types'
@ -89,6 +93,12 @@ export function useIsTransactionConfirmed(transactionHash?: string): boolean {
return Boolean(transactions[transactionHash].receipt)
}
export function useSwapTransactionStatus(swapResult: SwapResult | undefined): TransactionStatus | undefined {
const transaction = useTransaction(swapResult?.type === TradeFillType.Classic ? swapResult.response.hash : undefined)
if (!transaction) return undefined
return getTransactionStatus(transaction)
}
/**
* Returns whether a transaction happened in the last day (86400 seconds * 1000 milliseconds / second)
* @param tx to check for recency