Compare commits

..

12 Commits

Author SHA1 Message Date
Charles Bachmeier
05f95531e7
fix: block ip infringing collection () 2023-11-21 09:26:38 -08:00
Kristie Huang
78cb58329e
fix: android banner DownloadButton onClick should not propagate up ()
hotfix: fix: android banner DownloadButton onClick should not propagate up 
2023-11-20 16:33:29 -05:00
Tina
0e561f80ef
fix: only change input currency to weth after eth wrap completes [hotfix] ()
fix: only change input currency to weth after eth wrap completes for uniswapx eth input trades ()

* only change input currency to weth after wrap completes

* add e2e test

* update test
2023-11-17 12:10:21 -05:00
Tina
51e3bee414
fix: don't show approve gas costs for arbitrum [hotfix] ()
fix: disable showing approve cost for arbitrum ()

disable showing approve cost for arbitrum
2023-11-15 13:40:15 -05:00
cartcrom
57fc9e1eb7
fix: don't format uniswapx wrap amount [hotfix] ()
* fix: don't format wrap input amount

* lint
2023-11-15 13:25:33 -05:00
Jack Short
f6660bef03
fix: flushing user locale redux when locale is German ()
* fix: set current redux version to 3 ()

* fix: set current redux version to 3

* fix: tests

* fix: redux migration to flush german locale ()

* fix: redux migration to flush german locale

* lint

* my linter was not workking

---------

Co-authored-by: eddie <66155195+just-toby@users.noreply.github.com>
2023-11-14 14:41:39 -05:00
Jordan Frankfurt
ae0bedf24b
fix: deep linking behavior for android ()
fix: execute deep link code on both platforms
2023-11-13 16:19:36 -06:00
Kristie Huang
206c999835
fix: disable fees and uniswapx tests + skip localStorage reads for bl… ()
fix: disable fees and uniswapx tests + skip localStorage reads for blocked addresses ()

* fix: disable fees tests

* skip uniswapx tests for now

* turn off uniswapx for classic swap test

* skip local cache reads for blocked accounts

* fix: broken pools test ()

* test: update hardhat blocknumber ()

* init

* fix: remove console log

* fix: add comment

---------

Co-authored-by: Tina <59578595+tinaszheng@users.noreply.github.com>
Co-authored-by: cartcrom <cartergcromer@gmail.com>
Co-authored-by: cartcrom <39385577+cartcrom@users.noreply.github.com>
2023-11-13 16:35:57 -05:00
Kristie Huang
3bd0b1c9be
fix: use NativeCurrency for polygon matic ()
fix: use NativeCurrency for polygon matic ()

* fix: use NativeCurrency for polygon matic

* add comment

* update snapshots??

* Revert "update snapshots??"

This reverts commit 280758be118610cc9e13afcd6e420985e8a200d2.
2023-11-13 16:29:46 -05:00
Kristie Huang
3cc7cecf6a
fix: update function tests for 404ing collections ()
fix: update function tests for 404ing collections ()

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-11-13 15:59:40 -05:00
UL Service Account
9620365349 ci: add global CODEOWNERS 2023-11-10 01:07:24 +00:00
UL Service Account
8e4e8a90ab ci(t9n): download translations from crowdin 2023-11-10 01:07:24 +00:00
167 changed files with 131151 additions and 4513 deletions
.env.env.production
.github
CODEOWNERS
cypress/e2e
package.json
public
scripts
src
assets/svg
components
connection
constants
featureFlags
graphql/data
hooks
lib
locales

10
.env

@ -1,8 +1,8 @@
# These API keys are intentionally public. Please do not report them - thank you for your concern. # These API keys are intentionally public. Please do not report them - thank you for your concern.
ESLINT_NO_DEV_ERRORS=true ESLINT_NO_DEV_ERRORS=true
REACT_APP_AMPLITUDE_PROXY_URL="https://null.null" REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
REACT_APP_AWS_API_REGION="us-east-2" REACT_APP_AWS_API_REGION="us-east-2"
REACT_APP_AWS_API_ENDPOINT="https://null.null" REACT_APP_AWS_API_ENDPOINT="https://beta.api.uniswap.org/v1/graphql"
REACT_APP_BNB_RPC_URL="https://rough-sleek-hill.bsc.quiknode.pro/413cc98cbc776cda8fdf1d0f47003583ff73d9bf" REACT_APP_BNB_RPC_URL="https://rough-sleek-hill.bsc.quiknode.pro/413cc98cbc776cda8fdf1d0f47003583ff73d9bf"
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847" REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
REACT_APP_QUICKNODE_MAINNET_RPC_URL="https://magical-alien-tab.quiknode.pro/669e87e569a8277d3fbd9e202f9df93189f19f4c" REACT_APP_QUICKNODE_MAINNET_RPC_URL="https://magical-alien-tab.quiknode.pro/669e87e569a8277d3fbd9e202f9df93189f19f4c"
@ -10,7 +10,7 @@ REACT_APP_MOONPAY_API="https://api.moonpay.com"
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkV2?platform=web&env=staging" REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkV2?platform=web&env=staging"
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_test_DycfESRid31UaSxhI5yWKe1r5E5kKSz" REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_test_DycfESRid31UaSxhI5yWKe1r5E5kKSz"
REACT_APP_SENTRY_DSN="https://a3c62e400b8748b5a8d007150e2f38b7@o1037921.ingest.sentry.io/4504255148851200" REACT_APP_SENTRY_DSN="https://a3c62e400b8748b5a8d007150e2f38b7@o1037921.ingest.sentry.io/4504255148851200"
REACT_APP_STATSIG_PROXY_URL="https://null.null" REACT_APP_STATSIG_PROXY_URL="https://api.uniswap.org/v1/statsig-proxy"
REACT_APP_TEMP_API_URL="https://null.null" REACT_APP_TEMP_API_URL="https://temp.api.uniswap.org/v1"
REACT_APP_UNISWAP_API_URL="https://null.null" REACT_APP_UNISWAP_API_URL="https://api.uniswap.org/v2"
REACT_APP_WALLET_CONNECT_PROJECT_ID="c6c9bacd35afa3eb9e6cccf6d8464395" REACT_APP_WALLET_CONNECT_PROJECT_ID="c6c9bacd35afa3eb9e6cccf6d8464395"

@ -1,16 +1,16 @@
# These API keys are intentionally public. Please do not report them - thank you for your concern. # These API keys are intentionally public. Please do not report them - thank you for your concern.
REACT_APP_AMPLITUDE_PROXY_URL="https://null.null" REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
REACT_APP_AWS_API_ENDPOINT="https://null.null" REACT_APP_AWS_API_ENDPOINT="https://api.uniswap.org/v1/graphql"
REACT_APP_BNB_RPC_URL="https://old-wispy-arrow.bsc.quiknode.pro/f5c060177236065c1058531a0615ab4f7a34a2fd" REACT_APP_BNB_RPC_URL="https://old-wispy-arrow.bsc.quiknode.pro/f5c060177236065c1058531a0615ab4f7a34a2fd"
REACT_APP_FIREBASE_KEY="AIzaSyBcZWwTcTJHj_R6ipZcrJkXdq05PuX0Rs0" REACT_APP_FIREBASE_KEY="AIzaSyBcZWwTcTJHj_R6ipZcrJkXdq05PuX0Rs0"
REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF" REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF"
REACT_APP_GOOGLE_ANALYTICS_ID="G-KDP9B6W4H8" REACT_APP_GOOGLE_ANALYTICS_ID="G-KDP9B6W4H8"
REACT_APP_INFURA_KEY="099fc58e0de9451d80b18d7c74caa7c1" REACT_APP_INFURA_KEY="099fc58e0de9451d80b18d7c74caa7c1"
REACT_APP_MOONPAY_API="https://api.moonpay.com" REACT_APP_MOONPAY_API="https://api.moonpay.com"
REACT_APP_MOONPAY_LINK="https://null.null" REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkV2?platform=web&env=production"
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_live_uQG4BJC4w3cxnqpcSqAfohdBFDTsY6E" REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_live_uQG4BJC4w3cxnqpcSqAfohdBFDTsY6E"
REACT_APP_SENTRY_ENABLED=true REACT_APP_SENTRY_ENABLED=true
REACT_APP_SENTRY_TRACES_SAMPLE_RATE=0.00003 REACT_APP_SENTRY_TRACES_SAMPLE_RATE=0.00003
REACT_APP_STATSIG_PROXY_URL="https://null.null" REACT_APP_STATSIG_PROXY_URL="https://api.uniswap.org/v1/statsig-proxy"
REACT_APP_QUICKNODE_MAINNET_RPC_URL="https://ultra-blue-flower.quiknode.pro/770b22d5f362c537bc8fe19b034c45b22958f880" REACT_APP_QUICKNODE_MAINNET_RPC_URL="https://ultra-blue-flower.quiknode.pro/770b22d5f362c537bc8fe19b034c45b22958f880"
THE_GRAPH_SCHEMA_ENDPOINT="https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3?source=uniswap" THE_GRAPH_SCHEMA_ENDPOINT="https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3?source=uniswap"

1
.github/CODEOWNERS vendored Normal file

@ -0,0 +1 @@
* @uniswap/web-reviewers

22
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file

@ -0,0 +1,22 @@
---
name: Bug Report
about: Describe an issue in the Uniswap Interface
title: ''
labels: bug
assignees: ''
---
**Bug Description**
A clear and concise description of the bug.
**Steps to Reproduce**
1. Go to ...
2. Click on ...
...
**Expected Behavior**
A clear and concise description of what you expected to happen.
**Additional Context**
Add any other context about the problem here (screenshots, whether the bug only occurs only in certain mobile/desktop/browser environments, etc.)

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file

@ -0,0 +1,8 @@
blank_issues_enabled: true
contact_links:
- name: Support
url: https://discord.gg/FCfyBSbCU5
about: Please ask and answer questions here
- name: List a token
url: https://github.com/Uniswap/default-token-list#adding-a-token
about: Any requests to add a token to Uniswap should go here

@ -0,0 +1,19 @@
---
name: Feature Request
about: Suggest an idea for improving the UX of the Uniswap Interface
title: ''
labels: 'improvement'
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

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

49
.github/actions/setup/action.yml vendored Normal file

@ -0,0 +1,49 @@
name: Setup
description: checkout repo, setup node, and install node_modules
runs:
using: composite
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
registry-url: https://registry.npmjs.org
# cache is intentionally omitted, as it is faster with yarn v1 to cache node_modules.
- 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
key: ${{ runner.os }}-install-${{ hashFiles('yarn.lock') }}
- if: steps.install-cache.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile --ignore-scripts
shell: bash
# Run patch-package to apply patches to dependencies.
- run: yarn patch-package
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
# These operations cannot be cached, so they are run concurrently
# - ajv: Validators compile quickly, so caching can be omitted.
# - graphql: GraphQL is generated from schema and client-side graphql queries. The schema is always fetched and
# changes to client-side queries are hard to detect, so it is always re-generated.
# - i18n: Messages are extracted from source and compiled. No caching extractor is available (out-of-the-box).
- run: yarn concurrently --max-processes=100% npm:ajv npm:graphql npm:i18n
shell: bash

12
.github/dependabot.yml vendored Normal file

@ -0,0 +1,12 @@
version: 2
updates:
- package-ecosystem: npm
# Files stored in repository root
directory: '/'
schedule:
interval: 'daily'
allow:
- dependency-name: '@uniswap/default-token-list'
- dependency-name: '@uniswap/token-lists'
reviewers:
- 'Uniswap/dependabot-reviewers'

52
.github/pull_request_template.md vendored Normal file

@ -0,0 +1,52 @@
<!-- Your PR title must follow conventional commits: https://github.com/Uniswap/interface#pr-title -->
## Description
<!-- Summary of change, including motivation and context. -->
<!-- Use verb-driven language: "Fixes XYZ" instead of "This change fixes XYZ" -->
<!-- Delete inapplicable lines: -->
_Linear ticket:_
_Slack thread:_
_Relevant docs:_
<!-- Delete this section if your change does not affect UI. -->
## Screen capture
### Before
| Mobile | Desktop |
| ------------ | ------------ |
| paste_before | paste_before |
### After
| Mobile | Desktop |
| ------------ | ----------- |
| paste_after | paste_after |
## Test plan
<!-- Delete this section if your change is not a bug fix. -->
### Reproducing the error
<!-- Include steps to reproduce the bug. -->
1.
### QA (ie manual testing)
<!-- Include steps to test the change, ensuring no regression. -->
- [ ] N/A
#### Devices
<!-- If applicable, include different devices and screen sizes that may be affected, and how you've tested them. -->
### Automated testing
<!-- If N/A, check and note so it is obvious to your reviewers and does not show up as an incomplete task. -->
<!-- eg - [x] Unit test N/A -->
- [ ] Unit test
- [ ] Integration/E2E test

73
.github/workflows/1-main-to-staging.yml vendored Normal file

@ -0,0 +1,73 @@
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:
- name: Check test status
uses: actions/github-script@v6.4.1
with:
script: |
const statuses = await github.rest.repos.listCommitStatusesForRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: context.sha
})
const status = statuses.data.find(status => status.context === 'Test / promotion')?.state || 'missing'
core.info('Status: ' + status)
if (status !== 'success') {
core.setFailed('"Test / promotion" must be successful before pushing')
}
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
with:
token: ${{ secrets.RELEASE_SERVICE_ACCESS_TOKEN }}
ref: main
# The source file must exist for the corresponding translation messages to be downloaded.
- run: touch src/locales/en-US.po
- name: Download translations
uses: crowdin/github-action@3133cc916c35590475cf6705f482fb653d8e36e9
with:
upload_sources: false
download_translations: true
project_id: 458284
token: ${{ secrets.CROWDIN_PERSONAL_TOKEN_SECRET }}
source: 'src/locales/en-US.po'
translation: 'src/locales/%locale%.po'
localization_branch_name: main
create_pull_request: false
push_translations: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Git config
run: |
git config user.name 'UL Service Account'
git config user.email 'hello-happy-puppy@users.noreply.github.com'
- name: Add translations
run: |
rm src/locales/en-US.po
git add -f src/locales/*.po
git commit -m 'ci(t9n): download translations from crowdin'
- name: Add CODEOWNERS
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

@ -0,0 +1,64 @@
name: 2 | Deploy staging
on:
push:
branches:
- 'releases/staging'
jobs:
deploy-to-staging:
runs-on: ubuntu-latest
environment:
name: deploy/staging
steps:
- uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
continue-on-error: true
with:
payload: |
{
"text": "Deploy _started_ for ${{ github.ref_name }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- run: yarn build
env:
REACT_APP_STAGING: 1
- name: Update Cloudflare Pages deployment
id: 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 }}
# Cloudflare uses `main` as the default production branch, so we push using the `main` branch so that it can be aliased by a custom domain.
branch: main
- uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
continue-on-error: true
if: always()
with:
payload: |
{
"text": "Deploy *${{ steps.pages-deployment.outcome }}* for ${{ github.ref_name }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
- name: Upload source maps to Sentry
uses: getsentry/action-release@bd5f874fcda966ba48139b0140fb3ec0cb3aabdd
continue-on-error: true
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'

42
.github/workflows/3-staging-to-prod.yml vendored Normal file

@ -0,0 +1,42 @@
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:
- name: Check test status
uses: actions/github-script@v6.4.1
with:
script: |
const statuses = await github.rest.repos.listCommitStatusesForRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: context.sha
})
const status = statuses.data.find(status => status.context === 'Test / promotion')?.state || 'missing'
core.info('Status: ' + status)
if (status !== 'success') {
core.setFailed('"Test / promotion" must be successful before pushing')
}
- 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

111
.github/workflows/4-deploy-to-prod.yml vendored Normal file

@ -0,0 +1,111 @@
name: 4 | Deploy prod
on:
push:
branches:
- 'releases/prod'
jobs:
deploy-to-prod:
runs-on: ubuntu-latest
environment:
name: deploy/prod
steps:
- uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
continue-on-error: true
with:
payload: |
{
"text": "Deploy _started_ for ${{ github.ref_name }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- run: yarn build
- name: Bump and tag
id: github-tag-action
uses: mathieudutour/github-tag-action@d745f2e74aaf1ee82e747b181f7a0967978abee0
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: Publish 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: Update Cloudflare Pages deployment
uses: cloudflare/pages-action@364c7ca09a4b57837c5967871d64a2c31adb8c0d
id: pages-deployment
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: ${{ secrets.CLOUDFLARE_PROJECT_NAME }}
directory: build
githubToken: ${{ secrets.GITHUB_TOKEN }}
# Cloudflare uses `main` as the default production branch, so we push using the `main` branch so that it can be aliased by a custom domain.
branch: main
- uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
continue-on-error: true
if: always()
with:
payload: |
{
"text": "Deploy *${{ steps.pages-deployment.outcome }}* for ${{ github.ref_name }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
- name: Upload source maps to Sentry
uses: getsentry/action-release@4744f6a65149f441c5f396d5b0877307c0db52c7
continue-on-error: true
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'

17
.github/workflows/check-pr-title.yaml vendored Normal file

@ -0,0 +1,17 @@
name: Check PR Title
on:
pull_request_target:
types:
- opened
- edited
- synchronize
jobs:
# Ensures that the PR title adheres to [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/).
conventional-commit:
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v3.4.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

26
.github/workflows/crowdin.yaml vendored Normal file

@ -0,0 +1,26 @@
name: Crowdin Upload
on:
push:
branches:
- main
jobs:
upload-sources:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- run: yarn i18n:extract
- name: Upload Crowdin sources
uses: crowdin/github-action@3133cc916c35590475cf6705f482fb653d8e36e9
with:
upload_sources: true
download_translations: false
project_id: 458284
token: ${{ secrets.CROWDIN_PERSONAL_TOKEN_SECRET }}
source: 'src/locales/en-US.po'
translation: 'src/locales/%locale%.po'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

@ -0,0 +1,91 @@
name: Slack notification on pushes to releases/*
# This CI job will push notifications to Slack whenever code is merged into any releases/* branch
#
# The steps of the command line kung-fu shown below are as follows:
# First we take the JSON-formatted Github context
# echo $GITHUB_CONTEXT \
# Then we parse out the specific fields we want for our messages using jq and format it into tab-separated values
# | jq '.event.commits[] | [.url, .id[0:7], .author.username, .timestamp, .message] | @tsv' \
# We need to do some cleaning on this output - specifically removing quotes and replacing newlines with something easier to split
# | sed 's/"//g' | sed 's/\\t/;/g' | sed 's/\\n/;/g' | sed 's/\\//g' \
# We then use awk to format the TSV into a Slack message
# | awk -F';' '{print "• <"$1"|"$2"> (<https://github.com/"$3"|"$3">, "$4") - "$5}' \
# We need to deal with some escaping issues with newlines so that we don't break the Slack message format
# | sed 's/$/\\n/g' | tr -d '\n' \
# Finally we have to truncate the message to 3,000 characters max, otherwise Slack will reject it
# | awk '{print substr($0,0,3000);}' \
# Then shove the bytes into a file to store them in their exact format
# > /tmp/parsed_github_context
on:
push:
branches:
- 'releases/*'
jobs:
notify-slack:
runs-on: ubuntu-latest
environment:
name: notify/releases
steps:
- name: Parse event to slug
id: parse-slug
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
# Formats the contents of the GitHub event into slugs: one line per commit, formatted for Slack.
# Explanation for each line is in the comments above.
run: |
echo $GITHUB_CONTEXT \
| jq '.event.commits[] | [.url, .id[0:7], .author.username, .timestamp, .message] | @tsv' \
| sed 's/"//g' | sed 's/\\t/;/g' | sed 's/\\n/;/g' | sed 's/\\//g' \
| awk -F';' '{print "• <"$1"|"$2"> (<https://github.com/"$3"|"$3">, "$4") - "$5}' \
| sed 's/$/\\n/g' | tr -d '\n' \
| awk '{print substr($0,0,3000);}' \
> /tmp/parsed_github_context
echo "SLACK_COMMITS=$(cat /tmp/parsed_github_context)" >> "$GITHUB_OUTPUT"
- uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
with:
payload: |
{
"text": "GitHub Action build result: ${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}",
"blocks": [
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Code merged to <https://github.com/Uniswap/interface/tree/${{ github.ref }}|${{ github.ref_name }}> branch:*\n"
}
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Actor*: <https://github.com/${{ github.triggering_actor }}/|${{ github.triggering_actor }}>\n*Force pushed*: ${{ github.event.forced || false }}\n"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "${{ steps.parse-slug.outputs.SLACK_COMMITS || 'New branch created' }}"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "<${{ github.event.compare}}|View Diff>"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK

24
.github/workflows/semgrep.yml vendored Normal file

@ -0,0 +1,24 @@
name: Semgrep
on:
workflow_dispatch: {}
pull_request: {}
push:
branches:
- main
paths:
- .github/workflows/semgrep.yml
schedule:
# random HH:MM to avoid a load spike on GitHub Actions at 00:00
- cron: '2 11 * * *'
jobs:
semgrep:
name: semgrep/ci
runs-on: ubuntu-20.04
env:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
container:
image: returntocorp/semgrep
if: (github.actor != 'dependabot[bot]')
steps:
- uses: actions/checkout@v3
- run: semgrep ci

278
.github/workflows/test.yml vendored Normal file

@ -0,0 +1,278 @@
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 to node_modules/.cache, so they are 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:
push:
branches:
- main
- releases/staging
pull_request:
workflow_dispatch:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: actions/cache@v3
with:
path: node_modules/.cache
key: ${{ runner.os }}-eslint-${{ github.run_id }}
restore-keys: ${{ runner.os }}-eslint-
- 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
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: actions/cache@v3
with:
path: node_modules/.cache
key: ${{ runner.os }}-tsc-${{ github.run_id }}
restore-keys: ${{ runner.os }}-tsc-
- 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
steps:
- 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
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: actions/cache@v3
with:
path: node_modules/.cache
key: ${{ runner.os }}-jest-${{ github.run_id }}
restore-keys: ${{ runner.os }}-jest-
- run: yarn test --coverage --maxWorkers=100%
- uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
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:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: actions/cache@v3
with:
path: node_modules/.swc
key: ${{ runner.os }}-swc-${{ github.run_id }}
restore-keys: ${{ runner.os }}-swc-
- run: yarn build
- uses: actions/upload-artifact@v3
with:
name: build
path: build
if-no-files-found: error
cypress-typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: actions/cache@v3
with:
path: node_modules/.cache
key: ${{ runner.os }}-cypress-tsc-${{ github.run_id }}
restore-keys: ${{ runner.os }}-cypress-tsc-
- run: yarn typecheck:cypress
- if: failure() && github.ref_name == 'main'
uses: ./.github/actions/report
with:
name: Cypress typecheck
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
# Allows for parallel re-runs of cypress tests without re-building.
cypress-rerun:
runs-on: ubuntu-latest
steps:
- run: exit 0
cypress-test-matrix:
needs: [build, cypress-rerun]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
containers: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: actions/cache@v3
with:
path: /root/.cache/Cypress
key: ${{ runner.os }}-cypress-${{ hashFiles('**/node_modules/cypress/package.json') }}
- run: |
yarn cypress install
yarn cypress info
- uses: actions/download-artifact@v3
with:
name: build
path: build
- uses: actions/cache/restore@v3
with:
path: cache
key: ${{ runner.os }}-hardhat-${{ hashFiles('hardhat.config.js') }}-${{ github.run_id }}
restore-keys: ${{ runner.os }}-hardhat-${{ hashFiles('hardhat.config.js') }}-
- uses: cypress-io/github-action@v4
with:
install: false
record: true
parallel: true
start: yarn serve
wait-on: 'http://localhost:3000'
browser: electron
group: e2e
spec: ${{ github.ref_name == 'releases/staging' && 'cypress/{e2e,staging}/**/*.test.ts' || 'cypress/e2e/**/*.test.ts' }}
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COMMIT_INFO_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }}
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title || github.event.head_commit.message }}
COMMIT_INFO_AUTHOR: ${{ github.event.sender.login || github.event.head_commit.author.login }}
# Cypress requires an email for filtering by author, but GitHub does not expose one.
# GitHub's public profile email can be deterministically produced from user id/login.
COMMIT_INFO_EMAIL: ${{ github.event.sender.id || github.event.head_commit.author.id }}+${{ github.event.sender.login || github.event.head_commit.author.login }}@users.noreply.github.com
COMMIT_INFO_SHA: ${{ github.event.pull_request.head.sha || github.event.head_commit.sha }}
COMMIT_INFO_TIMESTAMP: ${{ github.event.pull_request.updated_at || github.event.head_commit.timestamp }}
CYPRESS_PULL_REQUEST_ID: ${{ github.event.pull_request.number }}
CYPRESS_PULL_REQUEST_URL: ${{ github.event.pull_request.html_url }}
- if: failure() && github.ref_name == 'main'
uses: ./.github/actions/report
with:
name: Cypress tests
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
- uses: actions/upload-artifact@v3
with:
name: hardhat-cache
path: cache
hardhat-cache:
needs: [cypress-test-matrix]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v3
with:
name: hardhat-cache
path: cache
- uses: actions/cache/save@v3
with:
path: cache
key: ${{ runner.os }}-hardhat-${{ hashFiles('hardhat.config.js') }}-${{ github.run_id }}
cloud-typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: actions/cache@v3
with:
path: node_modules/.cache
key: ${{ runner.os }}-cloud-tsc-${{ github.run_id }}
restore-keys: ${{ runner.os }}-cloud-tsc-
- run: yarn typecheck:cloud
- if: failure() && github.ref_name == 'main'
uses: ./.github/actions/report
with:
name: Cloud typecheck
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
cloud-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: actions/cache@v3
with:
path: node_modules/.cache
key: ${{ runner.os }}-cloud-jest-${{ github.run_id }}
restore-keys: ${{ runner.os }}-cloud-jest-
# Ignore start:cloud output so it doesn't flood the test output.
# Only use 1 worker for testing, as the other is used to run start:cloud (the proxy server under test).
- run: yarn start-server-and-test 'yarn start:cloud >/dev/null' 3000 'yarn test:cloud --coverage --maxWorkers=1'
- uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
flags: cloud-tests
pre:
if: ${{ github.ref_name == 'main' || github.ref_name == 'releases/staging' }}
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6.4.1
with:
script: |
github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: context.sha,
state: 'pending',
context: 'Test / promotion',
description: 'Running tests...',
target_url: 'https://github.com/Uniswap/interface/actions/runs/' + context.runId
})
post:
if: ${{ github.ref_name == 'main' || github.ref_name == 'releases/staging' }}
needs: [pre, lint, typecheck, deps-tests, unit-tests, cypress-test-matrix, cloud-tests]
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6.4.1
with:
script: |
github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: context.sha,
state: ${{ env.STATUS }} ? 'success' : 'failure',
context: 'Test / promotion',
description: ${{ env.STATUS }} ? 'All tests passed' : 'One or more tests failed and are blocking promotion',
target_url: 'https://github.com/Uniswap/interface/actions/runs/' + context.runId
})
env:
STATUS: |
${{ needs.lint.result == 'success' }} &&
${{ needs.typecheck.result == 'success' }} &&
${{ needs.deps-tests.result == 'success' }} &&
${{ needs.unit-tests.result == 'success' }} &&
${{ needs.cypress-test-matrix.result == 'success' }} &&
${{ needs.cloud-tests.result == 'success' }}

