Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd4042aa16 | ||
|
|
1dcafd2f2d | ||
|
|
66fcdb4465 | ||
|
|
e398e8b950 | ||
|
|
6424fdfbcd | ||
|
|
95814e3271 | ||
|
|
caa2524e27 | ||
|
|
d28a4b34cd | ||
|
|
f3a80c6272 | ||
|
|
b89ee36448 | ||
|
|
fbc55db937 | ||
|
|
835c62acfa | ||
|
|
8fe7c7a0a7 | ||
|
|
41113e6e41 | ||
|
|
58b25d29a9 | ||
|
|
a2db3e2719 | ||
|
|
b62f9066a7 | ||
|
|
258f22e037 | ||
|
|
38b306a80f | ||
|
|
9050f09bfe | ||
|
|
77d46c361a | ||
|
|
4fb48bdd1f | ||
|
|
cf2b6bf568 | ||
|
|
03095f4e48 | ||
|
|
b2966f8d29 | ||
|
|
ef6d1f20ed | ||
|
|
10b156ff2b | ||
|
|
146c5f29cf | ||
|
|
66a3475bf6 | ||
|
|
f6c393b016 | ||
|
|
15f8d34320 | ||
|
|
504e09d3dc | ||
|
|
1f755e8b0d | ||
|
|
f45a7f921b | ||
|
|
29db61ff90 | ||
|
|
8431ad9161 | ||
|
|
fd1aded517 | ||
|
|
27ad7cbd41 | ||
|
|
01e5de436a | ||
|
|
fd5aa1b51e | ||
|
|
a6e1a7e6d9 | ||
|
|
629fe2c144 | ||
|
|
d73763ce75 | ||
|
|
fe6df38997 | ||
|
|
719ee0f5b5 | ||
|
|
75bdf9a8d4 | ||
|
|
efbe3994bb | ||
|
|
93fe8e4349 | ||
|
|
6062f615a0 | ||
|
|
42e3af7b5c | ||
|
|
57274a800d | ||
|
|
5e591455b3 |
@@ -2,13 +2,18 @@
|
||||
|
||||
require('@uniswap/eslint-config/load')
|
||||
|
||||
const rulesDirPlugin = require('eslint-plugin-rulesdir')
|
||||
rulesDirPlugin.RULES_DIR = 'eslint_rules'
|
||||
|
||||
module.exports = {
|
||||
extends: '@uniswap/eslint-config/react',
|
||||
extends: ['@uniswap/eslint-config/react'],
|
||||
plugins: ['rulesdir'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*'],
|
||||
rules: {
|
||||
'multiline-comment-style': ['error', 'separate-lines'],
|
||||
'rulesdir/enforce-retry-on-import': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
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
|
||||
|
||||
26
.github/workflows/2-deploy-to-staging.yml
vendored
26
.github/workflows/2-deploy-to-staging.yml
vendored
@@ -10,6 +10,17 @@ jobs:
|
||||
environment:
|
||||
name: deploy/staging
|
||||
steps:
|
||||
- name: Send Slack message that deploy is starting
|
||||
uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
|
||||
continue-on-error: true
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "Staging deploy started for branch: ${{ github.ref_name }}"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- run: yarn prepare
|
||||
@@ -21,6 +32,7 @@ jobs:
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Update Cloudflare Pages deployment
|
||||
id: pages-deployment
|
||||
uses: cloudflare/pages-action@364c7ca09a4b57837c5967871d64a2c31adb8c0d
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
@@ -28,6 +40,20 @@ jobs:
|
||||
projectName: interface-staging
|
||||
directory: build
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Cloudflare uses `main` as the default production branch, so we push using the `main` branch so that it can be aliased by a custom domain.
|
||||
branch: main
|
||||
- name: Send Slack message about deployment outcome
|
||||
uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
|
||||
continue-on-error: true
|
||||
if: always()
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "Staging deploy **${{ steps.pages-deployment.outcome }}** for: ${{ github.ref_name }}"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
- name: Upload source maps to Sentry
|
||||
uses: getsentry/action-release@bd5f874fcda966ba48139b0140fb3ec0cb3aabdd
|
||||
continue-on-error: true
|
||||
|
||||
27
.github/workflows/4-deploy-to-prod.yml
vendored
27
.github/workflows/4-deploy-to-prod.yml
vendored
@@ -10,13 +10,24 @@ jobs:
|
||||
environment:
|
||||
name: deploy/prod
|
||||
steps:
|
||||
- name: Send Slack message that build is starting
|
||||
uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
|
||||
continue-on-error: true
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "Production deploy started for branch: ${{ github.ref_name }}"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- run: yarn prepare
|
||||
- run: yarn build
|
||||
- name: Bump and tag
|
||||
id: github-tag-action
|
||||
uses: mathieudutour/github-tag-action@v6.0
|
||||
uses: mathieudutour/github-tag-action@d745f2e74aaf1ee82e747b181f7a0967978abee0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
release_branches: releases/prod
|
||||
@@ -70,14 +81,28 @@ jobs:
|
||||
|
||||
- name: Update Cloudflare Pages deployment
|
||||
uses: cloudflare/pages-action@364c7ca09a4b57837c5967871d64a2c31adb8c0d
|
||||
id: pages-deployment
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
projectName: ${{ secrets.CLOUDFLARE_PROJECT_NAME }}
|
||||
directory: build
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Cloudflare uses `main` as the default production branch, so we push using the `main` branch so that it can be aliased by a custom domain.
|
||||
branch: main
|
||||
|
||||
- name: Send Slack message about deployment outcome
|
||||
uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
|
||||
continue-on-error: true
|
||||
if: always()
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "Production deploy **${{ steps.pages-deployment.outcome }}** for: ${{ github.ref_name }}"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
- name: Upload source maps to Sentry
|
||||
uses: getsentry/action-release@4744f6a65149f441c5f396d5b0877307c0db52c7
|
||||
continue-on-error: true
|
||||
|
||||
@@ -11,8 +11,10 @@ name: Slack notifications for releases/* merges
|
||||
# | sed 's/"//g' | sed 's/\\t/;/g' | sed 's/\\n/;/g' | sed 's/\\//g' \
|
||||
# We then use awk to format the TSV into a Slack message
|
||||
# | awk -F';' '{print "• <"$1"|"$2"> (<https://github.com/"$3"|"$3">, "$4") - "$5}' \
|
||||
# Finally, we need to deal with some escaping issues with newlines so that we don't break the Slack message format
|
||||
# We need to deal with some escaping issues with newlines so that we don't break the Slack message format
|
||||
# | sed 's/$/\\n/g' | tr -d '\n' \
|
||||
# Finally we have to truncate the message to 3,000 characters max, otherwise Slack will reject it
|
||||
# | awk '{print substr($0,0,3000);}' \
|
||||
# Then shove the bytes into a file to store them in their exact format
|
||||
# > /tmp/parsed_github_context
|
||||
|
||||
@@ -25,6 +27,8 @@ jobs:
|
||||
notify-slack:
|
||||
name: 'Emit Slack notification(s)'
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: notify/releases
|
||||
steps:
|
||||
- name: Parse event to slug
|
||||
id: parse-slug
|
||||
@@ -38,6 +42,7 @@ jobs:
|
||||
| sed 's/"//g' | sed 's/\\t/;/g' | sed 's/\\n/;/g' | sed 's/\\//g' \
|
||||
| awk -F';' '{print "• <"$1"|"$2"> (<https://github.com/"$3"|"$3">, "$4") - "$5}' \
|
||||
| sed 's/$/\\n/g' | tr -d '\n' \
|
||||
| awk '{print substr($0,0,3000);}' \
|
||||
> /tmp/parsed_github_context
|
||||
echo "SLACK_COMMITS=$(cat /tmp/parsed_github_context)" >> "$GITHUB_OUTPUT"
|
||||
- name: Send custom JSON data to Slack workflow
|
||||
|
||||
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -1,7 +1,7 @@
|
||||
name: Release
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 12 * * 1-4' # every day 12:00 UTC Monday-Thursday
|
||||
- cron: '0 16 * * 1-4' # every day 16:00 UTC Monday-Thursday
|
||||
# manual trigger
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
47
.github/workflows/test.yml
vendored
47
.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
|
||||
@@ -127,7 +137,7 @@ jobs:
|
||||
id: cypress-cache
|
||||
with:
|
||||
path: /root/.cache/Cypress
|
||||
key: ${{ runner.os }}-cypress
|
||||
key: ${{ runner.os }}-cypress-${{ hashFiles('**/node_modules/cypress/package.json') }}
|
||||
- run: |
|
||||
yarn cypress install
|
||||
yarn cypress info
|
||||
@@ -140,14 +150,25 @@ jobs:
|
||||
- uses: cypress-io/github-action@v4
|
||||
with:
|
||||
install: false
|
||||
record: true
|
||||
parallel: true
|
||||
start: yarn serve
|
||||
wait-on: 'http://localhost:3000'
|
||||
browser: chrome
|
||||
record: true
|
||||
parallel: true
|
||||
group: e2e
|
||||
env:
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
COMMIT_INFO_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }}
|
||||
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title || github.event.head_commit.message }}
|
||||
COMMIT_INFO_AUTHOR: ${{ github.event.sender.login || github.event.head_commit.author.login }}
|
||||
# Cypress requires an email for filtering by author, but GitHub does not expose one.
|
||||
# GitHub's public profile email can be deterministically produced from user id/login.
|
||||
COMMIT_INFO_EMAIL: ${{ github.event.sender.id || github.event.head_commit.author.id }}+${{ github.event.sender.login || github.event.head_commit.author.login }}@users.noreply.github.com
|
||||
COMMIT_INFO_SHA: ${{ github.event.pull_request.head.sha || github.event.head_commit.sha }}
|
||||
COMMIT_INFO_TIMESTAMP: ${{ github.event.pull_request.updated_at || github.event.head_commit.timestamp }}
|
||||
CYPRESS_PULL_REQUEST_ID: ${{ github.event.pull_request.number }}
|
||||
CYPRESS_PULL_REQUEST_URL: ${{ github.event.pull_request.html_url }}
|
||||
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
@@ -156,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 }}
|
||||
|
||||
@@ -84,8 +84,11 @@ module.exports = {
|
||||
},
|
||||
webpack: {
|
||||
plugins: [
|
||||
// react-markdown requires path to be global, and Webpack 5 does polyfill node globals, so we polyfill it.
|
||||
new ProvidePlugin({ process: 'process/browser' }),
|
||||
// Webpack 5 does not polyfill node globals, so we do so for those necessary:
|
||||
new ProvidePlugin({
|
||||
// - react-markdown requires process.cwd
|
||||
process: 'process/browser',
|
||||
}),
|
||||
// vanilla-extract has poor performance on M1 machines with 'debug' identifiers, so we use 'short' instead.
|
||||
// See https://vanilla-extract.style/documentation/integrations/webpack/#identifiers for docs.
|
||||
// See https://github.com/vanilla-extract-css/vanilla-extract/issues/771#issuecomment-1249524366.
|
||||
@@ -140,8 +143,11 @@ module.exports = {
|
||||
|
||||
return plugin
|
||||
}),
|
||||
// react-markdown requires path to be importable, and Webpack 5 does resolve node globals, so we resolve it.
|
||||
fallback: { path: require.resolve('path-browserify') },
|
||||
// Webpack 5 does not resolve node modules, so we do so for those necessary:
|
||||
fallback: {
|
||||
// - react-markdown requires path
|
||||
path: require.resolve('path-browserify'),
|
||||
},
|
||||
})
|
||||
|
||||
// Configure webpack transpilation (create-react-app specifies transpilation rules in a oneOf):
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -21,6 +21,7 @@ describe('Landing Page', () => {
|
||||
})
|
||||
|
||||
it('shows landing page when the unicorn icon in nav is selected', () => {
|
||||
cy.visit('/swap')
|
||||
cy.get(getTestSelector('uniswap-logo')).click()
|
||||
cy.get(getTestSelector('landing-page'))
|
||||
})
|
||||
|
||||
58
cypress/e2e/mini-portfolio/accounts.test.ts
Normal file
58
cypress/e2e/mini-portfolio/accounts.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { getTestSelector } from '../../utils'
|
||||
|
||||
describe('Mini Portfolio account drawer', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept(/api.uniswap.org\/v1\/graphql/, cy.spy().as('gqlSpy'))
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
})
|
||||
|
||||
it('fetches balances when account button is first hovered', () => {
|
||||
// The balances should not be fetched before the account button is hovered
|
||||
cy.get('@gqlSpy').should('not.have.been.called')
|
||||
|
||||
// Balances should have been fetched once after hover
|
||||
cy.get(getTestSelector('web3-status-connected')).trigger('mouseover')
|
||||
cy.get('@gqlSpy').should('have.been.calledOnce')
|
||||
|
||||
// Balances should not be refetched upon second hover
|
||||
cy.get(getTestSelector('web3-status-connected')).trigger('mouseover')
|
||||
cy.get('@gqlSpy').should('have.been.calledOnce')
|
||||
|
||||
// Balances should not be refetched upon opening drawer
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get('@gqlSpy').should('have.been.calledOnce')
|
||||
|
||||
// Balances should not be refetched upon closing & reopening drawer
|
||||
cy.get(getTestSelector('close-account-drawer')).click()
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get('@gqlSpy').should('have.been.calledOnce')
|
||||
})
|
||||
|
||||
it('refetches balances when account changes', () => {
|
||||
cy.hardhat().then((hardhat) => {
|
||||
const accountA = hardhat.wallets[0].address
|
||||
const accountB = hardhat.wallets[1].address
|
||||
|
||||
// Opens the account drawer
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
|
||||
// A shortened version of the first account's address should be shown
|
||||
cy.contains(accountA.slice(0, 6)).should('exist')
|
||||
|
||||
// Stores the current portfolio balance to later compare to next account's balance
|
||||
cy.get(getTestSelector('portfolio-total-balance'))
|
||||
.invoke('text')
|
||||
.then((originalBalance) => {
|
||||
// TODO(INFRA-3) Replace window.ethereum access below with cypress-hardhat utility
|
||||
// Simulates the wallet changing accounts via eip-1193 event
|
||||
cy.window().then((win) => win.ethereum.emit('accountsChanged', [accountB]))
|
||||
|
||||
// The second account's address should now be shown
|
||||
cy.contains(accountB.slice(0, 6)).should('exist')
|
||||
|
||||
// The second account's portfolio balance should differ from the original balance
|
||||
cy.get(getTestSelector('portfolio-total-balance')).should('not.have.text', originalBalance)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,24 +1,12 @@
|
||||
import { USDC_MAINNET } from '../../../src/constants/tokens'
|
||||
import { getTestSelector } from '../../utils'
|
||||
|
||||
describe('mini-portfolio activity history', () => {
|
||||
afterEach(() => {
|
||||
cy.intercept(
|
||||
{
|
||||
method: 'POST',
|
||||
url: 'https://beta.api.uniswap.org/v1/graphql',
|
||||
},
|
||||
// Pass an empty object to allow the original behavior
|
||||
{}
|
||||
).as('restoreOriginalBehavior')
|
||||
})
|
||||
|
||||
it('should deduplicate activity history by nonce', () => {
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
.hardhat({ automine: false })
|
||||
beforeEach(() => {
|
||||
cy.hardhat()
|
||||
.then((hardhat) => hardhat.wallet.getTransactionCount())
|
||||
.then((currentNonce) => {
|
||||
const nextNonce = currentNonce + 1
|
||||
// Mock graphql response to include a specific nonce.
|
||||
.then((nonce) => {
|
||||
// Mock graphql response to include specific nonces.
|
||||
cy.intercept(
|
||||
{
|
||||
method: 'POST',
|
||||
@@ -43,7 +31,7 @@ describe('mini-portfolio activity history', () => {
|
||||
status: 'CONFIRMED',
|
||||
to: '0x034a40764485f7e08ca16366e503491a6af7850d',
|
||||
from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
|
||||
nonce: currentNonce,
|
||||
nonce,
|
||||
__typename: 'Transaction',
|
||||
},
|
||||
assetChanges: [],
|
||||
@@ -61,7 +49,7 @@ describe('mini-portfolio activity history', () => {
|
||||
status: 'CONFIRMED',
|
||||
to: '0x1b5154aa4b8f027b9fd19341132fc9dae10f7359',
|
||||
from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
|
||||
nonce: nextNonce,
|
||||
nonce: nonce + 1,
|
||||
__typename: 'Transaction',
|
||||
},
|
||||
assetChanges: [
|
||||
@@ -101,33 +89,28 @@ describe('mini-portfolio activity history', () => {
|
||||
},
|
||||
},
|
||||
}
|
||||
).as('graphqlMock')
|
||||
|
||||
// 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', '')
|
||||
|
||||
// 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('#confirm-swap-or-send').click()
|
||||
cy.get(getTestSelector('dismiss-tx-confirmation')).click()
|
||||
|
||||
// Check activity history tab.
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('mini-portfolio-nav-activity')).click()
|
||||
|
||||
// Assert that the local pending transaction is replaced by a remote transaction with the same nonce.
|
||||
cy.contains('Swapping').should('not.exist')
|
||||
).as('graphql')
|
||||
})
|
||||
})
|
||||
|
||||
it('should deduplicate activity history by nonce', () => {
|
||||
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').should('have.value', '1')
|
||||
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()
|
||||
|
||||
// Check activity history tab.
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('mini-portfolio-nav-activity')).click()
|
||||
|
||||
// Assert that the local pending transaction is replaced by a remote transaction with the same nonce.
|
||||
cy.contains('Swapping').should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -51,9 +51,9 @@ describe('Permit2', () => {
|
||||
.then((hardhat) => hardhat.approval.getPermit2Allowance({ owner: hardhat.wallet, token: INPUT_TOKEN }))
|
||||
.then((allowance) => {
|
||||
cy.wrap(MaxUint160.eq(allowance.amount)).should('eq', true)
|
||||
// Asserts that the on-chain expiration is in 30 days, within a tolerance of 20 seconds.
|
||||
// Asserts that the on-chain expiration is in 30 days, within a tolerance of 40 seconds.
|
||||
const expected = Math.floor((approvalTime + 2_592_000_000) / 1000)
|
||||
cy.wrap(allowance.expiration).should('be.closeTo', expected, 20)
|
||||
cy.wrap(allowance.expiration).should('be.closeTo', expected, 40)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ describe('Service Worker', () => {
|
||||
throw new Error(
|
||||
'\n' +
|
||||
'Service Worker tests must be run on a production-like build\n' +
|
||||
'To test, build with `yarn build:e2e` and serve with `yarn serve`'
|
||||
'To test, build with `yarn build` and serve with `yarn serve`'
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -20,66 +20,78 @@ describe('Service Worker', () => {
|
||||
}
|
||||
})
|
||||
|
||||
function unregister() {
|
||||
return cy.log('unregister service worker').then(async () => {
|
||||
const cacheKeys = await window.caches.keys()
|
||||
const cacheKey = cacheKeys.find((key) => key.match(/precache/))
|
||||
if (cacheKey) {
|
||||
await window.caches.delete(cacheKey)
|
||||
}
|
||||
|
||||
function unregisterServiceWorker() {
|
||||
return cy.log('unregisters service worker').then(async () => {
|
||||
const sw = await window.navigator.serviceWorker.getRegistration(Cypress.config().baseUrl ?? undefined)
|
||||
await sw?.unregister()
|
||||
})
|
||||
}
|
||||
before(unregister)
|
||||
after(unregister)
|
||||
before(unregisterServiceWorker)
|
||||
after(unregisterServiceWorker)
|
||||
|
||||
beforeEach(() => {
|
||||
cy.intercept({ hostname: 'www.google-analytics.com' }, (req) => {
|
||||
const body = req.body.toString()
|
||||
if (req.query['ep.event_category'] === 'Service Worker' || body.includes('Service%20Worker')) {
|
||||
if (req.query['en'] === 'Not Installed' || body.includes('Not%20Installed')) {
|
||||
req.alias = 'NotInstalled'
|
||||
} else if (req.query['en'] === 'Cache Hit' || body.includes('Cache%20Hit')) {
|
||||
req.alias = 'CacheHit'
|
||||
} else if (req.query['en'] === 'Cache Miss' || body.includes('Cache%20Miss')) {
|
||||
req.alias = 'CacheMiss'
|
||||
}
|
||||
cy.intercept('https://api.uniswap.org/v1/amplitude-proxy', (req) => {
|
||||
const body = JSON.stringify(req.body)
|
||||
const serviceWorkerStatus = body.match(/"service_worker":"(\w+)"/)?.[1]
|
||||
if (serviceWorkerStatus) {
|
||||
req.alias = `ServiceWorker:${serviceWorkerStatus}`
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('installs a ServiceWorker', () => {
|
||||
it('installs a ServiceWorker and reports the uninstalled status to analytics', () => {
|
||||
cy.visit('/', { serviceWorker: true })
|
||||
.get('#swap-page')
|
||||
// This is emitted after caching the entry file, which takes some time to load.
|
||||
.wait('@NotInstalled', { timeout: 60000 })
|
||||
.window()
|
||||
.and((win) => {
|
||||
expect(win.navigator.serviceWorker.controller?.state).to.equal('activated')
|
||||
})
|
||||
cy.wait('@ServiceWorker:uninstalled')
|
||||
cy.window().should(
|
||||
'have.nested.property',
|
||||
// The parent is checked instead of the AUT because it is on the same origin,
|
||||
// and the AUT will not be considered "activated" until the parent is idle.
|
||||
'parent.navigator.serviceWorker.controller.state',
|
||||
'activated'
|
||||
)
|
||||
})
|
||||
|
||||
it('records a cache hit', () => {
|
||||
cy.visit('/', { serviceWorker: true }).get('#swap-page').wait('@CacheHit', { timeout: 20000 })
|
||||
})
|
||||
|
||||
it('records a cache miss', () => {
|
||||
cy.then(async () => {
|
||||
const cacheKeys = await window.caches.keys()
|
||||
const cacheKey = cacheKeys.find((key) => key.match(/precache/))
|
||||
assert(cacheKey)
|
||||
|
||||
const cache = await window.caches.open(cacheKey)
|
||||
const keys = await cache.keys()
|
||||
const key = keys.find((key) => key.url.match(/index/))
|
||||
assert(key)
|
||||
|
||||
await cache.put(key, new Response())
|
||||
describe('cache hit', () => {
|
||||
it('reports the hit to analytics', () => {
|
||||
cy.visit('/', { serviceWorker: true })
|
||||
cy.wait('@ServiceWorker:hit')
|
||||
})
|
||||
})
|
||||
|
||||
describe('cache miss', () => {
|
||||
let cache: Cache | undefined
|
||||
let request: Request | undefined
|
||||
let response: Response | undefined
|
||||
before(() => {
|
||||
// Mocks the index.html in the cache to force a cache miss.
|
||||
cy.visit('/', { serviceWorker: true }).then(async () => {
|
||||
const cacheKeys = await window.caches.keys()
|
||||
const cacheKey = cacheKeys.find((key) => key.match(/precache/))
|
||||
assert(cacheKey)
|
||||
|
||||
cache = await window.caches.open(cacheKey)
|
||||
const keys = await cache.keys()
|
||||
request = keys.find((key) => key.url.match(/index/))
|
||||
assert(request)
|
||||
|
||||
response = await cache.match(request)
|
||||
assert(response)
|
||||
|
||||
await cache.put(request, new Response())
|
||||
})
|
||||
})
|
||||
after(() => {
|
||||
// Restores the index.html in the cache so that re-runs behave as expected.
|
||||
// This is necessary because the Service Worker will not re-populate the cache.
|
||||
cy.then(async () => {
|
||||
if (cache && request && response) {
|
||||
await cache.put(request, response)
|
||||
}
|
||||
})
|
||||
})
|
||||
it('reports the miss to analytics', () => {
|
||||
cy.visit('/', { serviceWorker: true })
|
||||
cy.wait('@ServiceWorker:miss')
|
||||
})
|
||||
.visit('/', { serviceWorker: true })
|
||||
.get('#swap-page')
|
||||
.wait('@CacheMiss', { timeout: 20000 })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,358 +0,0 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { parseEther } from '@ethersproject/units'
|
||||
import { SupportedChainId, WETH9 } from '@uniswap/sdk-core'
|
||||
|
||||
import { UNI, USDC_MAINNET } from '../../src/constants/tokens'
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
const UNI_MAINNET = UNI[SupportedChainId.MAINNET]
|
||||
|
||||
describe('Swap', () => {
|
||||
describe('Swap on main page', () => {
|
||||
before(() => {
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
})
|
||||
|
||||
it('starts with ETH selected by default', () => {
|
||||
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.visit('/swap', { ethereum: 'hardhat' })
|
||||
.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.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.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.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.visit('/swap')
|
||||
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.visit('/swap')
|
||||
cy.contains('Settings').should('not.exist')
|
||||
cy.get(getTestSelector('swap-settings-button')).click()
|
||||
cy.contains('Max slippage').should('exist')
|
||||
cy.contains('Transaction deadline').should('exist')
|
||||
cy.contains('Auto Router API').should('exist')
|
||||
cy.get(getTestSelector('swap-settings-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', '')
|
||||
})
|
||||
|
||||
it('can swap ETH for USDC', () => {
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
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()
|
||||
|
||||
// ui check
|
||||
cy.get('#swap-currency-output [data-testid="balance-text"]').should(
|
||||
'have.text',
|
||||
`Balance: ${initialBalance + BALANCE_INCREMENT}`
|
||||
)
|
||||
|
||||
// chain state check
|
||||
cy.then(() => hardhat.getBalance(hardhat.wallet.address, USDC_MAINNET))
|
||||
.then((balance) => Number(balance.toFixed(1)))
|
||||
.should('eq', initialBalance + BALANCE_INCREMENT)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should be able to wrap ETH', () => {
|
||||
const BALANCE_INCREMENT = 1
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
.hardhat()
|
||||
.then((hardhat) => {
|
||||
cy.then(() => hardhat.getBalance(hardhat.wallet.address, WETH9[SupportedChainId.MAINNET]))
|
||||
.then((balance) => Number(balance.toFixed(1)))
|
||||
.then((initialWethBalance) => {
|
||||
// Select WETH for the token output.
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.contains('WETH').click()
|
||||
|
||||
// Enter the amount to wrap.
|
||||
cy.get('#swap-currency-output .token-amount-input').clear().type(BALANCE_INCREMENT.toString())
|
||||
cy.get('#swap-currency-input .token-amount-input').should('not.equal', '')
|
||||
|
||||
// Click the wrap button.
|
||||
cy.get(getTestSelector('wrap-button')).should('not.be.disabled')
|
||||
cy.get(getTestSelector('wrap-button')).click()
|
||||
|
||||
// The pending transaction indicator should be visible.
|
||||
cy.get(getTestSelector('web3-status-connected')).should('have.descendants', ':contains("1 Pending")')
|
||||
|
||||
// <automine transaction>
|
||||
|
||||
// The pending transaction indicator should be gone.
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.have.descendants', ':contains("1 Pending")')
|
||||
|
||||
// The UI balance should have increased.
|
||||
cy.get('#swap-currency-output [data-testid="balance-text"]').should(
|
||||
'have.text',
|
||||
`Balance: ${initialWethBalance + BALANCE_INCREMENT}`
|
||||
)
|
||||
|
||||
// There should be a successful wrap notification.
|
||||
cy.contains('Wrapped').should('exist')
|
||||
|
||||
// The user's WETH account balance should have increased
|
||||
cy.then(() => hardhat.getBalance(hardhat.wallet.address, WETH9[SupportedChainId.MAINNET]))
|
||||
.then((balance) => Number(balance.toFixed(1)))
|
||||
.should('eq', initialWethBalance + BALANCE_INCREMENT)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should be able to unwrap WETH', () => {
|
||||
const BALANCE_INCREMENT = 1
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
.hardhat()
|
||||
.then((hardhat) => {
|
||||
cy.then(() => hardhat.getBalance(hardhat.wallet.address, WETH9[SupportedChainId.MAINNET])).then(
|
||||
(initialBalance) => {
|
||||
// Select WETH for the token output.
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.contains('WETH').click()
|
||||
|
||||
// Enter the amount to wrap.
|
||||
cy.get('#swap-currency-output .token-amount-input').clear().type(BALANCE_INCREMENT.toString())
|
||||
cy.get('#swap-currency-input .token-amount-input').should('not.equal', '')
|
||||
|
||||
// Click the wrap button.
|
||||
cy.get(getTestSelector('wrap-button')).should('not.be.disabled')
|
||||
cy.get(getTestSelector('wrap-button')).click()
|
||||
|
||||
// <automine transaction>
|
||||
|
||||
// The pending transaction indicator should be visible.
|
||||
cy.contains('1 Pending').should('exist')
|
||||
// The user should see a notification telling them they successfully wrapped their ETH.
|
||||
cy.contains('Wrapped').should('exist')
|
||||
|
||||
// Switch to unwrapping the ETH we just wrapped.
|
||||
cy.get(getTestSelector('swap-currency-button')).click()
|
||||
cy.get(getTestSelector('wrap-button')).should('not.be.disabled')
|
||||
|
||||
// Click the Unwrap button.
|
||||
cy.get(getTestSelector('wrap-button')).click()
|
||||
|
||||
// The pending transaction indicator should be visible.
|
||||
cy.contains('1 Pending').should('exist')
|
||||
|
||||
// <automine transaction>
|
||||
|
||||
// The pending transaction indicator should be gone.
|
||||
cy.contains('1 Pending').should('not.exist')
|
||||
// The user should see a notification telling them they successfully unwrapped their ETH.
|
||||
cy.contains('Unwrapped').should('exist')
|
||||
|
||||
// The UI balance should have decreased.
|
||||
cy.get('#swap-currency-input [data-testid="balance-text"]').should(
|
||||
'have.text',
|
||||
`Balance: ${initialBalance.toFixed(0)}`
|
||||
)
|
||||
|
||||
// There should be a successful unwrap notification.
|
||||
cy.contains('Unwrapped').should('exist')
|
||||
|
||||
// The user's WETH account balance should not have changed from the initial balance
|
||||
cy.then(() => hardhat.getBalance(hardhat.wallet.address, WETH9[SupportedChainId.MAINNET]))
|
||||
.then((balance) => balance.toFixed(0))
|
||||
.should('eq', initialBalance.toFixed(0))
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
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'))
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
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()
|
||||
|
||||
// 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')
|
||||
|
||||
// Open the currency select modal.
|
||||
cy.get('#swap-currency-output .open-currency-select-button').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 })
|
||||
|
||||
// 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()
|
||||
|
||||
// The pending transaction indicator should be visible.
|
||||
cy.contains('Pending').should('exist')
|
||||
|
||||
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
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
114
cypress/e2e/swap/errors.test.ts
Normal file
114
cypress/e2e/swap/errors.test.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { SupportedChainId } from '@uniswap/sdk-core'
|
||||
|
||||
import { UNI, USDC_MAINNET } from '../../../src/constants/tokens'
|
||||
import { getBalance, getTestSelector } from '../../utils'
|
||||
|
||||
const UNI_MAINNET = UNI[SupportedChainId.MAINNET]
|
||||
|
||||
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('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
|
||||
|
||||
// Click outside of modal to dismiss it.
|
||||
cy.get('body').click('topRight')
|
||||
cy.get(getTestSelector('deadline-input')).should('not.exist')
|
||||
|
||||
// Attempt to swap.
|
||||
cy.get('#swap-currency-output .token-amount-input').clear().type('1').should('have.value', '1')
|
||||
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()
|
||||
|
||||
// The pending transaction indicator should reflect the state.
|
||||
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine(1, /* 10 minutes */ 1000 * 60 * 10)) // mines past the deadline
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
|
||||
|
||||
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
|
||||
// cy.get(getTestSelector('transaction-popup')).contains('Swap failed')
|
||||
|
||||
// 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)
|
||||
})
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
70
cypress/e2e/swap/swap.test.ts
Normal file
70
cypress/e2e/swap/swap.test.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { SupportedChainId } from '@uniswap/sdk-core'
|
||||
|
||||
import { UNI, USDC_MAINNET } from '../../../src/constants/tokens'
|
||||
import { getBalance, getTestSelector } from '../../utils'
|
||||
|
||||
const UNI_MAINNET = UNI[SupportedChainId.MAINNET]
|
||||
|
||||
describe('Swap', () => {
|
||||
describe('Swap on main page', () => {
|
||||
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('should default inputs from URL params ', () => {
|
||||
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}`)
|
||||
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}`)
|
||||
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('inputs reset when navigating between pages', () => {
|
||||
cy.visit('/swap')
|
||||
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
|
||||
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('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()
|
||||
|
||||
// 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')
|
||||
|
||||
// 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
86
cypress/e2e/swap/wrap.test.ts
Normal file
86
cypress/e2e/swap/wrap.test.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { CurrencyAmount, SupportedChainId, WETH9 } from '@uniswap/sdk-core'
|
||||
|
||||
import { getBalance, getTestSelector } from '../../utils'
|
||||
|
||||
const WETH = WETH9[SupportedChainId.MAINNET]
|
||||
|
||||
describe('Swap wrap', () => {
|
||||
beforeEach(() => {
|
||||
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', () => {
|
||||
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').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.
|
||||
cy.contains('Wrap').click()
|
||||
|
||||
// The pending transaction indicator should reflect the state.
|
||||
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
|
||||
|
||||
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
|
||||
// cy.get(getTestSelector('transaction-popup')).contains('Wrapped')
|
||||
// cy.get(getTestSelector('transaction-popup')).contains('1.00 ETH for 1.00 WETH')
|
||||
|
||||
// The UI balance should have increased.
|
||||
cy.get('#swap-currency-output').should('contain', `Balance: ${initialWethBalance + 1}`)
|
||||
|
||||
// The user's WETH account balance should have increased
|
||||
getBalance(WETH).should('equal', initialWethBalance + 1)
|
||||
})
|
||||
})
|
||||
|
||||
it('should be able to unwrap WETH', () => {
|
||||
cy.hardhat().then(async (hardhat) => {
|
||||
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(WETH, 1e18))
|
||||
await hardhat.mine()
|
||||
})
|
||||
|
||||
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').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.
|
||||
cy.contains('Unwrap').click()
|
||||
|
||||
// The pending transaction indicator should reflect the state.
|
||||
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
|
||||
|
||||
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
|
||||
// cy.get(getTestSelector('transaction-popup')).contains('Unwrapped')
|
||||
// cy.get(getTestSelector('transaction-popup')).contains('1.00 WETH for 1.00 ETH')
|
||||
|
||||
// The UI balance should have increased.
|
||||
cy.get('#swap-currency-input').should('contain', `Balance: ${initialWethBalance - 1}`)
|
||||
|
||||
// The user's WETH account balance should have increased
|
||||
getBalance(WETH).should('equal', initialWethBalance - 1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,96 +1,108 @@
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
function visit(darkMode: boolean) {
|
||||
cy.visit('/swap', {
|
||||
onBeforeLoad(win) {
|
||||
cy.stub(win, 'matchMedia')
|
||||
.withArgs('(prefers-color-scheme: dark)')
|
||||
.returns({
|
||||
matches: darkMode,
|
||||
addEventListener() {
|
||||
// do nothing
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('Wallet Dropdown', () => {
|
||||
before(() => {
|
||||
cy.visit('/pools')
|
||||
function itShouldChangeTheTheme() {
|
||||
it('should change the theme', () => {
|
||||
cy.get(getTestSelector('theme-lightmode')).click()
|
||||
|
||||
cy.get(getTestSelector('theme-lightmode')).should('not.have.css', 'background-color', 'rgba(0, 0, 0, 0)')
|
||||
cy.get(getTestSelector('theme-darkmode')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)')
|
||||
cy.get(getTestSelector('theme-auto')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)')
|
||||
|
||||
cy.get(getTestSelector('theme-darkmode')).click()
|
||||
cy.get(getTestSelector('theme-lightmode')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)')
|
||||
cy.get(getTestSelector('theme-darkmode')).should('not.have.css', 'background-color', 'rgba(0, 0, 0, 0)')
|
||||
cy.get(getTestSelector('theme-auto')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)')
|
||||
|
||||
cy.get(getTestSelector('theme-auto')).click()
|
||||
cy.get(getTestSelector('theme-lightmode')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)')
|
||||
cy.get(getTestSelector('theme-darkmode')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)')
|
||||
cy.get(getTestSelector('theme-auto')).should('not.have.css', 'background-color', 'rgba(0, 0, 0, 0)')
|
||||
})
|
||||
}
|
||||
|
||||
function itShouldChangeTheLanguage() {
|
||||
it('should select a language', () => {
|
||||
cy.get(getTestSelector('wallet-language-item')).contains('Deutsch').click({ force: true })
|
||||
cy.get(getTestSelector('wallet-header')).should('contain', 'Sprache')
|
||||
cy.get(getTestSelector('wallet-language-item')).contains('English').click({ force: true })
|
||||
cy.get(getTestSelector('wallet-header')).should('contain', 'Language')
|
||||
cy.get(getTestSelector('wallet-back')).click()
|
||||
})
|
||||
}
|
||||
|
||||
describe('connected', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/')
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
})
|
||||
itShouldChangeTheTheme()
|
||||
itShouldChangeTheLanguage()
|
||||
})
|
||||
|
||||
it('should change the theme', () => {
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
cy.get(getTestSelector('theme-lightmode')).click()
|
||||
|
||||
cy.get(getTestSelector('theme-lightmode')).should('not.have.css', 'background-color', 'rgba(0, 0, 0, 0)')
|
||||
cy.get(getTestSelector('theme-darkmode')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)')
|
||||
cy.get(getTestSelector('theme-auto')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)')
|
||||
|
||||
cy.get(getTestSelector('theme-darkmode')).click()
|
||||
cy.get(getTestSelector('theme-lightmode')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)')
|
||||
cy.get(getTestSelector('theme-darkmode')).should('not.have.css', 'background-color', 'rgba(0, 0, 0, 0)')
|
||||
cy.get(getTestSelector('theme-auto')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)')
|
||||
|
||||
cy.get(getTestSelector('theme-auto')).click()
|
||||
cy.get(getTestSelector('theme-lightmode')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)')
|
||||
cy.get(getTestSelector('theme-darkmode')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)')
|
||||
cy.get(getTestSelector('theme-auto')).should('not.have.css', 'background-color', 'rgba(0, 0, 0, 0)')
|
||||
describe('disconnected', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/')
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-disconnect')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
})
|
||||
itShouldChangeTheTheme()
|
||||
itShouldChangeTheLanguage()
|
||||
})
|
||||
|
||||
it('should select a language', () => {
|
||||
cy.get(getTestSelector('wallet-language-item')).contains('Deutsch').click({ force: true })
|
||||
cy.get(getTestSelector('wallet-header')).should('contain', 'Sprache')
|
||||
cy.get(getTestSelector('wallet-language-item')).contains('English').click({ force: true })
|
||||
cy.get(getTestSelector('wallet-header')).should('contain', 'Language')
|
||||
cy.get(getTestSelector('wallet-back')).click()
|
||||
describe('with color theme', () => {
|
||||
function visitSwapWithColorTheme({ dark }: { dark: boolean }) {
|
||||
cy.visit('/swap', {
|
||||
onBeforeLoad(win) {
|
||||
cy.stub(win, 'matchMedia')
|
||||
.withArgs('(prefers-color-scheme: dark)')
|
||||
.returns({
|
||||
matches: dark,
|
||||
addEventListener() {
|
||||
/* noop */
|
||||
},
|
||||
removeEventListener() {
|
||||
/* noop */
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
it('should properly use dark system theme when auto theme setting is selected', () => {
|
||||
visitSwapWithColorTheme({ dark: true })
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
cy.get(getTestSelector('theme-auto')).click()
|
||||
cy.get(getTestSelector('wallet-header')).should('have.css', 'color', 'rgb(152, 161, 192)')
|
||||
})
|
||||
|
||||
it('should properly use light system theme when auto theme setting is selected', () => {
|
||||
visitSwapWithColorTheme({ dark: false })
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
cy.get(getTestSelector('theme-auto')).click()
|
||||
cy.get(getTestSelector('wallet-header')).should('have.css', 'color', 'rgb(119, 128, 160)')
|
||||
})
|
||||
})
|
||||
|
||||
it('should change the theme when not connected', () => {
|
||||
cy.get(getTestSelector('wallet-disconnect')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
cy.get(getTestSelector('theme-lightmode')).should('exist')
|
||||
})
|
||||
describe('mobile', () => {
|
||||
beforeEach(() => {
|
||||
cy.viewport('iphone-6').visit('/')
|
||||
})
|
||||
|
||||
it('should select a language when not connected', () => {
|
||||
cy.get(getTestSelector('wallet-language-item')).contains('Deutsch').click({ force: true })
|
||||
cy.get(getTestSelector('wallet-header')).should('contain', 'Sprache')
|
||||
cy.get(getTestSelector('wallet-language-item')).contains('English').click({ force: true })
|
||||
cy.get(getTestSelector('wallet-header')).should('contain', 'Language')
|
||||
cy.get(getTestSelector('wallet-back')).click()
|
||||
})
|
||||
it('should dismiss the wallet bottom sheet when clicking buy crypto', () => {
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-buy-crypto')).click()
|
||||
cy.contains('Buy crypto').should('not.be.visible')
|
||||
})
|
||||
|
||||
it('should properly use dark system theme when auto theme setting is selected', () => {
|
||||
visit(true)
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
cy.get(getTestSelector('theme-auto')).click()
|
||||
cy.get(getTestSelector('wallet-header')).should('have.css', 'color', 'rgb(152, 161, 192)')
|
||||
})
|
||||
|
||||
it('should properly use light system theme when auto theme setting is selected', () => {
|
||||
visit(false)
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
cy.get(getTestSelector('theme-auto')).click()
|
||||
cy.get(getTestSelector('wallet-header')).should('have.css', 'color', 'rgb(119, 128, 160)')
|
||||
})
|
||||
|
||||
it('should dismiss the wallet bottom sheet when clicking buy crypto', () => {
|
||||
visit(false)
|
||||
cy.viewport('iphone-6')
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-buy-crypto')).click()
|
||||
cy.contains('Buy crypto').should('not.be.visible')
|
||||
})
|
||||
|
||||
it('should use a bottom sheet and dismiss when on a mobile screen size', () => {
|
||||
visit(true)
|
||||
cy.viewport('iphone-6')
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.root().click(15, 40)
|
||||
cy.get(getTestSelector('wallet-settings')).should('not.be.visible')
|
||||
it('should use a bottom sheet and dismiss when on a mobile screen size', () => {
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.root().click(15, 40)
|
||||
cy.get(getTestSelector('wallet-settings')).should('not.be.visible')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
|
||||
36
eslint_rules/enforce-retry-on-import.js
Normal file
36
eslint_rules/enforce-retry-on-import.js
Normal file
@@ -0,0 +1,36 @@
|
||||
/* eslint-env node */
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'enforce use of retry() for dynamic imports',
|
||||
category: 'Best Practices',
|
||||
recommended: false,
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
ImportExpression(node) {
|
||||
const grandParent = node.parent.parent
|
||||
if (
|
||||
!(
|
||||
grandParent &&
|
||||
grandParent.type === 'CallExpression' &&
|
||||
// Technically, we are only checking that a function named `retry` wraps the dynamic import.
|
||||
// We do not go as far as enforcing that it is import('utils/retry').retry
|
||||
grandParent.callee.name === 'retry' &&
|
||||
grandParent.arguments.length === 1 &&
|
||||
grandParent.arguments[0].type === 'ArrowFunctionExpression'
|
||||
)
|
||||
) {
|
||||
context.report({
|
||||
node,
|
||||
message: 'Dynamic import should be wrapped in retry (see `utils/retry.ts`): `retry(() => import(...))`',
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -19,7 +19,7 @@ module.exports = {
|
||||
chainId: 1,
|
||||
forking: mainnetFork,
|
||||
accounts: {
|
||||
count: 1,
|
||||
count: 2,
|
||||
},
|
||||
mining: {
|
||||
auto: true, // automine to make tests easier to write.
|
||||
|
||||
@@ -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",
|
||||
@@ -104,11 +104,14 @@
|
||||
"@vanilla-extract/jest-transform": "^1.1.1",
|
||||
"@vanilla-extract/webpack-plugin": "^2.1.11",
|
||||
"babel-plugin-istanbul": "^6.1.1",
|
||||
"cypress": "10.3.1",
|
||||
"buffer": "^6.0.3",
|
||||
"concurrently": "^8.0.1",
|
||||
"cypress": "12.12.0",
|
||||
"cypress-hardhat": "^2.3.0",
|
||||
"env-cmd": "^10.1.0",
|
||||
"eslint": "^7.11.0",
|
||||
"eslint-plugin-import": "^2.27",
|
||||
"eslint-plugin-rulesdir": "^0.2.2",
|
||||
"hardhat": "^2.14.0",
|
||||
"jest-fail-on-console": "^3.1.1",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
@@ -138,6 +141,7 @@
|
||||
"@graphql-codegen/typescript-operations": "^2.5.8",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
|
||||
"@graphql-codegen/typescript-resolvers": "^2.7.8",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@lingui/core": "^3.14.0",
|
||||
"@lingui/macro": "^3.14.0",
|
||||
"@lingui/react": "^3.14.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>
|
||||
|
||||
@@ -266,7 +266,7 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
|
||||
<PortfolioDrawerContainer>
|
||||
{totalBalance !== undefined ? (
|
||||
<FadeInColumn gap="xs">
|
||||
<ThemedText.HeadlineLarge fontWeight={500}>
|
||||
<ThemedText.HeadlineLarge fontWeight={500} data-testid="portfolio-total-balance">
|
||||
{formatNumber(totalBalance, NumberType.PortfolioBalance)}
|
||||
</ThemedText.HeadlineLarge>
|
||||
<AutoRow marginBottom="20px">
|
||||
|
||||
@@ -82,7 +82,24 @@ const ActivityGroupWrapper = styled(Column)`
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
function combineActivities(localMap: ActivityMap = {}, remoteMap: ActivityMap = {}): Array<Activity> {
|
||||
/* Detects transactions from same account with the same nonce and different hash */
|
||||
function wasTxCancelled(localActivity: Activity, remoteMap: ActivityMap, account: string): boolean {
|
||||
// handles locally cached tx's that were stored before we started tracking nonces
|
||||
if (!localActivity.nonce || localActivity.status !== TransactionStatus.Pending) return false
|
||||
|
||||
return Object.values(remoteMap).some((remoteTx) => {
|
||||
if (!remoteTx) return false
|
||||
|
||||
// Cancellations are only possible when both nonce and tx.from are the same
|
||||
if (remoteTx.nonce === localActivity.nonce && remoteTx.receipt?.from.toLowerCase() === account.toLowerCase()) {
|
||||
// If the remote tx has a different hash than the local tx, the local tx was cancelled
|
||||
return remoteTx.hash.toLowerCase() !== localActivity.hash.toLowerCase()
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
function combineActivities(localMap: ActivityMap = {}, remoteMap: ActivityMap = {}, account: string): Array<Activity> {
|
||||
const txHashes = [...new Set([...Object.keys(localMap), ...Object.keys(remoteMap)])]
|
||||
|
||||
// Merges local and remote activities w/ same hash, preferring remote data
|
||||
@@ -90,16 +107,12 @@ function combineActivities(localMap: ActivityMap = {}, remoteMap: ActivityMap =
|
||||
const localActivity = (localMap?.[hash] ?? {}) as Activity
|
||||
const remoteActivity = (remoteMap?.[hash] ?? {}) as Activity
|
||||
|
||||
// Check for nonce collision
|
||||
const isNonceCollision =
|
||||
localActivity.nonce !== undefined &&
|
||||
Object.keys(remoteMap).some((remoteHash) => remoteMap[remoteHash]?.nonce === localActivity.nonce)
|
||||
// TODO(WEB-2064): Display cancelled status in UI rather than completely hiding cancelled TXs
|
||||
if (wasTxCancelled(localActivity, remoteMap, account)) return acc
|
||||
|
||||
if (!isNonceCollision) {
|
||||
// TODO(cartcrom): determine best logic for which fields to prefer from which sources
|
||||
// i.e.prefer remote exact swap output instead of local estimated output
|
||||
acc.push({ ...localActivity, ...remoteActivity } as Activity)
|
||||
}
|
||||
// TODO(cartcrom): determine best logic for which fields to prefer from which sources
|
||||
// i.e.prefer remote exact swap output instead of local estimated output
|
||||
acc.push({ ...localActivity, ...remoteActivity } as Activity)
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
@@ -132,9 +145,9 @@ export function ActivityTab({ account }: { account: string }) {
|
||||
|
||||
const activityGroups = useMemo(() => {
|
||||
const remoteMap = parseRemoteActivities(data?.portfolios?.[0].assetActivities)
|
||||
const allActivities = combineActivities(localMap, remoteMap)
|
||||
const allActivities = combineActivities(localMap, remoteMap, account)
|
||||
return createGroups(allActivities)
|
||||
}, [data?.portfolios, localMap])
|
||||
}, [data?.portfolios, localMap, account])
|
||||
|
||||
if (!data && loading)
|
||||
return (
|
||||
|
||||
@@ -22,12 +22,14 @@ const EndColumn = styled(Column)`
|
||||
`
|
||||
|
||||
export default function PortfolioRow({
|
||||
['data-testid']: testId,
|
||||
left,
|
||||
title,
|
||||
descriptor,
|
||||
right,
|
||||
onClick,
|
||||
}: {
|
||||
'data-testid'?: string
|
||||
left: React.ReactNode
|
||||
title: React.ReactNode
|
||||
descriptor?: React.ReactNode
|
||||
@@ -36,7 +38,7 @@ export default function PortfolioRow({
|
||||
onClick?: () => void
|
||||
}) {
|
||||
return (
|
||||
<PortfolioRowWrapper onClick={onClick}>
|
||||
<PortfolioRowWrapper data-testid={testId} onClick={onClick}>
|
||||
{left}
|
||||
<AutoColumn grow>
|
||||
{title}
|
||||
|
||||
@@ -13,7 +13,7 @@ function wasPending(previousTxs: { [hash: string]: TransactionDetails | undefine
|
||||
return previousTx && isTxPending(previousTx)
|
||||
}
|
||||
|
||||
function useHasUpdatedTx() {
|
||||
function useHasUpdatedTx(account: string | undefined) {
|
||||
// TODO: consider monitoring tx's on chains other than the wallet's current chain
|
||||
const currentChainTxs = useAllTransactions()
|
||||
|
||||
@@ -27,12 +27,12 @@ function useHasUpdatedTx() {
|
||||
const previousPendingTxs = usePrevious(pendingTxs)
|
||||
|
||||
return useMemo(() => {
|
||||
if (!previousPendingTxs) return false
|
||||
if (!previousPendingTxs || !account) return false
|
||||
return Object.values(currentChainTxs).some(
|
||||
(tx) => !isTxPending(tx) && wasPending(previousPendingTxs, tx),
|
||||
(tx) => tx.from === account && !isTxPending(tx) && wasPending(previousPendingTxs, tx),
|
||||
[currentChainTxs, previousPendingTxs]
|
||||
)
|
||||
}, [currentChainTxs, previousPendingTxs])
|
||||
}, [account, currentChainTxs, previousPendingTxs])
|
||||
}
|
||||
|
||||
/* Prefetches & caches portfolio balances when the wrapped component is hovered or the user completes a transaction */
|
||||
@@ -49,16 +49,22 @@ export default function PrefetchBalancesWrapper({ children }: PropsWithChildren)
|
||||
}
|
||||
}, [account, prefetchPortfolioBalances])
|
||||
|
||||
// TODO(cartcrom): add delay for refetching on optimism, as there is high latency in new balances being available
|
||||
const hasUpdatedTx = useHasUpdatedTx()
|
||||
// Listens for recently updated transactions to keep portfolio balances fresh in apollo cache
|
||||
useEffect(() => {
|
||||
if (!hasUpdatedTx) return
|
||||
const prevAccount = usePrevious(account)
|
||||
|
||||
// If the drawer is open, fetch balances immediately, else set a flag to fetch on next hover
|
||||
if (drawerOpen) fetchBalances()
|
||||
else setHasUnfetchedBalances(true)
|
||||
}, [drawerOpen, fetchBalances, hasUpdatedTx])
|
||||
// TODO(cartcrom): add delay for refetching on optimism, as there is high latency in new balances being available
|
||||
const hasUpdatedTx = useHasUpdatedTx(account)
|
||||
// Listens for account changes & recently updated transactions to keep portfolio balances fresh in apollo cache
|
||||
useEffect(() => {
|
||||
const accountChanged = prevAccount !== undefined && prevAccount !== account
|
||||
if (hasUpdatedTx || accountChanged) {
|
||||
// If the drawer is open, fetch balances immediately, else set a flag to fetch on next hover
|
||||
if (drawerOpen) {
|
||||
fetchBalances()
|
||||
} else {
|
||||
setHasUnfetchedBalances(true)
|
||||
}
|
||||
}
|
||||
}, [account, prevAccount, drawerOpen, fetchBalances, hasUpdatedTx])
|
||||
|
||||
const onHover = useCallback(() => {
|
||||
if (hasUnfetchedBalances) fetchBalances()
|
||||
|
||||
@@ -205,7 +205,7 @@ function AccountDrawer() {
|
||||
name={InterfaceEventName.MINI_PORTFOLIO_TOGGLED}
|
||||
properties={{ type: 'close' }}
|
||||
>
|
||||
<CloseDrawer onClick={toggleWalletDrawer}>
|
||||
<CloseDrawer onClick={toggleWalletDrawer} data-testid="close-account-drawer">
|
||||
<CloseIcon />
|
||||
</CloseDrawer>
|
||||
</TraceEvent>
|
||||
|
||||
@@ -296,7 +296,7 @@ export function ButtonConfirmed({
|
||||
}
|
||||
}
|
||||
|
||||
export function ButtonError({ error, ...rest }: { error?: boolean } & ButtonProps) {
|
||||
export function ButtonError({ error, ...rest }: { error?: boolean } & BaseButtonProps) {
|
||||
if (error) {
|
||||
return <ButtonErrorStyle {...rest} />
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import styled, { DefaultTheme } from 'styled-components/macro'
|
||||
|
||||
type Gap = keyof DefaultTheme['grids']
|
||||
import styled from 'styled-components/macro'
|
||||
import { Gap } from 'theme'
|
||||
|
||||
export const Column = styled.div<{
|
||||
gap?: Gap
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Expand renders correctly 1`] = `
|
||||
<DocumentFragment>
|
||||
.c1 {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
width: 100%;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
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;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
cursor: pointer;
|
||||
-webkit-box-pack: end;
|
||||
-webkit-justify-content: flex-end;
|
||||
-ms-flex-pack: end;
|
||||
justify-content: flex-end;
|
||||
width: unset;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
color: #7780A0;
|
||||
-webkit-transform: rotate(0deg);
|
||||
-ms-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
-webkit-transition: -webkit-transform 250ms;
|
||||
-webkit-transition: transform 250ms;
|
||||
transition: transform 250ms;
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0"
|
||||
>
|
||||
<div
|
||||
class="c1 c2 c3"
|
||||
>
|
||||
<span>
|
||||
Header
|
||||
</span>
|
||||
<div
|
||||
aria-expanded="false"
|
||||
class="c1 c2 c4"
|
||||
>
|
||||
<span>
|
||||
Button
|
||||
</span>
|
||||
<svg
|
||||
class="c5"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<polyline
|
||||
points="6 9 12 15 18 9"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -1,20 +1,31 @@
|
||||
import { fireEvent, render, screen } from 'test-utils/render'
|
||||
import noop from 'utils/noop'
|
||||
|
||||
import Expand from './index'
|
||||
|
||||
describe('Expand', () => {
|
||||
it('renders correctly', () => {
|
||||
const { asFragment } = render(
|
||||
<Expand header={<span>Header</span>} button={<span>Button</span>}>
|
||||
it('does not render children when closed', () => {
|
||||
render(
|
||||
<Expand header={<span>Header</span>} isOpen={false} onToggle={noop} button={<span>Button</span>}>
|
||||
Body
|
||||
</Expand>
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
expect(screen.queryByText('Body')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('toggles children on button press', () => {
|
||||
it('renders children when open', () => {
|
||||
render(
|
||||
<Expand header={<span>Header</span>} button={<span>Button</span>}>
|
||||
<Expand header={<span>Header</span>} isOpen={true} onToggle={noop} button={<span>Button</span>}>
|
||||
Body
|
||||
</Expand>
|
||||
)
|
||||
expect(screen.queryByText('Body')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls `onToggle` when button is pressed', () => {
|
||||
const onToggle = jest.fn()
|
||||
render(
|
||||
<Expand header={<span>Header</span>} isOpen={false} onToggle={onToggle} button={<span>Button</span>}>
|
||||
Body
|
||||
</Expand>
|
||||
)
|
||||
@@ -22,9 +33,6 @@ describe('Expand', () => {
|
||||
const button = screen.getByText('Button')
|
||||
|
||||
fireEvent.click(button)
|
||||
expect(screen.queryByText('Body')).not.toBeNull()
|
||||
|
||||
fireEvent.click(button)
|
||||
expect(screen.queryByText('Body')).toBeNull()
|
||||
expect(onToggle).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Column from 'components/Column'
|
||||
import React, { PropsWithChildren, ReactElement, useState } from 'react'
|
||||
import React, { PropsWithChildren, ReactElement } from 'react'
|
||||
import { ChevronDown } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
@@ -11,9 +11,9 @@ const ButtonContainer = styled(Row)`
|
||||
width: unset;
|
||||
`
|
||||
|
||||
const ExpandIcon = styled(ChevronDown)<{ $isExpanded: boolean }>`
|
||||
const ExpandIcon = styled(ChevronDown)<{ $isOpen: boolean }>`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
transform: ${({ $isExpanded }) => ($isExpanded ? 'rotate(180deg)' : 'rotate(0deg)')};
|
||||
transform: ${({ $isOpen }) => ($isOpen ? 'rotate(180deg)' : 'rotate(0deg)')};
|
||||
transition: transform ${({ theme }) => theme.transition.duration.medium};
|
||||
`
|
||||
|
||||
@@ -22,22 +22,25 @@ export default function Expand({
|
||||
button,
|
||||
children,
|
||||
testId,
|
||||
isOpen,
|
||||
onToggle,
|
||||
}: PropsWithChildren<{
|
||||
header: ReactElement
|
||||
button: ReactElement
|
||||
testId?: string
|
||||
isOpen: boolean
|
||||
onToggle: () => void
|
||||
}>) {
|
||||
const [isExpanded, setExpanded] = useState(false)
|
||||
return (
|
||||
<Column gap="md">
|
||||
<RowBetween>
|
||||
{header}
|
||||
<ButtonContainer data-testid={testId} onClick={() => setExpanded(!isExpanded)} aria-expanded={isExpanded}>
|
||||
<ButtonContainer data-testid={testId} onClick={onToggle} aria-expanded={isOpen}>
|
||||
{button}
|
||||
<ExpandIcon $isExpanded={isExpanded} />
|
||||
<ExpandIcon $isOpen={isOpen} />
|
||||
</ButtonContainer>
|
||||
</RowBetween>
|
||||
{isExpanded && children}
|
||||
{isOpen && children}
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
|
||||
import { DetailsV2Variant, useDetailsV2Flag } from 'featureFlags/flags/nftDetails'
|
||||
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
|
||||
import { UnifiedRouterVariant, useUnifiedRoutingAPIFlag } from 'featureFlags/flags/unifiedRouter'
|
||||
import { useUpdateAtom } from 'jotai/utils'
|
||||
import { Children, PropsWithChildren, ReactElement, ReactNode, useCallback, useState } from 'react'
|
||||
import { X } from 'react-feather'
|
||||
@@ -207,6 +208,12 @@ export default function FeatureFlagModal() {
|
||||
featureFlag={FeatureFlag.detailsV2}
|
||||
label="Use the new details page for nfts"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={UnifiedRouterVariant}
|
||||
value={useUnifiedRoutingAPIFlag()}
|
||||
featureFlag={FeatureFlag.uraEnabled}
|
||||
label="Enable the Unified Routing API"
|
||||
/>
|
||||
<FeatureFlagGroup name="Debug">
|
||||
<FeatureFlagOption
|
||||
variant={TraceJsonRpcVariant}
|
||||
|
||||
@@ -29,6 +29,7 @@ function TransactionPopupContent({ tx, chainId }: { tx: TransactionDetails; chai
|
||||
|
||||
return (
|
||||
<PortfolioRow
|
||||
data-testid="transaction-popup"
|
||||
left={
|
||||
success ? (
|
||||
<Column>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Box } from 'rebass/styled-components'
|
||||
import styled, { DefaultTheme } from 'styled-components/macro'
|
||||
|
||||
type Gap = keyof DefaultTheme['grids']
|
||||
import styled from 'styled-components/macro'
|
||||
import { Gap } from 'theme'
|
||||
|
||||
// TODO(WEB-3289):
|
||||
// Setting `width: 100%` by default prevents composability in complex flex layouts.
|
||||
@@ -14,7 +13,7 @@ const Row = styled(Box)<{
|
||||
padding?: string
|
||||
border?: string
|
||||
borderRadius?: string
|
||||
gap?: string
|
||||
gap?: Gap | string
|
||||
}>`
|
||||
width: ${({ width }) => width ?? '100%'};
|
||||
display: flex;
|
||||
|
||||
@@ -8,8 +8,12 @@ import MaxSlippageSettings from '.'
|
||||
|
||||
const AUTO_SLIPPAGE = new Percent(5, 10_000)
|
||||
|
||||
const renderAndExpandSlippageSettings = () => {
|
||||
const renderSlippageSettings = () => {
|
||||
render(<MaxSlippageSettings autoSlippage={AUTO_SLIPPAGE} />)
|
||||
}
|
||||
|
||||
const renderAndExpandSlippageSettings = () => {
|
||||
renderSlippageSettings()
|
||||
|
||||
// By default, the button to expand Slippage component and show `input` will have `Auto` label
|
||||
fireEvent.click(screen.getByText('Auto'))
|
||||
@@ -20,7 +24,7 @@ const switchToCustomSlippage = () => {
|
||||
fireEvent.click(screen.getByText('Custom'))
|
||||
}
|
||||
|
||||
const getSlippageInput = () => screen.getByTestId('slippage-input') as HTMLInputElement
|
||||
const getSlippageInput = () => screen.queryByTestId('slippage-input') as HTMLInputElement
|
||||
|
||||
describe('MaxSlippageSettings', () => {
|
||||
describe('input', () => {
|
||||
@@ -28,6 +32,15 @@ describe('MaxSlippageSettings', () => {
|
||||
beforeEach(() => {
|
||||
store.dispatch(updateUserSlippageTolerance({ userSlippageTolerance: SlippageTolerance.Auto }))
|
||||
})
|
||||
it('is not expanded by default', () => {
|
||||
renderSlippageSettings()
|
||||
expect(getSlippageInput()).not.toBeInTheDocument()
|
||||
})
|
||||
it('is expanded by default when custom slippage is set', () => {
|
||||
store.dispatch(updateUserSlippageTolerance({ userSlippageTolerance: 10 }))
|
||||
renderSlippageSettings()
|
||||
expect(getSlippageInput()).toBeInTheDocument()
|
||||
})
|
||||
it('does not render auto slippage as a value, but a placeholder', () => {
|
||||
renderAndExpandSlippageSettings()
|
||||
switchToCustomSlippage()
|
||||
|
||||
@@ -53,6 +53,9 @@ export default function MaxSlippageSettings({ autoSlippage }: { autoSlippage: Pe
|
||||
const [slippageInput, setSlippageInput] = useState(defaultSlippageInputValue)
|
||||
const [slippageError, setSlippageError] = useState<SlippageError | false>(false)
|
||||
|
||||
// If user has previously entered a custom slippage, we want to show the settings expanded by default.
|
||||
const [isOpen, setIsOpen] = useState(defaultSlippageInputValue.length > 0)
|
||||
|
||||
const parseSlippageInput = (value: string) => {
|
||||
// Do not allow non-numerical characters in the input field or more than two decimals
|
||||
if (value.length > 0 && !NUMBER_WITH_MAX_TWO_DECIMAL_PLACES.test(value)) {
|
||||
@@ -93,6 +96,8 @@ export default function MaxSlippageSettings({ autoSlippage }: { autoSlippage: Pe
|
||||
return (
|
||||
<Expand
|
||||
testId="max-slippage-settings"
|
||||
isOpen={isOpen}
|
||||
onToggle={() => setIsOpen(!isOpen)}
|
||||
header={
|
||||
<Row width="auto">
|
||||
<ThemedText.BodySecondary>
|
||||
|
||||
36
src/components/Settings/MenuButton/index.test.tsx
Normal file
36
src/components/Settings/MenuButton/index.test.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
import { useUserSlippageTolerance } from 'state/user/hooks'
|
||||
import { SlippageTolerance } from 'state/user/types'
|
||||
import { mocked } from 'test-utils/mocked'
|
||||
import { render, screen } from 'test-utils/render'
|
||||
import { lightTheme } from 'theme/colors'
|
||||
import noop from 'utils/noop'
|
||||
|
||||
import MenuButton from '.'
|
||||
|
||||
jest.mock('state/user/hooks')
|
||||
|
||||
const renderButton = () => {
|
||||
render(<MenuButton disabled={false} onClick={noop} isActive={false} />)
|
||||
}
|
||||
|
||||
describe('MenuButton', () => {
|
||||
it('should render an icon when slippage is Auto', () => {
|
||||
mocked(useUserSlippageTolerance).mockReturnValue([SlippageTolerance.Auto, noop])
|
||||
renderButton()
|
||||
expect(screen.queryByText('slippage')).not.toBeInTheDocument()
|
||||
})
|
||||
it('should render an icon with a custom slippage value', () => {
|
||||
mocked(useUserSlippageTolerance).mockReturnValue([new Percent(5, 10_000), noop])
|
||||
renderButton()
|
||||
expect(screen.queryByText('0.05% slippage')).toBeInTheDocument()
|
||||
})
|
||||
it('should render an icon with a custom slippage and a warning when value is out of bounds', () => {
|
||||
mocked(useUserSlippageTolerance).mockReturnValue([new Percent(1, 10_000), noop])
|
||||
renderButton()
|
||||
expect(screen.getByTestId('settings-icon-with-slippage')).toHaveStyleRule(
|
||||
'background-color',
|
||||
lightTheme.accentWarningSoft
|
||||
)
|
||||
})
|
||||
})
|
||||
91
src/components/Settings/MenuButton/index.tsx
Normal file
91
src/components/Settings/MenuButton/index.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import Row from 'components/Row'
|
||||
import { Settings } from 'react-feather'
|
||||
import { useUserSlippageTolerance } from 'state/user/hooks'
|
||||
import { SlippageTolerance } from 'state/user/types'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import validateUserSlippageTolerance, { SlippageValidationResult } from 'utils/validateUserSlippageTolerance'
|
||||
|
||||
const Icon = styled(Settings)`
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
> * {
|
||||
stroke: ${({ theme }) => theme.textSecondary};
|
||||
}
|
||||
`
|
||||
|
||||
const Button = styled.button<{ isActive: boolean }>`
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
|
||||
:not([disabled]):hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
${({ isActive }) => isActive && `opacity: 0.7`}
|
||||
`
|
||||
|
||||
const IconContainer = styled(Row)`
|
||||
padding: 6px 12px;
|
||||
border-radius: 16px;
|
||||
`
|
||||
|
||||
const IconContainerWithSlippage = styled(IconContainer)<{ displayWarning?: boolean }>`
|
||||
div {
|
||||
color: ${({ theme, displayWarning }) => (displayWarning ? theme.accentWarning : theme.textSecondary)};
|
||||
}
|
||||
|
||||
background-color: ${({ theme, displayWarning }) =>
|
||||
displayWarning ? theme.accentWarningSoft : theme.backgroundModule};
|
||||
`
|
||||
|
||||
const ButtonContent = () => {
|
||||
const [userSlippageTolerance] = useUserSlippageTolerance()
|
||||
|
||||
if (userSlippageTolerance === SlippageTolerance.Auto) {
|
||||
return (
|
||||
<IconContainer>
|
||||
<Icon />
|
||||
</IconContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const isInvalidSlippage = validateUserSlippageTolerance(userSlippageTolerance) !== SlippageValidationResult.Valid
|
||||
|
||||
return (
|
||||
<IconContainerWithSlippage data-testid="settings-icon-with-slippage" gap="sm" displayWarning={isInvalidSlippage}>
|
||||
<ThemedText.Caption>
|
||||
<Trans>{userSlippageTolerance.toFixed(2)}% slippage</Trans>
|
||||
</ThemedText.Caption>
|
||||
<Icon />
|
||||
</IconContainerWithSlippage>
|
||||
)
|
||||
}
|
||||
|
||||
export default function MenuButton({
|
||||
disabled,
|
||||
onClick,
|
||||
isActive,
|
||||
}: {
|
||||
disabled: boolean
|
||||
onClick: () => void
|
||||
isActive: boolean
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
isActive={isActive}
|
||||
id="open-settings-dialog-button"
|
||||
data-testid="open-settings-dialog-button"
|
||||
aria-label={t`Transaction Settings`}
|
||||
>
|
||||
<ButtonContent />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -5,14 +5,18 @@ import { fireEvent, render, screen } from 'test-utils/render'
|
||||
|
||||
import TransactionDeadlineSettings from '.'
|
||||
|
||||
const renderAndExpandTransactionDeadlineSettings = () => {
|
||||
const renderTransactionDeadlineSettings = () => {
|
||||
render(<TransactionDeadlineSettings />)
|
||||
}
|
||||
|
||||
const renderAndExpandTransactionDeadlineSettings = () => {
|
||||
renderTransactionDeadlineSettings()
|
||||
|
||||
// By default, the button to expand Slippage component and show `input` will have `<deadline>m` label
|
||||
fireEvent.click(screen.getByText(`${DEFAULT_DEADLINE_FROM_NOW / 60}m`))
|
||||
}
|
||||
|
||||
const getDeadlineInput = () => screen.getByTestId('deadline-input') as HTMLInputElement
|
||||
const getDeadlineInput = () => screen.queryByTestId('deadline-input') as HTMLInputElement
|
||||
|
||||
describe('TransactionDeadlineSettings', () => {
|
||||
describe('input', () => {
|
||||
@@ -20,6 +24,15 @@ describe('TransactionDeadlineSettings', () => {
|
||||
beforeEach(() => {
|
||||
store.dispatch(updateUserDeadline({ userDeadline: DEFAULT_DEADLINE_FROM_NOW }))
|
||||
})
|
||||
it('is not expanded by default', () => {
|
||||
renderTransactionDeadlineSettings()
|
||||
expect(getDeadlineInput()).not.toBeInTheDocument()
|
||||
})
|
||||
it('is expanded by default when custom deadline is set', () => {
|
||||
store.dispatch(updateUserDeadline({ userDeadline: DEFAULT_DEADLINE_FROM_NOW * 2 }))
|
||||
renderTransactionDeadlineSettings()
|
||||
expect(getDeadlineInput()).toBeInTheDocument()
|
||||
})
|
||||
it('does not render default deadline as a value, but a placeholder', () => {
|
||||
renderAndExpandTransactionDeadlineSettings()
|
||||
expect(getDeadlineInput().value).toBe('')
|
||||
|
||||
@@ -26,6 +26,9 @@ export default function TransactionDeadlineSettings() {
|
||||
const [deadlineInput, setDeadlineInput] = useState(defaultInputValue)
|
||||
const [deadlineError, setDeadlineError] = useState<DeadlineError | false>(false)
|
||||
|
||||
// If user has previously entered a custom deadline, we want to show the settings expanded by default.
|
||||
const [isOpen, setIsOpen] = useState(defaultInputValue.length > 0)
|
||||
|
||||
function parseCustomDeadline(value: string) {
|
||||
// Do not allow non-numerical characters in the input field
|
||||
if (value.length > 0 && !NUMBERS_ONLY.test(value)) {
|
||||
@@ -56,6 +59,8 @@ export default function TransactionDeadlineSettings() {
|
||||
|
||||
return (
|
||||
<Expand
|
||||
isOpen={isOpen}
|
||||
onToggle={() => setIsOpen(!isOpen)}
|
||||
testId="transaction-deadline-settings"
|
||||
header={
|
||||
<Row width="auto">
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { t } from '@lingui/macro'
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
@@ -7,82 +6,39 @@ import { L2_CHAIN_IDS } from 'constants/chains'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
import { isSupportedChainId } from 'lib/hooks/routing/clientSideSmartOrderRouter'
|
||||
import { useRef } from 'react'
|
||||
import { Settings } from 'react-feather'
|
||||
import { useModalIsOpen, useToggleSettingsMenu } from 'state/application/hooks'
|
||||
import { ApplicationModal } from 'state/application/reducer'
|
||||
import styled from 'styled-components/macro'
|
||||
import { Divider } from 'theme'
|
||||
|
||||
import MaxSlippageSettings from './MaxSlippageSettings'
|
||||
import MenuButton from './MenuButton'
|
||||
import RouterPreferenceSettings from './RouterPreferenceSettings'
|
||||
import TransactionDeadlineSettings from './TransactionDeadlineSettings'
|
||||
|
||||
const StyledMenuIcon = styled(Settings)`
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
> * {
|
||||
stroke: ${({ theme }) => theme.textSecondary};
|
||||
}
|
||||
`
|
||||
|
||||
const StyledMenuButton = styled.button<{ disabled: boolean }>`
|
||||
const Menu = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-radius: 0.5rem;
|
||||
height: 20px;
|
||||
${({ disabled }) =>
|
||||
!disabled &&
|
||||
`
|
||||
:hover,
|
||||
:focus {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
opacity: 0.7;
|
||||
}
|
||||
`}
|
||||
`
|
||||
|
||||
const StyledMenu = styled.div`
|
||||
margin-left: 0.5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
border: none;
|
||||
text-align: left;
|
||||
`
|
||||
|
||||
const MenuFlyout = styled.span`
|
||||
const MenuFlyout = styled(AutoColumn)`
|
||||
min-width: 20.125rem;
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
|
||||
0px 24px 32px rgba(0, 0, 0, 0.01);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 1rem;
|
||||
position: absolute;
|
||||
top: 2rem;
|
||||
right: 0rem;
|
||||
top: 100%;
|
||||
margin-top: 10px;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
|
||||
min-width: 18.125rem;
|
||||
`};
|
||||
user-select: none;
|
||||
`
|
||||
|
||||
const Divider = styled.div`
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
border-width: 0;
|
||||
margin: 0;
|
||||
background-color: ${({ theme }) => theme.backgroundOutline};
|
||||
gap: 16px;
|
||||
padding: 1rem;
|
||||
`
|
||||
|
||||
export default function SettingsTab({ autoSlippage }: { autoSlippage: Percent }) {
|
||||
@@ -90,37 +46,29 @@ export default function SettingsTab({ autoSlippage }: { autoSlippage: Percent })
|
||||
const showDeadlineSettings = Boolean(chainId && !L2_CHAIN_IDS.includes(chainId))
|
||||
|
||||
const node = useRef<HTMLDivElement | null>(null)
|
||||
const open = useModalIsOpen(ApplicationModal.SETTINGS)
|
||||
const isOpen = useModalIsOpen(ApplicationModal.SETTINGS)
|
||||
|
||||
const toggle = useToggleSettingsMenu()
|
||||
useOnClickOutside(node, open ? toggle : undefined)
|
||||
const toggleMenu = useToggleSettingsMenu()
|
||||
useOnClickOutside(node, isOpen ? toggleMenu : undefined)
|
||||
|
||||
const isSupportedChain = isSupportedChainId(chainId)
|
||||
|
||||
return (
|
||||
<StyledMenu ref={node}>
|
||||
<StyledMenuButton
|
||||
disabled={!isSupportedChainId(chainId)}
|
||||
onClick={toggle}
|
||||
id="open-settings-dialog-button"
|
||||
data-testid="open-settings-dialog-button"
|
||||
aria-label={t`Transaction Settings`}
|
||||
>
|
||||
<StyledMenuIcon data-testid="swap-settings-button" />
|
||||
</StyledMenuButton>
|
||||
{open && (
|
||||
<Menu ref={node}>
|
||||
<MenuButton disabled={!isSupportedChain} isActive={isOpen} onClick={toggleMenu} />
|
||||
{isOpen && (
|
||||
<MenuFlyout>
|
||||
<AutoColumn gap="16px" style={{ padding: '1rem' }}>
|
||||
{isSupportedChainId(chainId) && <RouterPreferenceSettings />}
|
||||
<Divider />
|
||||
<MaxSlippageSettings autoSlippage={autoSlippage} />
|
||||
{showDeadlineSettings && (
|
||||
<>
|
||||
<Divider />
|
||||
<TransactionDeadlineSettings />
|
||||
</>
|
||||
)}
|
||||
</AutoColumn>
|
||||
<RouterPreferenceSettings />
|
||||
<Divider />
|
||||
<MaxSlippageSettings autoSlippage={autoSlippage} />
|
||||
{showDeadlineSettings && (
|
||||
<>
|
||||
<Divider />
|
||||
<TransactionDeadlineSettings />
|
||||
</>
|
||||
)}
|
||||
</MenuFlyout>
|
||||
)}
|
||||
</StyledMenu>
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@ import useAccountRiskCheck from 'hooks/useAccountRiskCheck'
|
||||
import { lazy } from 'react'
|
||||
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
|
||||
import { ApplicationModal } from 'state/application/reducer'
|
||||
import { retry } from 'utils/retry'
|
||||
|
||||
const Bag = lazy(() => import('nft/components/bag/Bag'))
|
||||
const TransactionCompleteModal = lazy(() => import('nft/components/collection/TransactionCompleteModal'))
|
||||
const AirdropModal = lazy(() => import('components/AirdropModal'))
|
||||
const Bag = lazy(() => retry(() => import('nft/components/bag/Bag')))
|
||||
const TransactionCompleteModal = lazy(() => retry(() => import('nft/components/collection/TransactionCompleteModal')))
|
||||
const AirdropModal = lazy(() => retry(() => import('components/AirdropModal')))
|
||||
|
||||
export default function TopLevelModals() {
|
||||
const addressClaimOpen = useModalIsOpen(ApplicationModal.ADDRESS_CLAIM)
|
||||
|
||||
@@ -20,7 +20,7 @@ import { TransactionSummary } from '../AccountDetails/TransactionSummary'
|
||||
import { ButtonLight, ButtonPrimary } from '../Button'
|
||||
import { AutoColumn, ColumnCenter } from '../Column'
|
||||
import Modal from '../Modal'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import Row, { RowBetween, RowFixed } from '../Row'
|
||||
import AnimatedConfirmation from './AnimatedConfirmation'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
@@ -28,16 +28,12 @@ const Wrapper = styled.div`
|
||||
border-radius: 20px;
|
||||
outline: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
`
|
||||
const Section = styled(AutoColumn)<{ inline?: boolean }>`
|
||||
padding: ${({ inline }) => (inline ? '0' : '0')};
|
||||
padding: 16px;
|
||||
`
|
||||
|
||||
const BottomSection = styled(Section)`
|
||||
const BottomSection = styled(AutoColumn)`
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
padding-bottom: 10px;
|
||||
`
|
||||
|
||||
const ConfirmedIcon = styled(ColumnCenter)<{ inline?: boolean }>`
|
||||
@@ -50,6 +46,10 @@ const StyledLogo = styled.img`
|
||||
margin-left: 6px;
|
||||
`
|
||||
|
||||
const ConfirmationModalContentWrapper = styled(AutoColumn)`
|
||||
padding-bottom: 12px;
|
||||
`
|
||||
|
||||
function ConfirmationPendingContent({
|
||||
onDismiss,
|
||||
pendingText,
|
||||
@@ -59,8 +59,6 @@ function ConfirmationPendingContent({
|
||||
pendingText: ReactNode
|
||||
inline?: boolean // not in modal
|
||||
}) {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<AutoColumn gap="md">
|
||||
@@ -74,15 +72,15 @@ function ConfirmationPendingContent({
|
||||
<CustomLightSpinner src={Circle} alt="loader" size={inline ? '40px' : '90px'} />
|
||||
</ConfirmedIcon>
|
||||
<AutoColumn gap="md" justify="center">
|
||||
<Text fontWeight={500} fontSize={20} color={theme.textPrimary} textAlign="center">
|
||||
<ThemedText.SubHeaderLarge color="textPrimary" textAlign="center">
|
||||
<Trans>Waiting for confirmation</Trans>
|
||||
</Text>
|
||||
<Text fontWeight={600} fontSize={16} color={theme.textPrimary} textAlign="center">
|
||||
</ThemedText.SubHeaderLarge>
|
||||
<ThemedText.SubHeader color="textPrimary" textAlign="center">
|
||||
{pendingText}
|
||||
</Text>
|
||||
<Text fontWeight={400} fontSize={12} color={theme.textSecondary} textAlign="center" marginBottom="12px">
|
||||
</ThemedText.SubHeader>
|
||||
<ThemedText.SubHeaderSmall color="textSecondary" textAlign="center" marginBottom="12px">
|
||||
<Trans>Confirm this transaction in your wallet</Trans>
|
||||
</Text>
|
||||
</ThemedText.SubHeaderSmall>
|
||||
</AutoColumn>
|
||||
</AutoColumn>
|
||||
</Wrapper>
|
||||
@@ -125,7 +123,7 @@ function TransactionSubmittedContent({
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Section inline={inline}>
|
||||
<AutoColumn>
|
||||
{!inline && (
|
||||
<RowBetween>
|
||||
<div />
|
||||
@@ -135,7 +133,7 @@ function TransactionSubmittedContent({
|
||||
<ConfirmedIcon inline={inline}>
|
||||
<ArrowUpCircle strokeWidth={1} size={inline ? '40px' : '75px'} color={theme.accentActive} />
|
||||
</ConfirmedIcon>
|
||||
<AutoColumn gap="md" justify="center" style={{ paddingBottom: '12px' }}>
|
||||
<ConfirmationModalContentWrapper gap="md" justify="center">
|
||||
<ThemedText.MediumHeader textAlign="center">
|
||||
<Trans>Transaction submitted</Trans>
|
||||
</ThemedText.MediumHeader>
|
||||
@@ -154,19 +152,19 @@ function TransactionSubmittedContent({
|
||||
</ButtonLight>
|
||||
)}
|
||||
<ButtonPrimary onClick={onDismiss} style={{ margin: '20px 0 0 0' }} data-testid="dismiss-tx-confirmation">
|
||||
<Text fontWeight={600} fontSize={20} color={theme.accentTextLightPrimary}>
|
||||
<ThemedText.HeadlineSmall color={theme.accentTextLightPrimary}>
|
||||
{inline ? <Trans>Return</Trans> : <Trans>Close</Trans>}
|
||||
</Text>
|
||||
</ThemedText.HeadlineSmall>
|
||||
</ButtonPrimary>
|
||||
{chainId && hash && (
|
||||
<ExternalLink href={getExplorerLink(chainId, hash, ExplorerDataType.TRANSACTION)}>
|
||||
<Text fontWeight={600} fontSize={14} color={theme.accentAction}>
|
||||
<ThemedText.Link color={theme.accentAction}>
|
||||
<Trans>View on {chainId === SupportedChainId.MAINNET ? 'Etherscan' : 'Block Explorer'}</Trans>
|
||||
</Text>
|
||||
</ThemedText.Link>
|
||||
</ExternalLink>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</Section>
|
||||
</ConfirmationModalContentWrapper>
|
||||
</AutoColumn>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
@@ -184,15 +182,15 @@ export function ConfirmationModalContent({
|
||||
}) {
|
||||
return (
|
||||
<Wrapper>
|
||||
<Section>
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
{title}
|
||||
</Text>
|
||||
<AutoColumn gap="sm">
|
||||
<Row>
|
||||
<Row justify="center" marginLeft="24px">
|
||||
<ThemedText.SubHeader>{title}</ThemedText.SubHeader>
|
||||
</Row>
|
||||
<CloseIcon onClick={onDismiss} data-cy="confirmation-close-icon" />
|
||||
</RowBetween>
|
||||
</Row>
|
||||
{topContent()}
|
||||
</Section>
|
||||
</AutoColumn>
|
||||
{bottomContent && <BottomSection gap="12px">{bottomContent()}</BottomSection>}
|
||||
</Wrapper>
|
||||
)
|
||||
@@ -202,7 +200,7 @@ export function TransactionErrorContent({ message, onDismiss }: { message: React
|
||||
const theme = useTheme()
|
||||
return (
|
||||
<Wrapper>
|
||||
<Section>
|
||||
<AutoColumn>
|
||||
<RowBetween>
|
||||
<Text fontWeight={600} fontSize={16}>
|
||||
<Trans>Error</Trans>
|
||||
@@ -213,7 +211,7 @@ export function TransactionErrorContent({ message, onDismiss }: { message: React
|
||||
<AlertTriangle color={theme.accentCritical} style={{ strokeWidth: 1 }} size={90} />
|
||||
<ThemedText.MediumHeader textAlign="center">{message}</ThemedText.MediumHeader>
|
||||
</AutoColumn>
|
||||
</Section>
|
||||
</AutoColumn>
|
||||
<BottomSection gap="12px">
|
||||
<ButtonPrimary onClick={onDismiss}>
|
||||
<Trans>Dismiss</Trans>
|
||||
@@ -252,7 +250,7 @@ function L2Content({
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Section inline={inline}>
|
||||
<AutoColumn>
|
||||
{!inline && (
|
||||
<RowBetween mb="16px">
|
||||
<Badge>
|
||||
@@ -277,7 +275,7 @@ function L2Content({
|
||||
)}
|
||||
</ConfirmedIcon>
|
||||
<AutoColumn gap="md" justify="center">
|
||||
<Text fontWeight={500} fontSize={20} textAlign="center">
|
||||
<ThemedText.SubHeaderLarge textAlign="center">
|
||||
{!hash ? (
|
||||
<Trans>Confirm transaction in wallet</Trans>
|
||||
) : !confirmed ? (
|
||||
@@ -287,20 +285,20 @@ function L2Content({
|
||||
) : (
|
||||
<Trans>Error</Trans>
|
||||
)}
|
||||
</Text>
|
||||
<Text fontWeight={400} fontSize={16} textAlign="center">
|
||||
</ThemedText.SubHeaderLarge>
|
||||
<ThemedText.BodySecondary textAlign="center">
|
||||
{transaction ? <TransactionSummary info={transaction.info} /> : pendingText}
|
||||
</Text>
|
||||
</ThemedText.BodySecondary>
|
||||
{chainId && hash ? (
|
||||
<ExternalLink href={getExplorerLink(chainId, hash, ExplorerDataType.TRANSACTION)}>
|
||||
<Text fontWeight={500} fontSize={14} color={theme.accentAction}>
|
||||
<ThemedText.SubHeaderSmall color={theme.accentAction}>
|
||||
<Trans>View on Explorer</Trans>
|
||||
</Text>
|
||||
</ThemedText.SubHeaderSmall>
|
||||
</ExternalLink>
|
||||
) : (
|
||||
<div style={{ height: '17px' }} />
|
||||
)}
|
||||
<Text color={theme.textTertiary} style={{ margin: '20px 0 0 0' }} fontSize="14px">
|
||||
<ThemedText.SubHeaderSmall color={theme.textTertiary} marginTop="20px">
|
||||
{!secondsToConfirm ? (
|
||||
<div style={{ height: '24px' }} />
|
||||
) : (
|
||||
@@ -311,14 +309,14 @@ function L2Content({
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</Text>
|
||||
</ThemedText.SubHeaderSmall>
|
||||
<ButtonPrimary onClick={onDismiss} style={{ margin: '4px 0 0 0' }}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
<ThemedText.SubHeaderLarge>
|
||||
{inline ? <Trans>Return</Trans> : <Trans>Close</Trans>}
|
||||
</Text>
|
||||
</ThemedText.SubHeaderLarge>
|
||||
</ButtonPrimary>
|
||||
</AutoColumn>
|
||||
</Section>
|
||||
</AutoColumn>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import {
|
||||
TEST_ALLOWED_SLIPPAGE,
|
||||
TEST_TOKEN_1,
|
||||
TEST_TRADE_EXACT_INPUT,
|
||||
TEST_TRADE_EXACT_OUTPUT,
|
||||
toCurrencyAmount,
|
||||
} from 'test-utils/constants'
|
||||
import { TEST_ALLOWED_SLIPPAGE, TEST_TRADE_EXACT_INPUT, TEST_TRADE_EXACT_OUTPUT } from 'test-utils/constants'
|
||||
import { act, render, screen } from 'test-utils/render'
|
||||
|
||||
import { AdvancedSwapDetails } from './AdvancedSwapDetails'
|
||||
@@ -27,9 +21,9 @@ describe('AdvancedSwapDetails.tsx', () => {
|
||||
})
|
||||
|
||||
it('renders correct tooltips for test trade with exact output and gas use estimate USD', async () => {
|
||||
TEST_TRADE_EXACT_OUTPUT.gasUseEstimateUSD = toCurrencyAmount(TEST_TOKEN_1, 1)
|
||||
TEST_TRADE_EXACT_OUTPUT.gasUseEstimateUSD = '1.00'
|
||||
render(<AdvancedSwapDetails trade={TEST_TRADE_EXACT_OUTPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} />)
|
||||
await act(() => userEvent.hover(screen.getByText(/Minimum output/i)))
|
||||
await act(() => userEvent.hover(screen.getByText(/Maximum input/i)))
|
||||
expect(await screen.getByText(/The minimum amount you are guaranteed to receive./i)).toBeVisible()
|
||||
await act(() => userEvent.hover(screen.getByText('Network fee')))
|
||||
expect(await screen.getByText(/The fee paid to miners who process your transaction./i)).toBeVisible()
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { LoadingRows } from 'components/Loader/styled'
|
||||
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
|
||||
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
import formatPriceImpact from 'utils/formatPriceImpact'
|
||||
|
||||
import { Separator, ThemedText } from '../../theme'
|
||||
import Column from '../Column'
|
||||
@@ -16,7 +17,7 @@ import RouterLabel from './RouterLabel'
|
||||
import SwapRoute from './SwapRoute'
|
||||
|
||||
interface AdvancedSwapDetailsProps {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType>
|
||||
trade: InterfaceTrade
|
||||
allowedSlippage: Percent
|
||||
syncing?: boolean
|
||||
}
|
||||
@@ -60,10 +61,20 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
|
||||
</ThemedText.BodySmall>
|
||||
</MouseoverTooltip>
|
||||
<TextWithLoadingPlaceholder syncing={syncing} width={50}>
|
||||
<ThemedText.BodySmall>~${trade.gasUseEstimateUSD.toFixed(2)}</ThemedText.BodySmall>
|
||||
<ThemedText.BodySmall>~${trade.gasUseEstimateUSD}</ThemedText.BodySmall>
|
||||
</TextWithLoadingPlaceholder>
|
||||
</RowBetween>
|
||||
)}
|
||||
<RowBetween>
|
||||
<MouseoverTooltip text={<Trans>The impact your trade has on the market price of this pool.</Trans>}>
|
||||
<ThemedText.BodySmall color="textSecondary">
|
||||
<Trans>Price Impact</Trans>
|
||||
</ThemedText.BodySmall>
|
||||
</MouseoverTooltip>
|
||||
<TextWithLoadingPlaceholder syncing={syncing} width={50}>
|
||||
<ThemedText.BodySmall>{formatPriceImpact(trade.priceImpact)}</ThemedText.BodySmall>
|
||||
</TextWithLoadingPlaceholder>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<MouseoverTooltip
|
||||
@@ -75,7 +86,7 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
|
||||
}
|
||||
>
|
||||
<ThemedText.BodySmall color="textSecondary">
|
||||
<Trans>Minimum output</Trans>
|
||||
{trade.tradeType === TradeType.EXACT_INPUT ? <Trans>Minimum output</Trans> : <Trans>Maximum input</Trans>}
|
||||
</ThemedText.BodySmall>
|
||||
</MouseoverTooltip>
|
||||
</RowFixed>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Trace } from '@uniswap/analytics'
|
||||
import { InterfaceModalName } from '@uniswap/analytics-events'
|
||||
import { Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { ReactNode, useCallback, useMemo, useState } from 'react'
|
||||
import { sendAnalyticsEvent, Trace } from '@uniswap/analytics'
|
||||
import { InterfaceModalName, SwapEventName, SwapPriceUpdateUserResponse } from '@uniswap/analytics-events'
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
import { getPriceUpdateBasisPoints } from 'lib/utils/analytics'
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
import { formatSwapPriceUpdatedEventProperties } from 'utils/loggingFormatters'
|
||||
import { tradeMeaningfullyDiffers } from 'utils/tradeMeaningFullyDiffer'
|
||||
|
||||
import TransactionConfirmationModal, {
|
||||
@@ -21,21 +22,17 @@ export default function ConfirmSwapModal({
|
||||
allowedSlippage,
|
||||
onConfirm,
|
||||
onDismiss,
|
||||
recipient,
|
||||
swapErrorMessage,
|
||||
isOpen,
|
||||
attemptingTxn,
|
||||
txHash,
|
||||
swapQuoteReceivedDate,
|
||||
fiatValueInput,
|
||||
fiatValueOutput,
|
||||
}: {
|
||||
isOpen: boolean
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
|
||||
originalTrade: Trade<Currency, Currency, TradeType> | undefined
|
||||
trade: InterfaceTrade
|
||||
originalTrade: InterfaceTrade | undefined
|
||||
attemptingTxn: boolean
|
||||
txHash: string | undefined
|
||||
recipient: string | null
|
||||
allowedSlippage: Percent
|
||||
onAcceptChanges: () => void
|
||||
onConfirm: () => void
|
||||
@@ -45,35 +42,34 @@ export default function ConfirmSwapModal({
|
||||
fiatValueInput: { data?: number; isLoading: boolean }
|
||||
fiatValueOutput: { data?: number; isLoading: boolean }
|
||||
}) {
|
||||
// shouldLogModalCloseEvent lets the child SwapModalHeader component know when modal has been closed
|
||||
// and an event triggered by modal closing should be logged.
|
||||
const [shouldLogModalCloseEvent, setShouldLogModalCloseEvent] = useState(false)
|
||||
const showAcceptChanges = useMemo(
|
||||
() => Boolean(trade && originalTrade && tradeMeaningfullyDiffers(trade, originalTrade)),
|
||||
() => Boolean(originalTrade && tradeMeaningfullyDiffers(trade, originalTrade)),
|
||||
[originalTrade, trade]
|
||||
)
|
||||
|
||||
const [lastExecutionPrice, setLastExecutionPrice] = useState(trade?.executionPrice)
|
||||
const [priceUpdate, setPriceUpdate] = useState<number>()
|
||||
useEffect(() => {
|
||||
if (lastExecutionPrice && !trade.executionPrice.equalTo(lastExecutionPrice)) {
|
||||
setPriceUpdate(getPriceUpdateBasisPoints(lastExecutionPrice, trade.executionPrice))
|
||||
setLastExecutionPrice(trade.executionPrice)
|
||||
}
|
||||
}, [lastExecutionPrice, setLastExecutionPrice, trade])
|
||||
|
||||
const onModalDismiss = useCallback(() => {
|
||||
if (isOpen) setShouldLogModalCloseEvent(true)
|
||||
sendAnalyticsEvent(
|
||||
SwapEventName.SWAP_PRICE_UPDATE_ACKNOWLEDGED,
|
||||
formatSwapPriceUpdatedEventProperties(trade, priceUpdate, SwapPriceUpdateUserResponse.REJECTED)
|
||||
)
|
||||
onDismiss()
|
||||
}, [isOpen, onDismiss])
|
||||
}, [onDismiss, priceUpdate, trade])
|
||||
|
||||
const modalHeader = useCallback(() => {
|
||||
return trade ? (
|
||||
<SwapModalHeader
|
||||
trade={trade}
|
||||
shouldLogModalCloseEvent={shouldLogModalCloseEvent}
|
||||
setShouldLogModalCloseEvent={setShouldLogModalCloseEvent}
|
||||
allowedSlippage={allowedSlippage}
|
||||
recipient={recipient}
|
||||
showAcceptChanges={showAcceptChanges}
|
||||
onAcceptChanges={onAcceptChanges}
|
||||
/>
|
||||
) : null
|
||||
}, [allowedSlippage, onAcceptChanges, recipient, showAcceptChanges, trade, shouldLogModalCloseEvent])
|
||||
return <SwapModalHeader trade={trade} allowedSlippage={allowedSlippage} />
|
||||
}, [allowedSlippage, trade])
|
||||
|
||||
const modalBottom = useCallback(() => {
|
||||
return trade ? (
|
||||
return (
|
||||
<SwapModalFooter
|
||||
onConfirm={onConfirm}
|
||||
trade={trade}
|
||||
@@ -84,25 +80,28 @@ export default function ConfirmSwapModal({
|
||||
swapQuoteReceivedDate={swapQuoteReceivedDate}
|
||||
fiatValueInput={fiatValueInput}
|
||||
fiatValueOutput={fiatValueOutput}
|
||||
showAcceptChanges={showAcceptChanges}
|
||||
onAcceptChanges={onAcceptChanges}
|
||||
/>
|
||||
) : null
|
||||
)
|
||||
}, [
|
||||
trade,
|
||||
onConfirm,
|
||||
txHash,
|
||||
allowedSlippage,
|
||||
showAcceptChanges,
|
||||
swapErrorMessage,
|
||||
trade,
|
||||
allowedSlippage,
|
||||
txHash,
|
||||
swapQuoteReceivedDate,
|
||||
fiatValueInput,
|
||||
fiatValueOutput,
|
||||
onAcceptChanges,
|
||||
])
|
||||
|
||||
// text to show while loading
|
||||
const pendingText = (
|
||||
<Trans>
|
||||
Swapping {trade?.inputAmount?.toSignificant(6)} {trade?.inputAmount?.currency?.symbol} for{' '}
|
||||
{trade?.outputAmount?.toSignificant(6)} {trade?.outputAmount?.currency?.symbol}
|
||||
Swapping {trade.inputAmount.toSignificant(6)} {trade.inputAmount.currency?.symbol} for{' '}
|
||||
{trade.outputAmount.toSignificant(6)} {trade.outputAmount.currency?.symbol}
|
||||
</Trans>
|
||||
)
|
||||
|
||||
@@ -112,7 +111,7 @@ export default function ConfirmSwapModal({
|
||||
<TransactionErrorContent onDismiss={onModalDismiss} message={swapErrorMessage} />
|
||||
) : (
|
||||
<ConfirmationModalContent
|
||||
title={<Trans>Confirm Swap</Trans>}
|
||||
title={<Trans>Review Swap</Trans>}
|
||||
onDismiss={onModalDismiss}
|
||||
topContent={modalHeader}
|
||||
bottomContent={modalBottom}
|
||||
@@ -124,13 +123,13 @@ export default function ConfirmSwapModal({
|
||||
return (
|
||||
<Trace modal={InterfaceModalName.CONFIRM_SWAP}>
|
||||
<TransactionConfirmationModal
|
||||
isOpen={isOpen}
|
||||
isOpen
|
||||
onDismiss={onModalDismiss}
|
||||
attemptingTxn={attemptingTxn}
|
||||
hash={txHash}
|
||||
content={confirmationContent}
|
||||
pendingText={pendingText}
|
||||
currencyToAdd={trade?.outputAmount.currency}
|
||||
currencyToAdd={trade.outputAmount.currency}
|
||||
/>
|
||||
</Trace>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
|
||||
import { Currency, TradeType } from '@uniswap/sdk-core'
|
||||
import { LoadingOpacityContainer } from 'components/Loader/styled'
|
||||
import { RowFixed } from 'components/Row'
|
||||
import { MouseoverTooltip, TooltipSize } from 'components/Tooltip'
|
||||
@@ -26,14 +25,14 @@ export default function GasEstimateTooltip({
|
||||
loading,
|
||||
disabled,
|
||||
}: {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> // dollar amount in active chain's stablecoin
|
||||
trade: InterfaceTrade // dollar amount in active chain's stablecoin
|
||||
loading: boolean
|
||||
disabled?: boolean
|
||||
}) {
|
||||
const formattedGasPriceString = trade?.gasUseEstimateUSD
|
||||
? trade.gasUseEstimateUSD.toFixed(2) === '0.00'
|
||||
? trade.gasUseEstimateUSD === '0.00'
|
||||
? '<$0.01'
|
||||
: '$' + trade.gasUseEstimateUSD.toFixed(2)
|
||||
: '$' + trade.gasUseEstimateUSD
|
||||
: undefined
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Percent } from '@uniswap/sdk-core'
|
||||
import { OutlineCard } from 'components/Card'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { opacify } from 'theme/utils'
|
||||
import formatPriceImpact from 'utils/formatPriceImpact'
|
||||
|
||||
import { ThemedText } from '../../theme'
|
||||
import { AutoColumn } from '../Column'
|
||||
@@ -18,8 +19,6 @@ interface PriceImpactWarningProps {
|
||||
priceImpact: Percent
|
||||
}
|
||||
|
||||
const formatPriceImpact = (priceImpact: Percent) => `${priceImpact.multiply(-1).toFixed(2)}%`
|
||||
|
||||
export default function PriceImpactWarning({ priceImpact }: PriceImpactWarningProps) {
|
||||
const theme = useTheme()
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { TEST_ALLOWED_SLIPPAGE, TEST_TOKEN_1, TEST_TRADE_EXACT_INPUT, toCurrencyAmount } from 'test-utils/constants'
|
||||
import { TEST_ALLOWED_SLIPPAGE, TEST_TRADE_EXACT_INPUT } from 'test-utils/constants'
|
||||
import { act, render, screen } from 'test-utils/render'
|
||||
|
||||
import SwapDetailsDropdown from './SwapDetailsDropdown'
|
||||
@@ -25,7 +25,7 @@ describe('SwapDetailsDropdown.tsx', () => {
|
||||
})
|
||||
|
||||
it('is interactive once loaded', async () => {
|
||||
TEST_TRADE_EXACT_INPUT.gasUseEstimateUSD = toCurrencyAmount(TEST_TOKEN_1, 1)
|
||||
TEST_TRADE_EXACT_INPUT.gasUseEstimateUSD = '1.00'
|
||||
render(
|
||||
<SwapDetailsDropdown
|
||||
trade={TEST_TRADE_EXACT_INPUT}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import AnimatedDropdown from 'components/AnimatedDropdown'
|
||||
import Column from 'components/Column'
|
||||
@@ -92,7 +92,7 @@ const Wrapper = styled(Column)`
|
||||
`
|
||||
|
||||
interface SwapDetailsInlineProps {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
|
||||
trade: InterfaceTrade | undefined
|
||||
syncing: boolean
|
||||
loading: boolean
|
||||
allowedSlippage: Percent
|
||||
|
||||
@@ -1,28 +1,21 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
import { useFiatOnRampButtonEnabled } from 'featureFlags/flags/fiatOnRampButton'
|
||||
import { subhead } from 'nft/css/common.css'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import SettingsTab from '../Settings'
|
||||
import SwapBuyFiatButton from './SwapBuyFiatButton'
|
||||
|
||||
const StyledSwapHeader = styled.div`
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 8px;
|
||||
width: 100%;
|
||||
const StyledSwapHeader = styled(RowBetween)`
|
||||
margin-bottom: 10px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
`
|
||||
|
||||
const TextHeader = styled.div`
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
margin-right: 8px;
|
||||
display: flex;
|
||||
line-height: 20px;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
const HeaderButtonContainer = styled(RowFixed)`
|
||||
padding: 0 12px;
|
||||
gap: 16px;
|
||||
`
|
||||
|
||||
export default function SwapHeader({ autoSlippage }: { autoSlippage: Percent }) {
|
||||
@@ -30,17 +23,15 @@ export default function SwapHeader({ autoSlippage }: { autoSlippage: Percent })
|
||||
|
||||
return (
|
||||
<StyledSwapHeader>
|
||||
<RowBetween>
|
||||
<RowFixed style={{ gap: '8px' }}>
|
||||
<TextHeader className={subhead}>
|
||||
<Trans>Swap</Trans>
|
||||
</TextHeader>
|
||||
{fiatOnRampButtonEnabled && <SwapBuyFiatButton />}
|
||||
</RowFixed>
|
||||
<RowFixed>
|
||||
<SettingsTab autoSlippage={autoSlippage} />
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
<HeaderButtonContainer>
|
||||
<ThemedText.SubHeader>
|
||||
<Trans>Swap</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
{fiatOnRampButtonEnabled && <SwapBuyFiatButton />}
|
||||
</HeaderButtonContainer>
|
||||
<RowFixed>
|
||||
<SettingsTab autoSlippage={autoSlippage} />
|
||||
</RowFixed>
|
||||
</StyledSwapHeader>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,27 +1,103 @@
|
||||
import { TEST_ALLOWED_SLIPPAGE, TEST_TRADE_EXACT_INPUT } from 'test-utils/constants'
|
||||
import { render, screen } from 'test-utils/render'
|
||||
import { TEST_ALLOWED_SLIPPAGE, TEST_TRADE_EXACT_INPUT, TEST_TRADE_EXACT_OUTPUT } from 'test-utils/constants'
|
||||
import { render, screen, within } from 'test-utils/render'
|
||||
|
||||
import SwapModalFooter from './SwapModalFooter'
|
||||
|
||||
const swapErrorMessage = 'swap error'
|
||||
const fiatValue = { data: 123, isLoading: false }
|
||||
|
||||
describe('SwapModalFooter.tsx', () => {
|
||||
it('renders with a disabled button with no account', () => {
|
||||
it('matches base snapshot, test trade exact input', () => {
|
||||
const { asFragment } = render(
|
||||
<SwapModalFooter
|
||||
trade={TEST_TRADE_EXACT_INPUT}
|
||||
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
|
||||
hash={undefined}
|
||||
onConfirm={() => null}
|
||||
disabledConfirm
|
||||
swapErrorMessage={swapErrorMessage}
|
||||
onConfirm={jest.fn()}
|
||||
swapErrorMessage={undefined}
|
||||
disabledConfirm={false}
|
||||
swapQuoteReceivedDate={undefined}
|
||||
fiatValueInput={fiatValue}
|
||||
fiatValueOutput={fiatValue}
|
||||
fiatValueInput={{
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
}}
|
||||
fiatValueOutput={{
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
}}
|
||||
showAcceptChanges={false}
|
||||
onAcceptChanges={jest.fn()}
|
||||
/>
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
expect(screen.getByTestId('confirm-swap-button')).toBeDisabled()
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
'The minimum amount you are guaranteed to receive. If the price slips any further, your transaction will revert.'
|
||||
)
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText('The fee paid to miners who process your transaction. This must be paid in $ETH.')
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByText('The impact your trade has on the market price of this pool.')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows accept changes section when available', () => {
|
||||
const mockAcceptChanges = jest.fn()
|
||||
render(
|
||||
<SwapModalFooter
|
||||
trade={TEST_TRADE_EXACT_INPUT}
|
||||
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
|
||||
hash={undefined}
|
||||
onConfirm={jest.fn()}
|
||||
swapErrorMessage={undefined}
|
||||
disabledConfirm={false}
|
||||
swapQuoteReceivedDate={undefined}
|
||||
fiatValueInput={{
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
}}
|
||||
fiatValueOutput={{
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
}}
|
||||
showAcceptChanges={true}
|
||||
onAcceptChanges={mockAcceptChanges}
|
||||
/>
|
||||
)
|
||||
const showAcceptChanges = screen.getByTestId('show-accept-changes')
|
||||
expect(showAcceptChanges).toBeInTheDocument()
|
||||
expect(within(showAcceptChanges).getByText('Price updated')).toBeVisible()
|
||||
expect(within(showAcceptChanges).getByText('Accept')).toBeVisible()
|
||||
})
|
||||
|
||||
it('test trade exact output, no recipient', () => {
|
||||
render(
|
||||
<SwapModalFooter
|
||||
trade={TEST_TRADE_EXACT_OUTPUT}
|
||||
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
|
||||
hash={undefined}
|
||||
onConfirm={jest.fn()}
|
||||
swapErrorMessage={undefined}
|
||||
disabledConfirm={false}
|
||||
swapQuoteReceivedDate={undefined}
|
||||
fiatValueInput={{
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
}}
|
||||
fiatValueOutput={{
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
}}
|
||||
showAcceptChanges={true}
|
||||
onAcceptChanges={jest.fn()}
|
||||
/>
|
||||
)
|
||||
expect(
|
||||
screen.getByText(
|
||||
'The maximum amount you are guaranteed to spend. If the price slips any further, your transaction will revert.'
|
||||
)
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText('The fee paid to miners who process your transaction. This must be paid in $ETH.')
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByText('The impact your trade has on the market price of this pool.')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,105 +1,48 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { formatPriceImpact } from '@uniswap/conedison/format'
|
||||
import { Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import Column from 'components/Column'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import useTransactionDeadline from 'hooks/useTransactionDeadline'
|
||||
import {
|
||||
formatPercentInBasisPointsNumber,
|
||||
formatPercentNumber,
|
||||
formatToDecimal,
|
||||
getDurationFromDateMilliseconds,
|
||||
getDurationUntilTimestampSeconds,
|
||||
getTokenAddress,
|
||||
} from 'lib/utils/analytics'
|
||||
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
import { ReactNode } from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { RouterPreference } from 'state/routing/slice'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
import { useRouterPreference, useUserSlippageTolerance } from 'state/user/hooks'
|
||||
import getRoutingDiagramEntries, { RoutingDiagramEntry } from 'utils/getRoutingDiagramEntries'
|
||||
import { computeRealizedPriceImpact } from 'utils/prices'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { formatTransactionAmount, priceToPreciseFloat } from 'utils/formatNumbers'
|
||||
import getRoutingDiagramEntries from 'utils/getRoutingDiagramEntries'
|
||||
import { formatSwapButtonClickEventProperties } from 'utils/loggingFormatters'
|
||||
import { getPriceImpactWarning } from 'utils/prices'
|
||||
|
||||
import { ButtonError } from '../Button'
|
||||
import { AutoRow } from '../Row'
|
||||
import { SwapCallbackError } from './styleds'
|
||||
import { ButtonError, SmallButtonPrimary } from '../Button'
|
||||
import Row, { AutoRow, RowBetween, RowFixed } from '../Row'
|
||||
import { SwapCallbackError, SwapShowAcceptChanges } from './styleds'
|
||||
import { Label } from './SwapModalHeaderAmount'
|
||||
|
||||
interface AnalyticsEventProps {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType>
|
||||
hash: string | undefined
|
||||
allowedSlippage: Percent
|
||||
transactionDeadlineSecondsSinceEpoch: number | undefined
|
||||
isAutoSlippage: boolean
|
||||
isAutoRouterApi: boolean
|
||||
swapQuoteReceivedDate: Date | undefined
|
||||
routes: RoutingDiagramEntry[]
|
||||
fiatValueInput?: number
|
||||
fiatValueOutput?: number
|
||||
}
|
||||
const DetailsContainer = styled(Column)`
|
||||
padding: 0 8px;
|
||||
`
|
||||
|
||||
const formatRoutesEventProperties = (routes: RoutingDiagramEntry[]) => {
|
||||
const routesEventProperties: Record<string, any[]> = {
|
||||
routes_percentages: [],
|
||||
routes_protocols: [],
|
||||
}
|
||||
const StyledAlertTriangle = styled(AlertTriangle)`
|
||||
margin-right: 8px;
|
||||
min-width: 24px;
|
||||
`
|
||||
|
||||
routes.forEach((route, index) => {
|
||||
routesEventProperties['routes_percentages'].push(formatPercentNumber(route.percent))
|
||||
routesEventProperties['routes_protocols'].push(route.protocol)
|
||||
routesEventProperties[`route_${index}_input_currency_symbols`] = route.path.map(
|
||||
(pathStep) => pathStep[0].symbol ?? ''
|
||||
)
|
||||
routesEventProperties[`route_${index}_output_currency_symbols`] = route.path.map(
|
||||
(pathStep) => pathStep[1].symbol ?? ''
|
||||
)
|
||||
routesEventProperties[`route_${index}_input_currency_addresses`] = route.path.map((pathStep) =>
|
||||
getTokenAddress(pathStep[0])
|
||||
)
|
||||
routesEventProperties[`route_${index}_output_currency_addresses`] = route.path.map((pathStep) =>
|
||||
getTokenAddress(pathStep[1])
|
||||
)
|
||||
routesEventProperties[`route_${index}_fee_amounts_hundredths_of_bps`] = route.path.map((pathStep) => pathStep[2])
|
||||
})
|
||||
const ConfirmButton = styled(ButtonError)`
|
||||
height: 56px;
|
||||
margin-top: 10px;
|
||||
`
|
||||
|
||||
return routesEventProperties
|
||||
}
|
||||
|
||||
const formatAnalyticsEventProperties = ({
|
||||
trade,
|
||||
hash,
|
||||
allowedSlippage,
|
||||
transactionDeadlineSecondsSinceEpoch,
|
||||
isAutoSlippage,
|
||||
isAutoRouterApi,
|
||||
swapQuoteReceivedDate,
|
||||
routes,
|
||||
fiatValueInput,
|
||||
fiatValueOutput,
|
||||
}: AnalyticsEventProps) => ({
|
||||
estimated_network_fee_usd: trade.gasUseEstimateUSD ? formatToDecimal(trade.gasUseEstimateUSD, 2) : undefined,
|
||||
transaction_hash: hash,
|
||||
transaction_deadline_seconds: getDurationUntilTimestampSeconds(transactionDeadlineSecondsSinceEpoch),
|
||||
token_in_address: getTokenAddress(trade.inputAmount.currency),
|
||||
token_out_address: getTokenAddress(trade.outputAmount.currency),
|
||||
token_in_symbol: trade.inputAmount.currency.symbol,
|
||||
token_out_symbol: trade.outputAmount.currency.symbol,
|
||||
token_in_amount: formatToDecimal(trade.inputAmount, trade.inputAmount.currency.decimals),
|
||||
token_out_amount: formatToDecimal(trade.outputAmount, trade.outputAmount.currency.decimals),
|
||||
token_in_amount_usd: fiatValueInput,
|
||||
token_out_amount_usd: fiatValueOutput,
|
||||
price_impact_basis_points: formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)),
|
||||
allowed_slippage_basis_points: formatPercentInBasisPointsNumber(allowedSlippage),
|
||||
is_auto_router_api: isAutoRouterApi,
|
||||
is_auto_slippage: isAutoSlippage,
|
||||
chain_id:
|
||||
trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId
|
||||
? trade.inputAmount.currency.chainId
|
||||
: undefined,
|
||||
duration_from_first_quote_to_swap_submission_milliseconds: swapQuoteReceivedDate
|
||||
? getDurationFromDateMilliseconds(swapQuoteReceivedDate)
|
||||
: undefined,
|
||||
swap_quote_block_number: trade.blockNumber,
|
||||
...formatRoutesEventProperties(routes),
|
||||
})
|
||||
const DetailRowValue = styled(ThemedText.BodySmall)`
|
||||
text-align: right;
|
||||
overflow-wrap: break-word;
|
||||
`
|
||||
|
||||
export default function SwapModalFooter({
|
||||
trade,
|
||||
@@ -111,8 +54,10 @@ export default function SwapModalFooter({
|
||||
swapQuoteReceivedDate,
|
||||
fiatValueInput,
|
||||
fiatValueOutput,
|
||||
showAcceptChanges,
|
||||
onAcceptChanges,
|
||||
}: {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType>
|
||||
trade: InterfaceTrade
|
||||
hash: string | undefined
|
||||
allowedSlippage: Percent
|
||||
onConfirm: () => void
|
||||
@@ -121,46 +66,142 @@ export default function SwapModalFooter({
|
||||
swapQuoteReceivedDate: Date | undefined
|
||||
fiatValueInput: { data?: number; isLoading: boolean }
|
||||
fiatValueOutput: { data?: number; isLoading: boolean }
|
||||
showAcceptChanges: boolean
|
||||
onAcceptChanges: () => void
|
||||
}) {
|
||||
const transactionDeadlineSecondsSinceEpoch = useTransactionDeadline()?.toNumber() // in seconds since epoch
|
||||
const isAutoSlippage = useUserSlippageTolerance()[0] === 'auto'
|
||||
const [routerPreference] = useRouterPreference()
|
||||
const routes = getRoutingDiagramEntries(trade)
|
||||
const theme = useTheme()
|
||||
const { chainId } = useWeb3React()
|
||||
const nativeCurrency = useNativeCurrency(chainId)
|
||||
|
||||
const label = `${trade.executionPrice.baseCurrency?.symbol} `
|
||||
const labelInverted = `${trade.executionPrice.quoteCurrency?.symbol}`
|
||||
const formattedPrice = formatTransactionAmount(priceToPreciseFloat(trade.executionPrice))
|
||||
|
||||
return (
|
||||
<>
|
||||
<AutoRow>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
element={InterfaceElementName.CONFIRM_SWAP_BUTTON}
|
||||
name={SwapEventName.SWAP_SUBMITTED_BUTTON_CLICKED}
|
||||
properties={formatAnalyticsEventProperties({
|
||||
trade,
|
||||
hash,
|
||||
allowedSlippage,
|
||||
transactionDeadlineSecondsSinceEpoch,
|
||||
isAutoSlippage,
|
||||
isAutoRouterApi: routerPreference === RouterPreference.AUTO || routerPreference === RouterPreference.API,
|
||||
swapQuoteReceivedDate,
|
||||
routes,
|
||||
fiatValueInput: fiatValueInput.data,
|
||||
fiatValueOutput: fiatValueOutput.data,
|
||||
})}
|
||||
>
|
||||
<ButtonError
|
||||
data-testid="confirm-swap-button"
|
||||
onClick={onConfirm}
|
||||
disabled={disabledConfirm}
|
||||
style={{ margin: '10px 0 0 0' }}
|
||||
id={InterfaceElementName.CONFIRM_SWAP_BUTTON}
|
||||
<DetailsContainer gap="md">
|
||||
<ThemedText.BodySmall>
|
||||
<Row align="flex-start" justify="space-between" gap="sm">
|
||||
<Label>
|
||||
<Trans>Exchange rate</Trans>
|
||||
</Label>
|
||||
<DetailRowValue>{`1 ${labelInverted} = ${formattedPrice ?? '-'} ${label}`}</DetailRowValue>
|
||||
</Row>
|
||||
</ThemedText.BodySmall>
|
||||
<ThemedText.BodySmall>
|
||||
<Row align="flex-start" justify="space-between" gap="sm">
|
||||
<MouseoverTooltip
|
||||
text={
|
||||
<Trans>
|
||||
The fee paid to miners who process your transaction. This must be paid in ${nativeCurrency.symbol}.
|
||||
</Trans>
|
||||
}
|
||||
>
|
||||
<Label cursor="help">
|
||||
<Trans>Network fee</Trans>
|
||||
</Label>
|
||||
</MouseoverTooltip>
|
||||
<DetailRowValue>{trade.gasUseEstimateUSD ? `~$${trade.gasUseEstimateUSD}` : '-'}</DetailRowValue>
|
||||
</Row>
|
||||
</ThemedText.BodySmall>
|
||||
<ThemedText.BodySmall>
|
||||
<Row align="flex-start" justify="space-between" gap="sm">
|
||||
<MouseoverTooltip text={<Trans>The impact your trade has on the market price of this pool.</Trans>}>
|
||||
<Label cursor="help">
|
||||
<Trans>Price impact</Trans>
|
||||
</Label>
|
||||
</MouseoverTooltip>
|
||||
<DetailRowValue color={getPriceImpactWarning(trade.priceImpact)}>
|
||||
{trade.priceImpact ? formatPriceImpact(trade.priceImpact) : '-'}
|
||||
</DetailRowValue>
|
||||
</Row>
|
||||
</ThemedText.BodySmall>
|
||||
<ThemedText.BodySmall>
|
||||
<Row align="flex-start" justify="space-between" gap="sm">
|
||||
<MouseoverTooltip
|
||||
text={
|
||||
trade.tradeType === TradeType.EXACT_INPUT ? (
|
||||
<Trans>
|
||||
The minimum amount you are guaranteed to receive. If the price slips any further, your transaction
|
||||
will revert.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
The maximum amount you are guaranteed to spend. If the price slips any further, your transaction
|
||||
will revert.
|
||||
</Trans>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Label cursor="help">
|
||||
{trade.tradeType === TradeType.EXACT_INPUT ? (
|
||||
<Trans>Minimum received</Trans>
|
||||
) : (
|
||||
<Trans>Maximum sent</Trans>
|
||||
)}
|
||||
</Label>
|
||||
</MouseoverTooltip>
|
||||
<DetailRowValue>
|
||||
{trade.tradeType === TradeType.EXACT_INPUT
|
||||
? `${trade.minimumAmountOut(allowedSlippage).toSignificant(6)} ${trade.outputAmount.currency.symbol}`
|
||||
: `${trade.maximumAmountIn(allowedSlippage).toSignificant(6)} ${trade.inputAmount.currency.symbol}`}
|
||||
</DetailRowValue>
|
||||
</Row>
|
||||
</ThemedText.BodySmall>
|
||||
</DetailsContainer>
|
||||
{showAcceptChanges ? (
|
||||
<SwapShowAcceptChanges data-testid="show-accept-changes">
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<StyledAlertTriangle size={20} />
|
||||
<ThemedText.DeprecatedMain color={theme.accentAction}>
|
||||
<Trans>Price updated</Trans>
|
||||
</ThemedText.DeprecatedMain>
|
||||
</RowFixed>
|
||||
<SmallButtonPrimary onClick={onAcceptChanges}>
|
||||
<Trans>Accept</Trans>
|
||||
</SmallButtonPrimary>
|
||||
</RowBetween>
|
||||
</SwapShowAcceptChanges>
|
||||
) : (
|
||||
<AutoRow>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
element={InterfaceElementName.CONFIRM_SWAP_BUTTON}
|
||||
name={SwapEventName.SWAP_SUBMITTED_BUTTON_CLICKED}
|
||||
properties={formatSwapButtonClickEventProperties({
|
||||
trade,
|
||||
hash,
|
||||
allowedSlippage,
|
||||
transactionDeadlineSecondsSinceEpoch,
|
||||
isAutoSlippage,
|
||||
isAutoRouterApi: routerPreference === RouterPreference.AUTO || routerPreference === RouterPreference.API,
|
||||
swapQuoteReceivedDate,
|
||||
routes,
|
||||
fiatValueInput: fiatValueInput.data,
|
||||
fiatValueOutput: fiatValueOutput.data,
|
||||
})}
|
||||
>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
<Trans>Confirm Swap</Trans>
|
||||
</Text>
|
||||
</ButtonError>
|
||||
</TraceEvent>
|
||||
{swapErrorMessage ? <SwapCallbackError error={swapErrorMessage} /> : null}
|
||||
</AutoRow>
|
||||
<ConfirmButton
|
||||
data-testid="confirm-swap-button"
|
||||
onClick={onConfirm}
|
||||
disabled={disabledConfirm}
|
||||
$borderRadius="12px"
|
||||
id={InterfaceElementName.CONFIRM_SWAP_BUTTON}
|
||||
>
|
||||
<ThemedText.HeadlineSmall color="accentTextLightPrimary">
|
||||
<Trans>Swap</Trans>
|
||||
</ThemedText.HeadlineSmall>
|
||||
</ConfirmButton>
|
||||
</TraceEvent>
|
||||
|
||||
{swapErrorMessage ? <SwapCallbackError error={swapErrorMessage} /> : null}
|
||||
</AutoRow>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,83 +1,44 @@
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import {
|
||||
TEST_ALLOWED_SLIPPAGE,
|
||||
TEST_RECIPIENT_ADDRESS,
|
||||
TEST_TRADE_EXACT_INPUT,
|
||||
TEST_TRADE_EXACT_OUTPUT,
|
||||
} from 'test-utils/constants'
|
||||
import { render, screen, within } from 'test-utils/render'
|
||||
import noop from 'utils/noop'
|
||||
import { formatCurrencyAmount, NumberType } from '@uniswap/conedison/format'
|
||||
import { TEST_ALLOWED_SLIPPAGE, TEST_TRADE_EXACT_INPUT, TEST_TRADE_EXACT_OUTPUT } from 'test-utils/constants'
|
||||
import { render, screen } from 'test-utils/render'
|
||||
|
||||
import SwapModalHeader from './SwapModalHeader'
|
||||
|
||||
jest.mock('@uniswap/analytics')
|
||||
const mockSendAnalyticsEvent = sendAnalyticsEvent as jest.MockedFunction<typeof sendAnalyticsEvent>
|
||||
|
||||
describe('SwapModalHeader.tsx', () => {
|
||||
let sendAnalyticsEventMock: jest.Mock<any, any>
|
||||
|
||||
beforeAll(() => {
|
||||
sendAnalyticsEventMock = jest.fn()
|
||||
})
|
||||
|
||||
it('matches base snapshot for test trade with exact input', () => {
|
||||
it('matches base snapshot, test trade exact input', () => {
|
||||
const { asFragment } = render(
|
||||
<SwapModalHeader
|
||||
trade={TEST_TRADE_EXACT_INPUT}
|
||||
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
|
||||
shouldLogModalCloseEvent={false}
|
||||
showAcceptChanges={false}
|
||||
setShouldLogModalCloseEvent={noop}
|
||||
onAcceptChanges={noop}
|
||||
recipient={TEST_RECIPIENT_ADDRESS}
|
||||
/>
|
||||
<SwapModalHeader trade={TEST_TRADE_EXACT_INPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} />
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
expect(screen.getByText(/Output is estimated. You will receive at least /i)).toBeInTheDocument()
|
||||
expect(screen.getByTestId('INPUT-amount')).toHaveTextContent(
|
||||
`${formatCurrencyAmount(TEST_TRADE_EXACT_INPUT.inputAmount, NumberType.TokenTx)} ${
|
||||
TEST_TRADE_EXACT_INPUT.inputAmount.currency.symbol ?? ''
|
||||
}`
|
||||
)
|
||||
expect(screen.getByTestId('OUTPUT-amount')).toHaveTextContent(
|
||||
`${formatCurrencyAmount(TEST_TRADE_EXACT_INPUT.outputAmount, NumberType.TokenTx)} ${
|
||||
TEST_TRADE_EXACT_INPUT.outputAmount.currency.symbol ?? ''
|
||||
}`
|
||||
)
|
||||
})
|
||||
|
||||
it('shows accept changes section and logs amplitude event', () => {
|
||||
const setShouldLogModalCloseEventFn = jest.fn()
|
||||
mockSendAnalyticsEvent.mockImplementation(sendAnalyticsEventMock)
|
||||
render(
|
||||
<SwapModalHeader
|
||||
trade={TEST_TRADE_EXACT_INPUT}
|
||||
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
|
||||
shouldLogModalCloseEvent
|
||||
showAcceptChanges
|
||||
setShouldLogModalCloseEvent={setShouldLogModalCloseEventFn}
|
||||
onAcceptChanges={noop}
|
||||
recipient={TEST_RECIPIENT_ADDRESS}
|
||||
/>
|
||||
it('test trade exact output, no recipient', () => {
|
||||
const { asFragment } = render(
|
||||
<SwapModalHeader trade={TEST_TRADE_EXACT_OUTPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} />
|
||||
)
|
||||
expect(setShouldLogModalCloseEventFn).toHaveBeenCalledWith(false)
|
||||
const showAcceptChanges = screen.getByTestId('show-accept-changes')
|
||||
expect(showAcceptChanges).toBeInTheDocument()
|
||||
expect(within(showAcceptChanges).getByText('Price Updated')).toBeVisible()
|
||||
expect(within(showAcceptChanges).getByText('Accept')).toBeVisible()
|
||||
expect(sendAnalyticsEventMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('renders correctly for test trade with exact output and no recipient', () => {
|
||||
const rendered = render(
|
||||
<SwapModalHeader
|
||||
trade={TEST_TRADE_EXACT_OUTPUT}
|
||||
allowedSlippage={TEST_ALLOWED_SLIPPAGE}
|
||||
shouldLogModalCloseEvent={false}
|
||||
showAcceptChanges={false}
|
||||
setShouldLogModalCloseEvent={noop}
|
||||
onAcceptChanges={noop}
|
||||
recipient={null}
|
||||
/>
|
||||
)
|
||||
expect(rendered.queryByTestId('recipient-info')).toBeNull()
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
expect(screen.getByText(/Input is estimated. You will sell at most/i)).toBeInTheDocument()
|
||||
expect(screen.getByTestId('input-symbol')).toHaveTextContent(
|
||||
TEST_TRADE_EXACT_OUTPUT.inputAmount.currency.symbol ?? ''
|
||||
|
||||
expect(screen.getByTestId('INPUT-amount')).toHaveTextContent(
|
||||
`${formatCurrencyAmount(TEST_TRADE_EXACT_OUTPUT.inputAmount, NumberType.TokenTx)} ${
|
||||
TEST_TRADE_EXACT_OUTPUT.inputAmount.currency.symbol ?? ''
|
||||
}`
|
||||
)
|
||||
expect(screen.getByTestId('output-symbol')).toHaveTextContent(
|
||||
TEST_TRADE_EXACT_OUTPUT.outputAmount.currency.symbol ?? ''
|
||||
expect(screen.getByTestId('OUTPUT-amount')).toHaveTextContent(
|
||||
`${formatCurrencyAmount(TEST_TRADE_EXACT_OUTPUT.outputAmount, NumberType.TokenTx)} ${
|
||||
TEST_TRADE_EXACT_OUTPUT.outputAmount.currency.symbol ?? ''
|
||||
}`
|
||||
)
|
||||
expect(screen.getByTestId('input-amount')).toHaveTextContent(TEST_TRADE_EXACT_OUTPUT.inputAmount.toExact())
|
||||
expect(screen.getByTestId('output-amount')).toHaveTextContent(TEST_TRADE_EXACT_OUTPUT.outputAmount.toExact())
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,216 +1,72 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { SwapEventName, SwapPriceUpdateUserResponse } from '@uniswap/analytics-events'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import Column, { AutoColumn } from 'components/Column'
|
||||
import { useUSDPrice } from 'hooks/useUSDPrice'
|
||||
import { getPriceUpdateBasisPoints } from 'lib/utils/analytics'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { AlertTriangle, ArrowDown } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { Field } from 'state/swap/actions'
|
||||
import styled from 'styled-components/macro'
|
||||
import { Divider, ThemedText } from 'theme'
|
||||
|
||||
import { ThemedText } from '../../theme'
|
||||
import { isAddress, shortenAddress } from '../../utils'
|
||||
import { computeFiatValuePriceImpact } from '../../utils/computeFiatValuePriceImpact'
|
||||
import { ButtonPrimary } from '../Button'
|
||||
import { LightCard } from '../Card'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { FiatValue } from '../CurrencyInputPanel/FiatValue'
|
||||
import CurrencyLogo from '../Logo/CurrencyLogo'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import TradePrice from '../swap/TradePrice'
|
||||
import { AdvancedSwapDetails } from './AdvancedSwapDetails'
|
||||
import { SwapShowAcceptChanges, TruncatedText } from './styleds'
|
||||
import { SwapModalHeaderAmount } from './SwapModalHeaderAmount'
|
||||
|
||||
const ArrowWrapper = styled.div`
|
||||
padding: 4px;
|
||||
border-radius: 12px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
position: relative;
|
||||
margin-top: -18px;
|
||||
margin-bottom: -18px;
|
||||
left: calc(50% - 16px);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
border: 4px solid;
|
||||
border-color: ${({ theme }) => theme.backgroundModule};
|
||||
z-index: 2;
|
||||
const Rule = styled(Divider)`
|
||||
margin: 16px 2px 24px 2px;
|
||||
`
|
||||
|
||||
const formatAnalyticsEventProperties = (
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType>,
|
||||
priceUpdate: number | undefined,
|
||||
response: SwapPriceUpdateUserResponse
|
||||
) => ({
|
||||
chain_id:
|
||||
trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId
|
||||
? trade.inputAmount.currency.chainId
|
||||
: undefined,
|
||||
response,
|
||||
token_in_symbol: trade.inputAmount.currency.symbol,
|
||||
token_out_symbol: trade.outputAmount.currency.symbol,
|
||||
price_update_basis_points: priceUpdate,
|
||||
})
|
||||
const HeaderContainer = styled(AutoColumn)`
|
||||
margin-top: 16px;
|
||||
`
|
||||
|
||||
export default function SwapModalHeader({
|
||||
trade,
|
||||
shouldLogModalCloseEvent,
|
||||
setShouldLogModalCloseEvent,
|
||||
allowedSlippage,
|
||||
recipient,
|
||||
showAcceptChanges,
|
||||
onAcceptChanges,
|
||||
}: {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType>
|
||||
shouldLogModalCloseEvent: boolean
|
||||
setShouldLogModalCloseEvent: (shouldLog: boolean) => void
|
||||
trade: InterfaceTrade
|
||||
allowedSlippage: Percent
|
||||
recipient: string | null
|
||||
showAcceptChanges: boolean
|
||||
onAcceptChanges: () => void
|
||||
}) {
|
||||
const theme = useTheme()
|
||||
|
||||
const [lastExecutionPrice, setLastExecutionPrice] = useState(trade.executionPrice)
|
||||
const [priceUpdate, setPriceUpdate] = useState<number | undefined>()
|
||||
|
||||
const fiatValueInput = useUSDPrice(trade.inputAmount)
|
||||
const fiatValueOutput = useUSDPrice(trade.outputAmount)
|
||||
|
||||
useEffect(() => {
|
||||
if (!trade.executionPrice.equalTo(lastExecutionPrice)) {
|
||||
setPriceUpdate(getPriceUpdateBasisPoints(lastExecutionPrice, trade.executionPrice))
|
||||
setLastExecutionPrice(trade.executionPrice)
|
||||
}
|
||||
}, [lastExecutionPrice, setLastExecutionPrice, trade.executionPrice])
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldLogModalCloseEvent && showAcceptChanges) {
|
||||
sendAnalyticsEvent(
|
||||
SwapEventName.SWAP_PRICE_UPDATE_ACKNOWLEDGED,
|
||||
formatAnalyticsEventProperties(trade, priceUpdate, SwapPriceUpdateUserResponse.REJECTED)
|
||||
)
|
||||
}
|
||||
setShouldLogModalCloseEvent(false)
|
||||
}, [shouldLogModalCloseEvent, showAcceptChanges, setShouldLogModalCloseEvent, trade, priceUpdate])
|
||||
|
||||
return (
|
||||
<AutoColumn gap="4px" style={{ marginTop: '1rem' }}>
|
||||
<LightCard padding="0.75rem 1rem">
|
||||
<AutoColumn gap="sm">
|
||||
<RowBetween align="center">
|
||||
<RowFixed gap="0px">
|
||||
<TruncatedText
|
||||
fontSize={24}
|
||||
fontWeight={500}
|
||||
color={showAcceptChanges && trade.tradeType === TradeType.EXACT_OUTPUT ? theme.accentAction : ''}
|
||||
data-testid="input-amount"
|
||||
>
|
||||
{trade.inputAmount.toSignificant(6)}
|
||||
</TruncatedText>
|
||||
</RowFixed>
|
||||
<RowFixed gap="0px">
|
||||
<CurrencyLogo currency={trade.inputAmount.currency} size="20px" style={{ marginRight: '12px' }} />
|
||||
<Text fontSize={20} fontWeight={500} data-testid="input-symbol">
|
||||
{trade.inputAmount.currency.symbol}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<FiatValue fiatValue={fiatValueInput} />
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</LightCard>
|
||||
<ArrowWrapper>
|
||||
<ArrowDown size="16" color={theme.textPrimary} />
|
||||
</ArrowWrapper>
|
||||
<LightCard padding="0.75rem 1rem" style={{ marginBottom: '0.25rem' }}>
|
||||
<AutoColumn gap="sm">
|
||||
<RowBetween align="flex-end">
|
||||
<RowFixed gap="0px">
|
||||
<TruncatedText fontSize={24} fontWeight={500} data-testid="output-amount">
|
||||
{trade.outputAmount.toSignificant(6)}
|
||||
</TruncatedText>
|
||||
</RowFixed>
|
||||
<RowFixed gap="0px">
|
||||
<CurrencyLogo currency={trade.outputAmount.currency} size="20px" style={{ marginRight: '12px' }} />
|
||||
<Text fontSize={20} fontWeight={500} data-testid="output-symbol">
|
||||
{trade.outputAmount.currency.symbol}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<ThemedText.DeprecatedBody fontSize={14} color={theme.textTertiary}>
|
||||
<FiatValue
|
||||
fiatValue={fiatValueOutput}
|
||||
priceImpact={computeFiatValuePriceImpact(fiatValueInput.data, fiatValueOutput.data)}
|
||||
/>
|
||||
</ThemedText.DeprecatedBody>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</LightCard>
|
||||
<RowBetween style={{ marginTop: '0.25rem', padding: '0 1rem' }}>
|
||||
<TradePrice price={trade.executionPrice} />
|
||||
</RowBetween>
|
||||
<LightCard style={{ padding: '.75rem', marginTop: '0.5rem' }}>
|
||||
<AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} />
|
||||
</LightCard>
|
||||
{showAcceptChanges ? (
|
||||
<SwapShowAcceptChanges justify="flex-start" gap="0px" data-testid="show-accept-changes">
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<AlertTriangle size={20} style={{ marginRight: '8px', minWidth: 24 }} />
|
||||
<ThemedText.DeprecatedMain color={theme.accentAction}>
|
||||
<Trans>Price Updated</Trans>
|
||||
</ThemedText.DeprecatedMain>
|
||||
</RowFixed>
|
||||
<ButtonPrimary
|
||||
style={{ padding: '.5rem', width: 'fit-content', fontSize: '0.825rem', borderRadius: '12px' }}
|
||||
onClick={onAcceptChanges}
|
||||
>
|
||||
<Trans>Accept</Trans>
|
||||
</ButtonPrimary>
|
||||
</RowBetween>
|
||||
</SwapShowAcceptChanges>
|
||||
) : null}
|
||||
|
||||
<AutoColumn justify="flex-start" gap="sm" style={{ padding: '.75rem 1rem' }}>
|
||||
{trade.tradeType === TradeType.EXACT_INPUT ? (
|
||||
<ThemedText.DeprecatedItalic fontWeight={400} textAlign="left" style={{ width: '100%' }}>
|
||||
<Trans>
|
||||
Output is estimated. You will receive at least{' '}
|
||||
<b>
|
||||
{trade.minimumAmountOut(allowedSlippage).toSignificant(6)} {trade.outputAmount.currency.symbol}
|
||||
</b>{' '}
|
||||
or the transaction will revert.
|
||||
</Trans>
|
||||
</ThemedText.DeprecatedItalic>
|
||||
) : (
|
||||
<ThemedText.DeprecatedItalic fontWeight={400} textAlign="left" style={{ width: '100%' }}>
|
||||
<Trans>
|
||||
Input is estimated. You will sell at most{' '}
|
||||
<b>
|
||||
{trade.maximumAmountIn(allowedSlippage).toSignificant(6)} {trade.inputAmount.currency.symbol}
|
||||
</b>{' '}
|
||||
or the transaction will revert.
|
||||
</Trans>
|
||||
</ThemedText.DeprecatedItalic>
|
||||
)}
|
||||
</AutoColumn>
|
||||
{recipient !== null ? (
|
||||
<AutoColumn justify="flex-start" gap="sm" style={{ padding: '12px 0 0 0px' }} data-testid="recipient-info">
|
||||
<ThemedText.DeprecatedMain>
|
||||
<Trans>
|
||||
Output will be sent to{' '}
|
||||
<b title={recipient}>{isAddress(recipient) ? shortenAddress(recipient) : recipient}</b>
|
||||
</Trans>
|
||||
</ThemedText.DeprecatedMain>
|
||||
</AutoColumn>
|
||||
) : null}
|
||||
</AutoColumn>
|
||||
<HeaderContainer gap="sm">
|
||||
<Column gap="lg">
|
||||
<SwapModalHeaderAmount
|
||||
field={Field.INPUT}
|
||||
label={<Trans>You pay</Trans>}
|
||||
amount={trade.inputAmount}
|
||||
usdAmount={fiatValueInput.data}
|
||||
/>
|
||||
<SwapModalHeaderAmount
|
||||
field={Field.OUTPUT}
|
||||
label={<Trans>You receive</Trans>}
|
||||
amount={trade.outputAmount}
|
||||
usdAmount={fiatValueOutput.data}
|
||||
tooltipText={
|
||||
trade.tradeType === TradeType.EXACT_INPUT ? (
|
||||
<ThemedText.Caption>
|
||||
<Trans>
|
||||
Output is estimated. You will receive at least{' '}
|
||||
<b>
|
||||
{trade.minimumAmountOut(allowedSlippage).toSignificant(6)} {trade.outputAmount.currency.symbol}
|
||||
</b>{' '}
|
||||
or the transaction will revert.
|
||||
</Trans>
|
||||
</ThemedText.Caption>
|
||||
) : (
|
||||
<ThemedText.Caption>
|
||||
<Trans>
|
||||
Input is estimated. You will sell at most{' '}
|
||||
<b>
|
||||
{trade.maximumAmountIn(allowedSlippage).toSignificant(6)} {trade.inputAmount.currency.symbol}
|
||||
</b>{' '}
|
||||
or the transaction will revert.
|
||||
</Trans>
|
||||
</ThemedText.Caption>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Column>
|
||||
<Rule />
|
||||
</HeaderContainer>
|
||||
)
|
||||
}
|
||||
|
||||
68
src/components/swap/SwapModalHeaderAmount.tsx
Normal file
68
src/components/swap/SwapModalHeaderAmount.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { formatCurrencyAmount, formatNumber, NumberType } from '@uniswap/conedison/format'
|
||||
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
|
||||
import Column from 'components/Column'
|
||||
import CurrencyLogo from 'components/Logo/CurrencyLogo'
|
||||
import Row from 'components/Row'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { useWindowSize } from 'hooks/useWindowSize'
|
||||
import { PropsWithChildren, ReactNode } from 'react'
|
||||
import { TextProps } from 'rebass'
|
||||
import { Field } from 'state/swap/actions'
|
||||
import styled from 'styled-components/macro'
|
||||
import { BREAKPOINTS, ThemedText } from 'theme'
|
||||
|
||||
const MAX_AMOUNT_STR_LENGTH = 9
|
||||
|
||||
export const Label = styled(ThemedText.BodySmall)<{ cursor?: string }>`
|
||||
cursor: ${({ cursor }) => cursor};
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
margin-right: 8px;
|
||||
`
|
||||
|
||||
const ResponsiveHeadline = ({ children, ...textProps }: PropsWithChildren<TextProps>) => {
|
||||
const { width } = useWindowSize()
|
||||
|
||||
if (width && width < BREAKPOINTS.xs) {
|
||||
return <ThemedText.HeadlineMedium {...textProps}>{children}</ThemedText.HeadlineMedium>
|
||||
}
|
||||
|
||||
return <ThemedText.HeadlineLarge {...textProps}>{children}</ThemedText.HeadlineLarge>
|
||||
}
|
||||
|
||||
interface AmountProps {
|
||||
field: Field
|
||||
tooltipText?: ReactNode
|
||||
label: ReactNode
|
||||
amount: CurrencyAmount<Currency> | undefined
|
||||
usdAmount?: number
|
||||
}
|
||||
|
||||
export function SwapModalHeaderAmount({ tooltipText, label, amount, usdAmount, field }: AmountProps) {
|
||||
let formattedAmount = formatCurrencyAmount(amount, NumberType.TokenTx)
|
||||
if (formattedAmount.length > MAX_AMOUNT_STR_LENGTH) {
|
||||
formattedAmount = formatCurrencyAmount(amount, NumberType.SwapTradeAmount)
|
||||
}
|
||||
|
||||
return (
|
||||
<Row align="center" justify="space-between" gap="md">
|
||||
<Column gap="xs">
|
||||
<ThemedText.BodySecondary>
|
||||
<MouseoverTooltip text={tooltipText} disabled={!tooltipText}>
|
||||
<Label cursor="help">{label}</Label>
|
||||
</MouseoverTooltip>
|
||||
</ThemedText.BodySecondary>
|
||||
<Column gap="xs">
|
||||
<ResponsiveHeadline data-testid={`${field}-amount`}>
|
||||
{formattedAmount} {amount?.currency.symbol}
|
||||
</ResponsiveHeadline>
|
||||
{usdAmount && (
|
||||
<ThemedText.BodySmall color="textTertiary">
|
||||
{formatNumber(usdAmount, NumberType.FiatTokenQuantity)}
|
||||
</ThemedText.BodySmall>
|
||||
)}
|
||||
</Column>
|
||||
</Column>
|
||||
{amount?.currency && <CurrencyLogo currency={amount.currency} size="36px" />}
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Currency, TradeType } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import Column from 'components/Column'
|
||||
import { LoadingRows } from 'components/Loader/styled'
|
||||
@@ -12,13 +11,7 @@ import getRoutingDiagramEntries from 'utils/getRoutingDiagramEntries'
|
||||
|
||||
import RouterLabel from './RouterLabel'
|
||||
|
||||
export default function SwapRoute({
|
||||
trade,
|
||||
syncing,
|
||||
}: {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType>
|
||||
syncing: boolean
|
||||
}) {
|
||||
export default function SwapRoute({ trade, syncing }: { trade: InterfaceTrade; syncing: boolean }) {
|
||||
const { chainId } = useWeb3React()
|
||||
const autoRouterSupported = useAutoRouterSupported()
|
||||
|
||||
@@ -28,9 +21,9 @@ export default function SwapRoute({
|
||||
// TODO(WEB-3303)
|
||||
// Can `trade.gasUseEstimateUSD` be defined when `chainId` is not in `SUPPORTED_GAS_ESTIMATE_CHAIN_IDS`?
|
||||
trade.gasUseEstimateUSD && chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId)
|
||||
? trade.gasUseEstimateUSD.toFixed(2) === '0.00'
|
||||
? trade.gasUseEstimateUSD === '0.00'
|
||||
? '<$0.01'
|
||||
: '$' + trade.gasUseEstimateUSD.toFixed(2)
|
||||
: '$' + trade.gasUseEstimateUSD
|
||||
: undefined
|
||||
|
||||
return (
|
||||
|
||||
@@ -32,17 +32,17 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
.c8 {
|
||||
width: -webkit-fit-content;
|
||||
width: -moz-fit-content;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
.c6 {
|
||||
color: #7780A0;
|
||||
}
|
||||
|
||||
.c8 {
|
||||
.c7 {
|
||||
color: #0D111C;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
.c5 {
|
||||
display: inline-block;
|
||||
height: inherit;
|
||||
}
|
||||
@@ -82,14 +82,54 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
|
||||
class="c2 c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c2 c3 c5"
|
||||
class="c5"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c6 css-zhpkf8"
|
||||
>
|
||||
Network fee
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c7 css-zhpkf8"
|
||||
>
|
||||
~$1.00
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c2 c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c5"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c6 css-zhpkf8"
|
||||
>
|
||||
Price Impact
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c7 css-zhpkf8"
|
||||
>
|
||||
105566.37%
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c2 c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c2 c3 c8"
|
||||
>
|
||||
<div
|
||||
class="c6"
|
||||
class="c5"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c7 css-zhpkf8"
|
||||
class="c6 css-zhpkf8"
|
||||
>
|
||||
Minimum output
|
||||
</div>
|
||||
@@ -97,7 +137,7 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c8 css-zhpkf8"
|
||||
class="c7 css-zhpkf8"
|
||||
>
|
||||
0.00000000000000098 DEF
|
||||
</div>
|
||||
@@ -106,14 +146,14 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
|
||||
class="c2 c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c2 c3 c5"
|
||||
class="c2 c3 c8"
|
||||
>
|
||||
<div
|
||||
class="c6"
|
||||
class="c5"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c7 css-zhpkf8"
|
||||
class="c6 css-zhpkf8"
|
||||
>
|
||||
Expected output
|
||||
</div>
|
||||
@@ -121,7 +161,7 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c8 css-zhpkf8"
|
||||
class="c7 css-zhpkf8"
|
||||
>
|
||||
0.000000000000001 DEF
|
||||
</div>
|
||||
@@ -133,16 +173,16 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
|
||||
class="c2 c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c7 css-zhpkf8"
|
||||
class="c6 css-zhpkf8"
|
||||
>
|
||||
Order routing
|
||||
</div>
|
||||
<div
|
||||
class="c6"
|
||||
class="c5"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c8 css-zhpkf8"
|
||||
class="c7 css-zhpkf8"
|
||||
>
|
||||
Uniswap API
|
||||
</div>
|
||||
|
||||
@@ -42,11 +42,11 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
|
||||
color: #0D111C;
|
||||
}
|
||||
|
||||
.c15 {
|
||||
.c12 {
|
||||
color: #7780A0;
|
||||
}
|
||||
|
||||
.c13 {
|
||||
.c16 {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: #D2D9EE;
|
||||
@@ -66,7 +66,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.c12 {
|
||||
.c15 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
@@ -89,11 +89,20 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.c14 {
|
||||
.c10 {
|
||||
display: inline-block;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.c11 {
|
||||
margin-right: 4px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.c11 > * {
|
||||
stroke: #98A1C0;
|
||||
}
|
||||
|
||||
.c8 {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
@@ -135,7 +144,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.c10 {
|
||||
.c13 {
|
||||
-webkit-transform: none;
|
||||
-ms-transform: none;
|
||||
transform: none;
|
||||
@@ -144,7 +153,7 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
|
||||
transition: transform 0.1s linear;
|
||||
}
|
||||
|
||||
.c11 {
|
||||
.c14 {
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
@@ -184,8 +193,32 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
|
||||
<div
|
||||
class="c2 c3 c6"
|
||||
>
|
||||
<svg
|
||||
<div
|
||||
class="c10"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div
|
||||
class="c2 c3 c6"
|
||||
>
|
||||
<svg
|
||||
class="c11"
|
||||
>
|
||||
gas-icon.svg
|
||||
</svg>
|
||||
<div
|
||||
class="c12 css-zhpkf8"
|
||||
>
|
||||
$1.00
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
class="c13"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="#98A1C0"
|
||||
@@ -207,15 +240,55 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c11"
|
||||
class="c14"
|
||||
data-testid="advanced-swap-details"
|
||||
>
|
||||
<div
|
||||
class="c12"
|
||||
class="c15"
|
||||
>
|
||||
<div
|
||||
class="c13"
|
||||
class="c16"
|
||||
/>
|
||||
<div
|
||||
class="c2 c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c10"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c12 css-zhpkf8"
|
||||
>
|
||||
Network fee
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c9 css-zhpkf8"
|
||||
>
|
||||
~$1.00
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c2 c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c10"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c12 css-zhpkf8"
|
||||
>
|
||||
Price Impact
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c9 css-zhpkf8"
|
||||
>
|
||||
105566.37%
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c2 c3 c4"
|
||||
>
|
||||
@@ -223,11 +296,11 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
|
||||
class="c2 c3 c6"
|
||||
>
|
||||
<div
|
||||
class="c14"
|
||||
class="c10"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c15 css-zhpkf8"
|
||||
class="c12 css-zhpkf8"
|
||||
>
|
||||
Minimum output
|
||||
</div>
|
||||
@@ -247,11 +320,11 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
|
||||
class="c2 c3 c6"
|
||||
>
|
||||
<div
|
||||
class="c14"
|
||||
class="c10"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c15 css-zhpkf8"
|
||||
class="c12 css-zhpkf8"
|
||||
>
|
||||
Expected output
|
||||
</div>
|
||||
@@ -265,18 +338,18 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c13"
|
||||
class="c16"
|
||||
/>
|
||||
<div
|
||||
class="c2 c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c15 css-zhpkf8"
|
||||
class="c12 css-zhpkf8"
|
||||
>
|
||||
Order routing
|
||||
</div>
|
||||
<div
|
||||
class="c14"
|
||||
class="c10"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
|
||||
@@ -1,7 +1,172 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SwapModalFooter.tsx renders with a disabled button with no account 1`] = `
|
||||
exports[`SwapModalFooter.tsx matches base snapshot, test trade exact input 1`] = `
|
||||
<DocumentFragment>
|
||||
.c3 {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
width: 100%;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
-webkit-align-items: flex-start;
|
||||
-webkit-box-align: flex-start;
|
||||
-ms-flex-align: flex-start;
|
||||
align-items: flex-start;
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
color: #0D111C;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
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;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
display: inline-block;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
color: #7780A0;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.c8 {
|
||||
cursor: help;
|
||||
color: #7780A0;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
text-align: right;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div
|
||||
class="c2 css-zhpkf8"
|
||||
>
|
||||
<div
|
||||
class="c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c2 c5 css-zhpkf8"
|
||||
>
|
||||
Exchange rate
|
||||
</div>
|
||||
<div
|
||||
class="c2 c6 css-zhpkf8"
|
||||
>
|
||||
1 DEF = 1.00 ABC
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c2 css-zhpkf8"
|
||||
>
|
||||
<div
|
||||
class="c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c2 c8 css-zhpkf8"
|
||||
cursor="help"
|
||||
>
|
||||
Network fee
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c2 c6 css-zhpkf8"
|
||||
>
|
||||
~$1.00
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c2 css-zhpkf8"
|
||||
>
|
||||
<div
|
||||
class="c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c2 c8 css-zhpkf8"
|
||||
cursor="help"
|
||||
>
|
||||
Price impact
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c6 css-zhpkf8"
|
||||
>
|
||||
105566.373%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c2 css-zhpkf8"
|
||||
>
|
||||
<div
|
||||
class="c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c2 c8 css-zhpkf8"
|
||||
cursor="help"
|
||||
>
|
||||
Minimum received
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c2 c6 css-zhpkf8"
|
||||
>
|
||||
0.00000000000000098 DEF
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
.c0 {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
@@ -58,6 +223,10 @@ exports[`SwapModalFooter.tsx renders with a disabled button with no account 1`]
|
||||
margin: !important;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
color: #F5F6FC;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
@@ -146,106 +315,24 @@ exports[`SwapModalFooter.tsx renders with a disabled button with no account 1`]
|
||||
}
|
||||
|
||||
.c6 {
|
||||
background-color: rgba(250,43,57,0.1);
|
||||
border-radius: 1rem;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
font-size: 0.825rem;
|
||||
width: 100%;
|
||||
padding: 3rem 1.25rem 1rem 1rem;
|
||||
margin-top: -2rem;
|
||||
color: #FA2B39;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.c6 p {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
background-color: rgba(250,43,57,0.1);
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
border-radius: 12px;
|
||||
min-width: 48px;
|
||||
height: 48px;
|
||||
height: 56px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0 c1 c2"
|
||||
>
|
||||
<button
|
||||
class="c3 c4 c5"
|
||||
class="c3 c4 c5 c6"
|
||||
data-testid="confirm-swap-button"
|
||||
disabled=""
|
||||
id="confirm-swap-or-send"
|
||||
style="margin: 10px 0px 0px 0px;"
|
||||
>
|
||||
<div
|
||||
class="css-10ob8xa"
|
||||
class="c7 css-iapcxi"
|
||||
>
|
||||
Confirm Swap
|
||||
Swap
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
class="c6"
|
||||
>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="9"
|
||||
y2="13"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="17"
|
||||
y2="17"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<p
|
||||
style="word-break: break-word;"
|
||||
>
|
||||
swap error
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact input 1`] = `
|
||||
exports[`SwapModalHeader.tsx matches base snapshot, test trade exact input 1`] = `
|
||||
<DocumentFragment>
|
||||
.c1 {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
.c3 {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
.c4 {
|
||||
width: 100%;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
@@ -26,99 +19,30 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.c8 {
|
||||
width: 100%;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
gap: 0px;
|
||||
}
|
||||
|
||||
.c16 {
|
||||
width: 100%;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
-webkit-align-items: flex-end;
|
||||
-webkit-box-align: flex-end;
|
||||
-ms-flex-align: flex-end;
|
||||
align-items: flex-end;
|
||||
-webkit-box-pack: start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.c9 {
|
||||
width: -webkit-fit-content;
|
||||
width: -moz-fit-content;
|
||||
width: fit-content;
|
||||
margin: -0px;
|
||||
}
|
||||
|
||||
.c22 {
|
||||
width: -webkit-fit-content;
|
||||
width: -moz-fit-content;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.c18 {
|
||||
color: #0D111C;
|
||||
}
|
||||
|
||||
.c24 {
|
||||
.c6 {
|
||||
color: #7780A0;
|
||||
}
|
||||
|
||||
.c21 {
|
||||
.c8 {
|
||||
color: #0D111C;
|
||||
}
|
||||
|
||||
.c12 {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
border-width: 0;
|
||||
margin: 0;
|
||||
background-color: #D2D9EE;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.c19 {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
border: 1px solid #E8ECFB;
|
||||
background-color: #F5F6FC;
|
||||
}
|
||||
|
||||
.c20 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
@@ -130,63 +54,39 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
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;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
display: grid;
|
||||
grid-auto-rows: auto;
|
||||
grid-row-gap: 4px;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
display: grid;
|
||||
grid-auto-rows: auto;
|
||||
grid-row-gap: 8px;
|
||||
}
|
||||
|
||||
.c25 {
|
||||
display: grid;
|
||||
grid-auto-rows: auto;
|
||||
grid-row-gap: 8px;
|
||||
justify-items: flex-start;
|
||||
}
|
||||
|
||||
.c13 {
|
||||
border-radius: 12px;
|
||||
border-radius: 12px;
|
||||
height: 24px;
|
||||
width: 50%;
|
||||
width: 50%;
|
||||
-webkit-animation: fAQEyV 1.5s infinite;
|
||||
animation: fAQEyV 1.5s infinite;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
background: linear-gradient( to left,#E8ECFB 25%,#fff 50%,#E8ECFB 75% );
|
||||
will-change: background-position;
|
||||
background-size: 400%;
|
||||
}
|
||||
|
||||
.c23 {
|
||||
display: inline-block;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.c14 {
|
||||
border-radius: 4px;
|
||||
width: 4rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.c12 {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
.c11 {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(white 60%,#ffffff00 calc(70% + 1px));
|
||||
box-shadow: 0 0 1px white;
|
||||
}
|
||||
|
||||
.c11 {
|
||||
.c10 {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
@@ -194,342 +94,334 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.c17 {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
.c7 {
|
||||
display: inline-block;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.c9 {
|
||||
cursor: help;
|
||||
color: #7780A0;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.c13 {
|
||||
margin: 16px 2px 24px 2px;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div
|
||||
class="c2"
|
||||
>
|
||||
<div
|
||||
class="c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c5"
|
||||
>
|
||||
<div
|
||||
class="c6 css-1jljtub"
|
||||
>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c8 c9 css-zhpkf8"
|
||||
cursor="help"
|
||||
>
|
||||
You pay
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c5"
|
||||
>
|
||||
<div
|
||||
class="c8 css-xdrz3"
|
||||
data-testid="INPUT-amount"
|
||||
>
|
||||
<0.00001 ABC
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c10"
|
||||
>
|
||||
<img
|
||||
alt="ABC logo"
|
||||
class="c11"
|
||||
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x0000000000000000000000000000000000000001/logo.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c5"
|
||||
>
|
||||
<div
|
||||
class="c6 css-1jljtub"
|
||||
>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c8 c9 css-zhpkf8"
|
||||
cursor="help"
|
||||
>
|
||||
You receive
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c5"
|
||||
>
|
||||
<div
|
||||
class="c8 css-xdrz3"
|
||||
data-testid="OUTPUT-amount"
|
||||
>
|
||||
<0.00001 DEF
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c10"
|
||||
>
|
||||
<img
|
||||
alt="DEF logo"
|
||||
class="c11"
|
||||
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x0000000000000000000000000000000000000002/logo.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c12 c13"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`SwapModalHeader.tsx test trade exact output, no recipient 1`] = `
|
||||
<DocumentFragment>
|
||||
.c3 {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
width: 100%;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
color: #7780A0;
|
||||
}
|
||||
|
||||
.c8 {
|
||||
color: #0D111C;
|
||||
}
|
||||
|
||||
.c12 {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
border-width: 0;
|
||||
margin: 0;
|
||||
background-color: #D2D9EE;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
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;
|
||||
padding: 0;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-gap: 0.25rem;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
text-align: left;
|
||||
-webkit-flex-wrap: wrap;
|
||||
-ms-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
-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;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
display: grid;
|
||||
grid-auto-rows: auto;
|
||||
grid-row-gap: 8px;
|
||||
}
|
||||
|
||||
.c11 {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(white 60%,#ffffff00 calc(70% + 1px));
|
||||
box-shadow: 0 0 1px white;
|
||||
}
|
||||
|
||||
.c10 {
|
||||
text-overflow: ellipsis;
|
||||
max-width: 220px;
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.c15 {
|
||||
padding: 4px;
|
||||
border-radius: 12px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
position: relative;
|
||||
margin-top: -18px;
|
||||
margin-bottom: -18px;
|
||||
left: calc(50% - 16px);
|
||||
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-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
background-color: #FFFFFF;
|
||||
border: 4px solid;
|
||||
border-color: #F5F6FC;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
display: inline-block;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.c9 {
|
||||
cursor: help;
|
||||
color: #7780A0;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.c13 {
|
||||
margin: 16px 2px 24px 2px;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0"
|
||||
style="margin-top: 1rem;"
|
||||
class="c0 c1"
|
||||
>
|
||||
<div
|
||||
class="c1 c2 c3"
|
||||
class="c2"
|
||||
>
|
||||
<div
|
||||
class="c4"
|
||||
class="c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c5 c6 c7"
|
||||
class="c5"
|
||||
>
|
||||
<div
|
||||
class="c5 c8 c9"
|
||||
class="c6 css-1jljtub"
|
||||
>
|
||||
<div
|
||||
class="c10 css-13xjr5l"
|
||||
data-testid="input-amount"
|
||||
>
|
||||
0.000000000000001
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c5 c8 c9"
|
||||
>
|
||||
<div
|
||||
class="c11"
|
||||
style="margin-right: 12px;"
|
||||
>
|
||||
<img
|
||||
alt="ABC logo"
|
||||
class="c12"
|
||||
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x0000000000000000000000000000000000000001/logo.png"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="css-10ob8xa"
|
||||
data-testid="input-symbol"
|
||||
>
|
||||
ABC
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c5 c6 c7"
|
||||
>
|
||||
<div
|
||||
class="css-zhpkf8"
|
||||
>
|
||||
<div
|
||||
class="c13 c14"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c15"
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
height="16"
|
||||
stroke="#0D111C"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="5"
|
||||
y2="19"
|
||||
/>
|
||||
<polyline
|
||||
points="19 12 12 19 5 12"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="c1 c2 c3"
|
||||
style="margin-bottom: 0.25rem;"
|
||||
>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="c5 c16 c7"
|
||||
>
|
||||
<div
|
||||
class="c5 c8 c9"
|
||||
>
|
||||
<div
|
||||
class="c10 css-1kwqs79"
|
||||
data-testid="output-amount"
|
||||
>
|
||||
0.000000000000001
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c5 c8 c9"
|
||||
>
|
||||
<div
|
||||
class="c11"
|
||||
style="margin-right: 12px;"
|
||||
>
|
||||
<img
|
||||
alt="DEF logo"
|
||||
class="c12"
|
||||
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x0000000000000000000000000000000000000002/logo.png"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="css-10ob8xa"
|
||||
data-testid="output-symbol"
|
||||
>
|
||||
DEF
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c5 c6 c7"
|
||||
>
|
||||
<div
|
||||
class="css-zhpkf8"
|
||||
>
|
||||
<div
|
||||
class="css-zhpkf8"
|
||||
>
|
||||
<div
|
||||
class="c13 c14"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c5 c6 c7"
|
||||
style="margin-top: 0.25rem; padding: 0px 1rem;"
|
||||
>
|
||||
<button
|
||||
class="c17"
|
||||
title="1 DEF = 1.00 ABC "
|
||||
>
|
||||
<div
|
||||
class="c18 css-zhpkf8"
|
||||
>
|
||||
1 DEF = 1.00 ABC
|
||||
</div>
|
||||
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="c5 c19 c3"
|
||||
style="padding: .75rem; margin-top: 0.5rem;"
|
||||
>
|
||||
<div
|
||||
class="c20"
|
||||
>
|
||||
<div
|
||||
class="c21"
|
||||
/>
|
||||
<div
|
||||
class="c5 c6 c7"
|
||||
>
|
||||
<div
|
||||
class="c5 c6 c22"
|
||||
>
|
||||
<div
|
||||
class="c23"
|
||||
class="c7"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c24 css-zhpkf8"
|
||||
class="c8 c9 css-zhpkf8"
|
||||
cursor="help"
|
||||
>
|
||||
Minimum output
|
||||
You pay
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c18 css-zhpkf8"
|
||||
class="c5"
|
||||
>
|
||||
0.00000000000000098 DEF
|
||||
<div
|
||||
class="c8 css-xdrz3"
|
||||
data-testid="INPUT-amount"
|
||||
>
|
||||
<0.00001 ABC
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c5 c6 c7"
|
||||
class="c10"
|
||||
>
|
||||
<img
|
||||
alt="ABC logo"
|
||||
class="c11"
|
||||
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x0000000000000000000000000000000000000001/logo.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c5"
|
||||
>
|
||||
<div
|
||||
class="c5 c6 c22"
|
||||
class="c6 css-1jljtub"
|
||||
>
|
||||
<div
|
||||
class="c23"
|
||||
class="c7"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c24 css-zhpkf8"
|
||||
class="c8 c9 css-zhpkf8"
|
||||
cursor="help"
|
||||
>
|
||||
Expected output
|
||||
You receive
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c18 css-zhpkf8"
|
||||
class="c5"
|
||||
>
|
||||
0.000000000000001 DEF
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c21"
|
||||
/>
|
||||
<div
|
||||
class="c5 c6 c7"
|
||||
>
|
||||
<div
|
||||
class="c24 css-zhpkf8"
|
||||
>
|
||||
Order routing
|
||||
</div>
|
||||
<div
|
||||
class="c23"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c18 css-zhpkf8"
|
||||
>
|
||||
Uniswap API
|
||||
</div>
|
||||
<div
|
||||
class="c8 css-xdrz3"
|
||||
data-testid="OUTPUT-amount"
|
||||
>
|
||||
<0.00001 GHI
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c25"
|
||||
style="padding: .75rem 1rem;"
|
||||
>
|
||||
<div
|
||||
class="c24 css-k51stg"
|
||||
style="width: 100%;"
|
||||
>
|
||||
Output is estimated. You will receive at least
|
||||
<b>
|
||||
0.00000000000000098 DEF
|
||||
</b>
|
||||
or the transaction will revert.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c25"
|
||||
data-testid="recipient-info"
|
||||
style="padding: 12px 0px 0px 0px;"
|
||||
>
|
||||
<div
|
||||
class="c24 css-8mokm4"
|
||||
>
|
||||
Output will be sent to
|
||||
<b
|
||||
title="0x0000000000000000000000000000000000000004"
|
||||
<div
|
||||
class="c10"
|
||||
>
|
||||
0x0000...0004
|
||||
</b>
|
||||
<img
|
||||
alt="GHI logo"
|
||||
class="c11"
|
||||
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x0000000000000000000000000000000000000003/logo.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c12 c13"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
@@ -2,7 +2,6 @@ import { SupportedChainId } from 'constants/chains'
|
||||
import { transparentize } from 'polished'
|
||||
import { ReactNode } from 'react'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
import styled, { css } from 'styled-components/macro'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
|
||||
@@ -29,6 +28,7 @@ export const SwapWrapper = styled.main<{ chainId: number | undefined }>`
|
||||
border-radius: 16px;
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
padding: 8px;
|
||||
padding-top: 12px;
|
||||
box-shadow: ${({ chainId }) => !!chainId && chainId === SupportedChainId.BNB && '0px 40px 120px 0px #f0b90b29'};
|
||||
z-index: ${Z_INDEX.default};
|
||||
transition: transform 250ms ease;
|
||||
@@ -63,13 +63,6 @@ export const ArrowWrapper = styled.div<{ clickable: boolean }>`
|
||||
: null}
|
||||
`
|
||||
|
||||
export const TruncatedText = styled(Text)`
|
||||
text-overflow: ellipsis;
|
||||
max-width: 220px;
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
`
|
||||
|
||||
// styles
|
||||
export const Dots = styled.span`
|
||||
&::after {
|
||||
@@ -135,7 +128,7 @@ export function SwapCallbackError({ error }: { error: ReactNode }) {
|
||||
export const SwapShowAcceptChanges = styled(AutoColumn)`
|
||||
background-color: ${({ theme }) => transparentize(0.95, theme.deprecated_primary3)};
|
||||
color: ${({ theme }) => theme.accentAction};
|
||||
padding: 0.5rem;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
margin-top: 8px;
|
||||
`
|
||||
|
||||
@@ -46,16 +46,16 @@ function useTryActivation() {
|
||||
|
||||
onSuccess()
|
||||
} catch (error) {
|
||||
// TODO(WEB-3162): re-add special treatment for already-pending injected errors & move debug to after didUserReject() check
|
||||
console.debug(`Connection failed: ${connection.getName()}`)
|
||||
console.error(error)
|
||||
|
||||
// Gracefully handles errors from the user rejecting a connection attempt
|
||||
if (didUserReject(connection, error)) {
|
||||
setActivationState(IDLE_ACTIVATION_STATE)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(WEB-3162): re-add special treatment for already-pending injected errors & move debug to after didUserReject() check
|
||||
console.debug(`Connection failed: ${connection.getName()}`)
|
||||
console.error(error)
|
||||
|
||||
// Failed Connection events are logged here, while successful ones are logged by Web3Provider
|
||||
sendAnalyticsEvent(InterfaceEventName.WALLET_CONNECT_TXN_COMPLETED, {
|
||||
result: WalletConnectionResult.FAILED,
|
||||
|
||||
@@ -113,3 +113,7 @@ export const L2_CHAIN_IDS = [
|
||||
] as const
|
||||
|
||||
export type SupportedL2ChainId = typeof L2_CHAIN_IDS[number]
|
||||
|
||||
export function isPolygonChain(chainId: number): chainId is SupportedChainId.POLYGON | SupportedChainId.POLYGON_MUMBAI {
|
||||
return chainId === SupportedChainId.POLYGON || chainId === SupportedChainId.POLYGON_MUMBAI
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -6,4 +6,5 @@ export enum FeatureFlag {
|
||||
permit2 = 'permit2',
|
||||
fiatOnRampButtonOnSwap = 'fiat_on_ramp_button_on_swap_page',
|
||||
detailsV2 = 'details_v2',
|
||||
uraEnabled = 'ura_enabled',
|
||||
}
|
||||
|
||||
12
src/featureFlags/flags/unifiedRouter.ts
Normal file
12
src/featureFlags/flags/unifiedRouter.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
|
||||
|
||||
export function useUnifiedRoutingAPIFlag(): BaseVariant {
|
||||
return useBaseFlag(FeatureFlag.uraEnabled)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-unused-modules
|
||||
export function useUnifiedRoutingAPIEnabled(): boolean {
|
||||
return useUnifiedRoutingAPIFlag() === BaseVariant.Enabled
|
||||
}
|
||||
|
||||
export { BaseVariant as UnifiedRouterVariant }
|
||||
@@ -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(
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useMemo } from 'react'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
|
||||
import useGasPrice from './useGasPrice'
|
||||
import useStablecoinPrice, { useStablecoinValue } from './useStablecoinPrice'
|
||||
import useStablecoinPrice, { useStablecoinAmountFromFiatValue, useStablecoinValue } from './useStablecoinPrice'
|
||||
|
||||
const DEFAULT_AUTO_SLIPPAGE = new Percent(1, 1000) // .10%
|
||||
|
||||
@@ -72,15 +72,14 @@ const MAX_AUTO_SLIPPAGE_TOLERANCE = new Percent(5, 100) // 5%
|
||||
/**
|
||||
* Returns slippage tolerance based on values from current trade, gas estimates from api, and active network.
|
||||
*/
|
||||
export default function useAutoSlippageTolerance(
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
|
||||
): Percent {
|
||||
export default function useAutoSlippageTolerance(trade?: InterfaceTrade): Percent {
|
||||
const { chainId } = useWeb3React()
|
||||
const onL2 = chainId && L2_CHAIN_IDS.includes(chainId)
|
||||
const outputDollarValue = useStablecoinValue(trade?.outputAmount)
|
||||
const nativeGasPrice = useGasPrice()
|
||||
|
||||
const gasEstimate = guesstimateGas(trade)
|
||||
const gasEstimateUSD = useStablecoinAmountFromFiatValue(trade?.gasUseEstimateUSD) ?? null
|
||||
const nativeCurrency = useNativeCurrency(chainId)
|
||||
const nativeCurrencyPrice = useStablecoinPrice((trade && nativeCurrency) ?? undefined)
|
||||
|
||||
@@ -100,9 +99,7 @@ export default function useAutoSlippageTolerance(
|
||||
// NOTE - dont use gas estimate for L2s yet - need to verify accuracy
|
||||
// if not, use local heuristic
|
||||
const dollarCostToUse =
|
||||
chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) && trade?.gasUseEstimateUSD
|
||||
? trade.gasUseEstimateUSD
|
||||
: dollarGasCost
|
||||
chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) && gasEstimateUSD ? gasEstimateUSD : dollarGasCost
|
||||
|
||||
if (outputDollarValue && dollarCostToUse) {
|
||||
// optimize for highest possible slippage without getting MEV'd
|
||||
@@ -121,5 +118,15 @@ export default function useAutoSlippageTolerance(
|
||||
}
|
||||
|
||||
return DEFAULT_AUTO_SLIPPAGE
|
||||
}, [trade, onL2, nativeGasPrice, gasEstimate, nativeCurrency, nativeCurrencyPrice, chainId, outputDollarValue])
|
||||
}, [
|
||||
trade,
|
||||
onL2,
|
||||
nativeGasPrice,
|
||||
gasEstimate,
|
||||
nativeCurrency,
|
||||
nativeCurrencyPrice,
|
||||
chainId,
|
||||
gasEstimateUSD,
|
||||
outputDollarValue,
|
||||
])
|
||||
}
|
||||
|
||||
@@ -83,15 +83,6 @@ describe('#useBestV3Trade ExactIn', () => {
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined)
|
||||
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
|
||||
})
|
||||
|
||||
it('does not compute client side v3 trade if routing api is SYNCING', () => {
|
||||
expectRouterMock(TradeState.SYNCING)
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI))
|
||||
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined)
|
||||
expect(result.current).toEqual({ state: TradeState.SYNCING, trade: undefined })
|
||||
})
|
||||
})
|
||||
|
||||
describe('when routing api is in error state', () => {
|
||||
@@ -167,15 +158,6 @@ describe('#useBestV3Trade ExactOut', () => {
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
|
||||
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
|
||||
})
|
||||
|
||||
it('does not compute client side v3 trade if routing api is SYNCING', () => {
|
||||
expectRouterMock(TradeState.SYNCING)
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
|
||||
expect(result.current).toEqual({ state: TradeState.SYNCING, trade: undefined })
|
||||
})
|
||||
})
|
||||
|
||||
describe('when routing api is in error state', () => {
|
||||
|
||||
@@ -23,7 +23,7 @@ export function useBestTrade(
|
||||
otherCurrency?: Currency
|
||||
): {
|
||||
state: TradeState
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
|
||||
trade?: InterfaceTrade
|
||||
} {
|
||||
const { chainId } = useWeb3React()
|
||||
const autoRouterSupported = useAutoRouterSupported()
|
||||
|
||||
@@ -5,7 +5,7 @@ import { SupportedChainId } from 'constants/chains'
|
||||
import JSBI from 'jsbi'
|
||||
import { useSingleContractWithCallData } from 'lib/hooks/multicall'
|
||||
import { useMemo } from 'react'
|
||||
import { InterfaceTrade, TradeState } from 'state/routing/types'
|
||||
import { ClassicTrade, InterfaceTrade, TradeState } from 'state/routing/types'
|
||||
|
||||
import { isCelo } from '../constants/tokens'
|
||||
import { useAllV3Routes } from './useAllV3Routes'
|
||||
@@ -33,7 +33,7 @@ export function useClientSideV3Trade<TTradeType extends TradeType>(
|
||||
tradeType: TTradeType,
|
||||
amountSpecified?: CurrencyAmount<Currency>,
|
||||
otherCurrency?: Currency
|
||||
): { state: TradeState; trade: InterfaceTrade<Currency, Currency, TTradeType> | undefined } {
|
||||
): { state: TradeState; trade: InterfaceTrade | undefined } {
|
||||
const [currencyIn, currencyOut] =
|
||||
tradeType === TradeType.EXACT_INPUT
|
||||
? [amountSpecified?.currency, otherCurrency]
|
||||
@@ -135,7 +135,7 @@ export function useClientSideV3Trade<TTradeType extends TradeType>(
|
||||
|
||||
return {
|
||||
state: TradeState.VALID,
|
||||
trade: new InterfaceTrade({
|
||||
trade: new ClassicTrade({
|
||||
v2Routes: [],
|
||||
v3Routes: [
|
||||
{
|
||||
|
||||
@@ -41,8 +41,8 @@ function useETHValue(currencyAmount?: CurrencyAmount<Currency>): {
|
||||
}
|
||||
}
|
||||
|
||||
if (!trade || !currencyAmount?.currency || !isGqlSupportedChain(chainId)) {
|
||||
return { data: undefined, isLoading: state === TradeState.LOADING || state === TradeState.SYNCING }
|
||||
if (!trade || state === TradeState.LOADING || !currencyAmount?.currency || !isGqlSupportedChain(chainId)) {
|
||||
return { data: undefined, isLoading: state === TradeState.LOADING }
|
||||
}
|
||||
|
||||
const { numerator, denominator } = trade.routes[0].midPrice
|
||||
|
||||
@@ -3,8 +3,10 @@ import { BigintIsh, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { AlphaRouter, AlphaRouterConfig, ChainId } from '@uniswap/smart-order-router'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { nativeOnChain } from 'constants/tokens'
|
||||
import JSBI from 'jsbi'
|
||||
import { GetQuoteResult } from 'state/routing/types'
|
||||
import { GetQuoteArgs } from 'state/routing/slice'
|
||||
import { QuoteResult, QuoteState, SwapRouterNativeAssets } from 'state/routing/types'
|
||||
import { transformSwapRouteToGetQuoteResult } from 'utils/transformSwapRouteToGetQuoteResult'
|
||||
|
||||
export function toSupportedChainId(chainId: ChainId): SupportedChainId | undefined {
|
||||
@@ -19,50 +21,41 @@ export function isSupportedChainId(chainId: ChainId | undefined): boolean {
|
||||
|
||||
async function getQuote(
|
||||
{
|
||||
type,
|
||||
tradeType,
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
amount: amountRaw,
|
||||
}: {
|
||||
type: 'exactIn' | 'exactOut'
|
||||
tradeType: TradeType
|
||||
tokenIn: { address: string; chainId: number; decimals: number; symbol?: string }
|
||||
tokenOut: { address: string; chainId: number; decimals: number; symbol?: string }
|
||||
amount: BigintIsh
|
||||
},
|
||||
router: AlphaRouter,
|
||||
config: Partial<AlphaRouterConfig>
|
||||
): Promise<{ data: GetQuoteResult; error?: unknown }> {
|
||||
const currencyIn = new Token(tokenIn.chainId, tokenIn.address, tokenIn.decimals, tokenIn.symbol)
|
||||
const currencyOut = new Token(tokenOut.chainId, tokenOut.address, tokenOut.decimals, tokenOut.symbol)
|
||||
routerConfig: Partial<AlphaRouterConfig>
|
||||
): Promise<QuoteResult> {
|
||||
const tokenInIsNative = Object.values(SwapRouterNativeAssets).includes(tokenIn.address as SwapRouterNativeAssets)
|
||||
const tokenOutIsNative = Object.values(SwapRouterNativeAssets).includes(tokenOut.address as SwapRouterNativeAssets)
|
||||
|
||||
const currencyIn = tokenInIsNative
|
||||
? nativeOnChain(tokenIn.chainId)
|
||||
: new Token(tokenIn.chainId, tokenIn.address, tokenIn.decimals, tokenIn.symbol)
|
||||
const currencyOut = tokenOutIsNative
|
||||
? nativeOnChain(tokenOut.chainId)
|
||||
: new Token(tokenOut.chainId, tokenOut.address, tokenOut.decimals, tokenOut.symbol)
|
||||
|
||||
const baseCurrency = tradeType === TradeType.EXACT_INPUT ? currencyIn : currencyOut
|
||||
const quoteCurrency = tradeType === TradeType.EXACT_INPUT ? currencyOut : currencyIn
|
||||
|
||||
const baseCurrency = type === 'exactIn' ? currencyIn : currencyOut
|
||||
const quoteCurrency = type === 'exactIn' ? currencyOut : currencyIn
|
||||
const amount = CurrencyAmount.fromRawAmount(baseCurrency, JSBI.BigInt(amountRaw))
|
||||
// TODO (WEB-2055): explore initializing client side routing on first load (when amountRaw is null) if there are enough users using client-side router preference.
|
||||
const swapRoute = await router.route(amount, quoteCurrency, tradeType, /*swapConfig=*/ undefined, routerConfig)
|
||||
|
||||
const swapRoute = await router.route(
|
||||
amount,
|
||||
quoteCurrency,
|
||||
type === 'exactIn' ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT,
|
||||
/*swapConfig=*/ undefined,
|
||||
config
|
||||
)
|
||||
if (!swapRoute) {
|
||||
return { state: QuoteState.NOT_FOUND }
|
||||
}
|
||||
|
||||
if (!swapRoute) throw new Error('Failed to generate client side quote')
|
||||
|
||||
return { data: transformSwapRouteToGetQuoteResult(type, amount, swapRoute) }
|
||||
}
|
||||
|
||||
interface QuoteArguments {
|
||||
tokenInAddress: string
|
||||
tokenInChainId: ChainId
|
||||
tokenInDecimals: number
|
||||
tokenInSymbol?: string
|
||||
tokenOutAddress: string
|
||||
tokenOutChainId: ChainId
|
||||
tokenOutDecimals: number
|
||||
tokenOutSymbol?: string
|
||||
amount: string
|
||||
type: 'exactIn' | 'exactOut'
|
||||
return transformSwapRouteToGetQuoteResult(tradeType, amount, swapRoute)
|
||||
}
|
||||
|
||||
export async function getClientSideQuote(
|
||||
@@ -76,14 +69,14 @@ export async function getClientSideQuote(
|
||||
tokenOutDecimals,
|
||||
tokenOutSymbol,
|
||||
amount,
|
||||
type,
|
||||
}: QuoteArguments,
|
||||
tradeType,
|
||||
}: GetQuoteArgs,
|
||||
router: AlphaRouter,
|
||||
config: Partial<AlphaRouterConfig>
|
||||
) {
|
||||
return getQuote(
|
||||
{
|
||||
type,
|
||||
tradeType,
|
||||
tokenIn: {
|
||||
address: tokenInAddress,
|
||||
chainId: tokenInChainId,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
|
||||
import { useMemo } from 'react'
|
||||
import { INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference } from 'state/routing/slice'
|
||||
import { currencyAddressForSwapQuote } from 'state/routing/utils'
|
||||
|
||||
/**
|
||||
* Returns query arguments for the Routing API query or undefined if the
|
||||
@@ -26,16 +27,16 @@ export function useRoutingAPIArguments({
|
||||
? undefined
|
||||
: {
|
||||
amount: amount.quotient.toString(),
|
||||
tokenInAddress: tokenIn.wrapped.address,
|
||||
tokenInAddress: currencyAddressForSwapQuote(tokenIn),
|
||||
tokenInChainId: tokenIn.wrapped.chainId,
|
||||
tokenInDecimals: tokenIn.wrapped.decimals,
|
||||
tokenInSymbol: tokenIn.wrapped.symbol,
|
||||
tokenOutAddress: tokenOut.wrapped.address,
|
||||
tokenOutAddress: currencyAddressForSwapQuote(tokenOut),
|
||||
tokenOutChainId: tokenOut.wrapped.chainId,
|
||||
tokenOutDecimals: tokenOut.wrapped.decimals,
|
||||
tokenOutSymbol: tokenOut.wrapped.symbol,
|
||||
routerPreference,
|
||||
type: (tradeType === TradeType.EXACT_INPUT ? 'exactIn' : 'exactOut') as 'exactIn' | 'exactOut',
|
||||
tradeType,
|
||||
},
|
||||
[amount, routerPreference, tokenIn, tokenOut, tradeType]
|
||||
)
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
} from 'make-plural/plurals'
|
||||
import { PluralCategory } from 'make-plural/plurals'
|
||||
import { ReactNode, useEffect } from 'react'
|
||||
import { retry } from 'utils/retry'
|
||||
|
||||
type LocalePlural = {
|
||||
[key in SupportedLocale]: (n: number | string, ord?: boolean) => PluralCategory
|
||||
@@ -79,7 +80,7 @@ const plurals: LocalePlural = {
|
||||
export async function dynamicActivate(locale: SupportedLocale) {
|
||||
i18n.loadLocaleData(locale, { plurals: () => plurals[locale] })
|
||||
try {
|
||||
const catalog = await import(`locales/${locale}.js`)
|
||||
const catalog = await retry(() => import(`locales/${locale}.js`))
|
||||
// Bundlers will either export it as default or as a named export named default.
|
||||
i18n.load(locale, catalog.messages || catalog.default.messages)
|
||||
} catch (error: unknown) {
|
||||
|
||||
@@ -39,7 +39,7 @@ export const formatSwapSignedAnalyticsEventProperties = ({
|
||||
fiatValues,
|
||||
txHash,
|
||||
}: {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> | Trade<Currency, Currency, TradeType>
|
||||
trade: InterfaceTrade | Trade<Currency, Currency, TradeType>
|
||||
fiatValues: { amountIn: number | undefined; amountOut: number | undefined }
|
||||
txHash: string
|
||||
}) => ({
|
||||
@@ -61,8 +61,7 @@ export const formatSwapSignedAnalyticsEventProperties = ({
|
||||
|
||||
export const formatSwapQuoteReceivedEventProperties = (
|
||||
trade: Trade<Currency, Currency, TradeType>,
|
||||
gasUseEstimateUSD?: CurrencyAmount<Token>,
|
||||
fetchingSwapQuoteStartTime?: Date
|
||||
gasUseEstimateUSD?: string
|
||||
) => {
|
||||
return {
|
||||
token_in_symbol: trade.inputAmount.currency.symbol,
|
||||
@@ -70,15 +69,12 @@ export const formatSwapQuoteReceivedEventProperties = (
|
||||
token_in_address: getTokenAddress(trade.inputAmount.currency),
|
||||
token_out_address: getTokenAddress(trade.outputAmount.currency),
|
||||
price_impact_basis_points: trade ? formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)) : undefined,
|
||||
estimated_network_fee_usd: gasUseEstimateUSD ? formatToDecimal(gasUseEstimateUSD, 2) : undefined,
|
||||
estimated_network_fee_usd: gasUseEstimateUSD,
|
||||
chain_id:
|
||||
trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId
|
||||
? trade.inputAmount.currency.chainId
|
||||
: 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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user