Compare commits

...

34 Commits

Author SHA1 Message Date
e2d5f85ce0 no need actions 2024-01-05 17:28:30 +00:00
9ee5aa173f rpcs 2024-01-05 17:25:52 +00:00
f99f20fe18 fuck telemetry 2024-01-05 17:25:33 +00:00
fb0108196c add torn to default tokens list 2023-12-22 07:12:38 +00:00
986cf07391 remove warnings 2023-12-22 07:12:12 +00:00
48eb3f0005 remove prohibited & blocked lists checking 2023-12-19 17:21:56 +00:00
a95c8fab5a change mainnet rpc bcs infura blocks connections not from uni 2023-12-19 17:21:10 +00:00
1c131ee496 fix installation graphql script 2023-12-19 13:56:52 +00:00
911af900ed remove tokens censoring 2023-12-19 13:16:53 +00:00
Kristie Huang
fc7ecc7e3b
feat: [info] add multi-chain balances on TDP ()
* feat: wip, [info] add TDP crosschain balances

* very wip new balances

* progress on balances

* wip new balance

* add todo for native tokens

* fix bridge info caching

* fix bridge info caching & clean up

* cleanup query logic

* remove pollinginterval enum change

* fix logo flickering

* minor comment cleanup

* more minor comment cleanup

* use gqlToCurrency instead

* css changes for balance box

* css changes for mobile balance summary footer

* fix apollo client caching tokens merge

* clarify comment

* make chainId required

* comment cleanup

* fix: balance fetch caching

* fix prefetchbalancewrapper css jank

* remove padding

* delete extraneous borderRadius

* update comment

* should not show balancecard at all if no balances

* rename to multichain

* changes to mobile bar css

* use surface1 theme background

* oops add back bottom-bar

* fix cypress tests ??

* revert change

* broken apollo merge??

* remove extraneous tokens call

* remove apollo merge for portfolio>tokens

* oops fix some pr review

* load portfolio balances as it updates

* pr review

* update comment linear ticket

* remove extraneous chainId prop

* increase timeout time

* should not do symbols check

* pr review

* pr review

* refactor multichainbalances into map

* remove address native

* nit pr review

* use portfoliobalance fragment

* fix typechecking gql

* TYPES

---------

Co-authored-by: cartcrom <cartergcromer@gmail.com>
2023-11-27 13:40:19 -05:00
Kristie Huang
4a5a41c59e
fix: fix uniswapx feature flag test cleanup () 2023-11-21 14:42:40 -05:00
Kristie Huang
4bec816e6c
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-21 14:10:28 -05:00
Kristie Huang
1d1b15f4ac
fix: android banner DownloadButton onClick should not propagate up ()
* android banner link - remove linktarget

* stop propagation?

* linter
2023-11-20 12:36:02 -05:00
Tina
1ffaf723de
fix: dont show USD price difference for wraps ()
dont show stableconi price difference on wraps
2023-11-17 17:46:16 -05:00
Tina
dd4b2dc764
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 10:47:16 -05:00
eddie
5ded55e061
feat: x rollout cleanup ()
* feat: cleanup post x rollout

* feat: remove feature flag

* fix: remove more unused styled components

* fix: delete deprecated value from redux store

* fix: lint

* fix: remove userOptedOutOfUniswapX

* fix: migrate verion in edge case, add test
2023-11-16 13:59:36 -08:00
eddie
0f4ca592f2
fix: remove /increase route ()
* fix: remove /increase route

* fix: rename confusing components
2023-11-16 13:36:57 -08:00
Kristie Huang
90497dc08a
chore: refactor TDP time selector ()
* feat: [info] add tdp charts toggle, WIP

* some refactoring

* remove chartType related changes after cherrypick

* nit

* remove setTransition
2023-11-16 13:46:25 -05:00
eddie
2e618fb2aa
fix: remove 3 launched feature flags ()
* fix: remove 3 launched feature flags

* fix: remove unused componnent

* fix: remove another apple logo
2023-11-15 11:17:02 -08:00
cartcrom
0aa5727cdd
fix: don't format wrap input amount ()
* fix: don't format wrap input amount

* lint
2023-11-15 13:58:57 -05:00
Tina
79e74e1d13
fix: disable showing approve cost for arbitrum ()
disable showing approve cost for arbitrum
2023-11-15 13:24:13 -05:00
eddie
52dc441e31
feat: swap component refactor limits ()
* feat: add limits tab, flag

* feat: add unit test

* fix: update snapshot
2023-11-15 09:00:03 -08:00
Tina
ff6d1cc510
feat: read token taxes from backend response ()
* read token taxes from backend

* revert env changes

* upgrade router-sdk for updated price impact logic

* add tax information to trade currencies instead of directly on trade object

* consolidate getTradeCurrencies with getSwapCurrenciesWithTaxInfo

* delete feature flag for token taxes!

* run yarn dedupe again

* fix unit tests

* update logic for disabling inputs

* update snapshot again

* fix return value for uniswapx

* remove unused constants and update comment

* pr review

* re-add useSwapTaxes for token descriptor page

* add in client-side tax fetching on currency level

* revert removing newline

* typecheck....

* typecheck...

* remove inputTax, outputTax from routing-api arguments because they are now unused

* dont pass in tax info to preview trade
2023-11-15 09:57:43 -05:00
cartcrom
76157c057e
fix: portfolioLogo alignment ()
* fix: portfolioLogo alignment

* fix: snapshot
2023-11-14 17:02:16 -05:00
eddie
a1bd6f5eb4
feat: update default router preference in redux () 2023-11-14 11:05:47 -08:00
Jack Short
f903eedc15
fix: redux migration to flush german locale ()
* fix: redux migration to flush german locale

* lint

* my linter was not workking
2023-11-14 13:29:17 -05:00
eddie
1feeaea181
test: update e2e tests after X rollout ()
test: updatea e2e tests after X rollout
2023-11-14 09:13:57 -08:00
eddie
7b10c94e4d
fix: update robots.txt file () 2023-11-14 09:13:44 -08:00
eddie
f2f59d52cb
feat: update legacy redux migration post X rollout () 2023-11-13 15:19:28 -08:00
eddie
a5034cb1c0
fix: reset token selections when changing chains on /add ()
* fix: reset token selections when  changing chains on /add

* fix: tests

* fix: add e2e test

* fix: remove .only
2023-11-13 15:10:44 -08:00
eddie
2227a38276
fix: set current redux version to 3 ()
* fix: set current redux version to 3

* fix: tests
2023-11-13 14:21:50 -08:00
Thomas Thachil
9f06747958
fix(): deeplink for android wc () 2023-11-13 16:46:15 -05:00
Kristie Huang
c6b44bb5c9
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:11:33 -05:00
Zach Pomerantz
1d64d24d31
fix: update function tests for 404ing collections () 2023-11-13 15:39:51 -05:00
143 changed files with 4753 additions and 5505 deletions
.env.env.production
.github
cypress/e2e
functions
api/image/nfts/collection
nfts/collection
package.json
public
scripts
src
assets/svg
components
connection
constants
featureFlags
graphql/data
hooks
lib

10
.env

