Compare commits

...

13 Commits

Author SHA1 Message Date
Jordan Frankfurt
bd4042aa16 chore: update pr template (#6634)
Co-authored-by: Jordan Frankfurt <jordan@CORN-Jordan-949.frankfurt>
2023-05-23 14:53:07 -07:00
github-actions[bot]
1dcafd2f2d chore(i18n): new Crowdin translations (#6215)
* chore(i18n): synchronize translations from crowdin [skip ci]

* chore: trigger actions

---------

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-05-23 14:39:58 -07:00
Jordan Frankfurt
66fcdb4465 feat: improve yarn prepare scripts (#6609)
* feat: improve yarn prepare scripts

* reset yarn.lock to main

* pr feedback

* Update scripts/prepare.js

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* Update scripts/prepare.js

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* Update scripts/prepare.js

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* Update scripts/prepare.js

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* Update scripts/prepare.js

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* Update scripts/prepare.js

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* Update scripts/prepare.js

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* pr feedback

* switch to using concurrently

* yarn dedupe

---------

Co-authored-by: Jordan Frankfurt <jordan@CORN-Jordan-949.frankfurt>
Co-authored-by: Jordan Frankfurt <jordan@corn-jordan-949.lan>
Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-05-23 16:07:05 -05:00
Jack Short
e398e8b950 fix: allow unsupported chain in pwat (#6629)
chore: allow unsupported chain in pwat
2023-05-23 13:36:28 -04:00
Tina
6424fdfbcd fix: Simplify event logging for SWAP_QUOTE_RECEIVED (#6628)
* simplify event logging

* remove unused function parameter
2023-05-23 11:06:18 -04:00
eddie
95814e3271 fix: prevent race condition for swap state (#6624) 2023-05-22 15:57:17 -07:00
Jack Short
caa2524e27 feat: [DetailsV2] instant buy (#6599)
* initial impl

* removing isopen change

* stopping refetching

* shared button

* pending animiation

* updating shared

* updating snapshots

* adding disabled state

* isLoading in hook

* pulling out ternary

* removing fragment

* separate file for offer button

* fixing price diff check

* remove unnecessary export

* changing name to useBuyAssetCallback
2023-05-22 18:28:48 -04:00
Zach Pomerantz
d28a4b34cd fix: do not attempt to cache i18n:extract (#6616) 2023-05-22 14:03:11 -07:00
cartcrom
f3a80c6272 feat: special case arb search (#6584)
* feat: special case arb search

* fix: check both current and existing token
2023-05-22 12:40:46 -04:00
Zach Pomerantz
b89ee36448 test(e2e): attempt to de-flake (#6611)
* test(e2e): improve memory mgmt

* test(e2e): record flakes

* test(e2e): simplify tests in attempt to de-flake

* test(e2e): more simplification

* test(e2e): disable transaction popup checks

* test(e2e): better wrap assertions

* test(e2e): always assert both inputs
2023-05-22 09:02:54 -07:00
Vignesh Mohankumar
fbc55db937 chore: remove chunkResponseStatus tag (#6586)
* chore: remove chunkResponseStatus tag

* lint
2023-05-21 17:25:58 -04:00
Jordan Frankfurt
835c62acfa fix: use ephemeral props for styled component (#6607)
* fix: use ephemeral props for styled component

* add
2023-05-20 16:55:37 -05:00
Zach Pomerantz
8fe7c7a0a7 build: notify from notify/test (#6597)
* build: notify from notify/test

* debug

* debug2

* revert debugs
2023-05-19 12:24:11 -07:00
65 changed files with 36798 additions and 18396 deletions

View File

@@ -53,16 +53,6 @@ runs:
shell: bash
# Messages are extracted from source.
# A record of source file content hashes is maintained in node_modules/.cache/lingui by a custom extractor.
# Messages are always extracted, but extraction may rely on the custom extractor's loaded cache.
- uses: actions/cache@v3
id: i18n-extract-cache
with:
path: |
src/locales/en-US.po
node_modules/.cache
key: ${{ runner.os }}-i18n-extract-${{ github.run_id }}
restore-keys: ${{ runner.os }}-i18n-extract-
- run: yarn i18n:extract
shell: bash

View File

@@ -6,7 +6,7 @@
<!-- Delete inapplicable lines: -->
_JIRA ticket:_
_Linear ticket:_
_Slack thread:_
_Relevant docs:_
@@ -14,9 +14,16 @@ _Relevant docs:_
<!-- Delete this section if your change does not affect UI. -->
## Screen capture
| Before | After (Desktop) | After (Mobile) |
| ------------ |---------------- | -------------- |
| paste_before | past_after | paste_after |
### Before
| Mobile | Desktop |
| ------------ | ------------ |
| paste_before | paste_before |
### After
| Mobile | Desktop |
| ------------ | ----------- |
| paste_after | paste_after |
## Test plan

View File

@@ -16,6 +16,8 @@ on:
jobs:
lint:
runs-on: ubuntu-latest
environment:
name: ${{ github.ref_name == 'main' && 'notify/test' }}
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
@@ -30,10 +32,12 @@ jobs:
uses: ./.github/actions/report
with:
name: Lint
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
typecheck:
runs-on: ubuntu-latest
environment:
name: ${{ github.ref_name == 'main' && 'notify/test' }}
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
@@ -48,10 +52,12 @@ jobs:
uses: ./.github/actions/report
with:
name: Typecheck
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
deps-tests:
runs-on: ubuntu-latest
environment:
name: ${{ github.ref_name == 'main' && 'notify/test' }}
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
@@ -60,10 +66,12 @@ jobs:
uses: ./.github/actions/report
with:
name: Dependency checks
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
unit-tests:
runs-on: ubuntu-latest
environment:
name: ${{ github.ref_name == 'main' && 'notify/test' }}
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
@@ -84,7 +92,7 @@ jobs:
uses: ./.github/actions/report
with:
name: Unit tests
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
build-e2e:
runs-on: ubuntu-latest
@@ -115,6 +123,8 @@ jobs:
cypress-test-matrix:
needs: [build-e2e, cypress-rerun]
runs-on: ubuntu-latest
environment:
name: ${{ github.ref_name == 'main' && 'notify/test' }}
container: cypress/browsers:node-18.14.1-chrome-111.0.5563.64-1-ff-111.0-edge-111.0.1661.43-1
strategy:
fail-fast: false
@@ -167,17 +177,17 @@ jobs:
verbose: true
flags: e2e-tests
- if: failure() && github.ref_name == 'main'
uses: ./.github/actions/report
with:
name: Cypress tests
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
# Included as a single job to check for cypress-test-matrix success, as a matrix cannot be checked.
cypress-tests:
if: always()
needs: [cypress-test-matrix]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- if: needs.cypress-test-matrix.result != 'success'
run: exit 1
- if: failure() && github.ref_name == 'main'
uses: ./.github/actions/report
with:
name: Cypress tests
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}

View File

@@ -1,17 +1,29 @@
import codeCoverageTask from '@cypress/code-coverage/task'
import { defineConfig } from 'cypress'
import { setupHardhatEvents } from 'cypress-hardhat'
import { unlinkSync } from 'fs'
export default defineConfig({
projectId: 'yp82ef',
videoUploadOnPasses: false,
defaultCommandTimeout: 24000, // 2x average block time
chromeWebSecurity: false,
experimentalMemoryManagement: true, // better memory management, see https://github.com/cypress-io/cypress/pull/25462
retries: { runMode: 2 },
e2e: {
async setupNodeEvents(on, config) {
await setupHardhatEvents(on, config)
codeCoverageTask(on, config)
// Delete recorded videos for specs that passed without flakes.
on('after:spec', async (spec, results) => {
if (results && results.video) {
// If there were no failures (including flakes), delete the recorded video.
if (!results.tests?.some((test) => test.attempts.some((attempt) => attempt?.state === 'failed'))) {
unlinkSync(results.video)
}
}
})
return {
...config,
// Only enable Chrome.

View File

@@ -1,3 +1,4 @@
import { USDC_MAINNET } from '../../../src/constants/tokens'
import { getTestSelector } from '../../utils'
describe('mini-portfolio activity history', () => {
@@ -93,24 +94,15 @@ describe('mini-portfolio activity history', () => {
})
it('should deduplicate activity history by nonce', () => {
cy.visit('/swap', { ethereum: 'hardhat' }).hardhat({ automine: false })
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' }).hardhat({
automine: false,
})
// Input swap info.
cy.get('#swap-currency-input .token-amount-input').clear().type('1')
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.contains('USDC').click()
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
cy.get('#swap-currency-input .token-amount-input').clear().type('1').should('have.value', '1')
cy.get('#swap-currency-output .token-amount-input').should('not.have.value', '')
// Set slippage to a high value.
cy.get(getTestSelector('open-settings-dialog-button')).click()
cy.get(getTestSelector('max-slippage-settings')).click()
cy.get(getTestSelector('slippage-input')).clear().type('5')
cy.get('body').click('topRight')
cy.get(getTestSelector('slippage-input')).should('not.exist')
// Click swap button.
cy.contains('1 USDC = ').should('exist')
cy.get('#swap-button').should('not.be', 'disabled').click()
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.get(getTestSelector('dismiss-tx-confirmation')).click()

View File

@@ -1,89 +1,114 @@
import { BigNumber } from '@ethersproject/bignumber'
import { parseEther } from '@ethersproject/units'
import { SupportedChainId } from '@uniswap/sdk-core'
import { USDC_MAINNET } from '../../../src/constants/tokens'
import { getTestSelector } from '../../utils'
import { UNI, USDC_MAINNET } from '../../../src/constants/tokens'
import { getBalance, getTestSelector } from '../../utils'
describe('Swap', () => {
it('should render and dismiss the wallet rejection modal', () => {
cy.visit('/swap', { ethereum: 'hardhat' })
.hardhat()
.then((hardhat) => {
cy.stub(hardhat.wallet, 'sendTransaction').log(false).rejects(new Error('user cancelled'))
const UNI_MAINNET = UNI[SupportedChainId.MAINNET]
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.get(getTestSelector('token-search-input')).clear().type(USDC_MAINNET.address)
cy.contains('USDC').click()
cy.get('#swap-currency-output .token-amount-input').clear().type('1')
cy.get('#swap-currency-input .token-amount-input').should('not.equal', '')
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.contains('Transaction rejected').should('exist')
cy.contains('Dismiss').click()
cy.contains('Transaction rejected').should('not.exist')
})
describe('Swap errors', () => {
it('wallet rejection', () => {
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' })
cy.hardhat().then((hardhat) => {
// 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')
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.contains('Transaction rejected').should('exist')
cy.contains('Dismiss').click()
cy.contains('Transaction rejected').should('not.exist')
})
})
it.skip('should render an error for slippage failure', () => {
cy.visit('/swap', { ethereum: 'hardhat' })
.hardhat({ automine: false })
.then((hardhat) => {
cy.then(() => hardhat.provider.getBalance(hardhat.wallet.address)).then((initialBalance) => {
// Gas estimation fails for this transaction (that would normally fail), so we stub it.
const send = cy.stub(hardhat.provider, 'send').log(false)
send.withArgs('eth_estimateGas').resolves(BigNumber.from(2_000_000))
send.callThrough()
it('transaction past deadline', () => {
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
// Set slippage to a very low value.
cy.get(getTestSelector('open-settings-dialog-button')).click()
cy.get(getTestSelector('max-slippage-settings')).click()
cy.get(getTestSelector('slippage-input')).clear().type('0.01')
cy.get('body').click('topRight')
cy.get(getTestSelector('slippage-input')).should('not.exist')
// Click outside of modal to dismiss it.
cy.get('body').click('topRight')
cy.get(getTestSelector('deadline-input')).should('not.exist')
// Open the currency select modal.
cy.get('#swap-currency-output .open-currency-select-button').click()
// Attempt to swap.
cy.get('#swap-currency-output .token-amount-input').clear().type('1').should('have.value', '1')
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.get(getTestSelector('dismiss-tx-confirmation')).click()
// Select UNI as output token
cy.get(getTestSelector('token-search-input')).clear().type('Uniswap')
cy.get(getTestSelector('currency-list-wrapper'))
.contains(/^Uniswap$/)
.first()
// Our scrolling library (react-window) seems to freeze when acted on by cypress, with this element set to
// `pointer-events: none`. This can be ignored using `{force: true}`.
.click({ force: true })
// 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')
// Swap 2 times.
const AMOUNT_TO_SWAP = 400
const NUMBER_OF_SWAPS = 2
const INDIVIDUAL_SWAP_INPUT = AMOUNT_TO_SWAP / NUMBER_OF_SWAPS
cy.get('#swap-currency-input .token-amount-input').clear().type(INDIVIDUAL_SWAP_INPUT.toString())
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.get(getTestSelector('dismiss-tx-confirmation')).click()
cy.get('#swap-currency-input .token-amount-input').clear().type(INDIVIDUAL_SWAP_INPUT.toString())
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.get(getTestSelector('dismiss-tx-confirmation')).click()
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
// cy.get(getTestSelector('transaction-popup')).contains('Swap failed')
// The pending transaction indicator should be visible.
cy.contains('Pending').should('exist')
// Verify the balance is unchanged.
cy.get('#swap-currency-output [data-testid="balance-text"]').should('have.text', `Balance: ${initialBalance}`)
getBalance(USDC_MAINNET).should('eq', initialBalance)
})
})
cy.then(() => hardhat.mine()).then(() => {
// The pending transaction indicator should not be visible.
cy.contains('Pending').should('not.exist')
// Check for a failed transaction notification.
cy.contains('Swap failed').should('exist')
// Assert that at least one of the swaps failed due to slippage.
cy.then(() => hardhat.provider.getBalance(hardhat.wallet.address)).then((finalBalance) => {
expect(finalBalance.gt(initialBalance.sub(parseEther(AMOUNT_TO_SWAP.toString())))).to.be.true
})
})
})
it('slippage failure', () => {
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${UNI_MAINNET.address}`, { ethereum: 'hardhat' })
cy.hardhat({ automine: false })
getBalance(USDC_MAINNET).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)
send.withArgs('eth_estimateGas').resolves(BigNumber.from(2_000_000))
send.callThrough()
})
// Set slippage to a very low value.
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(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())
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.get(getTestSelector('dismiss-tx-confirmation')).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.get(getTestSelector('dismiss-tx-confirmation')).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')
// Assert that the transactions were unsuccessful by checking on-chain balance.
getBalance(UNI_MAINNET).should('equal', initialBalance)
})
})
})

View File

@@ -0,0 +1,14 @@
import { getTestSelector } from '../../utils'
describe('Swap settings', () => {
it('Opens and closes the settings menu', () => {
cy.visit('/swap')
cy.contains('Settings').should('not.exist')
cy.get(getTestSelector('open-settings-dialog-button')).click()
cy.contains('Max slippage').should('exist')
cy.contains('Transaction deadline').should('exist')
cy.contains('Auto Router API').should('exist')
cy.get(getTestSelector('open-settings-dialog-button')).click()
cy.contains('Settings').should('not.exist')
})
})

View File

@@ -1,162 +1,69 @@
import { SupportedChainId } from '@uniswap/sdk-core'
import { UNI, USDC_MAINNET } from '../../../src/constants/tokens'
import { getTestSelector } from '../../utils'
import { getBalance, getTestSelector } from '../../utils'
const UNI_MAINNET = UNI[SupportedChainId.MAINNET]
describe('Swap', () => {
describe('Swap on main page', () => {
beforeEach(() => {
cy.visit('/swap', { ethereum: 'hardhat' })
})
it('starts with ETH selected by default', () => {
cy.visit('/swap')
cy.get(`#swap-currency-input .token-amount-input`).should('have.value', '')
cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'ETH')
cy.get(`#swap-currency-output .token-amount-input`).should('not.have.value')
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'Select token')
})
it('can enter an amount into input', () => {
cy.get('#swap-currency-input .token-amount-input').clear().type('0.001').should('have.value', '0.001')
})
it('zero swap amount', () => {
cy.get('#swap-currency-input .token-amount-input').clear().type('0.0').should('have.value', '0.0')
})
it('invalid swap amount', () => {
cy.get('#swap-currency-input .token-amount-input').clear().type('\\').should('have.value', '')
})
it('can enter an amount into output', () => {
cy.get('#swap-currency-output .token-amount-input').clear().type('0.001').should('have.value', '0.001')
})
it('zero output amount', () => {
cy.get('#swap-currency-output .token-amount-input').clear().type('0.0').should('have.value', '0.0')
})
it('should render an error when a transaction fails due to a passed deadline', () => {
const DEADLINE_MINUTES = 1
const TEN_MINUTES_MS = 1000 * 60 * DEADLINE_MINUTES * 10
cy.hardhat({ automine: false }).then((hardhat) => {
cy.then(() => hardhat.getBalance(hardhat.wallet.address, USDC_MAINNET))
.then((balance) => Number(balance.toFixed(1)))
.then((initialBalance) => {
// Input swap info.
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.contains('USDC').click()
cy.get('#swap-currency-output .token-amount-input').clear().type('1')
cy.get('#swap-currency-input .token-amount-input').should('not.equal', '')
// 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(DEADLINE_MINUTES.toString())
cy.get('body').click('topRight')
cy.get(getTestSelector('deadline-input')).should('not.exist')
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
// Dismiss the modal that appears when a transaction is broadcast to the network.
cy.get(getTestSelector('dismiss-tx-confirmation')).click()
// The UI should show the transaction as pending.
cy.contains('1 Pending').should('exist')
// Mine a block past the deadline.
cy.then(() => hardhat.mine(1, TEN_MINUTES_MS)).then(() => {
// The UI should no longer show the transaction as pending.
cy.contains('1 Pending').should('not.exist')
// Check that the user is informed of the failure
cy.contains('Swap failed').should('exist')
// Check that the balance is unchanged in the UI
cy.get('#swap-currency-output [data-testid="balance-text"]').should(
'have.text',
`Balance: ${initialBalance}`
)
// Check that the balance is unchanged on chain
cy.then(() => hardhat.getBalance(hardhat.wallet.address, USDC_MAINNET))
.then((balance) => Number(balance.toFixed(1)))
.should('eq', initialBalance)
})
})
})
})
it('should default inputs from URL params ', () => {
cy.visit(`/swap?inputCurrency=${UNI_MAINNET.address}`, { ethereum: 'hardhat' })
cy.visit(`/swap?inputCurrency=${UNI_MAINNET.address}`)
cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'UNI')
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'Select token')
cy.visit(`/swap?outputCurrency=${UNI_MAINNET.address}`, { ethereum: 'hardhat' })
cy.visit(`/swap?outputCurrency=${UNI_MAINNET.address}`)
cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'Select token')
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'UNI')
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${UNI_MAINNET.address}`, { ethereum: 'hardhat' })
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${UNI_MAINNET.address}`)
cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'ETH')
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'UNI')
})
it('ETH to wETH is same value (wrapped swaps have no price impact)', () => {
cy.get(`#swap-currency-output .open-currency-select-button`).click()
cy.contains('WETH').click()
cy.get('#swap-currency-input .token-amount-input').clear().type('0.01')
cy.get('#swap-currency-output .token-amount-input').should('have.value', '0.01')
})
it('Opens and closes the settings menu', () => {
cy.contains('Settings').should('not.exist')
cy.get(getTestSelector('open-settings-dialog-button')).click()
cy.contains('Max slippage').should('exist')
cy.contains('Transaction deadline').should('exist')
cy.contains('Auto Router API').should('exist')
cy.get(getTestSelector('open-settings-dialog-button')).click()
cy.contains('Settings').should('not.exist')
})
it('inputs reset when navigating between pages', () => {
cy.get('#swap-currency-input .token-amount-input').clear().type('0.01')
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
cy.visit('/pool')
cy.visit('/swap')
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
cy.get('#swap-currency-input .token-amount-input').type('0.01').should('have.value', '0.01')
cy.visit('/pool').visit('/swap')
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
})
it('can swap ETH for USDC', () => {
const TOKEN_ADDRESS = USDC_MAINNET.address
const BALANCE_INCREMENT = 1
cy.hardhat().then((hardhat) => {
cy.then(() => hardhat.getBalance(hardhat.wallet.address, USDC_MAINNET))
.then((balance) => Number(balance.toFixed(1)))
.then((initialBalance) => {
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.get(getTestSelector('token-search-input')).clear().type(TOKEN_ADDRESS)
cy.contains('USDC').click()
cy.get('#swap-currency-output .token-amount-input').clear().type(BALANCE_INCREMENT.toString())
cy.get('#swap-currency-input .token-amount-input').should('not.equal', '')
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.get(getTestSelector('dismiss-tx-confirmation')).click()
it('swaps ETH for USDC', () => {
cy.visit('/swap', { ethereum: 'hardhat' })
cy.hardhat({ automine: false })
getBalance(USDC_MAINNET).then((initialBalance) => {
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.get(getTestSelector('token-search-input')).clear().type(USDC_MAINNET.address)
cy.contains('USDC').click()
cy.get('#swap-currency-output .token-amount-input').clear().type('1').should('have.value', '1')
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.get(getTestSelector('dismiss-tx-confirmation')).click()
// ui check
cy.get('#swap-currency-output [data-testid="balance-text"]').should(
'have.text',
`Balance: ${initialBalance + BALANCE_INCREMENT}`
)
// The pending transaction indicator should reflect the state.
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')
// chain state check
cy.then(() => hardhat.getBalance(hardhat.wallet.address, USDC_MAINNET))
.then((balance) => Number(balance.toFixed(1)))
.should('eq', initialBalance + BALANCE_INCREMENT)
})
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
// cy.get(getTestSelector('transaction-popup')).contains('Swapped')
// 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)
})
})
})

View File

@@ -1,30 +1,31 @@
import { CurrencyAmount, SupportedChainId, WETH9 } from '@uniswap/sdk-core'
import { getTestSelector } from '../../utils'
import { getBalance, getTestSelector } from '../../utils'
const WETH = WETH9[SupportedChainId.MAINNET]
function getWethBalance() {
return cy
.hardhat()
.then((hardhat) => hardhat.getBalance(hardhat.wallet, WETH))
.then((balance) => Number(balance.toFixed(1)))
}
describe('Swap', () => {
describe('Swap wrap', () => {
beforeEach(() => {
cy.visit('/swap', { ethereum: 'hardhat' }).hardhat({ automine: false })
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${WETH.address}`, { ethereum: 'hardhat' }).hardhat({
automine: false,
})
})
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-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')
cy.get('#swap-currency-input .token-amount-input').should('have.value', '0.02')
})
it('should be able to wrap ETH', () => {
getWethBalance().then((initialWethBalance) => {
// Select WETH for the token output.
cy.get('#swap-currency-output').contains('Select token').click()
cy.contains('WETH').click()
getBalance(WETH).then((initialWethBalance) => {
cy.contains('Enter ETH amount')
// Enter the amount to wrap.
cy.get('#swap-currency-output .token-amount-input').click().type('1')
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.
cy.get('#swap-currency-input .token-amount-input').should('have.value', 1)
// Click the wrap button.
@@ -35,15 +36,15 @@ describe('Swap', () => {
cy.hardhat().then((hardhat) => hardhat.mine())
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
// There should be a successful wrap notification.
cy.get(getTestSelector('transaction-popup')).contains('Wrapped')
cy.get(getTestSelector('transaction-popup')).contains('1.00 ETH for 1.00 WETH')
// 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}`)
// The user's WETH account balance should have increased
getWethBalance().should('equal', initialWethBalance + 1)
getBalance(WETH).should('equal', initialWethBalance + 1)
})
})
@@ -53,17 +54,14 @@ describe('Swap', () => {
await hardhat.mine()
})
getWethBalance().then((initialWethBalance) => {
// Select WETH for the token output.
cy.get('#swap-currency-output').contains('Select token').click()
cy.contains('WETH').click()
getBalance(WETH).then((initialWethBalance) => {
// 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')
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.
cy.get('#swap-currency-input .token-amount-input').should('have.value', 1)
// Click the unwrap button.
@@ -74,15 +72,15 @@ describe('Swap', () => {
cy.hardhat().then((hardhat) => hardhat.mine())
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
// There should be a successful wrap notification.
cy.get(getTestSelector('transaction-popup')).contains('Unwrapped')
cy.get(getTestSelector('transaction-popup')).contains('1.00 WETH for 1.00 ETH')
// 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}`)
// The user's WETH account balance should have increased
getWethBalance().should('equal', initialWethBalance - 1)
getBalance(WETH).should('equal', initialWethBalance - 1)
})
})
})

View File

@@ -1,3 +1,13 @@
import { Currency } from '@uniswap/sdk-core'
export const getTestSelector = (selectorId: string) => `[data-testid=${selectorId}]`
export const getTestSelectorStartsWith = (selectorId: string) => `[data-testid^=${selectorId}]`
/** Gets the balance of a token as a Chainable. */
export function getBalance(token: Currency) {
return cy
.hardhat()
.then((hardhat) => hardhat.getBalance(hardhat.wallet, token))
.then((balance) => Number(balance.toFixed(1)))
}

View File

@@ -1,51 +1,3 @@
import { default as babelExtractor } from '@lingui/cli/api/extractors/babel'
import { createHash } from 'crypto'
import { mkdirSync, readFileSync, writeFileSync } from 'fs'
import * as path from 'path'
import * as pkgUp from 'pkg-up' // pkg-up is used by lingui, and is used here to match lingui's own extractors
/**
* A custom caching extractor for CI.
* Falls back to the babelExtractor in a non-CI (ie local) environment.
* Caches a file's latest extracted content's hash, and skips re-extracting if it is already present in the cache.
* In CI, re-extracting files takes over one minute, so this is a significant savings.
*/
const cachingExtractor: typeof babelExtractor = {
match(filename: string) {
return babelExtractor.match(filename)
},
extract(filename: string, code: string, ...options: unknown[]) {
if (!process.env.CI) return babelExtractor.extract(filename, code, ...options)
// This runs from node_modules/@lingui/conf, so we need to back out to the root.
const pkg = pkgUp.sync()
if (!pkg) throw new Error('No root found')
const root = path.dirname(pkg)
const filePath = path.join(root, filename)
const file = readFileSync(filePath)
const hash = createHash('sha256').update(file).digest('hex')
const cacheRoot = path.join(root, 'node_modules/.cache/lingui')
mkdirSync(cacheRoot, { recursive: true })
const cachePath = path.join(cacheRoot, filename.replace(/\//g, '-'))
// Only read from the cache if we're not performing a "clean" run, as a clean run must re-extract from all
// files to ensure that obsolete messages are removed.
if (!process.argv.includes('--clean')) {
try {
const cache = readFileSync(cachePath, 'utf8')
if (cache === hash) return
} catch (e) {
// It should not be considered an error if there is no cache file.
}
}
writeFileSync(cachePath, hash)
return babelExtractor.extract(filename, code, ...options)
},
}
const linguiConfig = {
catalogs: [
{
@@ -108,7 +60,6 @@ const linguiConfig = {
runtimeConfigModule: ['@lingui/core', 'i18n'],
sourceLocale: 'en-US',
pseudoLocale: 'pseudo',
extractors: [cachingExtractor],
}
export default linguiConfig

View File

@@ -18,7 +18,7 @@
"i18n:pseudo": "lingui extract --locale pseudo",
"i18n:compile": "lingui compile",
"i18n": "yarn i18n:extract --clean && yarn i18n:compile",
"prepare": "yarn ajv && yarn contracts && yarn graphql && yarn i18n",
"prepare": "concurrently \"npm:ajv\" \"npm:contracts\" \"npm:graphql\" \"npm:i18n\"",
"start": "craco start",
"build": "craco build",
"build:e2e": "REACT_APP_CSP_ALLOW_UNSAFE_EVAL=true REACT_APP_ADD_COVERAGE_INSTRUMENTATION=true craco build",
@@ -105,6 +105,7 @@
"@vanilla-extract/webpack-plugin": "^2.1.11",
"babel-plugin-istanbul": "^6.1.1",
"buffer": "^6.0.3",
"concurrently": "^8.0.1",
"cypress": "12.12.0",
"cypress-hardhat": "^2.3.0",
"env-cmd": "^10.1.0",

View File

@@ -10,14 +10,14 @@ export enum CardType {
Secondary = 'Secondary',
}
const StyledCard = styled.div<{ isDarkMode: boolean; backgroundImgSrc?: string; type: CardType }>`
const StyledCard = styled.div<{ $isDarkMode: boolean; $backgroundImgSrc?: string; $type: CardType }>`
display: flex;
background: ${({ isDarkMode, backgroundImgSrc, type, theme }) =>
isDarkMode
? `${type === CardType.Primary ? theme.backgroundModule : theme.backgroundSurface} ${
backgroundImgSrc ? ` url(${backgroundImgSrc})` : ''
background: ${({ $isDarkMode, $backgroundImgSrc, $type, theme }) =>
$isDarkMode
? `${$type === CardType.Primary ? theme.backgroundModule : theme.backgroundSurface} ${
$backgroundImgSrc ? ` url(${$backgroundImgSrc})` : ''
}`
: `${type === CardType.Primary ? 'white' : theme.backgroundModule} url(${backgroundImgSrc})`};
: `${$type === CardType.Primary ? 'white' : theme.backgroundModule} url(${$backgroundImgSrc})`};
background-size: auto 100%;
background-position: right;
background-repeat: no-repeat;
@@ -30,15 +30,15 @@ const StyledCard = styled.div<{ isDarkMode: boolean; backgroundImgSrc?: string;
padding: 24px;
height: 212px;
border-radius: 24px;
border: 1px solid ${({ theme, type }) => (type === CardType.Primary ? 'transparent' : theme.backgroundOutline)};
border: 1px solid ${({ theme, $type }) => ($type === CardType.Primary ? 'transparent' : theme.backgroundOutline)};
box-shadow: 0px 10px 24px 0px rgba(51, 53, 72, 0.04);
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} border`};
&:hover {
border: 1px solid ${({ theme, isDarkMode }) => (isDarkMode ? theme.backgroundInteractive : theme.textTertiary)};
border: 1px solid ${({ theme, $isDarkMode }) => ($isDarkMode ? theme.backgroundInteractive : theme.textTertiary)};
}
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
height: ${({ backgroundImgSrc }) => (backgroundImgSrc ? 360 : 260)}px;
height: ${({ $backgroundImgSrc }) => ($backgroundImgSrc ? 360 : 260)}px;
}
@media screen and (min-width: ${BREAKPOINTS.xl}px) {
padding: 32px;
@@ -125,14 +125,14 @@ const Card = ({
return (
<TraceEvent events={[BrowserEvent.onClick]} name={SharedEventName.ELEMENT_CLICKED} element={elementName}>
<StyledCard
type={type}
as={external ? 'a' : Link}
to={external ? undefined : to}
href={external ? to : undefined}
target={external ? '_blank' : undefined}
rel={external ? 'noopenener noreferrer' : undefined}
isDarkMode={isDarkMode}
backgroundImgSrc={backgroundImgSrc}
$backgroundImgSrc={backgroundImgSrc}
$isDarkMode={isDarkMode}
$type={type}
>
<TitleRow>
<CardTitle>{title}</CardTitle>

View File

@@ -359,6 +359,14 @@ export const UNI: { [chainId: number]: Token } = {
[SupportedChainId.GOERLI]: new Token(SupportedChainId.GOERLI, UNI_ADDRESS[5], 18, 'UNI', 'Uniswap'),
}
export const ARB = new Token(
SupportedChainId.ARBITRUM_ONE,
'0x912CE59144191C1204E64559FE8253a0e49E6548',
18,
'ARB',
'Arbitrum'
)
export const WRAPPED_NATIVE_CURRENCY: { [chainId: number]: Token | undefined } = {
...(WETH9 as Record<SupportedChainId, Token>),
[SupportedChainId.OPTIMISM]: new Token(

View File

@@ -1,6 +1,7 @@
import { WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import { ARB, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import gql from 'graphql-tag'
import { useMemo } from 'react'
import invariant from 'tiny-invariant'
import { Chain, SearchTokensQuery, useSearchTokensQuery } from './__generated__/types-and-hooks'
import { chainIdToBackendName } from './util'
@@ -41,19 +42,30 @@ gql`
}
`
const ARB_ADDRESS = ARB.address.toLowerCase()
export type SearchToken = NonNullable<NonNullable<SearchTokensQuery['searchTokens']>[number]>
function isMoreRevelantToken(current: SearchToken, existing: SearchToken | undefined, searchChain: Chain) {
if (!existing) return true
/* Returns the more relevant cross-chain token based on native status and search chain */
function dedupeCrosschainTokens(current: SearchToken, existing: SearchToken | undefined, searchChain: Chain) {
if (!existing) return current
invariant(current.project?.id === existing.project?.id, 'Cannot dedupe tokens within different tokenProjects')
// Always priotize natives, and if both tokens are native, prefer native on current chain (i.e. Matic on Polygon over Matic on Mainnet )
if (current.standard === 'NATIVE' && (existing.standard !== 'NATIVE' || current.chain === searchChain)) return true
// Special case: always prefer Arbitrum ARB over Mainnet ARB
if (current.address?.toLowerCase() === ARB_ADDRESS) return current
if (existing.address?.toLowerCase() === ARB_ADDRESS) return existing
// Always prioritize natives, and if both tokens are native, prefer native on current chain (i.e. Matic on Polygon over Matic on Mainnet )
if (current.standard === 'NATIVE' && (existing.standard !== 'NATIVE' || current.chain === searchChain)) return current
// Prefer tokens on the searched chain, otherwise prefer mainnet tokens
return current.chain === searchChain || (existing.chain !== searchChain && current.chain === Chain.Ethereum)
if (current.chain === searchChain || (existing.chain !== searchChain && current.chain === Chain.Ethereum))
return current
return existing
}
// Places natives first, wrapped native on current chain next, then sorts by volume
/* Places natives first, wrapped native on current chain next, then sorts by volume */
function searchTokenSortFunction(
searchChain: Chain,
wrappedNativeAddress: string | undefined,
@@ -87,7 +99,7 @@ export function useSearchTokens(searchQuery: string, chainId: number) {
data?.searchTokens?.forEach((token) => {
if (token.project?.id) {
const existing = selectionMap[token.project.id]
if (isMoreRevelantToken(token, existing, searchChain)) selectionMap[token.project.id] = token
selectionMap[token.project.id] = dedupeCrosschainTokens(token, existing, searchChain)
}
})
return Object.values(selectionMap).sort(

View File

@@ -61,8 +61,7 @@ export const formatSwapSignedAnalyticsEventProperties = ({
export const formatSwapQuoteReceivedEventProperties = (
trade: Trade<Currency, Currency, TradeType>,
gasUseEstimateUSD?: string,
fetchingSwapQuoteStartTime?: Date
gasUseEstimateUSD?: string
) => {
return {
token_in_symbol: trade.inputAmount.currency.symbol,
@@ -77,8 +76,5 @@ export const formatSwapQuoteReceivedEventProperties = (
: undefined,
token_in_amount: formatToDecimal(trade.inputAmount, trade.inputAmount.currency.decimals),
token_out_amount: formatToDecimal(trade.outputAmount, trade.outputAmount.currency.decimals),
quote_latency_milliseconds: fetchingSwapQuoteStartTime
? getDurationFromDateMilliseconds(fetchingSwapQuoteStartTime)
: undefined,
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,53 @@
import { Trans } from '@lingui/macro'
import { formatNumber } from '@uniswap/conedison/format'
import { ButtonPrimary } from 'components/Button'
import Loader from 'components/Icons/LoadingSpinner'
import { useBuyAssetCallback } from 'nft/hooks/useFetchAssets'
import { GenieAsset } from 'nft/types'
import styled from 'styled-components/macro'
import { OfferButton } from './OfferButton'
import { ButtonStyles } from './shared'
const StyledBuyButton = styled(ButtonPrimary)`
display: flex;
flex-direction: row;
padding: 16px 24px;
gap: 8px;
line-height: 24px;
white-space: nowrap;
${ButtonStyles}
`
const Price = styled.div`
color: ${({ theme }) => theme.accentTextLightSecondary};
`
export const BuyButton = ({ asset, onDataPage }: { asset: GenieAsset; onDataPage?: boolean }) => {
const { fetchAndPurchaseSingleAsset, isLoading: isLoadingRoute } = useBuyAssetCallback()
const price = asset.sellorders?.[0]?.price.value
if (!price) {
return <OfferButton />
}
return (
<>
<StyledBuyButton disabled={isLoadingRoute} onClick={() => fetchAndPurchaseSingleAsset(asset)}>
{isLoadingRoute ? (
<>
<Trans>Fetching Route</Trans>
<Loader size="24px" stroke="white" />
</>
) : (
<>
<Trans>Buy</Trans>
<Price>{formatNumber(price)} ETH</Price>
</>
)}
</StyledBuyButton>
{onDataPage && <OfferButton smallVersion />}
</>
)
}

View File

@@ -1,13 +1,12 @@
import { Trans } from '@lingui/macro'
import { ButtonGray, ButtonPrimary } from 'components/Button'
import Column from 'components/Column'
import Row from 'components/Row'
import { HandHoldingDollarIcon, VerifiedIcon } from 'nft/components/icons'
import { VerifiedIcon } from 'nft/components/icons'
import { GenieAsset } from 'nft/types'
import { formatEth } from 'nft/utils'
import styled, { css } from 'styled-components/macro'
import styled from 'styled-components/macro'
import { BREAKPOINTS, ThemedText } from 'theme'
import { BuyButton } from './BuyButton'
const HeaderContainer = styled(Row)`
gap: 24px;
`
@@ -28,38 +27,7 @@ const AssetText = styled(Column)`
margin-right: auto;
`
const ButtonStyles = css`
width: min-content;
flex-shrink: 0;
border-radius: 16px;
`
const BuyButton = styled(ButtonPrimary)`
display: flex;
flex-direction: row;
padding: 16px 24px;
gap: 8px;
line-height: 24px;
white-space: nowrap;
${ButtonStyles}
`
const Price = styled.div`
color: ${({ theme }) => theme.accentTextLightSecondary};
`
const MakeOfferButtonSmall = styled(ButtonPrimary)`
padding: 16px;
${ButtonStyles}
`
const MakeOfferButtonLarge = styled(ButtonGray)`
white-space: nowrap;
${ButtonStyles}
`
export const DataPageHeader = ({ asset }: { asset: GenieAsset }) => {
const price = asset.sellorders?.[0]?.price.value
return (
<HeaderContainer>
<AssetImage src={asset.imageUrl} />
@@ -73,21 +41,7 @@ export const DataPageHeader = ({ asset }: { asset: GenieAsset }) => {
</ThemedText.HeadlineMedium>
</AssetText>
<Row justifySelf="flex-end" width="min-content" gap="12px">
{price ? (
<>
<BuyButton>
<Trans>Buy</Trans>
<Price>{formatEth(price)} ETH</Price>
</BuyButton>
<MakeOfferButtonSmall>
<HandHoldingDollarIcon />
</MakeOfferButtonSmall>
</>
) : (
<MakeOfferButtonLarge>
<Trans>Make an offer</Trans>
</MakeOfferButtonLarge>
)}
<BuyButton asset={asset} onDataPage />
</Row>
</HeaderContainer>
)

View File

@@ -6,6 +6,7 @@ import { useEffect, useRef } from 'react'
import styled from 'styled-components/macro'
import { BREAKPOINTS } from 'theme'
import { BuyButton } from './BuyButton'
import { InfoChips } from './InfoChips'
import { MediaRenderer } from './MediaRenderer'
@@ -144,6 +145,7 @@ export const LandingPage = ({ asset, collection, setShowDataHeader }: LandingPag
<StyledHeadlineText>{asset.name ?? `${asset.collectionName} #${asset.tokenId}`}</StyledHeadlineText>
</InfoDetailsContainer>
<InfoChips asset={asset} />
<BuyButton asset={asset} />
</InfoContainer>
</LandingPageContainer>
)

View File

@@ -0,0 +1,32 @@
import { Trans } from '@lingui/macro'
import { ButtonGray, ButtonPrimary } from 'components/Button'
import { HandHoldingDollarIcon } from 'nft/components/icons'
import styled from 'styled-components/macro'
import { ButtonStyles } from './shared'
const MakeOfferButtonSmall = styled(ButtonPrimary)`
padding: 16px;
${ButtonStyles}
`
const MakeOfferButtonLarge = styled(ButtonGray)`
white-space: nowrap;
${ButtonStyles}
`
export const OfferButton = ({ smallVersion }: { smallVersion?: boolean }) => {
if (smallVersion) {
return (
<MakeOfferButtonSmall>
<HandHoldingDollarIcon />
</MakeOfferButtonSmall>
)
}
return (
<MakeOfferButtonLarge>
<Trans>Make an offer</Trans>
</MakeOfferButtonLarge>
)
}

View File

@@ -260,6 +260,17 @@ exports[`data page loads with header showing 1`] = `
background-color: #bdc8f3;
}
.c17 {
white-space: nowrap;
width: -webkit-min-content;
width: -moz-min-content;
width: min-content;
-webkit-flex-shrink: 0;
-ms-flex-negative: 0;
flex-shrink: 0;
border-radius: 16px;
}
.c6 {
gap: 24px;
}
@@ -276,17 +287,6 @@ exports[`data page loads with header showing 1`] = `
margin-right: auto;
}
.c17 {
white-space: nowrap;
width: -webkit-min-content;
width: -moz-min-content;
width: min-content;
-webkit-flex-shrink: 0;
-ms-flex-negative: 0;
flex-shrink: 0;
border-radius: 16px;
}
.c0 {
padding: 24px 64px;
height: 100vh;
@@ -776,6 +776,17 @@ exports[`data page loads without header showing 1`] = `
background-color: #bdc8f3;
}
.c17 {
white-space: nowrap;
width: -webkit-min-content;
width: -moz-min-content;
width: min-content;
-webkit-flex-shrink: 0;
-ms-flex-negative: 0;
flex-shrink: 0;
border-radius: 16px;
}
.c6 {
gap: 24px;
}
@@ -792,17 +803,6 @@ exports[`data page loads without header showing 1`] = `
margin-right: auto;
}
.c17 {
white-space: nowrap;
width: -webkit-min-content;
width: -moz-min-content;
width: min-content;
-webkit-flex-shrink: 0;
-ms-flex-negative: 0;
flex-shrink: 0;
border-radius: 16px;
}
.c0 {
padding: 24px 64px;
height: 100vh;

View File

@@ -104,6 +104,20 @@ exports[`Header loads with asset with a sell order 1`] = `
color: #0D111C;
}
.c4 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c12 {
padding: 16px;
width: 100%;
@@ -191,34 +205,15 @@ exports[`Header loads with asset with a sell order 1`] = `
outline: none;
}
.c4 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c2 {
gap: 24px;
}
.c3 {
width: 96px;
height: 96px;
border-radius: 20px;
object-fit: cover;
}
.c5 {
gap: 4px;
margin-right: auto;
.c16 {
padding: 16px;
width: -webkit-min-content;
width: -moz-min-content;
width: min-content;
-webkit-flex-shrink: 0;
-ms-flex-negative: 0;
flex-shrink: 0;
border-radius: 16px;
}
.c14 {
@@ -246,15 +241,20 @@ exports[`Header loads with asset with a sell order 1`] = `
color: #F5F6FCb8;
}
.c16 {
padding: 16px;
width: -webkit-min-content;
width: -moz-min-content;
width: min-content;
-webkit-flex-shrink: 0;
-ms-flex-negative: 0;
flex-shrink: 0;
border-radius: 16px;
.c2 {
gap: 24px;
}
.c3 {
width: 96px;
height: 96px;
border-radius: 20px;
object-fit: cover;
}
.c5 {
gap: 4px;
margin-right: auto;
}
@media screen and (max-width:1024px) {
@@ -315,7 +315,7 @@ exports[`Header loads with asset with a sell order 1`] = `
<div
class="c15"
>
100M ETH
100.00M ETH
</div>
</button>
<button
@@ -443,6 +443,20 @@ exports[`Header loads with asset with no sell orders 1`] = `
color: #0D111C;
}
.c4 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
}
.c12 {
padding: 16px;
width: 100%;
@@ -514,18 +528,15 @@ exports[`Header loads with asset with no sell orders 1`] = `
background-color: #bdc8f3;
}
.c4 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
.c14 {
white-space: nowrap;
width: -webkit-min-content;
width: -moz-min-content;
width: min-content;
-webkit-flex-shrink: 0;
-ms-flex-negative: 0;
flex-shrink: 0;
border-radius: 16px;
}
.c2 {
@@ -544,17 +555,6 @@ exports[`Header loads with asset with no sell orders 1`] = `
margin-right: auto;
}
.c14 {
white-space: nowrap;
width: -webkit-min-content;
width: -moz-min-content;
width: min-content;
-webkit-flex-shrink: 0;
-ms-flex-negative: 0;
flex-shrink: 0;
border-radius: 16px;
}
@media screen and (max-width:1024px) {
.c3 {
display: none;

View File

@@ -8,6 +8,29 @@ exports[`LandingPage renders it correctly 1`] = `
min-width: 0;
}
.c25 {
box-sizing: border-box;
margin: 0;
min-width: 0;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
display: inline-block;
text-align: center;
line-height: inherit;
-webkit-text-decoration: none;
text-decoration: none;
font-size: inherit;
padding-left: 16px;
padding-right: 16px;
padding-top: 8px;
padding-bottom: 8px;
color: white;
background-color: primary;
border: 0;
border-radius: 4px;
}
.c10 {
width: 100%;
display: -webkit-box;
@@ -138,6 +161,88 @@ exports[`LandingPage renders it correctly 1`] = `
align-items: center;
}
.c26 {
padding: 16px;
width: 100%;
font-weight: 500;
text-align: center;
border-radius: 20px;
outline: none;
border: 1px solid transparent;
color: #0D111C;
-webkit-text-decoration: none;
text-decoration: none;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-flex-wrap: nowrap;
-ms-flex-wrap: nowrap;
flex-wrap: nowrap;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
cursor: pointer;
position: relative;
z-index: 1;
will-change: transform;
-webkit-transition: -webkit-transform 450ms ease;
-webkit-transition: transform 450ms ease;
transition: transform 450ms ease;
-webkit-transform: perspective(1px) translateZ(0);
-ms-transform: perspective(1px) translateZ(0);
transform: perspective(1px) translateZ(0);
}
.c26:disabled {
opacity: 50%;
cursor: auto;
pointer-events: none;
}
.c26 > * {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.c26 > a {
-webkit-text-decoration: none;
text-decoration: none;
}
.c27 {
background-color: #F5F6FC;
color: #7780A0;
font-size: 16px;
font-weight: 500;
}
.c27:hover {
background-color: #d2daf7;
}
.c27:active {
background-color: #bdc8f3;
}
.c28 {
white-space: nowrap;
width: -webkit-min-content;
width: -moz-min-content;
width: min-content;
-webkit-flex-shrink: 0;
-ms-flex-negative: 0;
flex-shrink: 0;
border-radius: 16px;
}
.c21 {
background-color: #FFFFFF;
padding: 10px 12px 10px 8px;
@@ -442,6 +547,11 @@ exports[`LandingPage renders it correctly 1`] = `
</div>
</div>
</div>
<button
class="c25 c26 c27 c28"
>
Make an offer
</button>
</div>
</div>
</DocumentFragment>

View File

@@ -34,3 +34,9 @@ export const Scrim = styled.div<{ isBottom?: boolean }>`
`linear-gradient(180deg, ${opacify(0, theme.backgroundSurface)} 0%, ${theme.backgroundSurface} 100%)`};
display: flex;
`
export const ButtonStyles = css`
width: min-content;
flex-shrink: 0;
border-radius: 16px;
`

View File

@@ -1,10 +1,16 @@
import { useWeb3React } from '@web3-react/core'
import { useNftRouteLazyQuery } from 'graphql/data/__generated__/types-and-hooks'
import { BagStatus } from 'nft/types'
import { buildNftTradeInputFromBagItems, recalculateBagUsingPooledAssets } from 'nft/utils'
import { BagStatus, GenieAsset } from 'nft/types'
import {
buildNftTradeInput,
buildNftTradeInputFromBagItems,
filterUpdatedAssetsByState,
recalculateBagUsingPooledAssets,
} from 'nft/utils'
import { getNextBagState, getPurchasableAssets } from 'nft/utils/bag'
import { buildRouteResponse } from 'nft/utils/nftRoute'
import { useCallback, useMemo } from 'react'
import { compareAssetsWithTransactionRoute } from 'nft/utils/txRoute/combineItemsWithTxRoute'
import { useCallback, useMemo, useState } from 'react'
import { shallow } from 'zustand/shallow'
import { useBag } from './useBag'
@@ -100,3 +106,48 @@ export function useFetchAssets(): () => Promise<void> {
tokenTradeInput,
])
}
export const useBuyAssetCallback = () => {
const { account } = useWeb3React()
const [fetchGqlRoute] = useNftRouteLazyQuery()
const purchaseAssets = usePurchaseAssets()
const [isLoading, setIsLoading] = useState(false)
const fetchAndPurchaseSingleAsset = useCallback(
async (asset: GenieAsset) => {
setIsLoading(true)
fetchGqlRoute({
variables: {
senderAddress: account ? account : '',
nftTrades: buildNftTradeInput([asset]),
tokenTrades: undefined,
},
pollInterval: 0,
fetchPolicy: 'no-cache',
onCompleted: (data) => {
setIsLoading(false)
if (!data.nftRoute || !data.nftRoute.route) {
return
}
const { route, routeResponse } = buildRouteResponse(data.nftRoute, false)
const { updatedAssets } = compareAssetsWithTransactionRoute([asset], route)
const { priceChanged, unavailable } = filterUpdatedAssetsByState(updatedAssets)
const invalidData = priceChanged.length > 0 || unavailable.length > 0
if (invalidData) {
return
}
purchaseAssets(routeResponse, updatedAssets, false)
},
})
},
[account, fetchGqlRoute, purchaseAssets]
)
return useMemo(() => ({ fetchAndPurchaseSingleAsset, isLoading }), [fetchAndPurchaseSingleAsset, isLoading])
}

View File

@@ -3,6 +3,7 @@ import { InterfaceEventName } from '@uniswap/analytics-events'
import { CurrencyAmount, SupportedChainId, Token } from '@uniswap/sdk-core'
import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'
import { useWeb3React } from '@web3-react/core'
import { isSupportedChain } from 'constants/chains'
import usePermit2Allowance, { AllowanceState } from 'hooks/usePermit2Allowance'
import { useCallback, useMemo, useState } from 'react'
import invariant from 'tiny-invariant'
@@ -15,7 +16,7 @@ function getURAddress(chainId?: number, nftURAddress?: string) {
return nftURAddress ?? UNIVERSAL_ROUTER_ADDRESS(chainId)
}
return UNIVERSAL_ROUTER_ADDRESS(chainId)
return isSupportedChain(chainId) ? UNIVERSAL_ROUTER_ADDRESS(chainId) : undefined
}
export default function usePermit2Approval(

View File

@@ -1,7 +1,7 @@
import { useWeb3React } from '@web3-react/core'
import { RouteResponse, UpdatedGenieAsset } from 'nft/types'
import { useCallback } from 'react'
import shallow from 'zustand/shallow'
import { shallow } from 'zustand/shallow'
import { useBag } from './useBag'
import { useSendTransaction } from './useSendTransaction'

View File

@@ -23,7 +23,7 @@ export const buildNftTradeInputFromBagItems = (itemsInBag: BagItem[]): NftTradeI
return buildNftTradeInput(assetsToBuy)
}
const buildNftTradeInput = (assets: UpdatedGenieAsset[]): NftTradeInput[] => {
export const buildNftTradeInput = (assets: UpdatedGenieAsset[]): NftTradeInput[] => {
return assets.flatMap((asset) => {
const { id, address, marketplace, priceInfo, tokenId, tokenType } = asset

View File

@@ -177,8 +177,6 @@ export function Swap({
disableTokenInputs?: boolean
}) {
const { account, chainId: connectedChainId, connector } = useWeb3React()
const [newSwapQuoteNeedsLogging, setNewSwapQuoteNeedsLogging] = useState(true)
const [fetchingSwapQuoteStartTime, setFetchingSwapQuoteStartTime] = useState<Date | undefined>()
const trace = useTrace()
// token warning stuff
@@ -417,10 +415,20 @@ export function Swap({
if (stablecoinPriceImpact && !confirmPriceImpactWithoutFee(stablecoinPriceImpact)) {
return
}
setSwapState({ attemptingTxn: true, tradeToConfirm, showConfirm, swapErrorMessage: undefined, txHash: undefined })
setSwapState((currentState) => ({
...currentState,
attemptingTxn: true,
swapErrorMessage: undefined,
txHash: undefined,
}))
swapCallback()
.then((hash) => {
setSwapState({ attemptingTxn: false, tradeToConfirm, showConfirm, swapErrorMessage: undefined, txHash: hash })
setSwapState((currentState) => ({
...currentState,
attemptingTxn: false,
swapErrorMessage: undefined,
txHash: hash,
}))
sendEvent({
category: 'Swap',
action: 'transaction hash',
@@ -440,19 +448,16 @@ export function Swap({
})
})
.catch((error) => {
setSwapState({
setSwapState((currentState) => ({
...currentState,
attemptingTxn: false,
tradeToConfirm,
showConfirm,
swapErrorMessage: error.message,
txHash: undefined,
})
}))
})
}, [
swapCallback,
stablecoinPriceImpact,
tradeToConfirm,
showConfirm,
recipient,
recipientAddress,
account,
@@ -471,16 +476,16 @@ export function Swap({
}, [stablecoinPriceImpact, trade])
const handleConfirmDismiss = useCallback(() => {
setSwapState({ showConfirm: false, tradeToConfirm, attemptingTxn, swapErrorMessage, txHash })
setSwapState((currentState) => ({ ...currentState, showConfirm: false }))
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.INPUT, '')
}
}, [attemptingTxn, onUserInput, swapErrorMessage, tradeToConfirm, txHash])
}, [onUserInput, txHash])
const handleAcceptChanges = useCallback(() => {
setSwapState({ tradeToConfirm: trade, swapErrorMessage, txHash, attemptingTxn, showConfirm })
}, [attemptingTxn, showConfirm, swapErrorMessage, trade, txHash])
setSwapState((currentState) => ({ ...currentState, tradeToConfirm: trade }))
}, [trade])
const handleInputSelect = useCallback(
(inputCurrency: Currency) => {
@@ -519,42 +524,16 @@ export function Swap({
const priceImpactTooHigh = priceImpactSeverity > 3 && !isExpertMode
const showPriceImpactWarning = largerPriceImpact && priceImpactSeverity > 3
// Handle time based logging events and event properties.
const prevTrade = usePrevious(trade)
useEffect(() => {
const now = new Date()
// If a trade exists, and we need to log the receipt of this new swap quote:
if (newSwapQuoteNeedsLogging && !!trade) {
// Set the current datetime as the time of receipt of latest swap quote.
setSwapQuoteReceivedDate(now)
// Log swap quote.
sendAnalyticsEvent(SwapEventName.SWAP_QUOTE_RECEIVED, {
...formatSwapQuoteReceivedEventProperties(
trade,
trade.gasUseEstimateUSD ?? undefined,
fetchingSwapQuoteStartTime
),
...trace,
})
// Latest swap quote has just been logged, so we don't need to log the current trade anymore
// unless user inputs change again and a new trade is in the process of being generated.
setNewSwapQuoteNeedsLogging(false)
// New quote is not being fetched, so set start time of quote fetch to undefined.
setFetchingSwapQuoteStartTime(undefined)
}
// If another swap quote is being loaded based on changed user inputs:
if (routeIsLoading) {
setNewSwapQuoteNeedsLogging(true)
if (!fetchingSwapQuoteStartTime) setFetchingSwapQuoteStartTime(now)
}
}, [
newSwapQuoteNeedsLogging,
routeIsSyncing,
routeIsLoading,
fetchingSwapQuoteStartTime,
trade,
setSwapQuoteReceivedDate,
trace,
])
if (!trade || prevTrade === trade) return // no new swap quote to log
setSwapQuoteReceivedDate(new Date())
sendAnalyticsEvent(SwapEventName.SWAP_QUOTE_RECEIVED, {
...formatSwapQuoteReceivedEventProperties(trade, trade.gasUseEstimateUSD ?? undefined),
...trace,
})
}, [prevTrade, trade, trace])
const showDetailsDropdown = Boolean(
!showWrap && userHasSpecifiedInputOutput && (trade || routeIsLoading || routeIsSyncing)

View File

@@ -1,4 +1,4 @@
import { ErrorEvent, Event } from '@sentry/types'
import { ErrorEvent } from '@sentry/types'
import { beforeSend } from './errors'
@@ -45,58 +45,6 @@ describe('beforeSend', () => {
})
})
describe('chunkResponseStatus', () => {
afterEach(() => {
jest.restoreAllMocks()
})
it('handles when matching JS file not found', () => {
jest.spyOn(window.performance, 'getEntriesByType').mockReturnValue([])
const originalException = new Error(
'Loading chunk 20 failed. (error: https://app.uniswap.org/static/js/20.d55382e0.chunk.js)'
)
expect((beforeSend(ERROR, { originalException }) as Event).tags).toBeUndefined()
})
it('handles when matching CSS file not found', () => {
jest.spyOn(window.performance, 'getEntriesByType').mockReturnValue([])
const originalException = new Error(
'Loading chunk 20 failed. (error: https://app.uniswap.org/static/js/20.d55382e0.chunk.js)'
)
expect((beforeSend(ERROR, { originalException }) as Event).tags).toBeUndefined()
})
it('handles when performance is undefined', () => {
window.performance = undefined as any
const originalException = new Error('Loading CSS chunk 12 failed. (./static/css/12.d5b3cfe3.chunk.css)')
expect((beforeSend(ERROR, { originalException }) as Event).tags).toBeUndefined()
})
it('adds status for a matching JS file', () => {
jest.spyOn(window.performance, 'getEntriesByType').mockReturnValue([
{
name: 'https://app.uniswap.org/static/js/20.d55382e0.chunk.js',
responseStatus: 499,
} as PerformanceEntry,
])
const originalException = new Error(
'Loading chunk 20 failed. (error: https://app.uniswap.org/static/js/20.d55382e0.chunk.js)'
)
expect((beforeSend(ERROR, { originalException }) as Event).tags?.chunkResponseStatus).toBe(499)
})
it('adds status for a matching CSS file', () => {
jest.spyOn(window.performance, 'getEntriesByType').mockReturnValue([
{
name: 'https://app.uniswap.org/static/css/12.d5b3cfe3.chunk.css',
responseStatus: 200,
} as PerformanceEntry,
])
const originalException = new Error('Loading CSS chunk 12 failed. (./static/css/12.d5b3cfe3.chunk.css)')
expect((beforeSend(ERROR, { originalException }) as Event).tags?.chunkResponseStatus).toBe(200)
})
})
it('propagates an error', () => {
expect(beforeSend(ERROR, {})).toBe(ERROR)
})

View File

@@ -18,7 +18,6 @@ export const beforeSend: Required<ClientOptions>['beforeSend'] = (event: ErrorEv
}
updateRequestUrl(event)
addChunkResponseStatusTag(event, hint)
return event
}
@@ -42,39 +41,6 @@ function updateRequestUrl(event: ErrorEvent) {
}
}
// If a request fails due to a chunk error, this looks for that asset in the performance entries.
// If found, it adds a tag to the event with the response status of the chunk request.
function addChunkResponseStatusTag(event: ErrorEvent, hint: EventHint) {
const error = hint.originalException
if (error instanceof Error) {
let asset: string | undefined
if (error.message.match(/Loading chunk \d+ failed\. \(([a-zA-Z]+): .+\.chunk\.js\)/)) {
asset = error.message.match(/https?:\/\/.+?\.chunk\.js/)?.[0]
}
if (error.message.match(/Loading CSS chunk \d+ failed\. \(.+\.chunk\.css\)/)) {
const relativePath = error.message.match(/\/static\/css\/.*\.chunk\.css/)?.[0]
asset = `${window.origin}${relativePath}`
}
if (asset) {
const status = getChunkResponseStatus(asset)
if (status) {
if (!event.tags) {
event.tags = {}
}
event.tags.chunkResponseStatus = status
}
}
}
}
function getChunkResponseStatus(asset?: string): number | undefined {
const entries = [...(performance?.getEntriesByType('resource') ?? [])]
const resource = entries?.find(({ name }) => name === asset)
return resource?.responseStatus
}
function shouldRejectError(error: EventHint['originalException']) {
if (error instanceof Error) {
// ethers aggressively polls for block number, and it sometimes fails (whether spuriously or through rate-limiting).

View File

@@ -1183,7 +1183,7 @@
core-js-pure "^3.16.0"
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.17.2", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.7", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.17.2", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
version "7.21.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.5.tgz#8492dddda9644ae3bda3b45eabe87382caee7200"
integrity sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==
@@ -8160,11 +8160,6 @@ commander@^4.0.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
commander@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==
commander@^6.1.0, commander@^6.2.1:
version "6.2.1"
resolved "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz"
@@ -8228,6 +8223,21 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
concurrently@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-8.0.1.tgz#80c0591920a9fa3e68ba0dd8aa6eac8487eb904c"
integrity sha512-Sh8bGQMEL0TAmAm2meAXMjcASHZa7V0xXQVDBLknCPa9TPtkY9yYs+0cnGGgfdkW0SV1Mlg+hVGfXcoI8d3MJA==
dependencies:
chalk "^4.1.2"
date-fns "^2.29.3"
lodash "^4.17.21"
rxjs "^7.8.0"
shell-quote "^1.8.0"
spawn-command "0.0.2-1"
supports-color "^8.1.1"
tree-kill "^1.2.2"
yargs "^17.7.1"
confusing-browser-globals@^1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81"
@@ -9053,10 +9063,12 @@ dataloader@2.1.0:
resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-2.1.0.tgz#c69c538235e85e7ac6c6c444bae8ecabf5de9df7"
integrity sha512-qTcEYLen3r7ojZNgVUaRggOI+KM7jrKxXeSHhogh/TWxYMeONEMqY+hmkobiYQozsGIyg9OYVzO4ZIfoB4I0pQ==
date-fns@^2.16.1:
version "2.21.3"
resolved "https://registry.npmjs.org/date-fns/-/date-fns-2.21.3.tgz"
integrity sha512-HeYdzCaFflc1i4tGbj7JKMjM4cKGYoyxwcIIkHzNgCkX8xXDNJDZXgDDVchIWpN4eQc3lH37WarduXFZJOtxfw==
date-fns@^2.16.1, date-fns@^2.29.3:
version "2.30.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0"
integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==
dependencies:
"@babel/runtime" "^7.21.0"
dayjs@1.10.7, dayjs@^1.10.4:
version "1.10.7"
@@ -10357,7 +10369,7 @@ event-target-shim@^5.0.0:
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
eventemitter2@6.4.7, eventemitter2@^6.4.3:
eventemitter2@6.4.7:
version "6.4.7"
resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.7.tgz#a7f6c4d7abf28a14c1ef3442f21cb306a054271d"
integrity sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==
@@ -16617,10 +16629,10 @@ rxjs@^6.6.0, rxjs@^6.6.3, rxjs@^6.6.7:
dependencies:
tslib "^1.9.0"
rxjs@^7.5.5:
version "7.6.0"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.6.0.tgz#361da5362b6ddaa691a2de0b4f2d32028f1eb5a2"
integrity sha512-DDa7d8TFNUalGC9VqXvQ1euWNN7sc63TrUCuM9J998+ViviahMIjKSOU7rfcgFOF+FCD71BhDRv4hrFz+ImDLQ==
rxjs@^7.5.5, rxjs@^7.8.0:
version "7.8.1"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543"
integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==
dependencies:
tslib "^2.1.0"
@@ -16947,10 +16959,10 @@ shebang-regex@^3.0.0:
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
shell-quote@^1.7.3:
version "1.7.4"
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.4.tgz#33fe15dee71ab2a81fcbd3a52106c5cfb9fb75d8"
integrity sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==
shell-quote@^1.7.3, shell-quote@^1.8.0:
version "1.8.1"
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680"
integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==
side-channel@^1.0.4:
version "1.0.4"
@@ -17116,6 +17128,11 @@ sourcemap-codec@^1.4.4:
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
spawn-command@0.0.2-1:
version "0.0.2-1"
resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0"
integrity sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==
spawn-wrap@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/spawn-wrap/-/spawn-wrap-2.0.0.tgz#103685b8b8f9b79771318827aa78650a610d457e"
@@ -17907,6 +17924,11 @@ tr46@~0.0.3:
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
tree-kill@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
treeify@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/treeify/-/treeify-1.1.0.tgz#4e31c6a463accd0943879f30667c4fdaff411bb8"
@@ -19295,10 +19317,10 @@ yargs@^15.0.2, yargs@^15.3.1:
y18n "^4.0.0"
yargs-parser "^18.1.2"
yargs@^17.0.0:
version "17.6.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.2.tgz#2e23f2944e976339a1ee00f18c77fedee8332541"
integrity sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==
yargs@^17.0.0, yargs@^17.7.1:
version "17.7.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
dependencies:
cliui "^8.0.1"
escalade "^3.1.1"