Compare commits
34 Commits
Author | SHA1 | Date | |
---|---|---|---|
e2d5f85ce0 | |||
9ee5aa173f | |||
f99f20fe18 | |||
fb0108196c | |||
986cf07391 | |||
48eb3f0005 | |||
a95c8fab5a | |||
1c131ee496 | |||
911af900ed | |||
|
fc7ecc7e3b | ||
|
4a5a41c59e | ||
|
4bec816e6c | ||
|
1d1b15f4ac | ||
|
1ffaf723de | ||
|
dd4b2dc764 | ||
|
5ded55e061 | ||
|
0f4ca592f2 | ||
|
90497dc08a | ||
|
2e618fb2aa | ||
|
0aa5727cdd | ||
|
79e74e1d13 | ||
|
52dc441e31 | ||
|
ff6d1cc510 | ||
|
76157c057e | ||
|
a1bd6f5eb4 | ||
|
f903eedc15 | ||
|
1feeaea181 | ||
|
7b10c94e4d | ||
|
f2f59d52cb | ||
|
a5034cb1c0 | ||
|
2227a38276 | ||
|
9f06747958 | ||
|
c6b44bb5c9 | ||
|
1d64d24d31 |
.env.env.productionpackage.json
.github
cypress/e2e
functions
api/image/nfts/collection
nfts/collection
public
scripts
src
assets/svg
components
AccountDrawer
Banner/AndroidAnnouncementBanner
FeatureFlagModal
Logo
NavBar
Pools/PoolDetails
PoolDetailsHeader.test.tsxPoolDetailsLink.test.tsxPoolDetailsStats.test.tsxPoolDetailsStatsButtons.test.tsxPoolDetailsStatsButtons.tsx
__snapshots__
Popover
PrefetchBalancesWrapper
SearchModal
Settings/RouterPreferenceSettings
Tokens
TokenDetails
BalanceSummary.tsxChartSection.tsxMobileBalanceSummaryFooter.tsxSkeleton.tsxTimeSelector.tsxindex.tsx
TokenTable
WalletModal
swap
connection
constants
featureFlags
flags
androidGALaunch.tsfotAdjustments.tslimits.tsuniswapXDefault.tsuniswapXEthOutput.tsuniswapXExactOutput.ts
index.tsxgraphql/data
hooks
useAccountRiskCheck.tsuseGlobalChainSwitch.tsuseIsPoolsPage.tsuseSwapCallback.tsxuseUniversalRouter.ts
lib
10
.env
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
.github/CODEOWNERS
vendored
@ -1 +0,0 @@
|
||||
* @uniswap/web-reviewers
|
22
.github/ISSUE_TEMPLATE/bug-report.md
vendored
22
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@ -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.)
|
8
.github/ISSUE_TEMPLATE/config.yml
vendored
8
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -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
|
19
.github/ISSUE_TEMPLATE/feature-request.md
vendored
19
.github/ISSUE_TEMPLATE/feature-request.md
vendored
@ -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.
|
48
.github/actions/report/action.yml
vendored
48
.github/actions/report/action.yml
vendored
@ -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
|
49
.github/actions/setup/action.yml
vendored
49
.github/actions/setup/action.yml
vendored
@ -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
|
12
.github/dependabot.yml
vendored
12
.github/dependabot.yml
vendored
@ -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'
|
52
.github/pull_request_template.md
vendored
52
.github/pull_request_template.md
vendored
@ -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
|
73
.github/workflows/1-main-to-staging.yml
vendored
73
.github/workflows/1-main-to-staging.yml
vendored
@ -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
|
64
.github/workflows/2-deploy-to-staging.yml
vendored
64
.github/workflows/2-deploy-to-staging.yml
vendored
@ -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'
|
42
.github/workflows/3-staging-to-prod.yml
vendored
42
.github/workflows/3-staging-to-prod.yml
vendored
@ -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
|
111
.github/workflows/4-deploy-to-prod.yml
vendored
111
.github/workflows/4-deploy-to-prod.yml
vendored
@ -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'
|
17
.github/workflows/check-pr-title.yaml
vendored
17
.github/workflows/check-pr-title.yaml
vendored
@ -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 }}
|
26
.github/workflows/crowdin.yaml
vendored
26
.github/workflows/crowdin.yaml
vendored
@ -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
|
24
.github/workflows/semgrep.yml
vendored
24
.github/workflows/semgrep.yml
vendored
@ -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
|
278
.github/workflows/test.yml
vendored
278
.github/workflows/test.yml
vendored
@ -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> </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'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() {
|
||||
|
37
src/components/swap/SwapHeader.test.tsx
Normal file
37
src/components/swap/SwapHeader.test.tsx
Normal file
@ -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"
|
||||
>
|
||||
|
343
src/components/swap/__snapshots__/SwapHeader.test.tsx.snap
Normal file
343
src/components/swap/__snapshots__/SwapHeader.test.tsx.snap
Normal file
@ -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
|
||||
}
|
9
src/featureFlags/flags/limits.ts
Normal file
9
src/featureFlags/flags/limits.ts
Normal file
@ -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
Loading…
x
Reference in New Issue
Block a user