Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fe2af4574 | ||
|
|
38af86e1bb | ||
|
|
11a8df2a3e | ||
|
|
3726b6bb47 | ||
|
|
bfde34c774 | ||
|
|
bd8113d018 | ||
|
|
14e3ef044e | ||
|
|
4fc4bdcd55 | ||
|
|
3733570a89 | ||
|
|
7a042a5199 | ||
|
|
6d5e17a6e7 | ||
|
|
8301c5892c | ||
|
|
59b757dda0 | ||
|
|
92a6ec67b3 | ||
|
|
1d6a1e90d7 | ||
|
|
01aa3291b3 | ||
|
|
5539ebedf7 | ||
|
|
e6adddbf55 | ||
|
|
0050b1e165 | ||
|
|
5bf33ab004 | ||
|
|
a4cfeecd8c | ||
|
|
76cbfdd0b9 | ||
|
|
0db9e51e41 | ||
|
|
82e7925a17 | ||
|
|
2150347ba2 | ||
|
|
2f80646ddd | ||
|
|
55eea6a724 | ||
|
|
709a70652f | ||
|
|
5a7a041f12 | ||
|
|
b60d98fc17 | ||
|
|
38d9ab67eb | ||
|
|
5e6ef1575b | ||
|
|
4a015e9d0d | ||
|
|
c383a0a0a2 |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -1 +1 @@
|
||||
@uniswap/web-reviewers
|
||||
@uniswap/web-admins
|
||||
|
||||
48
.github/actions/report/action.yml
vendored
Normal file
48
.github/actions/report/action.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Report
|
||||
description: Report test failures via Slack
|
||||
inputs:
|
||||
name:
|
||||
description: The name of the failing test
|
||||
required: true
|
||||
SLACK_WEBHOOK_URL:
|
||||
description: The webhook URL to send the report to
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "${{ inputs.name }} failing on `${{ github.ref_name }}`",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "*${{ inputs.name }} failing on `${{ github.ref_name }}`:* <https://github.com/${{ github.repository}}/actions/runs/${{ github.run_id }}|view failing action>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "_This is blocking pull requests and branch promotions._\n_Please prioritize fixing the build._"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ inputs.SLACK_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
# The !oncall bot requires its own message:
|
||||
- uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "!oncall web"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ inputs.SLACK_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
55
.github/actions/setup/action.yml
vendored
55
.github/actions/setup/action.yml
vendored
@@ -10,11 +10,12 @@ runs:
|
||||
with:
|
||||
node-version: 14
|
||||
registry-url: https://registry.npmjs.org
|
||||
cache: 'yarn'
|
||||
|
||||
# node_modules/.cache is intentionally omitted, as this is used for build tool caches.
|
||||
- uses: actions/cache@v3
|
||||
id: install-cache
|
||||
with:
|
||||
# node_modules/.cache is intentionally omitted, as this is used for build tool caches.
|
||||
path: |
|
||||
node_modules
|
||||
!node_modules/.cache
|
||||
@@ -22,3 +23,55 @@ runs:
|
||||
- if: steps.install-cache.outputs.cache-hit != 'true'
|
||||
run: yarn install --frozen-lockfile --ignore-scripts
|
||||
shell: bash
|
||||
|
||||
# Validators compile quickly, so caching can be omitted.
|
||||
- run: yarn ajv
|
||||
shell: bash
|
||||
|
||||
# Contracts are compiled from source. If source hasn't changed, the contracts do not need to be re-compiled.
|
||||
- uses: actions/cache@v3
|
||||
id: contracts-cache
|
||||
with:
|
||||
path: |
|
||||
src/abis/types
|
||||
src/types/v3
|
||||
key: ${{ runner.os }}-contracts-${{ hashFiles('src/abis/**/*.json', 'node_modules/@uniswap/**/artifacts/contracts/**/*.json') }}
|
||||
- if: steps.contracts-cache.outputs.cache-hit != 'true'
|
||||
run: yarn contracts
|
||||
shell: bash
|
||||
|
||||
# GraphQL is generated from schema. The schema is always fetched, but if unchanged, graphql does not need to be re-generated.
|
||||
- run: yarn graphql:fetch
|
||||
shell: bash
|
||||
- uses: actions/cache@v3
|
||||
id: graphql-cache
|
||||
with:
|
||||
path: src/graphql/**/__generated__
|
||||
key: ${{ runner.os }}-graphql-${{ hashFiles('src/graphql/**/schema.graphql') }}
|
||||
- if: steps.graphql-cache.outputs.cache-hit != 'true'
|
||||
run: yarn graphql:generate
|
||||
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
|
||||
|
||||
# Translations are compiled from messages. If messages haven't changed, the translations do not need to be re-compiled.
|
||||
- uses: actions/cache@v3
|
||||
id: i18n-compile-cache
|
||||
with:
|
||||
path: src/locales/*.js
|
||||
key: ${{ runner.os }}-i18n-compile-${{ hashFiles('src/locales/*.po') }}
|
||||
- if: steps.i18n-compile-cache.outputs.cache-hit !='true'
|
||||
run: yarn i18n:compile
|
||||
shell: bash
|
||||
|
||||
1
.github/dependabot.yml
vendored
1
.github/dependabot.yml
vendored
@@ -8,6 +8,5 @@ updates:
|
||||
allow:
|
||||
- dependency-name: '@uniswap/default-token-list'
|
||||
- dependency-name: '@uniswap/token-lists'
|
||||
- dependency-name: '@uniswap/widgets'
|
||||
reviewers:
|
||||
- 'Uniswap/dependabot-reviewers'
|
||||
|
||||
32
.github/workflows/1-main-to-staging.yml
vendored
Normal file
32
.github/workflows/1-main-to-staging.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: 1 | Push main -> staging
|
||||
|
||||
# This CI job is responsible for pushing the current contents of the `main` branch to the
|
||||
# `releases/staging` branch, which will in turn kick off a deploy to the staging environment.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
# https://stackoverflow.com/questions/57921401/push-to-origin-from-github-action
|
||||
jobs:
|
||||
push-staging:
|
||||
name: 'Push to staging branch'
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: push/staging
|
||||
steps:
|
||||
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_SERVICE_ACCESS_TOKEN }}
|
||||
ref: main
|
||||
- name: Git config
|
||||
run: |
|
||||
git config user.name "UL Service Account"
|
||||
git config user.email "hello-happy-puppy@users.noreply.github.com"
|
||||
- name: Add CODEOWNERS file
|
||||
run: |
|
||||
echo "@uniswap/web-admins" > CODEOWNERS
|
||||
git add CODEOWNERS
|
||||
git commit -m "ci: add global CODEOWNERS"
|
||||
- name: Git push
|
||||
run: |
|
||||
git push origin main:releases/staging --force
|
||||
41
.github/workflows/2-deploy-to-staging.yml
vendored
Normal file
41
.github/workflows/2-deploy-to-staging.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: 2 | Deploy staging
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'releases/staging'
|
||||
|
||||
jobs:
|
||||
deploy-to-staging:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: deploy/staging
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- run: yarn prepare
|
||||
- run: yarn build
|
||||
env:
|
||||
REACT_APP_STAGING: 1
|
||||
- name: Setup node@16 (required by Cloudflare Pages)
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Update Cloudflare Pages deployment
|
||||
uses: cloudflare/pages-action@364c7ca09a4b57837c5967871d64a2c31adb8c0d
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
projectName: interface-staging
|
||||
directory: build
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Upload source maps to Sentry
|
||||
uses: getsentry/action-release@bd5f874fcda966ba48139b0140fb3ec0cb3aabdd
|
||||
continue-on-error: true
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
with:
|
||||
environment: staging
|
||||
sourcemaps: './build/static/js'
|
||||
url_prefix: '~/static/js'
|
||||
27
.github/workflows/3-staging-to-prod.yml
vendored
Normal file
27
.github/workflows/3-staging-to-prod.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: 3 | Push staging -> prod
|
||||
|
||||
# This CI job is responsible for force pushing the content of releases/staging to releases/prod. It
|
||||
# is restricted to web-reviewers through virtue of the GitHub environment protection rules for the
|
||||
# prod environment.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
push-prod:
|
||||
name: 'Push to prod branch'
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: push/prod
|
||||
steps:
|
||||
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_SERVICE_ACCESS_TOKEN }}
|
||||
ref: releases/staging
|
||||
- name: Git config
|
||||
run: |
|
||||
git config user.name "UL Service Account"
|
||||
git config user.email "hello-happy-puppy@users.noreply.github.com"
|
||||
- name: Git push
|
||||
run: |
|
||||
git push origin releases/staging:releases/prod --force
|
||||
91
.github/workflows/4-deploy-to-prod.yml
vendored
Normal file
91
.github/workflows/4-deploy-to-prod.yml
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
name: 4 | Deploy prod
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'releases/prod'
|
||||
|
||||
jobs:
|
||||
deploy-to-prod:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: deploy/prod
|
||||
steps:
|
||||
- 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
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
release_branches: releases/prod
|
||||
default_bump: patch
|
||||
|
||||
- name: Pin to IPFS
|
||||
id: pinata
|
||||
uses: anantaramdas/ipfs-pinata-deploy-action@39bbda1ce1fe24c69c6f57861b8038278d53688d
|
||||
with:
|
||||
pin-name: Uniswap ${{ steps.github-tag-action.outputs.new_tag }}
|
||||
path: './build'
|
||||
pinata-api-key: ${{ secrets.PINATA_API_KEY }}
|
||||
pinata-secret-api-key: ${{ secrets.PINATA_API_SECRET_KEY }}
|
||||
|
||||
- name: Convert CIDv0 to CIDv1
|
||||
id: convert-cidv0
|
||||
uses: uniswap/convert-cidv0-cidv1@v1.0.0
|
||||
with:
|
||||
cidv0: ${{ steps.pinata.outputs.hash }}
|
||||
|
||||
- name: Release
|
||||
uses: actions/create-release@v1.1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ steps.github-tag-action.outputs.new_tag }}
|
||||
release_name: Release ${{ steps.github-tag-action.outputs.new_tag }}
|
||||
body: |
|
||||
IPFS hash of the deployment:
|
||||
- CIDv0: `${{ steps.pinata.outputs.hash }}`
|
||||
- CIDv1: `${{ steps.convert-cidv0.outputs.cidv1 }}`
|
||||
|
||||
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
|
||||
|
||||
You can also access the Uniswap Interface from an IPFS gateway.
|
||||
**BEWARE**: The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to remember your settings, such as which tokens you have imported.
|
||||
**You should always use an IPFS gateway that enforces origin separation**, or our hosted deployment of the latest release at [app.uniswap.org](https://app.uniswap.org).
|
||||
Your Uniswap settings are never remembered across different URLs.
|
||||
|
||||
IPFS gateways:
|
||||
- https://${{ steps.convert-cidv0.outputs.cidv1 }}.ipfs.dweb.link/
|
||||
- https://${{ steps.convert-cidv0.outputs.cidv1 }}.ipfs.cf-ipfs.com/
|
||||
- [ipfs://${{ steps.pinata.outputs.hash }}/](ipfs://${{ steps.pinata.outputs.hash }}/)
|
||||
|
||||
${{ steps.github-tag-action.outputs.changelog }}
|
||||
|
||||
- name: Setup node@16 (required by Cloudflare Pages)
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Update Cloudflare Pages deployment
|
||||
uses: cloudflare/pages-action@364c7ca09a4b57837c5967871d64a2c31adb8c0d
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
projectName: ${{ secrets.CLOUDFLARE_PROJECT_NAME }}
|
||||
directory: build
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: main
|
||||
|
||||
- name: Upload source maps to Sentry
|
||||
uses: getsentry/action-release@4744f6a65149f441c5f396d5b0877307c0db52c7
|
||||
continue-on-error: true
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
with:
|
||||
environment: production
|
||||
sourcemaps: './build/static/js'
|
||||
url_prefix: '~/static/js'
|
||||
1
.github/workflows/release.yaml
vendored
1
.github/workflows/release.yaml
vendored
@@ -47,7 +47,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- run: yarn prepare
|
||||
- run: yarn build
|
||||
|
||||
- name: Pin to IPFS
|
||||
|
||||
32
.github/workflows/test.yml
vendored
32
.github/workflows/test.yml
vendored
@@ -2,6 +2,7 @@ name: Test
|
||||
|
||||
# Many build steps have their own caches, so each job has its own cache to improve subsequent build times.
|
||||
# Build tools are configured to cache cache to node_modules/.cache, so this is cached independently of node_modules.
|
||||
# Caches are saved every run (by keying on github.run_id), and the most recent available cache is loaded.
|
||||
# See https://jongleberry.medium.com/speed-up-your-ci-and-dx-with-node-modules-cache-ac8df82b7bb0.
|
||||
|
||||
on:
|
||||
@@ -25,6 +26,11 @@ jobs:
|
||||
key: ${{ runner.os }}-eslint-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-eslint-${{ hashFiles('**/yarn.lock') }}-
|
||||
- run: yarn lint
|
||||
- if: failure() && github.ref_name == 'main'
|
||||
uses: ./.github/actions/report
|
||||
with:
|
||||
name: Lint
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -37,8 +43,12 @@ jobs:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-tsc-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-tsc-${{ hashFiles('**/yarn.lock') }}-
|
||||
- run: yarn prepare
|
||||
- run: yarn typecheck
|
||||
- if: failure() && github.ref_name == 'main'
|
||||
uses: ./.github/actions/report
|
||||
with:
|
||||
name: Typecheck
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
|
||||
|
||||
deps-tests:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -46,6 +56,11 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- run: yarn yarn-deduplicate --strategy=highest --list --fail
|
||||
- if: failure() && github.ref_name == 'main'
|
||||
uses: ./.github/actions/report
|
||||
with:
|
||||
name: Dependency checks
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
|
||||
|
||||
unit-tests:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -58,7 +73,6 @@ jobs:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-jest-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-jest-${{ hashFiles('**/yarn.lock') }}-
|
||||
- run: yarn prepare
|
||||
- run: yarn test --coverage --maxWorkers=100%
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
@@ -66,6 +80,11 @@ jobs:
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
flags: unit-tests
|
||||
- if: failure() && github.ref_name == 'main'
|
||||
uses: ./.github/actions/report
|
||||
with:
|
||||
name: Unit tests
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
|
||||
|
||||
build-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -78,7 +97,6 @@ jobs:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-build-e2e-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-build-e2e-${{ hashFiles('**/yarn.lock') }}-
|
||||
- run: yarn prepare
|
||||
- run: yarn build:e2e
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
@@ -140,9 +158,15 @@ jobs:
|
||||
|
||||
# Included as a single job to check for cypress-test-matrix success, as a matrix cannot be checked.
|
||||
cypress-tests:
|
||||
if: ${{ always() }}
|
||||
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 }}
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -8,8 +8,10 @@
|
||||
/src/locales/**/en-US.po
|
||||
/src/locales/**/pseudo.po
|
||||
|
||||
# generated graphql types
|
||||
/src/graphql/**/__generated__
|
||||
# generated files
|
||||
/src/**/__generated__
|
||||
|
||||
# schema
|
||||
schema.graphql
|
||||
|
||||
# dependencies
|
||||
|
||||
1
CODEOWNERS
Normal file
1
CODEOWNERS
Normal file
@@ -0,0 +1 @@
|
||||
@uniswap/web-admins
|
||||
133
cypress/e2e/mini-portfolio/activity-history.test.ts
Normal file
133
cypress/e2e/mini-portfolio/activity-history.test.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
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 })
|
||||
.then((hardhat) => hardhat.wallet.getTransactionCount())
|
||||
.then((currentNonce) => {
|
||||
const nextNonce = currentNonce + 1
|
||||
// Mock graphql response to include a specific nonce.
|
||||
cy.intercept(
|
||||
{
|
||||
method: 'POST',
|
||||
url: 'https://beta.api.uniswap.org/v1/graphql',
|
||||
},
|
||||
{
|
||||
body: {
|
||||
data: {
|
||||
portfolios: [
|
||||
{
|
||||
id: 'UG9ydGZvbGlvOjB4NUNlYUI3NGU0NDZkQmQzYkY2OUUyNzcyMDBGMTI5ZDJiQzdBMzdhMQ==',
|
||||
assetActivities: [
|
||||
{
|
||||
id: 'QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnME5tUm1PVGs0T0RrNVl6UmtNR1kzWTJNNE9HRTVNVFEzTURBME9EWmtOVGhrTURnNFpqbG1NelkxTnpRM1l6WXdZek15WVRFNE4yWXlaRFEwWVdVNFh6QjRZV1EyWXpCa05XTmlOVEZsWWpjMU5qUTFaRGszT1RneE4yRTJZVEkxTmpreU1UbG1ZbVE1Wmw4d2VEQXpOR0UwTURjMk5EUTROV1kzWlRBNFkyRXhOak0yTm1VMU1ETTBPVEZoTm1GbU56ZzFNR1E9',
|
||||
timestamp: 1681150079,
|
||||
type: 'UNKNOWN',
|
||||
chain: 'ETHEREUM',
|
||||
transaction: {
|
||||
id: 'VHJhbnNhY3Rpb246MHg0NmRmOTk4ODk5YzRkMGY3Y2M4OGE5MTQ3MDA0ODZkNThkMDg4ZjlmMzY1NzQ3YzYwYzMyYTE4N2YyZDQ0YWU4XzB4YWQ2YzBkNWNiNTFlYjc1NjQ1ZDk3OTgxN2E2YTI1NjkyMTlmYmQ5Zl8weDAzNGE0MDc2NDQ4NWY3ZTA4Y2ExNjM2NmU1MDM0OTFhNmFmNzg1MGQ=',
|
||||
blockNumber: 17019453,
|
||||
hash: '0x46df998899c4d0f7cc88a914700486d58d088f9f365747c60c32a187f2d44ae8',
|
||||
status: 'CONFIRMED',
|
||||
to: '0x034a40764485f7e08ca16366e503491a6af7850d',
|
||||
from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
|
||||
nonce: currentNonce,
|
||||
__typename: 'Transaction',
|
||||
},
|
||||
assetChanges: [],
|
||||
__typename: 'AssetActivity',
|
||||
},
|
||||
{
|
||||
id: 'QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhneE16UXpaR1ppTlROaE9XRmpNR00yWW1aaVpqUTNNRFEyWWpObFkyRXhORGN3TUdZd00yWXhOMkV3WWpnM1pqWXpPRFpsWVRnNU16QTRNVFZtWmpoaFh6QjRZMkUzTXpOalkySm1OelZoTXpnME1ERXhPR1ZpT1RjNU9EVTJOemRpTkdRMk56TTBZemMwWmw4d2VERmlOVEUxTkdGaE5HSTRaakF5TjJJNVptUXhPVE0wTVRFek1tWmpPV1JoWlRFd1pqY3pOVGs9',
|
||||
timestamp: 1681149995,
|
||||
type: 'SEND',
|
||||
chain: 'ETHEREUM',
|
||||
transaction: {
|
||||
id: 'VHJhbnNhY3Rpb246MHgxMzQzZGZiNTNhOWFjMGM2YmZiZjQ3MDQ2YjNlY2ExNDcwMGYwM2YxN2EwYjg3ZjYzODZlYTg5MzA4MTVmZjhhXzB4Y2E3MzNjY2JmNzVhMzg0MDExOGViOTc5ODU2NzdiNGQ2NzM0Yzc0Zl8weDFiNTE1NGFhNGI4ZjAyN2I5ZmQxOTM0MTEzMmZjOWRhZTEwZjczNTk=',
|
||||
blockNumber: 17019446,
|
||||
hash: '0x1343dfb53a9ac0c6bfbf47046b3eca14700f03f17a0b87f6386ea8930815ff8a',
|
||||
status: 'CONFIRMED',
|
||||
to: '0x1b5154aa4b8f027b9fd19341132fc9dae10f7359',
|
||||
from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
|
||||
nonce: nextNonce,
|
||||
__typename: 'Transaction',
|
||||
},
|
||||
assetChanges: [
|
||||
{
|
||||
__typename: 'TokenTransfer',
|
||||
id: 'VG9rZW5UcmFuc2ZlcjoweDVjZWFiNzRlNDQ2ZGJkM2JmNjllMjc3MjAwZjEyOWQyYmM3YTM3YTFfMHhiMWRjNDlmMDY1N2FkNTA1YjUzNzUyN2RkOWE1MDk0YTM2NTkzMWMxXzB4MTM0M2RmYjUzYTlhYzBjNmJmYmY0NzA0NmIzZWNhMTQ3MDBmMDNmMTdhMGI4N2Y2Mzg2ZWE4OTMwODE1ZmY4YQ==',
|
||||
asset: {
|
||||
id: 'VG9rZW46RVRIRVJFVU1fMHgxY2MyYjA3MGNhZjAxNmE3ZGRjMzA0N2Y5MzI3MmU4Yzc3YzlkZGU5',
|
||||
name: 'USD Coin (USDC)',
|
||||
symbol: 'USDC',
|
||||
address: '0x1cc2b070caf016a7ddc3047f93272e8c77c9dde9',
|
||||
decimals: 6,
|
||||
chain: 'ETHEREUM',
|
||||
standard: null,
|
||||
project: {
|
||||
id: 'VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4MWNjMmIwNzBjYWYwMTZhN2RkYzMwNDdmOTMyNzJlOGM3N2M5ZGRlOQ==',
|
||||
isSpam: true,
|
||||
logo: null,
|
||||
__typename: 'TokenProject',
|
||||
},
|
||||
__typename: 'Token',
|
||||
},
|
||||
tokenStandard: 'ERC20',
|
||||
quantity: '18011.212084',
|
||||
sender: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
|
||||
recipient: '0xb1dc49f0657ad505b537527dd9a5094a365931c1',
|
||||
direction: 'OUT',
|
||||
transactedValue: null,
|
||||
},
|
||||
],
|
||||
__typename: 'AssetActivity',
|
||||
},
|
||||
],
|
||||
__typename: 'Portfolio',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
).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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -2,62 +2,22 @@ import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { parseEther } from '@ethersproject/units'
|
||||
import { SupportedChainId, WETH9 } from '@uniswap/sdk-core'
|
||||
|
||||
import { UNI as UNI_MAINNET, USDC_MAINNET } from '../../src/constants/tokens'
|
||||
import { FeatureFlag } from '../../src/featureFlags/flags/featureFlags'
|
||||
import { WETH_GOERLI } from '../fixtures/constants'
|
||||
import { UNI, USDC_MAINNET } from '../../src/constants/tokens'
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
const UNI_MAINNET = UNI[SupportedChainId.MAINNET]
|
||||
|
||||
describe('Swap', () => {
|
||||
const verifyAmount = (field: 'input' | 'output', amountText: string | null) => {
|
||||
if (amountText === null) {
|
||||
cy.get(`#swap-currency-${field} .token-amount-input`).should('not.have.value')
|
||||
} else {
|
||||
cy.get(`#swap-currency-${field} .token-amount-input`).should('have.value', amountText)
|
||||
}
|
||||
}
|
||||
|
||||
const verifyToken = (field: 'input' | 'output', tokenSymbol: string | null) => {
|
||||
if (tokenSymbol === null) {
|
||||
cy.get(`#swap-currency-${field} .token-symbol-container`).should('contain.text', 'Select token')
|
||||
} else {
|
||||
cy.get(`#swap-currency-${field} .token-symbol-container`).should('contain.text', tokenSymbol)
|
||||
}
|
||||
}
|
||||
|
||||
const selectToken = (tokenSymbol: string, field: 'input' | 'output') => {
|
||||
// open token selector...
|
||||
cy.get(`#swap-currency-${field} .open-currency-select-button`).click()
|
||||
// select token...
|
||||
cy.contains(tokenSymbol).click()
|
||||
|
||||
cy.get('body')
|
||||
.then(($body) => {
|
||||
if ($body.find(getTestSelector('TokenSafetyWrapper')).length) {
|
||||
return 'I understand'
|
||||
}
|
||||
|
||||
return 'no-op' // Don't click on anything, a no-op
|
||||
})
|
||||
.then((content) => {
|
||||
if (content !== 'no-op') {
|
||||
cy.contains(content).click()
|
||||
}
|
||||
})
|
||||
|
||||
// token selector should close...
|
||||
cy.contains('Search name or paste address').should('not.exist')
|
||||
}
|
||||
|
||||
describe('Swap on main page', () => {
|
||||
before(() => {
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
})
|
||||
|
||||
it('starts with ETH selected by default', () => {
|
||||
verifyAmount('input', '')
|
||||
verifyToken('input', 'ETH')
|
||||
verifyAmount('output', null)
|
||||
verifyToken('output', null)
|
||||
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', () => {
|
||||
@@ -97,6 +57,7 @@ describe('Swap', () => {
|
||||
|
||||
// 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')
|
||||
@@ -133,39 +94,24 @@ describe('Swap', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should have the correct default input from URL params ', () => {
|
||||
cy.visit(`/swap?inputCurrency=${WETH_GOERLI}`)
|
||||
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')
|
||||
|
||||
verifyToken('input', 'WETH')
|
||||
verifyToken('output', null)
|
||||
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')
|
||||
|
||||
selectToken('Ether', 'output')
|
||||
cy.get(getTestSelector('swap-currency-button')).first().click()
|
||||
|
||||
verifyToken('input', 'ETH')
|
||||
verifyToken('output', 'WETH')
|
||||
})
|
||||
|
||||
it('should have the correct default output from URL params ', () => {
|
||||
cy.visit(`/swap?outputCurrency=${WETH_GOERLI}`)
|
||||
|
||||
verifyToken('input', null)
|
||||
verifyToken('output', 'WETH')
|
||||
|
||||
cy.get(getTestSelector('swap-currency-button')).first().click()
|
||||
verifyToken('input', 'WETH')
|
||||
verifyToken('output', null)
|
||||
|
||||
selectToken('Ether', 'output')
|
||||
cy.get(getTestSelector('swap-currency-button')).first().click()
|
||||
|
||||
verifyToken('input', 'ETH')
|
||||
verifyToken('output', 'WETH')
|
||||
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')
|
||||
selectToken('WETH', 'output')
|
||||
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')
|
||||
})
|
||||
@@ -174,10 +120,9 @@ describe('Swap', () => {
|
||||
cy.visit('/swap')
|
||||
cy.contains('Settings').should('not.exist')
|
||||
cy.get(getTestSelector('swap-settings-button')).click()
|
||||
cy.contains('Slippage tolerance').should('exist')
|
||||
cy.contains('Max slippage').should('exist')
|
||||
cy.contains('Transaction deadline').should('exist')
|
||||
cy.contains('Auto Router API').should('exist')
|
||||
cy.contains('Expert Mode').should('exist')
|
||||
cy.get(getTestSelector('swap-settings-button')).click()
|
||||
cy.contains('Settings').should('not.exist')
|
||||
})
|
||||
@@ -333,7 +278,7 @@ describe('Swap', () => {
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
.hardhat()
|
||||
.then((hardhat) => {
|
||||
cy.stub(hardhat.wallet, 'sendTransaction').rejects(new Error('user cancelled'))
|
||||
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)
|
||||
@@ -347,83 +292,20 @@ describe('Swap', () => {
|
||||
cy.contains('Transaction rejected').should('not.exist')
|
||||
})
|
||||
})
|
||||
describe('Swap on Token Detail Page', () => {
|
||||
beforeEach(() => {
|
||||
// On mobile widths, we just link back to /swap instead of rendering the swap component.
|
||||
cy.viewport(1200, 800)
|
||||
cy.visit(`/tokens/ethereum/${UNI_MAINNET[1].address}`, {
|
||||
ethereum: 'hardhat',
|
||||
featureFlags: [FeatureFlag.removeWidget],
|
||||
}).then(() => {
|
||||
cy.wait('@eth_blockNumber')
|
||||
cy.scrollTo('top')
|
||||
})
|
||||
})
|
||||
|
||||
it('should have the expected output for a tokens detail page', () => {
|
||||
verifyAmount('input', '')
|
||||
verifyToken('input', null)
|
||||
verifyAmount('output', null)
|
||||
verifyToken('output', 'UNI')
|
||||
})
|
||||
|
||||
it('should automatically navigate to the new TDP', () => {
|
||||
selectToken('WETH', 'output')
|
||||
cy.url().should('include', `${WETH9[1].address}`)
|
||||
cy.url().should('not.include', `${UNI_MAINNET[1].address}`)
|
||||
})
|
||||
|
||||
it('should not share swap state with the main swap page', () => {
|
||||
verifyToken('output', 'UNI')
|
||||
selectToken('WETH', 'input')
|
||||
cy.visit('/swap', { featureFlags: [FeatureFlag.removeWidget] })
|
||||
cy.contains('UNI').should('not.exist')
|
||||
cy.contains('WETH').should('not.exist')
|
||||
})
|
||||
|
||||
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 show a L2 token even if the user is connected to a different network', () => {
|
||||
cy.visit('/tokens', { ethereum: 'hardhat', featureFlags: [FeatureFlag.removeWidget] })
|
||||
cy.get(getTestSelector('tokens-network-filter-selected')).click()
|
||||
cy.get(getTestSelector('tokens-network-filter-option-arbitrum')).click()
|
||||
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Arbitrum')
|
||||
cy.get(getTestSelector('token-table-row-ARB')).click()
|
||||
verifyToken('output', 'ARB')
|
||||
cy.contains('Connect to Arbitrum').should('exist')
|
||||
})
|
||||
})
|
||||
|
||||
it('should render an error for slippage failure', () => {
|
||||
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')
|
||||
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')
|
||||
@@ -431,16 +313,14 @@ describe('Swap', () => {
|
||||
// Open the currency select modal.
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
|
||||
// Wait for the currency list to load
|
||||
cy.contains('1inch').should('exist')
|
||||
|
||||
// Select UNI as output token
|
||||
cy.get(getTestSelector('token-search-input')).clear().type('Uniswap')
|
||||
cy.get(getTestSelector('currency-list-wrapper'))
|
||||
.contains(/^Uniswap$/)
|
||||
.first()
|
||||
.should('exist')
|
||||
.click()
|
||||
// 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
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { SupportedChainId, WETH9 } from '@uniswap/sdk-core'
|
||||
|
||||
import { UNI } from '../../src/constants/tokens'
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
const UNI_MAINNET = UNI[SupportedChainId.MAINNET]
|
||||
|
||||
const UNI_ADDRESS = '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984'
|
||||
|
||||
describe('Token details', () => {
|
||||
@@ -83,4 +88,70 @@ describe('Token details', () => {
|
||||
.should('include.text', 'Warning')
|
||||
.and('include.text', "This token isn't traded on leading U.S. centralized exchanges")
|
||||
})
|
||||
|
||||
describe('swapping', () => {
|
||||
beforeEach(() => {
|
||||
// On mobile widths, we just link back to /swap instead of rendering the swap component.
|
||||
cy.viewport(1200, 800)
|
||||
cy.visit(`/tokens/ethereum/${UNI_MAINNET.address}`, {
|
||||
ethereum: 'hardhat',
|
||||
}).then(() => {
|
||||
cy.wait('@eth_blockNumber')
|
||||
cy.scrollTo('top')
|
||||
})
|
||||
})
|
||||
|
||||
it('should have the expected output for a tokens detail page', () => {
|
||||
cy.get(`#swap-currency-input .token-amount-input`).should('have.value', '')
|
||||
cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'Select token')
|
||||
cy.get(`#swap-currency-output .token-amount-input`).should('not.have.value')
|
||||
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'UNI')
|
||||
})
|
||||
|
||||
it('should automatically navigate to the new TDP', () => {
|
||||
cy.get(`#swap-currency-output .open-currency-select-button`).click()
|
||||
cy.contains('WETH').click()
|
||||
cy.url().should('include', `${WETH9[1].address}`)
|
||||
cy.url().should('not.include', `${UNI_MAINNET.address}`)
|
||||
})
|
||||
|
||||
it.only('should not share swap state with the main swap page', () => {
|
||||
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'UNI')
|
||||
cy.get(`#swap-currency-input .open-currency-select-button`).click()
|
||||
cy.contains('WETH').click()
|
||||
cy.visit('/swap')
|
||||
cy.contains('UNI').should('not.exist')
|
||||
cy.contains('WETH').should('not.exist')
|
||||
})
|
||||
|
||||
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 show a L2 token even if the user is connected to a different network', () => {
|
||||
cy.visit('/tokens', { ethereum: 'hardhat' })
|
||||
cy.get(getTestSelector('tokens-network-filter-selected')).click()
|
||||
cy.get(getTestSelector('tokens-network-filter-option-arbitrum')).click()
|
||||
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Arbitrum')
|
||||
cy.get(getTestSelector('token-table-row-ARB')).click()
|
||||
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'ARB')
|
||||
cy.contains('Connect to Arbitrum').should('exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const WETH_GOERLI = '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6'
|
||||
80
cypress/support/commands.ts
Normal file
80
cypress/support/commands.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import 'cypress-hardhat/lib/browser'
|
||||
|
||||
import { Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge'
|
||||
|
||||
import { FeatureFlag } from '../../src/featureFlags/flags/featureFlags'
|
||||
import { UserState } from '../../src/state/user/reducer'
|
||||
import { CONNECTED_WALLET_USER_STATE } from '../utils/user-state'
|
||||
import { injected } from './ethereum'
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface ApplicationWindow {
|
||||
ethereum: Eip1193Bridge
|
||||
}
|
||||
interface VisitOptions {
|
||||
serviceWorker?: true
|
||||
featureFlags?: Array<FeatureFlag>
|
||||
/**
|
||||
* The mock ethereum provider to inject into the page.
|
||||
* @default 'goerli'
|
||||
*/
|
||||
// TODO(INFRA-175): Migrate all usage of 'goerli' to 'hardhat'.
|
||||
ethereum?: 'goerli' | 'hardhat'
|
||||
/**
|
||||
* Initial user state.
|
||||
* @default {@type import('../utils/user-state').CONNECTED_WALLET_USER_STATE}
|
||||
*/
|
||||
userState?: Partial<UserState>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sets up the injected provider to be a mock ethereum provider with the given mnemonic/index
|
||||
// eslint-disable-next-line no-undef
|
||||
Cypress.Commands.overwrite(
|
||||
'visit',
|
||||
(original, url: string | Partial<Cypress.VisitOptions>, options?: Partial<Cypress.VisitOptions>) => {
|
||||
if (typeof url !== 'string') throw new Error('Invalid arguments. The first argument to cy.visit must be the path.')
|
||||
|
||||
// Add a hash in the URL if it is not present (to use hash-based routing correctly with queryParams).
|
||||
let hashUrl = url.startsWith('/') && url.length > 2 && !url.startsWith('/#') ? `/#${url}` : url
|
||||
if (options?.ethereum === 'goerli') hashUrl += `${url.includes('?') ? '&' : '?'}chain=goerli`
|
||||
|
||||
return cy
|
||||
.intercept('/service-worker.js', options?.serviceWorker ? undefined : { statusCode: 404 })
|
||||
.provider()
|
||||
.then((provider) =>
|
||||
original({
|
||||
...options,
|
||||
url: hashUrl,
|
||||
onBeforeLoad(win) {
|
||||
options?.onBeforeLoad?.(win)
|
||||
|
||||
// We want to test from a clean state, so we clear the local storage (which clears redux).
|
||||
win.localStorage.clear()
|
||||
|
||||
// Set initial user state.
|
||||
win.localStorage.setItem(
|
||||
'redux_localstorage_simple_user', // storage key for the user reducer using 'redux-localstorage-simple'
|
||||
JSON.stringify(options?.userState ?? CONNECTED_WALLET_USER_STATE)
|
||||
)
|
||||
|
||||
// Set feature flags, if configured.
|
||||
if (options?.featureFlags) {
|
||||
const featureFlags = options.featureFlags.reduce((flags, flag) => ({ ...flags, [flag]: 'enabled' }), {})
|
||||
win.localStorage.setItem('featureFlags', JSON.stringify(featureFlags))
|
||||
}
|
||||
|
||||
// Inject the mock ethereum provider.
|
||||
if (options?.ethereum === 'hardhat') {
|
||||
win.ethereum = provider
|
||||
} else {
|
||||
win.ethereum = injected
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -6,125 +6,17 @@
|
||||
// ***********************************************************
|
||||
|
||||
import '@cypress/code-coverage/support'
|
||||
import 'cypress-hardhat/lib/browser'
|
||||
import './commands'
|
||||
import './setupTests'
|
||||
|
||||
import { Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge'
|
||||
import TokenListJSON from '@uniswap/default-token-list'
|
||||
import assert from 'assert'
|
||||
|
||||
import { FeatureFlag } from '../../src/featureFlags/flags/featureFlags'
|
||||
import { UserState } from '../../src/state/user/reducer'
|
||||
import { CONNECTED_WALLET_USER_STATE } from '../utils/user-state'
|
||||
import { injected } from './ethereum'
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface ApplicationWindow {
|
||||
ethereum: Eip1193Bridge
|
||||
}
|
||||
interface VisitOptions {
|
||||
serviceWorker?: true
|
||||
featureFlags?: Array<FeatureFlag>
|
||||
/**
|
||||
* The mock ethereum provider to inject into the page.
|
||||
* @default 'goerli'
|
||||
*/
|
||||
// TODO(INFRA-175): Migrate all usage of 'goerli' to 'hardhat'.
|
||||
ethereum?: 'goerli' | 'hardhat'
|
||||
/**
|
||||
* Initial user state.
|
||||
* @default {@type import('../utils/user-state').CONNECTED_WALLET_USER_STATE}
|
||||
*/
|
||||
userState?: Partial<UserState>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sets up the injected provider to be a mock ethereum provider with the given mnemonic/index
|
||||
// eslint-disable-next-line no-undef
|
||||
Cypress.Commands.overwrite(
|
||||
'visit',
|
||||
(original, url: string | Partial<Cypress.VisitOptions>, options?: Partial<Cypress.VisitOptions>) => {
|
||||
assert(typeof url === 'string')
|
||||
|
||||
// Add a hash in the URL if it is not present (to use hash-based routing correctly with queryParams).
|
||||
let hashUrl = url.startsWith('/') && url.length > 2 && !url.startsWith('/#') ? `/#${url}` : url
|
||||
if (options?.ethereum === 'goerli') hashUrl += `${url.includes('?') ? '&' : '?'}chain=goerli`
|
||||
|
||||
return cy
|
||||
.intercept('/service-worker.js', options?.serviceWorker ? undefined : { statusCode: 404 })
|
||||
.provider()
|
||||
.then((provider) =>
|
||||
original({
|
||||
...options,
|
||||
url: hashUrl,
|
||||
onBeforeLoad(win) {
|
||||
options?.onBeforeLoad?.(win)
|
||||
|
||||
// We want to test from a clean state, so we clear the local storage (which clears redux).
|
||||
win.localStorage.clear()
|
||||
|
||||
// Set initial user state.
|
||||
win.localStorage.setItem(
|
||||
'redux_localstorage_simple_user', // storage key for the user reducer using 'redux-localstorage-simple'
|
||||
JSON.stringify(options?.userState ?? CONNECTED_WALLET_USER_STATE)
|
||||
)
|
||||
|
||||
// Set feature flags, if configured.
|
||||
if (options?.featureFlags) {
|
||||
const featureFlags = options.featureFlags.reduce((flags, flag) => ({ ...flags, [flag]: 'enabled' }), {})
|
||||
win.localStorage.setItem('featureFlags', JSON.stringify(featureFlags))
|
||||
}
|
||||
|
||||
// Inject the mock ethereum provider.
|
||||
if (options?.ethereum === 'hardhat') {
|
||||
win.ethereum = provider
|
||||
} else {
|
||||
win.ethereum = injected
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
cy
|
||||
// Many API calls enforce that requests come from our app, so we must mock Origin and Referer.
|
||||
.intercept('*', (req) => {
|
||||
req.headers['referer'] = 'https://app.uniswap.org'
|
||||
req.headers['origin'] = 'https://app.uniswap.org'
|
||||
})
|
||||
// Infura uses a test endpoint, which allow-lists http://localhost:3000 instead.
|
||||
.intercept(/infura.io/, (req) => {
|
||||
req.headers['referer'] = 'http://localhost:3000'
|
||||
req.headers['origin'] = 'http://localhost:3000'
|
||||
req.alias = req.body.method
|
||||
req.continue()
|
||||
})
|
||||
// Mock Amplitude responses to avoid analytics from tests.
|
||||
.intercept('https://api.uniswap.org/v1/amplitude-proxy', (req) => {
|
||||
const requestBody = JSON.stringify(req.body)
|
||||
const byteSize = new Blob([requestBody]).size
|
||||
req.reply(
|
||||
JSON.stringify({
|
||||
code: 200,
|
||||
server_upload_time: Date.now(),
|
||||
payload_size_bytes: byteSize,
|
||||
events_ingested: req.body.events.length,
|
||||
})
|
||||
)
|
||||
})
|
||||
// Mock our own token list responses to avoid the latency of IPFS.
|
||||
.intercept('https://gateway.ipfs.io/ipns/tokens.uniswap.org', TokenListJSON)
|
||||
.intercept('https://gateway.ipfs.io/ipns/extendedtokens.uniswap.org', { statusCode: 201, body: { tokens: [] } })
|
||||
.intercept('https://gateway.ipfs.io/ipns/unsupportedtokens.uniswap.org', { statusCode: 201, body: { tokens: [] } })
|
||||
// Reset hardhat between tests to ensure isolation.
|
||||
// This resets the fork, as well as options like automine.
|
||||
.hardhat()
|
||||
.then((hardhat) => hardhat.reset())
|
||||
})
|
||||
// Squelch logs from fetches, as they clutter the logs so much as to make them unusable.
|
||||
// See https://docs.cypress.io/api/commands/intercept#Disabling-logs-for-a-request.
|
||||
// TODO(https://github.com/cypress-io/cypress/issues/26069): Squelch only wildcard logs once Cypress allows it.
|
||||
const log = Cypress.log
|
||||
Cypress.log = function (options, ...args) {
|
||||
if (options.displayName === 'script' || options.name === 'request') return
|
||||
return log(options, ...args)
|
||||
} as typeof log
|
||||
|
||||
Cypress.on('uncaught:exception', () => {
|
||||
// returning false here prevents Cypress from failing the test
|
||||
|
||||
41
cypress/support/setupTests.ts
Normal file
41
cypress/support/setupTests.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
// @ts-ignore
|
||||
import TokenListJSON from '@uniswap/default-token-list'
|
||||
|
||||
beforeEach(() => {
|
||||
// Many API calls enforce that requests come from our app, so we must mock Origin and Referer.
|
||||
cy.intercept('*', (req) => {
|
||||
req.headers['referer'] = 'https://app.uniswap.org'
|
||||
req.headers['origin'] = 'https://app.uniswap.org'
|
||||
})
|
||||
|
||||
// Infura uses a test endpoint, which allow-lists http://localhost:3000 instead.
|
||||
cy.intercept(/infura.io/, (req) => {
|
||||
req.headers['referer'] = 'http://localhost:3000'
|
||||
req.headers['origin'] = 'http://localhost:3000'
|
||||
req.alias = req.body.method
|
||||
req.continue()
|
||||
})
|
||||
|
||||
// Mock analytics responses to avoid analytics in tests.
|
||||
cy.intercept('https://api.uniswap.org/v1/amplitude-proxy', (req) => {
|
||||
const requestBody = JSON.stringify(req.body)
|
||||
const byteSize = new Blob([requestBody]).size
|
||||
req.reply(
|
||||
JSON.stringify({
|
||||
code: 200,
|
||||
server_upload_time: Date.now(),
|
||||
payload_size_bytes: byteSize,
|
||||
events_ingested: req.body.events.length,
|
||||
})
|
||||
)
|
||||
}).intercept('https://*.sentry.io', { statusCode: 200 })
|
||||
|
||||
// Mock our own token list responses to avoid the latency of IPFS.
|
||||
cy.intercept('https://gateway.ipfs.io/ipns/tokens.uniswap.org', TokenListJSON)
|
||||
.intercept('https://gateway.ipfs.io/ipns/extendedtokens.uniswap.org', { statusCode: 404 })
|
||||
.intercept('https://gateway.ipfs.io/ipns/unsupportedtokens.uniswap.org', { statusCode: 404 })
|
||||
|
||||
// Reset hardhat between tests to ensure isolation.
|
||||
// This resets the fork, as well as options like automine.
|
||||
cy.hardhat().then((hardhat) => hardhat.reset())
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-env node */
|
||||
|
||||
const defaultConfig = require('./graphql.config')
|
||||
const defaultConfig = require('./graphql.data.config')
|
||||
|
||||
module.exports = {
|
||||
src: defaultConfig.src,
|
||||
@@ -1,8 +1,63 @@
|
||||
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: [
|
||||
{
|
||||
path: '<rootDir>/src/locales/{locale}',
|
||||
include: ['<rootDir>/src'],
|
||||
include: ['<rootDir>/src/**/*.ts', '<rootDir>/src/**/*.tsx'],
|
||||
exclude: [
|
||||
'<rootDir>/src/**/*.d.ts',
|
||||
'<rootDir>/src/**/*.test.*',
|
||||
'<rootDir>/src/types/v3/**',
|
||||
'<rootDir>/src/abis/types/**',
|
||||
'<rootDir>/src/graphql/**/__generated__/**',
|
||||
],
|
||||
},
|
||||
],
|
||||
compileNamespace: 'cjs',
|
||||
@@ -53,6 +108,7 @@ const linguiConfig = {
|
||||
runtimeConfigModule: ['@lingui/core', 'i18n'],
|
||||
sourceLocale: 'en-US',
|
||||
pseudoLocale: 'pseudo',
|
||||
extractors: [cachingExtractor],
|
||||
}
|
||||
|
||||
export default linguiConfig
|
||||
|
||||
35
package.json
35
package.json
@@ -5,18 +5,20 @@
|
||||
"homepage": ".",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"scripts": {
|
||||
"ajv": "node scripts/compile-ajv-validators.js",
|
||||
"contracts:compile:abi": "typechain --target ethers-v5 --out-dir src/abis/types \"./src/abis/**/*.json\"",
|
||||
"contracts:compile:v3": "typechain --target ethers-v5 --out-dir src/types/v3 \"./node_modules/@uniswap/**/artifacts/contracts/**/*[!dbg].json\"",
|
||||
"contracts:compile": "yarn contracts:compile:abi && yarn contracts:compile:v3",
|
||||
"contracts": "yarn contracts:compile:abi && yarn contracts:compile:v3",
|
||||
"graphql:fetch": "node scripts/fetch-schema.js",
|
||||
"graphql:generate:data": "graphql-codegen --config apollo-codegen.ts",
|
||||
"graphql:generate:thegraph": "graphql-codegen --config apollo-codegen_thegraph.ts",
|
||||
"graphql:generate:data": "graphql-codegen --config graphql.data.codegen.config.ts",
|
||||
"graphql:generate:thegraph": "graphql-codegen --config graphql.thegraph.codegen.config.ts",
|
||||
"graphql:generate": "yarn graphql:generate:data && yarn graphql:generate:thegraph",
|
||||
"prei18n:extract": "node scripts/prei18n-extract.js",
|
||||
"graphql": "yarn graphql:fetch && yarn graphql:generate",
|
||||
"i18n:extract": "lingui extract --locale en-US",
|
||||
"i18n:compile": "yarn i18n:extract && lingui compile",
|
||||
"i18n:pseudo": "lingui extract --locale pseudo && lingui compile",
|
||||
"prepare": "yarn contracts:compile && yarn graphql:fetch && yarn graphql:generate && yarn i18n:compile",
|
||||
"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",
|
||||
"start": "craco start",
|
||||
"build": "craco build",
|
||||
"build:e2e": "REACT_APP_CSP_ALLOW_UNSAFE_EVAL=true REACT_APP_ADD_COVERAGE_INSTRUMENTATION=true craco build",
|
||||
@@ -97,7 +99,7 @@
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/wcag-contrast": "^3.0.0",
|
||||
"@uniswap/default-token-list": "^9.4.0",
|
||||
"@uniswap/eslint-config": "^1.1.1",
|
||||
"@uniswap/eslint-config": "^1.2.0",
|
||||
"@vanilla-extract/babel-plugin": "^1.1.7",
|
||||
"@vanilla-extract/jest-transform": "^1.1.1",
|
||||
"@vanilla-extract/webpack-plugin": "^2.1.11",
|
||||
@@ -159,7 +161,7 @@
|
||||
"@uniswap/router-sdk": "^1.3.0",
|
||||
"@uniswap/sdk-core": "^3.2.2",
|
||||
"@uniswap/smart-order-router": "^3.6.1",
|
||||
"@uniswap/token-lists": "^1.0.0-beta.30",
|
||||
"@uniswap/token-lists": "^1.0.0-beta.31",
|
||||
"@uniswap/universal-router-sdk": "^1.3.8",
|
||||
"@uniswap/v2-core": "1.0.0",
|
||||
"@uniswap/v2-periphery": "^1.1.0-beta.0",
|
||||
@@ -167,7 +169,6 @@
|
||||
"@uniswap/v3-core": "1.0.0",
|
||||
"@uniswap/v3-periphery": "^1.1.1",
|
||||
"@uniswap/v3-sdk": "^3.9.0",
|
||||
"@uniswap/widgets": "^2.49.0",
|
||||
"@vanilla-extract/css": "^1.7.2",
|
||||
"@vanilla-extract/css-utils": "^0.1.2",
|
||||
"@vanilla-extract/dynamic": "^2.0.2",
|
||||
@@ -190,6 +191,8 @@
|
||||
"@web3-react/types": "^8.2.0",
|
||||
"@web3-react/url": "^8.2.0",
|
||||
"@web3-react/walletconnect": "^8.2.0",
|
||||
"ajv": "^8.11.0",
|
||||
"ajv-formats": "^2.1.1",
|
||||
"array.prototype.flat": "^1.2.4",
|
||||
"array.prototype.flatmap": "^1.2.4",
|
||||
"cids": "^1.0.0",
|
||||
@@ -250,18 +253,6 @@
|
||||
"workbox-routing": "^6.1.0",
|
||||
"zustand": "^4.3.6"
|
||||
},
|
||||
"resolutions": {
|
||||
"@web3-react/coinbase-wallet": "^8.2.0",
|
||||
"@web3-react/core": "^8.2.0",
|
||||
"@web3-react/eip1193": "^8.2.0",
|
||||
"@web3-react/empty": "^8.2.0",
|
||||
"@web3-react/gnosis-safe": "^8.2.0",
|
||||
"@web3-react/metamask": "^8.2.0",
|
||||
"@web3-react/network": "^8.2.0",
|
||||
"@web3-react/types": "^8.2.0",
|
||||
"@web3-react/url": "^8.2.0",
|
||||
"@web3-react/walletconnect": "^8.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"npm": "please-use-yarn",
|
||||
"node": "14",
|
||||
|
||||
20
scripts/compile-ajv-validators.js
Normal file
20
scripts/compile-ajv-validators.js
Normal file
@@ -0,0 +1,20 @@
|
||||
/* eslint-env node */
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const Ajv = require('ajv')
|
||||
const standaloneCode = require('ajv/dist/standalone').default
|
||||
const addFormats = require('ajv-formats')
|
||||
const schema = require('@uniswap/token-lists/dist/tokenlist.schema.json')
|
||||
|
||||
const tokenListAjv = new Ajv({ code: { source: true, esm: true } })
|
||||
addFormats(tokenListAjv)
|
||||
const validateTokenList = tokenListAjv.compile(schema)
|
||||
let tokenListModuleCode = standaloneCode(tokenListAjv, validateTokenList)
|
||||
fs.writeFileSync(path.join(__dirname, '../src/utils/__generated__/validateTokenList.js'), tokenListModuleCode)
|
||||
|
||||
const tokensAjv = new Ajv({ code: { source: true, esm: true } })
|
||||
addFormats(tokensAjv)
|
||||
const validateTokens = tokensAjv.compile({ ...schema, required: ['tokens'] })
|
||||
let tokensModuleCode = standaloneCode(tokensAjv, validateTokens)
|
||||
fs.writeFileSync(path.join(__dirname, '../src/utils/__generated__/validateTokens.js'), tokensModuleCode)
|
||||
@@ -4,8 +4,8 @@ require('dotenv').config({ path: '.env.production' })
|
||||
const child_process = require('child_process')
|
||||
const fs = require('fs/promises')
|
||||
const { promisify } = require('util')
|
||||
const dataConfig = require('../graphql.config')
|
||||
const thegraphConfig = require('../graphql_thegraph.config')
|
||||
const dataConfig = require('../graphql.data.config')
|
||||
const thegraphConfig = require('../graphql.thegraph.config')
|
||||
|
||||
const exec = promisify(child_process.exec)
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
/* eslint-env node */
|
||||
|
||||
const { exec } = require('child_process')
|
||||
const isWindows = process.platform === 'win32' || /^(msys|cygwin)$/.test(process.env.OSTYPE)
|
||||
|
||||
if (isWindows) {
|
||||
exec(`type nul > src/locales/en-US.po`)
|
||||
} else {
|
||||
exec(`touch src/locales/en-US.po`)
|
||||
}
|
||||
@@ -87,10 +87,20 @@ function combineActivities(localMap: ActivityMap = {}, remoteMap: ActivityMap =
|
||||
|
||||
// Merges local and remote activities w/ same hash, preferring remote data
|
||||
return txHashes.reduce((acc: Array<Activity>, hash) => {
|
||||
const localActivity = localMap?.[hash] ?? {}
|
||||
const remoteActivity = remoteMap?.[hash] ?? {}
|
||||
// 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({ ...remoteActivity, ...localActivity } as Activity)
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { formatCurrencyAmount } from '@uniswap/conedison/format'
|
||||
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
|
||||
import { nativeOnChain } from '@uniswap/smart-order-router'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { TransactionPartsFragment, TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { ChainTokenMap, useAllTokensMultichain } from 'hooks/Tokens'
|
||||
import { useMemo } from 'react'
|
||||
import { useMultichainTransactions } from 'state/transactions/hooks'
|
||||
@@ -141,7 +141,7 @@ export function parseLocalActivity(
|
||||
? TransactionStatus.Confirmed
|
||||
: TransactionStatus.Failed
|
||||
|
||||
const receipt: TransactionPartsFragment | undefined = details.receipt
|
||||
const receipt = details.receipt
|
||||
? {
|
||||
id: details.receipt.transactionHash,
|
||||
...details.receipt,
|
||||
@@ -157,6 +157,7 @@ export function parseLocalActivity(
|
||||
status,
|
||||
timestamp: (details.confirmedTime ?? details.addedTime) / 1000,
|
||||
receipt,
|
||||
nonce: details.nonce,
|
||||
}
|
||||
|
||||
let additionalFields: Partial<Activity> = {}
|
||||
|
||||
@@ -254,6 +254,7 @@ function parseRemoteActivity(assetActivity: AssetActivityPartsFragment): Activit
|
||||
title: assetActivity.type,
|
||||
descriptor: assetActivity.transaction.to,
|
||||
receipt: assetActivity.transaction,
|
||||
nonce: assetActivity.transaction.nonce,
|
||||
}
|
||||
const parsedFields = ActivityParserByType[assetActivity.type]?.(changes, assetActivity)
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ export type Activity = {
|
||||
logos?: Array<string | undefined>
|
||||
currencies?: Array<Currency | undefined>
|
||||
otherAccount?: string
|
||||
receipt?: Receipt
|
||||
receipt?: Omit<Receipt, 'nonce'>
|
||||
nonce?: number | null
|
||||
}
|
||||
|
||||
export type ActivityMap = { [hash: string]: Activity | undefined }
|
||||
|
||||
@@ -30,6 +30,7 @@ const Wrapper = styled(Column)<{ numItems: number; isExpanded: boolean }>`
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
// TODO(WEB-3288): Replace this component to use `components/Expand` under the hood
|
||||
type ExpandoRowProps = PropsWithChildren<{ title?: string; numItems: number; isExpanded: boolean; toggle: () => void }>
|
||||
export function ExpandoRow({ title = t`Hidden`, numItems, isExpanded, toggle, children }: ExpandoRowProps) {
|
||||
if (numItems === 0) return null
|
||||
|
||||
105
src/components/Expand/__snapshots__/index.test.tsx.snap
Normal file
105
src/components/Expand/__snapshots__/index.test.tsx.snap
Normal file
@@ -0,0 +1,105 @@
|
||||
// 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>
|
||||
`;
|
||||
30
src/components/Expand/index.test.tsx
Normal file
30
src/components/Expand/index.test.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { fireEvent, render, screen } from 'test-utils/render'
|
||||
|
||||
import Expand from './index'
|
||||
|
||||
describe('Expand', () => {
|
||||
it('renders correctly', () => {
|
||||
const { asFragment } = render(
|
||||
<Expand header={<span>Header</span>} button={<span>Button</span>}>
|
||||
Body
|
||||
</Expand>
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('toggles children on button press', () => {
|
||||
render(
|
||||
<Expand header={<span>Header</span>} button={<span>Button</span>}>
|
||||
Body
|
||||
</Expand>
|
||||
)
|
||||
|
||||
const button = screen.getByText('Button')
|
||||
|
||||
fireEvent.click(button)
|
||||
expect(screen.queryByText('Body')).not.toBeNull()
|
||||
|
||||
fireEvent.click(button)
|
||||
expect(screen.queryByText('Body')).toBeNull()
|
||||
})
|
||||
})
|
||||
43
src/components/Expand/index.tsx
Normal file
43
src/components/Expand/index.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import Column from 'components/Column'
|
||||
import React, { PropsWithChildren, ReactElement, useState } from 'react'
|
||||
import { ChevronDown } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import Row, { RowBetween } from '../Row'
|
||||
|
||||
const ButtonContainer = styled(Row)`
|
||||
cursor: pointer;
|
||||
justify-content: flex-end;
|
||||
width: unset;
|
||||
`
|
||||
|
||||
const ExpandIcon = styled(ChevronDown)<{ $isExpanded: boolean }>`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
transform: ${({ $isExpanded }) => ($isExpanded ? 'rotate(180deg)' : 'rotate(0deg)')};
|
||||
transition: transform ${({ theme }) => theme.transition.duration.medium};
|
||||
`
|
||||
|
||||
export default function Expand({
|
||||
header,
|
||||
button,
|
||||
children,
|
||||
testId,
|
||||
}: PropsWithChildren<{
|
||||
header: ReactElement
|
||||
button: ReactElement
|
||||
testId?: string
|
||||
}>) {
|
||||
const [isExpanded, setExpanded] = useState(false)
|
||||
return (
|
||||
<Column gap="md">
|
||||
<RowBetween>
|
||||
{header}
|
||||
<ButtonContainer data-testid={testId} onClick={() => setExpanded(!isExpanded)} aria-expanded={isExpanded}>
|
||||
{button}
|
||||
<ExpandIcon $isExpanded={isExpanded} />
|
||||
</ButtonContainer>
|
||||
</RowBetween>
|
||||
{isExpanded && children}
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
|
||||
import { DetailsV2Variant, useDetailsV2Flag } from 'featureFlags/flags/nftDetails'
|
||||
import { useWidgetRemovalFlag, WidgetRemovalVariant } from 'featureFlags/flags/removeWidgetTdp'
|
||||
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
|
||||
import { useUpdateAtom } from 'jotai/utils'
|
||||
import { Children, PropsWithChildren, ReactElement, ReactNode, useCallback, useState } from 'react'
|
||||
@@ -208,12 +207,6 @@ export default function FeatureFlagModal() {
|
||||
featureFlag={FeatureFlag.detailsV2}
|
||||
label="Use the new details page for nfts"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={WidgetRemovalVariant}
|
||||
value={useWidgetRemovalFlag()}
|
||||
featureFlag={FeatureFlag.removeWidget}
|
||||
label="Swap Component on TDP"
|
||||
/>
|
||||
<FeatureFlagGroup name="Debug">
|
||||
<FeatureFlagOption
|
||||
variant={TraceJsonRpcVariant}
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function Loader({
|
||||
{...rest}
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 9.27455 20.9097 6.80375"
|
||||
d="M2,12 a10,10 0 0,1 10,-10 M12,22 a10,10 0 0,1 -10,-10 M22,12 a10,10 0 0,1 -10,10"
|
||||
strokeWidth={strokeWidth ?? '2.5'}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
|
||||
@@ -95,7 +95,7 @@ export const ChainSelector = ({ leftAlign }: ChainSelectorProps) => {
|
||||
|
||||
return (
|
||||
<Box position="relative" ref={ref}>
|
||||
<MouseoverTooltip text={t`Your wallet's current network is unsupported.`} disableHover={isSupported}>
|
||||
<MouseoverTooltip text={t`Your wallet's current network is unsupported.`} disabled={isSupported}>
|
||||
<Row
|
||||
as="button"
|
||||
gap="8"
|
||||
|
||||
@@ -57,13 +57,13 @@ export function FindPoolTabs({ origin }: { origin: string }) {
|
||||
export function AddRemoveTabs({
|
||||
adding,
|
||||
creating,
|
||||
defaultSlippage,
|
||||
autoSlippage,
|
||||
positionID,
|
||||
children,
|
||||
}: {
|
||||
adding: boolean
|
||||
creating: boolean
|
||||
defaultSlippage: Percent
|
||||
autoSlippage: Percent
|
||||
positionID?: string | undefined
|
||||
showBackLink?: boolean
|
||||
children?: ReactNode | undefined
|
||||
@@ -108,7 +108,7 @@ export function AddRemoveTabs({
|
||||
)}
|
||||
</ThemedText.DeprecatedMediumHeader>
|
||||
<Box style={{ marginRight: '.5rem' }}>{children}</Box>
|
||||
<SettingsTab placeholderSlippage={defaultSlippage} />
|
||||
<SettingsTab autoSlippage={autoSlippage} />
|
||||
</RowBetween>
|
||||
</Tabs>
|
||||
)
|
||||
|
||||
@@ -99,8 +99,8 @@ export default function Popover({
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null)
|
||||
const [arrowElement, setArrowElement] = useState<HTMLDivElement | null>(null)
|
||||
|
||||
const options = useMemo(
|
||||
(): Options => ({
|
||||
const options: Options = useMemo(
|
||||
() => ({
|
||||
placement,
|
||||
strategy: 'fixed',
|
||||
modifiers: [
|
||||
@@ -109,7 +109,7 @@ export default function Popover({
|
||||
{ name: 'preventOverflow', options: { padding: 8 } },
|
||||
],
|
||||
}),
|
||||
[arrowElement, offsetX, offsetY, placement]
|
||||
[placement, offsetX, offsetY, arrowElement]
|
||||
)
|
||||
|
||||
const { styles, update, attributes } = usePopper(referenceElement, popperElement, options)
|
||||
|
||||
50
src/components/Radio/index.tsx
Normal file
50
src/components/Radio/index.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { RowBetween } from 'components/Row'
|
||||
import { darken } from 'polished'
|
||||
import { PropsWithChildren } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
const Button = styled.button<{ isActive?: boolean; activeElement?: boolean }>`
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: 2px solid ${({ theme, isActive }) => (isActive ? theme.accentAction : theme.backgroundOutline)};
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
outline: none;
|
||||
padding: 5px;
|
||||
width: fit-content;
|
||||
`
|
||||
|
||||
const ButtonFill = styled.span<{ isActive?: boolean }>`
|
||||
background: ${({ theme, isActive }) => (isActive ? theme.accentAction : theme.textTertiary)};
|
||||
border-radius: 50%;
|
||||
:hover {
|
||||
background: ${({ isActive, theme }) =>
|
||||
isActive ? darken(0.05, theme.accentAction) : darken(0.05, theme.deprecated_bg4)};
|
||||
color: ${({ isActive, theme }) => (isActive ? theme.white : theme.textTertiary)};
|
||||
}
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
opacity: ${({ isActive }) => (isActive ? 1 : 0)};
|
||||
`
|
||||
|
||||
const Container = styled(RowBetween)`
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
interface RadioProps {
|
||||
className?: string
|
||||
isActive: boolean
|
||||
toggle: () => void
|
||||
}
|
||||
|
||||
export default function Radio({ className, isActive, children, toggle }: PropsWithChildren<RadioProps>) {
|
||||
return (
|
||||
<Container className={className} onClick={toggle}>
|
||||
{children}
|
||||
<Button isActive={isActive}>
|
||||
<ButtonFill isActive={isActive} />
|
||||
</Button>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Protocol } from '@uniswap/router-sdk'
|
||||
import { Currency, Percent } from '@uniswap/sdk-core'
|
||||
import { FeeAmount } from '@uniswap/v3-sdk'
|
||||
import { RoutingDiagramEntry } from 'components/swap/SwapRoute'
|
||||
import { DAI, USDC_MAINNET, WBTC } from 'constants/tokens'
|
||||
import { render } from 'test-utils/render'
|
||||
import { RoutingDiagramEntry } from 'utils/getRoutingDiagramEntries'
|
||||
|
||||
import RoutingDiagram from './RoutingDiagram'
|
||||
|
||||
|
||||
@@ -6,12 +6,12 @@ import Badge from 'components/Badge'
|
||||
import DoubleCurrencyLogo from 'components/DoubleLogo'
|
||||
import CurrencyLogo from 'components/Logo/CurrencyLogo'
|
||||
import Row, { AutoRow } from 'components/Row'
|
||||
import { RoutingDiagramEntry } from 'components/swap/SwapRoute'
|
||||
import { useTokenInfoFromActiveList } from 'hooks/useTokenInfoFromActiveList'
|
||||
import { Box } from 'rebass'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
import { RoutingDiagramEntry } from 'utils/getRoutingDiagramEntries'
|
||||
|
||||
import { ReactComponent as DotLine } from '../../assets/svg/dot_line.svg'
|
||||
import { MouseoverTooltip } from '../Tooltip'
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Box } from 'rebass/styled-components'
|
||||
import styled from 'styled-components/macro'
|
||||
import styled, { DefaultTheme } from 'styled-components/macro'
|
||||
|
||||
type Gap = keyof DefaultTheme['grids']
|
||||
|
||||
// TODO(WEB-3289):
|
||||
// Setting `width: 100%` by default prevents composability in complex flex layouts.
|
||||
// Same applies to `RowFixed` and its negative margins. This component needs to be
|
||||
// further investigated and improved to make UI work easier.
|
||||
const Row = styled(Box)<{
|
||||
width?: string
|
||||
align?: string
|
||||
@@ -18,7 +24,7 @@ const Row = styled(Box)<{
|
||||
padding: ${({ padding }) => padding};
|
||||
border: ${({ border }) => border};
|
||||
border-radius: ${({ borderRadius }) => borderRadius};
|
||||
gap: ${({ gap }) => gap};
|
||||
gap: ${({ gap, theme }) => gap && (theme.grids[gap as Gap] || gap)};
|
||||
`
|
||||
|
||||
export const RowBetween = styled(Row)`
|
||||
|
||||
46
src/components/Settings/Input/index.tsx
Normal file
46
src/components/Settings/Input/index.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import Row from '../../Row'
|
||||
|
||||
export const Input = styled.input`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
border: 0;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
text-align: right;
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
::placeholder {
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
}
|
||||
`
|
||||
|
||||
export const InputContainer = styled(Row)<{ error?: boolean }>`
|
||||
padding: 8px 16px;
|
||||
border-radius: 12px;
|
||||
width: auto;
|
||||
flex: 1;
|
||||
input {
|
||||
color: ${({ theme, error }) => (error ? theme.accentFailure : theme.textPrimary)};
|
||||
}
|
||||
border: 1px solid ${({ theme, error }) => (error ? theme.accentFailure : theme.deprecated_bg3)};
|
||||
${({ theme, error }) =>
|
||||
error
|
||||
? `
|
||||
border: 1px solid ${theme.accentFailure};
|
||||
:focus-within {
|
||||
border-color: ${theme.accentFailureSoft};
|
||||
}
|
||||
`
|
||||
: `
|
||||
border: 1px solid ${theme.backgroundOutline};
|
||||
:focus-within {
|
||||
border-color: ${theme.accentActiveSoft};
|
||||
}
|
||||
`}
|
||||
`
|
||||
98
src/components/Settings/MaxSlippageSettings/index.test.tsx
Normal file
98
src/components/Settings/MaxSlippageSettings/index.test.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
import store from 'state'
|
||||
import { updateUserSlippageTolerance } from 'state/user/reducer'
|
||||
import { SlippageTolerance } from 'state/user/types'
|
||||
import { fireEvent, render, screen } from 'test-utils/render'
|
||||
|
||||
import MaxSlippageSettings from '.'
|
||||
|
||||
const AUTO_SLIPPAGE = new Percent(5, 10_000)
|
||||
|
||||
const renderAndExpandSlippageSettings = () => {
|
||||
render(<MaxSlippageSettings autoSlippage={AUTO_SLIPPAGE} />)
|
||||
|
||||
// By default, the button to expand Slippage component and show `input` will have `Auto` label
|
||||
fireEvent.click(screen.getByText('Auto'))
|
||||
}
|
||||
|
||||
// Switch to custom mode by tapping on `Custom` label
|
||||
const switchToCustomSlippage = () => {
|
||||
fireEvent.click(screen.getByText('Custom'))
|
||||
}
|
||||
|
||||
const getSlippageInput = () => screen.getByTestId('slippage-input') as HTMLInputElement
|
||||
|
||||
describe('MaxSlippageSettings', () => {
|
||||
describe('input', () => {
|
||||
// Restore to default slippage before each unit test
|
||||
beforeEach(() => {
|
||||
store.dispatch(updateUserSlippageTolerance({ userSlippageTolerance: SlippageTolerance.Auto }))
|
||||
})
|
||||
it('does not render auto slippage as a value, but a placeholder', () => {
|
||||
renderAndExpandSlippageSettings()
|
||||
switchToCustomSlippage()
|
||||
|
||||
expect(getSlippageInput().value).toBe('')
|
||||
})
|
||||
it('renders custom slippage above the input', () => {
|
||||
renderAndExpandSlippageSettings()
|
||||
switchToCustomSlippage()
|
||||
|
||||
fireEvent.change(getSlippageInput(), { target: { value: '0.5' } })
|
||||
|
||||
expect(screen.queryAllByText('0.50%').length).toEqual(1)
|
||||
})
|
||||
it('updates input value on blur with the slippage in store', () => {
|
||||
renderAndExpandSlippageSettings()
|
||||
switchToCustomSlippage()
|
||||
|
||||
const input = getSlippageInput()
|
||||
fireEvent.change(input, { target: { value: '0.5' } })
|
||||
fireEvent.blur(input)
|
||||
|
||||
expect(input.value).toBe('0.50')
|
||||
})
|
||||
it('clears errors on blur and overwrites incorrect value with the latest correct value', () => {
|
||||
renderAndExpandSlippageSettings()
|
||||
switchToCustomSlippage()
|
||||
|
||||
const input = getSlippageInput()
|
||||
fireEvent.change(input, { target: { value: '5' } })
|
||||
fireEvent.change(input, { target: { value: '50' } })
|
||||
fireEvent.change(input, { target: { value: '500' } })
|
||||
fireEvent.blur(input)
|
||||
|
||||
expect(input.value).toBe('50.00')
|
||||
})
|
||||
it('does not allow to enter more than 2 digits after the decimal point', () => {
|
||||
renderAndExpandSlippageSettings()
|
||||
switchToCustomSlippage()
|
||||
|
||||
const input = getSlippageInput()
|
||||
fireEvent.change(input, { target: { value: '0.01' } })
|
||||
fireEvent.change(input, { target: { value: '0.011' } })
|
||||
|
||||
expect(input.value).toBe('0.01')
|
||||
})
|
||||
it('does not accept non-numerical values', () => {
|
||||
renderAndExpandSlippageSettings()
|
||||
switchToCustomSlippage()
|
||||
|
||||
const input = getSlippageInput()
|
||||
fireEvent.change(input, { target: { value: 'c' } })
|
||||
|
||||
expect(input.value).toBe('')
|
||||
})
|
||||
it('does not set slippage when user enters `.` value', () => {
|
||||
renderAndExpandSlippageSettings()
|
||||
switchToCustomSlippage()
|
||||
|
||||
const input = getSlippageInput()
|
||||
fireEvent.change(input, { target: { value: '.' } })
|
||||
expect(input.value).toBe('.')
|
||||
|
||||
fireEvent.blur(input)
|
||||
expect(input.value).toBe('')
|
||||
})
|
||||
})
|
||||
})
|
||||
175
src/components/Settings/MaxSlippageSettings/index.tsx
Normal file
175
src/components/Settings/MaxSlippageSettings/index.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
import Expand from 'components/Expand'
|
||||
import QuestionHelper from 'components/QuestionHelper'
|
||||
import Row, { RowBetween } from 'components/Row'
|
||||
import React, { useState } from 'react'
|
||||
import { useUserSlippageTolerance } from 'state/user/hooks'
|
||||
import { SlippageTolerance } from 'state/user/types'
|
||||
import styled from 'styled-components/macro'
|
||||
import { CautionTriangle, ThemedText } from 'theme'
|
||||
|
||||
import { Input, InputContainer } from '../Input'
|
||||
|
||||
enum SlippageError {
|
||||
InvalidInput = 'InvalidInput',
|
||||
}
|
||||
|
||||
const Option = styled(Row)<{ isActive: boolean }>`
|
||||
width: auto;
|
||||
cursor: pointer;
|
||||
padding: 6px 12px;
|
||||
text-align: center;
|
||||
gap: 4px;
|
||||
border-radius: 12px;
|
||||
background: ${({ isActive, theme }) => (isActive ? theme.backgroundInteractive : 'transparent')};
|
||||
pointer-events: ${({ isActive }) => isActive && 'none'};
|
||||
`
|
||||
|
||||
const Switch = styled(Row)`
|
||||
width: auto;
|
||||
padding: 4px;
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
border-radius: 16px;
|
||||
`
|
||||
|
||||
const NUMBER_WITH_MAX_TWO_DECIMAL_PLACES = /^(?:\d*\.\d{0,2}|\d+)$/
|
||||
const MINIMUM_RECOMMENDED_SLIPPAGE = new Percent(5, 10_000)
|
||||
const MAXIMUM_RECOMMENDED_SLIPPAGE = new Percent(1, 100)
|
||||
|
||||
export default function MaxSlippageSettings({ autoSlippage }: { autoSlippage: Percent }) {
|
||||
const [userSlippageTolerance, setUserSlippageTolerance] = useUserSlippageTolerance()
|
||||
|
||||
// In order to trigger `custom` mode, we need to set `userSlippageTolerance` to a value that is not `auto`.
|
||||
// To do so, we use `autoSlippage` value. However, since users are likely to change that value,
|
||||
// we render it as a placeholder instead of a value.
|
||||
const defaultSlippageInputValue =
|
||||
userSlippageTolerance !== SlippageTolerance.Auto && !userSlippageTolerance.equalTo(autoSlippage)
|
||||
? userSlippageTolerance.toFixed(2)
|
||||
: ''
|
||||
|
||||
// If user has previously entered a custom slippage, we want to show that value in the input field
|
||||
// instead of a placeholder.
|
||||
const [slippageInput, setSlippageInput] = useState(defaultSlippageInputValue)
|
||||
const [slippageError, setSlippageError] = useState<SlippageError | false>(false)
|
||||
|
||||
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)) {
|
||||
return
|
||||
}
|
||||
|
||||
setSlippageInput(value)
|
||||
setSlippageError(false)
|
||||
|
||||
// If the input is empty, set the slippage to the default
|
||||
if (value.length === 0) {
|
||||
setUserSlippageTolerance(SlippageTolerance.Auto)
|
||||
return
|
||||
}
|
||||
|
||||
if (value === '.') {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse user input and set the slippage if valid, error otherwise
|
||||
try {
|
||||
const parsed = Math.floor(Number.parseFloat(value) * 100)
|
||||
if (parsed > 5000) {
|
||||
setSlippageError(SlippageError.InvalidInput)
|
||||
} else {
|
||||
setUserSlippageTolerance(new Percent(parsed, 10_000))
|
||||
}
|
||||
} catch (e) {
|
||||
setSlippageError(SlippageError.InvalidInput)
|
||||
}
|
||||
}
|
||||
|
||||
const tooLow =
|
||||
userSlippageTolerance !== SlippageTolerance.Auto && userSlippageTolerance.lessThan(MINIMUM_RECOMMENDED_SLIPPAGE)
|
||||
const tooHigh =
|
||||
userSlippageTolerance !== SlippageTolerance.Auto && userSlippageTolerance.greaterThan(MAXIMUM_RECOMMENDED_SLIPPAGE)
|
||||
|
||||
return (
|
||||
<Expand
|
||||
testId="max-slippage-settings"
|
||||
header={
|
||||
<Row width="auto">
|
||||
<ThemedText.BodySecondary>
|
||||
<Trans>Max slippage</Trans>
|
||||
</ThemedText.BodySecondary>
|
||||
<QuestionHelper
|
||||
text={
|
||||
<Trans>Your transaction will revert if the price changes unfavorably by more than this percentage.</Trans>
|
||||
}
|
||||
/>
|
||||
</Row>
|
||||
}
|
||||
button={
|
||||
<ThemedText.BodyPrimary>
|
||||
{userSlippageTolerance === SlippageTolerance.Auto ? (
|
||||
<Trans>Auto</Trans>
|
||||
) : (
|
||||
`${userSlippageTolerance.toFixed(2)}%`
|
||||
)}
|
||||
</ThemedText.BodyPrimary>
|
||||
}
|
||||
>
|
||||
<RowBetween gap="md">
|
||||
<Switch>
|
||||
<Option
|
||||
onClick={() => {
|
||||
// Reset the input field when switching to auto
|
||||
setSlippageInput('')
|
||||
setUserSlippageTolerance(SlippageTolerance.Auto)
|
||||
}}
|
||||
isActive={userSlippageTolerance === SlippageTolerance.Auto}
|
||||
>
|
||||
<ThemedText.BodyPrimary>
|
||||
<Trans>Auto</Trans>
|
||||
</ThemedText.BodyPrimary>
|
||||
</Option>
|
||||
<Option
|
||||
onClick={() => {
|
||||
// When switching to custom slippage, use `auto` value as a default.
|
||||
setUserSlippageTolerance(autoSlippage)
|
||||
}}
|
||||
isActive={userSlippageTolerance !== SlippageTolerance.Auto}
|
||||
>
|
||||
<ThemedText.BodyPrimary>
|
||||
<Trans>Custom</Trans>
|
||||
</ThemedText.BodyPrimary>
|
||||
</Option>
|
||||
</Switch>
|
||||
<InputContainer gap="md" error={!!slippageError}>
|
||||
<Input
|
||||
data-testid="slippage-input"
|
||||
placeholder={autoSlippage.toFixed(2)}
|
||||
value={slippageInput}
|
||||
onChange={(e) => parseSlippageInput(e.target.value)}
|
||||
onBlur={() => {
|
||||
// When the input field is blurred, reset the input field to the default value
|
||||
setSlippageInput(defaultSlippageInputValue)
|
||||
setSlippageError(false)
|
||||
}}
|
||||
/>
|
||||
<ThemedText.BodyPrimary>%</ThemedText.BodyPrimary>
|
||||
</InputContainer>
|
||||
</RowBetween>
|
||||
{tooLow || tooHigh ? (
|
||||
<RowBetween gap="md">
|
||||
<CautionTriangle />
|
||||
<ThemedText.Caption color="accentWarning">
|
||||
{tooLow ? (
|
||||
<Trans>
|
||||
Slippage below {MINIMUM_RECOMMENDED_SLIPPAGE.toFixed(2)}% may result in a failed transaction
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>Your transaction may be frontrun and result in an unfavorable trade.</Trans>
|
||||
)}
|
||||
</ThemedText.Caption>
|
||||
</RowBetween>
|
||||
) : null}
|
||||
</Expand>
|
||||
)
|
||||
}
|
||||
81
src/components/Settings/RouterPreferenceSettings/index.tsx
Normal file
81
src/components/Settings/RouterPreferenceSettings/index.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import Column from 'components/Column'
|
||||
import Radio from 'components/Radio'
|
||||
import { RowBetween, RowFixed } from 'components/Row'
|
||||
import Toggle from 'components/Toggle'
|
||||
import { RouterPreference } from 'state/routing/slice'
|
||||
import { useRouterPreference } from 'state/user/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
const Preference = styled(Radio)`
|
||||
background-color: ${({ theme }) => theme.backgroundModule};
|
||||
padding: 12px 16px;
|
||||
`
|
||||
|
||||
const PreferencesContainer = styled(Column)`
|
||||
gap: 1.5px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
export default function RouterPreferenceSettings() {
|
||||
const [routerPreference, setRouterPreference] = useRouterPreference()
|
||||
|
||||
const isAutoRoutingActive = routerPreference === RouterPreference.AUTO
|
||||
|
||||
return (
|
||||
<Column gap="md">
|
||||
<RowBetween gap="sm">
|
||||
<RowFixed>
|
||||
<Column gap="xs">
|
||||
<ThemedText.BodySecondary>
|
||||
<Trans>Auto Router API</Trans>
|
||||
</ThemedText.BodySecondary>
|
||||
<ThemedText.Caption color="textSecondary">
|
||||
<Trans>Use the Uniswap Labs API to get faster quotes.</Trans>
|
||||
</ThemedText.Caption>
|
||||
</Column>
|
||||
</RowFixed>
|
||||
<Toggle
|
||||
id="toggle-optimized-router-button"
|
||||
isActive={isAutoRoutingActive}
|
||||
toggle={() => setRouterPreference(isAutoRoutingActive ? RouterPreference.API : RouterPreference.AUTO)}
|
||||
/>
|
||||
</RowBetween>
|
||||
{!isAutoRoutingActive && (
|
||||
<PreferencesContainer>
|
||||
<Preference
|
||||
isActive={routerPreference === RouterPreference.API}
|
||||
toggle={() => setRouterPreference(RouterPreference.API)}
|
||||
>
|
||||
<Column gap="xs">
|
||||
<ThemedText.BodyPrimary>
|
||||
<Trans>Uniswap API</Trans>
|
||||
</ThemedText.BodyPrimary>
|
||||
<ThemedText.Caption color="textSecondary">
|
||||
<Trans>Finds the best route on the Uniswap Protocol using the Uniswap Labs Routing API.</Trans>
|
||||
</ThemedText.Caption>
|
||||
</Column>
|
||||
</Preference>
|
||||
<Preference
|
||||
isActive={routerPreference === RouterPreference.CLIENT}
|
||||
toggle={() => setRouterPreference(RouterPreference.CLIENT)}
|
||||
>
|
||||
<Column gap="xs">
|
||||
<ThemedText.BodyPrimary>
|
||||
<Trans>Uniswap client</Trans>
|
||||
</ThemedText.BodyPrimary>
|
||||
<ThemedText.Caption color="textSecondary">
|
||||
<Trans>
|
||||
Finds the best route on the Uniswap Protocol through your browser. May result in high latency and
|
||||
prices.
|
||||
</Trans>
|
||||
</ThemedText.Caption>
|
||||
</Column>
|
||||
</Preference>
|
||||
</PreferencesContainer>
|
||||
)}
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { DEFAULT_DEADLINE_FROM_NOW } from 'constants/misc'
|
||||
import store from 'state'
|
||||
import { updateUserDeadline } from 'state/user/reducer'
|
||||
import { fireEvent, render, screen } from 'test-utils/render'
|
||||
|
||||
import TransactionDeadlineSettings from '.'
|
||||
|
||||
const renderAndExpandTransactionDeadlineSettings = () => {
|
||||
render(<TransactionDeadlineSettings />)
|
||||
|
||||
// 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
|
||||
|
||||
describe('TransactionDeadlineSettings', () => {
|
||||
describe('input', () => {
|
||||
// Restore to default transaction deadline before each unit test
|
||||
beforeEach(() => {
|
||||
store.dispatch(updateUserDeadline({ userDeadline: DEFAULT_DEADLINE_FROM_NOW }))
|
||||
})
|
||||
it('does not render default deadline as a value, but a placeholder', () => {
|
||||
renderAndExpandTransactionDeadlineSettings()
|
||||
expect(getDeadlineInput().value).toBe('')
|
||||
})
|
||||
it('renders custom deadline above the input', () => {
|
||||
renderAndExpandTransactionDeadlineSettings()
|
||||
|
||||
fireEvent.change(getDeadlineInput(), { target: { value: '50' } })
|
||||
|
||||
expect(screen.queryAllByText('50m').length).toEqual(1)
|
||||
})
|
||||
it('marks deadline as invalid if it is greater than 4320m (3 days) or 0m', () => {
|
||||
renderAndExpandTransactionDeadlineSettings()
|
||||
|
||||
const input = getDeadlineInput()
|
||||
fireEvent.change(input, { target: { value: '4321' } })
|
||||
fireEvent.change(input, { target: { value: '0' } })
|
||||
fireEvent.blur(input)
|
||||
|
||||
expect(input.value).toBe('')
|
||||
})
|
||||
it('clears errors on blur and overwrites incorrect value with the latest correct value', () => {
|
||||
renderAndExpandTransactionDeadlineSettings()
|
||||
|
||||
const input = getDeadlineInput()
|
||||
fireEvent.change(input, { target: { value: '5' } })
|
||||
fireEvent.change(input, { target: { value: '4321' } })
|
||||
|
||||
// Label renders latest correct value, at this point input is higlighted as invalid
|
||||
expect(screen.queryAllByText('5m').length).toEqual(1)
|
||||
|
||||
fireEvent.blur(input)
|
||||
|
||||
expect(input.value).toBe('5')
|
||||
})
|
||||
it('does not accept non-numerical values', () => {
|
||||
renderAndExpandTransactionDeadlineSettings()
|
||||
|
||||
const input = getDeadlineInput()
|
||||
fireEvent.change(input, { target: { value: 'c' } })
|
||||
|
||||
expect(input.value).toBe('')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import Expand from 'components/Expand'
|
||||
import QuestionHelper from 'components/QuestionHelper'
|
||||
import Row from 'components/Row'
|
||||
import { Input, InputContainer } from 'components/Settings/Input'
|
||||
import { DEFAULT_DEADLINE_FROM_NOW } from 'constants/misc'
|
||||
import ms from 'ms.macro'
|
||||
import React, { useState } from 'react'
|
||||
import { useUserTransactionTTL } from 'state/user/hooks'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
enum DeadlineError {
|
||||
InvalidInput = 'InvalidInput',
|
||||
}
|
||||
|
||||
const THREE_DAYS_IN_SECONDS = ms`3 days` / 1000
|
||||
const NUMBERS_ONLY = /^[0-9\b]+$/
|
||||
|
||||
export default function TransactionDeadlineSettings() {
|
||||
const [deadline, setDeadline] = useUserTransactionTTL()
|
||||
|
||||
const defaultInputValue = deadline && deadline !== DEFAULT_DEADLINE_FROM_NOW ? (deadline / 60).toString() : ''
|
||||
|
||||
// If user has previously entered a custom deadline, we want to show that value in the input field
|
||||
// instead of a placeholder by defualt
|
||||
const [deadlineInput, setDeadlineInput] = useState(defaultInputValue)
|
||||
const [deadlineError, setDeadlineError] = useState<DeadlineError | false>(false)
|
||||
|
||||
function parseCustomDeadline(value: string) {
|
||||
// Do not allow non-numerical characters in the input field
|
||||
if (value.length > 0 && !NUMBERS_ONLY.test(value)) {
|
||||
return
|
||||
}
|
||||
|
||||
setDeadlineInput(value)
|
||||
setDeadlineError(false)
|
||||
|
||||
// If the input is empty, set the deadline to the default
|
||||
if (value.length === 0) {
|
||||
setDeadline(DEFAULT_DEADLINE_FROM_NOW)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse user input and set the deadline if valid, error otherwise
|
||||
try {
|
||||
const parsed: number = Number.parseInt(value) * 60
|
||||
if (parsed === 0 || parsed > THREE_DAYS_IN_SECONDS) {
|
||||
setDeadlineError(DeadlineError.InvalidInput)
|
||||
} else {
|
||||
setDeadline(parsed)
|
||||
}
|
||||
} catch (error) {
|
||||
setDeadlineError(DeadlineError.InvalidInput)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Expand
|
||||
testId="transaction-deadline-settings"
|
||||
header={
|
||||
<Row width="auto">
|
||||
<ThemedText.BodySecondary>
|
||||
<Trans>Transaction deadline</Trans>
|
||||
</ThemedText.BodySecondary>
|
||||
<QuestionHelper
|
||||
text={<Trans>Your transaction will revert if it is pending for more than this period of time.</Trans>}
|
||||
/>
|
||||
</Row>
|
||||
}
|
||||
button={<Trans>{deadline / 60}m</Trans>}
|
||||
>
|
||||
<Row>
|
||||
<InputContainer gap="md" error={!!deadlineError}>
|
||||
<Input
|
||||
data-testid="deadline-input"
|
||||
placeholder={(DEFAULT_DEADLINE_FROM_NOW / 60).toString()}
|
||||
value={deadlineInput}
|
||||
onChange={(e) => parseCustomDeadline(e.target.value)}
|
||||
onBlur={() => {
|
||||
// When the input field is blurred, reset the input field to the current deadline
|
||||
setDeadlineInput(defaultInputValue)
|
||||
setDeadlineError(false)
|
||||
}}
|
||||
/>
|
||||
<ThemedText.BodyPrimary>
|
||||
<Trans>minutes</Trans>
|
||||
</ThemedText.BodyPrimary>
|
||||
</InputContainer>
|
||||
</Row>
|
||||
</Expand>
|
||||
)
|
||||
}
|
||||
@@ -1,43 +1,24 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { t } from '@lingui/macro'
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { L2_CHAIN_IDS } from 'constants/chains'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
import { isSupportedChainId } from 'lib/hooks/routing/clientSideSmartOrderRouter'
|
||||
import { useRef, useState } from 'react'
|
||||
import { Settings, X } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
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 { useOnClickOutside } from '../../hooks/useOnClickOutside'
|
||||
import { useModalIsOpen, useToggleSettingsMenu } from '../../state/application/hooks'
|
||||
import { ApplicationModal } from '../../state/application/reducer'
|
||||
import { useClientSideRouter, useExpertModeManager } from '../../state/user/hooks'
|
||||
import { ThemedText } from '../../theme'
|
||||
import { ButtonError } from '../Button'
|
||||
import { AutoColumn } from '../Column'
|
||||
import Modal from '../Modal'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import Toggle from '../Toggle'
|
||||
import TransactionSettings from '../TransactionSettings'
|
||||
import MaxSlippageSettings from './MaxSlippageSettings'
|
||||
import RouterPreferenceSettings from './RouterPreferenceSettings'
|
||||
import TransactionDeadlineSettings from './TransactionDeadlineSettings'
|
||||
|
||||
const StyledMenuIcon = styled(Settings)`
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
|
||||
> * {
|
||||
stroke: ${({ theme }) => theme.textSecondary};
|
||||
}
|
||||
`
|
||||
|
||||
const StyledCloseIcon = styled(X)`
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
> * {
|
||||
stroke: ${({ theme }) => theme.textSecondary};
|
||||
}
|
||||
@@ -53,7 +34,6 @@ const StyledMenuButton = styled.button<{ disabled: boolean }>`
|
||||
padding: 0;
|
||||
border-radius: 0.5rem;
|
||||
height: 20px;
|
||||
|
||||
${({ disabled }) =>
|
||||
!disabled &&
|
||||
`
|
||||
@@ -65,12 +45,6 @@ const StyledMenuButton = styled.button<{ disabled: boolean }>`
|
||||
}
|
||||
`}
|
||||
`
|
||||
const EmojiWrapper = styled.div`
|
||||
position: absolute;
|
||||
bottom: -6px;
|
||||
right: 0px;
|
||||
font-size: 14px;
|
||||
`
|
||||
|
||||
const StyledMenu = styled.div`
|
||||
margin-left: 0.5rem;
|
||||
@@ -97,89 +71,32 @@ const MenuFlyout = styled.span`
|
||||
right: 0rem;
|
||||
z-index: 100;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
|
||||
min-width: 18.125rem;
|
||||
`};
|
||||
|
||||
user-select: none;
|
||||
`
|
||||
|
||||
const Break = styled.div`
|
||||
const Divider = styled.div`
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: ${({ theme }) => theme.deprecated_bg3};
|
||||
border-width: 0;
|
||||
margin: 0;
|
||||
background-color: ${({ theme }) => theme.backgroundOutline};
|
||||
`
|
||||
|
||||
const ModalContentWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem 0;
|
||||
background-color: ${({ theme }) => theme.backgroundInteractive};
|
||||
border-radius: 20px;
|
||||
`
|
||||
|
||||
export default function SettingsTab({ placeholderSlippage }: { placeholderSlippage: Percent }) {
|
||||
export default function SettingsTab({ autoSlippage }: { autoSlippage: Percent }) {
|
||||
const { chainId } = useWeb3React()
|
||||
const showDeadlineSettings = Boolean(chainId && !L2_CHAIN_IDS.includes(chainId))
|
||||
|
||||
const node = useRef<HTMLDivElement | null>(null)
|
||||
const open = useModalIsOpen(ApplicationModal.SETTINGS)
|
||||
|
||||
const toggle = useToggleSettingsMenu()
|
||||
|
||||
const theme = useTheme()
|
||||
|
||||
const [expertMode, toggleExpertMode] = useExpertModeManager()
|
||||
|
||||
const [clientSideRouter, setClientSideRouter] = useClientSideRouter()
|
||||
|
||||
// show confirmation view before turning on
|
||||
const [showConfirmation, setShowConfirmation] = useState(false)
|
||||
|
||||
useOnClickOutside(node, open ? toggle : undefined)
|
||||
|
||||
return (
|
||||
<StyledMenu ref={node}>
|
||||
<Modal isOpen={showConfirmation} onDismiss={() => setShowConfirmation(false)} maxHeight={100}>
|
||||
<ModalContentWrapper>
|
||||
<AutoColumn gap="lg">
|
||||
<RowBetween style={{ padding: '0 2rem' }}>
|
||||
<div />
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
<Trans>Are you sure?</Trans>
|
||||
</Text>
|
||||
<StyledCloseIcon onClick={() => setShowConfirmation(false)} />
|
||||
</RowBetween>
|
||||
<Break />
|
||||
<AutoColumn gap="lg" style={{ padding: '0 2rem' }}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
<Trans>
|
||||
Expert mode turns off the confirm transaction prompt and allows high slippage trades that often result
|
||||
in bad rates and lost funds.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text fontWeight={600} fontSize={20}>
|
||||
<Trans>ONLY USE THIS MODE IF YOU KNOW WHAT YOU ARE DOING.</Trans>
|
||||
</Text>
|
||||
<ButtonError
|
||||
error={true}
|
||||
padding="12px"
|
||||
onClick={() => {
|
||||
const confirmWord = t`confirm`
|
||||
if (window.prompt(t`Please type the word "${confirmWord}" to enable expert mode.`) === confirmWord) {
|
||||
toggleExpertMode()
|
||||
setShowConfirmation(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Text fontSize={20} fontWeight={500} id="confirm-expert-mode">
|
||||
<Trans>Turn On Expert Mode</Trans>
|
||||
</Text>
|
||||
</ButtonError>
|
||||
</AutoColumn>
|
||||
</AutoColumn>
|
||||
</ModalContentWrapper>
|
||||
</Modal>
|
||||
<StyledMenuButton
|
||||
disabled={!isSupportedChainId(chainId)}
|
||||
onClick={toggle}
|
||||
@@ -188,72 +105,19 @@ export default function SettingsTab({ placeholderSlippage }: { placeholderSlippa
|
||||
aria-label={t`Transaction Settings`}
|
||||
>
|
||||
<StyledMenuIcon data-testid="swap-settings-button" />
|
||||
{expertMode && (
|
||||
<EmojiWrapper>
|
||||
<span role="img" aria-label="wizard-icon">
|
||||
🧙
|
||||
</span>
|
||||
</EmojiWrapper>
|
||||
)}
|
||||
</StyledMenuButton>
|
||||
{open && (
|
||||
<MenuFlyout>
|
||||
<AutoColumn gap="md" style={{ padding: '1rem' }}>
|
||||
<Text fontWeight={600} fontSize={14}>
|
||||
<Trans>Settings</Trans>
|
||||
</Text>
|
||||
<TransactionSettings placeholderSlippage={placeholderSlippage} />
|
||||
<Text fontWeight={600} fontSize={14}>
|
||||
<Trans>Interface Settings</Trans>
|
||||
</Text>
|
||||
{isSupportedChainId(chainId) && (
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<ThemedText.DeprecatedBlack fontWeight={400} fontSize={14} color={theme.textSecondary}>
|
||||
<Trans>Auto Router API</Trans>
|
||||
</ThemedText.DeprecatedBlack>
|
||||
<QuestionHelper text={<Trans>Use the Uniswap Labs API to get faster quotes.</Trans>} />
|
||||
</RowFixed>
|
||||
<Toggle
|
||||
id="toggle-optimized-router-button"
|
||||
isActive={!clientSideRouter}
|
||||
toggle={() => {
|
||||
sendEvent({
|
||||
category: 'Routing',
|
||||
action: clientSideRouter ? 'enable routing API' : 'disable routing API',
|
||||
})
|
||||
setClientSideRouter(!clientSideRouter)
|
||||
}}
|
||||
/>
|
||||
</RowBetween>
|
||||
<AutoColumn gap="16px" style={{ padding: '1rem' }}>
|
||||
{isSupportedChainId(chainId) && <RouterPreferenceSettings />}
|
||||
<Divider />
|
||||
<MaxSlippageSettings autoSlippage={autoSlippage} />
|
||||
{showDeadlineSettings && (
|
||||
<>
|
||||
<Divider />
|
||||
<TransactionDeadlineSettings />
|
||||
</>
|
||||
)}
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<ThemedText.DeprecatedBlack fontWeight={400} fontSize={14} color={theme.textSecondary}>
|
||||
<Trans>Expert Mode</Trans>
|
||||
</ThemedText.DeprecatedBlack>
|
||||
<QuestionHelper
|
||||
text={
|
||||
<Trans>Allow high price impact trades and skip the confirm screen. Use at your own risk.</Trans>
|
||||
}
|
||||
/>
|
||||
</RowFixed>
|
||||
<Toggle
|
||||
id="toggle-expert-mode-button"
|
||||
isActive={expertMode}
|
||||
toggle={
|
||||
expertMode
|
||||
? () => {
|
||||
toggleExpertMode()
|
||||
setShowConfirmation(false)
|
||||
}
|
||||
: () => {
|
||||
toggle()
|
||||
setShowConfirmation(true)
|
||||
}
|
||||
}
|
||||
/>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</MenuFlyout>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { WidgetSkeleton } from 'components/Widget'
|
||||
import { DEFAULT_WIDGET_WIDTH } from 'components/Widget'
|
||||
import { SwapSkeleton } from 'components/swap/SwapSkeleton'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
@@ -11,6 +10,8 @@ import { BreadcrumbNavLink } from './BreadcrumbNavLink'
|
||||
import { TokenPrice } from './PriceChart'
|
||||
import { StatPair, StatsWrapper, StatWrapper } from './StatsSection'
|
||||
|
||||
const SWAP_COMPONENT_WIDTH = 360
|
||||
|
||||
export const Hr = styled.hr`
|
||||
background-color: ${({ theme }) => theme.backgroundOutline};
|
||||
border: none;
|
||||
@@ -43,7 +44,7 @@ export const RightPanel = styled.div`
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
width: ${DEFAULT_WIDGET_WIDTH}px;
|
||||
width: ${SWAP_COMPONENT_WIDTH}px;
|
||||
|
||||
@media screen and (min-width: ${({ theme }) => theme.breakpoint.lg}px) {
|
||||
display: flex;
|
||||
@@ -260,7 +261,7 @@ export function TokenDetailsPageSkeleton() {
|
||||
<TokenDetailsLayout>
|
||||
<TokenDetailsSkeleton />
|
||||
<RightPanel>
|
||||
<WidgetSkeleton />
|
||||
<SwapSkeleton />
|
||||
</RightPanel>
|
||||
</TokenDetailsLayout>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Trace } from '@uniswap/analytics'
|
||||
import { InterfacePageName } from '@uniswap/analytics-events'
|
||||
import { Currency } from '@uniswap/widgets'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import CurrencyLogo from 'components/Logo/CurrencyLogo'
|
||||
import { AboutSection } from 'components/Tokens/TokenDetails/About'
|
||||
@@ -22,19 +21,14 @@ import TokenDetailsSkeleton, {
|
||||
import StatsSection from 'components/Tokens/TokenDetails/StatsSection'
|
||||
import TokenSafetyMessage from 'components/TokenSafety/TokenSafetyMessage'
|
||||
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
|
||||
import Widget from 'components/Widget'
|
||||
import { SwapTokens } from 'components/Widget/inputs'
|
||||
import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
|
||||
import { checkWarning } from 'constants/tokenSafety'
|
||||
import { useWidgetRemovalEnabled } from 'featureFlags/flags/removeWidgetTdp'
|
||||
import { TokenPriceQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { Chain, TokenQuery, TokenQueryData } from 'graphql/data/Token'
|
||||
import { QueryToken } from 'graphql/data/Token'
|
||||
import { CHAIN_NAME_TO_CHAIN_ID, getTokenDetailsURL } from 'graphql/data/util'
|
||||
import { useIsUserAddedTokenOnChain } from 'hooks/Tokens'
|
||||
import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch'
|
||||
import { UNKNOWN_TOKEN_SYMBOL, useTokenFromActiveNetwork } from 'lib/hooks/useCurrency'
|
||||
import { getTokenAddress } from 'lib/utils/analytics'
|
||||
import { Swap } from 'pages/Swap'
|
||||
import { useCallback, useMemo, useState, useTransition } from 'react'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
@@ -130,12 +124,10 @@ export default function TokenDetails({
|
||||
)
|
||||
|
||||
const { token: detailedToken, didFetchFromChain } = useRelevantToken(address, pageChainId, tokenQueryData)
|
||||
const { token: widgetInputToken } = useRelevantToken(inputTokenAddress, pageChainId, undefined)
|
||||
|
||||
const tokenWarning = address ? checkWarning(address) : null
|
||||
const isBlockedToken = tokenWarning?.canProceed === false
|
||||
const navigate = useNavigate()
|
||||
const widgetRemovalEnabled = useWidgetRemovalEnabled()
|
||||
|
||||
// Wrapping navigate in a transition prevents Suspense from unnecessarily showing fallbacks again.
|
||||
const [isPending, startTokenTransition] = useTransition()
|
||||
@@ -152,22 +144,6 @@ export default function TokenDetails({
|
||||
[address, crossChainMap, didFetchFromChain, navigate, detailedToken?.isNative]
|
||||
)
|
||||
useOnGlobalChainSwitch(navigateToTokenForChain)
|
||||
const navigateToWidgetSelectedToken = useCallback(
|
||||
(tokens: SwapTokens) => {
|
||||
const newDefaultToken = tokens[Field.OUTPUT] ?? tokens.default
|
||||
const address = newDefaultToken?.isNative ? NATIVE_CHAIN_ID : newDefaultToken?.address
|
||||
startTokenTransition(() =>
|
||||
navigate(
|
||||
getTokenDetailsURL({
|
||||
address,
|
||||
chain,
|
||||
inputAddress: tokens[Field.INPUT] ? getTokenAddress(tokens[Field.INPUT] as Currency) : null,
|
||||
})
|
||||
)
|
||||
)
|
||||
},
|
||||
[chain, navigate]
|
||||
)
|
||||
|
||||
const handleCurrencyChange = useCallback(
|
||||
(tokens: Pick<SwapState, Field.INPUT | Field.OUTPUT>) => {
|
||||
@@ -202,12 +178,6 @@ export default function TokenDetails({
|
||||
|
||||
const [openTokenSafetyModal, setOpenTokenSafetyModal] = useState(false)
|
||||
|
||||
const shouldShowSpeedbump = !useIsUserAddedTokenOnChain(address, pageChainId) && tokenWarning !== null
|
||||
const onReviewSwapClick = useCallback(
|
||||
() => new Promise<boolean>((resolve) => (shouldShowSpeedbump ? setContinueSwap({ resolve }) : resolve(true))),
|
||||
[shouldShowSpeedbump]
|
||||
)
|
||||
|
||||
const onResolveSwap = useCallback(
|
||||
(value: boolean) => {
|
||||
continueSwap?.resolve(value)
|
||||
@@ -268,26 +238,15 @@ export default function TokenDetails({
|
||||
|
||||
<RightPanel onClick={() => isBlockedToken && setOpenTokenSafetyModal(true)}>
|
||||
<div style={{ pointerEvents: isBlockedToken ? 'none' : 'auto' }}>
|
||||
{widgetRemovalEnabled ? (
|
||||
<Swap
|
||||
chainId={pageChainId}
|
||||
prefilledState={{
|
||||
[Field.INPUT]: { currencyId: inputTokenAddress },
|
||||
[Field.OUTPUT]: { currencyId: address === NATIVE_CHAIN_ID ? 'ETH' : address },
|
||||
}}
|
||||
onCurrencyChange={handleCurrencyChange}
|
||||
disableTokenInputs={pageChainId !== connectedChainId}
|
||||
/>
|
||||
) : (
|
||||
<Widget
|
||||
defaultTokens={{
|
||||
[Field.INPUT]: widgetInputToken ?? undefined,
|
||||
default: detailedToken ?? undefined,
|
||||
}}
|
||||
onDefaultTokenChange={navigateToWidgetSelectedToken}
|
||||
onReviewSwapClick={onReviewSwapClick}
|
||||
/>
|
||||
)}
|
||||
<Swap
|
||||
chainId={pageChainId}
|
||||
prefilledState={{
|
||||
[Field.INPUT]: { currencyId: inputTokenAddress },
|
||||
[Field.OUTPUT]: { currencyId: address === NATIVE_CHAIN_ID ? 'ETH' : address },
|
||||
}}
|
||||
onCurrencyChange={handleCurrencyChange}
|
||||
disableTokenInputs={pageChainId !== connectedChainId}
|
||||
/>
|
||||
</div>
|
||||
{tokenWarning && <TokenSafetyMessage tokenAddress={address} warning={tokenWarning} />}
|
||||
{detailedToken && <BalanceSummary token={detailedToken} />}
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
import { transparentize } from 'polished'
|
||||
import { ReactNode, useEffect, useState } from 'react'
|
||||
import { PropsWithChildren, ReactNode, useEffect, useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
import noop from 'utils/noop'
|
||||
|
||||
import Popover, { PopoverProps } from '../Popover'
|
||||
|
||||
export const TooltipContainer = styled.div`
|
||||
max-width: 256px;
|
||||
export enum TooltipSize {
|
||||
Small = '256px',
|
||||
Large = '400px',
|
||||
}
|
||||
|
||||
const getPaddingForSize = (size: TooltipSize) => {
|
||||
switch (size) {
|
||||
case TooltipSize.Small:
|
||||
return '12px'
|
||||
case TooltipSize.Large:
|
||||
return '16px 20px'
|
||||
}
|
||||
}
|
||||
|
||||
const TooltipContainer = styled.div<{ size: TooltipSize }>`
|
||||
max-width: ${({ size }) => size};
|
||||
width: calc(100vw - 16px);
|
||||
cursor: default;
|
||||
padding: 0.6rem 1rem;
|
||||
padding: ${({ size }) => getPaddingForSize(size)};
|
||||
pointer-events: auto;
|
||||
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
@@ -23,30 +38,23 @@ export const TooltipContainer = styled.div`
|
||||
box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.9, theme.shadow1)};
|
||||
`
|
||||
|
||||
interface TooltipProps extends Omit<PopoverProps, 'content'> {
|
||||
type TooltipProps = Omit<PopoverProps, 'content'> & {
|
||||
text: ReactNode
|
||||
open?: () => void
|
||||
close?: () => void
|
||||
disableHover?: boolean // disable the hover and content display
|
||||
size?: TooltipSize
|
||||
disabled?: boolean
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
interface TooltipContentProps extends Omit<PopoverProps, 'content'> {
|
||||
content: ReactNode
|
||||
onOpen?: () => void
|
||||
open?: () => void
|
||||
close?: () => void
|
||||
// whether to wrap the content in a `TooltipContainer`
|
||||
wrap?: boolean
|
||||
disableHover?: boolean // disable the hover and content display
|
||||
}
|
||||
|
||||
export default function Tooltip({ text, open, close, disableHover, ...rest }: TooltipProps) {
|
||||
// TODO(WEB-3305)
|
||||
// Migrate to MouseoverTooltip and move this component inline to MouseoverTooltip
|
||||
export default function Tooltip({ text, open, close, disabled, size = TooltipSize.Small, ...rest }: TooltipProps) {
|
||||
return (
|
||||
<Popover
|
||||
content={
|
||||
text && (
|
||||
<TooltipContainer onMouseEnter={disableHover ? noop : open} onMouseLeave={disableHover ? noop : close}>
|
||||
<TooltipContainer size={size} onMouseEnter={disabled ? noop : open} onMouseLeave={disabled ? noop : close}>
|
||||
{text}
|
||||
</TooltipContainer>
|
||||
)
|
||||
@@ -56,27 +64,24 @@ export default function Tooltip({ text, open, close, disableHover, ...rest }: To
|
||||
)
|
||||
}
|
||||
|
||||
function TooltipContent({ content, wrap = false, open, close, disableHover, ...rest }: TooltipContentProps) {
|
||||
return (
|
||||
<Popover
|
||||
content={
|
||||
wrap ? (
|
||||
<TooltipContainer onMouseEnter={disableHover ? noop : open} onMouseLeave={disableHover ? noop : close}>
|
||||
{content}
|
||||
</TooltipContainer>
|
||||
) : (
|
||||
content
|
||||
)
|
||||
}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
// TODO(WEB-3305)
|
||||
// Do not pass through PopoverProps. Prefer higher-level interface to control MouseoverTooltip.
|
||||
type MouseoverTooltipProps = Omit<PopoverProps, 'content' | 'show'> &
|
||||
PropsWithChildren<{
|
||||
text: ReactNode
|
||||
size?: TooltipSize
|
||||
disabled?: boolean
|
||||
timeout?: number
|
||||
placement?: PopoverProps['placement']
|
||||
onOpen?: () => void
|
||||
}>
|
||||
|
||||
/** Standard text tooltip. */
|
||||
export function MouseoverTooltip({ text, disableHover, children, timeout, ...rest }: Omit<TooltipProps, 'show'>) {
|
||||
export function MouseoverTooltip({ text, disabled, children, onOpen, timeout, ...rest }: MouseoverTooltipProps) {
|
||||
const [show, setShow] = useState(false)
|
||||
const open = () => text && setShow(true)
|
||||
const open = () => {
|
||||
setShow(true)
|
||||
onOpen?.()
|
||||
}
|
||||
const close = () => setShow(false)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -93,49 +98,10 @@ export function MouseoverTooltip({ text, disableHover, children, timeout, ...res
|
||||
}, [timeout, show])
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
{...rest}
|
||||
open={open}
|
||||
close={close}
|
||||
disableHover={disableHover}
|
||||
show={show}
|
||||
text={disableHover ? null : text}
|
||||
>
|
||||
<div onMouseEnter={disableHover ? noop : open} onMouseLeave={disableHover || timeout ? noop : close}>
|
||||
<Tooltip {...rest} open={open} close={close} disabled={disabled} show={show} text={disabled ? null : text}>
|
||||
<div onMouseEnter={disabled ? noop : open} onMouseLeave={disabled || timeout ? noop : close}>
|
||||
{children}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
/** Tooltip that displays custom content. */
|
||||
export function MouseoverTooltipContent({
|
||||
content,
|
||||
children,
|
||||
onOpen: openCallback = undefined,
|
||||
disableHover,
|
||||
...rest
|
||||
}: Omit<TooltipContentProps, 'show'>) {
|
||||
const [show, setShow] = useState(false)
|
||||
const open = () => {
|
||||
setShow(true)
|
||||
openCallback?.()
|
||||
}
|
||||
const close = () => {
|
||||
setShow(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipContent
|
||||
{...rest}
|
||||
open={open}
|
||||
close={close}
|
||||
show={!disableHover && show}
|
||||
content={disableHover ? null : content}
|
||||
>
|
||||
<div onMouseEnter={open} onMouseLeave={close}>
|
||||
{children}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { L2_CHAIN_IDS } from 'constants/chains'
|
||||
import { DEFAULT_DEADLINE_FROM_NOW } from 'constants/misc'
|
||||
import ms from 'ms.macro'
|
||||
import { darken } from 'polished'
|
||||
import { useState } from 'react'
|
||||
import { useUserSlippageTolerance, useUserTransactionTTL } from 'state/user/hooks'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
import { ThemedText } from '../../theme'
|
||||
import { AutoColumn } from '../Column'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
|
||||
enum SlippageError {
|
||||
InvalidInput = 'InvalidInput',
|
||||
}
|
||||
|
||||
enum DeadlineError {
|
||||
InvalidInput = 'InvalidInput',
|
||||
}
|
||||
|
||||
const FancyButton = styled.button`
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
align-items: center;
|
||||
height: 2rem;
|
||||
border-radius: 36px;
|
||||
font-size: 1rem;
|
||||
width: auto;
|
||||
min-width: 3.5rem;
|
||||
border: 1px solid ${({ theme }) => theme.deprecated_bg3};
|
||||
outline: none;
|
||||
background: ${({ theme }) => theme.deprecated_bg1};
|
||||
:hover {
|
||||
border: 1px solid ${({ theme }) => theme.deprecated_bg4};
|
||||
}
|
||||
:focus {
|
||||
border: 1px solid ${({ theme }) => theme.accentAction};
|
||||
}
|
||||
`
|
||||
|
||||
const Option = styled(FancyButton)<{ active: boolean }>`
|
||||
margin-right: 8px;
|
||||
border-radius: 12px;
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
background-color: ${({ active, theme }) => active && theme.accentAction};
|
||||
color: ${({ active, theme }) => (active ? theme.white : theme.textPrimary)};
|
||||
`
|
||||
|
||||
const Input = styled.input`
|
||||
background: ${({ theme }) => theme.deprecated_bg1};
|
||||
font-size: 16px;
|
||||
border-radius: 12px;
|
||||
width: auto;
|
||||
outline: none;
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
color: ${({ theme, color }) => (color === 'red' ? theme.accentFailure : theme.textPrimary)};
|
||||
text-align: right;
|
||||
|
||||
::placeholder {
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
}
|
||||
`
|
||||
|
||||
const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean }>`
|
||||
height: 2rem;
|
||||
position: relative;
|
||||
padding: 0 0.75rem;
|
||||
border-radius: 12px;
|
||||
flex: 1;
|
||||
border: ${({ theme, active, warning }) =>
|
||||
active
|
||||
? `1px solid ${warning ? theme.accentFailure : theme.accentAction}`
|
||||
: warning && `1px solid ${theme.accentFailure}`};
|
||||
:hover {
|
||||
border: ${({ theme, active, warning }) =>
|
||||
active && `1px solid ${warning ? darken(0.1, theme.accentFailure) : darken(0.1, theme.accentAction)}`};
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0px;
|
||||
border-radius: 2rem;
|
||||
}
|
||||
`
|
||||
|
||||
const SlippageEmojiContainer = styled.span`
|
||||
color: #f3841e;
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
display: none;
|
||||
`}
|
||||
`
|
||||
|
||||
interface TransactionSettingsProps {
|
||||
placeholderSlippage: Percent // varies according to the context in which the settings dialog is placed
|
||||
}
|
||||
|
||||
const THREE_DAYS_IN_SECONDS = ms`3 days` / 1000
|
||||
|
||||
export default function TransactionSettings({ placeholderSlippage }: TransactionSettingsProps) {
|
||||
const { chainId } = useWeb3React()
|
||||
const theme = useTheme()
|
||||
|
||||
const [userSlippageTolerance, setUserSlippageTolerance] = useUserSlippageTolerance()
|
||||
|
||||
const [deadline, setDeadline] = useUserTransactionTTL()
|
||||
|
||||
const [slippageInput, setSlippageInput] = useState('')
|
||||
const [slippageError, setSlippageError] = useState<SlippageError | false>(false)
|
||||
|
||||
const [deadlineInput, setDeadlineInput] = useState('')
|
||||
const [deadlineError, setDeadlineError] = useState<DeadlineError | false>(false)
|
||||
|
||||
function parseSlippageInput(value: string) {
|
||||
// populate what the user typed and clear the error
|
||||
setSlippageInput(value)
|
||||
setSlippageError(false)
|
||||
|
||||
if (value.length === 0) {
|
||||
setUserSlippageTolerance('auto')
|
||||
} else {
|
||||
const parsed = Math.floor(Number.parseFloat(value) * 100)
|
||||
|
||||
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 5000) {
|
||||
setUserSlippageTolerance('auto')
|
||||
if (value !== '.') {
|
||||
setSlippageError(SlippageError.InvalidInput)
|
||||
}
|
||||
} else {
|
||||
setUserSlippageTolerance(new Percent(parsed, 10_000))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const tooLow = userSlippageTolerance !== 'auto' && userSlippageTolerance.lessThan(new Percent(5, 10_000))
|
||||
const tooHigh = userSlippageTolerance !== 'auto' && userSlippageTolerance.greaterThan(new Percent(1, 100))
|
||||
|
||||
function parseCustomDeadline(value: string) {
|
||||
// populate what the user typed and clear the error
|
||||
setDeadlineInput(value)
|
||||
setDeadlineError(false)
|
||||
|
||||
if (value.length === 0) {
|
||||
setDeadline(DEFAULT_DEADLINE_FROM_NOW)
|
||||
} else {
|
||||
try {
|
||||
const parsed: number = Math.floor(Number.parseFloat(value) * 60)
|
||||
if (!Number.isInteger(parsed) || parsed < 60 || parsed > THREE_DAYS_IN_SECONDS) {
|
||||
setDeadlineError(DeadlineError.InvalidInput)
|
||||
} else {
|
||||
setDeadline(parsed)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
setDeadlineError(DeadlineError.InvalidInput)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const showCustomDeadlineRow = Boolean(chainId && !L2_CHAIN_IDS.includes(chainId))
|
||||
|
||||
return (
|
||||
<AutoColumn gap="md">
|
||||
<AutoColumn gap="sm">
|
||||
<RowFixed>
|
||||
<ThemedText.DeprecatedBlack fontWeight={400} fontSize={14} color={theme.textSecondary}>
|
||||
<Trans>Slippage tolerance</Trans>
|
||||
</ThemedText.DeprecatedBlack>
|
||||
<QuestionHelper
|
||||
text={
|
||||
<Trans>Your transaction will revert if the price changes unfavorably by more than this percentage.</Trans>
|
||||
}
|
||||
/>
|
||||
</RowFixed>
|
||||
<RowBetween>
|
||||
<Option
|
||||
onClick={() => {
|
||||
parseSlippageInput('')
|
||||
}}
|
||||
active={userSlippageTolerance === 'auto'}
|
||||
>
|
||||
<Trans>Auto</Trans>
|
||||
</Option>
|
||||
<OptionCustom active={userSlippageTolerance !== 'auto'} warning={!!slippageError} tabIndex={-1}>
|
||||
<RowBetween>
|
||||
{tooLow || tooHigh ? (
|
||||
<SlippageEmojiContainer>
|
||||
<span role="img" aria-label="warning">
|
||||
⚠️
|
||||
</span>
|
||||
</SlippageEmojiContainer>
|
||||
) : null}
|
||||
<Input
|
||||
data-testid="slippage-input"
|
||||
placeholder={placeholderSlippage.toFixed(2)}
|
||||
value={
|
||||
slippageInput.length > 0
|
||||
? slippageInput
|
||||
: userSlippageTolerance === 'auto'
|
||||
? ''
|
||||
: userSlippageTolerance.toFixed(2)
|
||||
}
|
||||
onChange={(e) => parseSlippageInput(e.target.value)}
|
||||
onBlur={() => {
|
||||
setSlippageInput('')
|
||||
setSlippageError(false)
|
||||
}}
|
||||
color={slippageError ? 'red' : ''}
|
||||
/>
|
||||
%
|
||||
</RowBetween>
|
||||
</OptionCustom>
|
||||
</RowBetween>
|
||||
{slippageError || tooLow || tooHigh ? (
|
||||
<RowBetween
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
paddingTop: '7px',
|
||||
color: slippageError ? 'red' : '#F3841E',
|
||||
}}
|
||||
>
|
||||
{slippageError ? (
|
||||
<Trans>Enter a valid slippage percentage</Trans>
|
||||
) : tooLow ? (
|
||||
<Trans>Your transaction may fail</Trans>
|
||||
) : (
|
||||
<Trans>Your transaction may be frontrun</Trans>
|
||||
)}
|
||||
</RowBetween>
|
||||
) : null}
|
||||
</AutoColumn>
|
||||
|
||||
{showCustomDeadlineRow && (
|
||||
<AutoColumn gap="sm">
|
||||
<RowFixed>
|
||||
<ThemedText.DeprecatedBlack fontSize={14} fontWeight={400} color={theme.textSecondary}>
|
||||
<Trans>Transaction deadline</Trans>
|
||||
</ThemedText.DeprecatedBlack>
|
||||
<QuestionHelper
|
||||
text={<Trans>Your transaction will revert if it is pending for more than this period of time.</Trans>}
|
||||
/>
|
||||
</RowFixed>
|
||||
<RowFixed>
|
||||
<OptionCustom style={{ width: '80px' }} warning={!!deadlineError} tabIndex={-1}>
|
||||
<Input
|
||||
data-testid="deadline-input"
|
||||
placeholder={(DEFAULT_DEADLINE_FROM_NOW / 60).toString()}
|
||||
value={
|
||||
deadlineInput.length > 0
|
||||
? deadlineInput
|
||||
: deadline === DEFAULT_DEADLINE_FROM_NOW
|
||||
? ''
|
||||
: (deadline / 60).toString()
|
||||
}
|
||||
onChange={(e) => parseCustomDeadline(e.target.value)}
|
||||
onBlur={() => {
|
||||
setDeadlineInput('')
|
||||
setDeadlineError(false)
|
||||
}}
|
||||
color={deadlineError ? 'red' : ''}
|
||||
/>
|
||||
</OptionCustom>
|
||||
<ThemedText.DeprecatedBody style={{ paddingLeft: '8px' }} fontSize={14}>
|
||||
<Trans>minutes</Trans>
|
||||
</ThemedText.DeprecatedBody>
|
||||
</RowFixed>
|
||||
</AutoColumn>
|
||||
)}
|
||||
</AutoColumn>
|
||||
)
|
||||
}
|
||||
@@ -86,7 +86,7 @@ function Updater() {
|
||||
}
|
||||
|
||||
function trace(event: any) {
|
||||
if (event.action !== 'request') return
|
||||
if (!event?.request) return
|
||||
const { method, id, params } = event.request
|
||||
console.groupCollapsed(method, id)
|
||||
console.debug(params)
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
|
||||
import {
|
||||
InterfaceEventName,
|
||||
InterfaceSectionName,
|
||||
SwapEventName,
|
||||
SwapPriceUpdateUserResponse,
|
||||
} from '@uniswap/analytics-events'
|
||||
import { Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, TradeType } from '@uniswap/sdk-core'
|
||||
import {
|
||||
AddEthereumChainParameter,
|
||||
DialogAnimationType,
|
||||
EMPTY_TOKEN_LIST,
|
||||
OnReviewSwapClick,
|
||||
SwapWidget,
|
||||
SwapWidgetSkeleton,
|
||||
} from '@uniswap/widgets'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useToggleAccountDrawer } from 'components/AccountDrawer'
|
||||
import { useActiveLocale } from 'hooks/useActiveLocale'
|
||||
import {
|
||||
formatPercentInBasisPointsNumber,
|
||||
formatSwapQuoteReceivedEventProperties,
|
||||
formatToDecimal,
|
||||
getDurationFromDateMilliseconds,
|
||||
getPriceUpdateBasisPoints,
|
||||
getTokenAddress,
|
||||
} from 'lib/utils/analytics'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useIsDarkMode } from 'theme/components/ThemeToggle'
|
||||
import { computeRealizedPriceImpact } from 'utils/prices'
|
||||
import { switchChain } from 'utils/switchChain'
|
||||
|
||||
import { DefaultTokens, SwapTokens, useSyncWidgetInputs } from './inputs'
|
||||
import { useSyncWidgetSettings } from './settings'
|
||||
import { DARK_THEME, LIGHT_THEME } from './theme'
|
||||
import { useSyncWidgetTransactions } from './transactions'
|
||||
|
||||
export const DEFAULT_WIDGET_WIDTH = 360
|
||||
|
||||
const WIDGET_ROUTER_URL = 'https://api.uniswap.org/v1/'
|
||||
|
||||
function useWidgetTheme() {
|
||||
return useIsDarkMode() ? DARK_THEME : LIGHT_THEME
|
||||
}
|
||||
|
||||
interface WidgetProps {
|
||||
defaultTokens: DefaultTokens
|
||||
width?: number | string
|
||||
onDefaultTokenChange?: (tokens: SwapTokens) => void
|
||||
onReviewSwapClick?: OnReviewSwapClick
|
||||
}
|
||||
|
||||
// TODO: Remove this component once the TDP is fully migrated to the swap component.
|
||||
// eslint-disable-next-line import/no-unused-modules
|
||||
export default function Widget({
|
||||
defaultTokens,
|
||||
width = DEFAULT_WIDGET_WIDTH,
|
||||
onDefaultTokenChange,
|
||||
onReviewSwapClick,
|
||||
}: WidgetProps) {
|
||||
const { connector, provider, chainId } = useWeb3React()
|
||||
const locale = useActiveLocale()
|
||||
const theme = useWidgetTheme()
|
||||
const { inputs, tokenSelector } = useSyncWidgetInputs({
|
||||
defaultTokens,
|
||||
onDefaultTokenChange,
|
||||
})
|
||||
const { settings } = useSyncWidgetSettings()
|
||||
const { transactions } = useSyncWidgetTransactions()
|
||||
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
const onConnectWalletClick = useCallback(() => {
|
||||
toggleWalletDrawer()
|
||||
return false // prevents the in-widget wallet modal from opening
|
||||
}, [toggleWalletDrawer])
|
||||
|
||||
const onSwitchChain = useCallback(
|
||||
// TODO(WEB-1757): Widget should not break if this rejects - upstream the catch to ignore it.
|
||||
({ chainId }: AddEthereumChainParameter) => switchChain(connector, Number(chainId)).catch(() => undefined),
|
||||
[connector]
|
||||
)
|
||||
|
||||
const trace = useTrace({ section: InterfaceSectionName.WIDGET })
|
||||
const [initialQuoteDate, setInitialQuoteDate] = useState<Date>()
|
||||
const onInitialSwapQuote = useCallback(
|
||||
(trade: Trade<Currency, Currency, TradeType>) => {
|
||||
setInitialQuoteDate(new Date())
|
||||
const eventProperties = {
|
||||
// TODO(1416): Include undefined values.
|
||||
...formatSwapQuoteReceivedEventProperties(
|
||||
trade,
|
||||
/* gasUseEstimateUSD= */ undefined,
|
||||
/* fetchingSwapQuoteStartTime= */ undefined
|
||||
),
|
||||
...trace,
|
||||
}
|
||||
sendAnalyticsEvent(SwapEventName.SWAP_QUOTE_RECEIVED, eventProperties)
|
||||
},
|
||||
[trace]
|
||||
)
|
||||
const onApproveToken = useCallback(() => {
|
||||
const input = inputs.value.INPUT
|
||||
if (!input) return
|
||||
const eventProperties = {
|
||||
chain_id: input.chainId,
|
||||
token_symbol: input.symbol,
|
||||
token_address: getTokenAddress(input),
|
||||
...trace,
|
||||
}
|
||||
sendAnalyticsEvent(InterfaceEventName.APPROVE_TOKEN_TXN_SUBMITTED, eventProperties)
|
||||
}, [inputs.value.INPUT, trace])
|
||||
const onExpandSwapDetails = useCallback(() => {
|
||||
sendAnalyticsEvent(SwapEventName.SWAP_DETAILS_EXPANDED, { ...trace })
|
||||
}, [trace])
|
||||
const onSwapPriceUpdateAck = useCallback(
|
||||
(stale: Trade<Currency, Currency, TradeType>, update: Trade<Currency, Currency, TradeType>) => {
|
||||
const eventProperties = {
|
||||
chain_id: update.inputAmount.currency.chainId,
|
||||
response: SwapPriceUpdateUserResponse.ACCEPTED,
|
||||
token_in_symbol: update.inputAmount.currency.symbol,
|
||||
token_out_symbol: update.outputAmount.currency.symbol,
|
||||
price_update_basis_points: getPriceUpdateBasisPoints(stale.executionPrice, update.executionPrice),
|
||||
...trace,
|
||||
}
|
||||
sendAnalyticsEvent(SwapEventName.SWAP_PRICE_UPDATE_ACKNOWLEDGED, eventProperties)
|
||||
},
|
||||
[trace]
|
||||
)
|
||||
const onSubmitSwapClick = useCallback(
|
||||
(trade: Trade<Currency, Currency, TradeType>) => {
|
||||
const eventProperties = {
|
||||
// TODO(1416): Include undefined values.
|
||||
estimated_network_fee_usd: undefined,
|
||||
transaction_deadline_seconds: undefined,
|
||||
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: undefined,
|
||||
token_out_amount_usd: undefined,
|
||||
price_impact_basis_points: formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)),
|
||||
allowed_slippage_basis_points: undefined,
|
||||
is_auto_router_api: undefined,
|
||||
is_auto_slippage: undefined,
|
||||
chain_id: trade.inputAmount.currency.chainId,
|
||||
duration_from_first_quote_to_swap_submission_milliseconds: getDurationFromDateMilliseconds(initialQuoteDate),
|
||||
swap_quote_block_number: undefined,
|
||||
...trace,
|
||||
}
|
||||
sendAnalyticsEvent(SwapEventName.SWAP_SUBMITTED_BUTTON_CLICKED, eventProperties)
|
||||
},
|
||||
[initialQuoteDate, trace]
|
||||
)
|
||||
|
||||
if (!(inputs.value.INPUT || inputs.value.OUTPUT)) {
|
||||
return <WidgetSkeleton />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ zIndex: 1, position: 'relative' }}>
|
||||
<SwapWidget
|
||||
hideConnectionUI
|
||||
brandedFooter={false}
|
||||
permit2
|
||||
routerUrl={WIDGET_ROUTER_URL}
|
||||
locale={locale}
|
||||
theme={theme}
|
||||
width={width}
|
||||
defaultChainId={chainId}
|
||||
onConnectWalletClick={onConnectWalletClick}
|
||||
provider={provider}
|
||||
onSwitchChain={onSwitchChain}
|
||||
tokenList={EMPTY_TOKEN_LIST} // prevents loading the default token list, as we use our own token selector UI
|
||||
{...inputs}
|
||||
{...settings}
|
||||
{...transactions}
|
||||
onExpandSwapDetails={onExpandSwapDetails}
|
||||
onReviewSwapClick={onReviewSwapClick}
|
||||
onSubmitSwapClick={onSubmitSwapClick}
|
||||
onSwapApprove={onApproveToken}
|
||||
onInitialSwapQuote={onInitialSwapQuote}
|
||||
onSwapPriceUpdateAck={onSwapPriceUpdateAck}
|
||||
dialogOptions={{
|
||||
pageCentered: true,
|
||||
animationType: DialogAnimationType.FADE,
|
||||
}}
|
||||
onError={(error, errorInfo) => {
|
||||
sendAnalyticsEvent(SwapEventName.SWAP_ERROR, { error, errorInfo, ...trace })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{tokenSelector}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function WidgetSkeleton({ width = DEFAULT_WIDGET_WIDTH }: { width?: number | string }) {
|
||||
const theme = useWidgetTheme()
|
||||
return <SwapWidgetSkeleton theme={theme} width={width} />
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
|
||||
import { InterfaceSectionName, SwapEventName } from '@uniswap/analytics-events'
|
||||
import { Currency, Field, SwapController, SwapEventHandlers, TradeType } from '@uniswap/widgets'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import CurrencySearchModal from 'components/SearchModal/CurrencySearchModal'
|
||||
import { isSupportedChain } from 'constants/chains'
|
||||
import usePrevious from 'hooks/usePrevious'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
const EMPTY_AMOUNT = ''
|
||||
|
||||
type SwapValue = Required<SwapController>['value']
|
||||
export type SwapTokens = Pick<SwapValue, Field.INPUT | Field.OUTPUT> & { default?: Currency }
|
||||
export type DefaultTokens = Partial<SwapTokens>
|
||||
|
||||
function missingDefaultToken(tokens: SwapTokens) {
|
||||
if (!tokens.default) return false
|
||||
return !tokens[Field.INPUT]?.equals(tokens.default) && !tokens[Field.OUTPUT]?.equals(tokens.default)
|
||||
}
|
||||
|
||||
function currenciesEqual(a: Currency | undefined, b: Currency | undefined) {
|
||||
if (a && b) {
|
||||
return a.equals(b)
|
||||
} else {
|
||||
return !a && !b
|
||||
}
|
||||
}
|
||||
|
||||
function tokensEqual(a: SwapTokens | undefined, b: SwapTokens | undefined) {
|
||||
if (!a || !b) {
|
||||
return !a && !b
|
||||
}
|
||||
return (
|
||||
currenciesEqual(a[Field.INPUT], b[Field.INPUT]) &&
|
||||
currenciesEqual(a[Field.OUTPUT], b[Field.OUTPUT]) &&
|
||||
currenciesEqual(a.default, b.default)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Integrates the Widget's inputs.
|
||||
* Treats the Widget as a controlled component, using the app's own token selector for selection.
|
||||
* Enforces that token is a part of the returned value.
|
||||
*/
|
||||
export function useSyncWidgetInputs({
|
||||
defaultTokens,
|
||||
onDefaultTokenChange,
|
||||
}: {
|
||||
defaultTokens: DefaultTokens
|
||||
onDefaultTokenChange?: (tokens: SwapTokens) => void
|
||||
}) {
|
||||
const trace = useTrace({ section: InterfaceSectionName.WIDGET })
|
||||
|
||||
const { chainId } = useWeb3React()
|
||||
const previousChainId = usePrevious(chainId)
|
||||
|
||||
const [type, setType] = useState<SwapValue['type']>(TradeType.EXACT_INPUT)
|
||||
const [amount, setAmount] = useState<SwapValue['amount']>(EMPTY_AMOUNT)
|
||||
const [tokens, setTokens] = useState<SwapTokens>({
|
||||
...defaultTokens,
|
||||
[Field.OUTPUT]: defaultTokens[Field.OUTPUT] ?? defaultTokens.default,
|
||||
})
|
||||
|
||||
// The most recent set of defaults, which can be used to check when the defaults are actually changing.
|
||||
const baseTokens = usePrevious(defaultTokens)
|
||||
useEffect(() => {
|
||||
if (!tokensEqual(baseTokens, defaultTokens)) {
|
||||
const input = defaultTokens[Field.INPUT]
|
||||
const output = defaultTokens[Field.OUTPUT] ?? defaultTokens.default
|
||||
setTokens({
|
||||
...defaultTokens,
|
||||
[Field.OUTPUT]: currenciesEqual(output, input) ? undefined : output,
|
||||
})
|
||||
}
|
||||
}, [baseTokens, defaultTokens])
|
||||
|
||||
/**
|
||||
* Clear the tokens if the chain changes.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (chainId !== previousChainId && !!previousChainId && isSupportedChain(chainId)) {
|
||||
setTokens({
|
||||
...defaultTokens,
|
||||
[Field.OUTPUT]: defaultTokens[Field.OUTPUT] ?? defaultTokens.default,
|
||||
})
|
||||
setAmount(EMPTY_AMOUNT)
|
||||
}
|
||||
}, [chainId, defaultTokens, previousChainId, tokens])
|
||||
|
||||
const onAmountChange = useCallback(
|
||||
(field: Field, amount: string, origin?: 'max') => {
|
||||
if (origin === 'max') {
|
||||
sendAnalyticsEvent(SwapEventName.SWAP_MAX_TOKEN_AMOUNT_SELECTED, { ...trace })
|
||||
}
|
||||
setType(field === Field.INPUT ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT)
|
||||
setAmount(amount)
|
||||
},
|
||||
[trace]
|
||||
)
|
||||
|
||||
const onSwitchTokens = useCallback(() => {
|
||||
sendAnalyticsEvent(SwapEventName.SWAP_TOKENS_REVERSED, { ...trace })
|
||||
setType((type) => invertTradeType(type))
|
||||
setTokens((tokens) => ({
|
||||
[Field.INPUT]: tokens[Field.OUTPUT],
|
||||
[Field.OUTPUT]: tokens[Field.INPUT],
|
||||
default: tokens.default,
|
||||
}))
|
||||
}, [trace])
|
||||
|
||||
const [selectingField, setSelectingField] = useState<Field>()
|
||||
const onTokenSelectorClick = useCallback((field: Field) => {
|
||||
setSelectingField(field)
|
||||
return false
|
||||
}, [])
|
||||
|
||||
const onTokenSelect = useCallback(
|
||||
(selectingToken: Currency) => {
|
||||
if (selectingField === undefined) return
|
||||
|
||||
const otherField = invertField(selectingField)
|
||||
const isFlip = tokens[otherField]?.equals(selectingToken)
|
||||
const update: SwapTokens = {
|
||||
[selectingField]: selectingToken,
|
||||
[otherField]: isFlip ? tokens[selectingField] : tokens[otherField],
|
||||
default: tokens.default,
|
||||
}
|
||||
|
||||
setType((type) => {
|
||||
// If flipping the tokens, also flip the type/amount.
|
||||
if (isFlip) {
|
||||
return invertTradeType(type)
|
||||
}
|
||||
|
||||
// Setting a new token should clear its amount, if it is set.
|
||||
const activeField = type === TradeType.EXACT_INPUT ? Field.INPUT : Field.OUTPUT
|
||||
if (selectingField === activeField) {
|
||||
setAmount(() => EMPTY_AMOUNT)
|
||||
}
|
||||
|
||||
return type
|
||||
})
|
||||
|
||||
if (missingDefaultToken(update)) {
|
||||
onDefaultTokenChange?.({
|
||||
...update,
|
||||
default: update[Field.OUTPUT] ?? selectingToken,
|
||||
})
|
||||
return
|
||||
}
|
||||
setTokens(update)
|
||||
},
|
||||
[onDefaultTokenChange, selectingField, tokens]
|
||||
)
|
||||
|
||||
const tokenSelector = (
|
||||
<CurrencySearchModal
|
||||
isOpen={selectingField !== undefined}
|
||||
onDismiss={() => setSelectingField(undefined)}
|
||||
selectedCurrency={selectingField && tokens[selectingField]}
|
||||
otherSelectedCurrency={selectingField && tokens[invertField(selectingField)]}
|
||||
onCurrencySelect={onTokenSelect}
|
||||
showCommonBases
|
||||
/>
|
||||
)
|
||||
|
||||
const value: SwapValue = useMemo(
|
||||
() => ({
|
||||
type,
|
||||
amount,
|
||||
// If the initial state has not yet been set, preemptively disable the widget by passing no tokens. Effectively,
|
||||
// this resets the widget - avoiding rendering stale state - because with no tokens the skeleton will be rendered.
|
||||
...(tokens[Field.INPUT] || tokens[Field.OUTPUT] ? tokens : undefined),
|
||||
}),
|
||||
[amount, tokens, type]
|
||||
)
|
||||
const valueHandlers: SwapEventHandlers = useMemo(
|
||||
() => ({ onAmountChange, onSwitchTokens, onTokenSelectorClick }),
|
||||
[onAmountChange, onSwitchTokens, onTokenSelectorClick]
|
||||
)
|
||||
return { inputs: { value, ...valueHandlers }, tokenSelector }
|
||||
}
|
||||
|
||||
// TODO(zzmp): Move to @uniswap/widgets.
|
||||
function invertField(field: Field) {
|
||||
switch (field) {
|
||||
case Field.INPUT:
|
||||
return Field.OUTPUT
|
||||
case Field.OUTPUT:
|
||||
return Field.INPUT
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(zzmp): Include in @uniswap/sdk-core (on TradeType, if possible).
|
||||
function invertTradeType(tradeType: TradeType) {
|
||||
switch (tradeType) {
|
||||
case TradeType.EXACT_INPUT:
|
||||
return TradeType.EXACT_OUTPUT
|
||||
case TradeType.EXACT_OUTPUT:
|
||||
return TradeType.EXACT_INPUT
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
import { RouterPreference, Slippage, SwapController, SwapEventHandlers } from '@uniswap/widgets'
|
||||
import { DEFAULT_DEADLINE_FROM_NOW } from 'constants/misc'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useUserSlippageTolerance, useUserTransactionTTL } from 'state/user/hooks'
|
||||
|
||||
/**
|
||||
* Integrates the Widget's settings, keeping the widget and app settings in sync.
|
||||
* NB: This acts as an integration layer, so certain values are duplicated in order to translate
|
||||
* between app and widget representations.
|
||||
*/
|
||||
export function useSyncWidgetSettings() {
|
||||
const [appTtl, setAppTtl] = useUserTransactionTTL()
|
||||
const [widgetTtl, setWidgetTtl] = useState<number | undefined>(appTtl / 60)
|
||||
const onTransactionDeadlineChange = useCallback(
|
||||
(widgetTtl: number | undefined) => {
|
||||
setWidgetTtl(widgetTtl)
|
||||
const appTtl = widgetTtl === undefined ? widgetTtl : widgetTtl * 60
|
||||
setAppTtl(appTtl ?? DEFAULT_DEADLINE_FROM_NOW)
|
||||
},
|
||||
[setAppTtl]
|
||||
)
|
||||
|
||||
const [appSlippage, setAppSlippage] = useUserSlippageTolerance()
|
||||
const [widgetSlippage, setWidgetSlippage] = useState<string | undefined>(
|
||||
appSlippage === 'auto' ? undefined : appSlippage.toFixed(2)
|
||||
)
|
||||
const onSlippageChange = useCallback(
|
||||
(widgetSlippage: Slippage) => {
|
||||
setWidgetSlippage(widgetSlippage.max)
|
||||
if (widgetSlippage.auto || !widgetSlippage.max) {
|
||||
setAppSlippage('auto')
|
||||
} else {
|
||||
setAppSlippage(new Percent(Math.floor(Number(widgetSlippage.max) * 100), 10_000))
|
||||
}
|
||||
},
|
||||
[setAppSlippage]
|
||||
)
|
||||
|
||||
const [routerPreference, onRouterPreferenceChange] = useState(RouterPreference.API)
|
||||
|
||||
const onSettingsReset = useCallback(() => {
|
||||
setWidgetTtl(undefined)
|
||||
setAppTtl(DEFAULT_DEADLINE_FROM_NOW)
|
||||
setWidgetSlippage(undefined)
|
||||
setAppSlippage('auto')
|
||||
}, [setAppSlippage, setAppTtl])
|
||||
|
||||
const settings: SwapController['settings'] = useMemo(() => {
|
||||
const auto = appSlippage === 'auto'
|
||||
return {
|
||||
slippage: { auto, max: widgetSlippage },
|
||||
transactionTtl: widgetTtl,
|
||||
routerPreference,
|
||||
}
|
||||
}, [appSlippage, widgetSlippage, widgetTtl, routerPreference])
|
||||
const settingsHandlers: SwapEventHandlers = useMemo(
|
||||
() => ({ onSettingsReset, onSlippageChange, onTransactionDeadlineChange, onRouterPreferenceChange }),
|
||||
[onSettingsReset, onSlippageChange, onTransactionDeadlineChange, onRouterPreferenceChange]
|
||||
)
|
||||
|
||||
return { settings: { settings, ...settingsHandlers } }
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { Theme } from '@uniswap/widgets'
|
||||
import { darkTheme, lightTheme } from 'theme/colors'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
|
||||
const zIndex = {
|
||||
modal: Z_INDEX.modal,
|
||||
}
|
||||
|
||||
const fonts = {
|
||||
fontFamily: 'Inter custom',
|
||||
}
|
||||
|
||||
export const LIGHT_THEME: Theme = {
|
||||
// surface
|
||||
accent: lightTheme.accentAction,
|
||||
accentSoft: lightTheme.accentActionSoft,
|
||||
container: lightTheme.backgroundSurface,
|
||||
module: lightTheme.backgroundModule,
|
||||
interactive: lightTheme.backgroundInteractive,
|
||||
outline: lightTheme.backgroundOutline,
|
||||
dialog: lightTheme.backgroundBackdrop,
|
||||
scrim: lightTheme.backgroundScrim,
|
||||
// text
|
||||
onAccent: lightTheme.white,
|
||||
primary: lightTheme.textPrimary,
|
||||
secondary: lightTheme.textSecondary,
|
||||
hint: lightTheme.textTertiary,
|
||||
onInteractive: lightTheme.accentTextDarkPrimary,
|
||||
// shadow
|
||||
deepShadow: lightTheme.deepShadow,
|
||||
networkDefaultShadow: lightTheme.networkDefaultShadow,
|
||||
|
||||
// state
|
||||
success: lightTheme.accentSuccess,
|
||||
warning: lightTheme.accentWarning,
|
||||
error: lightTheme.accentCritical,
|
||||
|
||||
...fonts,
|
||||
zIndex,
|
||||
}
|
||||
|
||||
export const DARK_THEME: Theme = {
|
||||
// surface
|
||||
accent: darkTheme.accentAction,
|
||||
accentSoft: darkTheme.accentActionSoft,
|
||||
container: darkTheme.backgroundSurface,
|
||||
module: darkTheme.backgroundModule,
|
||||
interactive: darkTheme.backgroundInteractive,
|
||||
outline: darkTheme.backgroundOutline,
|
||||
dialog: darkTheme.backgroundBackdrop,
|
||||
scrim: darkTheme.backgroundScrim,
|
||||
// text
|
||||
onAccent: darkTheme.white,
|
||||
primary: darkTheme.textPrimary,
|
||||
secondary: darkTheme.textSecondary,
|
||||
hint: darkTheme.textTertiary,
|
||||
onInteractive: darkTheme.accentTextLightPrimary,
|
||||
// shadow
|
||||
deepShadow: darkTheme.deepShadow,
|
||||
networkDefaultShadow: darkTheme.networkDefaultShadow,
|
||||
// state
|
||||
success: darkTheme.accentSuccess,
|
||||
warning: darkTheme.accentWarning,
|
||||
error: darkTheme.accentCritical,
|
||||
|
||||
...fonts,
|
||||
zIndex,
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
|
||||
import { InterfaceEventName, InterfaceSectionName, SwapEventName } from '@uniswap/analytics-events'
|
||||
import { Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, Percent } from '@uniswap/sdk-core'
|
||||
import {
|
||||
OnTxSuccess,
|
||||
TradeType,
|
||||
Transaction,
|
||||
TransactionEventHandlers,
|
||||
TransactionInfo,
|
||||
TransactionType,
|
||||
TransactionType as WidgetTransactionType,
|
||||
} from '@uniswap/widgets'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import {
|
||||
formatPercentInBasisPointsNumber,
|
||||
formatSwapSignedAnalyticsEventProperties,
|
||||
formatToDecimal,
|
||||
getTokenAddress,
|
||||
} from 'lib/utils/analytics'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTransactionAdder } from 'state/transactions/hooks'
|
||||
import {
|
||||
ExactInputSwapTransactionInfo,
|
||||
ExactOutputSwapTransactionInfo,
|
||||
TransactionType as AppTransactionType,
|
||||
WrapTransactionInfo,
|
||||
} from 'state/transactions/types'
|
||||
import { currencyId } from 'utils/currencyId'
|
||||
import { computeRealizedPriceImpact } from 'utils/prices'
|
||||
|
||||
interface AnalyticsEventProps {
|
||||
trade: Trade<Currency, Currency, TradeType>
|
||||
gasUsed: string | undefined
|
||||
blockNumber: number | undefined
|
||||
hash: string | undefined
|
||||
allowedSlippage: Percent
|
||||
succeeded: boolean
|
||||
}
|
||||
|
||||
const formatAnalyticsEventProperties = ({
|
||||
trade,
|
||||
hash,
|
||||
allowedSlippage,
|
||||
succeeded,
|
||||
gasUsed,
|
||||
blockNumber,
|
||||
}: AnalyticsEventProps) => ({
|
||||
estimated_network_fee_usd: gasUsed,
|
||||
transaction_hash: hash,
|
||||
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),
|
||||
price_impact_basis_points: formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)),
|
||||
allowed_slippage_basis_points: formatPercentInBasisPointsNumber(allowedSlippage),
|
||||
chain_id:
|
||||
trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId
|
||||
? trade.inputAmount.currency.chainId
|
||||
: undefined,
|
||||
swap_quote_block_number: blockNumber,
|
||||
succeeded,
|
||||
})
|
||||
|
||||
/** Integrates the Widget's transactions, showing the widget's transactions in the app. */
|
||||
export function useSyncWidgetTransactions() {
|
||||
const trace = useTrace({ section: InterfaceSectionName.WIDGET })
|
||||
|
||||
const { chainId } = useWeb3React()
|
||||
const addTransaction = useTransactionAdder()
|
||||
|
||||
const onTxSubmit = useCallback(
|
||||
(_hash: string, transaction: Transaction<TransactionInfo>) => {
|
||||
const { type, response } = transaction.info
|
||||
|
||||
if (!type || !response) {
|
||||
return
|
||||
} else if (type === WidgetTransactionType.WRAP || type === WidgetTransactionType.UNWRAP) {
|
||||
const { type, amount: transactionAmount } = transaction.info
|
||||
|
||||
const eventProperties = {
|
||||
// get this info from widget handlers
|
||||
token_in_address: getTokenAddress(transactionAmount.currency),
|
||||
token_out_address: getTokenAddress(transactionAmount.currency.wrapped),
|
||||
token_in_symbol: transactionAmount.currency.symbol,
|
||||
token_out_symbol: transactionAmount.currency.wrapped.symbol,
|
||||
chain_id: transactionAmount.currency.chainId,
|
||||
amount: transactionAmount
|
||||
? formatToDecimal(transactionAmount, transactionAmount?.currency.decimals)
|
||||
: undefined,
|
||||
type: type === WidgetTransactionType.WRAP ? TransactionType.WRAP : TransactionType.UNWRAP,
|
||||
...trace,
|
||||
}
|
||||
sendAnalyticsEvent(InterfaceEventName.WRAP_TOKEN_TXN_SUBMITTED, eventProperties)
|
||||
const { amount } = transaction.info
|
||||
addTransaction(response, {
|
||||
type: AppTransactionType.WRAP,
|
||||
unwrapped: type === WidgetTransactionType.UNWRAP,
|
||||
currencyAmountRaw: amount.quotient.toString(),
|
||||
chainId,
|
||||
} as WrapTransactionInfo)
|
||||
} else if (type === WidgetTransactionType.SWAP) {
|
||||
const { slippageTolerance, trade, tradeType } = transaction.info
|
||||
|
||||
const eventProperties = {
|
||||
...formatSwapSignedAnalyticsEventProperties({
|
||||
trade,
|
||||
// TODO: add once Widgets adds fiat values to callback
|
||||
fiatValues: { amountIn: undefined, amountOut: undefined },
|
||||
txHash: transaction.receipt?.transactionHash ?? '',
|
||||
}),
|
||||
...trace,
|
||||
}
|
||||
sendAnalyticsEvent(SwapEventName.SWAP_SIGNED, eventProperties)
|
||||
const baseTxInfo = {
|
||||
type: AppTransactionType.SWAP,
|
||||
tradeType,
|
||||
inputCurrencyId: currencyId(trade.inputAmount.currency),
|
||||
outputCurrencyId: currencyId(trade.outputAmount.currency),
|
||||
}
|
||||
if (tradeType === TradeType.EXACT_OUTPUT) {
|
||||
addTransaction(response, {
|
||||
...baseTxInfo,
|
||||
maximumInputCurrencyAmountRaw: trade.maximumAmountIn(slippageTolerance).quotient.toString(),
|
||||
outputCurrencyAmountRaw: trade.outputAmount.quotient.toString(),
|
||||
expectedInputCurrencyAmountRaw: trade.inputAmount.quotient.toString(),
|
||||
} as ExactOutputSwapTransactionInfo)
|
||||
} else {
|
||||
addTransaction(response, {
|
||||
...baseTxInfo,
|
||||
inputCurrencyAmountRaw: trade.inputAmount.quotient.toString(),
|
||||
expectedOutputCurrencyAmountRaw: trade.outputAmount.quotient.toString(),
|
||||
minimumOutputCurrencyAmountRaw: trade.minimumAmountOut(slippageTolerance).quotient.toString(),
|
||||
} as ExactInputSwapTransactionInfo)
|
||||
}
|
||||
}
|
||||
},
|
||||
[addTransaction, chainId, trace]
|
||||
)
|
||||
|
||||
const onTxSuccess: OnTxSuccess = useCallback((hash: string, tx) => {
|
||||
if (tx.info.type === TransactionType.SWAP) {
|
||||
const { trade, slippageTolerance } = tx.info
|
||||
sendAnalyticsEvent(
|
||||
SwapEventName.SWAP_TRANSACTION_COMPLETED,
|
||||
formatAnalyticsEventProperties({
|
||||
trade,
|
||||
hash,
|
||||
gasUsed: tx.receipt?.gasUsed?.toString(),
|
||||
blockNumber: tx.receipt?.blockNumber,
|
||||
allowedSlippage: slippageTolerance,
|
||||
succeeded: tx.receipt?.status === 1,
|
||||
})
|
||||
)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const txHandlers: TransactionEventHandlers = useMemo(() => ({ onTxSubmit, onTxSuccess }), [onTxSubmit, onTxSuccess])
|
||||
|
||||
return { transactions: { ...txHandlers } }
|
||||
}
|
||||
@@ -20,20 +20,18 @@ describe('AdvancedSwapDetails.tsx', () => {
|
||||
|
||||
it('renders correct copy on mouseover', async () => {
|
||||
render(<AdvancedSwapDetails trade={TEST_TRADE_EXACT_INPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} />)
|
||||
await act(() => userEvent.hover(screen.getByText('Price Impact')))
|
||||
expect(await screen.getByText(/The impact your trade has on the market price of this pool./i)).toBeVisible()
|
||||
await act(() => userEvent.hover(screen.getByText('Expected Output')))
|
||||
await act(() => userEvent.hover(screen.getByText('Expected output')))
|
||||
expect(await screen.getByText(/The amount you expect to receive at the current market price./i)).toBeVisible()
|
||||
await act(() => userEvent.hover(screen.getByText(/Minimum received/i)))
|
||||
await act(() => userEvent.hover(screen.getByText(/Minimum output/i)))
|
||||
expect(await screen.getByText(/The minimum amount you are guaranteed to receive./i)).toBeVisible()
|
||||
})
|
||||
|
||||
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)
|
||||
render(<AdvancedSwapDetails trade={TEST_TRADE_EXACT_OUTPUT} allowedSlippage={TEST_ALLOWED_SLIPPAGE} />)
|
||||
await act(() => userEvent.hover(screen.getByText(/Maximum sent/i)))
|
||||
await act(() => userEvent.hover(screen.getByText(/Minimum output/i)))
|
||||
expect(await screen.getByText(/The minimum amount you are guaranteed to receive./i)).toBeVisible()
|
||||
await act(() => userEvent.hover(screen.getByText('Network Fee')))
|
||||
await act(() => userEvent.hover(screen.getByText('Network fee')))
|
||||
expect(await screen.getByText(/The fee paid to miners who process your transaction./i)).toBeVisible()
|
||||
})
|
||||
|
||||
|
||||
@@ -1,30 +1,24 @@
|
||||
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 { useWeb3React } from '@web3-react/core'
|
||||
import Card from 'components/Card'
|
||||
import { LoadingRows } from 'components/Loader/styled'
|
||||
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
|
||||
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
import { useMemo } from 'react'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
|
||||
import { Separator, ThemedText } from '../../theme'
|
||||
import { computeRealizedPriceImpact } from '../../utils/prices'
|
||||
import { AutoColumn } from '../Column'
|
||||
import Column from '../Column'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import { MouseoverTooltip } from '../Tooltip'
|
||||
import FormattedPriceImpact from './FormattedPriceImpact'
|
||||
|
||||
const StyledCard = styled(Card)`
|
||||
padding: 0;
|
||||
`
|
||||
import { MouseoverTooltip, TooltipSize } from '../Tooltip'
|
||||
import RouterLabel from './RouterLabel'
|
||||
import SwapRoute from './SwapRoute'
|
||||
|
||||
interface AdvancedSwapDetailsProps {
|
||||
trade?: InterfaceTrade<Currency, Currency, TradeType>
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType>
|
||||
allowedSlippage: Percent
|
||||
syncing?: boolean
|
||||
hideInfoTooltips?: boolean
|
||||
}
|
||||
|
||||
function TextWithLoadingPlaceholder({
|
||||
@@ -45,119 +39,92 @@ function TextWithLoadingPlaceholder({
|
||||
)
|
||||
}
|
||||
|
||||
export function AdvancedSwapDetails({
|
||||
trade,
|
||||
allowedSlippage,
|
||||
syncing = false,
|
||||
hideInfoTooltips = false,
|
||||
}: AdvancedSwapDetailsProps) {
|
||||
const theme = useTheme()
|
||||
export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }: AdvancedSwapDetailsProps) {
|
||||
const { chainId } = useWeb3React()
|
||||
const nativeCurrency = useNativeCurrency(chainId)
|
||||
|
||||
const { expectedOutputAmount, priceImpact } = useMemo(() => {
|
||||
return {
|
||||
expectedOutputAmount: trade?.outputAmount,
|
||||
priceImpact: trade ? computeRealizedPriceImpact(trade) : undefined,
|
||||
}
|
||||
}, [trade])
|
||||
|
||||
return !trade ? null : (
|
||||
<StyledCard>
|
||||
<AutoColumn gap="sm">
|
||||
return (
|
||||
<Column gap="md">
|
||||
<Separator />
|
||||
{!trade.gasUseEstimateUSD || !chainId || !SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) ? null : (
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<MouseoverTooltip
|
||||
text={
|
||||
<Trans>
|
||||
The amount you expect to receive at the current market price. You may receive less or more if the
|
||||
market price changes while your transaction is pending.
|
||||
</Trans>
|
||||
}
|
||||
disableHover={hideInfoTooltips}
|
||||
>
|
||||
<ThemedText.DeprecatedSubHeader color={theme.textPrimary}>
|
||||
<Trans>Expected Output</Trans>
|
||||
</ThemedText.DeprecatedSubHeader>
|
||||
</MouseoverTooltip>
|
||||
</RowFixed>
|
||||
<TextWithLoadingPlaceholder syncing={syncing} width={65}>
|
||||
<ThemedText.DeprecatedBlack textAlign="right" fontSize={14}>
|
||||
{expectedOutputAmount
|
||||
? `${expectedOutputAmount.toSignificant(6)} ${expectedOutputAmount.currency.symbol}`
|
||||
: '-'}
|
||||
</ThemedText.DeprecatedBlack>
|
||||
</TextWithLoadingPlaceholder>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<MouseoverTooltip
|
||||
text={<Trans>The impact your trade has on the market price of this pool.</Trans>}
|
||||
disableHover={hideInfoTooltips}
|
||||
>
|
||||
<ThemedText.DeprecatedSubHeader color={theme.textPrimary}>
|
||||
<Trans>Price Impact</Trans>
|
||||
</ThemedText.DeprecatedSubHeader>
|
||||
</MouseoverTooltip>
|
||||
</RowFixed>
|
||||
<MouseoverTooltip
|
||||
text={
|
||||
<Trans>
|
||||
The fee paid to miners who process your transaction. This must be paid in {nativeCurrency.symbol}.
|
||||
</Trans>
|
||||
}
|
||||
>
|
||||
<ThemedText.BodySmall color="textSecondary">
|
||||
<Trans>Network fee</Trans>
|
||||
</ThemedText.BodySmall>
|
||||
</MouseoverTooltip>
|
||||
<TextWithLoadingPlaceholder syncing={syncing} width={50}>
|
||||
<ThemedText.DeprecatedBlack textAlign="right" fontSize={14}>
|
||||
<FormattedPriceImpact priceImpact={priceImpact} />
|
||||
</ThemedText.DeprecatedBlack>
|
||||
<ThemedText.BodySmall>~${trade.gasUseEstimateUSD.toFixed(2)}</ThemedText.BodySmall>
|
||||
</TextWithLoadingPlaceholder>
|
||||
</RowBetween>
|
||||
<Separator />
|
||||
<RowBetween>
|
||||
<RowFixed style={{ marginRight: '20px' }}>
|
||||
<MouseoverTooltip
|
||||
text={
|
||||
<Trans>
|
||||
The minimum amount you are guaranteed to receive. If the price slips any further, your transaction
|
||||
will revert.
|
||||
</Trans>
|
||||
}
|
||||
disableHover={hideInfoTooltips}
|
||||
>
|
||||
<ThemedText.DeprecatedSubHeader color={theme.textTertiary}>
|
||||
{trade.tradeType === TradeType.EXACT_INPUT ? (
|
||||
<Trans>Minimum received</Trans>
|
||||
) : (
|
||||
<Trans>Maximum sent</Trans>
|
||||
)}{' '}
|
||||
<Trans>after slippage</Trans> ({allowedSlippage.toFixed(2)}%)
|
||||
</ThemedText.DeprecatedSubHeader>
|
||||
</MouseoverTooltip>
|
||||
</RowFixed>
|
||||
<TextWithLoadingPlaceholder syncing={syncing} width={70}>
|
||||
<ThemedText.DeprecatedBlack textAlign="right" fontSize={14} color={theme.textTertiary}>
|
||||
{trade.tradeType === TradeType.EXACT_INPUT
|
||||
? `${trade.minimumAmountOut(allowedSlippage).toSignificant(6)} ${trade.outputAmount.currency.symbol}`
|
||||
: `${trade.maximumAmountIn(allowedSlippage).toSignificant(6)} ${trade.inputAmount.currency.symbol}`}
|
||||
</ThemedText.DeprecatedBlack>
|
||||
</TextWithLoadingPlaceholder>
|
||||
</RowBetween>
|
||||
{!trade?.gasUseEstimateUSD || !chainId || !SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) ? null : (
|
||||
<RowBetween>
|
||||
<MouseoverTooltip
|
||||
text={
|
||||
<Trans>
|
||||
The fee paid to miners who process your transaction. This must be paid in {nativeCurrency.symbol}.
|
||||
</Trans>
|
||||
}
|
||||
disableHover={hideInfoTooltips}
|
||||
>
|
||||
<ThemedText.DeprecatedSubHeader color={theme.textTertiary}>
|
||||
<Trans>Network Fee</Trans>
|
||||
</ThemedText.DeprecatedSubHeader>
|
||||
</MouseoverTooltip>
|
||||
<TextWithLoadingPlaceholder syncing={syncing} width={50}>
|
||||
<ThemedText.DeprecatedBlack textAlign="right" fontSize={14} color={theme.textTertiary}>
|
||||
~${trade.gasUseEstimateUSD.toFixed(2)}
|
||||
</ThemedText.DeprecatedBlack>
|
||||
</TextWithLoadingPlaceholder>
|
||||
</RowBetween>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</StyledCard>
|
||||
)}
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<MouseoverTooltip
|
||||
text={
|
||||
<Trans>
|
||||
The minimum amount you are guaranteed to receive. If the price slips any further, your transaction will
|
||||
revert.
|
||||
</Trans>
|
||||
}
|
||||
>
|
||||
<ThemedText.BodySmall color="textSecondary">
|
||||
<Trans>Minimum output</Trans>
|
||||
</ThemedText.BodySmall>
|
||||
</MouseoverTooltip>
|
||||
</RowFixed>
|
||||
<TextWithLoadingPlaceholder syncing={syncing} width={70}>
|
||||
<ThemedText.BodySmall>
|
||||
{trade.tradeType === TradeType.EXACT_INPUT
|
||||
? `${trade.minimumAmountOut(allowedSlippage).toSignificant(6)} ${trade.outputAmount.currency.symbol}`
|
||||
: `${trade.maximumAmountIn(allowedSlippage).toSignificant(6)} ${trade.inputAmount.currency.symbol}`}
|
||||
</ThemedText.BodySmall>
|
||||
</TextWithLoadingPlaceholder>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<MouseoverTooltip
|
||||
text={
|
||||
<Trans>
|
||||
The amount you expect to receive at the current market price. You may receive less or more if the market
|
||||
price changes while your transaction is pending.
|
||||
</Trans>
|
||||
}
|
||||
>
|
||||
<ThemedText.BodySmall color="textSecondary">
|
||||
<Trans>Expected output</Trans>
|
||||
</ThemedText.BodySmall>
|
||||
</MouseoverTooltip>
|
||||
</RowFixed>
|
||||
<TextWithLoadingPlaceholder syncing={syncing} width={65}>
|
||||
<ThemedText.BodySmall>
|
||||
{`${trade.outputAmount.toSignificant(6)} ${trade.outputAmount.currency.symbol}`}
|
||||
</ThemedText.BodySmall>
|
||||
</TextWithLoadingPlaceholder>
|
||||
</RowBetween>
|
||||
<Separator />
|
||||
<RowBetween>
|
||||
<ThemedText.BodySmall color="textSecondary">
|
||||
<Trans>Order routing</Trans>
|
||||
</ThemedText.BodySmall>
|
||||
<MouseoverTooltip
|
||||
size={TooltipSize.Large}
|
||||
text={<SwapRoute data-testid="swap-route-info" trade={trade} syncing={syncing} />}
|
||||
onOpen={() => {
|
||||
sendAnalyticsEvent(SwapEventName.SWAP_AUTOROUTER_VISUALIZATION_EXPANDED, {
|
||||
element: InterfaceElementName.AUTOROUTER_VISUALIZATION_ROW,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<RouterLabel />
|
||||
</MouseoverTooltip>
|
||||
</RowBetween>
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { useRef } from 'react'
|
||||
|
||||
let uniqueId = 0
|
||||
const getUniqueId = () => uniqueId++
|
||||
|
||||
export default function AutoRouterIcon({ className, id }: { className?: string; id?: string }) {
|
||||
const componentIdRef = useRef(id ?? getUniqueId())
|
||||
const componentId = `AutoRouterIconGradient${componentIdRef.current}`
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="23"
|
||||
height="20"
|
||||
viewBox="0 0 23 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id={componentId} x1="0" y1="0" x2="1" y2="0" gradientTransform="rotate(95)">
|
||||
<stop id="stop1" offset="0" stopColor="#2274E2" />
|
||||
<stop id="stop1" offset="0.5" stopColor="#2274E2" />
|
||||
<stop id="stop2" offset="1" stopColor="#3FB672" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
d="M16 16C10 16 9 10 5 10M16 16C16 17.6569 17.3431 19 19 19C20.6569 19 22 17.6569 22 16C22 14.3431 20.6569 13 19 13C17.3431 13 16 14.3431 16 16ZM5 10C9 10 10 4 16 4M5 10H1.5M16 4C16 5.65685 17.3431 7 19 7C20.6569 7 22 5.65685 22 4C22 2.34315 20.6569 1 19 1C17.3431 1 16 2.34315 16 4Z"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
stroke={`url(#${componentId})`}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
|
||||
import { warningSeverity } from '../../utils/prices'
|
||||
import { ErrorText } from './styleds'
|
||||
|
||||
export const formatPriceImpact = (priceImpact: Percent) => `${priceImpact.multiply(-1).toFixed(2)}%`
|
||||
|
||||
/**
|
||||
* Formatted version of price impact text with warning colors
|
||||
*/
|
||||
export default function FormattedPriceImpact({ priceImpact }: { priceImpact?: Percent }) {
|
||||
return (
|
||||
<ErrorText fontWeight={500} fontSize={14} severity={warningSeverity(priceImpact)}>
|
||||
{priceImpact ? formatPriceImpact(priceImpact) : '-'}
|
||||
</ErrorText>
|
||||
)
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Currency, TradeType } from '@uniswap/sdk-core'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { LoadingOpacityContainer } from 'components/Loader/styled'
|
||||
import { RowFixed } from 'components/Row'
|
||||
import { MouseoverTooltipContent } from 'components/Tooltip'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
import { ReactComponent as GasIcon } from '../../assets/images/gas-icon.svg'
|
||||
import { ResponsiveTooltipContainer } from './styleds'
|
||||
import SwapRoute from './SwapRoute'
|
||||
|
||||
const GasWrapper = styled(RowFixed)`
|
||||
border-radius: 8px;
|
||||
padding: 4px 6px;
|
||||
height: 24px;
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
background-color: ${({ theme }) => theme.deprecated_bg1};
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
user-select: none;
|
||||
`
|
||||
const StyledGasIcon = styled(GasIcon)`
|
||||
margin-right: 4px;
|
||||
height: 14px;
|
||||
& > * {
|
||||
stroke: ${({ theme }) => theme.textTertiary};
|
||||
}
|
||||
`
|
||||
|
||||
export default function GasEstimateBadge({
|
||||
trade,
|
||||
loading,
|
||||
showRoute,
|
||||
disableHover,
|
||||
}: {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined | null // dollar amount in active chain's stablecoin
|
||||
loading: boolean
|
||||
showRoute?: boolean // show route instead of gas estimation summary
|
||||
disableHover?: boolean
|
||||
}) {
|
||||
const formattedGasPriceString = trade?.gasUseEstimateUSD
|
||||
? trade.gasUseEstimateUSD.toFixed(2) === '0.00'
|
||||
? '<$0.01'
|
||||
: '$' + trade.gasUseEstimateUSD.toFixed(2)
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<MouseoverTooltipContent
|
||||
wrap={false}
|
||||
disableHover={disableHover}
|
||||
content={
|
||||
loading ? null : (
|
||||
<ResponsiveTooltipContainer
|
||||
origin="top right"
|
||||
style={{
|
||||
padding: showRoute ? '0' : '12px',
|
||||
border: 'none',
|
||||
borderRadius: showRoute ? '16px' : '12px',
|
||||
maxWidth: '400px',
|
||||
}}
|
||||
>
|
||||
{showRoute ? (
|
||||
trade ? (
|
||||
<SwapRoute trade={trade} syncing={loading} fixedOpen={showRoute} />
|
||||
) : null
|
||||
) : (
|
||||
<AutoColumn gap="4px" justify="center">
|
||||
<ThemedText.DeprecatedMain fontSize="12px" textAlign="center">
|
||||
<Trans>Estimated network fee</Trans>
|
||||
</ThemedText.DeprecatedMain>
|
||||
<ThemedText.DeprecatedBody textAlign="center" fontWeight={500} style={{ userSelect: 'none' }}>
|
||||
<Trans>${trade?.gasUseEstimateUSD?.toFixed(2)}</Trans>
|
||||
</ThemedText.DeprecatedBody>
|
||||
<ThemedText.DeprecatedMain fontSize="10px" textAlign="center" maxWidth="140px" color="text3">
|
||||
<Trans>Estimate may differ due to your wallet gas settings</Trans>
|
||||
</ThemedText.DeprecatedMain>
|
||||
</AutoColumn>
|
||||
)}
|
||||
</ResponsiveTooltipContainer>
|
||||
)
|
||||
}
|
||||
placement="bottom"
|
||||
onOpen={() =>
|
||||
sendEvent({
|
||||
category: 'Gas',
|
||||
action: 'Gas Details Tooltip Open',
|
||||
})
|
||||
}
|
||||
>
|
||||
<LoadingOpacityContainer $loading={loading}>
|
||||
<GasWrapper>
|
||||
<StyledGasIcon />
|
||||
{formattedGasPriceString ?? null}
|
||||
</GasWrapper>
|
||||
</LoadingOpacityContainer>
|
||||
</MouseoverTooltipContent>
|
||||
)
|
||||
}
|
||||
63
src/components/swap/GasEstimateTooltip.tsx
Normal file
63
src/components/swap/GasEstimateTooltip.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
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'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
import { ReactComponent as GasIcon } from '../../assets/images/gas-icon.svg'
|
||||
import SwapRoute from './SwapRoute'
|
||||
|
||||
const StyledGasIcon = styled(GasIcon)`
|
||||
margin-right: 4px;
|
||||
height: 18px;
|
||||
|
||||
// We apply the following to all children of the SVG in order to override the default color
|
||||
& > * {
|
||||
stroke: ${({ theme }) => theme.textTertiary};
|
||||
}
|
||||
`
|
||||
|
||||
export default function GasEstimateTooltip({
|
||||
trade,
|
||||
loading,
|
||||
disabled,
|
||||
}: {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> // dollar amount in active chain's stablecoin
|
||||
loading: boolean
|
||||
disabled?: boolean
|
||||
}) {
|
||||
const formattedGasPriceString = trade?.gasUseEstimateUSD
|
||||
? trade.gasUseEstimateUSD.toFixed(2) === '0.00'
|
||||
? '<$0.01'
|
||||
: '$' + trade.gasUseEstimateUSD.toFixed(2)
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<MouseoverTooltip
|
||||
disabled={disabled}
|
||||
size={TooltipSize.Large}
|
||||
// TODO(WEB-3304)
|
||||
// Most of Swap-related components accept either `syncing`, `loading` or both props at the same time.
|
||||
// We are often using them interchangeably, or pass both values as one of them (`syncing={loading || syncing}`).
|
||||
// This is confusing and can lead to unpredicted UI behavior. We should refactor and unify this.
|
||||
text={<SwapRoute trade={trade} syncing={loading} />}
|
||||
onOpen={() => {
|
||||
sendAnalyticsEvent(SwapEventName.SWAP_AUTOROUTER_VISUALIZATION_EXPANDED, {
|
||||
element: InterfaceElementName.AUTOROUTER_VISUALIZATION_ROW,
|
||||
})
|
||||
}}
|
||||
placement="bottom"
|
||||
>
|
||||
<LoadingOpacityContainer $loading={loading}>
|
||||
<RowFixed>
|
||||
<StyledGasIcon />
|
||||
<ThemedText.BodySmall color="textSecondary">{formattedGasPriceString}</ThemedText.BodySmall>
|
||||
</RowFixed>
|
||||
</LoadingOpacityContainer>
|
||||
</MouseoverTooltip>
|
||||
)
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import { ThemedText } from '../../theme'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import { MouseoverTooltip } from '../Tooltip'
|
||||
import { formatPriceImpact } from './FormattedPriceImpact'
|
||||
|
||||
const StyledCard = styled(OutlineCard)`
|
||||
padding: 12px;
|
||||
@@ -19,6 +18,8 @@ interface PriceImpactWarningProps {
|
||||
priceImpact: Percent
|
||||
}
|
||||
|
||||
const formatPriceImpact = (priceImpact: Percent) => `${priceImpact.multiply(-1).toFixed(2)}%`
|
||||
|
||||
export default function PriceImpactWarning({ priceImpact }: PriceImpactWarningProps) {
|
||||
const theme = useTheme()
|
||||
|
||||
|
||||
@@ -1,58 +1,15 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import useAutoRouterSupported from 'hooks/useAutoRouterSupported'
|
||||
import styled from 'styled-components/macro'
|
||||
import { RouterPreference } from 'state/routing/slice'
|
||||
import { useRouterPreference } from 'state/user/hooks'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
import { ReactComponent as StaticRouterIcon } from '../../assets/svg/static_route.svg'
|
||||
import AutoRouterIcon from './AutoRouterIcon'
|
||||
export default function RouterLabel() {
|
||||
const [routerPreference] = useRouterPreference()
|
||||
|
||||
const StyledAutoRouterIcon = styled(AutoRouterIcon)`
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
|
||||
:hover {
|
||||
filter: brightness(1.3);
|
||||
switch (routerPreference) {
|
||||
case RouterPreference.AUTO:
|
||||
case RouterPreference.API:
|
||||
return <ThemedText.BodySmall>Uniswap API</ThemedText.BodySmall>
|
||||
case RouterPreference.CLIENT:
|
||||
return <ThemedText.BodySmall>Uniswap Client</ThemedText.BodySmall>
|
||||
}
|
||||
`
|
||||
|
||||
const StyledStaticRouterIcon = styled(StaticRouterIcon)`
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
|
||||
fill: ${({ theme }) => theme.textTertiary};
|
||||
|
||||
:hover {
|
||||
filter: brightness(1.3);
|
||||
}
|
||||
`
|
||||
|
||||
const StyledAutoRouterLabel = styled(ThemedText.DeprecatedBlack)`
|
||||
line-height: 1rem;
|
||||
|
||||
/* fallback color */
|
||||
color: ${({ theme }) => theme.accentSuccess};
|
||||
|
||||
@supports (-webkit-background-clip: text) and (-webkit-text-fill-color: transparent) {
|
||||
background-image: linear-gradient(90deg, #2172e5 0%, #54e521 163.16%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
`
|
||||
|
||||
export function AutoRouterLogo() {
|
||||
const autoRouterSupported = useAutoRouterSupported()
|
||||
|
||||
return autoRouterSupported ? <StyledAutoRouterIcon /> : <StyledStaticRouterIcon />
|
||||
}
|
||||
|
||||
export function AutoRouterLabel() {
|
||||
const autoRouterSupported = useAutoRouterSupported()
|
||||
|
||||
return autoRouterSupported ? (
|
||||
<StyledAutoRouterLabel fontSize={14}>Auto Router</StyledAutoRouterLabel>
|
||||
) : (
|
||||
<ThemedText.DeprecatedBlack fontSize={14}>
|
||||
<Trans>Trade Route</Trans>
|
||||
</ThemedText.DeprecatedBlack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/an
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useAccountDrawer } from 'components/AccountDrawer'
|
||||
import { ButtonText } from 'components/Button'
|
||||
import { MouseoverTooltipContent } from 'components/Tooltip'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useBuyFiatFlowCompleted } from 'state/user/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
@@ -109,9 +109,8 @@ export default function SwapBuyFiatButton() {
|
||||
!fiatOnrampAvailabilityChecked || (fiatOnrampAvailabilityChecked && fiatOnrampAvailable)
|
||||
|
||||
return (
|
||||
<MouseoverTooltipContent
|
||||
wrap
|
||||
content={
|
||||
<MouseoverTooltip
|
||||
text={
|
||||
<div data-testid="fiat-on-ramp-unavailable-tooltip">
|
||||
<Trans>Crypto purchases are not available in your region. </Trans>
|
||||
<TraceEvent
|
||||
@@ -126,7 +125,7 @@ export default function SwapBuyFiatButton() {
|
||||
</div>
|
||||
}
|
||||
placement="bottom"
|
||||
disableHover={fiatOnRampsUnavailableTooltipDisabled}
|
||||
disabled={fiatOnRampsUnavailableTooltipDisabled}
|
||||
>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
@@ -139,6 +138,6 @@ export default function SwapBuyFiatButton() {
|
||||
{!buyFiatFlowCompleted && <Dot data-testid="buy-fiat-flow-incomplete-indicator" />}
|
||||
</StyledTextButton>
|
||||
</TraceEvent>
|
||||
</MouseoverTooltipContent>
|
||||
</MouseoverTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -38,6 +38,5 @@ describe('SwapDetailsDropdown.tsx', () => {
|
||||
expect(screen.getByTestId('trade-price-container')).toBeInTheDocument()
|
||||
await act(() => userEvent.click(screen.getByTestId('swap-details-header-row')))
|
||||
expect(screen.getByTestId('advanced-swap-details')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('swap-route-info')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,10 +4,9 @@ import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/anal
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import AnimatedDropdown from 'components/AnimatedDropdown'
|
||||
import { OutlineCard } from 'components/Card'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import Column from 'components/Column'
|
||||
import { LoadingOpacityContainer } from 'components/Loader/styled'
|
||||
import Row, { RowBetween, RowFixed } from 'components/Row'
|
||||
import { RowBetween, RowFixed } from 'components/Row'
|
||||
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
|
||||
import { useState } from 'react'
|
||||
import { ChevronDown } from 'react-feather'
|
||||
@@ -16,24 +15,9 @@ import styled, { keyframes, useTheme } from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
import { AdvancedSwapDetails } from './AdvancedSwapDetails'
|
||||
import GasEstimateBadge from './GasEstimateBadge'
|
||||
import SwapRoute from './SwapRoute'
|
||||
import GasEstimateTooltip from './GasEstimateTooltip'
|
||||
import TradePrice from './TradePrice'
|
||||
|
||||
const Wrapper = styled(Row)`
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
border-radius: inherit;
|
||||
padding: 8px 12px;
|
||||
margin-top: 0;
|
||||
min-height: 32px;
|
||||
`
|
||||
|
||||
const StyledCard = styled(OutlineCard)`
|
||||
padding: 12px;
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
`
|
||||
|
||||
const StyledHeaderRow = styled(RowBetween)<{ disabled: boolean; open: boolean }>`
|
||||
padding: 0;
|
||||
align-items: center;
|
||||
@@ -97,6 +81,16 @@ const Spinner = styled.div`
|
||||
top: -3px;
|
||||
`
|
||||
|
||||
const SwapDetailsWrapper = styled.div`
|
||||
padding-top: ${({ theme }) => theme.grids.md};
|
||||
`
|
||||
|
||||
const Wrapper = styled(Column)`
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
border-radius: 16px;
|
||||
padding: 12px 16px;
|
||||
`
|
||||
|
||||
interface SwapDetailsInlineProps {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
|
||||
syncing: boolean
|
||||
@@ -110,68 +104,58 @@ export default function SwapDetailsDropdown({ trade, syncing, loading, allowedSl
|
||||
const [showDetails, setShowDetails] = useState(false)
|
||||
|
||||
return (
|
||||
<Wrapper style={{ marginTop: '0' }}>
|
||||
<AutoColumn gap="sm" style={{ width: '100%', marginBottom: '-8px' }}>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SwapEventName.SWAP_DETAILS_EXPANDED}
|
||||
element={InterfaceElementName.SWAP_DETAILS_DROPDOWN}
|
||||
shouldLogImpression={!showDetails}
|
||||
<Wrapper>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SwapEventName.SWAP_DETAILS_EXPANDED}
|
||||
element={InterfaceElementName.SWAP_DETAILS_DROPDOWN}
|
||||
shouldLogImpression={!showDetails}
|
||||
>
|
||||
<StyledHeaderRow
|
||||
data-testid="swap-details-header-row"
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
disabled={!trade}
|
||||
open={showDetails}
|
||||
>
|
||||
<StyledHeaderRow
|
||||
data-testid="swap-details-header-row"
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
disabled={!trade}
|
||||
open={showDetails}
|
||||
>
|
||||
<RowFixed style={{ position: 'relative' }} align="center">
|
||||
{Boolean(loading || syncing) && (
|
||||
<StyledPolling>
|
||||
<StyledPollingDot>
|
||||
<Spinner />
|
||||
</StyledPollingDot>
|
||||
</StyledPolling>
|
||||
)}
|
||||
{trade ? (
|
||||
<LoadingOpacityContainer $loading={syncing} data-testid="trade-price-container">
|
||||
<TradePrice price={trade.executionPrice} />
|
||||
</LoadingOpacityContainer>
|
||||
) : loading || syncing ? (
|
||||
<ThemedText.DeprecatedMain fontSize={14}>
|
||||
<Trans>Fetching best price...</Trans>
|
||||
</ThemedText.DeprecatedMain>
|
||||
) : null}
|
||||
</RowFixed>
|
||||
<RowFixed>
|
||||
{!trade?.gasUseEstimateUSD ||
|
||||
showDetails ||
|
||||
!chainId ||
|
||||
!SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) ? null : (
|
||||
<GasEstimateBadge
|
||||
trade={trade}
|
||||
loading={syncing || loading}
|
||||
showRoute={!showDetails}
|
||||
disableHover={showDetails}
|
||||
/>
|
||||
)}
|
||||
<RotatingArrow
|
||||
stroke={trade ? theme.textTertiary : theme.deprecated_bg3}
|
||||
open={Boolean(trade && showDetails)}
|
||||
/>
|
||||
</RowFixed>
|
||||
</StyledHeaderRow>
|
||||
</TraceEvent>
|
||||
<AnimatedDropdown open={showDetails}>
|
||||
<AutoColumn gap="sm" style={{ padding: '0', paddingBottom: '8px' }}>
|
||||
<RowFixed>
|
||||
{Boolean(loading || syncing) && (
|
||||
<StyledPolling>
|
||||
<StyledPollingDot>
|
||||
<Spinner />
|
||||
</StyledPollingDot>
|
||||
</StyledPolling>
|
||||
)}
|
||||
{trade ? (
|
||||
<StyledCard data-testid="advanced-swap-details">
|
||||
<AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} syncing={syncing} />
|
||||
</StyledCard>
|
||||
<LoadingOpacityContainer $loading={syncing} data-testid="trade-price-container">
|
||||
<TradePrice price={trade.executionPrice} />
|
||||
</LoadingOpacityContainer>
|
||||
) : loading || syncing ? (
|
||||
<ThemedText.DeprecatedMain fontSize={14}>
|
||||
<Trans>Fetching best price...</Trans>
|
||||
</ThemedText.DeprecatedMain>
|
||||
) : null}
|
||||
{trade ? <SwapRoute data-testid="swap-route-info" trade={trade} syncing={syncing} /> : null}
|
||||
</AutoColumn>
|
||||
</RowFixed>
|
||||
<RowFixed>
|
||||
{!trade?.gasUseEstimateUSD ||
|
||||
showDetails ||
|
||||
!chainId ||
|
||||
!SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) ? null : (
|
||||
<GasEstimateTooltip trade={trade} loading={syncing || loading} disabled={showDetails} />
|
||||
)}
|
||||
<RotatingArrow
|
||||
stroke={trade ? theme.textTertiary : theme.deprecated_bg3}
|
||||
open={Boolean(trade && showDetails)}
|
||||
/>
|
||||
</RowFixed>
|
||||
</StyledHeaderRow>
|
||||
</TraceEvent>
|
||||
{trade && (
|
||||
<AnimatedDropdown open={showDetails}>
|
||||
<SwapDetailsWrapper data-testid="advanced-swap-details">
|
||||
<AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} syncing={syncing} />
|
||||
</SwapDetailsWrapper>
|
||||
</AnimatedDropdown>
|
||||
</AutoColumn>
|
||||
)}
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ const TextHeader = styled.div`
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
export default function SwapHeader({ allowedSlippage }: { allowedSlippage: Percent }) {
|
||||
export default function SwapHeader({ autoSlippage }: { autoSlippage: Percent }) {
|
||||
const fiatOnRampButtonEnabled = useFiatOnRampButtonEnabled()
|
||||
|
||||
return (
|
||||
@@ -38,7 +38,7 @@ export default function SwapHeader({ allowedSlippage }: { allowedSlippage: Perce
|
||||
{fiatOnRampButtonEnabled && <SwapBuyFiatButton />}
|
||||
</RowFixed>
|
||||
<RowFixed>
|
||||
<SettingsTab placeholderSlippage={allowedSlippage} />
|
||||
<SettingsTab autoSlippage={autoSlippage} />
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
</StyledSwapHeader>
|
||||
|
||||
@@ -13,14 +13,15 @@ import {
|
||||
} from 'lib/utils/analytics'
|
||||
import { ReactNode } from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import { RouterPreference } from 'state/routing/slice'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
import { useClientSideRouter, useUserSlippageTolerance } from 'state/user/hooks'
|
||||
import { useRouterPreference, useUserSlippageTolerance } from 'state/user/hooks'
|
||||
import getRoutingDiagramEntries, { RoutingDiagramEntry } from 'utils/getRoutingDiagramEntries'
|
||||
import { computeRealizedPriceImpact } from 'utils/prices'
|
||||
|
||||
import { ButtonError } from '../Button'
|
||||
import { AutoRow } from '../Row'
|
||||
import { SwapCallbackError } from './styleds'
|
||||
import { getTokenPath, RoutingDiagramEntry } from './SwapRoute'
|
||||
|
||||
interface AnalyticsEventProps {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType>
|
||||
@@ -123,8 +124,8 @@ export default function SwapModalFooter({
|
||||
}) {
|
||||
const transactionDeadlineSecondsSinceEpoch = useTransactionDeadline()?.toNumber() // in seconds since epoch
|
||||
const isAutoSlippage = useUserSlippageTolerance()[0] === 'auto'
|
||||
const [clientSideRouter] = useClientSideRouter()
|
||||
const routes = getTokenPath(trade)
|
||||
const [routerPreference] = useRouterPreference()
|
||||
const routes = getRoutingDiagramEntries(trade)
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -139,7 +140,7 @@ export default function SwapModalFooter({
|
||||
allowedSlippage,
|
||||
transactionDeadlineSecondsSinceEpoch,
|
||||
isAutoSlippage,
|
||||
isAutoRouterApi: !clientSideRouter,
|
||||
isAutoRouterApi: routerPreference === RouterPreference.AUTO || routerPreference === RouterPreference.API,
|
||||
swapQuoteReceivedDate,
|
||||
routes,
|
||||
fiatValueInput: fiatValueInput.data,
|
||||
|
||||
@@ -1,157 +1,71 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
|
||||
import { Protocol } from '@uniswap/router-sdk'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { Pair } from '@uniswap/v2-sdk'
|
||||
import { FeeAmount } from '@uniswap/v3-sdk'
|
||||
import { Currency, TradeType } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import AnimatedDropdown from 'components/AnimatedDropdown'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import Column from 'components/Column'
|
||||
import { LoadingRows } from 'components/Loader/styled'
|
||||
import RoutingDiagram from 'components/RoutingDiagram/RoutingDiagram'
|
||||
import { AutoRow, RowBetween } from 'components/Row'
|
||||
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
|
||||
import useAutoRouterSupported from 'hooks/useAutoRouterSupported'
|
||||
import { memo, useState } from 'react'
|
||||
import { Plus } from 'react-feather'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
import styled from 'styled-components/macro'
|
||||
import { Separator, ThemedText } from 'theme'
|
||||
import { useDarkModeManager } from 'theme/components/ThemeToggle'
|
||||
import getRoutingDiagramEntries from 'utils/getRoutingDiagramEntries'
|
||||
|
||||
import { AutoRouterLabel, AutoRouterLogo } from './RouterLabel'
|
||||
import RouterLabel from './RouterLabel'
|
||||
|
||||
const Wrapper = styled(AutoColumn)<{ darkMode?: boolean; fixedOpen?: boolean }>`
|
||||
padding: ${({ fixedOpen }) => (fixedOpen ? '12px' : '12px 8px 12px 12px')};
|
||||
border-radius: 16px;
|
||||
border: 1px solid ${({ theme, fixedOpen }) => (fixedOpen ? 'transparent' : theme.backgroundOutline)};
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const OpenCloseIcon = styled(Plus)<{ open?: boolean }>`
|
||||
margin-left: 8px;
|
||||
height: 20px;
|
||||
stroke-width: 2px;
|
||||
transition: transform 0.1s;
|
||||
transform: ${({ open }) => (open ? 'rotate(45deg)' : 'none')};
|
||||
stroke: ${({ theme }) => theme.textTertiary};
|
||||
cursor: pointer;
|
||||
:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
`
|
||||
|
||||
interface SwapRouteProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
export default function SwapRoute({
|
||||
trade,
|
||||
syncing,
|
||||
}: {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType>
|
||||
syncing: boolean
|
||||
fixedOpen?: boolean // fixed in open state, hide open/close icon
|
||||
}
|
||||
|
||||
export default memo(function SwapRoute({ trade, syncing, fixedOpen = false, ...rest }: SwapRouteProps) {
|
||||
const autoRouterSupported = useAutoRouterSupported()
|
||||
const routes = getTokenPath(trade)
|
||||
const [open, setOpen] = useState(false)
|
||||
}) {
|
||||
const { chainId } = useWeb3React()
|
||||
const autoRouterSupported = useAutoRouterSupported()
|
||||
|
||||
const [darkMode] = useDarkModeManager()
|
||||
const routes = getRoutingDiagramEntries(trade)
|
||||
|
||||
const formattedGasPriceString = trade?.gasUseEstimateUSD
|
||||
? trade.gasUseEstimateUSD.toFixed(2) === '0.00'
|
||||
? '<$0.01'
|
||||
: '$' + trade.gasUseEstimateUSD.toFixed(2)
|
||||
: undefined
|
||||
const gasPrice =
|
||||
// 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'
|
||||
? '<$0.01'
|
||||
: '$' + trade.gasUseEstimateUSD.toFixed(2)
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<Wrapper {...rest} darkMode={darkMode} fixedOpen={fixedOpen}>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SwapEventName.SWAP_AUTOROUTER_VISUALIZATION_EXPANDED}
|
||||
element={InterfaceElementName.AUTOROUTER_VISUALIZATION_ROW}
|
||||
shouldLogImpression={!open}
|
||||
>
|
||||
<RowBetween onClick={() => setOpen(!open)}>
|
||||
<AutoRow gap="4px" width="auto">
|
||||
<AutoRouterLogo />
|
||||
<AutoRouterLabel />
|
||||
</AutoRow>
|
||||
{fixedOpen ? null : <OpenCloseIcon open={open} />}
|
||||
</RowBetween>
|
||||
</TraceEvent>
|
||||
<AnimatedDropdown open={open || fixedOpen}>
|
||||
<AutoRow gap="4px" width="auto" style={{ paddingTop: '12px', margin: 0 }}>
|
||||
<Column gap="md">
|
||||
<RouterLabel />
|
||||
<Separator />
|
||||
{syncing ? (
|
||||
<LoadingRows>
|
||||
<div style={{ width: '100%', height: '30px' }} />
|
||||
</LoadingRows>
|
||||
) : (
|
||||
<RoutingDiagram
|
||||
currencyIn={trade.inputAmount.currency}
|
||||
currencyOut={trade.outputAmount.currency}
|
||||
routes={routes}
|
||||
/>
|
||||
)}
|
||||
{autoRouterSupported && (
|
||||
<>
|
||||
<Separator />
|
||||
{syncing ? (
|
||||
<LoadingRows>
|
||||
<div style={{ width: '400px', height: '30px' }} />
|
||||
<div style={{ width: '100%', height: '15px' }} />
|
||||
</LoadingRows>
|
||||
) : (
|
||||
<RoutingDiagram
|
||||
currencyIn={trade.inputAmount.currency}
|
||||
currencyOut={trade.outputAmount.currency}
|
||||
routes={routes}
|
||||
/>
|
||||
<ThemedText.Caption color="textSecondary">
|
||||
{gasPrice ? <Trans>Best price route costs ~{gasPrice} in gas.</Trans> : null}{' '}
|
||||
<Trans>
|
||||
This route optimizes your total output by considering split routes, multiple hops, and the gas cost of
|
||||
each step.
|
||||
</Trans>
|
||||
</ThemedText.Caption>
|
||||
)}
|
||||
|
||||
{autoRouterSupported && (
|
||||
<>
|
||||
<Separator />
|
||||
{syncing ? (
|
||||
<LoadingRows>
|
||||
<div style={{ width: '250px', height: '15px' }} />
|
||||
</LoadingRows>
|
||||
) : (
|
||||
<ThemedText.DeprecatedMain fontSize={12} width={400} margin={0}>
|
||||
{trade?.gasUseEstimateUSD && chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) ? (
|
||||
<Trans>Best price route costs ~{formattedGasPriceString} in gas. </Trans>
|
||||
) : null}{' '}
|
||||
<Trans>
|
||||
This route optimizes your total output by considering split routes, multiple hops, and the gas cost
|
||||
of each step.
|
||||
</Trans>
|
||||
</ThemedText.DeprecatedMain>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</AutoRow>
|
||||
</AnimatedDropdown>
|
||||
</Wrapper>
|
||||
</>
|
||||
)}
|
||||
</Column>
|
||||
)
|
||||
})
|
||||
|
||||
export interface RoutingDiagramEntry {
|
||||
percent: Percent
|
||||
path: [Currency, Currency, FeeAmount][]
|
||||
protocol: Protocol
|
||||
}
|
||||
|
||||
const V2_DEFAULT_FEE_TIER = 3000
|
||||
|
||||
/**
|
||||
* Loops through all routes on a trade and returns an array of diagram entries.
|
||||
*/
|
||||
export function getTokenPath(trade: InterfaceTrade<Currency, Currency, TradeType>): RoutingDiagramEntry[] {
|
||||
return trade.swaps.map(({ route: { path: tokenPath, pools, protocol }, inputAmount, outputAmount }) => {
|
||||
const portion =
|
||||
trade.tradeType === TradeType.EXACT_INPUT
|
||||
? inputAmount.divide(trade.inputAmount)
|
||||
: outputAmount.divide(trade.outputAmount)
|
||||
const percent = new Percent(portion.numerator, portion.denominator)
|
||||
const path: RoutingDiagramEntry['path'] = []
|
||||
for (let i = 0; i < pools.length; i++) {
|
||||
const nextPool = pools[i]
|
||||
const tokenIn = tokenPath[i]
|
||||
const tokenOut = tokenPath[i + 1]
|
||||
const entry: RoutingDiagramEntry['path'][0] = [
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
nextPool instanceof Pair ? V2_DEFAULT_FEE_TIER : nextPool.fee,
|
||||
]
|
||||
path.push(entry)
|
||||
}
|
||||
return {
|
||||
percent,
|
||||
path,
|
||||
protocol,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
10
src/components/swap/SwapSkeleton.test.tsx
Normal file
10
src/components/swap/SwapSkeleton.test.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { render } from 'test-utils/render'
|
||||
|
||||
import { SwapSkeleton } from './SwapSkeleton'
|
||||
|
||||
describe('SwapSkeleton.tsx', () => {
|
||||
it('renders a skeleton', () => {
|
||||
const { asFragment } = render(<SwapSkeleton />)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
109
src/components/swap/SwapSkeleton.tsx
Normal file
109
src/components/swap/SwapSkeleton.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { ArrowContainer } from 'pages/Swap'
|
||||
import { ArrowDown } from 'react-feather'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
import { ArrowWrapper } from './styleds'
|
||||
|
||||
const StyledArrowWrapper = styled(ArrowWrapper)`
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
margin: 0;
|
||||
`
|
||||
|
||||
const LoadingWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
justify-content: space-between;
|
||||
|
||||
padding: 8px;
|
||||
border: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
|
||||
border-radius: 16px;
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
`
|
||||
|
||||
const Blob = styled.div<{ width?: number; radius?: number }>`
|
||||
background-color: ${({ theme }) => theme.backgroundModule};
|
||||
border-radius: ${({ radius }) => (radius ?? 4) + 'px'};
|
||||
height: 56px;
|
||||
width: ${({ width }) => (width ? width + 'px' : '100%')};
|
||||
`
|
||||
|
||||
const ModuleBlob = styled(Blob)`
|
||||
background-color: ${({ theme }) => theme.backgroundOutline};
|
||||
height: 36px;
|
||||
`
|
||||
|
||||
const TitleColumn = styled.div`
|
||||
padding: 8px;
|
||||
`
|
||||
|
||||
const Row = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
const InputColumn = styled.div`
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
background-color: ${({ theme }) => theme.backgroundModule};
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
padding: 48px 12px;
|
||||
`
|
||||
|
||||
const OutputWrapper = styled.div`
|
||||
position: relative;
|
||||
`
|
||||
|
||||
function Title() {
|
||||
return (
|
||||
<TitleColumn>
|
||||
<ThemedText.SubHeader>
|
||||
<Trans>Swap</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
</TitleColumn>
|
||||
)
|
||||
}
|
||||
|
||||
function FloatingInput() {
|
||||
return (
|
||||
<Row>
|
||||
<ModuleBlob width={60} />
|
||||
<ModuleBlob width={100} radius={16} />
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
function FloatingButton() {
|
||||
return <Blob radius={16} />
|
||||
}
|
||||
|
||||
export function SwapSkeleton() {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<LoadingWrapper>
|
||||
<Title />
|
||||
<InputColumn>
|
||||
<FloatingInput />
|
||||
</InputColumn>
|
||||
<OutputWrapper>
|
||||
<StyledArrowWrapper clickable={false}>
|
||||
<ArrowContainer>
|
||||
<ArrowDown size="16" color={theme.textTertiary} />
|
||||
</ArrowContainer>
|
||||
</StyledArrowWrapper>
|
||||
<InputColumn>
|
||||
<FloatingInput />
|
||||
</InputColumn>
|
||||
</OutputWrapper>
|
||||
<FloatingButton />
|
||||
</LoadingWrapper>
|
||||
)
|
||||
}
|
||||
@@ -25,7 +25,6 @@ const StyledPriceContainer = styled.button`
|
||||
flex-direction: row;
|
||||
text-align: left;
|
||||
flex-wrap: wrap;
|
||||
padding: 8px 0;
|
||||
user-select: text;
|
||||
`
|
||||
|
||||
@@ -60,9 +59,9 @@ export default function TradePrice({ price }: TradePriceProps) {
|
||||
>
|
||||
<ThemedText.BodySmall>{text}</ThemedText.BodySmall>{' '}
|
||||
{usdPrice && (
|
||||
<ThemedText.DeprecatedDarkGray>
|
||||
<ThemedText.BodySmall color="textSecondary">
|
||||
<Trans>({formatNumber(usdPrice, NumberType.FiatTokenPrice)})</Trans>
|
||||
</ThemedText.DeprecatedDarkGray>
|
||||
</ThemedText.BodySmall>
|
||||
)}
|
||||
</StyledPriceContainer>
|
||||
)
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
|
||||
<DocumentFragment>
|
||||
.c0 {
|
||||
.c2 {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
.c3 {
|
||||
width: 100%;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
@@ -25,138 +25,127 @@ exports[`AdvancedSwapDetails.tsx matches base snapshot 1`] = `
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
.c4 {
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
.c5 {
|
||||
width: -webkit-fit-content;
|
||||
width: -moz-fit-content;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
color: #7780A0;
|
||||
}
|
||||
|
||||
.c8 {
|
||||
color: #0D111C;
|
||||
}
|
||||
|
||||
.c10 {
|
||||
.c1 {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: #D2D9EE;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
border-radius: 16px;
|
||||
.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;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
display: grid;
|
||||
grid-auto-rows: auto;
|
||||
grid-row-gap: 8px;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
.c6 {
|
||||
display: inline-block;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.c9 {
|
||||
color: #7780A0;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0 c1 c2"
|
||||
class="c0"
|
||||
>
|
||||
<div
|
||||
class="c3"
|
||||
class="c1"
|
||||
/>
|
||||
<div
|
||||
class="c2 c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c0 c4 c5"
|
||||
class="c2 c3 c5"
|
||||
>
|
||||
<div
|
||||
class="c0 c4 c6"
|
||||
class="c6"
|
||||
>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="css-zhpkf8"
|
||||
>
|
||||
Expected Output
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="c7 css-zhpkf8"
|
||||
>
|
||||
Minimum output
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c8 css-q4yjm0"
|
||||
>
|
||||
0.000000000000001 DEF
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c0 c4 c5"
|
||||
>
|
||||
<div
|
||||
class="c0 c4 c6"
|
||||
>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="css-zhpkf8"
|
||||
>
|
||||
Price Impact
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c8 css-q4yjm0"
|
||||
>
|
||||
<div
|
||||
class="c9 css-1aekuku"
|
||||
>
|
||||
105567.37%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c10"
|
||||
/>
|
||||
class="c8 css-zhpkf8"
|
||||
>
|
||||
0.00000000000000098 DEF
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c2 c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c0 c4 c5"
|
||||
class="c2 c3 c5"
|
||||
>
|
||||
<div
|
||||
class="c0 c4 c6"
|
||||
style="margin-right: 20px;"
|
||||
class="c6"
|
||||
>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="css-zhpkf8"
|
||||
>
|
||||
Minimum received after slippage (2.00%)
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="c7 css-zhpkf8"
|
||||
>
|
||||
Expected output
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-q4yjm0"
|
||||
>
|
||||
0.00000000000000098 DEF
|
||||
</div>
|
||||
<div
|
||||
class="c8 css-zhpkf8"
|
||||
>
|
||||
0.000000000000001 DEF
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c1"
|
||||
/>
|
||||
<div
|
||||
class="c2 c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c7 css-zhpkf8"
|
||||
>
|
||||
Order routing
|
||||
</div>
|
||||
<div
|
||||
class="c6"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c8 css-zhpkf8"
|
||||
>
|
||||
Uniswap API
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,27 +2,13 @@
|
||||
|
||||
exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
|
||||
<DocumentFragment>
|
||||
.c0 {
|
||||
.c2 {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.c20 {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.c37 {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
.c3 {
|
||||
width: 100%;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
@@ -39,42 +25,6 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.c21 {
|
||||
width: auto;
|
||||
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: 4px;
|
||||
}
|
||||
|
||||
.c38 {
|
||||
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: 1px;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
@@ -82,28 +32,6 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.c22 {
|
||||
-webkit-flex-wrap: wrap;
|
||||
-ms-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
margin: -4px;
|
||||
}
|
||||
|
||||
.c22 > * {
|
||||
margin: 4px !important;
|
||||
}
|
||||
|
||||
.c39 {
|
||||
-webkit-flex-wrap: wrap;
|
||||
-ms-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
margin: -1px;
|
||||
}
|
||||
|
||||
.c39 > * {
|
||||
margin: 1px !important;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
width: -webkit-fit-content;
|
||||
width: -moz-fit-content;
|
||||
@@ -114,35 +42,43 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
|
||||
color: #0D111C;
|
||||
}
|
||||
|
||||
.c44 {
|
||||
.c15 {
|
||||
color: #7780A0;
|
||||
}
|
||||
|
||||
.c17 {
|
||||
.c13 {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: #D2D9EE;
|
||||
}
|
||||
|
||||
.c11 {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
border-radius: 16px;
|
||||
.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;
|
||||
}
|
||||
|
||||
.c12 {
|
||||
border: 1px solid #B8C0DC;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
display: grid;
|
||||
grid-auto-rows: auto;
|
||||
grid-row-gap: 8px;
|
||||
}
|
||||
|
||||
.c18 {
|
||||
display: grid;
|
||||
grid-auto-rows: auto;
|
||||
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 {
|
||||
@@ -153,201 +89,11 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.c15 {
|
||||
.c14 {
|
||||
display: inline-block;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.c16 {
|
||||
color: #7780A0;
|
||||
}
|
||||
|
||||
.c14 {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.c33 {
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
background: #E8ECFB;
|
||||
border: unset;
|
||||
border-radius: 0.5rem;
|
||||
color: #000;
|
||||
display: -webkit-inline-box;
|
||||
display: -webkit-inline-flex;
|
||||
display: -ms-inline-flexbox;
|
||||
display: inline-flex;
|
||||
padding: 4px 6px;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.c29 {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(white 60%,#ffffff00 calc(70% + 1px));
|
||||
box-shadow: 0 0 1px white;
|
||||
}
|
||||
|
||||
.c28 {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.c41 {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.c42 {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.c43 {
|
||||
position: absolute;
|
||||
left: -10px !important;
|
||||
}
|
||||
|
||||
.c26 {
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.c27 {
|
||||
display: grid;
|
||||
grid-template-columns: 24px 1fr 24px;
|
||||
}
|
||||
|
||||
.c30 {
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
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;
|
||||
padding: 0.1rem 0.5rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.c40 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
padding: 4px 4px;
|
||||
}
|
||||
|
||||
.c31 {
|
||||
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;
|
||||
position: absolute;
|
||||
width: calc(100%);
|
||||
z-index: 1;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.c32 path {
|
||||
stroke: #98A1C0;
|
||||
}
|
||||
|
||||
.c34 {
|
||||
background-color: #E8ECFB;
|
||||
border-radius: 8px;
|
||||
display: grid;
|
||||
font-size: 12px;
|
||||
grid-gap: 4px;
|
||||
grid-auto-flow: column;
|
||||
-webkit-box-pack: start;
|
||||
-webkit-justify-content: start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: start;
|
||||
padding: 4px 6px 4px 4px;
|
||||
z-index: 1020;
|
||||
}
|
||||
|
||||
.c35 {
|
||||
background-color: #B8C0DC;
|
||||
border-radius: 4px;
|
||||
color: #7780A0;
|
||||
font-size: 10px;
|
||||
padding: 2px 4px;
|
||||
z-index: 1021;
|
||||
}
|
||||
|
||||
.c36 {
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
.c23 {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.c23:hover {
|
||||
-webkit-filter: brightness(1.3);
|
||||
filter: brightness(1.3);
|
||||
}
|
||||
|
||||
.c24 {
|
||||
line-height: 1rem;
|
||||
color: #40B66B;
|
||||
}
|
||||
|
||||
.c19 {
|
||||
padding: 12px 8px 12px 12px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid #D2D9EE;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.c25 {
|
||||
margin-left: 8px;
|
||||
height: 20px;
|
||||
stroke-width: 2px;
|
||||
-webkit-transition: -webkit-transform 0.1s;
|
||||
-webkit-transition: transform 0.1s;
|
||||
transition: transform 0.1s;
|
||||
-webkit-transform: none;
|
||||
-ms-transform: none;
|
||||
transform: none;
|
||||
stroke: #98A1C0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.c25:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.c8 {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
@@ -374,30 +120,12 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
|
||||
-webkit-flex-wrap: wrap;
|
||||
-ms-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
padding: 8px 0;
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
width: 100%;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
border-radius: inherit;
|
||||
padding: 8px 12px;
|
||||
margin-top: 0;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.c13 {
|
||||
padding: 12px;
|
||||
border: 1px solid #D2D9EE;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
padding: 0;
|
||||
-webkit-align-items: center;
|
||||
@@ -416,384 +144,145 @@ exports[`SwapDetailsDropdown.tsx renders a trade 1`] = `
|
||||
transition: transform 0.1s linear;
|
||||
}
|
||||
|
||||
@supports (-webkit-background-clip:text) and (-webkit-text-fill-color:transparent) {
|
||||
.c24 {
|
||||
background-image: linear-gradient(90deg,#2172e5 0%,#54e521 163.16%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.c11 {
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
border: 1px solid #D2D9EE;
|
||||
border-radius: 16px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0 c1 c2"
|
||||
style="margin-top: 0px;"
|
||||
class="c0 c1"
|
||||
>
|
||||
<div
|
||||
class="c3"
|
||||
style="width: 100%; margin-bottom: -8px;"
|
||||
class="c2 c3 c4 c5"
|
||||
data-testid="swap-details-header-row"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c4 c5"
|
||||
data-testid="swap-details-header-row"
|
||||
class="c2 c3 c6"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c6"
|
||||
style="position: relative;"
|
||||
class="c7"
|
||||
data-testid="trade-price-container"
|
||||
>
|
||||
<div
|
||||
class="c7"
|
||||
data-testid="trade-price-container"
|
||||
<button
|
||||
class="c8"
|
||||
title="1 DEF = 1.00 ABC "
|
||||
>
|
||||
<button
|
||||
class="c8"
|
||||
title="1 DEF = 1.00 ABC "
|
||||
<div
|
||||
class="c9 css-zhpkf8"
|
||||
>
|
||||
<div
|
||||
class="c9 css-zhpkf8"
|
||||
>
|
||||
1 DEF = 1.00 ABC
|
||||
</div>
|
||||
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1 c6"
|
||||
>
|
||||
<svg
|
||||
class="c10"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="#98A1C0"
|
||||
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>
|
||||
1 DEF = 1.00 ABC
|
||||
</div>
|
||||
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style="height: 0px; overflow: hidden; width: 100%; will-change: height;"
|
||||
class="c2 c3 c6"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
class="c10"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="#98A1C0"
|
||||
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
|
||||
style="height: 0px; overflow: hidden; width: 100%; will-change: height;"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c11"
|
||||
data-testid="advanced-swap-details"
|
||||
>
|
||||
<div
|
||||
class="c3"
|
||||
style="padding: 0px 0px 8px 0px;"
|
||||
class="c12"
|
||||
>
|
||||
<div
|
||||
class="c0 c11 c12 c13"
|
||||
data-testid="advanced-swap-details"
|
||||
class="c13"
|
||||
/>
|
||||
<div
|
||||
class="c2 c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c0 c11 c14"
|
||||
class="c2 c3 c6"
|
||||
>
|
||||
<div
|
||||
class="c3"
|
||||
class="c14"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c4"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c0 c1 c6"
|
||||
class="c15 css-zhpkf8"
|
||||
>
|
||||
<div
|
||||
class="c15"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="css-zhpkf8"
|
||||
>
|
||||
Expected Output
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c9 css-q4yjm0"
|
||||
>
|
||||
0.000000000000001 DEF
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1 c4"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c6"
|
||||
>
|
||||
<div
|
||||
class="c15"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="css-zhpkf8"
|
||||
>
|
||||
Price Impact
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c9 css-q4yjm0"
|
||||
>
|
||||
<div
|
||||
class="c16 css-1aekuku"
|
||||
>
|
||||
105567.37%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c17"
|
||||
/>
|
||||
<div
|
||||
class="c0 c1 c4"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c6"
|
||||
style="margin-right: 20px;"
|
||||
>
|
||||
<div
|
||||
class="c15"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="css-zhpkf8"
|
||||
>
|
||||
Minimum received after slippage (2.00%)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-q4yjm0"
|
||||
>
|
||||
0.00000000000000098 DEF
|
||||
Minimum output
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c9 css-zhpkf8"
|
||||
>
|
||||
0.00000000000000098 DEF
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c18 c19"
|
||||
data-testid="swap-route-info"
|
||||
class="c2 c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c4"
|
||||
class="c2 c3 c6"
|
||||
>
|
||||
<div
|
||||
class="c20 c21 c22"
|
||||
width="auto"
|
||||
class="c14"
|
||||
>
|
||||
<svg
|
||||
class="c23"
|
||||
fill="none"
|
||||
height="20"
|
||||
viewBox="0 0 23 20"
|
||||
width="23"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<lineargradient
|
||||
gradientTransform="rotate(95)"
|
||||
id="AutoRouterIconGradient0"
|
||||
x1="0"
|
||||
x2="1"
|
||||
y1="0"
|
||||
y2="0"
|
||||
>
|
||||
<stop
|
||||
id="stop1"
|
||||
offset="0"
|
||||
stop-color="#2274E2"
|
||||
/>
|
||||
<stop
|
||||
id="stop1"
|
||||
offset="0.5"
|
||||
stop-color="#2274E2"
|
||||
/>
|
||||
<stop
|
||||
id="stop2"
|
||||
offset="1"
|
||||
stop-color="#3FB672"
|
||||
/>
|
||||
</lineargradient>
|
||||
</defs>
|
||||
<path
|
||||
d="M16 16C10 16 9 10 5 10M16 16C16 17.6569 17.3431 19 19 19C20.6569 19 22 17.6569 22 16C22 14.3431 20.6569 13 19 13C17.3431 13 16 14.3431 16 16ZM5 10C9 10 10 4 16 4M5 10H1.5M16 4C16 5.65685 17.3431 7 19 7C20.6569 7 22 5.65685 22 4C22 2.34315 20.6569 1 19 1C17.3431 1 16 2.34315 16 4Z"
|
||||
stroke="url(#AutoRouterIconGradient0)"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
class="c9 c24 css-1aekuku"
|
||||
>
|
||||
Auto Router
|
||||
<div>
|
||||
<div
|
||||
class="c15 css-zhpkf8"
|
||||
>
|
||||
Expected output
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
class="c25"
|
||||
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"
|
||||
>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="5"
|
||||
y2="19"
|
||||
/>
|
||||
<line
|
||||
x1="5"
|
||||
x2="19"
|
||||
y1="12"
|
||||
y2="12"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
style="height: 0px; overflow: hidden; width: 100%; will-change: height;"
|
||||
class="c9 css-zhpkf8"
|
||||
>
|
||||
0.000000000000001 DEF
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c13"
|
||||
/>
|
||||
<div
|
||||
class="c2 c3 c4"
|
||||
>
|
||||
<div
|
||||
class="c15 css-zhpkf8"
|
||||
>
|
||||
Order routing
|
||||
</div>
|
||||
<div
|
||||
class="c14"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c20 c21 c22"
|
||||
style="padding-top: 12px; margin: 0px;"
|
||||
width="auto"
|
||||
class="c9 css-zhpkf8"
|
||||
>
|
||||
<div
|
||||
class="c26 css-vurnku"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c27"
|
||||
>
|
||||
<div
|
||||
class="c28"
|
||||
>
|
||||
<img
|
||||
alt="ABC logo"
|
||||
class="c29"
|
||||
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x0000000000000000000000000000000000000001/logo.png"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1 c30"
|
||||
>
|
||||
<div
|
||||
class="c31"
|
||||
>
|
||||
<svg
|
||||
class="c32"
|
||||
>
|
||||
dot_line.svg
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="c33 c34"
|
||||
>
|
||||
<div
|
||||
class="c33 c35"
|
||||
>
|
||||
<div
|
||||
class="c36 css-15li2d9"
|
||||
>
|
||||
V3
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c36 css-1aekuku"
|
||||
style="min-width: auto;"
|
||||
>
|
||||
100%
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c37 c38 c39"
|
||||
style="justify-content: space-evenly; z-index: 2;"
|
||||
width="100%"
|
||||
>
|
||||
<div
|
||||
class="c15"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c33 c40"
|
||||
>
|
||||
<div
|
||||
class="css-mbnpt3"
|
||||
>
|
||||
<div
|
||||
class="c41"
|
||||
>
|
||||
<div
|
||||
class="c42"
|
||||
>
|
||||
<div
|
||||
class="c28"
|
||||
>
|
||||
<img
|
||||
alt="DEF logo"
|
||||
class="c29"
|
||||
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x0000000000000000000000000000000000000002/logo.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c43"
|
||||
>
|
||||
<div
|
||||
class="c28"
|
||||
>
|
||||
<img
|
||||
alt="ABC logo"
|
||||
class="c29"
|
||||
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x0000000000000000000000000000000000000001/logo.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-1aekuku"
|
||||
>
|
||||
1%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c28"
|
||||
>
|
||||
<img
|
||||
alt="DEF logo"
|
||||
class="c29"
|
||||
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x0000000000000000000000000000000000000002/logo.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c17"
|
||||
/>
|
||||
<div
|
||||
class="c44 css-65u4ng"
|
||||
>
|
||||
This route optimizes your total output by considering split routes, multiple hops, and the gas cost of each step.
|
||||
</div>
|
||||
Uniswap API
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -81,7 +81,7 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
|
||||
margin: -0px;
|
||||
}
|
||||
|
||||
.c21 {
|
||||
.c22 {
|
||||
width: -webkit-fit-content;
|
||||
width: -moz-fit-content;
|
||||
width: fit-content;
|
||||
@@ -91,11 +91,11 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
|
||||
color: #0D111C;
|
||||
}
|
||||
|
||||
.c26 {
|
||||
.c24 {
|
||||
color: #7780A0;
|
||||
}
|
||||
|
||||
.c24 {
|
||||
.c21 {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: #D2D9EE;
|
||||
@@ -118,6 +118,21 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
|
||||
background-color: #F5F6FC;
|
||||
}
|
||||
|
||||
.c20 {
|
||||
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;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
display: grid;
|
||||
grid-auto-rows: auto;
|
||||
@@ -152,7 +167,7 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
|
||||
background-size: 400%;
|
||||
}
|
||||
|
||||
.c22 {
|
||||
.c23 {
|
||||
display: inline-block;
|
||||
height: inherit;
|
||||
}
|
||||
@@ -205,17 +220,12 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
|
||||
-webkit-flex-wrap: wrap;
|
||||
-ms-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
padding: 8px 0;
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.c23 {
|
||||
color: #7780A0;
|
||||
}
|
||||
|
||||
.c10 {
|
||||
text-overflow: ellipsis;
|
||||
max-width: 220px;
|
||||
@@ -223,10 +233,6 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.c20 {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.c15 {
|
||||
padding: 4px;
|
||||
border-radius: 12px;
|
||||
@@ -415,89 +421,79 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
|
||||
style="padding: .75rem; margin-top: 0.5rem;"
|
||||
>
|
||||
<div
|
||||
class="c5 c19 c20"
|
||||
class="c20"
|
||||
>
|
||||
<div
|
||||
class="c4"
|
||||
class="c21"
|
||||
/>
|
||||
<div
|
||||
class="c5 c6 c7"
|
||||
>
|
||||
<div
|
||||
class="c5 c6 c7"
|
||||
class="c5 c6 c22"
|
||||
>
|
||||
<div
|
||||
class="c5 c6 c21"
|
||||
class="c23"
|
||||
>
|
||||
<div
|
||||
class="c22"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="css-zhpkf8"
|
||||
>
|
||||
Expected Output
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="c24 css-zhpkf8"
|
||||
>
|
||||
Minimum output
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c18 css-q4yjm0"
|
||||
>
|
||||
0.000000000000001 DEF
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c5 c6 c7"
|
||||
>
|
||||
<div
|
||||
class="c5 c6 c21"
|
||||
>
|
||||
<div
|
||||
class="c22"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="css-zhpkf8"
|
||||
>
|
||||
Price Impact
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c18 css-q4yjm0"
|
||||
>
|
||||
<div
|
||||
class="c23 css-1aekuku"
|
||||
>
|
||||
105567.37%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c24"
|
||||
/>
|
||||
class="c18 css-zhpkf8"
|
||||
>
|
||||
0.00000000000000098 DEF
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c5 c6 c7"
|
||||
>
|
||||
<div
|
||||
class="c5 c6 c7"
|
||||
class="c5 c6 c22"
|
||||
>
|
||||
<div
|
||||
class="c5 c6 c21"
|
||||
style="margin-right: 20px;"
|
||||
class="c23"
|
||||
>
|
||||
<div
|
||||
class="c22"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="css-zhpkf8"
|
||||
>
|
||||
Minimum received after slippage (2.00%)
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="c24 css-zhpkf8"
|
||||
>
|
||||
Expected output
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="css-q4yjm0"
|
||||
>
|
||||
0.00000000000000098 DEF
|
||||
</div>
|
||||
<div
|
||||
class="c18 css-zhpkf8"
|
||||
>
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -508,7 +504,7 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
|
||||
style="padding: .75rem 1rem;"
|
||||
>
|
||||
<div
|
||||
class="c26 css-k51stg"
|
||||
class="c24 css-k51stg"
|
||||
style="width: 100%;"
|
||||
>
|
||||
Output is estimated. You will receive at least
|
||||
@@ -524,7 +520,7 @@ exports[`SwapModalHeader.tsx matches base snapshot for test trade with exact inp
|
||||
style="padding: 12px 0px 0px 0px;"
|
||||
>
|
||||
<div
|
||||
class="c26 css-8mokm4"
|
||||
class="c24 css-8mokm4"
|
||||
>
|
||||
Output will be sent to
|
||||
<b
|
||||
|
||||
221
src/components/swap/__snapshots__/SwapSkeleton.test.tsx.snap
Normal file
221
src/components/swap/__snapshots__/SwapSkeleton.test.tsx.snap
Normal file
@@ -0,0 +1,221 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SwapSkeleton.tsx renders a skeleton 1`] = `
|
||||
<DocumentFragment>
|
||||
.c2 {
|
||||
color: #0D111C;
|
||||
}
|
||||
|
||||
.c9 {
|
||||
border-radius: 12px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
position: relative;
|
||||
margin-top: -18px;
|
||||
margin-bottom: -18px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: #E8ECFB;
|
||||
border: 4px solid;
|
||||
border-color: #FFFFFF;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.c11 {
|
||||
display: -webkit-inline-box;
|
||||
display: -webkit-inline-flex;
|
||||
display: -ms-inline-flexbox;
|
||||
display: inline-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;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.c10 {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
-webkit-transform: translate(-50%,-50%);
|
||||
-ms-transform: translate(-50%,-50%);
|
||||
transform: translate(-50%,-50%);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
border: 1px solid #D2D9EE;
|
||||
border-radius: 16px;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
background-color: #F5F6FC;
|
||||
border-radius: 4px;
|
||||
height: 56px;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
background-color: #F5F6FC;
|
||||
border-radius: 16px;
|
||||
height: 56px;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.c12 {
|
||||
background-color: #F5F6FC;
|
||||
border-radius: 16px;
|
||||
height: 56px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
background-color: #D2D9EE;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
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: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-flow: column;
|
||||
-ms-flex-flow: column;
|
||||
flex-flow: column;
|
||||
background-color: #F5F6FC;
|
||||
border-radius: 16px;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
padding: 48px 12px;
|
||||
}
|
||||
|
||||
.c8 {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0"
|
||||
>
|
||||
<div
|
||||
class="c1"
|
||||
>
|
||||
<div
|
||||
class="c2 css-rjqmed"
|
||||
>
|
||||
Swap
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c3"
|
||||
>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="c5 c6"
|
||||
width="60"
|
||||
/>
|
||||
<div
|
||||
class="c7 c6"
|
||||
radius="16"
|
||||
width="100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c8"
|
||||
>
|
||||
<div
|
||||
class="c9 c10"
|
||||
>
|
||||
<div
|
||||
class="c11"
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
height="16"
|
||||
stroke="#98A1C0"
|
||||
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>
|
||||
<div
|
||||
class="c3"
|
||||
>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="c5 c6"
|
||||
width="60"
|
||||
/>
|
||||
<div
|
||||
class="c7 c6"
|
||||
radius="16"
|
||||
width="100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c12"
|
||||
radius="16"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -1,4 +1,3 @@
|
||||
import { TooltipContainer } from 'components/Tooltip'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { transparentize } from 'polished'
|
||||
import { ReactNode } from 'react'
|
||||
@@ -31,7 +30,7 @@ export const SwapWrapper = styled.main<{ chainId: number | undefined }>`
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
padding: 8px;
|
||||
box-shadow: ${({ chainId }) => !!chainId && chainId === SupportedChainId.BNB && '0px 40px 120px 0px #f0b90b29'};
|
||||
z-index: ${Z_INDEX.deprecated_content};
|
||||
z-index: ${Z_INDEX.default};
|
||||
transition: transform 250ms ease;
|
||||
|
||||
&:hover {
|
||||
@@ -64,17 +63,6 @@ export const ArrowWrapper = styled.div<{ clickable: boolean }>`
|
||||
: null}
|
||||
`
|
||||
|
||||
export const ErrorText = styled(Text)<{ severity?: 0 | 1 | 2 | 3 | 4 }>`
|
||||
color: ${({ theme, severity }) =>
|
||||
severity === 3 || severity === 4
|
||||
? theme.accentFailure
|
||||
: severity === 2
|
||||
? theme.deprecated_yellow2
|
||||
: severity === 1
|
||||
? theme.textPrimary
|
||||
: theme.textSecondary};
|
||||
`
|
||||
|
||||
export const TruncatedText = styled(Text)`
|
||||
text-overflow: ellipsis;
|
||||
max-width: 220px;
|
||||
@@ -151,15 +139,3 @@ export const SwapShowAcceptChanges = styled(AutoColumn)`
|
||||
border-radius: 12px;
|
||||
margin-top: 8px;
|
||||
`
|
||||
|
||||
export const ResponsiveTooltipContainer = styled(TooltipContainer)<{ origin?: string; width?: string }>`
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
border: 1px solid ${({ theme }) => theme.backgroundInteractive};
|
||||
padding: 1rem;
|
||||
width: ${({ width }) => width ?? 'auto'};
|
||||
|
||||
${({ theme, origin }) => theme.deprecated_mediaWidth.deprecated_upToExtraSmall`
|
||||
transform: scale(0.8);
|
||||
transform-origin: ${origin ?? 'top left'};
|
||||
`}
|
||||
`
|
||||
|
||||
@@ -3,7 +3,8 @@ export const UNI_EXTENDED_LIST = 'https://gateway.ipfs.io/ipns/extendedtokens.un
|
||||
const UNI_UNSUPPORTED_LIST = 'https://gateway.ipfs.io/ipns/unsupportedtokens.uniswap.org'
|
||||
const AAVE_LIST = 'tokenlist.aave.eth'
|
||||
const BA_LIST = 'https://raw.githubusercontent.com/The-Blockchain-Association/sec-notice-list/master/ba-sec-list.json'
|
||||
const CMC_ALL_LIST = 'https://s3.coinmarketcap.com/generated/dex/tokens/eth-tokens-all.json'
|
||||
// TODO(INFRA-179): Re-enable CMC list once we have a better solution for handling large lists.
|
||||
// const CMC_ALL_LIST = 'https://s3.coinmarketcap.com/generated/dex/tokens/eth-tokens-all.json'
|
||||
const COINGECKO_LIST = 'https://tokens.coingecko.com/uniswap/all.json'
|
||||
const COINGECKO_BNB_LIST = 'https://tokens.coingecko.com/binance-smart-chain/all.json'
|
||||
const COINGECKO_ARBITRUM_LIST = 'https://tokens.coingecko.com/arbitrum-one/all.json'
|
||||
@@ -29,7 +30,7 @@ export const DEFAULT_INACTIVE_LIST_URLS: string[] = [
|
||||
UNI_EXTENDED_LIST,
|
||||
COMPOUND_LIST,
|
||||
AAVE_LIST,
|
||||
CMC_ALL_LIST,
|
||||
// CMC_ALL_LIST,
|
||||
COINGECKO_LIST,
|
||||
COINGECKO_BNB_LIST,
|
||||
COINGECKO_ARBITRUM_LIST,
|
||||
|
||||
@@ -3,6 +3,8 @@ import JSBI from 'jsbi'
|
||||
|
||||
export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
|
||||
|
||||
// TODO(WEB-3290): Convert the deadline to minutes and remove unecessary conversions from
|
||||
// seconds to minutes in the codebase.
|
||||
// 30 minutes, denominated in seconds
|
||||
export const DEFAULT_DEADLINE_FROM_NOW = 60 * 30
|
||||
export const L2_DEADLINE_FROM_NOW = 60 * 5
|
||||
|
||||
@@ -6,5 +6,4 @@ export enum FeatureFlag {
|
||||
permit2 = 'permit2',
|
||||
fiatOnRampButtonOnSwap = 'fiat_on_ramp_button_on_swap_page',
|
||||
detailsV2 = 'details_v2',
|
||||
removeWidget = 'remove_widget_tdp',
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
|
||||
|
||||
export function useWidgetRemovalFlag(): BaseVariant {
|
||||
return useBaseFlag(FeatureFlag.removeWidget, BaseVariant.Control)
|
||||
}
|
||||
|
||||
export function useWidgetRemovalEnabled(): boolean {
|
||||
return useWidgetRemovalFlag() === BaseVariant.Enabled
|
||||
}
|
||||
|
||||
export { BaseVariant as WidgetRemovalVariant }
|
||||
23
src/graphql/data/nft/NftUniversalRouterAddress.ts
Normal file
23
src/graphql/data/nft/NftUniversalRouterAddress.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import { useNftUniversalRouterAddressQuery } from '../__generated__/types-and-hooks'
|
||||
|
||||
gql`
|
||||
query NftUniversalRouterAddress($chain: Chain = ETHEREUM) {
|
||||
nftRoute(chain: $chain, senderAddress: "", nftTrades: []) {
|
||||
toAddress
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export function useNftUniversalRouterAddress() {
|
||||
const { data, loading } = useNftUniversalRouterAddressQuery({
|
||||
// no cache because a different version of nftRoute query is going to be called around the same time
|
||||
fetchPolicy: 'no-cache',
|
||||
})
|
||||
|
||||
return {
|
||||
universalRouterAddress: data?.nftRoute?.toAddress,
|
||||
universalRouterAddressIsLoading: loading,
|
||||
}
|
||||
}
|
||||
@@ -99,6 +99,7 @@ fragment TransactionParts on Transaction {
|
||||
status
|
||||
to
|
||||
from
|
||||
nonce
|
||||
}
|
||||
|
||||
fragment AssetActivityParts on AssetActivity {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { isL2ChainId } from 'utils/chains'
|
||||
|
||||
import { useAllLists, useCombinedActiveList, useCombinedTokenMapFromUrls } from '../state/lists/hooks'
|
||||
import { WrappedTokenInfo } from '../state/lists/wrappedTokenInfo'
|
||||
import { deserializeToken, useUserAddedTokens, useUserAddedTokensOnChain } from '../state/user/hooks'
|
||||
import { deserializeToken, useUserAddedTokens } from '../state/user/hooks'
|
||||
import { useUnsupportedTokenList } from './../state/lists/hooks'
|
||||
|
||||
type Maybe<T> = T | null | undefined
|
||||
@@ -182,20 +182,6 @@ export function useIsUserAddedToken(currency: Currency | undefined | null): bool
|
||||
return !!userAddedTokens.find((token) => currency.equals(token))
|
||||
}
|
||||
|
||||
// Check if currency on specific chain is included in custom list from user storage
|
||||
export function useIsUserAddedTokenOnChain(
|
||||
address: string | undefined | null,
|
||||
chain: number | undefined | null
|
||||
): boolean {
|
||||
const userAddedTokens = useUserAddedTokensOnChain(chain)
|
||||
|
||||
if (!address || !chain) {
|
||||
return false
|
||||
}
|
||||
|
||||
return !!userAddedTokens.find((token) => token.address === address)
|
||||
}
|
||||
|
||||
// undefined if invalid or does not exist
|
||||
// null if loading or null was passed
|
||||
// otherwise returns the token
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CurrencyAmount, TradeType } from '@uniswap/sdk-core'
|
||||
import { DAI, USDC_MAINNET } from 'constants/tokens'
|
||||
import { RouterPreference } from 'state/routing/slice'
|
||||
import { TradeState } from 'state/routing/types'
|
||||
import { useClientSideRouter } from 'state/user/hooks'
|
||||
import { useRouterPreference } from 'state/user/hooks'
|
||||
import { mocked } from 'test-utils/mocked'
|
||||
|
||||
import { useRoutingAPITrade } from '../state/routing/useRoutingAPITrade'
|
||||
@@ -38,7 +38,7 @@ beforeEach(() => {
|
||||
|
||||
mocked(useIsWindowVisible).mockReturnValue(true)
|
||||
mocked(useAutoRouterSupported).mockReturnValue(true)
|
||||
mocked(useClientSideRouter).mockReturnValue([true, () => undefined])
|
||||
mocked(useRouterPreference).mockReturnValue([RouterPreference.CLIENT, () => undefined])
|
||||
})
|
||||
|
||||
describe('#useBestV3Trade ExactIn', () => {
|
||||
|
||||
@@ -2,10 +2,9 @@ import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
|
||||
import { useMemo } from 'react'
|
||||
import { RouterPreference } from 'state/routing/slice'
|
||||
import { InterfaceTrade, TradeState } from 'state/routing/types'
|
||||
import { useRoutingAPITrade } from 'state/routing/useRoutingAPITrade'
|
||||
import { useClientSideRouter } from 'state/user/hooks'
|
||||
import { useRouterPreference } from 'state/user/hooks'
|
||||
|
||||
import useAutoRouterSupported from './useAutoRouterSupported'
|
||||
import { useClientSideV3Trade } from './useClientSideV3Trade'
|
||||
@@ -46,12 +45,12 @@ export function useBestTrade(
|
||||
|
||||
const shouldGetTrade = !isAWrapTransaction && isWindowVisible
|
||||
|
||||
const [clientSideRouter] = useClientSideRouter()
|
||||
const [routerPreference] = useRouterPreference()
|
||||
const routingAPITrade = useRoutingAPITrade(
|
||||
tradeType,
|
||||
autoRouterSupported && shouldGetTrade ? debouncedAmount : undefined,
|
||||
debouncedOtherCurrency,
|
||||
clientSideRouter ? RouterPreference.CLIENT : RouterPreference.API
|
||||
routerPreference
|
||||
)
|
||||
|
||||
const isLoading = routingAPITrade.state === TradeState.LOADING
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useWeb3React } from '@web3-react/core'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { RouterPreference } from 'state/routing/slice'
|
||||
import { INTERNAL_ROUTER_PREFERENCE_PRICE } from 'state/routing/slice'
|
||||
import { useRoutingAPITrade } from 'state/routing/useRoutingAPITrade'
|
||||
|
||||
import { CUSD_CELO, DAI_OPTIMISM, USDC_ARBITRUM, USDC_MAINNET, USDC_POLYGON, USDT_BSC } from '../constants/tokens'
|
||||
@@ -28,7 +28,7 @@ export default function useStablecoinPrice(currency?: Currency): Price<Currency,
|
||||
const amountOut = chainId ? STABLECOIN_AMOUNT_OUT[chainId] : undefined
|
||||
const stablecoin = amountOut?.currency
|
||||
|
||||
const { trade } = useRoutingAPITrade(TradeType.EXACT_OUTPUT, amountOut, currency, RouterPreference.PRICE)
|
||||
const { trade } = useRoutingAPITrade(TradeType.EXACT_OUTPUT, amountOut, currency, INTERNAL_ROUTER_PREFERENCE_PRICE)
|
||||
const price = useMemo(() => {
|
||||
if (!currency || !stablecoin) {
|
||||
return undefined
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Currency, CurrencyAmount, Price, SupportedChainId, TradeType } from '@u
|
||||
import { nativeOnChain } from 'constants/tokens'
|
||||
import { Chain, useTokenSpotPriceQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { chainIdToBackendName, isGqlSupportedChain, PollingInterval } from 'graphql/data/util'
|
||||
import { RouterPreference } from 'state/routing/slice'
|
||||
import { INTERNAL_ROUTER_PREFERENCE_PRICE } from 'state/routing/slice'
|
||||
import { TradeState } from 'state/routing/types'
|
||||
import { useRoutingAPITrade } from 'state/routing/useRoutingAPITrade'
|
||||
import { getNativeTokenDBAddress } from 'utils/nativeTokens'
|
||||
@@ -30,7 +30,7 @@ function useETHValue(currencyAmount?: CurrencyAmount<Currency>): {
|
||||
TradeType.EXACT_OUTPUT,
|
||||
amountOut,
|
||||
currencyAmount?.currency,
|
||||
RouterPreference.PRICE
|
||||
INTERNAL_ROUTER_PREFERENCE_PRICE
|
||||
)
|
||||
|
||||
// Get ETH value of ETH or WETH
|
||||
|
||||
@@ -52,77 +52,73 @@ export function useUniversalRouterSwapCallback(
|
||||
const analyticsContext = useTrace()
|
||||
|
||||
return useCallback(async (): Promise<TransactionResponse> => {
|
||||
return trace(
|
||||
'swap.send',
|
||||
async ({ setTraceData, setTraceStatus, setTraceError }) => {
|
||||
return trace('swap.send', async ({ setTraceData, setTraceStatus, setTraceError }) => {
|
||||
try {
|
||||
if (!account) throw new Error('missing account')
|
||||
if (!chainId) throw new Error('missing chainId')
|
||||
if (!provider) throw new Error('missing provider')
|
||||
if (!trade) throw new Error('missing trade')
|
||||
|
||||
setTraceData('slippageTolerance', options.slippageTolerance.toFixed(2))
|
||||
const { calldata: data, value } = SwapRouter.swapERC20CallParameters(trade, {
|
||||
slippageTolerance: options.slippageTolerance,
|
||||
deadlineOrPreviousBlockhash: options.deadline?.toString(),
|
||||
inputTokenPermit: options.permit,
|
||||
fee: options.feeOptions,
|
||||
})
|
||||
const tx = {
|
||||
from: account,
|
||||
to: UNIVERSAL_ROUTER_ADDRESS(chainId),
|
||||
data,
|
||||
// TODO(https://github.com/Uniswap/universal-router-sdk/issues/113): universal-router-sdk returns a non-hexlified value.
|
||||
...(value && !isZero(value) ? { value: toHex(value) } : {}),
|
||||
}
|
||||
|
||||
let gasEstimate: BigNumber
|
||||
try {
|
||||
if (!account) throw new Error('missing account')
|
||||
if (!chainId) throw new Error('missing chainId')
|
||||
if (!provider) throw new Error('missing provider')
|
||||
if (!trade) throw new Error('missing trade')
|
||||
|
||||
setTraceData('slippageTolerance', options.slippageTolerance.toFixed(2))
|
||||
const { calldata: data, value } = SwapRouter.swapERC20CallParameters(trade, {
|
||||
slippageTolerance: options.slippageTolerance,
|
||||
deadlineOrPreviousBlockhash: options.deadline?.toString(),
|
||||
inputTokenPermit: options.permit,
|
||||
fee: options.feeOptions,
|
||||
})
|
||||
const tx = {
|
||||
from: account,
|
||||
to: UNIVERSAL_ROUTER_ADDRESS(chainId),
|
||||
data,
|
||||
// TODO(https://github.com/Uniswap/universal-router-sdk/issues/113): universal-router-sdk returns a non-hexlified value.
|
||||
...(value && !isZero(value) ? { value: toHex(value) } : {}),
|
||||
}
|
||||
|
||||
let gasEstimate: BigNumber
|
||||
try {
|
||||
gasEstimate = await provider.estimateGas(tx)
|
||||
} catch (gasError) {
|
||||
setTraceStatus('failed_precondition')
|
||||
setTraceError(gasError)
|
||||
console.warn(gasError)
|
||||
throw new GasEstimationError()
|
||||
}
|
||||
const gasLimit = calculateGasMargin(gasEstimate)
|
||||
setTraceData('gasLimit', gasLimit.toNumber())
|
||||
const response = await provider
|
||||
.getSigner()
|
||||
.sendTransaction({ ...tx, gasLimit })
|
||||
.then((response) => {
|
||||
sendAnalyticsEvent(SwapEventName.SWAP_SIGNED, {
|
||||
...formatSwapSignedAnalyticsEventProperties({
|
||||
trade,
|
||||
fiatValues,
|
||||
txHash: response.hash,
|
||||
}),
|
||||
gasEstimate = await provider.estimateGas(tx)
|
||||
} catch (gasError) {
|
||||
setTraceStatus('failed_precondition')
|
||||
setTraceError(gasError)
|
||||
console.warn(gasError)
|
||||
throw new GasEstimationError()
|
||||
}
|
||||
const gasLimit = calculateGasMargin(gasEstimate)
|
||||
setTraceData('gasLimit', gasLimit.toNumber())
|
||||
const response = await provider
|
||||
.getSigner()
|
||||
.sendTransaction({ ...tx, gasLimit })
|
||||
.then((response) => {
|
||||
sendAnalyticsEvent(SwapEventName.SWAP_SIGNED, {
|
||||
...formatSwapSignedAnalyticsEventProperties({
|
||||
trade,
|
||||
fiatValues,
|
||||
txHash: response.hash,
|
||||
}),
|
||||
...analyticsContext,
|
||||
})
|
||||
if (tx.data !== response.data) {
|
||||
sendAnalyticsEvent(SwapEventName.SWAP_MODIFIED_IN_WALLET, {
|
||||
txHash: response.hash,
|
||||
...analyticsContext,
|
||||
})
|
||||
if (tx.data !== response.data) {
|
||||
sendAnalyticsEvent(SwapEventName.SWAP_MODIFIED_IN_WALLET, {
|
||||
txHash: response.hash,
|
||||
...analyticsContext,
|
||||
})
|
||||
throw new ModifiedSwapError()
|
||||
}
|
||||
return response
|
||||
})
|
||||
return response
|
||||
} catch (swapError: unknown) {
|
||||
if (swapError instanceof ModifiedSwapError) throw swapError
|
||||
throw new ModifiedSwapError()
|
||||
}
|
||||
return response
|
||||
})
|
||||
return response
|
||||
} catch (swapError: unknown) {
|
||||
if (swapError instanceof ModifiedSwapError) throw swapError
|
||||
|
||||
// Cancellations are not failures, and must be accounted for as 'cancelled'.
|
||||
if (didUserReject(swapError)) setTraceStatus('cancelled')
|
||||
// Cancellations are not failures, and must be accounted for as 'cancelled'.
|
||||
if (didUserReject(swapError)) setTraceStatus('cancelled')
|
||||
|
||||
// GasEstimationErrors are already traced when they are thrown.
|
||||
if (!(swapError instanceof GasEstimationError)) setTraceError(swapError)
|
||||
// GasEstimationErrors are already traced when they are thrown.
|
||||
if (!(swapError instanceof GasEstimationError)) setTraceError(swapError)
|
||||
|
||||
throw new Error(swapErrorToUserReadableMessage(swapError))
|
||||
}
|
||||
},
|
||||
{ tags: { is_widget: false } }
|
||||
)
|
||||
throw new Error(swapErrorToUserReadableMessage(swapError))
|
||||
}
|
||||
})
|
||||
}, [
|
||||
account,
|
||||
analyticsContext,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
|
||||
import { useMemo } from 'react'
|
||||
import { RouterPreference } from 'state/routing/slice'
|
||||
import { INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference } from 'state/routing/slice'
|
||||
|
||||
/**
|
||||
* Returns query arguments for the Routing API query or undefined if the
|
||||
@@ -18,7 +18,7 @@ export function useRoutingAPIArguments({
|
||||
tokenOut: Currency | undefined
|
||||
amount: CurrencyAmount<Currency> | undefined
|
||||
tradeType: TradeType
|
||||
routerPreference: RouterPreference
|
||||
routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE
|
||||
}) {
|
||||
return useMemo(
|
||||
() =>
|
||||
|
||||
@@ -4,7 +4,10 @@ import { SupportedChainId } from 'constants/chains'
|
||||
import useBlockNumber, { useFastForwardBlockNumber } from 'lib/hooks/useBlockNumber'
|
||||
import ms from 'ms.macro'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { retry, RetryableError, RetryOptions } from 'utils/retry'
|
||||
import { useTransactionRemover } from 'state/transactions/hooks'
|
||||
import { TransactionDetails } from 'state/transactions/types'
|
||||
|
||||
import { retry, RetryableError, RetryOptions } from './retry'
|
||||
|
||||
interface Transaction {
|
||||
addedTime: number
|
||||
@@ -39,16 +42,17 @@ const RETRY_OPTIONS_BY_CHAIN_ID: { [chainId: number]: RetryOptions } = {
|
||||
const DEFAULT_RETRY_OPTIONS: RetryOptions = { n: 1, minWait: 0, maxWait: 0 }
|
||||
|
||||
interface UpdaterProps {
|
||||
pendingTransactions: { [hash: string]: Transaction }
|
||||
pendingTransactions: { [hash: string]: TransactionDetails }
|
||||
onCheck: (tx: { chainId: number; hash: string; blockNumber: number }) => void
|
||||
onReceipt: (tx: { chainId: number; hash: string; receipt: TransactionReceipt }) => void
|
||||
}
|
||||
|
||||
export default function Updater({ pendingTransactions, onCheck, onReceipt }: UpdaterProps): null {
|
||||
const { chainId, provider } = useWeb3React()
|
||||
const { account, chainId, provider } = useWeb3React()
|
||||
|
||||
const lastBlockNumber = useBlockNumber()
|
||||
const fastForwardBlockNumber = useFastForwardBlockNumber()
|
||||
const removeTransaction = useTransactionRemover()
|
||||
|
||||
const getReceipt = useCallback(
|
||||
(hash: string) => {
|
||||
@@ -56,8 +60,18 @@ export default function Updater({ pendingTransactions, onCheck, onReceipt }: Upd
|
||||
const retryOptions = RETRY_OPTIONS_BY_CHAIN_ID[chainId] ?? DEFAULT_RETRY_OPTIONS
|
||||
return retry(
|
||||
() =>
|
||||
provider.getTransactionReceipt(hash).then((receipt) => {
|
||||
provider.getTransactionReceipt(hash).then(async (receipt) => {
|
||||
if (receipt === null) {
|
||||
if (account) {
|
||||
const transactionCount = await provider.getTransactionCount(account)
|
||||
const tx = pendingTransactions[hash]
|
||||
// We check for the presence of a nonce because we haven't always saved them,
|
||||
// so this code may run against old store state where nonce is undefined.
|
||||
if (tx.nonce && tx.nonce < transactionCount) {
|
||||
// We remove pending transactions from redux if they are no longer the latest nonce.
|
||||
removeTransaction(hash)
|
||||
}
|
||||
}
|
||||
console.debug(`Retrying tranasaction receipt for ${hash}`)
|
||||
throw new RetryableError()
|
||||
}
|
||||
@@ -66,7 +80,7 @@ export default function Updater({ pendingTransactions, onCheck, onReceipt }: Upd
|
||||
retryOptions
|
||||
)
|
||||
},
|
||||
[chainId, provider]
|
||||
[account, chainId, pendingTransactions, provider, removeTransaction]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user