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:
parent
ace81ecc84
commit
02883aca13
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user