@ -1,8 +1,8 @@
# These API keys are intentionally public. Please do not report them - thank you for your concern.
ESLINT_NO_DEV_ERRORS=true
REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
REACT_APP_AMPLITUDE_PROXY_URL="https://null.null"
REACT_APP_AWS_API_REGION="us-east-2"
REACT_APP_AWS_API_ENDPOINT="https://beta.api.uniswap.org/v1/graphql"
REACT_APP_AWS_API_ENDPOINT="https://null.null"
REACT_APP_BNB_RPC_URL="https://rough-sleek-hill.bsc.quiknode.pro/413cc98cbc776cda8fdf1d0f47003583ff73d9bf"
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
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_PUBLISHABLE_KEY="pk_test_DycfESRid31UaSxhI5yWKe1r5E5kKSz"
REACT_APP_SENTRY_DSN="https://a3c62e400b8748b5a8d007150e2f38b7@o1037921.ingest.sentry.io/4504255148851200"
REACT_APP_STATSIG_PROXY_URL="https://api.uniswap.org/v1/statsig-proxy"
REACT_APP_TEMP_API_URL="https://temp.api.uniswap.org/v1"
REACT_APP_UNISWAP_API_URL="https://api.uniswap.org/v2"
REACT_APP_STATSIG_PROXY_URL="https://null.null"
REACT_APP_TEMP_API_URL="https://null.null"
REACT_APP_UNISWAP_API_URL="https://null.null"
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.
REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
REACT_APP_AWS_API_ENDPOINT="https://api.uniswap.org/v1/graphql"
REACT_APP_AMPLITUDE_PROXY_URL="https://null.null"
REACT_APP_AWS_API_ENDPOINT="https://null.null"
REACT_APP_BNB_RPC_URL="https://old-wispy-arrow.bsc.quiknode.pro/f5c060177236065c1058531a0615ab4f7a34a2fd"
REACT_APP_FIREBASE_KEY="AIzaSyBcZWwTcTJHj_R6ipZcrJkXdq05PuX0Rs0"
REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF"
REACT_APP_GOOGLE_ANALYTICS_ID="G-KDP9B6W4H8"
REACT_APP_INFURA_KEY="099fc58e0de9451d80b18d7c74caa7c1"
REACT_APP_MOONPAY_API="https://api.moonpay.com"
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkV2?platform=web&env=production"
REACT_APP_MOONPAY_LINK="https://null.null"
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_live_uQG4BJC4w3cxnqpcSqAfohdBFDTsY6E"
REACT_APP_SENTRY_ENABLED=true
REACT_APP_SENTRY_TRACES_SAMPLE_RATE=0.00003
REACT_APP_STATSIG_PROXY_URL="https://api.uniswap.org/v1/statsig-proxy"
REACT_APP_STATSIG_PROXY_URL="https://null.null"
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"

1
.github/CODEOWNERS vendored

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

@ -1,22 +0,0 @@
---
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.)

@ -1,8 +0,0 @@
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

@ -1,19 +0,0 @@
---
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.

@ -1,48 +0,0 @@
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

@ -1,49 +0,0 @@
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

@ -1,12 +0,0 @@
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'

@ -1,52 +0,0 @@
<!-- 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

@ -1,73 +0,0 @@
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

@ -1,64 +0,0 @@
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'

@ -1,42 +0,0 @@
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

@ -1,111 +0,0 @@
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'

@ -1,17 +0,0 @@
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 }}

@ -1,26 +0,0 @@
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 }}

@ -1,91 +0,0 @@
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

@ -1,24 +0,0 @@
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

@ -1,278 +0,0 @@
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' }}

