From 02883aca13bb9978b30e62dbdf21c30d12144e0e Mon Sep 17 00:00:00 2001 From: eddie <66155195+just-toby@users.noreply.github.com> Date: Fri, 28 Jul 2023 12:48:12 -0700 Subject: [PATCH] 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 --- cypress/e2e/swap/errors.test.ts | 36 +++++++++++++------ .../MiniPortfolio/Activity/parseLocal.ts | 14 +++++--- src/components/swap/ConfirmSwapModal.tsx | 11 ++++-- .../PendingModalContent.test.tsx | 21 +++++++++-- .../swap/PendingModalContent/index.tsx | 11 +++--- src/state/transactions/hooks.tsx | 10 ++++++ 6 files changed, 77 insertions(+), 26 deletions(-) diff --git a/cypress/e2e/swap/errors.test.ts b/cypress/e2e/swap/errors.test.ts index 9f33085e17..13d8dfc7a0 100644 --- a/cypress/e2e/swap/errors.test.ts +++ b/cypress/e2e/swap/errors.test.ts @@ -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,7 +97,9 @@ describe('Swap errors', () => { cy.contains('Confirm swap').click() cy.wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt') cy.contains('Swap submitted') - cy.get(getTestSelector('confirmation-close-icon')).click() + if (i === 0) { + cy.get(getTestSelector('confirmation-close-icon')).click() + } } cy.get(getTestSelector('web3-status-connected')).should('contain', '2 Pending') @@ -98,10 +107,15 @@ describe('Swap errors', () => { 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) + }) }) }) }) diff --git a/src/components/AccountDrawer/MiniPortfolio/Activity/parseLocal.ts b/src/components/AccountDrawer/MiniPortfolio/Activity/parseLocal.ts index f3cd81e259..93592248bd 100644 --- a/src/components/AccountDrawer/MiniPortfolio/Activity/parseLocal.ts +++ b/src/components/AccountDrawer/MiniPortfolio/Activity/parseLocal.ts @@ -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, diff --git a/src/components/swap/ConfirmSwapModal.tsx b/src/components/swap/ConfirmSwapModal.tsx index d6283a2b1c..5a7ab63fc2 100644 --- a/src/components/swap/ConfirmSwapModal.tsx +++ b/src/components/swap/ConfirmSwapModal.tsx @@ -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) { diff --git a/src/components/swap/PendingModalContent/PendingModalContent.test.tsx b/src/components/swap/PendingModalContent/PendingModalContent.test.tsx index c629a215bc..8bd0599ed8 100644 --- a/src/components/swap/PendingModalContent/PendingModalContent.test.tsx +++ b/src/components/swap/PendingModalContent/PendingModalContent.test.tsx @@ -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( { 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() diff --git a/src/components/swap/PendingModalContent/index.tsx b/src/components/swap/PendingModalContent/index.tsx index 2923790ef6..cb8c800f81 100644 --- a/src/components/swap/PendingModalContent/index.tsx +++ b/src/components/swap/PendingModalContent/index.tsx @@ -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 diff --git a/src/state/transactions/hooks.tsx b/src/state/transactions/hooks.tsx index 617fe19b4d..954e6e85f3 100644 --- a/src/state/transactions/hooks.tsx +++ b/src/state/transactions/hooks.tsx @@ -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