Commit 02883aca authored by eddie's avatar eddie Committed by GitHub

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
parent ace81ecc
import { BigNumber } from '@ethersproject/bignumber' 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 { 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' import { getBalance, getTestSelector } from '../../utils'
const UNI_MAINNET = UNI[ChainId.MAINNET]
describe('Swap errors', () => { describe('Swap errors', () => {
it('wallet rejection', () => { it('wallet rejection', () => {
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' }) cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' })
...@@ -64,10 +62,19 @@ describe('Swap errors', () => { ...@@ -64,10 +62,19 @@ describe('Swap errors', () => {
}) })
}) })
it.skip('slippage failure', () => { it('slippage failure', () => {
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${UNI_MAINNET.address}`, { ethereum: 'hardhat' }) cy.visit(`/swap?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`, { ethereum: 'hardhat' })
cy.hardhat({ automine: false }) cy.hardhat({ automine: false }).then(async (hardhat) => {
getBalance(USDC_MAINNET).then((initialBalance) => { 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. // Gas estimation fails for this transaction (that would normally fail), so we stub it.
cy.hardhat().then((hardhat) => { cy.hardhat().then((hardhat) => {
const send = cy.stub(hardhat.provider, 'send').log(false) const send = cy.stub(hardhat.provider, 'send').log(false)
...@@ -90,7 +97,9 @@ describe('Swap errors', () => { ...@@ -90,7 +97,9 @@ describe('Swap errors', () => {
cy.contains('Confirm swap').click() cy.contains('Confirm swap').click()
cy.wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt') cy.wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
cy.contains('Swap submitted') 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') cy.get(getTestSelector('web3-status-connected')).should('contain', '2 Pending')
...@@ -98,10 +107,15 @@ describe('Swap errors', () => { ...@@ -98,10 +107,15 @@ describe('Swap errors', () => {
cy.hardhat().then((hardhat) => hardhat.mine()) cy.hardhat().then((hardhat) => hardhat.mine())
cy.wait('@eth_getTransactionReceipt') 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('web3-status-connected')).should('not.contain', 'Pending')
cy.get(getTestSelector('popups')).contains('Swapped')
cy.get(getTestSelector('popups')).contains('Swap failed') 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( ...@@ -138,17 +138,21 @@ function parseMigrateCreateV3(
return { descriptor, currencies: [baseCurrency, quoteCurrency] } 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( export function transactionToActivity(
details: TransactionDetails, details: TransactionDetails,
chainId: ChainId, chainId: ChainId,
tokens: ChainTokenMap tokens: ChainTokenMap
): Activity | undefined { ): Activity | undefined {
try { try {
const status = !details.receipt const status = getTransactionStatus(details)
? TransactionStatus.Pending
: details.receipt.status === 1 || details.receipt?.status === undefined
? TransactionStatus.Confirmed
: TransactionStatus.Failed
const defaultFields = { const defaultFields = {
hash: details.hash, hash: details.hash,
......
...@@ -14,6 +14,7 @@ import Modal, { MODAL_TRANSITION_DURATION } from 'components/Modal' ...@@ -14,6 +14,7 @@ import Modal, { MODAL_TRANSITION_DURATION } from 'components/Modal'
import { RowFixed } from 'components/Row' import { RowFixed } from 'components/Row'
import { getChainInfo } from 'constants/chainInfo' import { getChainInfo } from 'constants/chainInfo'
import { USDT as USDT_MAINNET } from 'constants/tokens' import { USDT as USDT_MAINNET } from 'constants/tokens'
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import { useMaxAmountIn } from 'hooks/useMaxAmountIn' import { useMaxAmountIn } from 'hooks/useMaxAmountIn'
import { Allowance, AllowanceState } from 'hooks/usePermit2Allowance' import { Allowance, AllowanceState } from 'hooks/usePermit2Allowance'
import usePrevious from 'hooks/usePrevious' import usePrevious from 'hooks/usePrevious'
...@@ -24,7 +25,7 @@ import { getPriceUpdateBasisPoints } from 'lib/utils/analytics' ...@@ -24,7 +25,7 @@ import { getPriceUpdateBasisPoints } from 'lib/utils/analytics'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { InterfaceTrade, TradeFillType } from 'state/routing/types' import { InterfaceTrade, TradeFillType } from 'state/routing/types'
import { Field } from 'state/swap/actions' 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 styled from 'styled-components/macro'
import { ThemedText } from 'theme' import { ThemedText } from 'theme'
import invariant from 'tiny-invariant' import invariant from 'tiny-invariant'
...@@ -297,7 +298,13 @@ export default function ConfirmSwapModal({ ...@@ -297,7 +298,13 @@ export default function ConfirmSwapModal({
doesTradeDiffer: Boolean(doesTradeDiffer), 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(() => { useEffect(() => {
// Reset the modal state if the user rejected the swap. // Reset the modal state if the user rejected the swap.
if (swapError && !swapFailed) { if (swapError && !swapFailed) {
......
import { BigNumber } from '@ethersproject/bignumber' import { BigNumber } from '@ethersproject/bignumber'
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import { TradeFillType } from 'state/routing/types' 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 { TEST_TRADE_EXACT_INPUT } from 'test-utils/constants'
import { mocked } from 'test-utils/mocked' import { mocked } from 'test-utils/mocked'
import { render, screen } from 'test-utils/render' import { render, screen } from 'test-utils/render'
...@@ -14,7 +15,7 @@ jest.mock('state/transactions/hooks') ...@@ -14,7 +15,7 @@ jest.mock('state/transactions/hooks')
describe('PendingModalContent', () => { describe('PendingModalContent', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks() jest.clearAllMocks()
mocked(useIsTransactionConfirmed).mockReturnValue(false) mocked(useSwapTransactionStatus).mockReturnValue(TransactionStatus.Pending)
}) })
it('renders null for invalid content', () => { it('renders null for invalid content', () => {
...@@ -128,7 +129,7 @@ describe('PendingModalContent', () => { ...@@ -128,7 +129,7 @@ describe('PendingModalContent', () => {
}) })
it('renders the success icon instead of the given logo when confirmed and successful', () => { it('renders the success icon instead of the given logo when confirmed and successful', () => {
mocked(useIsTransactionConfirmed).mockReturnValue(true) mocked(useSwapTransactionStatus).mockReturnValue(TransactionStatus.Confirmed)
render( render(
<PendingModalContent <PendingModalContent
...@@ -138,6 +139,20 @@ describe('PendingModalContent', () => { ...@@ -138,6 +139,20 @@ describe('PendingModalContent', () => {
ConfirmModalState.PENDING_CONFIRMATION, ConfirmModalState.PENDING_CONFIRMATION,
]} ]}
currentStep={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() expect(screen.queryByTestId('pending-modal-failure-icon')).toBeNull()
......
...@@ -5,6 +5,7 @@ import { OrderContent } from 'components/AccountDrawer/MiniPortfolio/Activity/Of ...@@ -5,6 +5,7 @@ import { OrderContent } from 'components/AccountDrawer/MiniPortfolio/Activity/Of
import { ColumnCenter } from 'components/Column' import { ColumnCenter } from 'components/Column'
import Column from 'components/Column' import Column from 'components/Column'
import Row from 'components/Row' import Row from 'components/Row'
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import { SwapResult } from 'hooks/useSwapCallback' import { SwapResult } from 'hooks/useSwapCallback'
import { useUnmountingAnimation } from 'hooks/useUnmountingAnimation' import { useUnmountingAnimation } from 'hooks/useUnmountingAnimation'
import { UniswapXOrderStatus } from 'lib/hooks/orders/types' import { UniswapXOrderStatus } from 'lib/hooks/orders/types'
...@@ -12,7 +13,7 @@ import { ReactNode, useRef } from 'react' ...@@ -12,7 +13,7 @@ import { ReactNode, useRef } from 'react'
import { InterfaceTrade, TradeFillType } from 'state/routing/types' import { InterfaceTrade, TradeFillType } from 'state/routing/types'
import { useOrder } from 'state/signatures/hooks' import { useOrder } from 'state/signatures/hooks'
import { UniswapXOrderDetails } from 'state/signatures/types' 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 styled, { css, keyframes } from 'styled-components/macro'
import { ExternalLink } from 'theme' import { ExternalLink } from 'theme'
import { ThemedText } from 'theme/components/text' import { ThemedText } from 'theme/components/text'
...@@ -223,14 +224,14 @@ export function PendingModalContent({ ...@@ -223,14 +224,14 @@ export function PendingModalContent({
}: PendingModalContentProps) { }: PendingModalContentProps) {
const { chainId } = useWeb3React() const { chainId } = useWeb3React()
const classicSwapConfirmed = useIsTransactionConfirmed( const swapStatus = useSwapTransactionStatus(swapResult)
swapResult?.type === TradeFillType.Classic ? swapResult.response.hash : undefined
) const classicSwapConfirmed = swapStatus === TransactionStatus.Confirmed
const wrapConfirmed = useIsTransactionConfirmed(wrapTxHash) const wrapConfirmed = useIsTransactionConfirmed(wrapTxHash)
// TODO(UniswapX): Support UniswapX status here too // TODO(UniswapX): Support UniswapX status here too
const uniswapXSwapConfirmed = Boolean(swapResult) const uniswapXSwapConfirmed = Boolean(swapResult)
const swapConfirmed = TradeFillType.Classic ? classicSwapConfirmed : uniswapXSwapConfirmed const swapConfirmed = swapResult?.type === TradeFillType.Classic ? classicSwapConfirmed : uniswapXSwapConfirmed
const swapPending = swapResult !== undefined && !swapConfirmed const swapPending = swapResult !== undefined && !swapConfirmed
const wrapPending = wrapTxHash != undefined && !wrapConfirmed const wrapPending = wrapTxHash != undefined && !wrapConfirmed
......
...@@ -2,8 +2,12 @@ import { BigNumber } from '@ethersproject/bignumber' ...@@ -2,8 +2,12 @@ import { BigNumber } from '@ethersproject/bignumber'
import type { TransactionResponse } from '@ethersproject/providers' import type { TransactionResponse } from '@ethersproject/providers'
import { ChainId, SUPPORTED_CHAINS, Token } from '@uniswap/sdk-core' import { ChainId, SUPPORTED_CHAINS, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/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 { useCallback, useMemo } from 'react'
import { useAppDispatch, useAppSelector } from 'state/hooks' import { useAppDispatch, useAppSelector } from 'state/hooks'
import { TradeFillType } from 'state/routing/types'
import { addTransaction, removeTransaction } from './reducer' import { addTransaction, removeTransaction } from './reducer'
import { TransactionDetails, TransactionInfo, TransactionType } from './types' import { TransactionDetails, TransactionInfo, TransactionType } from './types'
...@@ -89,6 +93,12 @@ export function useIsTransactionConfirmed(transactionHash?: string): boolean { ...@@ -89,6 +93,12 @@ export function useIsTransactionConfirmed(transactionHash?: string): boolean {
return Boolean(transactions[transactionHash].receipt) 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) * Returns whether a transaction happened in the last day (86400 seconds * 1000 milliseconds / second)
* @param tx to check for recency * @param tx to check for recency
......
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