Commit 0b66fde2 authored by Zach Pomerantz's avatar Zach Pomerantz Committed by GitHub

fix: only remove expired transactions from updater (#6625)

* test(e2e): disable video compression

* refactor: improve popupList impl/test

* test(e2e): log JSON-RPC calls

* fix: retry backoff logic

* test(e2e): wait for hardhat/popup assertions

* fix: remove transactions after expired

* test(e2e): re-enable other swap tests
Co-authored-by: default avatarJordan Frankfurt <jordanwfrankfurt@gmail.com>

* chore: rm console.log

* fix: expire txs after 6h

* refactor: dry trade info

* test(e2e): use default deadline

---------
Co-authored-by: default avatarJordan Frankfurt <jordanwfrankfurt@gmail.com>
parent 57883859
......@@ -9,6 +9,7 @@ export default defineConfig({
chromeWebSecurity: false,
experimentalMemoryManagement: true, // better memory management, see https://github.com/cypress-io/cypress/pull/25462
retries: { runMode: 2 },
videoCompression: false,
e2e: {
async setupNodeEvents(on, config) {
await setupHardhatEvents(on, config)
......
import { BigNumber } from '@ethersproject/bignumber'
import { SupportedChainId } from '@uniswap/sdk-core'
import { DEFAULT_DEADLINE_FROM_NOW } from '../../../src/constants/misc'
import { UNI, USDC_MAINNET } from '../../../src/constants/tokens'
import { getBalance, getTestSelector } from '../../utils'
......@@ -13,15 +14,18 @@ describe('Swap errors', () => {
// Stub the wallet to reject any transaction.
cy.stub(hardhat.wallet, 'sendTransaction').log(false).rejects(new Error('user cancelled'))
// Attempt to swap.
cy.get('#swap-currency-output .token-amount-input').clear().type('1').should('have.value', '1')
// Enter amount to swap
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
// Submit transaction
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.contains('Confirm swap').click()
cy.wait('@eth_estimateGas')
cy.contains('Review swap').should('exist')
cy.get('body').click('topRight')
cy.contains('Review swap').should('not.exist')
// Verify rejection
cy.contains('Review swap')
cy.contains('Confirm swap')
})
})
......@@ -29,32 +33,33 @@ describe('Swap errors', () => {
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' })
cy.hardhat({ automine: false })
getBalance(USDC_MAINNET).then((initialBalance) => {
// Set deadline to minimum. (1 minute)
cy.get(getTestSelector('open-settings-dialog-button')).click()
cy.get(getTestSelector('transaction-deadline-settings')).click()
cy.get(getTestSelector('deadline-input')).clear().type('1') // 1 minute
// Click outside of modal to dismiss it.
cy.get('body').click('topRight')
cy.get(getTestSelector('deadline-input')).should('not.exist')
// Attempt to swap.
cy.get('#swap-currency-output .token-amount-input').clear().type('1').should('have.value', '1')
// Enter amount to swap
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
// Submit transaction
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.contains('Confirm swap').click()
cy.wait('@eth_estimateGas').wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
cy.contains('Transaction submitted')
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')
cy.hardhat().then((hardhat) => hardhat.mine(1, /* 10 minutes */ 1000 * 60 * 10)) // mines past the deadline
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
// cy.get(getTestSelector('transaction-popup')).contains('Swap failed')
// Mine transaction
cy.hardhat().then(async (hardhat) => {
// Remove the transaction from the mempool, so that it doesn't fail but it is past the deadline.
// This should result in it being removed from pending transactions, without a failure notificiation.
const transactions = await hardhat.send('eth_pendingTransactions', [])
await hardhat.send('hardhat_dropTransaction', [transactions[0].hash])
// Mine past the deadline
await hardhat.mine(1, DEFAULT_DEADLINE_FROM_NOW + 1)
})
cy.wait('@eth_getTransactionReceipt')
// Verify the balance is unchanged.
cy.get('#swap-currency-output [data-testid="balance-text"]').should('have.text', `Balance: ${initialBalance}`)
// Verify transaction did not occur
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
cy.get(getTestSelector('popups')).should('not.contain', 'Swap failed')
cy.get('#swap-currency-output').contains(`Balance: ${initialBalance}`)
getBalance(USDC_MAINNET).should('eq', initialBalance)
})
})
......@@ -74,43 +79,29 @@ describe('Swap errors', () => {
cy.get(getTestSelector('open-settings-dialog-button')).click()
cy.get(getTestSelector('max-slippage-settings')).click()
cy.get(getTestSelector('slippage-input')).clear().type('0.01')
// Click outside of modal to dismiss it.
cy.get('body').click('topRight')
cy.get('body').click('topRight') // close modal
cy.get(getTestSelector('slippage-input')).should('not.exist')
// Swap 2 times.
const AMOUNT_TO_SWAP = 200
cy.get('#swap-currency-input .token-amount-input')
.clear()
.type(AMOUNT_TO_SWAP.toString())
.should('have.value', AMOUNT_TO_SWAP.toString())
// Submit 2 transactions
for (let i = 0; i < 2; i++) {
cy.get('#swap-currency-input .token-amount-input').type('200').should('have.value', '200')
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.contains('Confirm Swap').should('exist')
cy.contains('Confirm swap').click()
cy.wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
cy.contains('Transaction submitted')
cy.get(getTestSelector('confirmation-close-icon')).click()
cy.get('#swap-currency-input .token-amount-input')
.clear()
.type(AMOUNT_TO_SWAP.toString())
.should('have.value', AMOUNT_TO_SWAP.toString())
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.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')
cy.hardhat().then((hardhat) => hardhat.mine())
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
// cy.get(getTestSelector('transaction-popup')).contains('Swap failed')
// Mine transactions
cy.hardhat().then((hardhat) => hardhat.mine())
cy.wait('@eth_getTransactionReceipt')
// Assert that the transactions were unsuccessful by checking on-chain balance.
getBalance(UNI_MAINNET).should('equal', initialBalance)
// Verify transaction did not occur
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
cy.get(getTestSelector('popups')).contains('Swap failed')
getBalance(UNI_MAINNET).should('eq', initialBalance)
})
})
})
......@@ -41,29 +41,35 @@ describe('Swap', () => {
cy.visit('/swap', { ethereum: 'hardhat' })
cy.hardhat({ automine: false })
getBalance(USDC_MAINNET).then((initialBalance) => {
// Select USDC
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.get(getTestSelector('token-search-input')).clear().type(USDC_MAINNET.address)
cy.get(getTestSelector('token-search-input')).type(USDC_MAINNET.address)
cy.contains('USDC').click()
cy.get('#swap-currency-output .token-amount-input').clear().type('1').should('have.value', '1')
// Enter amount to swap
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
// Submit transaction
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.contains('Review swap')
cy.contains('Confirm swap').click()
cy.wait('@eth_estimateGas').wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
cy.contains('Transaction submitted')
cy.get(getTestSelector('confirmation-close-icon')).click()
// The pending transaction indicator should reflect the state.
cy.contains('Transaction submitted').should('not.exist')
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
cy.hardhat().then((hardhat) => hardhat.mine())
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
// cy.get(getTestSelector('transaction-popup')).contains('Swapped')
// Mine transaction
cy.hardhat().then((hardhat) => hardhat.mine())
cy.wait('@eth_getTransactionReceipt')
// Verify the balance is updated.
cy.get('#swap-currency-output [data-testid="balance-text"]').should(
'have.text',
`Balance: ${initialBalance + 1}`
)
getBalance(USDC_MAINNET).should('eq', initialBalance + 1)
// Verify transaction
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
cy.get(getTestSelector('popups')).contains('Swapped')
const finalBalance = initialBalance + 1
cy.get('#swap-currency-output').contains(`Balance: ${finalBalance}`)
getBalance(USDC_MAINNET).should('eq', finalBalance)
})
})
})
......
......@@ -12,7 +12,7 @@ describe('Swap wrap', () => {
})
it('ETH to wETH is same value (wrapped swaps have no price impact)', () => {
cy.get('#swap-currency-input .token-amount-input').clear().type('0.01').should('have.value', '0.01')
cy.get('#swap-currency-input .token-amount-input').type('0.01').should('have.value', '0.01')
cy.get('#swap-currency-output .token-amount-input').should('have.value', '0.01')
cy.get('#swap-currency-output .token-amount-input').clear().type('0.02').should('have.value', '0.02')
......@@ -20,31 +20,28 @@ describe('Swap wrap', () => {
})
it('should be able to wrap ETH', () => {
getBalance(WETH).then((initialWethBalance) => {
getBalance(WETH).then((initialBalance) => {
cy.contains('Enter ETH amount')
// Enter the amount to wrap.
cy.get('#swap-currency-output .token-amount-input').click().type('1').should('have.value', 1)
// This also ensures we don't click "Wrap" before the UI has caught up.
// Enter amount to wrap
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', 1)
cy.get('#swap-currency-input .token-amount-input').should('have.value', 1)
// Click the wrap button.
// Submit transaction
cy.contains('Wrap').click()
// The pending transaction indicator should reflect the state.
cy.wait('@eth_estimateGas').wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
cy.hardhat().then((hardhat) => hardhat.mine())
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
// cy.get(getTestSelector('transaction-popup')).contains('Wrapped')
// cy.get(getTestSelector('transaction-popup')).contains('1.00 ETH for 1.00 WETH')
// The UI balance should have increased.
cy.get('#swap-currency-output').should('contain', `Balance: ${initialWethBalance + 1}`)
// Mine transaction
cy.hardhat().then((hardhat) => hardhat.mine())
cy.wait('@eth_getTransactionReceipt')
// The user's WETH account balance should have increased
getBalance(WETH).should('equal', initialWethBalance + 1)
// Verify transaction
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
cy.get(getTestSelector('popups')).contains('Wrapped')
const finalBalance = initialBalance + 1
cy.get('#swap-currency-output').contains(`Balance: ${finalBalance}`)
getBalance(WETH).should('equal', finalBalance)
})
})
......@@ -54,33 +51,30 @@ describe('Swap wrap', () => {
await hardhat.mine()
})
getBalance(WETH).then((initialWethBalance) => {
// Swap input/output to unwrap WETH.
getBalance(WETH).then((initialBalance) => {
// Swap input/output to unwrap WETH
cy.get(getTestSelector('swap-currency-button')).click()
cy.contains('Enter WETH amount')
// Enter the amount to unwrap.
cy.get('#swap-currency-output .token-amount-input').click().type('1').should('have.value', 1)
// This also ensures we don't click "Wrap" before the UI has caught up.
// Enter the amount to unwrap
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', 1)
cy.get('#swap-currency-input .token-amount-input').should('have.value', 1)
// Click the unwrap button.
// Submit transaction
cy.contains('Unwrap').click()
// The pending transaction indicator should reflect the state.
cy.wait('@eth_estimateGas').wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
cy.hardhat().then((hardhat) => hardhat.mine())
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
// cy.get(getTestSelector('transaction-popup')).contains('Unwrapped')
// cy.get(getTestSelector('transaction-popup')).contains('1.00 WETH for 1.00 ETH')
// The UI balance should have increased.
cy.get('#swap-currency-input').should('contain', `Balance: ${initialWethBalance - 1}`)
// Mine transaction
cy.hardhat().then((hardhat) => hardhat.mine())
cy.wait('@eth_getTransactionReceipt')
// The user's WETH account balance should have increased
getBalance(WETH).should('equal', initialWethBalance - 1)
// Verify transaction
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
cy.get(getTestSelector('popups')).contains('Unwrapped')
const finalBalance = initialBalance - 1
cy.get('#swap-currency-input').contains(`Balance: ${finalBalance}`)
getBalance(WETH).should('equal', finalBalance)
})
})
})
// @ts-ignore
import TokenListJSON from '@uniswap/default-token-list'
import { CyHttpMessages } from 'cypress/types/net-stubbing'
beforeEach(() => {
// Many API calls enforce that requests come from our app, so we must mock Origin and Referer.
......@@ -16,6 +17,9 @@ beforeEach(() => {
req.continue()
})
// Log requests to hardhat.
cy.intercept(/:8545/, logJsonRpc)
// Mock analytics responses to avoid analytics in tests.
cy.intercept('https://api.uniswap.org/v1/amplitude-proxy', (req) => {
const requestBody = JSON.stringify(req.body)
......@@ -39,3 +43,21 @@ beforeEach(() => {
// This resets the fork, as well as options like automine.
cy.hardhat().then((hardhat) => hardhat.reset())
})
function logJsonRpc(req: CyHttpMessages.IncomingHttpRequest) {
req.alias = req.body.method
const log = Cypress.log({
autoEnd: false,
name: req.body.method,
message: req.body.params?.map((param: unknown) =>
typeof param === 'object' ? '{...}' : param?.toString().substring(0, 10)
),
})
req.on('after:response', (res) => {
if (res.statusCode === 200) {
log.end()
} else {
log.error(new Error(`${res.statusCode}: ${res.statusMessage}`))
}
})
}
......@@ -29,7 +29,6 @@ function TransactionPopupContent({ tx, chainId }: { tx: TransactionDetails; chai
return (
<PortfolioRow
data-testid="transaction-popup"
left={
success ? (
<Column>
......
......@@ -66,14 +66,14 @@ export default function Popups() {
return (
<>
<FixedPopupColumn gap="20px" extraPadding={urlWarningActive} xlPadding={isNotOnMainnet}>
<FixedPopupColumn gap="20px" extraPadding={urlWarningActive} xlPadding={isNotOnMainnet} data-testid="popups">
<ClaimPopup />
{activePopups.map((item) => (
<PopupItem key={item.key} content={item.content} popKey={item.key} removeAfterMs={item.removeAfterMs} />
))}
</FixedPopupColumn>
{activePopups?.length > 0 && (
<MobilePopupWrapper>
<MobilePopupWrapper data-testid="popups">
<MobilePopupInner>
{activePopups // reverse so new items up front
.slice(0)
......
......@@ -4,7 +4,7 @@ import { PermitSignature } from 'hooks/usePermitAllowance'
import { useMemo } from 'react'
import { useTransactionAdder } from '../state/transactions/hooks'
import { TransactionType } from '../state/transactions/types'
import { TransactionInfo, TransactionType } from '../state/transactions/types'
import { currencyId } from '../utils/currencyId'
import useTransactionDeadline from './useTransactionDeadline'
import { useUniversalRouterSwapCallback } from './useUniversalRouter'
......@@ -30,33 +30,30 @@ export function useSwapCallback(
const callback = useMemo(() => {
if (!trade || !swapCallback) return null
return () =>
swapCallback().then((response) => {
addTransaction(
response,
trade.tradeType === TradeType.EXACT_INPUT
? {
const info: TransactionInfo = {
type: TransactionType.SWAP,
tradeType: TradeType.EXACT_INPUT,
inputCurrencyId: currencyId(trade.inputAmount.currency),
outputCurrencyId: currencyId(trade.outputAmount.currency),
...(trade.tradeType === TradeType.EXACT_INPUT
? {
tradeType: TradeType.EXACT_INPUT,
inputCurrencyAmountRaw: trade.inputAmount.quotient.toString(),
expectedOutputCurrencyAmountRaw: trade.outputAmount.quotient.toString(),
outputCurrencyId: currencyId(trade.outputAmount.currency),
minimumOutputCurrencyAmountRaw: trade.minimumAmountOut(allowedSlippage).quotient.toString(),
}
: {
type: TransactionType.SWAP,
tradeType: TradeType.EXACT_OUTPUT,
inputCurrencyId: currencyId(trade.inputAmount.currency),
maximumInputCurrencyAmountRaw: trade.maximumAmountIn(allowedSlippage).quotient.toString(),
outputCurrencyId: currencyId(trade.outputAmount.currency),
outputCurrencyAmountRaw: trade.outputAmount.quotient.toString(),
expectedInputCurrencyAmountRaw: trade.inputAmount.quotient.toString(),
}),
}
)
return () =>
swapCallback().then((response) => {
addTransaction(response, info, deadline?.toNumber())
return response.hash
})
}, [addTransaction, allowedSlippage, swapCallback, trade])
}, [addTransaction, allowedSlippage, deadline, swapCallback, trade])
return {
callback,
......
import { retry, RetryableError } from './retry'
import { CanceledError, retry, RetryableError } from './retry'
describe('retry', () => {
function makeFn<T>(fails: number, result: T, retryable = true): () => Promise<T> {
......@@ -32,7 +32,7 @@ describe('retry', () => {
it('cancel causes promise to reject', async () => {
const { promise, cancel } = retry(makeFn(2, 'abc'), { n: 3, minWait: 100, maxWait: 100 })
cancel()
await expect(promise).rejects.toThrow('Cancelled')
await expect(promise).rejects.toThrow(expect.any(CanceledError))
})
it('cancel no-op after complete', async () => {
......
......@@ -6,21 +6,15 @@ function waitRandom(min: number, max: number): Promise<void> {
return wait(min + Math.round(Math.random() * Math.max(0, max - min)))
}
/**
* This error is thrown if the function is cancelled before completing
*/
class CancelledError extends Error {
public isCancelledError = true as const
constructor() {
super('Cancelled')
}
/** Thrown if the function is canceled before resolving. */
export class CanceledError extends Error {
name = 'CanceledError'
message = 'Retryable was canceled'
}
/**
* Throw this error if the function should retry
*/
/** May be thrown to force a retry. */
export class RetryableError extends Error {
public isRetryableError = true as const
name = 'RetryableError'
}
export interface RetryOptions {
......@@ -30,7 +24,7 @@ export interface RetryOptions {
}
/**
* Retries the function that returns the promise until the promise successfully resolves up to n retries
* Retries a function until its returned promise successfully resolves, up to n times.
* @param fn function to retry
* @param n how many times to retry
* @param minWait min wait between retries in ms
......@@ -59,7 +53,7 @@ export function retry<T>(
if (completed) {
break
}
if (n <= 0 || !error.isRetryableError) {
if (n <= 0 || !(error instanceof RetryableError)) {
reject(error)
completed = true
break
......@@ -74,7 +68,7 @@ export function retry<T>(
cancel: () => {
if (completed) return
completed = true
rejectCancelled(new CancelledError())
rejectCancelled(new CanceledError())
},
}
}
import { TransactionReceipt } from '@ethersproject/abstract-provider'
import { useWeb3React } from '@web3-react/core'
import { SupportedChainId } from 'constants/chains'
import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp'
import useBlockNumber, { useFastForwardBlockNumber } from 'lib/hooks/useBlockNumber'
import ms from 'ms.macro'
import { useCallback, useEffect } from 'react'
import { useTransactionRemover } from 'state/transactions/hooks'
import { TransactionDetails } from 'state/transactions/types'
import { retry, RetryableError, RetryOptions } from './retry'
import { CanceledError, retry, RetryableError, RetryOptions } from './retry'
interface Transaction {
addedTime: number
......@@ -53,6 +54,7 @@ export default function Updater({ pendingTransactions, onCheck, onReceipt }: Upd
const lastBlockNumber = useBlockNumber()
const fastForwardBlockNumber = useFastForwardBlockNumber()
const removeTransaction = useTransactionRemover()
const blockTimestamp = useCurrentBlockTimestamp()
const getReceipt = useCallback(
(hash: string) => {
......@@ -63,16 +65,17 @@ export default function Updater({ pendingTransactions, onCheck, onReceipt }: Upd
provider.getTransactionReceipt(hash).then(async (receipt) => {
if (receipt === null) {
if (account) {
const transactionCount = await provider.getTransactionCount(account)
const tx = pendingTransactions[hash]
// We check for the presence of a nonce because we haven't always saved them,
// so this code may run against old store state where nonce is undefined.
if (tx.nonce && tx.nonce < transactionCount) {
// We remove pending transactions from redux if they are no longer the latest nonce.
// Remove transactions past their deadline or - if there is no deadline - older than 6 hours.
if (tx.deadline) {
// Deadlines are expressed as seconds since epoch, as they are used on-chain.
if (blockTimestamp && tx.deadline < blockTimestamp.toNumber()) {
removeTransaction(hash)
}
} else if (tx.addedTime + ms`6h` < Date.now()) {
removeTransaction(hash)
}
}
console.debug(`Retrying tranasaction receipt for ${hash}`)
throw new RetryableError()
}
return receipt
......@@ -80,7 +83,7 @@ export default function Updater({ pendingTransactions, onCheck, onReceipt }: Upd
retryOptions
)
},
[account, chainId, pendingTransactions, provider, removeTransaction]
[account, blockTimestamp, chainId, pendingTransactions, provider, removeTransaction]
)
useEffect(() => {
......@@ -92,17 +95,12 @@ export default function Updater({ pendingTransactions, onCheck, onReceipt }: Upd
const { promise, cancel } = getReceipt(hash)
promise
.then((receipt) => {
if (receipt) {
fastForwardBlockNumber(receipt.blockNumber)
onReceipt({ chainId, hash, receipt })
} else {
onCheck({ chainId, hash, blockNumber: lastBlockNumber })
}
})
.catch((error) => {
if (!error.isCancelledError) {
console.warn(`Failed to get transaction receipt for ${hash}`, error)
}
if (error instanceof CanceledError) return
onCheck({ chainId, hash, blockNumber: lastBlockNumber })
})
return cancel
})
......
......@@ -21,26 +21,53 @@ describe('application reducer', () => {
})
})
describe('popupList', () => {
describe('addPopup', () => {
it('adds the popup to list with a generated id', () => {
store.dispatch(addPopup({ content: { txn: { hash: 'abc' } } }))
const list = store.getState().popupList
expect(list).toHaveLength(1)
expect(typeof list[0].key).toEqual('string')
expect(list[0].show).toEqual(true)
expect(list[0].content).toEqual({ txn: { hash: 'abc' } })
expect(list[0].removeAfterMs).toEqual(10000)
expect(list).toEqual([
{
key: expect.any(String),
show: true,
content: { txn: { hash: 'abc' } },
removeAfterMs: 10000,
},
])
})
it('replaces any existing popups with the same key', () => {
store.dispatch(addPopup({ key: 'abc', content: { txn: { hash: 'abc' } } }))
store.dispatch(addPopup({ key: 'abc', content: { txn: { hash: 'def' } } }))
const list = store.getState().popupList
expect(list).toHaveLength(1)
expect(list[0].key).toEqual('abc')
expect(list[0].show).toEqual(true)
expect(list[0].content).toEqual({ txn: { hash: 'def' } })
expect(list[0].removeAfterMs).toEqual(10000)
expect(list).toEqual([
{
key: 'abc',
show: true,
content: { txn: { hash: 'def' } },
removeAfterMs: 10000,
},
])
})
})
describe('removePopup', () => {
beforeEach(() => {
store.dispatch(addPopup({ key: 'abc', content: { txn: { hash: 'abc' } } }))
})
it('hides the popup', () => {
store.dispatch(removePopup({ key: 'abc' }))
const list = store.getState().popupList
expect(list).toEqual([
{
key: 'abc',
show: false,
content: { txn: { hash: 'abc' } },
removeAfterMs: 10000,
},
])
})
})
})
......@@ -66,16 +93,4 @@ describe('application reducer', () => {
expect(store.getState().chainId).toEqual(1)
})
})
describe('removePopup', () => {
beforeEach(() => {
store.dispatch(addPopup({ key: 'abc', content: { txn: { hash: 'abc' } } }))
})
it('hides the popup', () => {
expect(store.getState().popupList[0].show).toBe(true)
store.dispatch(removePopup({ key: 'abc' }))
expect(store.getState().popupList).toHaveLength(1)
expect(store.getState().popupList[0].show).toBe(false)
})
})
})
......@@ -68,20 +68,23 @@ const applicationSlice = createSlice({
state.openModal = action.payload
},
addPopup(state, { payload: { content, key, removeAfterMs = DEFAULT_TXN_DISMISS_MS } }) {
state.popupList = (key ? state.popupList.filter((popup) => popup.key !== key) : state.popupList).concat([
key = key || nanoid()
state.popupList = [
...state.popupList.filter((popup) => popup.key !== key),
{
key: key || nanoid(),
key,
show: true,
content,
removeAfterMs,
},
])
]
},
removePopup(state, { payload: { key } }) {
state.popupList.forEach((p) => {
if (p.key === key) {
p.show = false
state.popupList = state.popupList.map((popup) => {
if (popup.key === key) {
popup.show = false
}
return popup
})
},
},
......
......@@ -9,12 +9,16 @@ import { addTransaction, removeTransaction } from './reducer'
import { TransactionDetails, TransactionInfo, TransactionType } from './types'
// helper that can take a ethers library transaction response and add it to the list of transactions
export function useTransactionAdder(): (response: TransactionResponse, info: TransactionInfo) => void {
export function useTransactionAdder(): (
response: TransactionResponse,
info: TransactionInfo,
deadline?: number
) => void {
const { chainId, account } = useWeb3React()
const dispatch = useAppDispatch()
return useCallback(
(response: TransactionResponse, info: TransactionInfo) => {
(response: TransactionResponse, info: TransactionInfo, deadline?: number) => {
if (!account) return
if (!chainId) return
......@@ -22,7 +26,7 @@ export function useTransactionAdder(): (response: TransactionResponse, info: Tra
if (!hash) {
throw Error('No transaction hash found.')
}
dispatch(addTransaction({ hash, from: account, info, chainId, nonce }))
dispatch(addTransaction({ hash, from: account, info, chainId, nonce, deadline }))
},
[account, chainId, dispatch]
)
......
......@@ -4,8 +4,6 @@ import { SupportedChainId } from 'constants/chains'
import { updateVersion } from '../global/actions'
import { TransactionDetails, TransactionInfo } from './types'
const now = () => new Date().getTime()
// TODO(WEB-2053): update this to be a map of account -> chainId -> txHash -> TransactionDetails
// to simplify usage, once we're able to invalidate localstorage
export interface TransactionState {
......@@ -20,6 +18,7 @@ interface AddTransactionPayload {
hash: string
info: TransactionInfo
nonce: number
deadline?: number
}
export const initialState: TransactionState = {}
......@@ -30,13 +29,13 @@ const transactionSlice = createSlice({
reducers: {
addTransaction(
transactions,
{ payload: { chainId, from, hash, info, nonce } }: { payload: AddTransactionPayload }
{ payload: { chainId, from, hash, info, nonce, deadline } }: { payload: AddTransactionPayload }
) {
if (transactions[chainId]?.[hash]) {
throw Error('Attempted to add existing transaction.')
}
const txs = transactions[chainId] ?? {}
txs[hash] = { hash, info, from, addedTime: now(), nonce }
txs[hash] = { hash, info, from, addedTime: Date.now(), nonce, deadline }
transactions[chainId] = txs
},
clearAllTransactions(transactions, { payload: { chainId } }) {
......@@ -65,7 +64,7 @@ const transactionSlice = createSlice({
return
}
tx.receipt = receipt
tx.confirmedTime = now()
tx.confirmedTime = Date.now()
},
},
extraReducers: (builder) => {
......
......@@ -203,6 +203,7 @@ export interface TransactionDetails {
lastCheckedBlockNumber?: number
addedTime: number
confirmedTime?: number
deadline?: number
from: string
info: TransactionInfo
nonce?: number
......
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