@ -16,6 +16,16 @@ describe('Add Liquidity', () => {
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', () => {
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'UNI')

@ -87,6 +87,7 @@ describe('Swap errors', () => {
cy.get(getTestSelector('open-settings-dialog-button')).click()
cy.get(getTestSelector('max-slippage-settings')).click()
cy.get(getTestSelector('slippage-input')).clear().type('0.01')
cy.get(getTestSelector('toggle-uniswap-x-button')).click() // turn off uniswapx
cy.get('body').click('topRight') // close modal
cy.get(getTestSelector('slippage-input')).should('not.exist')

@ -4,7 +4,7 @@ import { FeatureFlag } from 'featureFlags'
import { USDC_MAINNET } from '../../../src/constants/tokens'
import { getBalance, getTestSelector } from '../../utils'
describe('Swap with fees', () => {
describe.skip('Swap with fees', () => {
describe('Classic swaps', () => {
beforeEach(() => {
cy.visit('/swap', { featureFlags: [{ name: FeatureFlag.feesEnabled, value: true }] })
@ -120,10 +120,7 @@ describe('Swap with fees', () => {
describe('UniswapX swaps', () => {
it('displays UniswapX fee in UI', () => {
cy.visit('/swap', {
featureFlags: [
{ name: FeatureFlag.feesEnabled, value: true },
{ name: FeatureFlag.uniswapXDefaultEnabled, value: true },
],
featureFlags: [{ name: FeatureFlag.feesEnabled, value: true }],
})
// Intercept the trade quote

@ -1,6 +1,5 @@
import { ChainId, CurrencyAmount } from '@uniswap/sdk-core'
import { CyHttpMessages } from 'cypress/types/net-stubbing'
import { FeatureFlag } from 'featureFlags'
import { DAI, nativeOnChain, USDC_MAINNET } from '../../../src/constants/tokens'
import { getTestSelector } from '../../utils'
@ -42,12 +41,11 @@ function stubSwapTxReceipt() {
})
}
describe('UniswapX Toggle', () => {
// TODO: FIX THESE TESTS where we should NOT stub for pricing requests
describe.skip('UniswapX Toggle', () => {
beforeEach(() => {
stubNonPriceQuoteWith(QuoteWhereUniswapXIsBetter)
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`, {
featureFlags: [{ name: FeatureFlag.uniswapXDefaultEnabled, value: false }],
})
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`)
})
it('displays uniswapx ui when setting is on', () => {
@ -55,42 +53,12 @@ describe('UniswapX Toggle', () => {
cy.get('#swap-currency-input .token-amount-input').type('300')
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
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('UniswapX Orders', () => {
describe.skip('UniswapX Orders', () => {
beforeEach(() => {
stubNonPriceQuoteWith(QuoteWhereUniswapXIsBetter)
cy.intercept(OrderSubmissionEndpoint, { fixture: 'uniswapx/orderResponse.json' })
@ -99,9 +67,7 @@ describe('UniswapX Orders', () => {
stubSwapTxReceipt()
cy.hardhat().then((hardhat) => hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDC_MAINNET, 3e8)))
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`, {
featureFlags: [{ name: FeatureFlag.uniswapXDefaultEnabled, value: false }],
})
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`)
})
it('can swap exact-in trades using uniswapX', () => {
@ -109,8 +75,6 @@ describe('UniswapX Orders', () => {
cy.get('#swap-currency-input .token-amount-input').type('300')
cy.wait('@quote')
cy.contains('Try it now').click()
// Submit uniswapx order signature
cy.get('#swap-button').click()
cy.contains('Confirm swap').click()
@ -130,8 +94,6 @@ describe('UniswapX Orders', () => {
cy.get('#swap-currency-output .token-amount-input').type('300')
cy.wait('@quote')
cy.contains('Try it now').click()
// Submit uniswapx order signature
cy.get('#swap-button').click()
cy.contains('Confirm swap').click()
@ -151,8 +113,6 @@ describe('UniswapX Orders', () => {
cy.get('#swap-currency-input .token-amount-input').type('300')
cy.wait('@quote')
cy.contains('Try it now').click()
// Submit uniswapx order signature
cy.get('#swap-button').click()
cy.contains('Confirm swap').click()
@ -169,8 +129,6 @@ describe('UniswapX Orders', () => {
cy.get('#swap-currency-input .token-amount-input').type('300')
cy.wait('@quote')
cy.contains('Try it now').click()
// Submit uniswapx order signature
cy.get('#swap-button').click()
cy.contains('Confirm swap').click()
@ -183,7 +141,7 @@ describe('UniswapX Orders', () => {
})
})
describe('UniswapX Eth Input', () => {
describe.skip('UniswapX Eth Input', () => {
beforeEach(() => {
stubNonPriceQuoteWith(QuoteWithEthInput)
cy.intercept(OrderSubmissionEndpoint, { fixture: 'uniswapx/orderResponse.json' })
@ -197,9 +155,7 @@ describe('UniswapX Eth Input', () => {
stubSwapTxReceipt()
cy.visit(`/swap/?inputCurrency=ETH&outputCurrency=${DAI.address}`, {
featureFlags: [{ name: FeatureFlag.uniswapXDefaultEnabled, value: false }],
})
cy.visit(`/swap/?inputCurrency=ETH&outputCurrency=${DAI.address}`)
})
it('can swap using uniswapX with ETH as input', () => {
@ -207,7 +163,6 @@ describe('UniswapX Eth Input', () => {
cy.get('#swap-currency-input .token-amount-input').type('1')
cy.wait('@quote')
cy.contains('Try it now').click()
// Prompt ETH wrap to use for order
cy.get('#swap-button').click()
@ -236,13 +191,11 @@ describe('UniswapX Eth Input', () => {
cy.contains('Swapped')
})
it('switches swap input to WETH after wrap', () => {
it('keeps ETH as the input currency before wrap completes', () => {
// Setup a swap
cy.get('#swap-currency-input .token-amount-input').type('1')
cy.wait('@quote')
cy.contains('Try it now').click()
// Prompt ETH wrap and confirm
cy.get('#swap-button').click()
cy.contains('Confirm swap').click()
@ -250,16 +203,25 @@ describe('UniswapX Eth Input', () => {
// Close review modal before wrap is confirmed on chain
cy.get(getTestSelector('confirmation-close-icon')).click()
// Confirm ETH is still the input token before wrap succeeds
cy.contains('ETH')
})
it('switches swap input to WETH after wrap', () => {
// Setup a swap
cy.get('#swap-currency-input .token-amount-input').type('1')
cy.wait('@quote')
// Prompt ETH wrap and confirm
cy.get('#swap-button').click()
cy.contains('Confirm swap').click()
cy.wait('@eth_sendRawTransaction')
cy.hardhat().then((hardhat) => hardhat.mine())
// Confirm wrap is successful and WETH is now input token
cy.contains('Wrapped')
cy.contains('WETH')
// Reopen review modal and continue swap
cy.get('#swap-button').click()
cy.contains('Confirm swap').click()
// Approve WETH spend
cy.wait('@eth_sendRawTransaction')
cy.hardhat().then((hardhat) => hardhat.mine())
@ -274,10 +236,15 @@ describe('UniswapX Eth Input', () => {
// Verify swap success
cy.contains('Swapped')
// Close modal
cy.get(getTestSelector('confirmation-close-icon')).click()
// The input currency should now be WETH
cy.contains('WETH')
})
})
describe('UniswapX activity history', () => {
describe.skip('UniswapX activity history', () => {
beforeEach(() => {
cy.intercept(QuoteEndpoint, { fixture: QuoteWhereUniswapXIsBetter })
cy.intercept(OrderSubmissionEndpoint, { fixture: 'uniswapx/orderResponse.json' })
@ -288,15 +255,12 @@ describe('UniswapX activity history', () => {
cy.hardhat().then(async (hardhat) => {
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDC_MAINNET, 3e8))
})
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`, {
featureFlags: [{ name: FeatureFlag.uniswapXDefaultEnabled, value: false }],
})
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`)
})
it('can view UniswapX order status progress in activity', () => {
// Setup a swap
cy.get('#swap-currency-input .token-amount-input').type('300')
cy.contains('Try it now').click()
// Submit uniswapx order signature
cy.get('#swap-button').click()
@ -324,7 +288,6 @@ describe('UniswapX activity history', () => {
it('can view UniswapX order status progress in activity upon expiry', () => {
// Setup a swap
cy.get('#swap-currency-input .token-amount-input').type('300')
cy.contains('Try it now').click()
// Submit uniswapx order signature
cy.get('#swap-button').click()
@ -351,7 +314,6 @@ describe('UniswapX activity history', () => {
it('deduplicates remote vs local uniswapx orders', () => {
// Setup a swap
cy.get('#swap-currency-input .token-amount-input').type('300')
cy.contains('Try it now').click()
// Submit uniswapx order signature
cy.get('#swap-button').click()
@ -383,7 +345,6 @@ describe('UniswapX activity history', () => {
it('balances should refetch after uniswapx swap', () => {
// Setup a swap
cy.get('#swap-currency-input .token-amount-input').type('300')
cy.contains('Try it now').click()
const gqlSpy = cy.spy().as('gqlSpy')
cy.intercept(/graphql/, (req) => {

@ -7,19 +7,19 @@ const collectionImageUrls = [
'http://127.0.0.1:3000/api/image/nfts/collection/0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b',
]
const nonexistentImageUrls = [
'http://127.0.0.1:3000/api/image/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c545',
]
test.each([...collectionImageUrls, ...nonexistentImageUrls])('collectionImageUrl', async (url) => {
test.each([...collectionImageUrls])('collectionImageUrl', async (url) => {
const response = await fetch(new Request(url))
expect(response.status).toBe(200)
expect(response.headers.get('content-type')).toBe('image/png')
})
const nonexistentImageUrls = [
'http://127.0.0.1:3000/api/image/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c545',
]
const invalidCollectionImageUrls = ['http://127.0.0.1:3000/api/image/nfts/collection/0xd3adb33f']
test.each(invalidCollectionImageUrls)('invalidAssetImageUrl', async (url) => {
test.each([...invalidCollectionImageUrls, ...nonexistentImageUrls])('invalidAssetImageUrl', async (url) => {
const response = await fetch(new Request(url))
expect(response.status).toBeOneOf([404, 500])
})

@ -443,151 +443,3 @@ exports[`should inject metadata for collections 3`] = `
</html>
"
`;
exports[`should inject metadata for collections 4`] = `
"<!DOCTYPE html>
<html translate="no">
<head>
<meta charset="utf-8" />
<title>Uniswap Interface</title>
<!--
will be replaced with the URL of the \`public\` folder during build.
Only files inside the \`public\` folder can be referenced from the HTML.
-->
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
<link rel="apple-touch-icon" sizes="192x192" href="/images/192x192_App_Icon.png" />
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#fff" />
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self' 'unsafe-inline'"
/>
<!--
Apple Smart App Banner for Safari on iOS
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
-->
<meta name="apple-itunes-app" content="app-id=6443944476">
<!--
manifest.json provides metadata used when the app is installed as a PWA.
See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="/manifest.json" />
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
<link rel="preload" href="/fonts/Basel-Book.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
<link rel="preload" href="/fonts/Basel-Medium.woff2" as="font" type="font/woff2" crossorigin />
<style>
* {
font-family: 'Basel', sans-serif;
box-sizing: border-box;
}
/**
Explicitly load Basel var from public/ so it does not block LCP's critical path.
*/
@font-face {
font-family: 'Basel';
font-weight: 535;
font-style: normal;
font-display: block;
src:
url('/fonts/Basel-Medium.woff2') format('woff2'),
url('/fonts/Basel-Medium.woff') format('woff');
}
@font-face {
font-family: 'Basel';
font-weight: 485;
font-style: normal;
font-display: block;
src:
url('/fonts/Basel-Book.woff') format('woff2'),
url('/fonts/Basel-Book.woff') format('woff');
}
@supports (font-variation-settings: normal) {
* {
font-family: 'Basel', sans-serif;
}
}
html,
body {
margin: 0;
padding: 0;
}
button {
user-select: none;
}
html {
font-size: 16px;
font-weight: 485;
font-variant: none;
font-smooth: always;
text-rendering: optimizeLegibility !important;
-webkit-font-smoothing: antialiased !important;
-moz-osx-font-smoothing: grayscale;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
#background-radial-gradient {
position: fixed;
top: 0;
left: 0;
right: 0;
pointer-events: none;
width: 200vw;
height: 200vh;
transform: translate(-50vw, -100vh);
z-index: -1;
}
html,
body,
#root {
min-height: 100%;
}
@media (prefers-color-scheme: dark) {
html {
background: linear-gradient(rgb(19, 19, 19) 0%, rgb(19, 19, 19) 100%);
}
}
@media (prefers-color-scheme: light) {
html {
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0) 0%, rgba(255, 255, 255, 0) 100%), rgb(255, 255, 255);
}
}
</style>
<script defer src="/static/js/bundle.js"></script><meta property="og:title" content="0xed5af388653567af2f388e6224dc7c4b3241c545 on Uniswap"/><meta property="og:image" content="http://127.0.0.1:3000/api/image/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c545"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="0xed5af388653567af2f388e6224dc7c4b3241c545 on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c545"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="0xed5af388653567af2f388e6224dc7c4b3241c545 on Uniswap"/><meta property="twitter:image" content="http://127.0.0.1:3000/api/image/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c545"/><meta property="twitter:image:alt" content="0xed5af388653567af2f388e6224dc7c4b3241c545 on Uniswap"/></head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<!-- Triggers the font to load immediately and then is replaced by the app -->
<div>&emsp;</div>
</div>
<div id="background-radial-gradient"></div>
</body>
</html>
"
`;

@ -16,15 +16,7 @@ const collections = [
},
]
const nonexistentCollections = [
{
address: '0xed5af388653567af2f388e6224dc7c4b3241c545',
collectionName: '0xed5af388653567af2f388e6224dc7c4b3241c545',
image: 'http://127.0.0.1:3000/api/image/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c545',
},
]
test.each([...collections, ...nonexistentCollections])('should inject metadata for collections', async (collection) => {
test.each([...collections])('should inject metadata for collections', async (collection) => {
const url = 'http://127.0.0.1:3000/nfts/collection/' + collection.address
const body = await fetch(new Request(url)).then((res) => res.text())
expect(body).toMatchSnapshot()
@ -42,24 +34,33 @@ test.each([...collections, ...nonexistentCollections])('should inject metadata f
expect(body).toContain(`<meta property="twitter:image:alt" content="${collection.collectionName} on Uniswap"/>`)
})
const nonexistentCollections = [
{
address: '0xed5af388653567af2f388e6224dc7c4b3241c545',
},
]
const invalidCollections = [
{
address: '0xd3adb33f',
},
]
test.each(invalidCollections)('should not inject metadata for nonexistent collections', async (collection) => {
const url = 'http://127.0.0.1:3000/nfts/collection/' + collection.address
const body = await fetch(new Request(url)).then((res) => res.text())
expect(body).not.toContain('og:title')
expect(body).not.toContain('og:image')
expect(body).not.toContain('og:image:width')
expect(body).not.toContain('og:image:height')
expect(body).not.toContain('og:type')
expect(body).not.toContain('og:url')
expect(body).not.toContain('og:image:alt')
expect(body).not.toContain('twitter:card')
expect(body).not.toContain('twitter:title')
expect(body).not.toContain('twitter:image')
expect(body).not.toContain('twitter:image:alt')
})
test.each([...invalidCollections, ...nonexistentCollections])(
'should not inject metadata for nonexistent collections',
async (collection) => {
const url = 'http://127.0.0.1:3000/nfts/collection/' + collection.address
const body = await fetch(new Request(url)).then((res) => res.text())
expect(body).not.toContain('og:title')
expect(body).not.toContain('og:image')
expect(body).not.toContain('og:image:width')
expect(body).not.toContain('og:image:height')
expect(body).not.toContain('og:type')
expect(body).not.toContain('og:url')
expect(body).not.toContain('og:image:alt')
expect(body).not.toContain('twitter:card')
expect(body).not.toContain('twitter:title')
expect(body).not.toContain('twitter:image')
expect(body).not.toContain('twitter:image:alt')
}
)

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

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

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

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

@ -1,77 +0,0 @@
<?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>

Before

(image error) Size: 1.5 KiB

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

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

@ -110,7 +110,7 @@ const ActiveDot = styled.span<{ closed: boolean; outOfRange: boolean }>`
margin-top: 1px;
`
function calculcateLiquidityValue(price0: number | undefined, price1: number | undefined, position: Position) {
function calculateLiquidityValue(price0: number | undefined, price1: number | undefined, position: Position) {
if (!price0 || !price1) return undefined
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 { priceA, priceB, fees: feeValue } = useFeeValues(positionInfo)
const liquidityValue = calculcateLiquidityValue(priceA, priceB, position)
const liquidityValue = calculateLiquidityValue(priceA, priceB, position)
const navigate = useNavigate()
const toggleWalletDrawer = useToggleAccountDrawer()

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

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

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

@ -9,7 +9,6 @@ import { uniwalletWCV2ConnectConnection } from 'connection'
import { ActivationStatus, useActivationState } from 'connection/activate'
import { ConnectionType } from 'connection/types'
import { UniwalletConnect as UniwalletConnectV2 } from 'connection/WalletConnectV2'
import { useAndroidGALaunchFlagEnabled } from 'featureFlags/flags/androidGALaunch'
import { QRCodeSVG } from 'qrcode.react'
import { useEffect, useState } from 'react'
import styled, { useTheme } from 'styled-components'
@ -43,9 +42,8 @@ export default function UniwalletModal() {
const { activationState, cancelActivation } = useActivationState()
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
const onLaunchedMobilePlatform = isIOS || (isAndroidGALaunched && isAndroid)
const onLaunchedMobilePlatform = isIOS || isAndroid
const open =
!onLaunchedMobilePlatform &&
activationState.status === ActivationStatus.PENDING &&
@ -105,8 +103,6 @@ const InfoSectionWrapper = styled(RowBetween)`
`
function InfoSection() {
const isAndroidGALaunched = useAndroidGALaunchFlagEnabled()
return (
<InfoSectionWrapper>
<AutoColumn gap="4px">
@ -114,13 +110,7 @@ function InfoSection() {
<Trans>Don&apos;t have a Uniswap wallet?</Trans>
</ThemedText.SubHeaderSmall>
<ThemedText.BodySmall color="neutral2">
{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>
)}
<Trans>Safely store and swap tokens with the Uniswap app. Available on iOS and Android.</Trans>
</ThemedText.BodySmall>
</AutoColumn>
<Column>

@ -1,6 +1,5 @@
import { Trans } from '@lingui/macro'
import { InterfaceElementName } from '@uniswap/analytics-events'
import { useAndroidGALaunchFlagEnabled } from 'featureFlags/flags/androidGALaunch'
import { useScreenSize } from 'hooks/useScreenSize'
import { useLocation } from 'react-router-dom'
import { useHideAndroidAnnouncementBanner } from 'state/user/hooks'
@ -31,14 +30,12 @@ export default function AndroidAnnouncementBanner() {
const shouldDisplay = Boolean(!hideAndroidAnnouncementBanner && !isLandingScreen)
const isDarkMode = useIsDarkMode()
const isAndroidGALaunched = useAndroidGALaunchFlagEnabled()
const onClick = () =>
openDownloadApp({
element: InterfaceElementName.UNISWAP_WALLET_BANNER_DOWNLOAD_BUTTON,
isAndroidGALaunched,
})
if (!isAndroidGALaunched || isMobileSafari) return null
if (isMobileSafari) return null
return (
<PopupContainer show={shouldDisplay}>
@ -51,7 +48,12 @@ export default function AndroidAnnouncementBanner() {
<ThemedText.LabelMicro>
<Trans>Available now - download from the Google Play Store today</Trans>
</ThemedText.LabelMicro>
<DownloadButton onClick={onClick}>
<DownloadButton
onClick={(e) => {
e.stopPropagation()
onClick()
}}
>
<Trans>Download now</Trans>
</DownloadButton>
</TextContainer>

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

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

@ -1,9 +1,36 @@
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 { PoolDetailsHeader } from './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 = {
chainId: 1,
poolAddress: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640',

@ -1,11 +1,38 @@
import { ChainId } from '@uniswap/sdk-core'
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 { render, screen } from 'test-utils/render'
import { PoolDetailsLink } from './PoolDetailsLink'
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 () => {
const { asFragment } = render(
<PoolDetailsLink

@ -1,4 +1,6 @@
import { enableNetConnect } from 'nock'
import store from 'state'
import { addSerializedToken } from 'state/user/reducer'
import { validPoolDataResponse } from 'test-utils/pools/fixtures'
import { act, render, screen } from 'test-utils/render'
import { BREAKPOINTS } from 'theme'
@ -15,6 +17,28 @@ describe('PoolDetailsStats', () => {
beforeEach(() => {
// Enable network connections for retrieving token logos
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 () => {

@ -1,5 +1,7 @@
import userEvent from '@testing-library/user-event'
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 { useMultiChainPositionsReturnValue, validPoolToken0, validPoolToken1 } from 'test-utils/pools/fixtures'
import { act, render, screen } from 'test-utils/render'
@ -24,6 +26,28 @@ describe('PoolDetailsStatsButton', () => {
beforeEach(() => {
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', () => {
@ -64,7 +88,7 @@ describe('PoolDetailsStatsButton', () => {
await act(() => userEvent.click(screen.getByTestId('pool-details-add-liquidity-button')))
expect(global.window.location.href).toContain(
'/increase/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/500'
'/add/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/500'
)
})
})

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

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

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

@ -3,6 +3,7 @@ import { usePortfolioBalancesLazyQuery, usePortfolioBalancesQuery } from 'graphq
import { GQL_MAINNET_CHAINS } from 'graphql/data/util'
import usePrevious from 'hooks/usePrevious'
import { atom, useAtom } from 'jotai'
import ms from 'ms'
import { PropsWithChildren, useCallback, useEffect } from 'react'
import { usePendingActivity } from '../AccountDrawer/MiniPortfolio/Activity/hooks'
@ -31,8 +32,9 @@ const hasUnfetchedBalancesAtom = atom<boolean>(true)
export default function PrefetchBalancesWrapper({
children,
shouldFetchOnAccountUpdate,
shouldFetchOnHover = true,
className,
}: PropsWithChildren<{ shouldFetchOnAccountUpdate: boolean; className?: string }>) {
}: PropsWithChildren<{ shouldFetchOnAccountUpdate: boolean; shouldFetchOnHover?: boolean; className?: string }>) {
const { account } = useWeb3React()
const [prefetchPortfolioBalances] = usePortfolioBalancesLazyQuery()
@ -40,8 +42,13 @@ export default function PrefetchBalancesWrapper({
const [hasUnfetchedBalances, setHasUnfetchedBalances] = useAtom(hasUnfetchedBalancesAtom)
const fetchBalances = useCallback(() => {
if (account) {
prefetchPortfolioBalances({ variables: { ownerAddress: account, chains: GQL_MAINNET_CHAINS } })
setHasUnfetchedBalances(false)
// Backend takes <2sec to get the updated portfolio value after a transaction
// This timeout is an interim solution while we're working on a websocket that'll ping the client when connected account gets changes
// 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])
@ -62,12 +69,18 @@ export default function PrefetchBalancesWrapper({
}
}, [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(() => {
if (hasUnfetchedBalances) fetchBalances()
}, [fetchBalances, hasUnfetchedBalances])
return (
<div onMouseEnter={onHover} className={className}>
<div onMouseEnter={shouldFetchOnHover ? onHover : undefined} className={className}>
{children}
</div>
)

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

@ -3,11 +3,8 @@ import Column from 'components/Column'
import UniswapXBrandMark from 'components/Logo/UniswapXBrandMark'
import { RowBetween, RowFixed } from 'components/Row'
import Toggle from 'components/Toggle'
import { useUniswapXDefaultEnabled } from 'featureFlags/flags/uniswapXDefault'
import { useAppDispatch } from 'state/hooks'
import { RouterPreference } from 'state/routing/types'
import { useRouterPreference, useUserOptedOutOfUniswapX } from 'state/user/hooks'
import { updateDisabledUniswapX, updateOptedOutOfUniswapX } from 'state/user/reducer'
import { useRouterPreference } from 'state/user/hooks'
import styled from 'styled-components'
import { ExternalLink, ThemedText } from 'theme/components'
@ -22,12 +19,6 @@ const InlineLink = styled(ThemedText.BodySmall)`
export default function RouterPreferenceSettings() {
const [routerPreference, setRouterPreference] = useRouterPreference()
const dispatch = useAppDispatch()
const userOptedOutOfUniswapX = useUserOptedOutOfUniswapX()
const isUniswapXDefaultEnabled = useUniswapXDefaultEnabled()
const isUniswapXOverrideEnabled = isUniswapXDefaultEnabled && !userOptedOutOfUniswapX
const uniswapXInEffect = routerPreference === RouterPreference.X || isUniswapXOverrideEnabled
return (
<RowBetween gap="sm">
@ -46,21 +37,9 @@ export default function RouterPreferenceSettings() {
</RowFixed>
<Toggle
id="toggle-uniswap-x-button"
// 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}
isActive={routerPreference === RouterPreference.X}
toggle={() => {
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)
setRouterPreference(routerPreference === RouterPreference.X ? RouterPreference.API : RouterPreference.X)
}}
/>
</RowBetween>

@ -1,22 +1,30 @@
import { Trans } from '@lingui/macro'
import { ChainId, Currency } from '@uniswap/sdk-core'
import { ChainId, Currency, CurrencyAmount } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { PortfolioLogo } from 'components/AccountDrawer/MiniPortfolio/PortfolioLogo'
import { getChainInfo } from 'constants/chainInfo'
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 useCurrencyBalance from 'lib/hooks/useCurrencyBalance'
import { useMemo } from 'react'
import styled, { useTheme } from 'styled-components'
import { useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import { ThemedText } from 'theme/components'
import { NumberType, useFormatter } from 'utils/formatNumbers'
const BalancesCard = styled.div`
border-radius: 16px;
import { MultiChainMap } from '.'
const BalancesCard = styled.div<{ isInfoTDPEnabled?: boolean }>`
color: ${({ theme }) => theme.neutral1};
display: none;
display: flex;
flex-direction: column;
gap: 24px;
height: fit-content;
padding: 16px;
${({ isInfoTDPEnabled }) => !isInfoTDPEnabled && 'padding: 16px;'}
width: 100%;
// 768 hardcoded to match NFT-redesign navbar breakpoints
@ -48,11 +56,13 @@ const BalanceContainer = styled.div`
flex: 1;
`
const BalanceAmountsContainer = styled.div`
const BalanceAmountsContainer = styled.div<{ isInfoTDPEnabled?: boolean }>`
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 100%;
${({ isInfoTDPEnabled }) => isInfoTDPEnabled && 'margin-left: 12px;'}
`
const StyledNetworkLabel = styled.div`
@ -61,49 +71,187 @@ const StyledNetworkLabel = styled.div`
line-height: 16px;
`
export default function BalanceSummary({ token }: { token: Currency }) {
const { account, chainId } = useWeb3React()
const theme = useTheme()
const { label, color } = getChainInfo(asSupportedChain(chainId) ?? ChainId.MAINNET)
const balance = useCurrencyBalance(account, token)
const { formatCurrencyAmount } = useFormatter()
interface BalanceProps {
currency?: Currency
chainId?: ChainId
balance?: CurrencyAmount<Currency> // TODO(WEB-3026): only used for pre-Info-project calculations, should remove after project goes live
gqlBalance?: PortfolioTokenBalancePartsFragment
onClick?: () => void
}
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({
amount: balance,
type: NumberType.TokenNonTx,
})
const formattedUsdValue = formatCurrencyAmount({
amount: useStablecoinValue(balance),
type: NumberType.FiatTokenStats,
type: NumberType.PortfolioBalance,
})
const formattedGqlBalance = formatNumber({
input: gqlBalance?.quantity,
type: NumberType.TokenNonTx,
})
const formattedUsdGqlValue = formatNumber({
input: gqlBalance?.denominatedValue?.value,
type: NumberType.PortfolioBalance,
})
const currencies = useMemo(() => [token], [token])
if (isInfoTDPEnabled) {
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>
)
}
}
if (!account || !balance) {
const ConnectedChainBalanceSummary = ({
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 (
<BalancesCard>
<BalanceSection>
<ThemedText.SubHeaderSmall color={theme.neutral1}>
<Trans>Your balance on {label}</Trans>
</ThemedText.SubHeaderSmall>
<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 isInfoTDPEnabled={isInfoTDPEnabled}>
{!isInfoTDPEnabled && <ConnectedChainBalanceSummary connectedChainBalance={connectedChainBalance} />}
{isInfoTDPEnabled && (
<>
<PageChainBalanceSummary pageChainBalance={pageChainBalance} />
<OtherChainsBalanceSummary otherChainBalances={otherChainBalances} hasPageChainBalance={!!pageChainBalance} />
</>
)}
</BalancesCard>
)
}

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

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

@ -11,6 +11,7 @@ import { textFadeIn } from 'theme/styles'
import { LoadingBubble } from '../loading'
import { AboutContainer, AboutHeader } from './About'
import { BreadcrumbNav, BreadcrumbNavLink } from './BreadcrumbNavLink'
import { ChartContainer } from './ChartSection'
import { StatPair, StatsWrapper, StatWrapper } from './StatsSection'
const SWAP_COMPONENT_WIDTH = 360
@ -53,14 +54,6 @@ export const RightPanel = styled.div<{ isInfoTDPEnabled?: boolean }>`
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`
display: flex;
flex-direction: row;

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

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

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

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

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

@ -31,7 +31,6 @@ import { ThemedText } from 'theme/components'
import invariant from 'tiny-invariant'
import { isL2ChainId } from 'utils/chains'
import { SignatureExpiredError } from 'utils/errors'
import { NumberType, useFormatter } from 'utils/formatNumbers'
import { formatSwapPriceUpdatedEventProperties } from 'utils/loggingFormatters'
import { didUserReject } from 'utils/swapErrorToUserReadableMessage'
import { tradeMeaningfullyDiffers } from 'utils/tradeMeaningFullyDiffer'
@ -82,7 +81,6 @@ function useConfirmModalState({
const [confirmModalState, setConfirmModalState] = useState<ConfirmModalState>(ConfirmModalState.REVIEWING)
const [approvalError, setApprovalError] = useState<PendingModalError>()
const [pendingModalSteps, setPendingModalSteps] = useState<PendingConfirmModalState[]>([])
const { formatCurrencyAmount } = useFormatter()
// This is a function instead of a memoized value because we do _not_ want it to update as the allowance changes.
// For example, if the user needs to complete 3 steps initially, we should always show 3 step indicators
@ -117,14 +115,7 @@ function useConfirmModalState({
const nativeCurrency = useNativeCurrency(chainId)
const [wrapTxHash, setWrapTxHash] = useState<string>()
const { execute: onWrap } = useWrapCallback(
nativeCurrency,
trade.inputAmount.currency,
formatCurrencyAmount({
amount: trade.inputAmount,
type: NumberType.SwapTradeAmount,
})
)
const { execute: onWrap } = useWrapCallback(nativeCurrency, trade.inputAmount.currency, trade.inputAmount.toExact())
const wrapConfirmed = useIsTransactionConfirmed(wrapTxHash)
const prevWrapConfirmed = usePrevious(wrapConfirmed)
const catchUserReject = async (e: any, errorType: PendingModalError) => {
@ -142,9 +133,6 @@ function useConfirmModalState({
onWrap?.()
.then((wrapTxHash) => {
setWrapTxHash(wrapTxHash)
// After the wrap has succeeded, reset the input currency to be WETH
// because the trade will be on WETH -> token
onCurrencySelection(Field.INPUT, trade.inputAmount.currency)
sendAnalyticsEvent(InterfaceEventName.WRAP_TOKEN_TXN_SUBMITTED, {
chain_id: chainId,
token_symbol: maximumAmountIn?.currency.symbol,
@ -192,7 +180,6 @@ function useConfirmModalState({
onWrap,
trace,
trade,
onCurrencySelection,
]
)
@ -209,10 +196,20 @@ function useConfirmModalState({
useEffect(() => {
// If the wrapping step finished, trigger the next step (allowance or swap).
if (wrapConfirmed && !prevWrapConfirmed) {
// After the wrap has succeeded, reset the input currency to be WETH
// because the trade will be on WETH -> token
onCurrencySelection(Field.INPUT, trade.inputAmount.currency)
// moves on to either approve WETH or to swap submission
performStep(pendingModalSteps[1])
}
}, [pendingModalSteps, performStep, prevWrapConfirmed, wrapConfirmed])
}, [
pendingModalSteps,
performStep,
prevWrapConfirmed,
wrapConfirmed,
onCurrencySelection,
trade.inputAmount.currency,
])
useEffect(() => {
if (

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

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

@ -0,0 +1,37 @@
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,8 +1,9 @@
import { Trans } from '@lingui/macro'
import { Percent } from '@uniswap/sdk-core'
import { useLimitsEnabled } from 'featureFlags/flags/limits'
import { InterfaceTrade } from 'state/routing/types'
import styled from 'styled-components'
import { ThemedText } from 'theme/components'
import { ButtonText } from 'theme/components'
import { RowBetween, RowFixed } from '../Row'
import SettingsTab from '../Settings'
@ -18,22 +19,49 @@ const HeaderButtonContainer = styled(RowFixed)`
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({
autoSlippage,
chainId,
trade,
selectedTab,
onClickTab,
}: {
autoSlippage: Percent
chainId?: number
trade?: InterfaceTrade
selectedTab: SwapTab
onClickTab: (tab: SwapTab) => void
}) {
const limitsEnabled = useLimitsEnabled()
return (
<StyledSwapHeader>
<HeaderButtonContainer>
<ThemedText.SubHeader>
<StyledTextButton $isActive={selectedTab === SwapTab.Swap} onClick={() => onClickTab?.(SwapTab.Swap)}>
<Trans>Swap</Trans>
</ThemedText.SubHeader>
</StyledTextButton>
<SwapBuyFiatButton />
{limitsEnabled && (
<StyledTextButton $isActive={selectedTab === SwapTab.Limit} onClick={() => onClickTab?.(SwapTab.Limit)}>
<Trans>Limit</Trans>
</StyledTextButton>
)}
</HeaderButtonContainer>
<RowFixed>
<SettingsTab autoSlippage={autoSlippage} chainId={chainId} trade={trade} />

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

@ -120,6 +120,12 @@ exports[`SwapBuyFiatButton.tsx matches base snapshot 1`] = `
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;
}
.c4:focus {
@ -132,6 +138,10 @@ exports[`SwapBuyFiatButton.tsx matches base snapshot 1`] = `
text-decoration: none;
}
.c4:hover {
opacity: 0.6;
}
<div
class="c0"
>

@ -0,0 +1,343 @@
// 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
class=""
>
~-105566.373%
~-108834.406%
</span>
</div>
</div>
@ -7165,14 +7165,14 @@ exports[`SwapLineItem.tsx fee on buy 1`] = `
<div
class="c3 css-obwv3p"
>
0.000000000000000952 DEF
0.00000000000000098 DEF
</div>
</div>
<div
class="c13"
/>
<div>
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.
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.
<a
class="c14"
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
class="c3 c6 css-142zc9n"
>
0.000000000000000952 DEF
0.00000000000000098 DEF
</div>
</div>
</div>
@ -8761,7 +8761,7 @@ exports[`SwapLineItem.tsx fee on sell 1`] = `
<span
class=""
>
~-105566.373%
~-108834.406%
</span>
</div>
</div>
@ -9044,14 +9044,14 @@ exports[`SwapLineItem.tsx fee on sell 1`] = `
<div
class="c3 css-obwv3p"
>
0.000000000000000952 DEF
0.00000000000000098 DEF
</div>
</div>
<div
class="c13"
/>
<div>
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.
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.
<a
class="c14"
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
class="c3 c6 css-142zc9n"
>
0.000000000000000952 DEF
0.00000000000000098 DEF
</div>
</div>
</div>

@ -4,7 +4,6 @@ import { AlertTriangle } from 'react-feather'
import styled, { css } from 'styled-components'
import { Z_INDEX } from 'theme/zIndex'
import { useIsDarkMode } from '../../theme/components/ThemeToggle'
import { AutoColumn } from '../Column'
export const PageWrapper = styled.div`
@ -61,109 +60,6 @@ const SwapWrapperInner = styled.div`
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 }>`
border-radius: 12px;
height: 40px;

@ -3,7 +3,7 @@ import { URI_AVAILABLE, WalletConnect, WalletConnectConstructorArgs } from '@web
import { sendAnalyticsEvent } from 'analytics'
import { L1_CHAIN_IDS, L2_CHAIN_IDS } from 'constants/chains'
import { Z_INDEX } from 'theme/zIndex'
import { isIOS } from 'utils/userAgent'
import { isAndroid, isIOS } from 'utils/userAgent'
import { RPC_URLS } from '../constants/networks'
@ -83,7 +83,7 @@ export class UniwalletConnect extends WalletConnectV2 {
this.events.emit(UniwalletConnect.UNI_URI_AVAILABLE, `https://uniswap.org/app/wc?uri=${uri}`)
// Opens deeplink to Uniswap Wallet if on iOS
if (isIOS) {
if (isIOS || isAndroid) {
// Using window.location.href to open the deep link ensures smooth navigation and leverages OS handling for installed apps,
// avoiding potential popup blockers or inconsistent behavior associated with window.open
window.location.href = `uniswap://wc?uri=${encodeURIComponent(uri)}`

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

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

@ -1,8 +1,6 @@
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'
const UNI_UNSUPPORTED_LIST = 'https://cloudflare-ipfs.com/ipns/unsupportedtokens.uniswap.org'
const AAVE_LIST = 'tokenlist.aave.eth'
const BA_LIST = 'https://raw.githubusercontent.com/The-Blockchain-Association/sec-notice-list/master/ba-sec-list.json'
// 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 COINGECKO_LIST = 'https://tokens.coingecko.com/uniswap/all.json'
@ -27,8 +25,6 @@ export const AVALANCHE_LIST =
export const BASE_LIST =
'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
export const DEFAULT_ACTIVE_LIST_URLS: string[] = [UNI_LIST]
export const DEFAULT_INACTIVE_LIST_URLS: string[] = [
@ -52,8 +48,7 @@ export const DEFAULT_INACTIVE_LIST_URLS: string[] = [
CELO_LIST,
PLASMA_BNB_LIST,
AVALANCHE_LIST,
BASE_LIST,
...UNSUPPORTED_LIST_URLS,
BASE_LIST
]
export const DEFAULT_LIST_OF_LISTS: string[] = [...DEFAULT_ACTIVE_LIST_URLS, ...DEFAULT_INACTIVE_LIST_URLS]

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

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

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

@ -2,7 +2,7 @@ import { TokenInfo } from '@uniswap/token-lists'
import { ListsState } from 'state/lists/reducer'
import store from '../state'
import { UNI_EXTENDED_LIST, UNI_LIST, UNSUPPORTED_LIST_URLS } from './lists'
import { UNI_EXTENDED_LIST, UNI_LIST} from './lists'
import { COMMON_BASES } from './routing'
import brokenTokenList from './tokenLists/broken.tokenlist.json'
import { NATIVE_CHAIN_ID } from './tokens'
@ -37,14 +37,6 @@ class TokenSafetyLookupTable {
brokenTokenList.tokens.forEach((token) => {
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) {

@ -15,6 +15,13 @@ export const USDC_MAINNET = new Token(
'USDC',
'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_SEPOLIA = new Token(ChainId.SEPOLIA, '0x6f14C02Fc1F78322cFd7d707aB90f18baD3B54f5', 6, 'USDC', 'USD//C')
export const USDC_OPTIMISM = new Token(
@ -90,7 +97,13 @@ export const MATIC_MAINNET = new Token(
'MATIC',
'Polygon Matic'
)
const MATIC_POLYGON = new Token(ChainId.POLYGON, '0x0000000000000000000000000000000000001010', 18, 'MATIC', 'Matic')
export const MATIC_POLYGON = new Token(
ChainId.POLYGON,
'0x0000000000000000000000000000000000001010',
18,
'MATIC',
'Matic'
)
export const DAI_POLYGON = new Token(
ChainId.POLYGON,
'0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063',
@ -143,13 +156,6 @@ export const WBTC_OPTIMISM = new Token(
'WBTC',
'Wrapped BTC'
)
const MATIC_POLYGON_MUMBAI = new Token(
ChainId.POLYGON_MUMBAI,
'0x0000000000000000000000000000000000001010',
18,
'MATIC',
'Matic'
)
export const WETH_POLYGON_MUMBAI = new Token(
ChainId.POLYGON_MUMBAI,
'0xa6fa4fb5f76172d178d61b04b0ecd319c5d1c0aa',
@ -359,14 +365,21 @@ export function isPolygon(chainId: number): chainId is ChainId.POLYGON | ChainId
return chainId === ChainId.POLYGON_MUMBAI || chainId === ChainId.POLYGON
}
function getPolygonNativeCurrency(chainId: number) {
switch (chainId) {
case ChainId.POLYGON:
return MATIC_POLYGON
case ChainId.POLYGON_MUMBAI:
return MATIC_POLYGON_MUMBAI
default:
throw new Error('Not polygon')
class PolygonNativeCurrency extends NativeCurrency {
equals(other: Currency): boolean {
return other.isNative && other.chainId === this.chainId
}
get wrapped(): Token {
if (!isPolygon(this.chainId)) throw new Error('Not Polygon')
const wrapped = WRAPPED_NATIVE_CURRENCY[this.chainId]
invariant(wrapped instanceof Token)
return wrapped
}
public constructor(chainId: number) {
if (!isPolygon(chainId)) throw new Error('Not Polygon')
super(chainId, 18, 'MATIC', 'Matic')
}
}
@ -433,7 +446,7 @@ export function nativeOnChain(chainId: number): NativeCurrency | Token {
if (cachedNativeCurrency[chainId]) return cachedNativeCurrency[chainId]
let nativeCurrency: NativeCurrency | Token
if (isPolygon(chainId)) {
nativeCurrency = getPolygonNativeCurrency(chainId)
nativeCurrency = new PolygonNativeCurrency(chainId)
} else if (isCelo(chainId)) {
nativeCurrency = getCeloNativeCurrency(chainId)
} else if (isBsc(chainId)) {

@ -1,10 +0,0 @@
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
}

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

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

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

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

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

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

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

@ -49,6 +49,22 @@ 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: {

@ -1,6 +1,39 @@
import gql from 'graphql-tag'
import { PortfolioTokenBalancePartsFragment } from './__generated__/types-and-hooks'
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!]!) {
portfolios(ownerAddresses: [$ownerAddress], chains: $chains) {
id
@ -19,35 +52,10 @@ gql`
}
}
tokenBalances {
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
}
...PortfolioTokenBalanceParts
}
}
}
`
export type PortfolioToken = NonNullable<PortfolioTokenBalancePartsFragment['token']>

@ -1,4 +1,3 @@
import ms from 'ms'
import { useEffect } from 'react'
import { ApplicationModal, setOpenModal } from 'state/application/reducer'
import { useAppDispatch } from 'state/hooks'
@ -7,34 +6,23 @@ export default function useAccountRiskCheck(account: string | null | undefined)
const dispatch = useAppDispatch()
useEffect(() => {
if (account) {
const riskCheckLocalStorageKey = `risk-check-${account}`
const now = Date.now()
try {
// Check local browser cache
const storedTime = localStorage.getItem(riskCheckLocalStorageKey)
const checkExpirationTime = storedTime ? parseInt(storedTime) : now - 1
if (checkExpirationTime < Date.now()) {
const headers = new Headers({ 'Content-Type': 'application/json' })
fetch('https://api.uniswap.org/v1/screen', {
method: 'POST',
headers,
body: JSON.stringify({ address: account }),
})
.then((res) => res.json())
.then((data) => {
if (data.block) {
dispatch(setOpenModal(ApplicationModal.BLOCKED_ACCOUNT))
}
})
.catch(() => {
dispatch(setOpenModal(null))
})
if (!account) return
// TODO: add back local browser cacheing (revisit 11/13/2023)
const headers = new Headers({ 'Content-Type': 'application/json' })
fetch('https://api.uniswap.org/v1/screen', {
method: 'POST',
headers,
body: JSON.stringify({ address: account }),
})
.then((res) => res.json())
.then((data) => {
if (data.block) {
dispatch(setOpenModal(ApplicationModal.BLOCKED_ACCOUNT))
}
} finally {
// Set item to have 1 day local cache storage
localStorage.setItem(riskCheckLocalStorageKey, (now + ms(`1d`)).toString())
}
}
})
.catch(() => {
dispatch(setOpenModal(null))
})
}, [account, dispatch])
}

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

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

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

@ -73,12 +73,8 @@ export function useUniversalRouterSwapCallback(
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, {
slippageTolerance: taxAdjustedSlippageTolerance,
slippageTolerance: options.slippageTolerance,
deadlineOrPreviousBlockhash: options.deadline?.toString(),
inputTokenPermit: options.permit,
fee: options.feeOptions,

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

@ -68,9 +68,12 @@ 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 (typeof tokenAddress !== 'string' || !isSupportedChain(chainId) || !formattedAddress) return undefined
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)
}, [chainId, tokenAddress, formattedAddress, isLoading, parsedDecimals, parsedSymbol, parsedName])
}, [tokenAddress, chainId, formattedAddress, isLoading, decimals?.result, parsedDecimals, parsedSymbol, parsedName])
}
type TokenMap = { [address: string]: Token }

@ -108,6 +108,10 @@ export function useTokenBalance(account?: string, token?: Token): CurrencyAmount
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(
account?: string,
currencies?: (Currency | undefined)[]

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

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