1
CODEOWNERS Normal file

@ -0,0 +1 @@
* @uniswap/web-admins

@ -16,16 +16,6 @@ describe('Add Liquidity', () => {
cy.contains('0.05% fee tier') cy.contains('0.05% fee tier')
}) })
it('clears the token selection when chain changes', () => {
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/ETH/500')
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'UNI')
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'ETH')
cy.get('[data-testid="chain-selector"]').last().click()
cy.contains('Polygon').click()
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'ETH')
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('not.contain.text', 'UNI')
})
it('does not crash if token is duplicated', () => { it('does not crash if token is duplicated', () => {
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984') cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'UNI') cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'UNI')

@ -1,6 +1,7 @@
import { BigNumber } from '@ethersproject/bignumber' import { BigNumber } from '@ethersproject/bignumber'
import { InterfaceSectionName } from '@uniswap/analytics-events' import { InterfaceSectionName } from '@uniswap/analytics-events'
import { CurrencyAmount } from '@uniswap/sdk-core' import { CurrencyAmount } from '@uniswap/sdk-core'
import { FeatureFlag } from 'featureFlags'
import { DEFAULT_DEADLINE_FROM_NOW } from '../../../src/constants/misc' import { DEFAULT_DEADLINE_FROM_NOW } from '../../../src/constants/misc'
import { DAI, USDC_MAINNET } from '../../../src/constants/tokens' import { DAI, USDC_MAINNET } from '../../../src/constants/tokens'
@ -64,7 +65,9 @@ describe('Swap errors', () => {
}) })
it('slippage failure', () => { it('slippage failure', () => {
cy.visit(`/swap?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`) cy.visit(`/swap?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`, {
featureFlags: [{ name: FeatureFlag.uniswapXDefaultEnabled, value: false }],
})
cy.hardhat({ automine: false }).then(async (hardhat) => { cy.hardhat({ automine: false }).then(async (hardhat) => {
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDC_MAINNET, 500e6)) await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDC_MAINNET, 500e6))
await hardhat.mine() await hardhat.mine()

@ -120,7 +120,10 @@ describe.skip('Swap with fees', () => {
describe('UniswapX swaps', () => { describe('UniswapX swaps', () => {
it('displays UniswapX fee in UI', () => { it('displays UniswapX fee in UI', () => {
cy.visit('/swap', { cy.visit('/swap', {
featureFlags: [{ name: FeatureFlag.feesEnabled, value: true }], featureFlags: [
{ name: FeatureFlag.feesEnabled, value: true },
{ name: FeatureFlag.uniswapXDefaultEnabled, value: true },
],
}) })
// Intercept the trade quote // Intercept the trade quote

@ -1,5 +1,6 @@
import { ChainId, CurrencyAmount } from '@uniswap/sdk-core' import { ChainId, CurrencyAmount } from '@uniswap/sdk-core'
import { CyHttpMessages } from 'cypress/types/net-stubbing' import { CyHttpMessages } from 'cypress/types/net-stubbing'
import { FeatureFlag } from 'featureFlags'
import { DAI, nativeOnChain, USDC_MAINNET } from '../../../src/constants/tokens' import { DAI, nativeOnChain, USDC_MAINNET } from '../../../src/constants/tokens'
import { getTestSelector } from '../../utils' import { getTestSelector } from '../../utils'
@ -45,7 +46,9 @@ function stubSwapTxReceipt() {
describe.skip('UniswapX Toggle', () => { describe.skip('UniswapX Toggle', () => {
beforeEach(() => { beforeEach(() => {
stubNonPriceQuoteWith(QuoteWhereUniswapXIsBetter) stubNonPriceQuoteWith(QuoteWhereUniswapXIsBetter)
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`) cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`, {
featureFlags: [{ name: FeatureFlag.uniswapXDefaultEnabled, value: false }],
})
}) })
it('displays uniswapx ui when setting is on', () => { it('displays uniswapx ui when setting is on', () => {
@ -53,9 +56,39 @@ describe.skip('UniswapX Toggle', () => {
cy.get('#swap-currency-input .token-amount-input').type('300') cy.get('#swap-currency-input .token-amount-input').type('300')
cy.wait('@quote') cy.wait('@quote')
// UniswapX UI should not be visible
cy.get(getTestSelector('gas-estimate-uniswapx-icon')).should('not.exist')
// Opt-in to UniswapX
cy.contains('Try it now').click()
// UniswapX UI should be visible // UniswapX UI should be visible
cy.get(getTestSelector('gas-estimate-uniswapx-icon')).should('exist') cy.get(getTestSelector('gas-estimate-uniswapx-icon')).should('exist')
}) })
it('prompts opt-in if UniswapX is better', () => {
// Setup a swap
cy.get('#swap-currency-input .token-amount-input').type('300')
cy.wait('@quote')
// UniswapX should not display in gas estimate row before opt-in
cy.get(getTestSelector('gas-estimate-uniswapx-icon')).should('not.exist')
// UniswapX mustache should be visible
cy.contains('Try it now').click()
// Opt-in dialog should now be hidden
cy.contains('Try it now').should('not.be.visible')
// UniswapX should display in gas estimate row
cy.get(getTestSelector('gas-estimate-uniswapx-icon')).should('exist')
// Opt-in dialog should not reappear if user manually toggles UniswapX off
cy.get(getTestSelector('open-settings-dialog-button')).click()
cy.get(getTestSelector('toggle-uniswap-x-button')).click()
cy.get(getTestSelector('open-settings-dialog-button')).click()
cy.contains('Try it now').should('not.be.visible')
})
}) })
describe.skip('UniswapX Orders', () => { describe.skip('UniswapX Orders', () => {
@ -67,7 +100,9 @@ describe.skip('UniswapX Orders', () => {
stubSwapTxReceipt() stubSwapTxReceipt()
cy.hardhat().then((hardhat) => hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDC_MAINNET, 3e8))) cy.hardhat().then((hardhat) => hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDC_MAINNET, 3e8)))
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`) cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`, {
featureFlags: [{ name: FeatureFlag.uniswapXDefaultEnabled, value: false }],
})
}) })
it('can swap exact-in trades using uniswapX', () => { it('can swap exact-in trades using uniswapX', () => {
@ -75,6 +110,8 @@ describe.skip('UniswapX Orders', () => {
cy.get('#swap-currency-input .token-amount-input').type('300') cy.get('#swap-currency-input .token-amount-input').type('300')
cy.wait('@quote') cy.wait('@quote')
cy.contains('Try it now').click()
// Submit uniswapx order signature // Submit uniswapx order signature
cy.get('#swap-button').click() cy.get('#swap-button').click()
cy.contains('Confirm swap').click() cy.contains('Confirm swap').click()
@ -94,6 +131,8 @@ describe.skip('UniswapX Orders', () => {
cy.get('#swap-currency-output .token-amount-input').type('300') cy.get('#swap-currency-output .token-amount-input').type('300')
cy.wait('@quote') cy.wait('@quote')
cy.contains('Try it now').click()
// Submit uniswapx order signature // Submit uniswapx order signature
cy.get('#swap-button').click() cy.get('#swap-button').click()
cy.contains('Confirm swap').click() cy.contains('Confirm swap').click()
@ -113,6 +152,8 @@ describe.skip('UniswapX Orders', () => {
cy.get('#swap-currency-input .token-amount-input').type('300') cy.get('#swap-currency-input .token-amount-input').type('300')
cy.wait('@quote') cy.wait('@quote')
cy.contains('Try it now').click()
// Submit uniswapx order signature // Submit uniswapx order signature
cy.get('#swap-button').click() cy.get('#swap-button').click()
cy.contains('Confirm swap').click() cy.contains('Confirm swap').click()
@ -129,6 +170,8 @@ describe.skip('UniswapX Orders', () => {
cy.get('#swap-currency-input .token-amount-input').type('300') cy.get('#swap-currency-input .token-amount-input').type('300')
cy.wait('@quote') cy.wait('@quote')
cy.contains('Try it now').click()
// Submit uniswapx order signature // Submit uniswapx order signature
cy.get('#swap-button').click() cy.get('#swap-button').click()
cy.contains('Confirm swap').click() cy.contains('Confirm swap').click()
@ -155,7 +198,9 @@ describe.skip('UniswapX Eth Input', () => {
stubSwapTxReceipt() stubSwapTxReceipt()
cy.visit(`/swap/?inputCurrency=ETH&outputCurrency=${DAI.address}`) cy.visit(`/swap/?inputCurrency=ETH&outputCurrency=${DAI.address}`, {
featureFlags: [{ name: FeatureFlag.uniswapXDefaultEnabled, value: false }],
})
}) })
it('can swap using uniswapX with ETH as input', () => { it('can swap using uniswapX with ETH as input', () => {
@ -163,6 +208,7 @@ describe.skip('UniswapX Eth Input', () => {
cy.get('#swap-currency-input .token-amount-input').type('1') cy.get('#swap-currency-input .token-amount-input').type('1')
cy.wait('@quote') cy.wait('@quote')
cy.contains('Try it now').click()
// Prompt ETH wrap to use for order // Prompt ETH wrap to use for order
cy.get('#swap-button').click() cy.get('#swap-button').click()
@ -196,6 +242,8 @@ describe.skip('UniswapX Eth Input', () => {
cy.get('#swap-currency-input .token-amount-input').type('1') cy.get('#swap-currency-input .token-amount-input').type('1')
cy.wait('@quote') cy.wait('@quote')
cy.contains('Try it now').click()
// Prompt ETH wrap and confirm // Prompt ETH wrap and confirm
cy.get('#swap-button').click() cy.get('#swap-button').click()
cy.contains('Confirm swap').click() cy.contains('Confirm swap').click()
@ -255,12 +303,15 @@ describe.skip('UniswapX activity history', () => {
cy.hardhat().then(async (hardhat) => { cy.hardhat().then(async (hardhat) => {
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDC_MAINNET, 3e8)) await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDC_MAINNET, 3e8))
}) })
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`) cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`, {
featureFlags: [{ name: FeatureFlag.uniswapXDefaultEnabled, value: false }],
})
}) })
it('can view UniswapX order status progress in activity', () => { it('can view UniswapX order status progress in activity', () => {
// Setup a swap // Setup a swap
cy.get('#swap-currency-input .token-amount-input').type('300') cy.get('#swap-currency-input .token-amount-input').type('300')
cy.contains('Try it now').click()
// Submit uniswapx order signature // Submit uniswapx order signature
cy.get('#swap-button').click() cy.get('#swap-button').click()
@ -288,6 +339,7 @@ describe.skip('UniswapX activity history', () => {
it('can view UniswapX order status progress in activity upon expiry', () => { it('can view UniswapX order status progress in activity upon expiry', () => {
// Setup a swap // Setup a swap
cy.get('#swap-currency-input .token-amount-input').type('300') cy.get('#swap-currency-input .token-amount-input').type('300')
cy.contains('Try it now').click()
// Submit uniswapx order signature // Submit uniswapx order signature
cy.get('#swap-button').click() cy.get('#swap-button').click()
@ -314,6 +366,7 @@ describe.skip('UniswapX activity history', () => {
it('deduplicates remote vs local uniswapx orders', () => { it('deduplicates remote vs local uniswapx orders', () => {
// Setup a swap // Setup a swap
cy.get('#swap-currency-input .token-amount-input').type('300') cy.get('#swap-currency-input .token-amount-input').type('300')
cy.contains('Try it now').click()
// Submit uniswapx order signature // Submit uniswapx order signature
cy.get('#swap-button').click() cy.get('#swap-button').click()
@ -345,6 +398,7 @@ describe.skip('UniswapX activity history', () => {
it('balances should refetch after uniswapx swap', () => { it('balances should refetch after uniswapx swap', () => {
// Setup a swap // Setup a swap
cy.get('#swap-currency-input .token-amount-input').type('300') cy.get('#swap-currency-input .token-amount-input').type('300')
cy.contains('Try it now').click()
const gqlSpy = cy.spy().as('gqlSpy') const gqlSpy = cy.spy().as('gqlSpy')
cy.intercept(/graphql/, (req) => { cy.intercept(/graphql/, (req) => {

@ -202,7 +202,7 @@
"@uniswap/merkle-distributor": "^1.0.1", "@uniswap/merkle-distributor": "^1.0.1",
"@uniswap/permit2-sdk": "^1.2.0", "@uniswap/permit2-sdk": "^1.2.0",
"@uniswap/redux-multicall": "^1.1.8", "@uniswap/redux-multicall": "^1.1.8",
"@uniswap/router-sdk": "^1.7.1", "@uniswap/router-sdk": "^1.6.0",
"@uniswap/sdk-core": "4.0.7", "@uniswap/sdk-core": "4.0.7",
"@uniswap/smart-order-router": "^3.15.0", "@uniswap/smart-order-router": "^3.15.0",
"@uniswap/token-lists": "^1.0.0-beta.33", "@uniswap/token-lists": "^1.0.0-beta.33",

@ -72,6 +72,12 @@
<changefreq>weekly</changefreq> <changefreq>weekly</changefreq>
<priority>0.6</priority> <priority>0.6</priority>
</url> </url>
<url>
<loc>https://app.uniswap.org/increase</loc>
<lastmod>2023-10-11T19:57:27.976Z</lastmod>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
</url>
<url> <url>
<loc>https://app.uniswap.org/migrate/v2</loc> <loc>https://app.uniswap.org/migrate/v2</loc>
<lastmod>2023-10-11T19:57:27.976Z</lastmod> <lastmod>2023-10-11T19:57:27.976Z</lastmod>

@ -1,6 +1,10 @@
# * # *
User-agent: * User-agent: *
Disallow: Disallow: /static/js/
Allow: /
# Host
Host: https://app.uniswap.org
# Sitemaps # Sitemaps
Sitemap: https://app.uniswap.org/sitemap.xml Sitemap: https://app.uniswap.org/sitemap.xml

@ -9,13 +9,19 @@ const thegraphConfig = require('../graphql.thegraph.config')
const exec = promisify(child_process.exec) const exec = promisify(child_process.exec)
async function fetchSchema(url, outputFile) { function fetchSchema(url, outputFile) {
try { exec(`yarn --silent get-graphql-schema --h Origin=https://app.uniswap.org ${url}`)
const { stdout } = await exec(`yarn --silent get-graphql-schema --h Origin=https://app.uniswap.org ${url}`); .then(({ stderr, stdout }) => {
await fs.writeFile(outputFile, stdout); if (stderr) {
} catch(err){ throw new Error(stderr)
console.error(`Failed to fetch schema from ${url}`) } else {
} fs.writeFile(outputFile, stdout)
}
})
.catch((err) => {
console.error(err)
console.error(`Failed to fetch schema from ${url}`)
})
} }
fetchSchema(process.env.THE_GRAPH_SCHEMA_ENDPOINT, thegraphConfig.schema) fetchSchema(process.env.THE_GRAPH_SCHEMA_ENDPOINT, thegraphConfig.schema)

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 22.773 22.773" style="enable-background:new 0 0 22.773 22.773;" xml:space="preserve">
<g>
<g>
<path d="M15.769,0c0.053,0,0.106,0,0.162,0c0.13,1.606-0.483,2.806-1.228,3.675c-0.731,0.863-1.732,1.7-3.351,1.573
c-0.108-1.583,0.506-2.694,1.25-3.561C13.292,0.879,14.557,0.16,15.769,0z"/>
<path d="M20.67,16.716c0,0.016,0,0.03,0,0.045c-0.455,1.378-1.104,2.559-1.896,3.655c-0.723,0.995-1.609,2.334-3.191,2.334
c-1.367,0-2.275-0.879-3.676-0.903c-1.482-0.024-2.297,0.735-3.652,0.926c-0.155,0-0.31,0-0.462,0
c-0.995-0.144-1.798-0.932-2.383-1.642c-1.725-2.098-3.058-4.808-3.306-8.276c0-0.34,0-0.679,0-1.019
c0.105-2.482,1.311-4.5,2.914-5.478c0.846-0.52,2.009-0.963,3.304-0.765c0.555,0.086,1.122,0.276,1.619,0.464
c0.471,0.181,1.06,0.502,1.618,0.485c0.378-0.011,0.754-0.208,1.135-0.347c1.116-0.403,2.21-0.865,3.652-0.648
c1.733,0.262,2.963,1.032,3.723,2.22c-1.466,0.933-2.625,2.339-2.427,4.74C17.818,14.688,19.086,15.964,20.67,16.716z"/>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

(image error) Size: 1.5 KiB

@ -1,4 +1,5 @@
import { InterfaceElementName } from '@uniswap/analytics-events' import { InterfaceElementName } from '@uniswap/analytics-events'
import { useAndroidGALaunchFlagEnabled } from 'featureFlags/flags/androidGALaunch'
import { PropsWithChildren, useCallback } from 'react' import { PropsWithChildren, useCallback } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { ClickableStyle } from 'theme/components' import { ClickableStyle } from 'theme/components'
@ -41,11 +42,12 @@ export function DownloadButton({
text?: string text?: string
element: InterfaceElementName element: InterfaceElementName
}) { }) {
const isAndroidGALaunched = useAndroidGALaunchFlagEnabled()
const onButtonClick = useCallback(() => { const onButtonClick = useCallback(() => {
// handles any actions required by the parent, i.e. cancelling wallet connection attempt or dismissing an ad // handles any actions required by the parent, i.e. cancelling wallet connection attempt or dismissing an ad
onClick?.() onClick?.()
openDownloadApp({ element }) openDownloadApp({ element, isAndroidGALaunched })
}, [element, onClick]) }, [element, isAndroidGALaunched, onClick])
return ( return (
<BaseButton branded onClick={onButtonClick}> <BaseButton branded onClick={onButtonClick}>

@ -90,7 +90,7 @@ const DescriptionText = styled(ThemedText.LabelMicro)`
function useOrderAmounts( function useOrderAmounts(
orderDetails?: UniswapXOrderDetails orderDetails?: UniswapXOrderDetails
): Pick<InterfaceTrade, 'inputAmount' | 'outputAmount'> | undefined { ): Pick<InterfaceTrade, 'inputAmount' | 'postTaxOutputAmount'> | undefined {
const inputCurrency = useCurrency(orderDetails?.swapInfo?.inputCurrencyId, orderDetails?.chainId) const inputCurrency = useCurrency(orderDetails?.swapInfo?.inputCurrencyId, orderDetails?.chainId)
const outputCurrency = useCurrency(orderDetails?.swapInfo?.outputCurrencyId, orderDetails?.chainId) const outputCurrency = useCurrency(orderDetails?.swapInfo?.outputCurrencyId, orderDetails?.chainId)
@ -106,7 +106,7 @@ function useOrderAmounts(
if (swapInfo.tradeType === TradeType.EXACT_INPUT) { if (swapInfo.tradeType === TradeType.EXACT_INPUT) {
return { return {
inputAmount: CurrencyAmount.fromRawAmount(inputCurrency, swapInfo.inputCurrencyAmountRaw), inputAmount: CurrencyAmount.fromRawAmount(inputCurrency, swapInfo.inputCurrencyAmountRaw),
outputAmount: CurrencyAmount.fromRawAmount( postTaxOutputAmount: CurrencyAmount.fromRawAmount(
outputCurrency, outputCurrency,
swapInfo.settledOutputCurrencyAmountRaw ?? swapInfo.expectedOutputCurrencyAmountRaw swapInfo.settledOutputCurrencyAmountRaw ?? swapInfo.expectedOutputCurrencyAmountRaw
), ),
@ -114,7 +114,7 @@ function useOrderAmounts(
} else { } else {
return { return {
inputAmount: CurrencyAmount.fromRawAmount(inputCurrency, swapInfo.expectedInputCurrencyAmountRaw), inputAmount: CurrencyAmount.fromRawAmount(inputCurrency, swapInfo.expectedInputCurrencyAmountRaw),
outputAmount: CurrencyAmount.fromRawAmount(outputCurrency, swapInfo.outputCurrencyAmountRaw), postTaxOutputAmount: CurrencyAmount.fromRawAmount(outputCurrency, swapInfo.outputCurrencyAmountRaw),
} }
} }
} }

@ -110,7 +110,7 @@ const ActiveDot = styled.span<{ closed: boolean; outOfRange: boolean }>`
margin-top: 1px; margin-top: 1px;
` `
function calculateLiquidityValue(price0: number | undefined, price1: number | undefined, position: Position) { function calculcateLiquidityValue(price0: number | undefined, price1: number | undefined, position: Position) {
if (!price0 || !price1) return undefined if (!price0 || !price1) return undefined
const value0 = parseFloat(position.amount0.toExact()) * price0 const value0 = parseFloat(position.amount0.toExact()) * price0
@ -124,7 +124,7 @@ function PositionListItem({ positionInfo }: { positionInfo: PositionInfo }) {
const { chainId, position, pool, details, inRange, closed } = positionInfo const { chainId, position, pool, details, inRange, closed } = positionInfo
const { priceA, priceB, fees: feeValue } = useFeeValues(positionInfo) const { priceA, priceB, fees: feeValue } = useFeeValues(positionInfo)
const liquidityValue = calculateLiquidityValue(priceA, priceB, position) const liquidityValue = calculcateLiquidityValue(priceA, priceB, position)
const navigate = useNavigate() const navigate = useNavigate()
const toggleWalletDrawer = useToggleAccountDrawer() const toggleWalletDrawer = useToggleAccountDrawer()

@ -36,9 +36,7 @@ const DoubleLogoContainer = styled.div`
} }
` `
const LogoContainer = styled.div` const StyledLogoParentContainer = styled.div`
display: flex;
align-items: center;
position: relative; position: relative;
top: 0; top: 0;
left: 0; left: 0;
@ -59,9 +57,9 @@ const CircleLogoImage = styled.img<{ size: string }>`
const L2LogoContainer = styled.div` const L2LogoContainer = styled.div`
border-radius: ${getDefaultBorderRadius(16)}px; border-radius: ${getDefaultBorderRadius(16)}px;
height: 16px; height: 16px;
left: 70%; left: 60%;
position: absolute; position: absolute;
top: 70%; top: 60%;
outline: 2px solid ${({ theme }) => theme.surface1}; outline: 2px solid ${({ theme }) => theme.surface1};
width: 16px; width: 16px;
display: flex; display: flex;
@ -157,10 +155,10 @@ function SquareL2Logo({ chainId }: { chainId: ChainId }) {
*/ */
export function PortfolioLogo(props: PortfolioLogoProps) { export function PortfolioLogo(props: PortfolioLogoProps) {
return ( return (
<LogoContainer style={props.style}> <StyledLogoParentContainer style={props.style}>
{getLogo(props)} {getLogo(props)}
<SquareL2Logo chainId={props.chainId} /> <SquareL2Logo chainId={props.chainId} />
</LogoContainer> </StyledLogoParentContainer>
) )
} }

@ -4,8 +4,7 @@ import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrap
import Row from 'components/Row' import Row from 'components/Row'
import { DeltaArrow } from 'components/Tokens/TokenDetails/Delta' import { DeltaArrow } from 'components/Tokens/TokenDetails/Delta'
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore' import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
import { PortfolioTokenBalancePartsFragment } from 'graphql/data/__generated__/types-and-hooks' import { TokenBalance } from 'graphql/data/__generated__/types-and-hooks'
import { PortfolioToken } from 'graphql/data/portfolios'
import { getTokenDetailsURL, gqlToCurrency, logSentryErrorForUnsupportedChain } from 'graphql/data/util' import { getTokenDetailsURL, gqlToCurrency, logSentryErrorForUnsupportedChain } from 'graphql/data/util'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent' import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent'
@ -29,7 +28,7 @@ export default function Tokens({ account }: { account: string }) {
const { data } = useCachedPortfolioBalancesQuery({ account }) const { data } = useCachedPortfolioBalancesQuery({ account })
const tokenBalances = data?.portfolios?.[0].tokenBalances const tokenBalances = data?.portfolios?.[0].tokenBalances as TokenBalance[] | undefined
const { visibleTokens, hiddenTokens } = useMemo( const { visibleTokens, hiddenTokens } = useMemo(
() => splitHiddenTokens(tokenBalances ?? [], { hideSmallBalances }), () => splitHiddenTokens(tokenBalances ?? [], { hideSmallBalances }),
@ -70,12 +69,9 @@ const TokenNameText = styled(ThemedText.SubHeader)`
${EllipsisStyle} ${EllipsisStyle}
` `
function TokenRow({ type PortfolioToken = NonNullable<TokenBalance['token']>
token,
quantity, function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: TokenBalance & { token: PortfolioToken }) {
denominatedValue,
tokenProjectMarket,
}: PortfolioTokenBalancePartsFragment & { token: PortfolioToken }) {
const { formatDelta } = useFormatter() const { formatDelta } = useFormatter()
const percentChange = tokenProjectMarket?.pricePercentChange?.value ?? 0 const percentChange = tokenProjectMarket?.pricePercentChange?.value ?? 0

@ -32,14 +32,6 @@ exports[`PortfolioLogo renders with L2 icon 1`] = `
} }
.c0 { .c0 {
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: relative; position: relative;
top: 0; top: 0;
left: 0; left: 0;
@ -54,9 +46,9 @@ exports[`PortfolioLogo renders with L2 icon 1`] = `
.c3 { .c3 {
border-radius: 4px; border-radius: 4px;
height: 16px; height: 16px;
left: 70%; left: 60%;
position: absolute; position: absolute;
top: 70%; top: 60%;
outline: 2px solid #FFFFFF; outline: 2px solid #FFFFFF;
width: 16px; width: 16px;
display: -webkit-box; display: -webkit-box;
@ -158,14 +150,6 @@ exports[`PortfolioLogo renders without L2 icon 1`] = `
} }
.c0 { .c0 {
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: relative; position: relative;
top: 0; top: 0;
left: 0; left: 0;

@ -9,6 +9,7 @@ import { uniwalletWCV2ConnectConnection } from 'connection'
import { ActivationStatus, useActivationState } from 'connection/activate' import { ActivationStatus, useActivationState } from 'connection/activate'
import { ConnectionType } from 'connection/types' import { ConnectionType } from 'connection/types'
import { UniwalletConnect as UniwalletConnectV2 } from 'connection/WalletConnectV2' import { UniwalletConnect as UniwalletConnectV2 } from 'connection/WalletConnectV2'
import { useAndroidGALaunchFlagEnabled } from 'featureFlags/flags/androidGALaunch'
import { QRCodeSVG } from 'qrcode.react' import { QRCodeSVG } from 'qrcode.react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import styled, { useTheme } from 'styled-components' import styled, { useTheme } from 'styled-components'
@ -42,8 +43,9 @@ export default function UniwalletModal() {
const { activationState, cancelActivation } = useActivationState() const { activationState, cancelActivation } = useActivationState()
const [uri, setUri] = useState<string>() const [uri, setUri] = useState<string>()
const isAndroidGALaunched = useAndroidGALaunchFlagEnabled()
// Displays the modal if not on iOS/Android, a Uniswap Wallet Connection is pending, & qrcode URI is available // Displays the modal if not on iOS/Android, a Uniswap Wallet Connection is pending, & qrcode URI is available
const onLaunchedMobilePlatform = isIOS || isAndroid const onLaunchedMobilePlatform = isIOS || (isAndroidGALaunched && isAndroid)
const open = const open =
!onLaunchedMobilePlatform && !onLaunchedMobilePlatform &&
activationState.status === ActivationStatus.PENDING && activationState.status === ActivationStatus.PENDING &&
@ -103,6 +105,8 @@ const InfoSectionWrapper = styled(RowBetween)`
` `
function InfoSection() { function InfoSection() {
const isAndroidGALaunched = useAndroidGALaunchFlagEnabled()
return ( return (
<InfoSectionWrapper> <InfoSectionWrapper>
<AutoColumn gap="4px"> <AutoColumn gap="4px">
@ -110,7 +114,13 @@ function InfoSection() {
<Trans>Don&apos;t have a Uniswap wallet?</Trans> <Trans>Don&apos;t have a Uniswap wallet?</Trans>
</ThemedText.SubHeaderSmall> </ThemedText.SubHeaderSmall>
<ThemedText.BodySmall color="neutral2"> <ThemedText.BodySmall color="neutral2">
<Trans>Safely store and swap tokens with the Uniswap app. Available on iOS and Android.</Trans> {isAndroidGALaunched ? (
<Trans>Safely store and swap tokens with the Uniswap app. Available on iOS and Android.</Trans>
) : (
<Trans>
Download in the App Store to safely store your tokens and NFTs, swap tokens, and connect to crypto apps.
</Trans>
)}
</ThemedText.BodySmall> </ThemedText.BodySmall>
</AutoColumn> </AutoColumn>
<Column> <Column>

@ -1,5 +1,6 @@
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { InterfaceElementName } from '@uniswap/analytics-events' import { InterfaceElementName } from '@uniswap/analytics-events'
import { useAndroidGALaunchFlagEnabled } from 'featureFlags/flags/androidGALaunch'
import { useScreenSize } from 'hooks/useScreenSize' import { useScreenSize } from 'hooks/useScreenSize'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import { useHideAndroidAnnouncementBanner } from 'state/user/hooks' import { useHideAndroidAnnouncementBanner } from 'state/user/hooks'
@ -30,12 +31,14 @@ export default function AndroidAnnouncementBanner() {
const shouldDisplay = Boolean(!hideAndroidAnnouncementBanner && !isLandingScreen) const shouldDisplay = Boolean(!hideAndroidAnnouncementBanner && !isLandingScreen)
const isDarkMode = useIsDarkMode() const isDarkMode = useIsDarkMode()
const isAndroidGALaunched = useAndroidGALaunchFlagEnabled()
const onClick = () => const onClick = () =>
openDownloadApp({ openDownloadApp({
element: InterfaceElementName.UNISWAP_WALLET_BANNER_DOWNLOAD_BUTTON, element: InterfaceElementName.UNISWAP_WALLET_BANNER_DOWNLOAD_BUTTON,
isAndroidGALaunched,
}) })
if (isMobileSafari) return null if (!isAndroidGALaunched || isMobileSafari) return null
return ( return (
<PopupContainer show={shouldDisplay}> <PopupContainer show={shouldDisplay}>

@ -3,17 +3,21 @@ import Column from 'components/Column'
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateConfig, useUpdateFlag } from 'featureFlags' import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateConfig, useUpdateFlag } from 'featureFlags'
import { DynamicConfigName } from 'featureFlags/dynamicConfig' import { DynamicConfigName } from 'featureFlags/dynamicConfig'
import { useQuickRouteChains } from 'featureFlags/dynamicConfig/quickRouteChains' import { useQuickRouteChains } from 'featureFlags/dynamicConfig/quickRouteChains'
import { useAndroidGALaunchFlag } from 'featureFlags/flags/androidGALaunch'
import { useCurrencyConversionFlag } from 'featureFlags/flags/currencyConversion' import { useCurrencyConversionFlag } from 'featureFlags/flags/currencyConversion'
import { useFallbackProviderEnabledFlag } from 'featureFlags/flags/fallbackProvider' import { useFallbackProviderEnabledFlag } from 'featureFlags/flags/fallbackProvider'
import { useFotAdjustmentsFlag } from 'featureFlags/flags/fotAdjustments'
import { useInfoExploreFlag } from 'featureFlags/flags/infoExplore' import { useInfoExploreFlag } from 'featureFlags/flags/infoExplore'
import { useInfoLiveViewsFlag } from 'featureFlags/flags/infoLiveViews' import { useInfoLiveViewsFlag } from 'featureFlags/flags/infoLiveViews'
import { useInfoPoolPageFlag } from 'featureFlags/flags/infoPoolPage' import { useInfoPoolPageFlag } from 'featureFlags/flags/infoPoolPage'
import { useInfoTDPFlag } from 'featureFlags/flags/infoTDP' import { useInfoTDPFlag } from 'featureFlags/flags/infoTDP'
import { useLimitsEnabledFlag } from 'featureFlags/flags/limits'
import { useMultichainUXFlag } from 'featureFlags/flags/multichainUx' import { useMultichainUXFlag } from 'featureFlags/flags/multichainUx'
import { useProgressIndicatorV2Flag } from 'featureFlags/flags/progressIndicatorV2' import { useProgressIndicatorV2Flag } from 'featureFlags/flags/progressIndicatorV2'
import { useQuickRouteMainnetFlag } from 'featureFlags/flags/quickRouteMainnet' import { useQuickRouteMainnetFlag } from 'featureFlags/flags/quickRouteMainnet'
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc' import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
import { useUniswapXDefaultEnabledFlag } from 'featureFlags/flags/uniswapXDefault'
import { useUniswapXEthOutputFlag } from 'featureFlags/flags/uniswapXEthOutput'
import { useUniswapXExactOutputFlag } from 'featureFlags/flags/uniswapXExactOutput'
import { useUniswapXSyntheticQuoteFlag } from 'featureFlags/flags/uniswapXUseSyntheticQuote' import { useUniswapXSyntheticQuoteFlag } from 'featureFlags/flags/uniswapXUseSyntheticQuote'
import { useFeesEnabledFlag } from 'featureFlags/flags/useFees' import { useFeesEnabledFlag } from 'featureFlags/flags/useFees'
import { useUpdateAtom } from 'jotai/utils' import { useUpdateAtom } from 'jotai/utils'
@ -271,12 +275,6 @@ export default function FeatureFlagModal() {
featureFlag={FeatureFlag.feesEnabled} featureFlag={FeatureFlag.feesEnabled}
label="Enable Swap Fees" label="Enable Swap Fees"
/> />
<FeatureFlagOption
variant={BaseVariant}
value={useLimitsEnabledFlag()}
featureFlag={FeatureFlag.limitsEnabled}
label="Enable Limits"
/>
<FeatureFlagOption <FeatureFlagOption
variant={BaseVariant} variant={BaseVariant}
value={useFallbackProviderEnabledFlag()} value={useFallbackProviderEnabledFlag()}
@ -295,12 +293,24 @@ export default function FeatureFlagModal() {
featureFlag={FeatureFlag.multichainUX} featureFlag={FeatureFlag.multichainUX}
label="Updated Multichain UX" label="Updated Multichain UX"
/> />
<FeatureFlagOption
variant={BaseVariant}
value={useFotAdjustmentsFlag()}
featureFlag={FeatureFlag.fotAdjustedmentsEnabled}
label="Enable fee-on-transfer UI and slippage adjustments"
/>
<FeatureFlagOption <FeatureFlagOption
variant={BaseVariant} variant={BaseVariant}
value={useProgressIndicatorV2Flag()} value={useProgressIndicatorV2Flag()}
featureFlag={FeatureFlag.progressIndicatorV2} featureFlag={FeatureFlag.progressIndicatorV2}
label="Refreshed swap progress indicator" label="Refreshed swap progress indicator"
/> />
<FeatureFlagOption
variant={BaseVariant}
value={useAndroidGALaunchFlag()}
featureFlag={FeatureFlag.androidGALaunch}
label="Android Nov 14th GA launch"
/>
<FeatureFlagGroup name="Quick routes"> <FeatureFlagGroup name="Quick routes">
<FeatureFlagOption <FeatureFlagOption
variant={BaseVariant} variant={BaseVariant}
@ -323,6 +333,24 @@ export default function FeatureFlagModal() {
featureFlag={FeatureFlag.uniswapXSyntheticQuote} featureFlag={FeatureFlag.uniswapXSyntheticQuote}
label="Force synthetic quotes for UniswapX" label="Force synthetic quotes for UniswapX"
/> />
<FeatureFlagOption
variant={BaseVariant}
value={useUniswapXEthOutputFlag()}
featureFlag={FeatureFlag.uniswapXEthOutputEnabled}
label="Enable eth output for UniswapX orders"
/>
<FeatureFlagOption
variant={BaseVariant}
value={useUniswapXExactOutputFlag()}
featureFlag={FeatureFlag.uniswapXExactOutputEnabled}
label="Enable exact output for UniswapX orders"
/>
<FeatureFlagOption
variant={BaseVariant}
value={useUniswapXDefaultEnabledFlag()}
featureFlag={FeatureFlag.uniswapXDefaultEnabled}
label="Enable UniswapX by default"
/>
</FeatureFlagGroup> </FeatureFlagGroup>
<FeatureFlagGroup name="Info Site Migration"> <FeatureFlagGroup name="Info Site Migration">
<FeatureFlagOption <FeatureFlagOption

@ -0,0 +1,8 @@
export const AppleLogo = (props: React.SVGProps<SVGSVGElement>) => (
<svg viewBox="0 0 814 1000" {...props}>
<path
fill="currentColor"
d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57-155.5-127C46.7 790.7 0 663 0 541.8c0-194.4 126.4-297.5 250.8-297.5 66.1 0 121.2 43.4 162.7 43.4 39.5 0 101.1-46 176.3-46 28.5 0 130.9 2.6 198.3 99.2zm-234-181.5c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.5 32.4-55.1 83.6-55.1 135.5 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 135.5-71.3z"
/>
</svg>
)

@ -1,7 +1,9 @@
import { t, Trans } from '@lingui/macro' import { t, Trans } from '@lingui/macro'
import { InterfaceElementName } from '@uniswap/analytics-events' import { InterfaceElementName } from '@uniswap/analytics-events'
import { ReactComponent as AppleLogo } from 'assets/svg/apple_logo.svg'
import FeatureFlagModal from 'components/FeatureFlagModal/FeatureFlagModal' import FeatureFlagModal from 'components/FeatureFlagModal/FeatureFlagModal'
import { PrivacyPolicyModal } from 'components/PrivacyPolicy' import { PrivacyPolicyModal } from 'components/PrivacyPolicy'
import { useAndroidGALaunchFlagEnabled } from 'featureFlags/flags/androidGALaunch'
import { useOnClickOutside } from 'hooks/useOnClickOutside' import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { Box } from 'nft/components/Box' import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex' import { Column, Row } from 'nft/components/Flex'
@ -130,6 +132,8 @@ export const MenuDropdown = () => {
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)
useOnClickOutside(ref, isOpen ? toggleOpen : undefined) useOnClickOutside(ref, isOpen ? toggleOpen : undefined)
const isAndroidGALaunched = useAndroidGALaunchFlagEnabled()
return ( return (
<> <>
<Box position="relative" ref={ref} marginRight="4"> <Box position="relative" ref={ref} marginRight="4">
@ -171,23 +175,35 @@ export const MenuDropdown = () => {
onClick={() => onClick={() =>
openDownloadApp({ openDownloadApp({
element: InterfaceElementName.UNISWAP_WALLET_NAVBAR_MENU_DOWNLOAD_BUTTON, element: InterfaceElementName.UNISWAP_WALLET_NAVBAR_MENU_DOWNLOAD_BUTTON,
isAndroidGALaunched,
}) })
} }
> >
<PrimaryMenuRow close={toggleOpen}> <PrimaryMenuRow close={toggleOpen}>
<> {isAndroidGALaunched ? (
<Icon> <>
<UniswapAppLogo width="24px" height="24px" /> <Icon>
</Icon> <UniswapAppLogo width="24px" height="24px" />
<div> </Icon>
<ThemedText.BodyPrimary> <div>
<Trans>Download Uniswap</Trans> <ThemedText.BodyPrimary>
</ThemedText.BodyPrimary> <Trans>Download Uniswap</Trans>
<ThemedText.LabelSmall> </ThemedText.BodyPrimary>
<Trans>Available on iOS and Android</Trans> <ThemedText.LabelSmall>
</ThemedText.LabelSmall> <Trans>Available on iOS and Android</Trans>
</div> </ThemedText.LabelSmall>
</> </div>
</>
) : (
<>
<Icon>
<AppleLogo width="24px" height="24px" fill={theme.neutral1} />
</Icon>
<PrimaryMenuRow.Text>
<Trans>Download Uniswap app</Trans>
</PrimaryMenuRow.Text>
</>
)}
</PrimaryMenuRow> </PrimaryMenuRow>
</Box> </Box>
</Column> </Column>

@ -1,36 +1,9 @@
import userEvent from '@testing-library/user-event' import userEvent from '@testing-library/user-event'
import store from 'state'
import { addSerializedToken } from 'state/user/reducer'
import { act, render, screen } from 'test-utils/render' import { act, render, screen } from 'test-utils/render'
import { PoolDetailsHeader } from './PoolDetailsHeader' import { PoolDetailsHeader } from './PoolDetailsHeader'
describe('PoolDetailsHeader', () => { describe('PoolDetailsHeader', () => {
beforeEach(() => {
store.dispatch(
addSerializedToken({
serializedToken: {
chainId: 1,
address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
symbol: 'USDC',
name: 'USD Coin',
decimals: 6,
},
})
)
store.dispatch(
addSerializedToken({
serializedToken: {
chainId: 1,
address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
symbol: 'WETH',
name: 'Wrapped Ether',
decimals: 18,
},
})
)
})
const mockProps = { const mockProps = {
chainId: 1, chainId: 1,
poolAddress: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640', poolAddress: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640',

@ -1,38 +1,11 @@
import { ChainId } from '@uniswap/sdk-core' import { ChainId } from '@uniswap/sdk-core'
import { USDC_MAINNET } from 'constants/tokens' import { USDC_MAINNET } from 'constants/tokens'
import store from 'state'
import { addSerializedToken } from 'state/user/reducer'
import { usdcWethPoolAddress, validPoolToken0, validPoolToken1 } from 'test-utils/pools/fixtures' import { usdcWethPoolAddress, validPoolToken0, validPoolToken1 } from 'test-utils/pools/fixtures'
import { render, screen } from 'test-utils/render' import { render, screen } from 'test-utils/render'
import { PoolDetailsLink } from './PoolDetailsLink' import { PoolDetailsLink } from './PoolDetailsLink'
describe('PoolDetailsHeader', () => { describe('PoolDetailsHeader', () => {
beforeEach(() => {
store.dispatch(
addSerializedToken({
serializedToken: {
chainId: 1,
address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
symbol: 'USDC',
name: 'USD Coin',
decimals: 6,
},
})
)
store.dispatch(
addSerializedToken({
serializedToken: {
chainId: 1,
address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
symbol: 'WETH',
name: 'Wrapped Ether',
decimals: 18,
},
})
)
})
it('renders link for pool address', async () => { it('renders link for pool address', async () => {
const { asFragment } = render( const { asFragment } = render(
<PoolDetailsLink <PoolDetailsLink

@ -1,6 +1,4 @@
import { enableNetConnect } from 'nock' import { enableNetConnect } from 'nock'
import store from 'state'
import { addSerializedToken } from 'state/user/reducer'
import { validPoolDataResponse } from 'test-utils/pools/fixtures' import { validPoolDataResponse } from 'test-utils/pools/fixtures'
import { act, render, screen } from 'test-utils/render' import { act, render, screen } from 'test-utils/render'
import { BREAKPOINTS } from 'theme' import { BREAKPOINTS } from 'theme'
@ -17,28 +15,6 @@ describe('PoolDetailsStats', () => {
beforeEach(() => { beforeEach(() => {
// Enable network connections for retrieving token logos // Enable network connections for retrieving token logos
enableNetConnect() enableNetConnect()
store.dispatch(
addSerializedToken({
serializedToken: {
chainId: 1,
address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
symbol: 'USDC',
name: 'USD Coin',
decimals: 6,
},
})
)
store.dispatch(
addSerializedToken({
serializedToken: {
chainId: 1,
address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
symbol: 'WETH',
name: 'Wrapped Ether',
decimals: 18,
},
})
)
}) })
it('renders stats text correctly', async () => { it('renders stats text correctly', async () => {

@ -1,7 +1,5 @@
import userEvent from '@testing-library/user-event' import userEvent from '@testing-library/user-event'
import useMultiChainPositions from 'components/AccountDrawer/MiniPortfolio/Pools/useMultiChainPositions' import useMultiChainPositions from 'components/AccountDrawer/MiniPortfolio/Pools/useMultiChainPositions'
import store from 'state'
import { addSerializedToken } from 'state/user/reducer'
import { mocked } from 'test-utils/mocked' import { mocked } from 'test-utils/mocked'
import { useMultiChainPositionsReturnValue, validPoolToken0, validPoolToken1 } from 'test-utils/pools/fixtures' import { useMultiChainPositionsReturnValue, validPoolToken0, validPoolToken1 } from 'test-utils/pools/fixtures'
import { act, render, screen } from 'test-utils/render' import { act, render, screen } from 'test-utils/render'
@ -26,28 +24,6 @@ describe('PoolDetailsStatsButton', () => {
beforeEach(() => { beforeEach(() => {
mocked(useMultiChainPositions).mockReturnValue(useMultiChainPositionsReturnValue) mocked(useMultiChainPositions).mockReturnValue(useMultiChainPositionsReturnValue)
store.dispatch(
addSerializedToken({
serializedToken: {
chainId: 1,
address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
symbol: 'USDC',
name: 'USD Coin',
decimals: 6,
},
})
)
store.dispatch(
addSerializedToken({
serializedToken: {
chainId: 1,
address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
symbol: 'WETH',
name: 'Wrapped Ether',
decimals: 18,
},
})
)
}) })
it('loading skeleton shown correctly', () => { it('loading skeleton shown correctly', () => {
@ -88,7 +64,7 @@ describe('PoolDetailsStatsButton', () => {
await act(() => userEvent.click(screen.getByTestId('pool-details-add-liquidity-button'))) await act(() => userEvent.click(screen.getByTestId('pool-details-add-liquidity-button')))
expect(global.window.location.href).toContain( expect(global.window.location.href).toContain(
'/add/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/500' '/increase/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/500'
) )
}) })
}) })

@ -68,7 +68,7 @@ export function PoolDetailsStatsButtons({ chainId, token0, token1, feeTier, load
navigate( navigate(
toSwap toSwap
? `/swap?inputCurrency=${currencyId(currency0)}&outputCurrency=${currencyId(currency1)}` ? `/swap?inputCurrency=${currencyId(currency0)}&outputCurrency=${currencyId(currency1)}`
: `/add/${currencyId(currency0)}/${currencyId(currency1)}/${feeTier}${tokenId ? `/${tokenId}` : ''}` : `/increase/${currencyId(currency0)}/${currencyId(currency1)}/${feeTier}${tokenId ? `/${tokenId}` : ''}`
) )
} }
} }

@ -541,7 +541,7 @@ exports[`PoolDetailsHeader renders link for token address 1`] = `
class="c5" class="c5"
> >
<img <img
alt="USDC logo" alt="UNKNOWN logo"
class="c6" class="c6"
loading="lazy" loading="lazy"
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png" src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png"

@ -253,7 +253,7 @@ exports[`PoolDetailsStats pool balance chart not visible on mobile 1`] = `
class="c10" class="c10"
> >
<img <img
alt="USDC logo" alt="UNKNOWN logo"
class="c11" class="c11"
loading="lazy" loading="lazy"
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png" src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png"

@ -20,7 +20,7 @@ const ReferenceElement = styled.div`
height: inherit; height: inherit;
` `
const Arrow = styled.div` export const Arrow = styled.div`
width: 8px; width: 8px;
height: 8px; height: 8px;
z-index: 9998; z-index: 9998;

@ -3,7 +3,6 @@ import { usePortfolioBalancesLazyQuery, usePortfolioBalancesQuery } from 'graphq
import { GQL_MAINNET_CHAINS } from 'graphql/data/util' import { GQL_MAINNET_CHAINS } from 'graphql/data/util'
import usePrevious from 'hooks/usePrevious' import usePrevious from 'hooks/usePrevious'
import { atom, useAtom } from 'jotai' import { atom, useAtom } from 'jotai'
import ms from 'ms'
import { PropsWithChildren, useCallback, useEffect } from 'react' import { PropsWithChildren, useCallback, useEffect } from 'react'
import { usePendingActivity } from '../AccountDrawer/MiniPortfolio/Activity/hooks' import { usePendingActivity } from '../AccountDrawer/MiniPortfolio/Activity/hooks'
@ -32,9 +31,8 @@ const hasUnfetchedBalancesAtom = atom<boolean>(true)
export default function PrefetchBalancesWrapper({ export default function PrefetchBalancesWrapper({
children, children,
shouldFetchOnAccountUpdate, shouldFetchOnAccountUpdate,
shouldFetchOnHover = true,
className, className,
}: PropsWithChildren<{ shouldFetchOnAccountUpdate: boolean; shouldFetchOnHover?: boolean; className?: string }>) { }: PropsWithChildren<{ shouldFetchOnAccountUpdate: boolean; className?: string }>) {
const { account } = useWeb3React() const { account } = useWeb3React()
const [prefetchPortfolioBalances] = usePortfolioBalancesLazyQuery() const [prefetchPortfolioBalances] = usePortfolioBalancesLazyQuery()
@ -42,13 +40,8 @@ export default function PrefetchBalancesWrapper({
const [hasUnfetchedBalances, setHasUnfetchedBalances] = useAtom(hasUnfetchedBalancesAtom) const [hasUnfetchedBalances, setHasUnfetchedBalances] = useAtom(hasUnfetchedBalancesAtom)
const fetchBalances = useCallback(() => { const fetchBalances = useCallback(() => {
if (account) { if (account) {
// Backend takes <2sec to get the updated portfolio value after a transaction prefetchPortfolioBalances({ variables: { ownerAddress: account, chains: GQL_MAINNET_CHAINS } })
// This timeout is an interim solution while we're working on a websocket that'll ping the client when connected account gets changes setHasUnfetchedBalances(false)
// TODO(WEB-3131): remove this timeout after websocket is implemented
setTimeout(() => {
prefetchPortfolioBalances({ variables: { ownerAddress: account, chains: GQL_MAINNET_CHAINS } })
setHasUnfetchedBalances(false)
}, ms('3.5s'))
} }
}, [account, prefetchPortfolioBalances, setHasUnfetchedBalances]) }, [account, prefetchPortfolioBalances, setHasUnfetchedBalances])
@ -69,18 +62,12 @@ export default function PrefetchBalancesWrapper({
} }
}, [account, prevAccount, shouldFetchOnAccountUpdate, fetchBalances, hasUpdatedTx, setHasUnfetchedBalances]) }, [account, prevAccount, shouldFetchOnAccountUpdate, fetchBalances, hasUpdatedTx, setHasUnfetchedBalances])
// Temporary workaround to fix balances on TDP - this fetches balances if shouldFetchOnAccountUpdate becomes true while hasUnfetchedBalances is true
// TODO(WEB-3071) remove this logic once balance provider refactor is done
useEffect(() => {
if (hasUnfetchedBalances && shouldFetchOnAccountUpdate) fetchBalances()
}, [fetchBalances, hasUnfetchedBalances, shouldFetchOnAccountUpdate])
const onHover = useCallback(() => { const onHover = useCallback(() => {
if (hasUnfetchedBalances) fetchBalances() if (hasUnfetchedBalances) fetchBalances()
}, [fetchBalances, hasUnfetchedBalances]) }, [fetchBalances, hasUnfetchedBalances])
return ( return (
<div onMouseEnter={shouldFetchOnHover ? onHover : undefined} className={className}> <div onMouseEnter={onHover} className={className}>
{children} {children}
</div> </div>
) )

@ -5,6 +5,7 @@ import { ChainId, Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { Trace } from 'analytics' import { Trace } from 'analytics'
import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper' import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper'
import { TokenBalance } from 'graphql/data/__generated__/types-and-hooks'
import { supportedChainIdFromGQLChain } from 'graphql/data/util' import { supportedChainIdFromGQLChain } from 'graphql/data/util'
import useDebounce from 'hooks/useDebounce' import useDebounce from 'hooks/useDebounce'
import { useOnClickOutside } from 'hooks/useOnClickOutside' import { useOnClickOutside } from 'hooks/useOnClickOutside'
@ -100,7 +101,7 @@ export function CurrencySearch({
}, [chainId, data?.portfolios]) }, [chainId, data?.portfolios])
const sortedTokens: Token[] = useMemo(() => { const sortedTokens: Token[] = useMemo(() => {
const portfolioTokenBalances = data?.portfolios?.[0].tokenBalances const portfolioTokenBalances = data?.portfolios?.[0].tokenBalances as TokenBalance[] | undefined
const portfolioTokens = splitHiddenTokens(portfolioTokenBalances ?? []) const portfolioTokens = splitHiddenTokens(portfolioTokenBalances ?? [])
.visibleTokens.map((tokenBalance) => { .visibleTokens.map((tokenBalance) => {
if (!tokenBalance?.token?.chain || !tokenBalance.token?.address || !tokenBalance.token?.decimals) { if (!tokenBalance?.token?.chain || !tokenBalance.token?.address || !tokenBalance.token?.decimals) {

@ -3,8 +3,11 @@ import Column from 'components/Column'
import UniswapXBrandMark from 'components/Logo/UniswapXBrandMark' import UniswapXBrandMark from 'components/Logo/UniswapXBrandMark'
import { RowBetween, RowFixed } from 'components/Row' import { RowBetween, RowFixed } from 'components/Row'
import Toggle from 'components/Toggle' import Toggle from 'components/Toggle'
import { useUniswapXDefaultEnabled } from 'featureFlags/flags/uniswapXDefault'
import { useAppDispatch } from 'state/hooks'
import { RouterPreference } from 'state/routing/types' import { RouterPreference } from 'state/routing/types'
import { useRouterPreference } from 'state/user/hooks' import { useRouterPreference, useUserOptedOutOfUniswapX } from 'state/user/hooks'
import { updateDisabledUniswapX, updateOptedOutOfUniswapX } from 'state/user/reducer'
import styled from 'styled-components' import styled from 'styled-components'
import { ExternalLink, ThemedText } from 'theme/components' import { ExternalLink, ThemedText } from 'theme/components'
@ -19,6 +22,12 @@ const InlineLink = styled(ThemedText.BodySmall)`
export default function RouterPreferenceSettings() { export default function RouterPreferenceSettings() {
const [routerPreference, setRouterPreference] = useRouterPreference() const [routerPreference, setRouterPreference] = useRouterPreference()
const dispatch = useAppDispatch()
const userOptedOutOfUniswapX = useUserOptedOutOfUniswapX()
const isUniswapXDefaultEnabled = useUniswapXDefaultEnabled()
const isUniswapXOverrideEnabled = isUniswapXDefaultEnabled && !userOptedOutOfUniswapX
const uniswapXInEffect = routerPreference === RouterPreference.X || isUniswapXOverrideEnabled
return ( return (
<RowBetween gap="sm"> <RowBetween gap="sm">
@ -37,9 +46,21 @@ export default function RouterPreferenceSettings() {
</RowFixed> </RowFixed>
<Toggle <Toggle
id="toggle-uniswap-x-button" id="toggle-uniswap-x-button"
isActive={routerPreference === RouterPreference.X} // If UniswapX-by-default is enabled we need to render this as active even if routerPreference === RouterPreference.API
// because we're going to default to the UniswapX quote.
// If the user manually toggles it off, this doesn't apply.
isActive={uniswapXInEffect}
toggle={() => { toggle={() => {
setRouterPreference(routerPreference === RouterPreference.X ? RouterPreference.API : RouterPreference.X) if (uniswapXInEffect) {
if (isUniswapXDefaultEnabled) {
// We need to remember if a opts out of UniswapX, so we don't request UniswapX quotes.
dispatch(updateOptedOutOfUniswapX({ optedOutOfUniswapX: true }))
} else {
// We need to remember if a user disables Uniswap X, so we don't show the opt-in flow again.
dispatch(updateDisabledUniswapX({ disabledUniswapX: true }))
}
}
setRouterPreference(uniswapXInEffect ? RouterPreference.API : RouterPreference.X)
}} }}
/> />
</RowBetween> </RowBetween>

@ -1,30 +1,22 @@
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { ChainId, Currency, CurrencyAmount } from '@uniswap/sdk-core' import { ChainId, Currency } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { PortfolioLogo } from 'components/AccountDrawer/MiniPortfolio/PortfolioLogo' import { PortfolioLogo } from 'components/AccountDrawer/MiniPortfolio/PortfolioLogo'
import { getChainInfo } from 'constants/chainInfo' import { getChainInfo } from 'constants/chainInfo'
import { asSupportedChain } from 'constants/chains' import { asSupportedChain } from 'constants/chains'
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
import { useInfoTDPEnabled } from 'featureFlags/flags/infoTDP'
import { Chain, PortfolioTokenBalancePartsFragment } from 'graphql/data/__generated__/types-and-hooks'
import { getTokenDetailsURL, gqlToCurrency, supportedChainIdFromGQLChain } from 'graphql/data/util'
import { useStablecoinValue } from 'hooks/useStablecoinPrice' import { useStablecoinValue } from 'hooks/useStablecoinPrice'
import useCurrencyBalance from 'lib/hooks/useCurrencyBalance' import useCurrencyBalance from 'lib/hooks/useCurrencyBalance'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useNavigate } from 'react-router-dom' import styled, { useTheme } from 'styled-components'
import styled from 'styled-components'
import { ThemedText } from 'theme/components' import { ThemedText } from 'theme/components'
import { NumberType, useFormatter } from 'utils/formatNumbers' import { NumberType, useFormatter } from 'utils/formatNumbers'
import { MultiChainMap } from '.' const BalancesCard = styled.div`
border-radius: 16px;
const BalancesCard = styled.div<{ isInfoTDPEnabled?: boolean }>`
color: ${({ theme }) => theme.neutral1}; color: ${({ theme }) => theme.neutral1};
display: flex; display: none;
flex-direction: column;
gap: 24px;
height: fit-content; height: fit-content;
${({ isInfoTDPEnabled }) => !isInfoTDPEnabled && 'padding: 16px;'} padding: 16px;
width: 100%; width: 100%;
// 768 hardcoded to match NFT-redesign navbar breakpoints // 768 hardcoded to match NFT-redesign navbar breakpoints
@ -56,13 +48,11 @@ const BalanceContainer = styled.div`
flex: 1; flex: 1;
` `
const BalanceAmountsContainer = styled.div<{ isInfoTDPEnabled?: boolean }>` const BalanceAmountsContainer = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
width: 100%;
${({ isInfoTDPEnabled }) => isInfoTDPEnabled && 'margin-left: 12px;'}
` `
const StyledNetworkLabel = styled.div` const StyledNetworkLabel = styled.div`
@ -71,187 +61,49 @@ const StyledNetworkLabel = styled.div`
line-height: 16px; line-height: 16px;
` `
interface BalanceProps { export default function BalanceSummary({ token }: { token: Currency }) {
currency?: Currency const { account, chainId } = useWeb3React()
chainId?: ChainId const theme = useTheme()
balance?: CurrencyAmount<Currency> // TODO(WEB-3026): only used for pre-Info-project calculations, should remove after project goes live const { label, color } = getChainInfo(asSupportedChain(chainId) ?? ChainId.MAINNET)
gqlBalance?: PortfolioTokenBalancePartsFragment const balance = useCurrencyBalance(account, token)
onClick?: () => void const { formatCurrencyAmount } = useFormatter()
}
const Balance = ({ currency, chainId = ChainId.MAINNET, balance, gqlBalance, onClick }: BalanceProps) => {
const { formatCurrencyAmount, formatNumber } = useFormatter()
const { label: chainName, color } = getChainInfo(asSupportedChain(chainId) ?? ChainId.MAINNET)
const currencies = useMemo(() => [currency], [currency])
const isInfoTDPEnabled = useInfoExplorePageEnabled()
const formattedBalance = formatCurrencyAmount({ const formattedBalance = formatCurrencyAmount({
amount: balance, amount: balance,
type: NumberType.TokenNonTx, type: NumberType.TokenNonTx,
}) })
const formattedUsdValue = formatCurrencyAmount({ const formattedUsdValue = formatCurrencyAmount({
amount: useStablecoinValue(balance), amount: useStablecoinValue(balance),
type: NumberType.PortfolioBalance, type: NumberType.FiatTokenStats,
})
const formattedGqlBalance = formatNumber({
input: gqlBalance?.quantity,
type: NumberType.TokenNonTx,
})
const formattedUsdGqlValue = formatNumber({
input: gqlBalance?.denominatedValue?.value,
type: NumberType.PortfolioBalance,
}) })
if (isInfoTDPEnabled) { const currencies = useMemo(() => [token], [token])
return (
<BalanceRow onClick={onClick}>
<PortfolioLogo currencies={currencies} chainId={chainId} size="2rem" />
<BalanceAmountsContainer isInfoTDPEnabled>
<BalanceItem>
<ThemedText.BodyPrimary>{formattedUsdGqlValue}</ThemedText.BodyPrimary>
</BalanceItem>
<BalanceItem>
<ThemedText.BodySecondary>{formattedGqlBalance}</ThemedText.BodySecondary>
</BalanceItem>
</BalanceAmountsContainer>
</BalanceRow>
)
} else {
return (
<BalanceRow>
<PortfolioLogo currencies={currencies} chainId={chainId} size="2rem" />
<BalanceContainer>
<BalanceAmountsContainer>
<BalanceItem>
<ThemedText.SubHeader>
{formattedBalance} {currency?.symbol}
</ThemedText.SubHeader>
</BalanceItem>
<BalanceItem>
<ThemedText.BodyPrimary>{formattedUsdValue}</ThemedText.BodyPrimary>
</BalanceItem>
</BalanceAmountsContainer>
<StyledNetworkLabel color={color}>{chainName}</StyledNetworkLabel>
</BalanceContainer>
</BalanceRow>
)
}
}
const ConnectedChainBalanceSummary = ({ if (!account || !balance) {
connectedChainBalance,
}: {
connectedChainBalance?: CurrencyAmount<Currency>
}) => {
const { chainId: connectedChainId } = useWeb3React()
if (!connectedChainId || !connectedChainBalance || !connectedChainBalance.greaterThan(0)) return null
const token = connectedChainBalance.currency
const { label: chainName } = getChainInfo(asSupportedChain(connectedChainId) ?? ChainId.MAINNET)
return (
<BalanceSection>
<ThemedText.SubHeaderSmall color="neutral1">
<Trans>Your balance on {chainName}</Trans>
</ThemedText.SubHeaderSmall>
<Balance currency={token} chainId={connectedChainId} balance={connectedChainBalance} />
</BalanceSection>
)
}
const PageChainBalanceSummary = ({ pageChainBalance }: { pageChainBalance?: PortfolioTokenBalancePartsFragment }) => {
if (!pageChainBalance || !pageChainBalance.token) return null
const currency = gqlToCurrency(pageChainBalance.token)
return (
<BalanceSection>
<ThemedText.HeadlineSmall color="neutral1">
<Trans>Your balance</Trans>
</ThemedText.HeadlineSmall>
<Balance currency={currency} chainId={currency?.chainId} gqlBalance={pageChainBalance} />
</BalanceSection>
)
}
const OtherChainsBalanceSummary = ({
otherChainBalances,
hasPageChainBalance,
}: {
otherChainBalances: readonly PortfolioTokenBalancePartsFragment[]
hasPageChainBalance: boolean
}) => {
const navigate = useNavigate()
const isInfoExplorePageEnabled = useInfoExplorePageEnabled()
if (!otherChainBalances.length) return null
return (
<BalanceSection>
{hasPageChainBalance ? (
<ThemedText.SubHeaderSmall>
<Trans>On other networks</Trans>
</ThemedText.SubHeaderSmall>
) : (
<ThemedText.HeadlineSmall>
<Trans>Balance on other networks</Trans>
</ThemedText.HeadlineSmall>
)}
{otherChainBalances.map((balance) => {
const currency = balance.token && gqlToCurrency(balance.token)
const chainId = (balance.token && supportedChainIdFromGQLChain(balance.token.chain)) ?? ChainId.MAINNET
return (
<Balance
key={balance.id}
currency={currency}
chainId={chainId}
gqlBalance={balance}
onClick={() =>
navigate(
getTokenDetailsURL({
address: balance.token?.address,
chain: balance.token?.chain ?? Chain.Ethereum,
isInfoExplorePageEnabled,
})
)
}
/>
)
})}
</BalanceSection>
)
}
export default function BalanceSummary({
currency,
chain,
multiChainMap,
}: {
currency: Currency
chain: Chain
multiChainMap: MultiChainMap
}) {
const { account } = useWeb3React()
const isInfoTDPEnabled = useInfoTDPEnabled()
const connectedChainBalance = useCurrencyBalance(account, currency)
const pageChainBalance = multiChainMap[chain].balance
const otherChainBalances: PortfolioTokenBalancePartsFragment[] = []
for (const [key, value] of Object.entries(multiChainMap)) {
if (key !== chain && value.balance !== undefined) {
otherChainBalances.push(value.balance)
}
}
const hasBalances = pageChainBalance || Boolean(otherChainBalances.length)
if (!account || !hasBalances) {
return null return null
} }
return ( return (
<BalancesCard isInfoTDPEnabled={isInfoTDPEnabled}> <BalancesCard>
{!isInfoTDPEnabled && <ConnectedChainBalanceSummary connectedChainBalance={connectedChainBalance} />} <BalanceSection>
{isInfoTDPEnabled && ( <ThemedText.SubHeaderSmall color={theme.neutral1}>
<> <Trans>Your balance on {label}</Trans>
<PageChainBalanceSummary pageChainBalance={pageChainBalance} /> </ThemedText.SubHeaderSmall>
<OtherChainsBalanceSummary otherChainBalances={otherChainBalances} hasPageChainBalance={!!pageChainBalance} /> <BalanceRow>
</> <PortfolioLogo currencies={currencies} chainId={token.chainId} size="2rem" />
)} <BalanceContainer>
<BalanceAmountsContainer>
<BalanceItem>
<ThemedText.SubHeader>
{formattedBalance} {token.symbol}
</ThemedText.SubHeader>
</BalanceItem>
<BalanceItem>
<ThemedText.BodyPrimary>{formattedUsdValue}</ThemedText.BodyPrimary>
</BalanceItem>
</BalanceAmountsContainer>
<StyledNetworkLabel color={color}>{label}</StyledNetworkLabel>
</BalanceContainer>
</BalanceRow>
</BalanceSection>
</BalancesCard> </BalancesCard>
) )
} }

@ -1,24 +1,15 @@
import { ParentSize } from '@visx/responsive' import { ParentSize } from '@visx/responsive'
import { LoadingChart } from 'components/Tokens/TokenDetails/Skeleton' import { ChartContainer, LoadingChart } from 'components/Tokens/TokenDetails/Skeleton'
import { TokenPriceQuery } from 'graphql/data/TokenPrice' import { TokenPriceQuery } from 'graphql/data/TokenPrice'
import { isPricePoint, PricePoint } from 'graphql/data/util' import { isPricePoint, PricePoint } from 'graphql/data/util'
import { TimePeriod } from 'graphql/data/util'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
import { pageTimePeriodAtom } from 'pages/TokenDetails' import { pageTimePeriodAtom } from 'pages/TokenDetails'
import { Suspense, useMemo } from 'react' import { startTransition, Suspense, useMemo } from 'react'
import styled from 'styled-components'
import { PriceChart } from '../../Charts/PriceChart' import { PriceChart } from '../../Charts/PriceChart'
import TimePeriodSelector from './TimeSelector' import TimePeriodSelector from './TimeSelector'
export const ChartContainer = styled.div`
display: flex;
flex-direction: column;
height: 436px;
margin-bottom: 24px;
align-items: flex-start;
width: 100%;
`
function usePriceHistory(tokenPriceData: TokenPriceQuery): PricePoint[] | undefined { function usePriceHistory(tokenPriceData: TokenPriceQuery): PricePoint[] | undefined {
// Appends the current price to the end of the priceHistory array // Appends the current price to the end of the priceHistory array
const priceHistory = useMemo(() => { const priceHistory = useMemo(() => {
@ -34,28 +25,49 @@ function usePriceHistory(tokenPriceData: TokenPriceQuery): PricePoint[] | undefi
return priceHistory return priceHistory
} }
export default function ChartSection({ tokenPriceQuery }: { tokenPriceQuery?: TokenPriceQuery }) { export default function ChartSection({
tokenPriceQuery,
onChangeTimePeriod,
}: {
tokenPriceQuery?: TokenPriceQuery
onChangeTimePeriod: OnChangeTimePeriod
}) {
if (!tokenPriceQuery) { if (!tokenPriceQuery) {
return <LoadingChart /> return <LoadingChart />
} }
return ( return (
<Suspense fallback={<LoadingChart />}> <Suspense fallback={<LoadingChart />}>
<ChartContainer data-testid="chart-container"> <ChartContainer>
<Chart tokenPriceQuery={tokenPriceQuery} /> <Chart tokenPriceQuery={tokenPriceQuery} onChangeTimePeriod={onChangeTimePeriod} />
<TimePeriodSelector />
</ChartContainer> </ChartContainer>
</Suspense> </Suspense>
) )
} }
function Chart({ tokenPriceQuery }: { tokenPriceQuery: TokenPriceQuery }) { export type OnChangeTimePeriod = (t: TimePeriod) => void
function Chart({
tokenPriceQuery,
onChangeTimePeriod,
}: {
tokenPriceQuery: TokenPriceQuery
onChangeTimePeriod: OnChangeTimePeriod
}) {
const prices = usePriceHistory(tokenPriceQuery) const prices = usePriceHistory(tokenPriceQuery)
// Initializes time period to global & maintain separate time period for subsequent changes
const timePeriod = useAtomValue(pageTimePeriodAtom) const timePeriod = useAtomValue(pageTimePeriodAtom)
return ( return (
<ParentSize> <ChartContainer data-testid="chart-container">
{({ width }) => <PriceChart prices={prices} width={width} height={392} timePeriod={timePeriod} />} <ParentSize>
</ParentSize> {({ width }) => <PriceChart prices={prices} width={width} height={392} timePeriod={timePeriod} />}
</ParentSize>
<TimePeriodSelector
currentTimePeriod={timePeriod}
onTimeChange={(t: TimePeriod) => {
startTransition(() => onChangeTimePeriod(t))
}}
/>
</ChartContainer>
) )
} }

@ -2,20 +2,21 @@ import { Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core' import { Currency } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { NATIVE_CHAIN_ID } from 'constants/tokens' import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { useInfoTDPEnabled } from 'featureFlags/flags/infoTDP'
import { PortfolioTokenBalancePartsFragment } from 'graphql/data/__generated__/types-and-hooks'
import { CHAIN_ID_TO_BACKEND_NAME } from 'graphql/data/util' import { CHAIN_ID_TO_BACKEND_NAME } from 'graphql/data/util'
import { useStablecoinValue } from 'hooks/useStablecoinPrice' import { useStablecoinValue } from 'hooks/useStablecoinPrice'
import useCurrencyBalance from 'lib/hooks/useCurrencyBalance' import useCurrencyBalance from 'lib/hooks/useCurrencyBalance'
import styled, { css } from 'styled-components' import styled from 'styled-components'
import { StyledInternalLink, ThemedText } from 'theme/components' import { StyledInternalLink } from 'theme/components'
import { NumberType, useFormatter } from 'utils/formatNumbers' import { NumberType, useFormatter } from 'utils/formatNumbers'
const Wrapper = styled.div<{ isInfoTDPEnabled?: boolean }>` const Wrapper = styled.div`
align-content: center; align-content: center;
align-items: center; align-items: center;
background-color: ${({ theme }) => theme.surface1};
border: 1px solid ${({ theme }) => theme.surface3}; border: 1px solid ${({ theme }) => theme.surface3};
border-bottom: none;
background-color: ${({ theme }) => theme.surface1};
border-radius: 20px 20px 0px 0px;
bottom: 52px;
color: ${({ theme }) => theme.neutral2}; color: ${({ theme }) => theme.neutral2};
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -25,24 +26,9 @@ const Wrapper = styled.div<{ isInfoTDPEnabled?: boolean }>`
justify-content: space-between; justify-content: space-between;
left: 0; left: 0;
line-height: 20px; line-height: 20px;
padding: 12px 16px;
position: fixed; position: fixed;
width: 100%;
${({ isInfoTDPEnabled }) =>
isInfoTDPEnabled
? css`
border-radius: 20px;
bottom: 56px;
margin: 8px;
padding: 12px 32px;
width: calc(100vw - 16px);
`
: css`
border-bottom: none;
border-radius: 20px 20px 0px 0px;
bottom: 52px;
padding: 12px 16px;
width: 100%;
`}
@media screen and (min-width: ${({ theme }) => theme.breakpoint.md}px) { @media screen and (min-width: ${({ theme }) => theme.breakpoint.md}px) {
bottom: 0px; bottom: 0px;
@ -51,29 +37,27 @@ const Wrapper = styled.div<{ isInfoTDPEnabled?: boolean }>`
display: none; display: none;
} }
` `
const BalanceValue = styled.div<{ isInfoTDPEnabled?: boolean }>` const BalanceValue = styled.div`
color: ${({ theme }) => theme.neutral1}; color: ${({ theme }) => theme.neutral1};
font-size: 20px; font-size: 20px;
line-height: ${({ isInfoTDPEnabled }) => (isInfoTDPEnabled ? '20px' : '28px')}; line-height: 28px;
display: flex; display: flex;
gap: 8px; gap: 8px;
` `
const Balance = styled.div<{ isInfoTDPEnabled?: boolean }>` const Balance = styled.div`
align-items: ${({ isInfoTDPEnabled }) => (isInfoTDPEnabled ? 'flex-end' : 'center')}; align-items: center;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; gap: 8px;
` `
const BalanceInfo = styled.div<{ isInfoTDPEnabled?: boolean }>` const BalanceInfo = styled.div`
display: flex; display: flex;
flex: 10 1 auto; flex: 10 1 auto;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
${({ isInfoTDPEnabled }) => isInfoTDPEnabled && 'gap: 6px;'}
` `
const FiatValue = styled(ThemedText.Caption)<{ isInfoTDPEnabled?: boolean }>` const FiatValue = styled.span`
${({ isInfoTDPEnabled, theme }) => !isInfoTDPEnabled && `color: ${theme.neutral2};`}
font-size: 12px; font-size: 12px;
line-height: 16px; line-height: 16px;
@ -81,15 +65,15 @@ const FiatValue = styled(ThemedText.Caption)<{ isInfoTDPEnabled?: boolean }>`
line-height: 24px; line-height: 24px;
} }
` `
const SwapButton = styled(StyledInternalLink)<{ isInfoTDPEnabled?: boolean }>` const SwapButton = styled(StyledInternalLink)`
background-color: ${({ theme }) => theme.accent1}; background-color: ${({ theme }) => theme.accent1};
border: none; border: none;
border-radius: ${({ isInfoTDPEnabled }) => (isInfoTDPEnabled ? '22px' : '12px')}; border-radius: 12px;
color: ${({ theme }) => theme.deprecated_accentTextLightPrimary}; color: ${({ theme }) => theme.deprecated_accentTextLightPrimary};
display: flex; display: flex;
flex: 1 1 auto; flex: 1 1 auto;
padding: 12px 16px; padding: 12px 16px;
font-size: ${({ isInfoTDPEnabled }) => (isInfoTDPEnabled ? '16px' : '1em')}; font-size: 1em;
font-weight: 535; font-weight: 535;
height: 44px; height: 44px;
justify-content: center; justify-content: center;
@ -97,18 +81,10 @@ const SwapButton = styled(StyledInternalLink)<{ isInfoTDPEnabled?: boolean }>`
max-width: 100vw; max-width: 100vw;
` `
export default function MobileBalanceSummaryFooter({ export default function MobileBalanceSummaryFooter({ token }: { token: Currency }) {
currency,
pageChainBalance,
}: {
currency: Currency
pageChainBalance?: PortfolioTokenBalancePartsFragment
}) {
const isInfoTDPEnabled = useInfoTDPEnabled()
const { account } = useWeb3React() const { account } = useWeb3React()
const balance = useCurrencyBalance(account, currency) const balance = useCurrencyBalance(account, token)
const { formatCurrencyAmount, formatNumber } = useFormatter() const { formatCurrencyAmount } = useFormatter()
const formattedBalance = formatCurrencyAmount({ const formattedBalance = formatCurrencyAmount({
amount: balance, amount: balance,
type: NumberType.TokenNonTx, type: NumberType.TokenNonTx,
@ -117,35 +93,22 @@ export default function MobileBalanceSummaryFooter({
amount: useStablecoinValue(balance), amount: useStablecoinValue(balance),
type: NumberType.FiatTokenStats, type: NumberType.FiatTokenStats,
}) })
const formattedGqlBalance = formatNumber({ const chain = CHAIN_ID_TO_BACKEND_NAME[token.chainId].toLowerCase()
input: pageChainBalance?.quantity,
type: NumberType.TokenNonTx,
})
const formattedUsdGqlValue = formatNumber({
input: pageChainBalance?.denominatedValue?.value,
type: NumberType.PortfolioBalance,
})
const chain = CHAIN_ID_TO_BACKEND_NAME[currency.chainId].toLowerCase()
return ( return (
<Wrapper isInfoTDPEnabled={isInfoTDPEnabled}> <Wrapper>
{Boolean(account && (isInfoTDPEnabled ? pageChainBalance : balance)) && ( {Boolean(account && balance) && (
<BalanceInfo isInfoTDPEnabled={isInfoTDPEnabled}> <BalanceInfo>
{isInfoTDPEnabled ? <Trans>Your balance</Trans> : <Trans>Your {currency.symbol} balance</Trans>} <Trans>Your {token.symbol} balance</Trans>
<Balance isInfoTDPEnabled={isInfoTDPEnabled}> <Balance>
<BalanceValue isInfoTDPEnabled={isInfoTDPEnabled}> <BalanceValue>
{isInfoTDPEnabled ? formattedGqlBalance : formattedBalance} {currency.symbol} {formattedBalance} {token.symbol}
</BalanceValue> </BalanceValue>
<FiatValue isInfoTDPEnabled={isInfoTDPEnabled}> <FiatValue>{formattedUsdValue}</FiatValue>
{isInfoTDPEnabled ? `(${formattedUsdGqlValue})` : formattedUsdValue}
</FiatValue>
</Balance> </Balance>
</BalanceInfo> </BalanceInfo>
)} )}
<SwapButton <SwapButton to={`/swap?chainName=${chain}&outputCurrency=${token.isNative ? NATIVE_CHAIN_ID : token.address}`}>
isInfoTDPEnabled={isInfoTDPEnabled}
to={`/swap?chainName=${chain}&outputCurrency=${currency.isNative ? NATIVE_CHAIN_ID : currency.address}`}
>
<Trans>Swap</Trans> <Trans>Swap</Trans>
</SwapButton> </SwapButton>
</Wrapper> </Wrapper>

@ -11,7 +11,6 @@ import { textFadeIn } from 'theme/styles'
import { LoadingBubble } from '../loading' import { LoadingBubble } from '../loading'
import { AboutContainer, AboutHeader } from './About' import { AboutContainer, AboutHeader } from './About'
import { BreadcrumbNav, BreadcrumbNavLink } from './BreadcrumbNavLink' import { BreadcrumbNav, BreadcrumbNavLink } from './BreadcrumbNavLink'
import { ChartContainer } from './ChartSection'
import { StatPair, StatsWrapper, StatWrapper } from './StatsSection' import { StatPair, StatsWrapper, StatWrapper } from './StatsSection'
const SWAP_COMPONENT_WIDTH = 360 const SWAP_COMPONENT_WIDTH = 360
@ -54,6 +53,14 @@ export const RightPanel = styled.div<{ isInfoTDPEnabled?: boolean }>`
display: flex; display: flex;
} }
` `
export const ChartContainer = styled.div`
display: flex;
flex-direction: column;
height: 436px;
margin-bottom: 24px;
align-items: flex-start;
width: 100%;
`
const LoadingChartContainer = styled.div` const LoadingChartContainer = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;

@ -1,5 +1,5 @@
import { useAtom } from 'jotai' import { TimePeriod } from 'graphql/data/util'
import { pageTimePeriodAtom } from 'pages/TokenDetails' import { startTransition, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { MEDIUM_MEDIA_BREAKPOINT } from '../constants' import { MEDIUM_MEDIA_BREAKPOINT } from '../constants'
@ -46,13 +46,26 @@ const TimeButton = styled.button<{ active: boolean }>`
} }
` `
export default function TimePeriodSelector() { export default function TimePeriodSelector({
const [timePeriod, setTimePeriod] = useAtom(pageTimePeriodAtom) currentTimePeriod,
onTimeChange,
}: {
currentTimePeriod: TimePeriod
onTimeChange: (t: TimePeriod) => void
}) {
const [timePeriod, setTimePeriod] = useState(currentTimePeriod)
return ( return (
<TimeOptionsWrapper> <TimeOptionsWrapper>
<TimeOptionsContainer> <TimeOptionsContainer>
{ORDERED_TIMES.map((time) => ( {ORDERED_TIMES.map((time) => (
<TimeButton key={DISPLAYS[time]} active={timePeriod === time} onClick={() => setTimePeriod(time)}> <TimeButton
key={DISPLAYS[time]}
active={timePeriod === time}
onClick={() => {
startTransition(() => onTimeChange(time))
setTimePeriod(time)
}}
>
{DISPLAYS[time]} {DISPLAYS[time]}
</TimeButton> </TimeButton>
))} ))}

@ -3,11 +3,12 @@ import { InterfacePageName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { Trace } from 'analytics' import { Trace } from 'analytics'
import { PortfolioLogo } from 'components/AccountDrawer/MiniPortfolio/PortfolioLogo' import { PortfolioLogo } from 'components/AccountDrawer/MiniPortfolio/PortfolioLogo'
import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper'
import { AboutSection } from 'components/Tokens/TokenDetails/About' import { AboutSection } from 'components/Tokens/TokenDetails/About'
import AddressSection from 'components/Tokens/TokenDetails/AddressSection' import AddressSection from 'components/Tokens/TokenDetails/AddressSection'
import BalanceSummary from 'components/Tokens/TokenDetails/BalanceSummary'
import { BreadcrumbNav, BreadcrumbNavLink } from 'components/Tokens/TokenDetails/BreadcrumbNavLink' import { BreadcrumbNav, BreadcrumbNavLink } from 'components/Tokens/TokenDetails/BreadcrumbNavLink'
import ChartSection from 'components/Tokens/TokenDetails/ChartSection' import ChartSection from 'components/Tokens/TokenDetails/ChartSection'
import MobileBalanceSummaryFooter from 'components/Tokens/TokenDetails/MobileBalanceSummaryFooter'
import ShareButton from 'components/Tokens/TokenDetails/ShareButton' import ShareButton from 'components/Tokens/TokenDetails/ShareButton'
import TokenDetailsSkeleton, { import TokenDetailsSkeleton, {
Hr, Hr,
@ -24,13 +25,8 @@ import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
import { checkWarning } from 'constants/tokenSafety' import { checkWarning } from 'constants/tokenSafety'
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore' import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
import { useInfoTDPEnabled } from 'featureFlags/flags/infoTDP' import { useInfoTDPEnabled } from 'featureFlags/flags/infoTDP'
import { import { TokenPriceQuery } from 'graphql/data/__generated__/types-and-hooks'
Chain, import { Chain, TokenQuery, TokenQueryData } from 'graphql/data/Token'
PortfolioTokenBalancePartsFragment,
TokenPriceQuery,
TokenQuery,
} from 'graphql/data/__generated__/types-and-hooks'
import { TokenQueryData } from 'graphql/data/Token'
import { getTokenDetailsURL, gqlToCurrency, InterfaceGqlChain, supportedChainIdFromGQLChain } from 'graphql/data/util' import { getTokenDetailsURL, gqlToCurrency, InterfaceGqlChain, supportedChainIdFromGQLChain } from 'graphql/data/util'
import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch' import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch'
import { UNKNOWN_TOKEN_SYMBOL, useTokenFromActiveNetwork } from 'lib/hooks/useCurrency' import { UNKNOWN_TOKEN_SYMBOL, useTokenFromActiveNetwork } from 'lib/hooks/useCurrency'
@ -45,9 +41,8 @@ import { CopyContractAddress } from 'theme/components'
import { isAddress, shortenAddress } from 'utils' import { isAddress, shortenAddress } from 'utils'
import { addressesAreEquivalent } from 'utils/addressesAreEquivalent' import { addressesAreEquivalent } from 'utils/addressesAreEquivalent'
import BalanceSummary from './BalanceSummary' import { OnChangeTimePeriod } from './ChartSection'
import InvalidTokenDetails from './InvalidTokenDetails' import InvalidTokenDetails from './InvalidTokenDetails'
import MobileBalanceSummaryFooter from './MobileBalanceSummaryFooter'
import { TokenDescription } from './TokenDescription' import { TokenDescription } from './TokenDescription'
const TokenSymbol = styled.span` const TokenSymbol = styled.span`
@ -100,13 +95,14 @@ function useRelevantToken(
[onChainToken, queryToken] [onChainToken, queryToken]
) )
} }
export type MultiChainMap = { [chain: string]: { address?: string; balance?: PortfolioTokenBalancePartsFragment } }
type TokenDetailsProps = { type TokenDetailsProps = {
urlAddress?: string urlAddress?: string
inputTokenAddress?: string inputTokenAddress?: string
chain: InterfaceGqlChain chain: InterfaceGqlChain
tokenQuery: TokenQuery tokenQuery: TokenQuery
tokenPriceQuery?: TokenPriceQuery tokenPriceQuery?: TokenPriceQuery
onChangeTimePeriod: OnChangeTimePeriod
} }
export default function TokenDetails({ export default function TokenDetails({
urlAddress, urlAddress,
@ -114,6 +110,7 @@ export default function TokenDetails({
chain, chain,
tokenQuery, tokenQuery,
tokenPriceQuery, tokenPriceQuery,
onChangeTimePeriod,
}: TokenDetailsProps) { }: TokenDetailsProps) {
if (!urlAddress) { if (!urlAddress) {
throw new Error('Invalid token details route: tokenAddress param is undefined') throw new Error('Invalid token details route: tokenAddress param is undefined')
@ -123,25 +120,17 @@ export default function TokenDetails({
[urlAddress] [urlAddress]
) )
const { account, chainId: connectedChainId } = useWeb3React() const { chainId: connectedChainId } = useWeb3React()
const pageChainId = supportedChainIdFromGQLChain(chain) const pageChainId = supportedChainIdFromGQLChain(chain)
const tokenQueryData = tokenQuery.token const tokenQueryData = tokenQuery.token
const { data: balanceQuery } = useCachedPortfolioBalancesQuery({ account }) const crossChainMap = useMemo(
const multiChainMap = useMemo(() => { () =>
const tokenBalances = balanceQuery?.portfolios?.[0].tokenBalances tokenQueryData?.project?.tokens.reduce((map, current) => {
const tokensAcrossChains = tokenQueryData?.project?.tokens if (current) map[current.chain] = current.address
if (!tokensAcrossChains) return {} return map
return tokensAcrossChains.reduce((map, current) => { }, {} as { [key: string]: string | undefined }) ?? {},
if (current) { [tokenQueryData]
if (!map[current.chain]) { )
map[current.chain] = {}
}
map[current.chain].address = current.address
map[current.chain].balance = tokenBalances?.find((tokenBalance) => tokenBalance.token?.id === current.id)
}
return map
}, {} as MultiChainMap)
}, [balanceQuery?.portfolios, tokenQueryData?.project?.tokens])
const { token: detailedToken, didFetchFromChain } = useRelevantToken(address, pageChainId, tokenQueryData) const { token: detailedToken, didFetchFromChain } = useRelevantToken(address, pageChainId, tokenQueryData)
@ -157,7 +146,7 @@ export default function TokenDetails({
const navigateToTokenForChain = useCallback( const navigateToTokenForChain = useCallback(
(update: Chain) => { (update: Chain) => {
if (!address) return if (!address) return
const bridgedAddress = multiChainMap[update].address const bridgedAddress = crossChainMap[update]
if (bridgedAddress) { if (bridgedAddress) {
startTokenTransition(() => startTokenTransition(() =>
navigate( navigate(
@ -172,7 +161,7 @@ export default function TokenDetails({
startTokenTransition(() => navigate(getTokenDetailsURL({ address, chain: update, isInfoExplorePageEnabled }))) startTokenTransition(() => navigate(getTokenDetailsURL({ address, chain: update, isInfoExplorePageEnabled })))
} }
}, },
[address, multiChainMap, didFetchFromChain, detailedToken?.isNative, navigate, isInfoExplorePageEnabled] [address, crossChainMap, didFetchFromChain, detailedToken?.isNative, navigate, isInfoExplorePageEnabled]
) )
useOnGlobalChainSwitch(navigateToTokenForChain) useOnGlobalChainSwitch(navigateToTokenForChain)
@ -269,7 +258,7 @@ export default function TokenDetails({
<ShareButton currency={detailedToken} /> <ShareButton currency={detailedToken} />
</TokenActions> </TokenActions>
</TokenInfoContainer> </TokenInfoContainer>
<ChartSection tokenPriceQuery={tokenPriceQuery} /> <ChartSection tokenPriceQuery={tokenPriceQuery} onChangeTimePeriod={onChangeTimePeriod} />
<StatsSection chainId={pageChainId} address={address} tokenQueryData={tokenQueryData} /> <StatsSection chainId={pageChainId} address={address} tokenQueryData={tokenQueryData} />
<Hr /> <Hr />
@ -297,7 +286,7 @@ export default function TokenDetails({
/> />
</div> </div>
{tokenWarning && <TokenSafetyMessage tokenAddress={address} warning={tokenWarning} />} {tokenWarning && <TokenSafetyMessage tokenAddress={address} warning={tokenWarning} />}
{detailedToken && <BalanceSummary currency={detailedToken} chain={chain} multiChainMap={multiChainMap} />} {!isInfoTDPEnabled && detailedToken && <BalanceSummary token={detailedToken} />}
{isInfoTDPEnabled && ( {isInfoTDPEnabled && (
<TokenDescription <TokenDescription
tokenAddress={address} tokenAddress={address}
@ -307,9 +296,7 @@ export default function TokenDetails({
/> />
)} )}
</RightPanel> </RightPanel>
{detailedToken && ( {!isInfoTDPEnabled && detailedToken && <MobileBalanceSummaryFooter token={detailedToken} />}
<MobileBalanceSummaryFooter currency={detailedToken} pageChainBalance={multiChainMap[chain].balance} />
)}
<TokenSafetyModal <TokenSafetyModal
isOpen={openTokenSafetyModal || !!continueSwap} isOpen={openTokenSafetyModal || !!continueSwap}

@ -114,7 +114,7 @@ const ClickableContent = styled.div<{ gap?: number }>`
cursor: pointer; cursor: pointer;
` `
const ClickableName = styled(ClickableContent)` const ClickableName = styled(ClickableContent)`
gap: 14px; gap: 12px;
max-width: 100%; max-width: 100%;
` `
const StyledHeaderRow = styled(StyledTokenRow)` const StyledHeaderRow = styled(StyledTokenRow)`

@ -3,14 +3,6 @@
exports[`LoadedRow.tsx renders a row 1`] = ` exports[`LoadedRow.tsx renders a row 1`] = `
<DocumentFragment> <DocumentFragment>
.c7 { .c7 {
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: relative; position: relative;
top: 0; top: 0;
left: 0; left: 0;
@ -102,7 +94,7 @@ exports[`LoadedRow.tsx renders a row 1`] = `
} }
.c6 { .c6 {
gap: 14px; gap: 12px;
max-width: 100%; max-width: 100%;
} }

@ -6,6 +6,7 @@ import { AutoRow } from 'components/Row'
import { connections, deprecatedNetworkConnection, networkConnection } from 'connection' import { connections, deprecatedNetworkConnection, networkConnection } from 'connection'
import { ActivationStatus, useActivationState } from 'connection/activate' import { ActivationStatus, useActivationState } from 'connection/activate'
import { isSupportedChain } from 'constants/chains' import { isSupportedChain } from 'constants/chains'
import { useAndroidGALaunchFlagEnabled } from 'featureFlags/flags/androidGALaunch'
import { useFallbackProviderEnabled } from 'featureFlags/flags/fallbackProvider' import { useFallbackProviderEnabled } from 'featureFlags/flags/fallbackProvider'
import { useEffect } from 'react' import { useEffect } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
@ -40,6 +41,7 @@ const PrivacyPolicyWrapper = styled.div`
export default function WalletModal({ openSettings }: { openSettings: () => void }) { export default function WalletModal({ openSettings }: { openSettings: () => void }) {
const { connector, chainId } = useWeb3React() const { connector, chainId } = useWeb3React()
const isAndroidGALaunched = useAndroidGALaunchFlagEnabled()
const { activationState } = useActivationState() const { activationState } = useActivationState()
const fallbackProviderEnabled = useFallbackProviderEnabled() const fallbackProviderEnabled = useFallbackProviderEnabled()
@ -66,7 +68,7 @@ export default function WalletModal({ openSettings }: { openSettings: () => void
<AutoColumn gap="16px"> <AutoColumn gap="16px">
<OptionGrid data-testid="option-grid"> <OptionGrid data-testid="option-grid">
{connections {connections
.filter((connection) => connection.shouldDisplay()) .filter((connection) => connection.shouldDisplay(isAndroidGALaunched))
.map((connection) => ( .map((connection) => (
<Option key={connection.getName()} connection={connection} /> <Option key={connection.getName()} connection={connection} />
))} ))}

@ -6,7 +6,7 @@ import { useTheme } from 'styled-components'
import { ThemedText } from 'theme/components' import { ThemedText } from 'theme/components'
import { useFormatter } from 'utils/formatNumbers' import { useFormatter } from 'utils/formatNumbers'
export function TradeSummary({ trade }: { trade: Pick<InterfaceTrade, 'inputAmount' | 'outputAmount'> }) { export function TradeSummary({ trade }: { trade: Pick<InterfaceTrade, 'inputAmount' | 'postTaxOutputAmount'> }) {
const theme = useTheme() const theme = useTheme()
const { formatReviewSwapCurrencyAmount } = useFormatter() const { formatReviewSwapCurrencyAmount } = useFormatter()
@ -17,9 +17,9 @@ export function TradeSummary({ trade }: { trade: Pick<InterfaceTrade, 'inputAmou
{formatReviewSwapCurrencyAmount(trade.inputAmount)} {trade.inputAmount.currency.symbol} {formatReviewSwapCurrencyAmount(trade.inputAmount)} {trade.inputAmount.currency.symbol}
</ThemedText.LabelSmall> </ThemedText.LabelSmall>
<ArrowRight color={theme.neutral1} size="12px" /> <ArrowRight color={theme.neutral1} size="12px" />
<CurrencyLogo currency={trade.outputAmount.currency} size="16px" /> <CurrencyLogo currency={trade.postTaxOutputAmount.currency} size="16px" />
<ThemedText.LabelSmall color="neutral1"> <ThemedText.LabelSmall color="neutral1">
{formatReviewSwapCurrencyAmount(trade.outputAmount)} {trade.outputAmount.currency.symbol} {formatReviewSwapCurrencyAmount(trade.postTaxOutputAmount)} {trade.postTaxOutputAmount.currency.symbol}
</ThemedText.LabelSmall> </ThemedText.LabelSmall>
</Row> </Row>
) )

@ -29,18 +29,12 @@ const StyledTextButton = styled(ButtonText)`
color: ${({ theme }) => theme.neutral2}; color: ${({ theme }) => theme.neutral2};
gap: 4px; gap: 4px;
font-weight: 485; font-weight: 485;
transition-duration: ${({ theme }) => theme.transition.duration.fast};
transition-timing-function: ease-in-out;
transition-property: opacity, color, background-color;
&:focus { &:focus {
text-decoration: none; text-decoration: none;
} }
&:active { &:active {
text-decoration: none; text-decoration: none;
} }
:hover {
opacity: ${({ theme }) => theme.opacity.hover};
}
` `
export default function SwapBuyFiatButton() { export default function SwapBuyFiatButton() {

@ -1,37 +0,0 @@
import { TEST_ALLOWED_SLIPPAGE, TEST_TRADE_EXACT_INPUT } from 'test-utils/constants'
import { render, screen } from 'test-utils/render'
import SwapHeader, { SwapTab } from './SwapHeader'
jest.mock('../../featureFlags/flags/limits', () => ({ useLimitsEnabled: () => true }))
describe('SwapHeader.tsx', () => {
it('matches base snapshot', () => {
const { asFragment } = render(
<SwapHeader
trade={TEST_TRADE_EXACT_INPUT}
selectedTab={SwapTab.Swap}
autoSlippage={TEST_ALLOWED_SLIPPAGE}
onClickTab={jest.fn()}
/>
)
expect(asFragment()).toMatchSnapshot()
expect(screen.getByText('Swap')).toBeInTheDocument()
expect(screen.getByText('Buy')).toBeInTheDocument()
expect(screen.getByText('Limit')).toBeInTheDocument()
})
it('calls callback for switching tabs', () => {
const onClickTab = jest.fn()
render(
<SwapHeader
trade={TEST_TRADE_EXACT_INPUT}
selectedTab={SwapTab.Swap}
autoSlippage={TEST_ALLOWED_SLIPPAGE}
onClickTab={onClickTab}
/>
)
screen.getByText('Limit').click()
expect(onClickTab).toHaveBeenCalledWith(SwapTab.Limit)
})
})

@ -1,9 +1,8 @@
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { Percent } from '@uniswap/sdk-core' import { Percent } from '@uniswap/sdk-core'
import { useLimitsEnabled } from 'featureFlags/flags/limits'
import { InterfaceTrade } from 'state/routing/types' import { InterfaceTrade } from 'state/routing/types'
import styled from 'styled-components' import styled from 'styled-components'
import { ButtonText } from 'theme/components' import { ThemedText } from 'theme/components'
import { RowBetween, RowFixed } from '../Row' import { RowBetween, RowFixed } from '../Row'
import SettingsTab from '../Settings' import SettingsTab from '../Settings'
@ -19,49 +18,22 @@ const HeaderButtonContainer = styled(RowFixed)`
gap: 16px; gap: 16px;
` `
const StyledTextButton = styled(ButtonText)<{ $isActive: boolean }>`
color: ${({ theme, $isActive }) => ($isActive ? theme.neutral1 : theme.neutral2)};
gap: 4px;
font-weight: 485;
&:focus {
text-decoration: none;
}
&:active {
text-decoration: none;
}
`
export enum SwapTab {
Swap = 'swap',
Limit = 'limit',
}
export default function SwapHeader({ export default function SwapHeader({
autoSlippage, autoSlippage,
chainId, chainId,
trade, trade,
selectedTab,
onClickTab,
}: { }: {
autoSlippage: Percent autoSlippage: Percent
chainId?: number chainId?: number
trade?: InterfaceTrade trade?: InterfaceTrade
selectedTab: SwapTab
onClickTab: (tab: SwapTab) => void
}) { }) {
const limitsEnabled = useLimitsEnabled()
return ( return (
<StyledSwapHeader> <StyledSwapHeader>
<HeaderButtonContainer> <HeaderButtonContainer>
<StyledTextButton $isActive={selectedTab === SwapTab.Swap} onClick={() => onClickTab?.(SwapTab.Swap)}> <ThemedText.SubHeader>
<Trans>Swap</Trans> <Trans>Swap</Trans>
</StyledTextButton> </ThemedText.SubHeader>
<SwapBuyFiatButton /> <SwapBuyFiatButton />
{limitsEnabled && (
<StyledTextButton $isActive={selectedTab === SwapTab.Limit} onClick={() => onClickTab?.(SwapTab.Limit)}>
<Trans>Limit</Trans>
</StyledTextButton>
)}
</HeaderButtonContainer> </HeaderButtonContainer>
<RowFixed> <RowFixed>
<SettingsTab autoSlippage={autoSlippage} chainId={chainId} trade={trade} /> <SettingsTab autoSlippage={autoSlippage} chainId={chainId} trade={trade} />

@ -24,7 +24,7 @@ export default function SwapModalHeader({
allowedSlippage: Percent allowedSlippage: Percent
}) { }) {
const fiatValueInput = useUSDPrice(trade.inputAmount) const fiatValueInput = useUSDPrice(trade.inputAmount)
const fiatValueOutput = useUSDPrice(trade.outputAmount) const fiatValueOutput = useUSDPrice(trade.postTaxOutputAmount)
return ( return (
<HeaderContainer gap="sm"> <HeaderContainer gap="sm">
@ -40,7 +40,7 @@ export default function SwapModalHeader({
<SwapModalHeaderAmount <SwapModalHeaderAmount
field={Field.OUTPUT} field={Field.OUTPUT}
label={<Trans>You receive</Trans>} label={<Trans>You receive</Trans>}
amount={trade.outputAmount} amount={trade.postTaxOutputAmount}
currency={trade.outputAmount.currency} currency={trade.outputAmount.currency}
usdAmount={fiatValueOutput.data} usdAmount={fiatValueOutput.data}
isLoading={isPreviewTrade(trade) && trade.tradeType === TradeType.EXACT_INPUT} isLoading={isPreviewTrade(trade) && trade.tradeType === TradeType.EXACT_INPUT}

@ -120,12 +120,6 @@ exports[`SwapBuyFiatButton.tsx matches base snapshot 1`] = `
color: #7D7D7D; color: #7D7D7D;
gap: 4px; gap: 4px;
font-weight: 485; font-weight: 485;
-webkit-transition-duration: 125ms;
transition-duration: 125ms;
-webkit-transition-timing-function: ease-in-out;
transition-timing-function: ease-in-out;
-webkit-transition-property: opacity,color,background-color;
transition-property: opacity,color,background-color;
} }
.c4:focus { .c4:focus {
@ -138,10 +132,6 @@ exports[`SwapBuyFiatButton.tsx matches base snapshot 1`] = `
text-decoration: none; text-decoration: none;
} }
.c4:hover {
opacity: 0.6;
}
<div <div
class="c0" class="c0"
> >

@ -1,343 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SwapHeader.tsx matches base snapshot 1`] = `
<DocumentFragment>
.c0 {
box-sizing: border-box;
margin: 0;
min-width: 0;
}
.c9 {
box-sizing: border-box;
margin: 0;
min-width: 0;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
display: inline-block;
text-align: center;
line-height: inherit;
-webkit-text-decoration: none;
text-decoration: none;
font-size: inherit;
padding-left: 16px;
padding-right: 16px;
padding-top: 8px;
padding-bottom: 8px;
color: white;
background-color: primary;
border: 0;
border-radius: 4px;
}
.c1 {
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;
}
.c2 {
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
}
.c4 {
width: -webkit-fit-content;
width: -moz-fit-content;
width: fit-content;
}
.c6 {
outline: none;
border: none;
font-size: inherit;
padding: 0;
margin: 0;
background: none;
cursor: pointer;
-webkit-transition-duration: 125ms;
transition-duration: 125ms;
-webkit-transition-timing-function: ease-in-out;
transition-timing-function: ease-in-out;
-webkit-transition-property: opacity,color,background-color;
transition-property: opacity,color,background-color;
}
.c6:hover {
opacity: 0.6;
}
.c6:focus {
-webkit-text-decoration: underline;
text-decoration: underline;
}
.c10 {
padding: 16px;
width: 100%;
line-height: 24px;
font-weight: 535;
text-align: center;
border-radius: 16px;
outline: none;
border: 1px solid transparent;
color: #222222;
-webkit-text-decoration: none;
text-decoration: none;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-flex-wrap: nowrap;
-ms-flex-wrap: nowrap;
flex-wrap: nowrap;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
cursor: pointer;
position: relative;
z-index: 1;
will-change: transform;
-webkit-transition: -webkit-transform 450ms ease;
-webkit-transition: transform 450ms ease;
transition: transform 450ms ease;
-webkit-transform: perspective(1px) translateZ(0);
-ms-transform: perspective(1px) translateZ(0);
transform: perspective(1px) translateZ(0);
}
.c10:disabled {
opacity: 50%;
cursor: auto;
pointer-events: none;
}
.c10 > * {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.c10 > a {
-webkit-text-decoration: none;
text-decoration: none;
}
.c11 {
padding: 0;
width: -webkit-fit-content;
width: -moz-fit-content;
width: fit-content;
background: none;
-webkit-text-decoration: none;
text-decoration: none;
}
.c11:focus {
-webkit-text-decoration: underline;
text-decoration: underline;
}
.c11:hover {
opacity: 0.9;
}
.c11:active {
-webkit-text-decoration: underline;
text-decoration: underline;
}
.c11:disabled {
opacity: 50%;
cursor: auto;
}
.c8 {
display: inline-block;
height: inherit;
}
.c17 {
height: 24px;
width: 24px;
}
.c17 > * {
fill: #7D7D7D;
}
.c15 {
border: none;
background-color: transparent;
margin: 0;
padding: 0;
cursor: pointer;
outline: none;
}
.c15:not([disabled]):hover {
opacity: 0.7;
}
.c16 {
padding: 6px 12px;
border-radius: 16px;
}
.c14 {
position: relative;
}
.c12 {
color: #7D7D7D;
gap: 4px;
font-weight: 485;
-webkit-transition-duration: 125ms;
transition-duration: 125ms;
-webkit-transition-timing-function: ease-in-out;
transition-timing-function: ease-in-out;
-webkit-transition-property: opacity,color,background-color;
transition-property: opacity,color,background-color;
}
.c12:focus {
-webkit-text-decoration: none;
text-decoration: none;
}
.c12:active {
-webkit-text-decoration: none;
text-decoration: none;
}
.c12:hover {
opacity: 0.6;
}
.c3 {
margin-bottom: 10px;
color: #7D7D7D;
}
.c5 {
padding: 0 12px;
gap: 16px;
}
.c7 {
color: #222222;
gap: 4px;
font-weight: 485;
}
.c7:focus {
-webkit-text-decoration: none;
text-decoration: none;
}
.c7:active {
-webkit-text-decoration: none;
text-decoration: none;
}
.c13 {
color: #7D7D7D;
gap: 4px;
font-weight: 485;
}
.c13:focus {
-webkit-text-decoration: none;
text-decoration: none;
}
.c13:active {
-webkit-text-decoration: none;
text-decoration: none;
}
<div
class="c0 c1 c2 c3"
>
<div
class="c0 c1 c4 c5"
>
<button
class="c6 c7"
>
Swap
</button>
<div
class="c8"
>
<div>
<button
class="c9 c10 c11 c12"
data-testid="buy-fiat-button"
>
Buy
</button>
</div>
</div>
<button
class="c6 c13"
>
Limit
</button>
</div>
<div
class="c0 c1 c4"
>
<div
class="c14"
>
<button
aria-label="Transaction Settings"
class="c15"
data-testid="open-settings-dialog-button"
disabled=""
id="open-settings-dialog-button"
>
<div
class="c0 c1 c16"
>
<svg
class="c17"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M20.83 14.6C19.9 14.06 19.33 13.07 19.33 12C19.33 10.93 19.9 9.93999 20.83 9.39999C20.99 9.29999 21.05 9.1 20.95 8.94L19.28 6.06C19.22 5.95 19.11 5.89001 19 5.89001C18.94 5.89001 18.88 5.91 18.83 5.94C18.37 6.2 17.85 6.34 17.33 6.34C16.8 6.34 16.28 6.19999 15.81 5.92999C14.88 5.38999 14.31 4.41 14.31 3.34C14.31 3.15 14.16 3 13.98 3H10.02C9.83999 3 9.69 3.15 9.69 3.34C9.69 4.41 9.12 5.38999 8.19 5.92999C7.72 6.19999 7.20001 6.34 6.67001 6.34C6.15001 6.34 5.63001 6.2 5.17001 5.94C5.01001 5.84 4.81 5.9 4.72 6.06L3.04001 8.94C3.01001 8.99 3 9.05001 3 9.10001C3 9.22001 3.06001 9.32999 3.17001 9.39999C4.10001 9.93999 4.67001 10.92 4.67001 11.99C4.67001 13.07 4.09999 14.06 3.17999 14.6H3.17001C3.01001 14.7 2.94999 14.9 3.04999 15.06L4.72 17.94C4.78 18.05 4.89 18.11 5 18.11C5.06 18.11 5.12001 18.09 5.17001 18.06C6.11001 17.53 7.26 17.53 8.19 18.07C9.11 18.61 9.67999 19.59 9.67999 20.66C9.67999 20.85 9.82999 21 10.02 21H13.98C14.16 21 14.31 20.85 14.31 20.66C14.31 19.59 14.88 18.61 15.81 18.07C16.28 17.8 16.8 17.66 17.33 17.66C17.85 17.66 18.37 17.8 18.83 18.06C18.99 18.16 19.19 18.1 19.28 17.94L20.96 15.06C20.99 15.01 21 14.95 21 14.9C21 14.78 20.94 14.67 20.83 14.6ZM12 15C10.34 15 9 13.66 9 12C9 10.34 10.34 9 12 9C13.66 9 15 10.34 15 12C15 13.66 13.66 15 12 15Z"
fill="currentColor"
/>
</svg>
</div>
</button>
</div>
</div>
</div>
</DocumentFragment>
`;

@ -6882,7 +6882,7 @@ exports[`SwapLineItem.tsx fee on buy 1`] = `
<span <span
class="" class=""
> >
~-108834.406% ~-105566.373%
</span> </span>
</div> </div>
</div> </div>
@ -7165,14 +7165,14 @@ exports[`SwapLineItem.tsx fee on buy 1`] = `
<div <div
class="c3 css-obwv3p" class="c3 css-obwv3p"
> >
0.00000000000000098 DEF 0.000000000000000952 DEF
</div> </div>
</div> </div>
<div <div
class="c13" class="c13"
/> />
<div> <div>
If the price moves so that you will receive less than 0.00000000000000098 DEF, your transaction will be reverted. This is the minimum amount you are guaranteed to receive. If the price moves so that you will receive less than 0.000000000000000952 DEF, your transaction will be reverted. This is the minimum amount you are guaranteed to receive.
<a <a
class="c14" class="c14"
href="https://support.uniswap.org/hc/en-us/articles/8643879653261-What-is-Price-Slippage-" href="https://support.uniswap.org/hc/en-us/articles/8643879653261-What-is-Price-Slippage-"
@ -7549,7 +7549,7 @@ exports[`SwapLineItem.tsx fee on buy 1`] = `
<div <div
class="c3 c6 css-142zc9n" class="c3 c6 css-142zc9n"
> >
0.00000000000000098 DEF 0.000000000000000952 DEF
</div> </div>
</div> </div>
</div> </div>
@ -8761,7 +8761,7 @@ exports[`SwapLineItem.tsx fee on sell 1`] = `
<span <span
class="" class=""
> >
~-108834.406% ~-105566.373%
</span> </span>
</div> </div>
</div> </div>
@ -9044,14 +9044,14 @@ exports[`SwapLineItem.tsx fee on sell 1`] = `
<div <div
class="c3 css-obwv3p" class="c3 css-obwv3p"
> >
0.00000000000000098 DEF 0.000000000000000952 DEF
</div> </div>
</div> </div>
<div <div
class="c13" class="c13"
/> />
<div> <div>
If the price moves so that you will receive less than 0.00000000000000098 DEF, your transaction will be reverted. This is the minimum amount you are guaranteed to receive. If the price moves so that you will receive less than 0.000000000000000952 DEF, your transaction will be reverted. This is the minimum amount you are guaranteed to receive.
<a <a
class="c14" class="c14"
href="https://support.uniswap.org/hc/en-us/articles/8643879653261-What-is-Price-Slippage-" href="https://support.uniswap.org/hc/en-us/articles/8643879653261-What-is-Price-Slippage-"
@ -9428,7 +9428,7 @@ exports[`SwapLineItem.tsx fee on sell 1`] = `
<div <div
class="c3 c6 css-142zc9n" class="c3 c6 css-142zc9n"
> >
0.00000000000000098 DEF 0.000000000000000952 DEF
</div> </div>
</div> </div>
</div> </div>

@ -4,6 +4,7 @@ import { AlertTriangle } from 'react-feather'
import styled, { css } from 'styled-components' import styled, { css } from 'styled-components'
import { Z_INDEX } from 'theme/zIndex' import { Z_INDEX } from 'theme/zIndex'
import { useIsDarkMode } from '../../theme/components/ThemeToggle'
import { AutoColumn } from '../Column' import { AutoColumn } from '../Column'
export const PageWrapper = styled.div` export const PageWrapper = styled.div`
@ -60,6 +61,109 @@ const SwapWrapperInner = styled.div`
padding-top: 12px; padding-top: 12px;
` `
export const UniswapPopoverContainer = styled.div`
padding: 18px;
color: ${({ theme }) => theme.neutral1};
font-weight: 485;
font-size: 12px;
line-height: 16px;
word-break: break-word;
background: ${({ theme }) => theme.surface1};
border-radius: 20px;
border: 1px solid ${({ theme }) => theme.surface3};
box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.9, theme.shadow1)};
position: relative;
overflow: hidden;
`
const springDownKeyframes = `@keyframes spring-down {
0% { transform: translateY(-80px); }
25% { transform: translateY(4px); }
50% { transform: translateY(-1px); }
75% { transform: translateY(0px); }
100% { transform: translateY(0px); }
}`
const backUpKeyframes = `@keyframes back-up {
0% { transform: translateY(0px); }
100% { transform: translateY(-80px); }
}`
export const UniswapXShine = (props: any) => {
const isDarkMode = useIsDarkMode()
return <UniswapXShineInner {...props} style={{ opacity: isDarkMode ? 0.15 : 0.05, ...props.style }} />
}
const UniswapXShineInner = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
pointer-events: none;
background: linear-gradient(130deg, transparent 20%, ${({ theme }) => theme.accent1}, transparent 80%);
opacity: 0.15;
`
// overflow hidden to hide the SwapMustacheShadow
export const SwapOptInSmallContainer = styled.div<{ visible: boolean }>`
visibility: ${({ visible }) => (visible ? 'visible' : 'hidden')};
overflow: hidden;
margin-top: -14px;
transform: translateY(${({ visible }) => (visible ? 0 : -80)}px);
transition: all ease 400ms;
animation: ${({ visible }) => (visible ? `spring-down 900ms ease forwards` : 'back-up 200ms ease forwards')};
${springDownKeyframes}
${backUpKeyframes}
`
export const UniswapXOptInLargeContainerPositioner = styled.div`
position: absolute;
top: 211px;
right: ${-320 - 15}px;
width: 320px;
align-items: center;
min-height: 170px;
display: flex;
pointer-events: none;
`
export const UniswapXOptInLargeContainer = styled.div<{ visible: boolean }>`
opacity: ${({ visible }) => (visible ? 1 : 0)};
transform: ${({ visible }) => `translateY(${visible ? 0 : -6}px)`};
transition: all ease-in 300ms;
transition-delay: ${({ visible }) => (visible ? '350ms' : '0')};
pointer-events: ${({ visible }) => (visible ? 'auto' : 'none')};
`
export const SwapMustache = styled.main`
position: relative;
background: ${({ theme }) => theme.surface1};
border-radius: 16px;
border-top-left-radius: 0;
border-top-right-radius: 0;
border: 1px solid ${({ theme }) => theme.surface3};
border-top-width: 0;
padding: 18px;
padding-top: calc(12px + 18px);
z-index: 0;
transition: transform 250ms ease;
`
export const SwapMustacheShadow = styled.main`
position: absolute;
top: 0;
left: 0;
border-radius: 16px;
height: 100%;
width: 100%;
transform: translateY(-100%);
box-shadow: 0 0 20px 20px ${({ theme }) => theme.surface2};
background: red;
`
export const ArrowWrapper = styled.div<{ clickable: boolean }>` export const ArrowWrapper = styled.div<{ clickable: boolean }>`
border-radius: 12px; border-radius: 12px;
height: 40px; height: 40px;

@ -11,7 +11,7 @@ import COINBASE_ICON from 'assets/wallets/coinbase-icon.svg'
import UNIWALLET_ICON from 'assets/wallets/uniswap-wallet-icon.png' import UNIWALLET_ICON from 'assets/wallets/uniswap-wallet-icon.png'
import WALLET_CONNECT_ICON from 'assets/wallets/walletconnect-icon.svg' import WALLET_CONNECT_ICON from 'assets/wallets/walletconnect-icon.svg'
import { useSyncExternalStore } from 'react' import { useSyncExternalStore } from 'react'
import { isMobile, isNonSupportedPhone } from 'utils/userAgent' import { isMobile, isNonIOSPhone, isNonSupportedPhone } from 'utils/userAgent'
import { RPC_URLS } from '../constants/networks' import { RPC_URLS } from '../constants/networks'
import { DEPRECATED_RPC_PROVIDERS, RPC_PROVIDERS } from '../constants/providers' import { DEPRECATED_RPC_PROVIDERS, RPC_PROVIDERS } from '../constants/providers'
@ -149,7 +149,8 @@ export const uniwalletWCV2ConnectConnection: Connection = {
hooks: web3WCV2UniwalletConnectHooks, hooks: web3WCV2UniwalletConnectHooks,
type: ConnectionType.UNISWAP_WALLET_V2, type: ConnectionType.UNISWAP_WALLET_V2,
getIcon: () => UNIWALLET_ICON, getIcon: () => UNIWALLET_ICON,
shouldDisplay: () => Boolean(!getIsInjectedMobileBrowser() && !isNonSupportedPhone), shouldDisplay: (isAndroidGALaunched) =>
Boolean(!getIsInjectedMobileBrowser() && (isAndroidGALaunched ? !isNonSupportedPhone : !isNonIOSPhone)),
} }
const [web3CoinbaseWallet, web3CoinbaseWalletHooks] = initializeConnector<CoinbaseWallet>( const [web3CoinbaseWallet, web3CoinbaseWalletHooks] = initializeConnector<CoinbaseWallet>(

@ -26,6 +26,6 @@ export interface Connection {
hooks: Web3ReactHooks hooks: Web3ReactHooks
type: ConnectionType type: ConnectionType
getIcon?(isDarkMode: boolean): string getIcon?(isDarkMode: boolean): string
shouldDisplay(): boolean shouldDisplay(isAndroidGALaunched?: boolean): boolean
overrideActivate?: (chainId?: ChainId) => boolean overrideActivate?: (chainId?: ChainId) => boolean
} }

@ -1,6 +1,8 @@
export const UNI_LIST = 'https://cloudflare-ipfs.com/ipns/tokens.uniswap.org' export const UNI_LIST = 'https://cloudflare-ipfs.com/ipns/tokens.uniswap.org'
export const UNI_EXTENDED_LIST = 'https://cloudflare-ipfs.com/ipns/extendedtokens.uniswap.org' export const UNI_EXTENDED_LIST = 'https://cloudflare-ipfs.com/ipns/extendedtokens.uniswap.org'
const UNI_UNSUPPORTED_LIST = 'https://cloudflare-ipfs.com/ipns/unsupportedtokens.uniswap.org'
const AAVE_LIST = 'tokenlist.aave.eth' const AAVE_LIST = 'tokenlist.aave.eth'
const BA_LIST = 'https://raw.githubusercontent.com/The-Blockchain-Association/sec-notice-list/master/ba-sec-list.json'
// TODO(WEB-2282): Re-enable CMC list once we have a better solution for handling large lists. // TODO(WEB-2282): 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 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_LIST = 'https://tokens.coingecko.com/uniswap/all.json'
@ -25,6 +27,8 @@ export const AVALANCHE_LIST =
export const BASE_LIST = export const BASE_LIST =
'https://raw.githubusercontent.com/ethereum-optimism/ethereum-optimism.github.io/master/optimism.tokenlist.json' 'https://raw.githubusercontent.com/ethereum-optimism/ethereum-optimism.github.io/master/optimism.tokenlist.json'
export const UNSUPPORTED_LIST_URLS: string[] = [BA_LIST, UNI_UNSUPPORTED_LIST]
// default lists to be 'active' aka searched across // default lists to be 'active' aka searched across
export const DEFAULT_ACTIVE_LIST_URLS: string[] = [UNI_LIST] export const DEFAULT_ACTIVE_LIST_URLS: string[] = [UNI_LIST]
export const DEFAULT_INACTIVE_LIST_URLS: string[] = [ export const DEFAULT_INACTIVE_LIST_URLS: string[] = [
@ -48,7 +52,8 @@ export const DEFAULT_INACTIVE_LIST_URLS: string[] = [
CELO_LIST, CELO_LIST,
PLASMA_BNB_LIST, PLASMA_BNB_LIST,
AVALANCHE_LIST, AVALANCHE_LIST,
BASE_LIST BASE_LIST,
...UNSUPPORTED_LIST_URLS,
] ]
export const DEFAULT_LIST_OF_LISTS: string[] = [...DEFAULT_ACTIVE_LIST_URLS, ...DEFAULT_INACTIVE_LIST_URLS] export const DEFAULT_LIST_OF_LISTS: string[] = [...DEFAULT_ACTIVE_LIST_URLS, ...DEFAULT_INACTIVE_LIST_URLS]

@ -126,27 +126,34 @@ export const FALLBACK_URLS = {
* These are the URLs used by the interface when there is not another available source of chain data. * These are the URLs used by the interface when there is not another available source of chain data.
*/ */
export const RPC_URLS = { export const RPC_URLS = {
[ChainId.MAINNET]: [`https://rpc.mevblocker.io/fast`, QUICKNODE_MAINNET_RPC_URL, ...FALLBACK_URLS[ChainId.MAINNET]], [ChainId.MAINNET]: [
[ChainId.GOERLI]: [`https://ethereum-goerli.publicnode.com`, ...FALLBACK_URLS[ChainId.GOERLI]], `https://mainnet.infura.io/v3/${INFURA_KEY}`,
QUICKNODE_MAINNET_RPC_URL,
...FALLBACK_URLS[ChainId.MAINNET],
],
[ChainId.GOERLI]: [`https://goerli.infura.io/v3/${INFURA_KEY}`, ...FALLBACK_URLS[ChainId.GOERLI]],
[ChainId.SEPOLIA]: [`https://sepolia.infura.io/v3/${INFURA_KEY}`, ...FALLBACK_URLS[ChainId.SEPOLIA]], [ChainId.SEPOLIA]: [`https://sepolia.infura.io/v3/${INFURA_KEY}`, ...FALLBACK_URLS[ChainId.SEPOLIA]],
[ChainId.OPTIMISM]: [`https://optimism.llamarpc.com`, ...FALLBACK_URLS[ChainId.OPTIMISM]], [ChainId.OPTIMISM]: [`https://optimism-mainnet.infura.io/v3/${INFURA_KEY}`, ...FALLBACK_URLS[ChainId.OPTIMISM]],
[ChainId.OPTIMISM_GOERLI]: [ [ChainId.OPTIMISM_GOERLI]: [
`https://optimism-goerli.infura.io/v3/${INFURA_KEY}`, `https://optimism-goerli.infura.io/v3/${INFURA_KEY}`,
...FALLBACK_URLS[ChainId.OPTIMISM_GOERLI], ...FALLBACK_URLS[ChainId.OPTIMISM_GOERLI],
], ],
[ChainId.ARBITRUM_ONE]: [`https://arbitrum.llamarpc.com`, ...FALLBACK_URLS[ChainId.ARBITRUM_ONE]], [ChainId.ARBITRUM_ONE]: [
`https://arbitrum-mainnet.infura.io/v3/${INFURA_KEY}`,
...FALLBACK_URLS[ChainId.ARBITRUM_ONE],
],
[ChainId.ARBITRUM_GOERLI]: [ [ChainId.ARBITRUM_GOERLI]: [
`https://arbitrum-goerli.infura.io/v3/${INFURA_KEY}`, `https://arbitrum-goerli.infura.io/v3/${INFURA_KEY}`,
...FALLBACK_URLS[ChainId.ARBITRUM_GOERLI], ...FALLBACK_URLS[ChainId.ARBITRUM_GOERLI],
], ],
[ChainId.POLYGON]: [`https://polygon.llamarpc.com`, ...FALLBACK_URLS[ChainId.POLYGON]], [ChainId.POLYGON]: [`https://polygon-mainnet.infura.io/v3/${INFURA_KEY}`, ...FALLBACK_URLS[ChainId.POLYGON]],
[ChainId.POLYGON_MUMBAI]: [ [ChainId.POLYGON_MUMBAI]: [
`https://polygon-mumbai.infura.io/v3/${INFURA_KEY}`, `https://polygon-mumbai.infura.io/v3/${INFURA_KEY}`,
...FALLBACK_URLS[ChainId.POLYGON_MUMBAI], ...FALLBACK_URLS[ChainId.POLYGON_MUMBAI],
], ],
[ChainId.CELO]: FALLBACK_URLS[ChainId.CELO], [ChainId.CELO]: FALLBACK_URLS[ChainId.CELO],
[ChainId.CELO_ALFAJORES]: FALLBACK_URLS[ChainId.CELO_ALFAJORES], [ChainId.CELO_ALFAJORES]: FALLBACK_URLS[ChainId.CELO_ALFAJORES],
[ChainId.BNB]: ['https://bsc.publicnode.com', 'https://binance.llamarpc.com', ...FALLBACK_URLS[ChainId.BNB]], [ChainId.BNB]: [QUICKNODE_BNB_RPC_URL, ...FALLBACK_URLS[ChainId.BNB]],
[ChainId.AVALANCHE]: [`https://avalanche-mainnet.infura.io/v3/${INFURA_KEY}`, ...FALLBACK_URLS[ChainId.AVALANCHE]], [ChainId.AVALANCHE]: [`https://avalanche-mainnet.infura.io/v3/${INFURA_KEY}`, ...FALLBACK_URLS[ChainId.AVALANCHE]],
[ChainId.BASE]: [`https://base-mainnet.infura.io/v3/${INFURA_KEY}`, ...FALLBACK_URLS[ChainId.BASE]], [ChainId.BASE]: [`https://base-mainnet.infura.io/v3/${INFURA_KEY}`, ...FALLBACK_URLS[ChainId.BASE]],
} }

@ -20,7 +20,6 @@ import {
OP, OP,
PORTAL_ETH_CELO, PORTAL_ETH_CELO,
PORTAL_USDC_CELO, PORTAL_USDC_CELO,
TORN_MAINNET,
USDC_ARBITRUM, USDC_ARBITRUM,
USDC_ARBITRUM_GOERLI, USDC_ARBITRUM_GOERLI,
USDC_AVALANCHE, USDC_AVALANCHE,
@ -68,10 +67,10 @@ const WRAPPED_NATIVE_CURRENCIES_ONLY: ChainTokenList = Object.fromEntries(
export const COMMON_BASES: ChainCurrencyList = { export const COMMON_BASES: ChainCurrencyList = {
[ChainId.MAINNET]: [ [ChainId.MAINNET]: [
nativeOnChain(ChainId.MAINNET), nativeOnChain(ChainId.MAINNET),
TORN_MAINNET,
DAI, DAI,
USDC_MAINNET, USDC_MAINNET,
USDT, USDT,
WBTC,
WRAPPED_NATIVE_CURRENCY[ChainId.MAINNET] as Token, WRAPPED_NATIVE_CURRENCY[ChainId.MAINNET] as Token,
], ],
[ChainId.GOERLI]: [nativeOnChain(ChainId.GOERLI), WRAPPED_NATIVE_CURRENCY[ChainId.GOERLI] as Token], [ChainId.GOERLI]: [nativeOnChain(ChainId.GOERLI), WRAPPED_NATIVE_CURRENCY[ChainId.GOERLI] as Token],

@ -93,6 +93,10 @@ export function checkWarning(tokenAddress: string, chainId?: number | null) {
return null return null
case TOKEN_LIST_TYPES.UNI_EXTENDED: case TOKEN_LIST_TYPES.UNI_EXTENDED:
return MediumWarning return MediumWarning
case TOKEN_LIST_TYPES.UNKNOWN:
return StrongWarning
case TOKEN_LIST_TYPES.BLOCKED:
return BlockedWarning
case TOKEN_LIST_TYPES.BROKEN: case TOKEN_LIST_TYPES.BROKEN:
return BlockedWarning return BlockedWarning
} }

@ -2,7 +2,7 @@ import { TokenInfo } from '@uniswap/token-lists'
import { ListsState } from 'state/lists/reducer' import { ListsState } from 'state/lists/reducer'
import store from '../state' import store from '../state'
import { UNI_EXTENDED_LIST, UNI_LIST} from './lists' import { UNI_EXTENDED_LIST, UNI_LIST, UNSUPPORTED_LIST_URLS } from './lists'
import { COMMON_BASES } from './routing' import { COMMON_BASES } from './routing'
import brokenTokenList from './tokenLists/broken.tokenlist.json' import brokenTokenList from './tokenLists/broken.tokenlist.json'
import { NATIVE_CHAIN_ID } from './tokens' import { NATIVE_CHAIN_ID } from './tokens'
@ -37,6 +37,14 @@ class TokenSafetyLookupTable {
brokenTokenList.tokens.forEach((token) => { brokenTokenList.tokens.forEach((token) => {
this.dict[token.address.toLowerCase()] = TOKEN_LIST_TYPES.BROKEN this.dict[token.address.toLowerCase()] = TOKEN_LIST_TYPES.BROKEN
}) })
// Initialize blocked tokens from all urls included
UNSUPPORTED_LIST_URLS.map((url) => lists.byUrl[url]?.current?.tokens)
.filter((x): x is TokenInfo[] => !!x)
.flat(1)
.forEach((token) => {
this.dict[token.address.toLowerCase()] = TOKEN_LIST_TYPES.BLOCKED
})
} }
checkToken(address: string, chainId?: number | null) { checkToken(address: string, chainId?: number | null) {

@ -15,13 +15,6 @@ export const USDC_MAINNET = new Token(
'USDC', 'USDC',
'USD//C' 'USD//C'
) )
export const TORN_MAINNET = new Token(
ChainId.MAINNET,
'0x77777FeDdddFfC19Ff86DB637967013e6C6A116C',
18,
'TORN',
'Tornado Cash'
)
const USDC_GOERLI = new Token(ChainId.GOERLI, '0x07865c6e87b9f70255377e024ace6630c1eaa37f', 6, 'USDC', 'USD//C') const USDC_GOERLI = new Token(ChainId.GOERLI, '0x07865c6e87b9f70255377e024ace6630c1eaa37f', 6, 'USDC', 'USD//C')
const USDC_SEPOLIA = new Token(ChainId.SEPOLIA, '0x6f14C02Fc1F78322cFd7d707aB90f18baD3B54f5', 6, 'USDC', 'USD//C') const USDC_SEPOLIA = new Token(ChainId.SEPOLIA, '0x6f14C02Fc1F78322cFd7d707aB90f18baD3B54f5', 6, 'USDC', 'USD//C')
export const USDC_OPTIMISM = new Token( export const USDC_OPTIMISM = new Token(

@ -0,0 +1,10 @@
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useAndroidGALaunchFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.androidGALaunch)
}
// todo(kristiehuang): add statsig flag after staging goes out
export function useAndroidGALaunchFlagEnabled(): boolean {
return useAndroidGALaunchFlag() === BaseVariant.Enabled
}

@ -0,0 +1,9 @@
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useFotAdjustmentsFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.fotAdjustedmentsEnabled)
}
export function useFotAdjustmentsEnabled(): boolean {
return useFotAdjustmentsFlag() === BaseVariant.Enabled
}

@ -1,9 +0,0 @@
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useLimitsEnabledFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.limitsEnabled)
}
export function useLimitsEnabled(): boolean {
return useLimitsEnabledFlag() === BaseVariant.Enabled
}

@ -0,0 +1,9 @@
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useUniswapXDefaultEnabledFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.uniswapXDefaultEnabled)
}
export function useUniswapXDefaultEnabled(): boolean {
return useUniswapXDefaultEnabledFlag() === BaseVariant.Enabled
}

@ -0,0 +1,9 @@
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useUniswapXEthOutputFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.uniswapXEthOutputEnabled)
}
export function useUniswapXEthOutputEnabled(): boolean {
return useUniswapXEthOutputFlag() === BaseVariant.Enabled
}

@ -0,0 +1,9 @@
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useUniswapXExactOutputFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.uniswapXExactOutputEnabled)
}
export function useUniswapXExactOutputEnabled(): boolean {
return useUniswapXExactOutputFlag() === BaseVariant.Enabled
}

@ -10,16 +10,20 @@ export enum FeatureFlag {
debounceSwapQuote = 'debounce_swap_quote', debounceSwapQuote = 'debounce_swap_quote',
fallbackProvider = 'fallback_provider', fallbackProvider = 'fallback_provider',
uniswapXSyntheticQuote = 'uniswapx_synthetic_quote', uniswapXSyntheticQuote = 'uniswapx_synthetic_quote',
uniswapXEthOutputEnabled = 'uniswapx_eth_output_enabled',
uniswapXExactOutputEnabled = 'uniswapx_exact_output_enabled',
multichainUX = 'multichain_ux', multichainUX = 'multichain_ux',
currencyConversion = 'currency_conversion', currencyConversion = 'currency_conversion',
fotAdjustedmentsEnabled = 'fot_dynamic_adjustments_enabled',
infoExplore = 'info_explore', infoExplore = 'info_explore',
infoTDP = 'info_tdp', infoTDP = 'info_tdp',
infoPoolPage = 'info_pool_page', infoPoolPage = 'info_pool_page',
infoLiveViews = 'info_live_views', infoLiveViews = 'info_live_views',
uniswapXDefaultEnabled = 'uniswapx_default_enabled',
quickRouteMainnet = 'enable_quick_route_mainnet', quickRouteMainnet = 'enable_quick_route_mainnet',
progressIndicatorV2 = 'progress_indicator_v2', progressIndicatorV2 = 'progress_indicator_v2',
feesEnabled = 'fees_enabled', feesEnabled = 'fees_enabled',
limitsEnabled = 'limits_enabled', androidGALaunch = 'android_ga_launch',
} }
interface FeatureFlagsContextType { interface FeatureFlagsContextType {

@ -97,4 +97,7 @@ gql`
} }
} }
` `
export type { Chain, TokenQuery } from './__generated__/types-and-hooks'
export type TokenQueryData = TokenQuery['token'] export type TokenQueryData = TokenQuery['token']

@ -49,22 +49,6 @@ export const apolloClient = new ApolloClient({
}, },
}, },
}, },
TokenProject: {
fields: {
tokens: {
// cache data may be lost when replacing the tokens array
merge(existing, incoming) {
if (!existing) {
return incoming
} else if (Array.isArray(existing)) {
return [...existing, ...incoming]
} else {
return [existing, ...incoming]
}
},
},
},
},
}, },
}), }),
defaultOptions: { defaultOptions: {

@ -1,39 +1,6 @@
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { PortfolioTokenBalancePartsFragment } from './__generated__/types-and-hooks'
gql` gql`
fragment PortfolioTokenBalanceParts on TokenBalance {
id
quantity
denominatedValue {
id
currency
value
}
token {
id
chain
address
name
symbol
standard
decimals
}
tokenProjectMarket {
id
pricePercentChange(duration: DAY) {
id
value
}
tokenProject {
id
logoUrl
isSpam
}
}
}
query PortfolioBalances($ownerAddress: String!, $chains: [Chain!]!) { query PortfolioBalances($ownerAddress: String!, $chains: [Chain!]!) {
portfolios(ownerAddresses: [$ownerAddress], chains: $chains) { portfolios(ownerAddresses: [$ownerAddress], chains: $chains) {
id id
@ -52,10 +19,35 @@ gql`
} }
} }
tokenBalances { tokenBalances {
...PortfolioTokenBalanceParts id
quantity
denominatedValue {
id
currency
value
}
tokenProjectMarket {
id
pricePercentChange(duration: DAY) {
id
value
}
tokenProject {
id
logoUrl
isSpam
}
}
token {
id
chain
address
name
symbol
standard
decimals
}
} }
} }
} }
` `
export type PortfolioToken = NonNullable<PortfolioTokenBalancePartsFragment['token']>

@ -1,5 +1,5 @@
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { Chain } from 'graphql/data/__generated__/types-and-hooks' import { Chain } from 'graphql/data/Token'
import { chainIdToBackendName } from 'graphql/data/util' import { chainIdToBackendName } from 'graphql/data/util'
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'

@ -6,6 +6,7 @@ export function useIsPoolsPage() {
pathname.startsWith('/pools') || pathname.startsWith('/pools') ||
pathname.startsWith('/pool') || pathname.startsWith('/pool') ||
pathname.startsWith('/add') || pathname.startsWith('/add') ||
pathname.startsWith('/remove') pathname.startsWith('/remove') ||
pathname.startsWith('/increase')
) )
} }

@ -84,13 +84,13 @@ export function useSwapCallback(
? { ? {
tradeType: TradeType.EXACT_INPUT, tradeType: TradeType.EXACT_INPUT,
inputCurrencyAmountRaw: trade.inputAmount.quotient.toString(), inputCurrencyAmountRaw: trade.inputAmount.quotient.toString(),
expectedOutputCurrencyAmountRaw: trade.outputAmount.quotient.toString(), expectedOutputCurrencyAmountRaw: trade.postTaxOutputAmount.quotient.toString(),
minimumOutputCurrencyAmountRaw: trade.minimumAmountOut(allowedSlippage).quotient.toString(), minimumOutputCurrencyAmountRaw: trade.minimumAmountOut(allowedSlippage).quotient.toString(),
} }
: { : {
tradeType: TradeType.EXACT_OUTPUT, tradeType: TradeType.EXACT_OUTPUT,
maximumInputCurrencyAmountRaw: trade.maximumAmountIn(allowedSlippage).quotient.toString(), maximumInputCurrencyAmountRaw: trade.maximumAmountIn(allowedSlippage).quotient.toString(),
outputCurrencyAmountRaw: trade.outputAmount.quotient.toString(), outputCurrencyAmountRaw: trade.postTaxOutputAmount.quotient.toString(),
expectedInputCurrencyAmountRaw: trade.inputAmount.quotient.toString(), expectedInputCurrencyAmountRaw: trade.inputAmount.quotient.toString(),
}), }),
} }

@ -73,8 +73,12 @@ export function useUniversalRouterSwapCallback(
setTraceData('slippageTolerance', options.slippageTolerance.toFixed(2)) setTraceData('slippageTolerance', options.slippageTolerance.toFixed(2))
// universal-router-sdk reconstructs V2Trade objects, so rather than updating the trade amounts to account for tax, we adjust the slippage tolerance as a workaround
// TODO(WEB-2725): update universal-router-sdk to not reconstruct trades
const taxAdjustedSlippageTolerance = options.slippageTolerance.add(trade.totalTaxRate)
const { calldata: data, value } = SwapRouter.swapERC20CallParameters(trade, { const { calldata: data, value } = SwapRouter.swapERC20CallParameters(trade, {
slippageTolerance: options.slippageTolerance, slippageTolerance: taxAdjustedSlippageTolerance,
deadlineOrPreviousBlockhash: options.deadline?.toString(), deadlineOrPreviousBlockhash: options.deadline?.toString(),
inputTokenPermit: options.permit, inputTokenPermit: options.permit,
fee: options.feeOptions, fee: options.feeOptions,

@ -1,10 +1,14 @@
import { SkipToken, skipToken } from '@reduxjs/toolkit/query/react' import { SkipToken, skipToken } from '@reduxjs/toolkit/query/react'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core'
import { useUniswapXDefaultEnabled } from 'featureFlags/flags/uniswapXDefault'
import { useUniswapXEthOutputEnabled } from 'featureFlags/flags/uniswapXEthOutput'
import { useUniswapXExactOutputEnabled } from 'featureFlags/flags/uniswapXExactOutput'
import { useUniswapXSyntheticQuoteEnabled } from 'featureFlags/flags/uniswapXUseSyntheticQuote' import { useUniswapXSyntheticQuoteEnabled } from 'featureFlags/flags/uniswapXUseSyntheticQuote'
import { useFeesEnabled } from 'featureFlags/flags/useFees' import { useFeesEnabled } from 'featureFlags/flags/useFees'
import { useMemo } from 'react' import { useMemo } from 'react'
import { GetQuoteArgs, INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference } from 'state/routing/types' import { GetQuoteArgs, INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference } from 'state/routing/types'
import { currencyAddressForSwapQuote } from 'state/routing/utils' import { currencyAddressForSwapQuote } from 'state/routing/utils'
import { useUserDisabledUniswapX, useUserOptedOutOfUniswapX } from 'state/user/hooks'
/** /**
* Returns query arguments for the Routing API query or undefined if the * Returns query arguments for the Routing API query or undefined if the
@ -18,6 +22,8 @@ export function useRoutingAPIArguments({
amount, amount,
tradeType, tradeType,
routerPreference, routerPreference,
inputTax,
outputTax,
}: { }: {
account?: string account?: string
tokenIn?: Currency tokenIn?: Currency
@ -25,8 +31,15 @@ export function useRoutingAPIArguments({
amount?: CurrencyAmount<Currency> amount?: CurrencyAmount<Currency>
tradeType: TradeType tradeType: TradeType
routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE routerPreference: RouterPreference | typeof INTERNAL_ROUTER_PREFERENCE_PRICE
inputTax: Percent
outputTax: Percent
}): GetQuoteArgs | SkipToken { }): GetQuoteArgs | SkipToken {
const uniswapXForceSyntheticQuotes = useUniswapXSyntheticQuoteEnabled() const uniswapXForceSyntheticQuotes = useUniswapXSyntheticQuoteEnabled()
const userDisabledUniswapX = useUserDisabledUniswapX()
const userOptedOutOfUniswapX = useUserOptedOutOfUniswapX()
const uniswapXEthOutputEnabled = useUniswapXEthOutputEnabled()
const uniswapXExactOutputEnabled = useUniswapXExactOutputEnabled()
const isUniswapXDefaultEnabled = useUniswapXDefaultEnabled()
const feesEnabled = useFeesEnabled() const feesEnabled = useFeesEnabled()
// Don't enable fee logic if this is a quote for pricing // Don't enable fee logic if this is a quote for pricing
@ -51,8 +64,31 @@ export function useRoutingAPIArguments({
tradeType, tradeType,
needsWrapIfUniswapX: tokenIn.isNative, needsWrapIfUniswapX: tokenIn.isNative,
uniswapXForceSyntheticQuotes, uniswapXForceSyntheticQuotes,
userDisabledUniswapX,
userOptedOutOfUniswapX,
uniswapXEthOutputEnabled,
uniswapXExactOutputEnabled,
isUniswapXDefaultEnabled,
sendPortionEnabled, sendPortionEnabled,
inputTax,
outputTax,
}, },
[account, amount, routerPreference, tokenIn, tokenOut, tradeType, uniswapXForceSyntheticQuotes, sendPortionEnabled] [
account,
amount,
routerPreference,
tokenIn,
tokenOut,
tradeType,
uniswapXExactOutputEnabled,
uniswapXForceSyntheticQuotes,
userDisabledUniswapX,
userOptedOutOfUniswapX,
uniswapXEthOutputEnabled,
isUniswapXDefaultEnabled,
sendPortionEnabled,
inputTax,
outputTax,
]
) )
} }

@ -68,12 +68,9 @@ export function useTokenFromActiveNetwork(tokenAddress: string | undefined): Tok
// If the token is on another chain, we cannot fetch it on-chain, and it is invalid. // If the token is on another chain, we cannot fetch it on-chain, and it is invalid.
if (typeof tokenAddress !== 'string' || !isSupportedChain(chainId) || !formattedAddress) return undefined if (typeof tokenAddress !== 'string' || !isSupportedChain(chainId) || !formattedAddress) return undefined
if (isLoading || !chainId) return null if (isLoading || !chainId) return null
if (!decimals?.result?.[0] && parsedSymbol === UNKNOWN_TOKEN_SYMBOL && parsedName === UNKNOWN_TOKEN_NAME) {
return undefined
}
return new Token(chainId, formattedAddress, parsedDecimals, parsedSymbol, parsedName) return new Token(chainId, formattedAddress, parsedDecimals, parsedSymbol, parsedName)
}, [tokenAddress, chainId, formattedAddress, isLoading, decimals?.result, parsedDecimals, parsedSymbol, parsedName]) }, [chainId, tokenAddress, formattedAddress, isLoading, parsedDecimals, parsedSymbol, parsedName])
} }
type TokenMap = { [address: string]: Token } type TokenMap = { [address: string]: Token }

@ -108,10 +108,6 @@ export function useTokenBalance(account?: string, token?: Token): CurrencyAmount
return tokenBalances[token.address] return tokenBalances[token.address]
} }
/**
* Returns balances for tokens on currently-connected chainId via RPC.
* See useCachedPortfolioBalancesQuery for multichain portfolio balances via GQL.
*/
export function useCurrencyBalances( export function useCurrencyBalances(
account?: string, account?: string,
currencies?: (Currency | undefined)[] currencies?: (Currency | undefined)[]

@ -51,7 +51,7 @@ export function formatCommonPropertiesForTrade(
token_in_symbol: trade.inputAmount.currency.symbol, token_in_symbol: trade.inputAmount.currency.symbol,
token_out_symbol: trade.outputAmount.currency.symbol, token_out_symbol: trade.outputAmount.currency.symbol,
token_in_amount: formatToDecimal(trade.inputAmount, trade.inputAmount.currency.decimals), token_in_amount: formatToDecimal(trade.inputAmount, trade.inputAmount.currency.decimals),
token_out_amount: formatToDecimal(trade.outputAmount, trade.outputAmount.currency.decimals), token_out_amount: formatToDecimal(trade.postTaxOutputAmount, trade.outputAmount.currency.decimals),
price_impact_basis_points: isClassicTrade(trade) price_impact_basis_points: isClassicTrade(trade)
? formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)) ? formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade))
: undefined, : undefined,
@ -64,8 +64,6 @@ export function formatCommonPropertiesForTrade(
allowed_slippage: formatPercentNumber(allowedSlippage), allowed_slippage: formatPercentNumber(allowedSlippage),
method: getQuoteMethod(trade), method: getQuoteMethod(trade),
fee_usd: outputFeeFiatValue, fee_usd: outputFeeFiatValue,
token_out_detected_tax: formatPercentNumber(trade.outputTax),
token_in_detected_tax: formatPercentNumber(trade.inputTax),
} }
} }
@ -103,6 +101,8 @@ export const formatSwapQuoteReceivedEventProperties = (
trade: InterfaceTrade, trade: InterfaceTrade,
allowedSlippage: Percent, allowedSlippage: Percent,
swapQuoteLatencyMs: number | undefined, swapQuoteLatencyMs: number | undefined,
inputTax: Percent,
outputTax: Percent,
outputFeeFiatValue: number | undefined outputFeeFiatValue: number | undefined
) => { ) => {
return { return {
@ -112,5 +112,7 @@ export const formatSwapQuoteReceivedEventProperties = (
token_in_amount_max: trade.maximumAmountIn(allowedSlippage).toExact(), token_in_amount_max: trade.maximumAmountIn(allowedSlippage).toExact(),
token_out_amount_min: trade.minimumAmountOut(allowedSlippage).toExact(), token_out_amount_min: trade.minimumAmountOut(allowedSlippage).toExact(),
quote_latency_milliseconds: swapQuoteLatencyMs, quote_latency_milliseconds: swapQuoteLatencyMs,
token_out_detected_tax: formatPercentNumber(outputTax),
token_in_detected_tax: formatPercentNumber(inputTax),
} }
} }

3816
src/locales/af-ZA.po Normal file

File diff suppressed because it is too large Load Diff

3816
src/locales/ar-SA.po Normal file

File diff suppressed because it is too large Load Diff

3816
src/locales/ca-ES.po Normal file

File diff suppressed because it is too large Load Diff

3816
src/locales/cs-CZ.po Normal file

File diff suppressed because it is too large Load Diff

3816
src/locales/da-DK.po Normal file

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More