Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd4042aa16 | ||
|
|
1dcafd2f2d | ||
|
|
66fcdb4465 | ||
|
|
e398e8b950 | ||
|
|
6424fdfbcd | ||
|
|
95814e3271 | ||
|
|
caa2524e27 | ||
|
|
d28a4b34cd | ||
|
|
f3a80c6272 | ||
|
|
b89ee36448 | ||
|
|
fbc55db937 | ||
|
|
835c62acfa | ||
|
|
8fe7c7a0a7 |
10
.github/actions/setup/action.yml
vendored
10
.github/actions/setup/action.yml
vendored
@@ -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
|
||||
|
||||
|
||||
15
.github/pull_request_template.md
vendored
15
.github/pull_request_template.md
vendored
@@ -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
|
||||
|
||||
30
.github/workflows/test.yml
vendored
30
.github/workflows/test.yml
vendored
@@ -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 }}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
14
cypress/e2e/swap/settings.test.ts
Normal file
14
cypress/e2e/swap/settings.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
1656
src/locales/af-ZA.po
1656
src/locales/af-ZA.po
File diff suppressed because it is too large
Load Diff
1662
src/locales/ar-SA.po
1662
src/locales/ar-SA.po
File diff suppressed because it is too large
Load Diff
1676
src/locales/ca-ES.po
1676
src/locales/ca-ES.po
File diff suppressed because it is too large
Load Diff
1650
src/locales/cs-CZ.po
1650
src/locales/cs-CZ.po
File diff suppressed because it is too large
Load Diff
1666
src/locales/da-DK.po
1666
src/locales/da-DK.po
File diff suppressed because it is too large
Load Diff
1626
src/locales/de-DE.po
1626
src/locales/de-DE.po
File diff suppressed because it is too large
Load Diff
1670
src/locales/el-GR.po
1670
src/locales/el-GR.po
File diff suppressed because it is too large
Load Diff
1674
src/locales/es-ES.po
1674
src/locales/es-ES.po
File diff suppressed because it is too large
Load Diff
1656
src/locales/fi-FI.po
1656
src/locales/fi-FI.po
File diff suppressed because it is too large
Load Diff
1373
src/locales/fr-FR.po
1373
src/locales/fr-FR.po
File diff suppressed because it is too large
Load Diff
1668
src/locales/he-IL.po
1668
src/locales/he-IL.po
File diff suppressed because it is too large
Load Diff
1681
src/locales/hu-HU.po
1681
src/locales/hu-HU.po
File diff suppressed because it is too large
Load Diff
1550
src/locales/id-ID.po
1550
src/locales/id-ID.po
File diff suppressed because it is too large
Load Diff
1664
src/locales/it-IT.po
1664
src/locales/it-IT.po
File diff suppressed because it is too large
Load Diff
1350
src/locales/ja-JP.po
1350
src/locales/ja-JP.po
File diff suppressed because it is too large
Load Diff
1650
src/locales/ko-KR.po
1650
src/locales/ko-KR.po
File diff suppressed because it is too large
Load Diff
1674
src/locales/nl-NL.po
1674
src/locales/nl-NL.po
File diff suppressed because it is too large
Load Diff
1662
src/locales/no-NO.po
1662
src/locales/no-NO.po
File diff suppressed because it is too large
Load Diff
2120
src/locales/pl-PL.po
2120
src/locales/pl-PL.po
File diff suppressed because it is too large
Load Diff
1684
src/locales/pt-BR.po
1684
src/locales/pt-BR.po
File diff suppressed because it is too large
Load Diff
1684
src/locales/pt-PT.po
1684
src/locales/pt-PT.po
File diff suppressed because it is too large
Load Diff
1674
src/locales/ro-RO.po
1674
src/locales/ro-RO.po
File diff suppressed because it is too large
Load Diff
1408
src/locales/ru-RU.po
1408
src/locales/ru-RU.po
File diff suppressed because it is too large
Load Diff
1452
src/locales/sl-SI.po
1452
src/locales/sl-SI.po
File diff suppressed because it is too large
Load Diff
1656
src/locales/sr-SP.po
1656
src/locales/sr-SP.po
File diff suppressed because it is too large
Load Diff
1666
src/locales/sv-SE.po
1666
src/locales/sv-SE.po
File diff suppressed because it is too large
Load Diff
1145
src/locales/sw-TZ.po
1145
src/locales/sw-TZ.po
File diff suppressed because it is too large
Load Diff
2154
src/locales/th-TH.po
2154
src/locales/th-TH.po
File diff suppressed because it is too large
Load Diff
1668
src/locales/tr-TR.po
1668
src/locales/tr-TR.po
File diff suppressed because it is too large
Load Diff
1670
src/locales/uk-UA.po
1670
src/locales/uk-UA.po
File diff suppressed because it is too large
Load Diff
1676
src/locales/vi-VN.po
1676
src/locales/vi-VN.po
File diff suppressed because it is too large
Load Diff
1494
src/locales/zh-CN.po
1494
src/locales/zh-CN.po
File diff suppressed because it is too large
Load Diff
1492
src/locales/zh-TW.po
1492
src/locales/zh-TW.po
File diff suppressed because it is too large
Load Diff
53
src/nft/components/details/detailsV2/BuyButton.tsx
Normal file
53
src/nft/components/details/detailsV2/BuyButton.tsx
Normal 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 />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
32
src/nft/components/details/detailsV2/OfferButton.tsx
Normal file
32
src/nft/components/details/detailsV2/OfferButton.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
`
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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).
|
||||
|
||||
68
yarn.lock
68
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user