Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
566da07448 | ||
|
|
31a3840b1f | ||
|
|
f89d7ccd5e | ||
|
|
628417f696 | ||
|
|
ea8c7326d6 | ||
|
|
dd5feaacb2 | ||
|
|
cc919ab3df | ||
|
|
53d6eb0922 | ||
|
|
db0d3cf3fa | ||
|
|
ace4276bcb | ||
|
|
50a2dc9560 | ||
|
|
8cdec6188c | ||
|
|
5325d0cbe5 | ||
|
|
c16e49e774 | ||
|
|
7e709e10db | ||
|
|
7389b178fd | ||
|
|
48f8c6a141 | ||
|
|
091876a374 | ||
|
|
d0e4aa832a | ||
|
|
b17a38d94b | ||
|
|
22136b2708 | ||
|
|
1897330ffc | ||
|
|
6131d0079f | ||
|
|
e6814994f6 | ||
|
|
fea7d3a867 | ||
|
|
f4f0f29409 | ||
|
|
fa25e3c3e5 | ||
|
|
51d2b3792f | ||
|
|
04ded04e74 | ||
|
|
cf188a86de | ||
|
|
f7e2435868 | ||
|
|
5817d3bbdb | ||
|
|
bdeb62ad64 | ||
|
|
e42effdbfd | ||
|
|
c0a5adf43d | ||
|
|
fd0489e654 | ||
|
|
caa0a2967d | ||
|
|
11ec2333dd | ||
|
|
251339a9ef | ||
|
|
148e415fe8 | ||
|
|
fd964c5b74 | ||
|
|
83b6eec271 | ||
|
|
cd76fffbbe | ||
|
|
2c0ac56296 | ||
|
|
f836e3ca32 | ||
|
|
1733fbb378 | ||
|
|
78142270a8 | ||
|
|
db4e2a9bee | ||
|
|
9cee94a473 | ||
|
|
db5a14387f | ||
|
|
5dc7d36669 | ||
|
|
a3cbe672c7 | ||
|
|
dc368ed7ac | ||
|
|
b109248b4c | ||
|
|
64cc6fb88c | ||
|
|
74f6a4ef3f | ||
|
|
f468001404 | ||
|
|
f26ec2ff1b | ||
|
|
61d1036d28 | ||
|
|
e11d2080a4 | ||
|
|
da33423719 | ||
|
|
bd4545538d | ||
|
|
4274db67d5 | ||
|
|
28498706cb | ||
|
|
68c71a67dd | ||
|
|
fe195b63f7 | ||
|
|
86b85e25a5 | ||
|
|
0ea635ce15 |
3
.env
3
.env
@@ -1,2 +1,3 @@
|
||||
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
|
||||
REACT_APP_LOCALES="locales"
|
||||
REACT_APP_FORTMATIC_KEY="pk_live_357F77728B8EB880"
|
||||
REACT_APP_LOCALES="locales"
|
||||
|
||||
24
.github/pull_request_template.md
vendored
Normal file
24
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
Your PR title must follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary), and should start with one of the following [types](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#type):
|
||||
|
||||
- build: Changes that affect the build system or external dependencies (example scopes: yarn, eslint, typescript)
|
||||
- ci: Changes to our CI configuration files and scripts (example scopes: vercel, github, cypress)
|
||||
- docs: Documentation only changes
|
||||
- feat: A new feature
|
||||
- fix: A bug fix
|
||||
- perf: A code change that improves performance
|
||||
- refactor: A code change that neither fixes a bug nor adds a feature
|
||||
- style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
||||
- test: Adding missing tests or correcting existing tests
|
||||
|
||||
Example commit messages:
|
||||
|
||||
- feat: adds support for gnosis safe wallet
|
||||
- fix: removes a polling memory leak
|
||||
- chore: bumps redux version
|
||||
|
||||
Other things to note:
|
||||
|
||||
- Please describe the change using verb statements (ex: Removes X from Y)
|
||||
- PRs with multiple changes should use a list of verb statements
|
||||
- Add any relevant unit / integration tests
|
||||
- Changes will be previewable via vercel. Non-obvious changes should include instructions for how to reproduce them
|
||||
40
.github/workflows/bundle.yaml
vendored
40
.github/workflows/bundle.yaml
vendored
@@ -1,40 +0,0 @@
|
||||
name: Widgets
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: yarn widgets:build
|
||||
33
.github/workflows/crowdin-sync.yaml
vendored
33
.github/workflows/crowdin-sync.yaml
vendored
@@ -1,6 +1,5 @@
|
||||
name: Crowdin Download
|
||||
|
||||
# hourly we sync translations from Crowdin
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 * * * *' # every hour we download translations and update the pr from crowdin
|
||||
@@ -14,35 +13,27 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up node
|
||||
uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
registry-url: https://registry.npmjs.org
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
- uses: actions/cache@v3
|
||||
id: install-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
path: node_modules/
|
||||
key: ${{ runner.os }}-install-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
- if: steps.install-cache.outputs.cache-hit != 'true'
|
||||
run: yarn install --frozen-lockfile --ignore-scripts
|
||||
|
||||
- name: Extract translations
|
||||
run: "yarn i18n:extract"
|
||||
- run: yarn i18n:extract
|
||||
|
||||
- name: Synchronize
|
||||
uses: crowdin/github-action@1.1.0
|
||||
- name: Download Crowdin translations
|
||||
uses: crowdin/github-action@1.4.9
|
||||
with:
|
||||
upload_sources: false
|
||||
download_translations: true
|
||||
|
||||
31
.github/workflows/crowdin.yaml
vendored
31
.github/workflows/crowdin.yaml
vendored
@@ -1,6 +1,5 @@
|
||||
name: Crowdin Upload
|
||||
|
||||
# on any push to main, we upload the translations to be translated
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -12,34 +11,26 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up node
|
||||
uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
registry-url: https://registry.npmjs.org
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
- uses: actions/cache@v3
|
||||
id: install-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
path: node_modules/
|
||||
key: ${{ runner.os }}-install-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
- if: steps.install-cache.outputs.cache-hit != 'true'
|
||||
run: yarn install --frozen-lockfile --ignore-scripts
|
||||
|
||||
- name: Extract translations
|
||||
run: "yarn i18n:extract"
|
||||
- run: yarn i18n:extract
|
||||
|
||||
- name: Synchronize
|
||||
- name: Upload Crowdin sources
|
||||
uses: crowdin/github-action@1.1.0
|
||||
with:
|
||||
upload_sources: true
|
||||
|
||||
50
.github/workflows/integration-tests.yaml
vendored
50
.github/workflows/integration-tests.yaml
vendored
@@ -1,50 +0,0 @@
|
||||
name: Integration Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
integration-tests:
|
||||
name: Cypress
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- run: yarn cypress install
|
||||
|
||||
- run: yarn build
|
||||
env:
|
||||
CI: false
|
||||
REACT_APP_SERVICE_WORKER: false
|
||||
|
||||
- run: yarn test:e2e
|
||||
env:
|
||||
CYPRESS_INTEGRATION_TEST_PRIVATE_KEY: ${{ secrets.CYPRESS_INTEGRATION_TEST_PRIVATE_KEY }}
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
25
.github/workflows/lint.yml
vendored
25
.github/workflows/lint.yml
vendored
@@ -14,29 +14,22 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up node
|
||||
uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
registry-url: https://registry.npmjs.org
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
- uses: actions/cache@v3
|
||||
id: install-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
path: node_modules/
|
||||
key: ${{ runner.os }}-install-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
- if: steps.install-cache.outputs.cache-hit != 'true'
|
||||
run: yarn install --frozen-lockfile --ignore-scripts
|
||||
|
||||
- name: Run eslint w/ autofix
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.owner.login == github.repository_owner }}
|
||||
|
||||
25
.github/workflows/release.yaml
vendored
25
.github/workflows/release.yaml
vendored
@@ -14,8 +14,7 @@ jobs:
|
||||
new_tag: ${{ steps.github_tag_action.outputs.new_tag }}
|
||||
changelog: ${{ steps.github_tag_action.outputs.changelog }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Bump version and push tag
|
||||
id: github_tag_action
|
||||
@@ -31,20 +30,26 @@ jobs:
|
||||
needs: bump_version
|
||||
if: ${{ needs.bump_version.outputs.new_tag != null }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up node
|
||||
uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
registry-url: https://registry.npmjs.org
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
- uses: actions/cache@v3
|
||||
id: install-cache
|
||||
with:
|
||||
path: node_modules/
|
||||
key: ${{ runner.os }}-install-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Build the IPFS bundle
|
||||
run: yarn build
|
||||
- if: steps.install-cache.outputs.cache-hit != 'true'
|
||||
run: yarn install --frozen-lockfile --ignore-scripts
|
||||
|
||||
- run: yarn prepare
|
||||
|
||||
- run: yarn build
|
||||
|
||||
- name: Pin to IPFS
|
||||
id: upload
|
||||
|
||||
103
.github/workflows/tests-e2e.yaml
vendored
Normal file
103
.github/workflows/tests-e2e.yaml
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
name: End-to-End Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
registry-url: https://registry.npmjs.org
|
||||
cache: 'yarn'
|
||||
|
||||
- uses: actions/cache@v3
|
||||
id: install-cache
|
||||
with:
|
||||
path: node_modules/
|
||||
key: ${{ runner.os }}-install-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- if: steps.install-cache.outputs.cache-hit != 'true'
|
||||
run: yarn install --frozen-lockfile --ignore-scripts
|
||||
|
||||
- run: yarn prepare
|
||||
|
||||
- run: yarn build
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: build
|
||||
path: build
|
||||
if-no-files-found: error
|
||||
|
||||
- uses: actions/cache@v3
|
||||
id: cypress-cache
|
||||
with:
|
||||
path: /home/runner/.cache/Cypress
|
||||
key: ${{ runner.os }}-cypress-${{ hashFiles('node_modules/cypress') }}
|
||||
|
||||
- if: steps.cypress-cache.outputs.cache-hit != 'true'
|
||||
run: yarn cypress install
|
||||
|
||||
cypress-tests:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
containers: [1, 2, 3, 4]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
registry-url: https://registry.npmjs.org
|
||||
cache: 'yarn'
|
||||
|
||||
- uses: actions/cache@v3
|
||||
id: install-cache
|
||||
with:
|
||||
path: node_modules/ # this should always be a cache hit, from install
|
||||
key: ${{ runner.os }}-install-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- if: steps.install-cache.outputs.cache-hit != 'true'
|
||||
run: yarn install --frozen-lockfile --ignore-scripts
|
||||
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: build
|
||||
path: build
|
||||
|
||||
- uses: actions/cache@v3
|
||||
id: cypress-cache
|
||||
with:
|
||||
path: /home/runner/.cache/Cypress
|
||||
key: ${{ runner.os }}-cypress-${{ hashFiles('node_modules/cypress') }}
|
||||
|
||||
- if: steps.cypress-cache.outputs.cache-hit != 'true'
|
||||
run: yarn cypress install
|
||||
|
||||
- uses: cypress-io/github-action@v4
|
||||
with:
|
||||
install: false
|
||||
start: yarn serve
|
||||
wait-on: 'http://localhost:3000'
|
||||
browser: chrome
|
||||
record: true
|
||||
parallel: true
|
||||
env:
|
||||
CI: false # disables lint checks when building
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
35
.github/workflows/tests-unit.yaml
vendored
Normal file
35
.github/workflows/tests-unit.yaml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Unit Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
unit-tests:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
registry-url: https://registry.npmjs.org
|
||||
cache: 'yarn'
|
||||
|
||||
- uses: actions/cache@v3
|
||||
id: install-cache
|
||||
with:
|
||||
path: node_modules/
|
||||
key: ${{ runner.os }}-install-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- if: steps.install-cache.outputs.cache-hit != 'true'
|
||||
run: yarn install --frozen-lockfile --ignore-scripts
|
||||
|
||||
- run: yarn prepare
|
||||
|
||||
- run: yarn test
|
||||
40
.github/workflows/unit-tests.yaml
vendored
40
.github/workflows/unit-tests.yaml
vendored
@@ -1,40 +0,0 @@
|
||||
name: Unit Tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
unit-tests:
|
||||
name: Unit tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Run unit tests
|
||||
run: yarn test
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -3,9 +3,6 @@
|
||||
# generated contract types
|
||||
/src/types/v3
|
||||
/src/abis/types
|
||||
/src/lib/locales/**/*.js
|
||||
/src/lib/locales/**/en-US.po
|
||||
/src/lib/locales/**/pseudo.po
|
||||
/src/locales/**/*.js
|
||||
/src/locales/**/en-US.po
|
||||
/src/locales/**/pseudo.po
|
||||
@@ -19,8 +16,6 @@
|
||||
|
||||
# builds
|
||||
/build
|
||||
/cosmos-export
|
||||
/dist
|
||||
/dts
|
||||
|
||||
# misc
|
||||
@@ -39,10 +34,8 @@ yarn-error.log*
|
||||
notes.txt
|
||||
.idea/
|
||||
|
||||
.vscode/
|
||||
|
||||
package-lock.json
|
||||
|
||||
cypress/videos
|
||||
cypress/screenshots
|
||||
cypress/fixtures/example.json
|
||||
|
||||
|
||||
19
.vscode/settings.json
vendored
Normal file
19
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"npm.packageManager": "yarn",
|
||||
"typescript.updateImportsOnFileMove.enabled": "always",
|
||||
"javascript.updateImportsOnFileMove.enabled": "always",
|
||||
"editor.formatOnSaveMode": "file",
|
||||
"editor.tabCompletion": "on",
|
||||
"editor.tabSize": 2,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.inlineSuggest.enabled": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
},
|
||||
"files.eol": "\n",
|
||||
"eslint.enable": true,
|
||||
"eslint.debug": true,
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,73 @@
|
||||
|
||||
# Contributing
|
||||
|
||||
Thank you for your interest in contributing to the Uniswap interface! 🦄
|
||||
|
||||
# Development
|
||||
|
||||
Before running anything, you'll need to install the dependencies:
|
||||
|
||||
```
|
||||
yarn install
|
||||
```
|
||||
|
||||
## Running the interface locally
|
||||
|
||||
1. `yarn install`
|
||||
1. `yarn start`
|
||||
```
|
||||
yarn start
|
||||
```
|
||||
|
||||
The interface should automatically open. If it does not, navigate to [http://localhost:3000].
|
||||
|
||||
## Creating a production build
|
||||
|
||||
1. `yarn install`
|
||||
1. `yarn build`
|
||||
```
|
||||
yarn build
|
||||
```
|
||||
|
||||
To serve the production build:
|
||||
|
||||
```
|
||||
yarn serve
|
||||
```
|
||||
|
||||
Then, navigate to [http://localhost:3000] to see it.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
```
|
||||
yarn test
|
||||
```
|
||||
|
||||
By default, this runs only unit tests that have been affected since the last commit. To run _all_ unit tests:
|
||||
|
||||
```
|
||||
yarn test --watchAll
|
||||
```
|
||||
|
||||
## Running integration tests (cypress)
|
||||
|
||||
Integration tests require a server to be running. In order to see your changes quickly, run `start` in its own tab/window:
|
||||
|
||||
```
|
||||
yarn start
|
||||
```
|
||||
|
||||
Integration tests are run using `cypress`. When developing locally, use `cypress:open` for an interactive UI, and to inspect the rendered page:
|
||||
|
||||
```
|
||||
yarn cypress:open
|
||||
```
|
||||
|
||||
To run _all_ cypress integration tests _from the command line_:
|
||||
|
||||
```
|
||||
yarn cypress:run
|
||||
```
|
||||
|
||||
## Engineering standards
|
||||
|
||||
Code merged into the `main` branch of this repository should adhere to high standards of correctness and maintainability.
|
||||
Use your best judgment when applying these standards. If code is in the critical path, will be frequently visited, or
|
||||
Code merged into the `main` branch of this repository should adhere to high standards of correctness and maintainability.
|
||||
Use your best judgment when applying these standards. If code is in the critical path, will be frequently visited, or
|
||||
makes large architectural changes, consider following all the standards.
|
||||
|
||||
- Have at least one engineer approve of large code refactorings
|
||||
@@ -39,7 +88,7 @@ The following points should help guide your development:
|
||||
- Avoid adding steps to the development/build processes
|
||||
- The build must be deterministic, i.e. a particular commit hash always produces the same build
|
||||
- Decentralization: anyone can run the interface
|
||||
- An Ethereum node should be the only critical dependency
|
||||
- An Ethereum node should be the only critical dependency
|
||||
- All other external dependencies should only enhance the UX ([graceful degradation](https://developer.mozilla.org/en-US/docs/Glossary/Graceful_degradation))
|
||||
- Accessibility: anyone can use the interface
|
||||
- The interface should be responsive, small and also run well on low performance devices (majority of swaps on mobile!)
|
||||
@@ -48,14 +97,14 @@ The following points should help guide your development:
|
||||
|
||||
Releases are cut automatically from the `main` branch Monday-Thursday in the morning according to the [release workflow](./.github/workflows/release.yaml).
|
||||
|
||||
Fix pull requests should be merged whenever ready and tested.
|
||||
Fix pull requests should be merged whenever ready and tested.
|
||||
If a fix is urgently needed in production, releases can be manually triggered on [GitHub](https://github.com/Uniswap/uniswap-interface/actions/workflows/release.yaml)
|
||||
after the fix is merged into `main`.
|
||||
|
||||
Features should not be merged into `main` until they are ready for users.
|
||||
When building larger features or collaborating with other developers, create a new branch from `main` to track its development.
|
||||
Use the automatic Vercel preview for sharing the feature to collect feedback.
|
||||
When the feature is ready for review, create a new pull request from the feature branch into `main` and request reviews from
|
||||
When the feature is ready for review, create a new pull request from the feature branch into `main` and request reviews from
|
||||
the appropriate UX reviewers (PMs or designers).
|
||||
|
||||
## Finding a first issue
|
||||
@@ -65,7 +114,7 @@ Start with issues with the label
|
||||
|
||||
# Translations
|
||||
|
||||
Uniswap uses [Crowdin](https://crowdin.com/project/uniswap-interface) for managing translations.
|
||||
Uniswap uses [Crowdin](https://crowdin.com/project/uniswap-interface) for managing translations.
|
||||
[This workflow](./.github/workflows/crowdin.yaml) uploads new strings for translation to the Crowdin project whenever code using the [lingui translation macros](https://lingui.js.org/ref/macro.html) is merged into `main`.
|
||||
|
||||
Every hour, translations are synced back down from Crowdin to the repository in [this other workflow](./.github/workflows/crowdin-sync.yaml).
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
# Uniswap Interface
|
||||
|
||||
An open source interface for Uniswap -- a protocol for decentralized exchange of Ethereum tokens.
|
||||
|
||||
- Website: [uniswap.org](https://uniswap.org/)
|
||||
- Interface: [app.uniswap.org](https://app.uniswap.org)
|
||||
- Docs: [uniswap.org/docs/](https://docs.uniswap.org/)
|
||||
- Twitter: [@Uniswap](https://twitter.com/Uniswap)
|
||||
- Reddit: [/r/Uniswap](https://www.reddit.com/r/Uniswap/)
|
||||
- Email: [contact@uniswap.org](mailto:contact@uniswap.org)
|
||||
- Discord: [Uniswap](https://discord.gg/FCfyBSbCU5)
|
||||
- Whitepapers:
|
||||
- [V1](https://hackmd.io/C-DvwDSfSxuh-Gd4WKE_ig)
|
||||
- [V2](https://uniswap.org/whitepaper.pdf)
|
||||
- [V3](https://uniswap.org/whitepaper-v3.pdf)
|
||||
|
||||
## Accessing the Uniswap Interface
|
||||
|
||||
To access the Uniswap Interface, use an IPFS gateway link from the
|
||||
[latest release](https://github.com/Uniswap/uniswap-interface/releases/latest),
|
||||
or visit [app.uniswap.org](https://app.uniswap.org).
|
||||
|
||||
## Unsupported tokens
|
||||
|
||||
Check out `useUnsupportedTokenList()` in [src/state/lists/hooks.ts](./src/state/lists/hooks.ts) for blocking tokens in your instance of the interface.
|
||||
|
||||
You can block an entire list of tokens by passing in a tokenlist like [here](./src/constants/lists.ts) or you can block specific tokens by adding them to [unsupported.tokenlist.json](./src/constants/tokenLists/unsupported.tokenlist.json).
|
||||
|
||||
## Contributions
|
||||
|
||||
For steps on local deployment, development, and code contribution, please see [CONTRIBUTING](./CONTRIBUTING.md).
|
||||
|
||||
## Accessing Uniswap V2
|
||||
|
||||
The Uniswap Interface supports swapping, adding liquidity, removing liquidity and migrating liquidity for Uniswap protocol V2.
|
||||
|
||||
- Swap on Uniswap V2: https://app.uniswap.org/#/swap?use=v2
|
||||
- View V2 liquidity: https://app.uniswap.org/#/pool/v2
|
||||
- Add V2 liquidity: https://app.uniswap.org/#/add/v2
|
||||
- Migrate V2 liquidity to V3: https://app.uniswap.org/#/migrate/v2
|
||||
|
||||
## Accessing Uniswap V1
|
||||
|
||||
The Uniswap V1 interface for mainnet and testnets is accessible via IPFS gateways
|
||||
linked from the [v1.0.0 release](https://github.com/Uniswap/uniswap-interface/releases/tag/v1.0.0).
|
||||
46
README.md
46
README.md
@@ -1,5 +1,3 @@
|
||||
This repo is home to the Uniswap Widgets package and the web app interface [app.uniswap.org](https://app.uniswap.org).
|
||||
|
||||
# Uniswap Labs Interface
|
||||
|
||||
[](https://github.com/Uniswap/interface/actions/workflows/unit-tests.yaml)
|
||||
@@ -8,14 +6,46 @@ This repo is home to the Uniswap Widgets package and the web app interface [app.
|
||||
[](https://github.com/Uniswap/interface/actions/workflows/release.yaml)
|
||||
[](https://crowdin.com/project/uniswap-interface)
|
||||
|
||||
The web application hosted at https://app.uniswap.org is a convenient way to access the core functionality of the Uniswap Protocol.
|
||||
An open source interface for Uniswap -- a protocol for decentralized exchange of Ethereum tokens.
|
||||
|
||||
For documentation of the interface including how to contribute or access prior builds, please view the README here: [INTERFACE_README.md](./INTERFACE_README.md)
|
||||
- Website: [uniswap.org](https://uniswap.org/)
|
||||
- Interface: [app.uniswap.org](https://app.uniswap.org)
|
||||
- Docs: [uniswap.org/docs/](https://docs.uniswap.org/)
|
||||
- Twitter: [@Uniswap](https://twitter.com/Uniswap)
|
||||
- Reddit: [/r/Uniswap](https://www.reddit.com/r/Uniswap/)
|
||||
- Email: [contact@uniswap.org](mailto:contact@uniswap.org)
|
||||
- Discord: [Uniswap](https://discord.gg/FCfyBSbCU5)
|
||||
- Whitepapers:
|
||||
- [V1](https://hackmd.io/C-DvwDSfSxuh-Gd4WKE_ig)
|
||||
- [V2](https://uniswap.org/whitepaper.pdf)
|
||||
- [V3](https://uniswap.org/whitepaper-v3.pdf)
|
||||
|
||||
# Uniswap Labs Widgets
|
||||
## Accessing the Uniswap Interface
|
||||
|
||||
The `@uniswap/widgets` package is an npm package of React components used to provide subsets of the Uniswap Protocol functionality in a small and configurable user interface element.
|
||||
To access the Uniswap Interface, use an IPFS gateway link from the
|
||||
[latest release](https://github.com/Uniswap/uniswap-interface/releases/latest),
|
||||
or visit [app.uniswap.org](https://app.uniswap.org).
|
||||
|
||||
The npm package can be found here. [@uniswap/widgets](https://www.npmjs.com/package/@uniswap/widgets)
|
||||
## Unsupported tokens
|
||||
|
||||
For documentation of the widgets package, please view the README here: [WIDGETS_README.md](./WIDGETS_README.md).
|
||||
Check out `useUnsupportedTokenList()` in [src/state/lists/hooks.ts](./src/state/lists/hooks.ts) for blocking tokens in your instance of the interface.
|
||||
|
||||
You can block an entire list of tokens by passing in a tokenlist like [here](./src/constants/lists.ts) or you can block specific tokens by adding them to [unsupported.tokenlist.json](./src/constants/tokenLists/unsupported.tokenlist.json).
|
||||
|
||||
## Contributions
|
||||
|
||||
For steps on local deployment, development, and code contribution, please see [CONTRIBUTING](./CONTRIBUTING.md).
|
||||
|
||||
## Accessing Uniswap V2
|
||||
|
||||
The Uniswap Interface supports swapping, adding liquidity, removing liquidity and migrating liquidity for Uniswap protocol V2.
|
||||
|
||||
- Swap on Uniswap V2: https://app.uniswap.org/#/swap?use=v2
|
||||
- View V2 liquidity: https://app.uniswap.org/#/pool/v2
|
||||
- Add V2 liquidity: https://app.uniswap.org/#/add/v2
|
||||
- Migrate V2 liquidity to V3: https://app.uniswap.org/#/migrate/v2
|
||||
|
||||
## Accessing Uniswap V1
|
||||
|
||||
The Uniswap V1 interface for mainnet and testnets is accessible via IPFS gateways
|
||||
linked from the [v1.0.0 release](https://github.com/Uniswap/uniswap-interface/releases/tag/v1.0.0).
|
||||
@@ -1,40 +0,0 @@
|
||||
# Uniswap Labs Swap Widget
|
||||
|
||||
The Swap Widget bundles the whole swapping experience into a single React component that developers can easily embed in their app with one line of code.
|
||||
|
||||

|
||||
|
||||
You can customize the theme (colors, fonts, border radius, and more) to match the style of your application. You can also configure your own default token list and optionally set a convenience fee on swaps executed through the widget on your site.
|
||||
|
||||
## Installation
|
||||
|
||||
Install the widgets library via `npm` or `yarn`. If you do not already use the widget's peerDependencies `redux` and `react-redux`, then you'll need to add them as well.
|
||||
|
||||
```js
|
||||
yarn add @uniswap/widgets redux react-redux
|
||||
```
|
||||
```js
|
||||
npm i --save @uniswap/widgets redux react-redux
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [overview](https://docs.uniswap.org/sdk/widgets/swap-widget)
|
||||
- [api reference](https://docs.uniswap.org/sdk/widgets/swap-widget/api)
|
||||
|
||||
## Example Apps
|
||||
|
||||
Uniswap Labs maintains two demo apps in branches of the [widgets-demo](https://github.com/Uniswap/widgets-demo) repo:
|
||||
|
||||
- [NextJS](https://github.com/Uniswap/widgets-demo/tree/nextjs)
|
||||
- [Create React App](https://github.com/Uniswap/widgets-demo/tree/cra)
|
||||
|
||||
Others have also also released the widget in production to their userbase:
|
||||
|
||||
- [OpenSea](https://opensea.io/)
|
||||
- [Friends With Benefits](https://www.fwb.help/)
|
||||
- [Oasis](https://oasis.app/)
|
||||
|
||||
## Legal notice
|
||||
|
||||
Uniswap Labs encourages integrators to evaluate their own regulatory obligations when integrating this widget into their products, including, but not limited to, those related to economic or trade sanctions compliance.
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"watchDirs": [
|
||||
"src"
|
||||
],
|
||||
"webpack": {
|
||||
"configPath": "react-scripts/config/webpack.config",
|
||||
"overridePath": "cosmos.override.cjs"
|
||||
},
|
||||
"port": 5001
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const { DefinePlugin } = require('webpack')
|
||||
|
||||
// Renders the cosmos fixtures in isolation, instead of using public/index.html.
|
||||
module.exports = (webpackConfig) => ({
|
||||
...webpackConfig,
|
||||
plugins: webpackConfig.plugins.map((plugin) => {
|
||||
if (plugin instanceof HtmlWebpackPlugin) {
|
||||
return new HtmlWebpackPlugin({
|
||||
templateContent: '<body></body>',
|
||||
})
|
||||
}
|
||||
if (plugin instanceof DefinePlugin) {
|
||||
return new DefinePlugin({
|
||||
...plugin.definitions,
|
||||
'process.env': {
|
||||
...plugin.definitions['process.env'],
|
||||
REACT_APP_IS_WIDGET: true,
|
||||
REACT_APP_LOCALES: '"../locales"',
|
||||
},
|
||||
})
|
||||
}
|
||||
return plugin
|
||||
}),
|
||||
})
|
||||
@@ -1,16 +0,0 @@
|
||||
// Custom test environment to provide `TextEncoder`/`TextDecoder`
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const Environment = require('jest-environment-jsdom')
|
||||
|
||||
module.exports = class CustomTestEnvironment extends Environment {
|
||||
async setup() {
|
||||
await super.setup()
|
||||
if (typeof this.global.TextEncoder === 'undefined') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { TextEncoder, TextDecoder } = require('util')
|
||||
this.global.TextEncoder = TextEncoder
|
||||
this.global.TextDecoder = TextDecoder
|
||||
}
|
||||
}
|
||||
}
|
||||
20
cypress.config.ts
Normal file
20
cypress.config.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { defineConfig } from 'cypress'
|
||||
|
||||
export default defineConfig({
|
||||
projectId: 'yp82ef',
|
||||
videoUploadOnPasses: false,
|
||||
defaultCommandTimeout: 10000,
|
||||
chromeWebSecurity: false,
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
return {
|
||||
...config,
|
||||
// Only enable Chrome.
|
||||
// Electron (the default) has issues injecting window.ethereum before pageload, so it is not viable.
|
||||
browsers: config.browsers.filter(({ name }) => name === 'chrome'),
|
||||
}
|
||||
},
|
||||
baseUrl: 'http://localhost:3000',
|
||||
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
|
||||
},
|
||||
})
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"projectId": "yp82ef",
|
||||
"baseUrl": "http://localhost:3000",
|
||||
"pluginsFile": false,
|
||||
"supportFile": "cypress/support/index.js",
|
||||
"video": false,
|
||||
"defaultCommandTimeout": 10000
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import { TEST_ADDRESS_NEVER_USE_SHORTENED } from '../support/commands'
|
||||
|
||||
describe('Landing Page', () => {
|
||||
beforeEach(() => cy.visit('/'))
|
||||
it('loads swap page', () => {
|
||||
@@ -15,9 +13,4 @@ describe('Landing Page', () => {
|
||||
cy.get('#pool-nav-link').click()
|
||||
cy.url().should('include', '/pool')
|
||||
})
|
||||
|
||||
it('is connected', () => {
|
||||
cy.get('#web3-status-connected').click()
|
||||
cy.get('#web3-account-identifier-row').contains(TEST_ADDRESS_NEVER_USE_SHORTENED)
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
describe('Pool', () => {
|
||||
beforeEach(() => cy.visit('/pool'))
|
||||
|
||||
it('add liquidity links to /add/ETH', () => {
|
||||
cy.get('#join-pool-button').click()
|
||||
cy.url().should('contain', '/add/ETH')
|
||||
84
cypress/e2e/service-worker.test.ts
Normal file
84
cypress/e2e/service-worker.test.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import assert = require('assert')
|
||||
|
||||
describe('Service Worker', () => {
|
||||
before(() => {
|
||||
// Fail fast if there is no Service Worker on this build.
|
||||
cy.request({ url: '/service-worker.js', headers: { 'Service-Worker': 'script' } }).then((response) => {
|
||||
const isValid = isValidServiceWorker(response)
|
||||
if (!isValid) {
|
||||
throw new Error(
|
||||
'\n' +
|
||||
'Service Worker tests must be run on a production-like build\n' +
|
||||
'To test, build with `yarn build:e2e` and serve with `yarn serve`'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
function isValidServiceWorker(response: Cypress.Response<any>) {
|
||||
const contentType = response.headers['content-type']
|
||||
return !(response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1))
|
||||
}
|
||||
})
|
||||
|
||||
function unregister() {
|
||||
return cy.log('unregister service worker').then(async () => {
|
||||
const cacheKeys = await window.caches.keys()
|
||||
const cacheKey = cacheKeys.find((key) => key.match(/precache/))
|
||||
if (cacheKey) {
|
||||
await window.caches.delete(cacheKey)
|
||||
}
|
||||
|
||||
const sw = await window.navigator.serviceWorker.getRegistration(Cypress.config().baseUrl ?? undefined)
|
||||
await sw?.unregister()
|
||||
})
|
||||
}
|
||||
before(unregister)
|
||||
after(unregister)
|
||||
|
||||
beforeEach(() => {
|
||||
cy.intercept({ hostname: 'www.google-analytics.com' }, (req) => {
|
||||
const body = req.body.toString()
|
||||
if (req.query['ep.event_category'] === 'Service Worker' || body.includes('Service%20Worker')) {
|
||||
if (req.query['en'] === 'Not Installed' || body.includes('Not%20Installed')) {
|
||||
req.alias = 'NotInstalled'
|
||||
} else if (req.query['en'] === 'Cache Hit' || body.includes('Cache%20Hit')) {
|
||||
req.alias = 'CacheHit'
|
||||
} else if (req.query['en'] === 'Cache Miss' || body.includes('Cache%20Miss')) {
|
||||
req.alias = 'CacheMiss'
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('installs a ServiceWorker', () => {
|
||||
cy.visit('/', { serviceWorker: true })
|
||||
.get('#swap-page')
|
||||
.wait('@NotInstalled', { timeout: 20000 })
|
||||
.window({ timeout: 20000 })
|
||||
.and((win) => {
|
||||
expect(win.navigator.serviceWorker.controller?.state).to.equal('activated')
|
||||
})
|
||||
})
|
||||
|
||||
it('records a cache hit', () => {
|
||||
cy.visit('/', { serviceWorker: true }).get('#swap-page').wait('@CacheHit', { timeout: 20000 })
|
||||
})
|
||||
|
||||
it('records a cache miss', () => {
|
||||
cy.then(async () => {
|
||||
const cacheKeys = await window.caches.keys()
|
||||
const cacheKey = cacheKeys.find((key) => key.match(/precache/))
|
||||
assert(cacheKey)
|
||||
|
||||
const cache = await window.caches.open(cacheKey)
|
||||
const keys = await cache.keys()
|
||||
const key = keys.find((key) => key.url.match(/index/))
|
||||
assert(key)
|
||||
|
||||
await cache.put(key, new Response())
|
||||
})
|
||||
.visit('/', { serviceWorker: true })
|
||||
.get('#swap-page')
|
||||
.wait('@CacheMiss', { timeout: 20000 })
|
||||
})
|
||||
})
|
||||
@@ -48,6 +48,13 @@ describe('Swap', () => {
|
||||
cy.get('#add-recipient-button').should('not.exist')
|
||||
})
|
||||
|
||||
it('ETH to wETH is same value (wrapped swaps have no price impact)', () => {
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.get('.token-item-0xc778417E063141139Fce010982780140Aa0cD5Ab').click({ force: true })
|
||||
cy.get('#swap-currency-input .token-amount-input').type('0.01', { force: true, delay: 100 })
|
||||
cy.get('#swap-currency-output .token-amount-input').should('have.value', '0.01')
|
||||
})
|
||||
|
||||
describe('expert mode', () => {
|
||||
beforeEach(() => {
|
||||
cy.window().then((win) => {
|
||||
30
cypress/e2e/wallet.test.ts
Normal file
30
cypress/e2e/wallet.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { TEST_ADDRESS_NEVER_USE_SHORTENED } from '../support/ethereum'
|
||||
|
||||
describe('Wallet', () => {
|
||||
before(() => {
|
||||
cy.visit('/')
|
||||
})
|
||||
|
||||
it('displays account details', () => {
|
||||
cy.get('#web3-status-connected').contains(TEST_ADDRESS_NEVER_USE_SHORTENED).click()
|
||||
})
|
||||
|
||||
it('displays account view in wallet modal', () => {
|
||||
cy.get('#web3-account-identifier-row').contains(TEST_ADDRESS_NEVER_USE_SHORTENED)
|
||||
})
|
||||
|
||||
it('changes back to the options grid', () => {
|
||||
cy.get('[data-cy=wallet-change]').click()
|
||||
cy.get('[data-cy=option-grid]').should('exist')
|
||||
})
|
||||
|
||||
it('selects injected wallet option', () => {
|
||||
cy.contains('Injected').click()
|
||||
cy.get('#web3-account-identifier-row').contains(TEST_ADDRESS_NEVER_USE_SHORTENED)
|
||||
})
|
||||
|
||||
it('shows connect buttons after disconnect', () => {
|
||||
cy.get('[data-cy=wallet-disconnect]').click()
|
||||
cy.get('[data-cy=option-grid]').should('exist')
|
||||
})
|
||||
})
|
||||
10
cypress/support/commands.d.ts
vendored
10
cypress/support/commands.d.ts
vendored
@@ -1,10 +0,0 @@
|
||||
export const TEST_ADDRESS_NEVER_USE: string
|
||||
|
||||
export const TEST_ADDRESS_NEVER_USE_SHORTENED: string
|
||||
|
||||
// declare namespace Cypress {
|
||||
// // eslint-disable-next-line @typescript-eslint/class-name-casing
|
||||
// interface cy {
|
||||
// additionalCommands(): void
|
||||
// }
|
||||
// }
|
||||
52
cypress/support/e2e.ts
Normal file
52
cypress/support/e2e.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// ***********************************************************
|
||||
// This file is processed and loaded automatically before your test files.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.ts using ES2015 syntax:
|
||||
import { injected } from './ethereum'
|
||||
import assert = require('assert')
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface ApplicationWindow {
|
||||
ethereum: typeof injected
|
||||
}
|
||||
interface VisitOptions {
|
||||
serviceWorker?: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sets up the injected provider to be a mock ethereum provider with the given mnemonic/index
|
||||
// eslint-disable-next-line no-undef
|
||||
Cypress.Commands.overwrite(
|
||||
'visit',
|
||||
(original, url: string | Partial<Cypress.VisitOptions>, options?: Partial<Cypress.VisitOptions>) => {
|
||||
assert(typeof url === 'string')
|
||||
|
||||
cy.intercept('/service-worker.js', options?.serviceWorker ? undefined : { statusCode: 404 }).then(() => {
|
||||
original({
|
||||
...options,
|
||||
url: (url.startsWith('/') && url.length > 2 && !url.startsWith('/#') ? `/#${url}` : url) + '?chain=rinkeby',
|
||||
onBeforeLoad(win) {
|
||||
options?.onBeforeLoad?.(win)
|
||||
win.localStorage.clear()
|
||||
win.ethereum = injected
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
beforeEach(() => {
|
||||
// Infura security policies are based on Origin headers.
|
||||
// These are stripped by cypress because chromeWebSecurity === false; this adds them back in.
|
||||
cy.intercept(/infura.io/, (res) => {
|
||||
res.headers['origin'] = 'http://localhost:3000'
|
||||
res.continue()
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,6 @@
|
||||
// ***********************************************
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
/**
|
||||
* Updates cy.visit() to include an injected window.ethereum provider.
|
||||
*/
|
||||
|
||||
import { Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge'
|
||||
import { JsonRpcProvider } from '@ethersproject/providers'
|
||||
@@ -20,14 +18,16 @@ export const TEST_ADDRESS_NEVER_USE_SHORTENED = `${TEST_ADDRESS_NEVER_USE.substr
|
||||
6
|
||||
)}...${TEST_ADDRESS_NEVER_USE.substr(-4, 4)}`
|
||||
|
||||
class CustomizedBridge extends Eip1193Bridge {
|
||||
const provider = new JsonRpcProvider('https://rinkeby.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847', 4)
|
||||
const signer = new Wallet(TEST_PRIVATE_KEY, provider)
|
||||
export const injected = new (class extends Eip1193Bridge {
|
||||
chainId = 4
|
||||
|
||||
async sendAsync(...args) {
|
||||
async sendAsync(...args: any[]) {
|
||||
console.debug('sendAsync called', ...args)
|
||||
return this.send(...args)
|
||||
}
|
||||
async send(...args) {
|
||||
async send(...args: any[]) {
|
||||
console.debug('send called', ...args)
|
||||
const isCallbackForm = typeof args[0] === 'object' && typeof args[1] === 'function'
|
||||
let callback
|
||||
@@ -71,19 +71,4 @@ class CustomizedBridge extends Eip1193Bridge {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sets up the injected provider to be a mock ethereum provider with the given mnemonic/index
|
||||
// eslint-disable-next-line no-undef
|
||||
Cypress.Commands.overwrite('visit', (original, url, options) => {
|
||||
return original(url.startsWith('/') && url.length > 2 && !url.startsWith('/#') ? `/#${url}` : url, {
|
||||
...options,
|
||||
onBeforeLoad(win) {
|
||||
options && options.onBeforeLoad && options.onBeforeLoad(win)
|
||||
win.localStorage.clear()
|
||||
const provider = new JsonRpcProvider('https://rinkeby.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847', 4)
|
||||
const signer = new Wallet(TEST_PRIVATE_KEY, provider)
|
||||
win.ethereum = new CustomizedBridge(signer, provider)
|
||||
},
|
||||
})
|
||||
})
|
||||
})(signer, provider)
|
||||
@@ -1,9 +0,0 @@
|
||||
// ***********************************************************
|
||||
// This file is processed and loaded automatically before your test files.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.ts using ES2015 syntax:
|
||||
import './commands'
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"baseUrl": "../node_modules",
|
||||
"target": "es5",
|
||||
"lib": ["es5", "dom"],
|
||||
"types": ["cypress"]
|
||||
|
||||
263
package.json
263
package.json
@@ -3,34 +3,61 @@
|
||||
"version": "1.0.7",
|
||||
"description": "Uniswap Interface",
|
||||
"homepage": ".",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"type": "module",
|
||||
"types": "dist/index.d.ts",
|
||||
"main": "dist/cjs/index.cjs",
|
||||
"module": "dist/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/cjs/index.cjs"
|
||||
},
|
||||
"./locales/*": {
|
||||
"import": "./dist/locales/*.js",
|
||||
"require": "./dist/cjs/locales/*.cjs"
|
||||
},
|
||||
"./fonts.css": {
|
||||
"import": "./dist/fonts.css",
|
||||
"require": "./dist/fonts.css"
|
||||
"license": "GPL-3.0-or-later",
|
||||
"scripts": {
|
||||
"contracts:compile:abi": "typechain --target ethers-v5 --out-dir src/abis/types \"./src/abis/**/*.json\"",
|
||||
"contracts:compile:v3": "typechain --target ethers-v5 --out-dir src/types/v3 \"./node_modules/@uniswap/**/artifacts/contracts/**/*[!dbg].json\"",
|
||||
"contracts:compile": "yarn contracts:compile:abi && yarn contracts:compile:v3",
|
||||
"graphql:generate": "graphql-codegen --config codegen.yml",
|
||||
"prei18n:extract": "touch src/locales/en-US.po",
|
||||
"i18n:extract": "lingui extract --locale en-US",
|
||||
"i18n:compile": "yarn i18n:extract && lingui compile",
|
||||
"i18n:pseudo": "lingui extract --locale pseudo && lingui compile",
|
||||
"prepare": "yarn contracts:compile && yarn graphql:generate && yarn i18n:compile",
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"serve": "serve build -l 3000",
|
||||
"test": "react-scripts test --coverage",
|
||||
"cypress:open": "cypress open --browser chrome --e2e",
|
||||
"cypress:run": "cypress run --browser chrome --e2e"
|
||||
},
|
||||
"jest": {
|
||||
"collectCoverageFrom": [
|
||||
"src/components/**/*.ts*",
|
||||
"src/hooks/**/*.ts*",
|
||||
"src/lib/hooks/**/*.ts*",
|
||||
"src/lib/state/**/*.ts*",
|
||||
"src/lib/utils/**/*.ts*",
|
||||
"src/pages/**/*.ts*",
|
||||
"src/state/**/*.ts*",
|
||||
"src/utils/**/*.ts*"
|
||||
],
|
||||
"coverageThreshold": {
|
||||
"global": {
|
||||
"branches": 4,
|
||||
"functions": 6,
|
||||
"lines": 9,
|
||||
"statements": 9
|
||||
}
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-transform-runtime": "^7.17.0",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@babel/preset-react": "^7.16.7",
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@coinbase/wallet-sdk": "^3.2.0",
|
||||
"@ethersproject/experimental": "^5.4.0",
|
||||
"@fontsource/ibm-plex-mono": "^4.5.1",
|
||||
"@fontsource/inter": "^4.5.1",
|
||||
"@gnosis.pm/safe-apps-web3-react": "^0.6.0",
|
||||
"@graphql-codegen/cli": "1.21.5",
|
||||
"@graphql-codegen/typescript": "1.22.3",
|
||||
@@ -41,19 +68,11 @@
|
||||
"@lingui/macro": "^3.9.0",
|
||||
"@lingui/react": "^3.9.0",
|
||||
"@metamask/jazzicon": "^2.0.0",
|
||||
"@popperjs/core": "^2.4.4",
|
||||
"@reach/dialog": "^0.10.3",
|
||||
"@reach/portal": "^0.10.3",
|
||||
"@react-hook/window-scroll": "^1.3.0",
|
||||
"@rollup/plugin-alias": "^3.1.9",
|
||||
"@rollup/plugin-babel": "^5.3.0",
|
||||
"@rollup/plugin-commonjs": "^21.0.1",
|
||||
"@rollup/plugin-eslint": "^8.0.1",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@rollup/plugin-node-resolve": "^13.1.3",
|
||||
"@rollup/plugin-replace": "^3.0.1",
|
||||
"@rollup/plugin-typescript": "^8.3.0",
|
||||
"@rollup/plugin-url": "^6.1.0",
|
||||
"@svgr/rollup": "^6.2.0",
|
||||
"@reduxjs/toolkit": "^1.6.1",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.0.0",
|
||||
"@testing-library/react-hooks": "^7.0.2",
|
||||
@@ -86,19 +105,36 @@
|
||||
"@uniswap/governance": "^1.0.2",
|
||||
"@uniswap/liquidity-staker": "^1.0.2",
|
||||
"@uniswap/merkle-distributor": "1.0.1",
|
||||
"@uniswap/redux-multicall": "^1.1.1",
|
||||
"@uniswap/router-sdk": "^1.0.3",
|
||||
"@uniswap/sdk-core": "^3.0.1",
|
||||
"@uniswap/smart-order-router": "^2.5.26",
|
||||
"@uniswap/token-lists": "^1.0.0-beta.30",
|
||||
"@uniswap/v2-core": "1.0.0",
|
||||
"@uniswap/v2-periphery": "^1.1.0-beta.0",
|
||||
"@uniswap/v2-sdk": "^3.0.1",
|
||||
"@uniswap/v3-core": "1.0.0",
|
||||
"@uniswap/v3-periphery": "^1.1.1",
|
||||
"@web3-react/metamask": "^8.0.19-beta.0",
|
||||
"@web3-react/walletconnect": "^8.0.26-beta.0",
|
||||
"@uniswap/v3-sdk": "^3.8.2",
|
||||
"@walletconnect/ethereum-provider": "1.7.1",
|
||||
"@web3-react/coinbase-wallet": "^8.0.33-beta.0",
|
||||
"@web3-react/core": "^8.0.33-beta.0",
|
||||
"@web3-react/eip1193": "^8.0.25-beta.0",
|
||||
"@web3-react/empty": "^8.0.19-beta.0",
|
||||
"@web3-react/gnosis-safe": "^8.0.5-beta.0",
|
||||
"@web3-react/metamask": "^8.0.26-beta.0",
|
||||
"@web3-react/network": "^8.0.26-beta.0",
|
||||
"@web3-react/types": "^8.0.19-beta.0",
|
||||
"@web3-react/url": "^8.0.24-beta.0",
|
||||
"@web3-react/walletconnect": "^8.0.34-beta.0",
|
||||
"ajv": "^6.12.3",
|
||||
"array.prototype.flat": "^1.2.4",
|
||||
"array.prototype.flatmap": "^1.2.4",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"cids": "^1.0.0",
|
||||
"copy-to-clipboard": "^3.2.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"cypress": "^7.7.0",
|
||||
"cypress": "^10.1.0",
|
||||
"d3": "^7.0.0",
|
||||
"env-cmd": "^10.1.0",
|
||||
"eslint": "^7.11.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-better-styled-components": "^1.1.2",
|
||||
@@ -107,112 +143,14 @@
|
||||
"eslint-plugin-react-hooks": "^4.0.0",
|
||||
"eslint-plugin-simple-import-sort": "^7.0.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"ethers": "^5.1.4",
|
||||
"firebase": "^9.1.3",
|
||||
"fortmatic": "^2.4.0",
|
||||
"graphql": "^15.5.0",
|
||||
"graphql-request": "^3.4.0",
|
||||
"immer": "^9.0.6",
|
||||
"inter-ui": "^3.13.1",
|
||||
"jest-styled-components": "^7.0.5",
|
||||
"polyfill-object.fromentries": "^1.0.1",
|
||||
"prettier": "^2.2.1",
|
||||
"qs": "^6.9.4",
|
||||
"react": "^17.0.1",
|
||||
"react-confetti": "^6.0.0",
|
||||
"react-cosmos": "^5.6.6",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-ga4": "^1.4.1",
|
||||
"react-is": "^17.0.2",
|
||||
"react-markdown": "^4.3.1",
|
||||
"react-redux": "^7.2.2",
|
||||
"react-router-dom": "^5.0.0",
|
||||
"react-scripts": "^4.0.3",
|
||||
"react-spring": "^8.0.27",
|
||||
"react-use-gesture": "^6.0.14",
|
||||
"redux": "^4.1.2",
|
||||
"redux-localstorage-simple": "^2.3.1",
|
||||
"rollup": "^2.63.0",
|
||||
"rollup-plugin-copy": "^3.4.0",
|
||||
"rollup-plugin-delete": "^2.0.0",
|
||||
"rollup-plugin-dts": "^4.1.0",
|
||||
"rollup-plugin-multi-input": "^1.3.1",
|
||||
"rollup-plugin-node-externals": "^3.1.2",
|
||||
"rollup-plugin-scss": "^3.0.0",
|
||||
"rollup-plugin-typescript2": "^0.31.1",
|
||||
"sass": "^1.45.1",
|
||||
"serve": "^11.3.2",
|
||||
"start-server-and-test": "^1.11.0",
|
||||
"typechain": "^5.0.0",
|
||||
"typescript": "^4.4.3",
|
||||
"ua-parser-js": "^0.7.28",
|
||||
"use-count-up": "^2.2.5",
|
||||
"use-resize-observer": "^8.0.0",
|
||||
"wcag-contrast": "^3.0.0",
|
||||
"web-vitals": "^2.1.0",
|
||||
"web3-react-abstract-connector": "npm:@web3-react/abstract-connector@^6.0.7",
|
||||
"web3-react-fortmatic-connector": "npm:@web3-react/fortmatic-connector@^6.0.9",
|
||||
"web3-react-injected-connector": "npm:@web3-react/injected-connector@^6.0.7",
|
||||
"web3-react-types": "npm:@web3-react/types@^6.0.7",
|
||||
"web3-react-walletconnect-connector": "npm:@web3-react/walletconnect-connector@^7.0.2-alpha.0",
|
||||
"web3-react-walletlink-connector": "npm:@web3-react/walletlink-connector@^6.2.13",
|
||||
"workbox-core": "^6.1.0",
|
||||
"workbox-precaching": "^6.1.0",
|
||||
"workbox-routing": "^6.1.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"@walletconnect/ethereum-provider": "1.7.1"
|
||||
},
|
||||
"scripts": {
|
||||
"contracts:compile:abi": "typechain --target ethers-v5 --out-dir src/abis/types \"./src/abis/**/*.json\"",
|
||||
"contracts:compile:v3": "typechain --target ethers-v5 --out-dir src/types/v3 \"./node_modules/@uniswap/**/artifacts/contracts/**/*.json\"",
|
||||
"contracts:compile": "yarn contracts:compile:abi && yarn contracts:compile:v3",
|
||||
"graphql:generate": "graphql-codegen --config codegen.yml",
|
||||
"prei18n:extract": "touch src/locales/en-US.po",
|
||||
"i18n:extract": "lingui extract --locale en-US",
|
||||
"i18n:compile": "yarn i18n:extract && lingui compile",
|
||||
"i18n:pseudo": "lingui extract --locale pseudo && lingui compile",
|
||||
"prepare": "yarn contracts:compile && yarn graphql:generate && yarn i18n:compile",
|
||||
"prepublishOnly": "yarn widgets:build",
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --env=./custom-test-env.cjs",
|
||||
"test:e2e": "start-server-and-test 'serve build -l 3000' http://localhost:3000 'cypress run --record'",
|
||||
"widgets:start": "cosmos",
|
||||
"widgets:build": "rollup --config --failAfterWarnings --configPlugin typescript2"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.17.0",
|
||||
"@fontsource/ibm-plex-mono": "^4.5.1",
|
||||
"@fontsource/inter": "^4.5.1",
|
||||
"@popperjs/core": "^2.4.4",
|
||||
"@reduxjs/toolkit": "^1.6.1",
|
||||
"@uniswap/redux-multicall": "^1.1.1",
|
||||
"@uniswap/router-sdk": "^1.0.3",
|
||||
"@uniswap/sdk-core": "^3.0.1",
|
||||
"@uniswap/smart-order-router": "^2.5.26",
|
||||
"@uniswap/token-lists": "^1.0.0-beta.27",
|
||||
"@uniswap/v2-sdk": "^3.0.1",
|
||||
"@uniswap/v3-sdk": "^3.8.2",
|
||||
"@web3-react/core": "^8.0.23-beta.0",
|
||||
"@web3-react/eip1193": "^8.0.18-beta.0",
|
||||
"@web3-react/empty": "^8.0.12-beta.0",
|
||||
"@web3-react/types": "^8.0.12-beta.0",
|
||||
"@web3-react/url": "^8.0.17-beta.0",
|
||||
"ajv": "^6.12.3",
|
||||
"cids": "^1.0.0",
|
||||
"ethers": "^5.1.4",
|
||||
"immer": "^9.0.6",
|
||||
"jotai": "^1.3.7",
|
||||
"jsbi": "^3.1.4",
|
||||
"make-plural": "^7.0.0",
|
||||
@@ -221,29 +159,46 @@
|
||||
"multihashes": "^4.0.2",
|
||||
"node-vibrant": "^3.2.1-alpha.1",
|
||||
"polished": "^3.3.2",
|
||||
"polyfill-object.fromentries": "^1.0.1",
|
||||
"popper-max-size-modifier": "^0.2.0",
|
||||
"prettier": "^2.2.1",
|
||||
"qs": "^6.9.4",
|
||||
"react": "^17.0.1",
|
||||
"react-confetti": "^6.0.0",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-feather": "^2.0.8",
|
||||
"react-ga4": "^1.4.1",
|
||||
"react-is": "^17.0.2",
|
||||
"react-markdown": "^4.3.1",
|
||||
"react-popper": "^2.2.3",
|
||||
"react-redux": "^7.2.2",
|
||||
"react-router-dom": "^5.0.0",
|
||||
"react-scripts": "^4.0.3",
|
||||
"react-spring": "^8.0.27",
|
||||
"react-use-gesture": "^6.0.14",
|
||||
"react-virtualized-auto-sizer": "^1.0.2",
|
||||
"react-window": "^1.8.5",
|
||||
"rebass": "^4.0.7",
|
||||
"redux": "^4.1.2",
|
||||
"redux-localstorage-simple": "^2.3.1",
|
||||
"sass": "^1.45.1",
|
||||
"serve": "^11.3.2",
|
||||
"setimmediate": "^1.0.5",
|
||||
"styled-components": "^5.3.0",
|
||||
"tiny-invariant": "^1.2.0",
|
||||
"typechain": "^5.0.0",
|
||||
"typescript": "^4.4.3",
|
||||
"ua-parser-js": "^0.7.28",
|
||||
"use-count-up": "^2.2.5",
|
||||
"use-resize-observer": "^8.0.0",
|
||||
"wcag-contrast": "^3.0.0",
|
||||
"web3-react-core": "npm:@web3-react/core@^6.0.9",
|
||||
"wicg-inert": "^3.1.1"
|
||||
"web-vitals": "^2.1.0",
|
||||
"workbox-core": "^6.1.0",
|
||||
"workbox-navigation-preload": "^6.1.0",
|
||||
"workbox-precaching": "^6.1.0",
|
||||
"workbox-routing": "^6.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/runtime": "^7.17.0",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-redux": "^7.2.2",
|
||||
"redux": "^4.1.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bufferutil": "^4.0.6",
|
||||
"encoding": "^0.1.13",
|
||||
"utf-8-validate": "^5.0.8"
|
||||
"resolutions": {
|
||||
"@walletconnect/ethereum-provider": "1.7.1"
|
||||
}
|
||||
}
|
||||
|
||||
182
rollup.config.ts
182
rollup.config.ts
@@ -1,182 +0,0 @@
|
||||
/**
|
||||
* Bundles the widgets library, which is released independently of the interface application.
|
||||
* This library lives in src/lib, but shares code with the interface application.
|
||||
*/
|
||||
|
||||
import alias from '@rollup/plugin-alias'
|
||||
import babel from '@rollup/plugin-babel'
|
||||
import commonjs from '@rollup/plugin-commonjs'
|
||||
import json from '@rollup/plugin-json'
|
||||
import resolve from '@rollup/plugin-node-resolve'
|
||||
import replace from '@rollup/plugin-replace'
|
||||
import typescript from '@rollup/plugin-typescript'
|
||||
import url from '@rollup/plugin-url'
|
||||
import svgr from '@svgr/rollup'
|
||||
import path from 'path'
|
||||
import { RollupWarning } from 'rollup'
|
||||
import copy from 'rollup-plugin-copy'
|
||||
import del from 'rollup-plugin-delete'
|
||||
import dts from 'rollup-plugin-dts'
|
||||
// @ts-ignore // missing types
|
||||
import multi from 'rollup-plugin-multi-input'
|
||||
import externals from 'rollup-plugin-node-externals'
|
||||
import sass from 'rollup-plugin-scss'
|
||||
import { CompilerOptions } from 'typescript'
|
||||
|
||||
const REPLACEMENTS = {
|
||||
'process.env.REACT_APP_IS_WIDGET': true,
|
||||
'process.env.REACT_APP_LOCALES': '"./locales"',
|
||||
// esm requires fully-specified paths:
|
||||
'react/jsx-runtime': 'react/jsx-runtime.js',
|
||||
}
|
||||
|
||||
const EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx']
|
||||
const ASSET_EXTENSIONS = ['.png', '.svg']
|
||||
function isAsset(source: string) {
|
||||
const extname = path.extname(source)
|
||||
return extname && [...ASSET_EXTENSIONS, '.css', '.scss'].includes(extname)
|
||||
}
|
||||
|
||||
function isEthers(source: string) {
|
||||
// @ethersproject/* modules are provided by ethers, with the exception of experimental.
|
||||
return source.startsWith('@ethersproject/') && !source.endsWith('experimental')
|
||||
}
|
||||
|
||||
const TS_CONFIG = './tsconfig.lib.json'
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { baseUrl, paths }: CompilerOptions = require(TS_CONFIG).compilerOptions
|
||||
const aliases = Object.entries({ ...paths }).flatMap(([find, replacements]) => {
|
||||
return replacements.map((replacement) => ({
|
||||
find: path.dirname(find),
|
||||
replacement: path.join(__dirname, baseUrl || '.', path.dirname(replacement)),
|
||||
}))
|
||||
})
|
||||
|
||||
const plugins = [
|
||||
// Dependency resolution
|
||||
resolve({ extensions: EXTENSIONS }), // resolves third-party modules within node_modules/
|
||||
alias({ entries: aliases }), // resolves paths aliased through the tsconfig (babel does not use tsconfig path resolution)
|
||||
|
||||
// Source code transformation
|
||||
replace({ ...REPLACEMENTS, preventAssignment: true }),
|
||||
json(), // imports json as ES6; doing so enables type-checking and module resolution
|
||||
]
|
||||
|
||||
const check = {
|
||||
input: 'src/lib/index.tsx',
|
||||
output: { file: 'dist/widgets.tsc', inlineDynamicImports: true },
|
||||
external: (source: string) => isAsset(source) || isEthers(source),
|
||||
plugins: [
|
||||
externals({ exclude: ['constants'], deps: true, peerDeps: true }), // marks builtins, dependencies, and peerDependencies external
|
||||
...plugins,
|
||||
typescript({ tsconfig: TS_CONFIG }),
|
||||
],
|
||||
onwarn: squelchTranspilationWarnings, // this pipeline is only for typechecking and generating definitions
|
||||
}
|
||||
|
||||
const type = {
|
||||
input: 'dist/dts/lib/index.d.ts',
|
||||
output: { file: 'dist/index.d.ts' },
|
||||
external: (source: string) => isAsset(source) || isEthers(source),
|
||||
plugins: [
|
||||
externals({ exclude: ['constants'], deps: true, peerDeps: true }),
|
||||
dts({ compilerOptions: { baseUrl: 'dist/dts' } }),
|
||||
process.env.ROLLUP_WATCH ? undefined : del({ hook: 'buildEnd', targets: ['dist/widgets.tsc', 'dist/dts'] }),
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* This exports scheme works for nextjs and for CRA5.
|
||||
*
|
||||
* It will also work for CRA4 if you use direct imports:
|
||||
* instead of `import { SwapWidget } from '@uniswap/widgets'`,
|
||||
* `import { SwapWidget } from '@uniswap/widgets/dist/index.js'`.
|
||||
* I do not know why CRA4 does not seem to use exports for resolution.
|
||||
*
|
||||
* Note that chunks are enabled. This is so the tokenlist spec can be loaded async,
|
||||
* to improve first load time (due to ajv). Locales are also in separate chunks.
|
||||
*
|
||||
* Lastly, note that JSON and lingui are bundled into the library, as neither are fully
|
||||
* supported/compatible with ES Modules. Both _could_ be bundled only with esm, but this
|
||||
* yields a less complex pipeline.
|
||||
*/
|
||||
|
||||
const transpile = {
|
||||
input: 'src/lib/index.tsx',
|
||||
output: [
|
||||
{
|
||||
dir: 'dist',
|
||||
format: 'esm',
|
||||
sourcemap: false,
|
||||
},
|
||||
{
|
||||
dir: 'dist/cjs',
|
||||
entryFileNames: '[name].cjs',
|
||||
chunkFileNames: '[name]-[hash].cjs',
|
||||
format: 'cjs',
|
||||
sourcemap: false,
|
||||
},
|
||||
],
|
||||
external: isEthers,
|
||||
plugins: [
|
||||
externals({
|
||||
exclude: [
|
||||
'constants',
|
||||
/@lingui\/(core|react)/, // @lingui incorrectly exports esm, so it must be bundled in
|
||||
/\.json$/, // esm does not support JSON loading, so it must be bundled in
|
||||
],
|
||||
deps: true,
|
||||
peerDeps: true,
|
||||
}),
|
||||
...plugins,
|
||||
|
||||
// Source code transformation
|
||||
url({ include: ASSET_EXTENSIONS.map((extname) => '**/*' + extname), limit: Infinity }), // imports assets as data URIs
|
||||
svgr({ exportType: 'named', svgo: false }), // imports svgs as React components
|
||||
sass({ output: 'dist/fonts.css' }), // generates fonts.css
|
||||
commonjs(), // transforms cjs dependencies into tree-shakeable ES modules
|
||||
|
||||
babel({
|
||||
babelHelpers: 'runtime',
|
||||
presets: ['@babel/preset-env', ['@babel/preset-react', { runtime: 'automatic' }], '@babel/preset-typescript'],
|
||||
extensions: EXTENSIONS,
|
||||
plugins: [
|
||||
'macros', // enables @lingui and styled-components macros
|
||||
'@babel/plugin-transform-runtime', // embeds the babel runtime for library distribution
|
||||
],
|
||||
}),
|
||||
],
|
||||
onwarn: squelchTypeWarnings, // this pipeline is only for transpilation
|
||||
}
|
||||
|
||||
const locales = {
|
||||
input: 'src/locales/*.js',
|
||||
output: [
|
||||
{
|
||||
dir: 'dist',
|
||||
format: 'esm',
|
||||
sourcemap: false,
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
copy({
|
||||
copyOnce: true,
|
||||
targets: [{ src: 'src/locales/*.js', dest: 'dist/cjs/locales', rename: (name) => `${name}.cjs` }],
|
||||
}),
|
||||
commonjs(),
|
||||
multi(),
|
||||
],
|
||||
}
|
||||
|
||||
const config = [check, type, transpile, locales]
|
||||
export default config
|
||||
|
||||
function squelchTranspilationWarnings(warning: RollupWarning, warn: (warning: RollupWarning) => void) {
|
||||
if (warning.pluginCode === 'TS5055') return
|
||||
warn(warning)
|
||||
}
|
||||
|
||||
function squelchTypeWarnings(warning: RollupWarning, warn: (warning: RollupWarning) => void) {
|
||||
if (warning.code === 'UNUSED_EXTERNAL_IMPORT') return
|
||||
warn(warning)
|
||||
}
|
||||
BIN
src/assets/images/tally.png
Normal file
BIN
src/assets/images/tally.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -10,7 +10,6 @@ const CopyIcon = styled(LinkStyledButton)`
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
text-decoration: none;
|
||||
font-size: 12px;
|
||||
:hover,
|
||||
:active,
|
||||
:focus {
|
||||
@@ -18,20 +17,36 @@ const CopyIcon = styled(LinkStyledButton)`
|
||||
color: ${({ color, theme }) => color || theme.text2};
|
||||
}
|
||||
`
|
||||
const TransactionStatusText = styled.span`
|
||||
const StyledText = styled.span`
|
||||
margin-left: 0.25rem;
|
||||
font-size: 12px;
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const Copied = ({ iconSize }: { iconSize?: number }) => (
|
||||
<StyledText>
|
||||
<CheckCircle size={iconSize ?? '16'} />
|
||||
<StyledText>
|
||||
<Trans>Copied</Trans>
|
||||
</StyledText>
|
||||
</StyledText>
|
||||
)
|
||||
|
||||
const Icon = ({ iconSize }: { iconSize?: number }) => (
|
||||
<StyledText>
|
||||
<Copy size={iconSize ?? '16'} />
|
||||
</StyledText>
|
||||
)
|
||||
|
||||
interface BaseProps {
|
||||
toCopy: string
|
||||
color?: string
|
||||
iconSize?: number
|
||||
iconPosition?: 'left' | 'right'
|
||||
}
|
||||
export type CopyHelperProps = BaseProps & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, keyof BaseProps>
|
||||
|
||||
export default function CopyHelper({ color, toCopy, children }: CopyHelperProps) {
|
||||
export default function CopyHelper({ color, toCopy, children, iconSize, iconPosition }: CopyHelperProps) {
|
||||
const [isCopied, setCopied] = useCopyClipboard()
|
||||
const copy = useCallback(() => {
|
||||
setCopied(toCopy)
|
||||
@@ -39,20 +54,11 @@ export default function CopyHelper({ color, toCopy, children }: CopyHelperProps)
|
||||
|
||||
return (
|
||||
<CopyIcon onClick={copy} color={color}>
|
||||
{iconPosition === 'left' ? isCopied ? <Copied iconSize={iconSize} /> : <Icon iconSize={iconSize} /> : null}
|
||||
{iconPosition === 'left' && <> </>}
|
||||
{isCopied ? '' : children}
|
||||
|
||||
{isCopied ? (
|
||||
<TransactionStatusText>
|
||||
<CheckCircle size={'12'} />
|
||||
<TransactionStatusText>
|
||||
<Trans>Copied</Trans>
|
||||
</TransactionStatusText>
|
||||
</TransactionStatusText>
|
||||
) : (
|
||||
<TransactionStatusText>
|
||||
<Copy size={'12'} />
|
||||
</TransactionStatusText>
|
||||
)}
|
||||
{iconPosition === 'right' && <> </>}
|
||||
{iconPosition === 'right' ? isCopied ? <Copied iconSize={iconSize} /> : <Icon iconSize={iconSize} /> : null}
|
||||
</CopyIcon>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ import {
|
||||
DepositLiquidityStakingTransactionInfo,
|
||||
ExactInputSwapTransactionInfo,
|
||||
ExactOutputSwapTransactionInfo,
|
||||
ExecuteTransactionInfo,
|
||||
MigrateV2LiquidityToV3TransactionInfo,
|
||||
QueueTransactionInfo,
|
||||
RemoveLiquidityV3TransactionInfo,
|
||||
SubmitProposalTransactionInfo,
|
||||
TransactionInfo,
|
||||
@@ -126,6 +128,16 @@ function VoteSummary({ info }: { info: VoteTransactionInfo }) {
|
||||
}
|
||||
}
|
||||
|
||||
function QueueSummary({ info }: { info: QueueTransactionInfo }) {
|
||||
const proposalKey = `${info.governorAddress}/${info.proposalId}`
|
||||
return <Trans>Queue proposal {proposalKey}.</Trans>
|
||||
}
|
||||
|
||||
function ExecuteSummary({ info }: { info: ExecuteTransactionInfo }) {
|
||||
const proposalKey = `${info.governorAddress}/${info.proposalId}`
|
||||
return <Trans>Execute proposal {proposalKey}.</Trans>
|
||||
}
|
||||
|
||||
function DelegateSummary({ info: { delegatee } }: { info: DelegateTransactionInfo }) {
|
||||
const { ENSName } = useENSName(delegatee)
|
||||
return <Trans>Delegate voting power to {ENSName ?? delegatee}</Trans>
|
||||
@@ -339,6 +351,12 @@ export function TransactionSummary({ info }: { info: TransactionInfo }) {
|
||||
case TransactionType.REMOVE_LIQUIDITY_V3:
|
||||
return <RemoveLiquidityV3Summary info={info} />
|
||||
|
||||
case TransactionType.QUEUE:
|
||||
return <QueueSummary info={info} />
|
||||
|
||||
case TransactionType.EXECUTE:
|
||||
return <ExecuteSummary info={info} />
|
||||
|
||||
case TransactionType.SUBMIT_PROPOSAL:
|
||||
return <SubmitProposalTransactionSummary info={info} />
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Connector } from '@web3-react/types'
|
||||
import CopyHelper from 'components/AccountDetails/Copy'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { useCallback, useContext } from 'react'
|
||||
import { ExternalLink as LinkIcon } from 'react-feather'
|
||||
import { useAppDispatch } from 'state/hooks'
|
||||
import { updateSelectedWallet } from 'state/user/reducer'
|
||||
import styled, { ThemeContext } from 'styled-components/macro'
|
||||
import { AbstractConnector } from 'web3-react-abstract-connector'
|
||||
|
||||
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
||||
import { injected, walletlink } from '../../connectors'
|
||||
import { coinbaseWallet, injected } from '../../connectors'
|
||||
import { SUPPORTED_WALLETS } from '../../constants/wallet'
|
||||
import { clearAllTransactions } from '../../state/transactions/reducer'
|
||||
import { ExternalLink, LinkStyledButton, ThemedText } from '../../theme'
|
||||
@@ -17,7 +18,6 @@ import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
import StatusIcon from '../Identicon/StatusIcon'
|
||||
import { AutoRow } from '../Row'
|
||||
import Copy from './Copy'
|
||||
import Transaction from './Transaction'
|
||||
|
||||
const HeaderRow = styled.div`
|
||||
@@ -177,7 +177,7 @@ const IconWrapper = styled.div<{ size?: number }>`
|
||||
`};
|
||||
`
|
||||
|
||||
function WrappedStatusIcon({ connector }: { connector: AbstractConnector | Connector }) {
|
||||
function WrappedStatusIcon({ connector }: { connector: Connector }) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<StatusIcon connector={connector} />
|
||||
@@ -265,12 +265,16 @@ export default function AccountDetails({
|
||||
<AccountGroupingRow>
|
||||
{formatConnectorName()}
|
||||
<div>
|
||||
{connector !== injected && connector !== walletlink && (
|
||||
{/* Coinbase Wallet reloads the page right now, which breaks the selectedWallet from being set properly on localStorage */}
|
||||
{connector !== coinbaseWallet && (
|
||||
<WalletAction
|
||||
style={{ fontSize: '.825rem', fontWeight: 400, marginRight: '8px' }}
|
||||
onClick={() => {
|
||||
;(connector as any).close()
|
||||
connector.deactivate ? connector.deactivate() : connector.resetState()
|
||||
dispatch(updateSelectedWallet({ wallet: undefined }))
|
||||
openOptions()
|
||||
}}
|
||||
data-cy="wallet-disconnect"
|
||||
>
|
||||
<Trans>Disconnect</Trans>
|
||||
</WalletAction>
|
||||
@@ -280,6 +284,7 @@ export default function AccountDetails({
|
||||
onClick={() => {
|
||||
openOptions()
|
||||
}}
|
||||
data-cy="wallet-change"
|
||||
>
|
||||
<Trans>Change</Trans>
|
||||
</WalletAction>
|
||||
@@ -310,11 +315,11 @@ export default function AccountDetails({
|
||||
<AccountControl>
|
||||
<div>
|
||||
{account && (
|
||||
<Copy toCopy={account}>
|
||||
<CopyHelper toCopy={account} iconPosition="left">
|
||||
<span style={{ marginLeft: '4px' }}>
|
||||
<Trans>Copy Address</Trans>
|
||||
</span>
|
||||
</Copy>
|
||||
</CopyHelper>
|
||||
)}
|
||||
{chainId && account && (
|
||||
<AddressLink
|
||||
@@ -336,11 +341,11 @@ export default function AccountDetails({
|
||||
<AccountControl>
|
||||
<div>
|
||||
{account && (
|
||||
<Copy toCopy={account}>
|
||||
<CopyHelper toCopy={account} iconPosition="left">
|
||||
<span style={{ marginLeft: '4px' }}>
|
||||
<Trans>Copy Address</Trans>
|
||||
</span>
|
||||
</Copy>
|
||||
</CopyHelper>
|
||||
)}
|
||||
{chainId && account && (
|
||||
<AddressLink
|
||||
|
||||
@@ -18,6 +18,9 @@ const WarningIcon = styled(AlertOctagon)`
|
||||
min-width: 22px;
|
||||
color: ${({ theme }) => theme.warning};
|
||||
`
|
||||
const Copy = styled(CopyHelper)`
|
||||
font-size: 12px;
|
||||
`
|
||||
|
||||
interface ConnectedAccountBlockedProps {
|
||||
account: string | null | undefined
|
||||
@@ -46,9 +49,9 @@ export default function ConnectedAccountBlocked(props: ConnectedAccountBlockedPr
|
||||
<ThemedText.Main fontSize={12}>
|
||||
<Trans>If you believe this is an error, please send an email including your address to </Trans>{' '}
|
||||
</ThemedText.Main>
|
||||
<CopyHelper toCopy="compliance@uniswap.org" color={theme.primary1}>
|
||||
compliance@uniswap.org.
|
||||
</CopyHelper>
|
||||
<Copy iconSize={12} toCopy="compliance@uniswap.org" color={theme.primary1} iconPosition="right">
|
||||
compliance@uniswap.org
|
||||
</Copy>
|
||||
</ContentWrapper>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
@@ -27,13 +27,16 @@ export function FiatValue({
|
||||
return theme.red1
|
||||
}, [priceImpact, theme.green1, theme.red1, theme.text3, theme.yellow1])
|
||||
|
||||
const p = Number(fiatValue?.toFixed())
|
||||
const visibleDecimalPlaces = p < 1.05 ? 4 : 2
|
||||
|
||||
return (
|
||||
<ThemedText.Body fontSize={14} color={fiatValue ? theme.text3 : theme.text4}>
|
||||
{fiatValue ? (
|
||||
<Trans>
|
||||
$
|
||||
<HoverInlineText
|
||||
text={fiatValue?.toSignificant(6, { groupSeparator: ',' })}
|
||||
text={fiatValue?.toFixed(visibleDecimalPlaces, { groupSeparator: ',' })}
|
||||
textColor={fiatValue ? theme.text3 : theme.text4}
|
||||
/>
|
||||
</Trans>
|
||||
|
||||
@@ -5,17 +5,23 @@ import styled from 'styled-components/macro'
|
||||
|
||||
import Logo from '../Logo'
|
||||
|
||||
const StyledLogo = styled(Logo)<{ size: string; native: boolean }>`
|
||||
const StyledLogo = styled(Logo)<{ size: string }>`
|
||||
width: ${({ size }) => size};
|
||||
height: ${({ size }) => size};
|
||||
background: radial-gradient(white 50%, #ffffff00 calc(75% + 1px), #ffffff00 100%);
|
||||
border-radius: 50%;
|
||||
-mox-box-shadow: 0 0 1px ${({ native }) => (native ? 'white' : 'black')};
|
||||
-webkit-box-shadow: 0 0 1px ${({ native }) => (native ? 'white' : 'black')};
|
||||
box-shadow: 0 0 1px ${({ native }) => (native ? 'white' : 'black')};
|
||||
-mox-box-shadow: 0 0 1px black;
|
||||
-webkit-box-shadow: 0 0 1px black;
|
||||
box-shadow: 0 0 1px black;
|
||||
border: 0px solid rgba(255, 255, 255, 0);
|
||||
`
|
||||
|
||||
const StyledNativeLogo = styled(StyledLogo)`
|
||||
-mox-box-shadow: 0 0 1px white;
|
||||
-webkit-box-shadow: 0 0 1px white;
|
||||
box-shadow: 0 0 1px white;
|
||||
`
|
||||
|
||||
export default function CurrencyLogo({
|
||||
currency,
|
||||
size = '24px',
|
||||
@@ -26,16 +32,13 @@ export default function CurrencyLogo({
|
||||
size?: string
|
||||
style?: React.CSSProperties
|
||||
}) {
|
||||
const logoURIs = useCurrencyLogoURIs(currency)
|
||||
const props = {
|
||||
alt: `${currency?.symbol ?? 'token'} logo`,
|
||||
size,
|
||||
srcs: useCurrencyLogoURIs(currency),
|
||||
style,
|
||||
...rest,
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledLogo
|
||||
size={size}
|
||||
native={currency?.isNative ?? false}
|
||||
srcs={logoURIs}
|
||||
alt={`${currency?.symbol ?? 'token'} logo`}
|
||||
style={style}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
return currency?.isNative ? <StyledNativeLogo {...props} /> : <StyledLogo {...props} />
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import React, { ErrorInfo } from 'react'
|
||||
import ReactGA from 'react-ga4'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import store, { AppState } from '../../state'
|
||||
@@ -85,7 +85,10 @@ export default class ErrorBoundary extends React.Component<unknown, ErrorBoundar
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
ReactGA.event('exception', { description: error.toString() + errorInfo.toString(), fatal: true })
|
||||
sendEvent('exception', {
|
||||
description: error.toString() + errorInfo.toString(),
|
||||
fatal: true,
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import { FeeAmount } from '@uniswap/v3-sdk'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import { ButtonGray } from 'components/Button'
|
||||
import Card from 'components/Card'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
@@ -10,8 +11,7 @@ import { useFeeTierDistribution } from 'hooks/useFeeTierDistribution'
|
||||
import { PoolState, usePools } from 'hooks/usePools'
|
||||
import usePrevious from 'hooks/usePrevious'
|
||||
import { DynamicSection } from 'pages/AddLiquidity/styled'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import ReactGA from 'react-ga4'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Box } from 'rebass'
|
||||
import styled, { keyframes } from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
@@ -101,7 +101,7 @@ export default function FeeSelector({
|
||||
|
||||
const handleFeePoolSelectWithEvent = useCallback(
|
||||
(fee: FeeAmount) => {
|
||||
ReactGA.event({
|
||||
sendEvent({
|
||||
category: 'FeePoolSelect',
|
||||
action: 'Manual',
|
||||
})
|
||||
@@ -122,7 +122,7 @@ export default function FeeSelector({
|
||||
setShowOptions(false)
|
||||
|
||||
recommended.current = true
|
||||
ReactGA.event({
|
||||
sendEvent({
|
||||
category: 'FeePoolSelect',
|
||||
action: ' Recommended',
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { getWalletForConnector } from 'connectors'
|
||||
import { CHAIN_INFO } from 'constants/chainInfo'
|
||||
import { CHAIN_IDS_TO_NAMES, SupportedChainId } from 'constants/chains'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
@@ -11,12 +12,12 @@ import { ArrowDownCircle, ChevronDown } from 'react-feather'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { useModalOpen, useToggleModal } from 'state/application/hooks'
|
||||
import { addPopup, ApplicationModal } from 'state/application/reducer'
|
||||
import { useAppDispatch } from 'state/hooks'
|
||||
import { updateWalletError } from 'state/wallet/reducer'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ExternalLink, MEDIA_WIDTHS } from 'theme'
|
||||
import { replaceURLParam } from 'utils/routes'
|
||||
|
||||
import { useAppDispatch } from '../../state/hooks'
|
||||
import { switchToNetwork } from '../../utils/switchToNetwork'
|
||||
import { isChainAllowed, switchChain } from 'utils/switchChain'
|
||||
|
||||
const ActiveRowLinkList = styled.div`
|
||||
display: flex;
|
||||
@@ -93,6 +94,13 @@ const FlyoutRowActiveIndicator = styled.div`
|
||||
height: 9px;
|
||||
width: 9px;
|
||||
`
|
||||
|
||||
const CircleContainer = styled.div`
|
||||
width: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
const LinkOutCircle = styled(ArrowDownCircle)`
|
||||
transform: rotate(230deg);
|
||||
width: 16px;
|
||||
@@ -177,8 +185,8 @@ function Row({
|
||||
targetChain: SupportedChainId
|
||||
onSelectChain: (targetChain: number) => void
|
||||
}) {
|
||||
const { library, chainId } = useActiveWeb3React()
|
||||
if (!library || !chainId) {
|
||||
const { provider, chainId } = useActiveWeb3React()
|
||||
if (!provider || !chainId) {
|
||||
return null
|
||||
}
|
||||
const active = chainId === targetChain
|
||||
@@ -188,7 +196,11 @@ function Row({
|
||||
<FlyoutRow onClick={() => onSelectChain(targetChain)} active={active}>
|
||||
<Logo src={logoUrl} />
|
||||
<NetworkLabel>{label}</NetworkLabel>
|
||||
{chainId === targetChain && <FlyoutRowActiveIndicator />}
|
||||
{chainId === targetChain && (
|
||||
<CircleContainer>
|
||||
<FlyoutRowActiveIndicator />
|
||||
</CircleContainer>
|
||||
)}
|
||||
</FlyoutRow>
|
||||
)
|
||||
|
||||
@@ -197,21 +209,30 @@ function Row({
|
||||
<ActiveRowWrapper>
|
||||
{rowContent}
|
||||
<ActiveRowLinkList>
|
||||
{bridge ? (
|
||||
{bridge && (
|
||||
<ExternalLink href={bridge}>
|
||||
<BridgeLabel chainId={chainId} /> <LinkOutCircle />
|
||||
<BridgeLabel chainId={chainId} />
|
||||
<CircleContainer>
|
||||
<LinkOutCircle />
|
||||
</CircleContainer>
|
||||
</ExternalLink>
|
||||
) : null}
|
||||
{explorer ? (
|
||||
)}
|
||||
{explorer && (
|
||||
<ExternalLink href={explorer}>
|
||||
<ExplorerLabel chainId={chainId} /> <LinkOutCircle />
|
||||
<ExplorerLabel chainId={chainId} />
|
||||
<CircleContainer>
|
||||
<LinkOutCircle />
|
||||
</CircleContainer>
|
||||
</ExternalLink>
|
||||
) : null}
|
||||
{helpCenterUrl ? (
|
||||
)}
|
||||
{helpCenterUrl && (
|
||||
<ExternalLink href={helpCenterUrl}>
|
||||
<Trans>Help Center</Trans> <LinkOutCircle />
|
||||
<Trans>Help Center</Trans>
|
||||
<CircleContainer>
|
||||
<LinkOutCircle />
|
||||
</CircleContainer>
|
||||
</ExternalLink>
|
||||
) : null}
|
||||
)}
|
||||
</ActiveRowLinkList>
|
||||
</ActiveRowWrapper>
|
||||
)
|
||||
@@ -237,8 +258,16 @@ const getChainNameFromId = (id: string | number) => {
|
||||
return CHAIN_IDS_TO_NAMES[id as SupportedChainId] || ''
|
||||
}
|
||||
|
||||
const NETWORK_SELECTOR_CHAINS = [
|
||||
SupportedChainId.MAINNET,
|
||||
SupportedChainId.POLYGON,
|
||||
SupportedChainId.OPTIMISM,
|
||||
SupportedChainId.ARBITRUM_ONE,
|
||||
]
|
||||
|
||||
export default function NetworkSelector() {
|
||||
const { chainId, library } = useActiveWeb3React()
|
||||
const dispatch = useAppDispatch()
|
||||
const { chainId, provider, connector } = useActiveWeb3React()
|
||||
const parsedQs = useParsedQueryString()
|
||||
const { urlChain, urlChainId } = getParsedChainId(parsedQs)
|
||||
const prevChainId = usePrevious(chainId)
|
||||
@@ -251,37 +280,39 @@ export default function NetworkSelector() {
|
||||
|
||||
const info = chainId ? CHAIN_INFO[chainId] : undefined
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
const onSelectChain = useCallback(
|
||||
async (targetChain: number, skipToggle?: boolean) => {
|
||||
if (!connector) return
|
||||
|
||||
const handleChainSwitch = useCallback(
|
||||
(targetChain: number, skipToggle?: boolean) => {
|
||||
if (!library?.provider) return
|
||||
switchToNetwork({ provider: library.provider, chainId: targetChain })
|
||||
.then(() => {
|
||||
if (!skipToggle) {
|
||||
toggle()
|
||||
}
|
||||
history.replace({
|
||||
search: replaceURLParam(history.location.search, 'chain', getChainNameFromId(targetChain)),
|
||||
})
|
||||
const wallet = getWalletForConnector(connector)
|
||||
|
||||
try {
|
||||
dispatch(updateWalletError({ wallet, error: undefined }))
|
||||
await switchChain(connector, targetChain)
|
||||
if (!skipToggle) {
|
||||
toggle()
|
||||
}
|
||||
history.replace({
|
||||
search: replaceURLParam(history.location.search, 'chain', getChainNameFromId(targetChain)),
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to switch networks', error)
|
||||
} catch (error) {
|
||||
console.error('Failed to switch networks', error)
|
||||
|
||||
// we want app network <-> chainId param to be in sync, so if user changes the network by changing the URL
|
||||
// but the request fails, revert the URL back to current chainId
|
||||
if (chainId) {
|
||||
history.replace({ search: replaceURLParam(history.location.search, 'chain', getChainNameFromId(chainId)) })
|
||||
}
|
||||
// we want app network <-> chainId param to be in sync, so if user changes the network by changing the URL
|
||||
// but the request fails, revert the URL back to current chainId
|
||||
if (chainId) {
|
||||
history.replace({ search: replaceURLParam(history.location.search, 'chain', getChainNameFromId(chainId)) })
|
||||
}
|
||||
|
||||
if (!skipToggle) {
|
||||
toggle()
|
||||
}
|
||||
if (!skipToggle) {
|
||||
toggle()
|
||||
}
|
||||
|
||||
dispatch(addPopup({ content: { failedSwitchNetwork: targetChain }, key: `failed-network-switch` }))
|
||||
})
|
||||
dispatch(updateWalletError({ wallet, error: error.message }))
|
||||
dispatch(addPopup({ content: { failedSwitchNetwork: targetChain }, key: `failed-network-switch` }))
|
||||
}
|
||||
},
|
||||
[dispatch, library, toggle, history, chainId]
|
||||
[connector, toggle, dispatch, history, chainId]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -292,9 +323,9 @@ export default function NetworkSelector() {
|
||||
history.replace({ search: replaceURLParam(history.location.search, 'chain', getChainNameFromId(chainId)) })
|
||||
// otherwise assume network change originates from URL
|
||||
} else if (urlChainId && urlChainId !== chainId) {
|
||||
handleChainSwitch(urlChainId, true)
|
||||
onSelectChain(urlChainId, true)
|
||||
}
|
||||
}, [chainId, urlChainId, prevChainId, handleChainSwitch, history])
|
||||
}, [chainId, urlChainId, prevChainId, onSelectChain, history])
|
||||
|
||||
// set chain parameter on initial load if not there
|
||||
useEffect(() => {
|
||||
@@ -303,7 +334,7 @@ export default function NetworkSelector() {
|
||||
}
|
||||
}, [chainId, history, urlChainId, urlChain])
|
||||
|
||||
if (!chainId || !info || !library) {
|
||||
if (!chainId || !info || !provider) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -320,10 +351,11 @@ export default function NetworkSelector() {
|
||||
<FlyoutHeader>
|
||||
<Trans>Select a network</Trans>
|
||||
</FlyoutHeader>
|
||||
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.MAINNET} />
|
||||
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.POLYGON} />
|
||||
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.OPTIMISM} />
|
||||
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.ARBITRUM_ONE} />
|
||||
{NETWORK_SELECTOR_CHAINS.map((chainId: SupportedChainId) =>
|
||||
isChainAllowed(connector, chainId) ? (
|
||||
<Row onSelectChain={onSelectChain} targetChain={chainId} key={chainId} />
|
||||
) : null
|
||||
)}
|
||||
</FlyoutMenuContents>
|
||||
</FlyoutMenu>
|
||||
)}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useUserHasSubmittedClaim } from 'state/transactions/hooks'
|
||||
import { useDarkModeManager } from 'state/user/hooks'
|
||||
import { useNativeCurrencyBalances } from 'state/wallet/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { isChainAllowed } from 'utils/switchChain'
|
||||
|
||||
import { ReactComponent as Logo } from '../../assets/svg/logo.svg'
|
||||
import { ExternalLink, ThemedText } from '../../theme'
|
||||
@@ -76,7 +77,7 @@ const HeaderElement = styled.div`
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
/* addresses safari's lack of support for "gap" */
|
||||
/* addresses safaris lack of support for "gap" */
|
||||
& > *:not(:first-child) {
|
||||
margin-left: 8px;
|
||||
}
|
||||
@@ -246,7 +247,9 @@ const StyledExternalLink = styled(ExternalLink).attrs({
|
||||
`
|
||||
|
||||
export default function Header() {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const { account, chainId, connector } = useActiveWeb3React()
|
||||
|
||||
const chainAllowed = chainId && isChainAllowed(connector, chainId)
|
||||
|
||||
const userEthBalance = useNativeCurrencyBalances(account ? [account] : [])?.[account ?? '']
|
||||
const [darkMode] = useDarkModeManager()
|
||||
@@ -265,7 +268,7 @@ export default function Header() {
|
||||
const {
|
||||
infoLink,
|
||||
nativeCurrency: { symbol: nativeCurrencySymbol },
|
||||
} = CHAIN_INFO[chainId ? chainId : SupportedChainId.MAINNET]
|
||||
} = CHAIN_INFO[!chainId || !chainAllowed ? SupportedChainId.MAINNET : chainId]
|
||||
|
||||
return (
|
||||
<HeaderFrame showBackground={scrollY > 45}>
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import { Connector } from '@web3-react/types'
|
||||
import { AbstractConnector } from 'web3-react-abstract-connector'
|
||||
|
||||
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
|
||||
import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
|
||||
import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
|
||||
import { fortmatic, injected, walletconnect, walletlink } from '../../connectors'
|
||||
import { coinbaseWallet, fortmatic, injected, walletConnect } from '../../connectors'
|
||||
import Identicon from '../Identicon'
|
||||
|
||||
export default function StatusIcon({ connector }: { connector: AbstractConnector | Connector }) {
|
||||
export default function StatusIcon({ connector }: { connector: Connector }) {
|
||||
switch (connector) {
|
||||
case injected:
|
||||
return <Identicon />
|
||||
case walletconnect:
|
||||
return <img src={WalletConnectIcon} alt={'WalletConnect'} />
|
||||
case walletlink:
|
||||
return <img src={CoinbaseWalletIcon} alt={'Coinbase Wallet'} />
|
||||
case walletConnect:
|
||||
return <img src={WalletConnectIcon} alt="WalletConnect" />
|
||||
case coinbaseWallet:
|
||||
return <img src={CoinbaseWalletIcon} alt="Coinbase Wallet" />
|
||||
case fortmatic:
|
||||
return <img src={FortmaticIcon} alt={'Fortmatic'} />
|
||||
return <img src={FortmaticIcon} alt="Fortmatic" />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Currency, Price, Token } from '@uniswap/sdk-core'
|
||||
import { FeeAmount } from '@uniswap/v3-sdk'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import { AutoColumn, ColumnCenter } from 'components/Column'
|
||||
import Loader from 'components/Loader'
|
||||
import { format } from 'd3'
|
||||
@@ -9,7 +10,6 @@ import useTheme from 'hooks/useTheme'
|
||||
import { saturate } from 'polished'
|
||||
import React, { ReactNode, useCallback, useMemo } from 'react'
|
||||
import { BarChart2, CloudOff, Inbox } from 'react-feather'
|
||||
import ReactGA from 'react-ga4'
|
||||
import { batch } from 'react-redux'
|
||||
import { Bound } from 'state/mint/v3/actions'
|
||||
import styled from 'styled-components/macro'
|
||||
@@ -158,7 +158,7 @@ export default function LiquidityChartRangeInput({
|
||||
)
|
||||
|
||||
if (isError) {
|
||||
ReactGA.event('exception', { description: error.toString(), fatal: false })
|
||||
sendEvent('exception', { description: error.toString(), fatal: false })
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { Heart, X } from 'react-feather'
|
||||
import ReactGA from 'react-ga4'
|
||||
import styled, { keyframes } from 'styled-components/macro'
|
||||
|
||||
import tokenLogo from '../../assets/images/token-logo.png'
|
||||
@@ -65,7 +65,7 @@ export default function ClaimPopup() {
|
||||
const showClaimModal = useModalOpen(ApplicationModal.SELF_CLAIM)
|
||||
const toggleSelfClaimModal = useToggleSelfClaimModal()
|
||||
const handleToggleSelfClaimModal = useCallback(() => {
|
||||
ReactGA.event({
|
||||
sendEvent({
|
||||
category: 'MerkleDrop',
|
||||
action: 'Toggle self claim modal',
|
||||
})
|
||||
@@ -79,7 +79,7 @@ export default function ClaimPopup() {
|
||||
// listen for available claim and show popup if needed
|
||||
useEffect(() => {
|
||||
if (userHasAvailableclaim) {
|
||||
ReactGA.event({
|
||||
sendEvent({
|
||||
category: 'MerkleDrop',
|
||||
action: 'Show claim popup',
|
||||
})
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { RowFixed } from 'components/Row'
|
||||
import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp'
|
||||
import { useEffect } from 'react'
|
||||
import { MessageCircle, X } from 'react-feather'
|
||||
import ReactGA from 'react-ga4'
|
||||
import { useShowSurveyPopup } from 'state/user/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ExternalLink, ThemedText, Z_INDEX } from 'theme'
|
||||
@@ -62,7 +62,7 @@ export default function SurveyPopup() {
|
||||
if (Math.random() < 0.01) {
|
||||
setShowSurveyPopup(true)
|
||||
// log a case of succesful view
|
||||
ReactGA.event({
|
||||
sendEvent({
|
||||
category: 'Survey',
|
||||
action: 'Saw Survey',
|
||||
})
|
||||
@@ -80,7 +80,7 @@ export default function SurveyPopup() {
|
||||
<Wrapper gap="10px">
|
||||
<WrappedCloseIcon
|
||||
onClick={() => {
|
||||
ReactGA.event({
|
||||
sendEvent({
|
||||
category: 'Survey',
|
||||
action: 'Clicked Survey Link',
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { ButtonText } from 'components/Button'
|
||||
import PositionListItem from 'components/PositionListItem'
|
||||
import Toggle from 'components/Toggle'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
import { MEDIA_WIDTHS } from 'theme'
|
||||
@@ -28,9 +28,37 @@ const MobileHeader = styled.div`
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: ${MEDIA_WIDTHS.upToExtraSmall}px) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
}
|
||||
`
|
||||
|
||||
const ToggleWrap = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const ToggleLabel = styled.div`
|
||||
opacity: 0.6;
|
||||
margin-right: 10px;
|
||||
`
|
||||
|
||||
const MobileTogglePosition = styled.div`
|
||||
@media screen and (max-width: ${MEDIA_WIDTHS.upToExtraSmall}px) {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
}
|
||||
`
|
||||
|
||||
type PositionListProps = React.PropsWithChildren<{
|
||||
@@ -51,12 +79,35 @@ export default function PositionList({
|
||||
<Trans>Your positions</Trans>
|
||||
{positions && ' (' + positions.length + ')'}
|
||||
</div>
|
||||
<ButtonText style={{ opacity: 0.6 }} onClick={() => setUserHideClosedPositions(!userHideClosedPositions)}>
|
||||
<Trans>Hide closed positions</Trans>
|
||||
</ButtonText>
|
||||
<ToggleWrap>
|
||||
<ToggleLabel>
|
||||
<Trans>Show closed positions</Trans>
|
||||
</ToggleLabel>
|
||||
<Toggle
|
||||
id="desktop-hide-closed-positions"
|
||||
isActive={!userHideClosedPositions}
|
||||
toggle={() => {
|
||||
setUserHideClosedPositions(!userHideClosedPositions)
|
||||
}}
|
||||
/>
|
||||
</ToggleWrap>
|
||||
</DesktopHeader>
|
||||
<MobileHeader>
|
||||
<Trans>Your positions</Trans>
|
||||
<ToggleWrap>
|
||||
<ToggleLabel>
|
||||
<Trans>Show closed positions</Trans>
|
||||
</ToggleLabel>
|
||||
<MobileTogglePosition>
|
||||
<Toggle
|
||||
id="mobile-hide-closed-positions"
|
||||
isActive={!userHideClosedPositions}
|
||||
toggle={() => {
|
||||
setUserHideClosedPositions(!userHideClosedPositions)
|
||||
}}
|
||||
/>
|
||||
</MobileTogglePosition>
|
||||
</ToggleWrap>
|
||||
</MobileHeader>
|
||||
{positions.map((p) => {
|
||||
return <PositionListItem key={p.tokenId.toString()} positionDetails={p} />
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import Card, { DarkGreyCard } from 'components/Card'
|
||||
import Row, { AutoRow, RowBetween } from 'components/Row'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { ArrowDown, Info, X } from 'react-feather'
|
||||
import ReactGA from 'react-ga4'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ExternalLink, ThemedText } from 'theme'
|
||||
import { isMobile } from 'utils/userAgent'
|
||||
@@ -87,7 +87,7 @@ export function PrivacyPolicyModal() {
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
|
||||
ReactGA.event({
|
||||
sendEvent({
|
||||
category: 'Modal',
|
||||
action: 'Show Legal',
|
||||
})
|
||||
@@ -167,11 +167,13 @@ export function PrivacyPolicy() {
|
||||
</AutoColumn>
|
||||
</DarkGreyCard>
|
||||
))}
|
||||
<Row justify="center" marginBottom="1rem">
|
||||
<ExternalLink href="https://help.uniswap.org/en/articles/5675203-terms-of-service-faq">
|
||||
<Trans>Learn more</Trans>
|
||||
</ExternalLink>
|
||||
</Row>
|
||||
<ThemedText.Body fontSize={12}>
|
||||
<Row justify="center" marginBottom="1rem">
|
||||
<ExternalLink href="https://help.uniswap.org/en/articles/5675203-terms-of-service-faq">
|
||||
<Trans>Learn more</Trans>
|
||||
</ExternalLink>
|
||||
</Row>
|
||||
</ThemedText.Body>
|
||||
</AutoColumn>
|
||||
</AutoColumn>
|
||||
</Wrapper>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import { ButtonOutlined } from 'components/Button'
|
||||
import { AutoRow } from 'components/Row'
|
||||
import React from 'react'
|
||||
import ReactGA from 'react-ga4'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
@@ -20,7 +20,7 @@ export default function PresetsButtons({ setFullRange }: { setFullRange: () => v
|
||||
<Button
|
||||
onClick={() => {
|
||||
setFullRange()
|
||||
ReactGA.event({
|
||||
sendEvent({
|
||||
category: 'Liquidity',
|
||||
action: 'Full Range Clicked',
|
||||
})
|
||||
|
||||
@@ -5,8 +5,8 @@ import Badge from 'components/Badge'
|
||||
import CurrencyLogo from 'components/CurrencyLogo'
|
||||
import DoubleCurrencyLogo from 'components/DoubleLogo'
|
||||
import Row, { AutoRow } from 'components/Row'
|
||||
import { RoutingDiagramEntry } from 'components/swap/SwapRoute'
|
||||
import { useTokenInfoFromActiveList } from 'hooks/useTokenInfoFromActiveList'
|
||||
import { RoutingDiagramEntry } from 'lib/components/Swap/RoutingDiagram/utils'
|
||||
import { Box } from 'rebass'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText, Z_INDEX } from 'theme'
|
||||
|
||||
@@ -49,6 +49,8 @@ export default function CommonBases({
|
||||
const isSelected = selectedCurrency?.equals(currency)
|
||||
return (
|
||||
<BaseWrapper
|
||||
tabIndex={0}
|
||||
onKeyPress={(e) => !isSelected && e.key === 'Enter' && onSelect(currency)}
|
||||
onClick={() => !isSelected && onSelect(currency)}
|
||||
disable={isSelected}
|
||||
key={currencyId(currency)}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders currency rows correctly when currencies list is non-empty 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
|
||||
>
|
||||
<div
|
||||
style="height: 168px; width: 100%;"
|
||||
>
|
||||
<div
|
||||
class="sc-bdnxRM Row-sc-u7azg8-0 Row__RowBetween-sc-u7azg8-1 styleds__MenuItem-sc-muzgnq-3 lmTMKd hLLNig hzJkYd firMKT token-item-0x6B175474E89094C44Da98b954EedeAC495271d0F"
|
||||
style="position: absolute; left: 0px; top: 0px; height: 56px; width: 100%;"
|
||||
tabindex="0"
|
||||
>
|
||||
CurrencyLogo currency=DAI
|
||||
<div
|
||||
class="Column-sc-1r2yyln-0 cYEAJI"
|
||||
>
|
||||
<div
|
||||
class="css-8mokm4"
|
||||
title="Dai Stablecoin"
|
||||
>
|
||||
DAI
|
||||
</div>
|
||||
<div
|
||||
class="theme__TextWrapper-sc-5lu8um-0 gVIOIC css-165qfk5"
|
||||
>
|
||||
Dai Stablecoin
|
||||
</div>
|
||||
</div>
|
||||
<span />
|
||||
</div>
|
||||
<div
|
||||
class="sc-bdnxRM Row-sc-u7azg8-0 Row__RowBetween-sc-u7azg8-1 styleds__MenuItem-sc-muzgnq-3 lmTMKd hLLNig hzJkYd firMKT token-item-0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
||||
style="position: absolute; left: 0px; top: 56px; height: 56px; width: 100%;"
|
||||
tabindex="0"
|
||||
>
|
||||
CurrencyLogo currency=USDC
|
||||
<div
|
||||
class="Column-sc-1r2yyln-0 cYEAJI"
|
||||
>
|
||||
<div
|
||||
class="css-8mokm4"
|
||||
title="USD//C"
|
||||
>
|
||||
USDC
|
||||
</div>
|
||||
<div
|
||||
class="theme__TextWrapper-sc-5lu8um-0 gVIOIC css-165qfk5"
|
||||
>
|
||||
USD//C
|
||||
</div>
|
||||
</div>
|
||||
<span />
|
||||
</div>
|
||||
<div
|
||||
class="sc-bdnxRM Row-sc-u7azg8-0 Row__RowBetween-sc-u7azg8-1 styleds__MenuItem-sc-muzgnq-3 lmTMKd hLLNig hzJkYd firMKT token-item-0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
|
||||
style="position: absolute; left: 0px; top: 112px; height: 56px; width: 100%;"
|
||||
tabindex="0"
|
||||
>
|
||||
CurrencyLogo currency=WBTC
|
||||
<div
|
||||
class="Column-sc-1r2yyln-0 cYEAJI"
|
||||
>
|
||||
<div
|
||||
class="css-8mokm4"
|
||||
title="Wrapped BTC"
|
||||
>
|
||||
WBTC
|
||||
</div>
|
||||
<div
|
||||
class="theme__TextWrapper-sc-5lu8um-0 gVIOIC css-165qfk5"
|
||||
>
|
||||
Wrapped BTC
|
||||
</div>
|
||||
</div>
|
||||
<span />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`renders loading rows when isLoading is true 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
|
||||
>
|
||||
<div
|
||||
style="height: 0px; width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
73
src/components/SearchModal/CurrencyList/index.test.tsx
Normal file
73
src/components/SearchModal/CurrencyList/index.test.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Currency, CurrencyAmount as mockCurrencyAmount, Token as mockToken } from '@uniswap/sdk-core'
|
||||
import { DAI, USDC_MAINNET, WBTC } from 'constants/tokens'
|
||||
import * as mockJSBI from 'jsbi'
|
||||
import { render } from 'test-utils'
|
||||
|
||||
import CurrencyList from '.'
|
||||
|
||||
const noOp = function () {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
const mockCurrencyAmt = {
|
||||
[DAI.address]: mockCurrencyAmount.fromRawAmount(DAI, mockJSBI.default.BigInt(100)),
|
||||
[USDC_MAINNET.address]: mockCurrencyAmount.fromRawAmount(USDC_MAINNET, mockJSBI.default.BigInt(10)),
|
||||
[WBTC.address]: mockCurrencyAmount.fromRawAmount(WBTC, mockJSBI.default.BigInt(1)),
|
||||
}
|
||||
|
||||
jest.mock(
|
||||
'components/CurrencyLogo',
|
||||
() =>
|
||||
({ currency }: { currency: Currency }) =>
|
||||
`CurrencyLogo currency=${currency.symbol}`
|
||||
)
|
||||
|
||||
jest.mock('hooks/useActiveWeb3React', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
default: () => ({
|
||||
account: '123',
|
||||
active: true,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('../../../state/wallet/hooks', () => {
|
||||
return {
|
||||
useCurrencyBalance: (currency: Currency) => {
|
||||
return mockCurrencyAmt[(currency as mockToken).address]
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
it('renders loading rows when isLoading is true', () => {
|
||||
const { asFragment } = render(
|
||||
<CurrencyList
|
||||
height={10}
|
||||
currencies={[]}
|
||||
otherListTokens={[]}
|
||||
selectedCurrency={null}
|
||||
onCurrencySelect={noOp}
|
||||
showImportView={noOp}
|
||||
setImportToken={noOp}
|
||||
isLoading={true}
|
||||
/>
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders currency rows correctly when currencies list is non-empty', () => {
|
||||
const { asFragment } = render(
|
||||
<CurrencyList
|
||||
height={10}
|
||||
currencies={[DAI, USDC_MAINNET, WBTC]}
|
||||
otherListTokens={[]}
|
||||
selectedCurrency={null}
|
||||
onCurrencySelect={noOp}
|
||||
showImportView={noOp}
|
||||
setImportToken={noOp}
|
||||
isLoading={false}
|
||||
/>
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
})
|
||||
@@ -9,20 +9,20 @@ import { FixedSizeList } from 'react-window'
|
||||
import { Text } from 'rebass'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import TokenListLogo from '../../assets/svg/tokenlist.svg'
|
||||
import { useIsUserAddedToken } from '../../hooks/Tokens'
|
||||
import { useCombinedActiveList } from '../../state/lists/hooks'
|
||||
import { WrappedTokenInfo } from '../../state/lists/wrappedTokenInfo'
|
||||
import { useCurrencyBalance } from '../../state/wallet/hooks'
|
||||
import { ThemedText } from '../../theme'
|
||||
import { isTokenOnList } from '../../utils'
|
||||
import Column from '../Column'
|
||||
import CurrencyLogo from '../CurrencyLogo'
|
||||
import Loader from '../Loader'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import { MouseoverTooltip } from '../Tooltip'
|
||||
import ImportRow from './ImportRow'
|
||||
import { MenuItem } from './styleds'
|
||||
import TokenListLogo from '../../../assets/svg/tokenlist.svg'
|
||||
import { useIsUserAddedToken } from '../../../hooks/Tokens'
|
||||
import { useCombinedActiveList } from '../../../state/lists/hooks'
|
||||
import { WrappedTokenInfo } from '../../../state/lists/wrappedTokenInfo'
|
||||
import { useCurrencyBalance } from '../../../state/wallet/hooks'
|
||||
import { ThemedText } from '../../../theme'
|
||||
import { isTokenOnList } from '../../../utils'
|
||||
import Column from '../../Column'
|
||||
import CurrencyLogo from '../../CurrencyLogo'
|
||||
import Loader from '../../Loader'
|
||||
import { RowBetween, RowFixed } from '../../Row'
|
||||
import { MouseoverTooltip } from '../../Tooltip'
|
||||
import ImportRow from '../ImportRow'
|
||||
import { LoadingRows, MenuItem } from '../styleds'
|
||||
|
||||
function currencyKey(currency: Currency): string {
|
||||
return currency.isToken ? currency.address : 'ETHER'
|
||||
@@ -124,8 +124,10 @@ function CurrencyRow({
|
||||
// only show add or remove buttons if not on selected list
|
||||
return (
|
||||
<MenuItem
|
||||
tabIndex={0}
|
||||
style={style}
|
||||
className={`token-item-${key}`}
|
||||
onKeyPress={(e) => (!isSelected && e.key === 'Enter' ? onSelect() : null)}
|
||||
onClick={() => (isSelected ? null : onSelect())}
|
||||
disabled={isSelected}
|
||||
selected={otherSelected}
|
||||
@@ -195,6 +197,7 @@ export default function CurrencyList({
|
||||
showImportView,
|
||||
setImportToken,
|
||||
showCurrencyAmount,
|
||||
isLoading,
|
||||
}: {
|
||||
height: number
|
||||
currencies: Currency[]
|
||||
@@ -206,6 +209,7 @@ export default function CurrencyList({
|
||||
showImportView: () => void
|
||||
setImportToken: (token: Token) => void
|
||||
showCurrencyAmount?: boolean
|
||||
isLoading: boolean
|
||||
}) {
|
||||
const itemData: (Currency | BreakLine)[] = useMemo(() => {
|
||||
if (otherListTokens && otherListTokens?.length > 0) {
|
||||
@@ -232,7 +236,15 @@ export default function CurrencyList({
|
||||
|
||||
const showImport = index > currencies.length
|
||||
|
||||
if (showImport && token) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<LoadingRows>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</LoadingRows>
|
||||
)
|
||||
} else if (showImport && token) {
|
||||
return (
|
||||
<ImportRow style={style} token={token} showImportView={showImportView} setImportToken={setImportToken} dim />
|
||||
)
|
||||
@@ -259,6 +271,7 @@ export default function CurrencyList({
|
||||
setImportToken,
|
||||
showImportView,
|
||||
showCurrencyAmount,
|
||||
isLoading,
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { Currency, Token } from '@uniswap/sdk-core'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import useDebounce from 'hooks/useDebounce'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
@@ -11,7 +12,6 @@ import { getTokenFilter } from 'lib/hooks/useTokenList/filtering'
|
||||
import { tokenComparator, useSortTokensByQuery } from 'lib/hooks/useTokenList/sorting'
|
||||
import { KeyboardEvent, RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Edit } from 'react-feather'
|
||||
import ReactGA from 'react-ga4'
|
||||
import AutoSizer from 'react-virtualized-auto-sizer'
|
||||
import { FixedSizeList } from 'react-window'
|
||||
import { Text } from 'rebass'
|
||||
@@ -74,6 +74,8 @@ export function CurrencySearch({
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const theme = useTheme()
|
||||
|
||||
const [tokenLoaderTimerElapsed, setTokenLoaderTimerElapsed] = useState(false)
|
||||
|
||||
// refs for fixed size lists
|
||||
const fixedList = useRef<FixedSizeList>()
|
||||
|
||||
@@ -91,7 +93,7 @@ export function CurrencySearch({
|
||||
|
||||
useEffect(() => {
|
||||
if (isAddressSearch) {
|
||||
ReactGA.event({
|
||||
sendEvent({
|
||||
category: 'Currency Select',
|
||||
action: 'Search by address',
|
||||
label: isAddressSearch,
|
||||
@@ -103,10 +105,11 @@ export function CurrencySearch({
|
||||
return Object.values(allTokens).filter(getTokenFilter(debouncedQuery))
|
||||
}, [allTokens, debouncedQuery])
|
||||
|
||||
const balances = useAllTokenBalances()
|
||||
const [balances, balancesIsLoading] = useAllTokenBalances()
|
||||
const sortedTokens: Token[] = useMemo(() => {
|
||||
return filteredTokens.sort(tokenComparator.bind(null, balances))
|
||||
}, [balances, filteredTokens])
|
||||
void balancesIsLoading // creates a new array once balances load to update hooks
|
||||
return [...filteredTokens].sort(tokenComparator.bind(null, balances))
|
||||
}, [balances, filteredTokens, balancesIsLoading])
|
||||
|
||||
const filteredSortedTokens = useSortTokensByQuery(debouncedQuery, sortedTokens)
|
||||
|
||||
@@ -173,6 +176,14 @@ export function CurrencySearch({
|
||||
filteredTokens.length === 0 || (debouncedQuery.length > 2 && !isAddressSearch) ? debouncedQuery : undefined
|
||||
)
|
||||
|
||||
// Timeout token loader after 3 seconds to avoid hanging in a loading state.
|
||||
useEffect(() => {
|
||||
const tokenLoaderTimer = setTimeout(() => {
|
||||
setTokenLoaderTimerElapsed(true)
|
||||
}, 3000)
|
||||
return () => clearTimeout(tokenLoaderTimer)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ContentWrapper>
|
||||
<PaddedColumn gap="16px">
|
||||
@@ -218,6 +229,7 @@ export function CurrencySearch({
|
||||
showImportView={showImportView}
|
||||
setImportToken={setImportToken}
|
||||
showCurrencyAmount={showCurrencyAmount}
|
||||
isLoading={balancesIsLoading && !tokenLoaderTimerElapsed}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { TokenList } from '@uniswap/token-lists'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import { ButtonPrimary } from 'components/Button'
|
||||
import Card from 'components/Card'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
@@ -11,7 +12,6 @@ import useTheme from 'hooks/useTheme'
|
||||
import { transparentize } from 'polished'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { AlertTriangle, ArrowLeft } from 'react-feather'
|
||||
import ReactGA from 'react-ga4'
|
||||
import { useAppDispatch } from 'state/hooks'
|
||||
import { enableList, removeList } from 'state/lists/actions'
|
||||
import { useAllLists } from 'state/lists/hooks'
|
||||
@@ -54,7 +54,7 @@ export function ImportList({ listURL, list, setModalView, onDismiss }: ImportPro
|
||||
setAddError(null)
|
||||
fetchList(listURL)
|
||||
.then(() => {
|
||||
ReactGA.event({
|
||||
sendEvent({
|
||||
category: 'Lists',
|
||||
action: 'Add List',
|
||||
label: listURL,
|
||||
@@ -66,7 +66,7 @@ export function ImportList({ listURL, list, setModalView, onDismiss }: ImportPro
|
||||
setModalView(CurrencyModalView.manage)
|
||||
})
|
||||
.catch((error) => {
|
||||
ReactGA.event({
|
||||
sendEvent({
|
||||
category: 'Lists',
|
||||
action: 'Add List Failed',
|
||||
label: listURL,
|
||||
|
||||
@@ -63,7 +63,7 @@ export default function ImportRow({
|
||||
const list = token instanceof WrappedTokenInfo ? token.list : undefined
|
||||
|
||||
return (
|
||||
<TokenSection style={style}>
|
||||
<TokenSection tabIndex={0} style={style}>
|
||||
<CurrencyLogo currency={token} size={'24px'} style={{ opacity: dim ? '0.6' : '1' }} />
|
||||
<AutoColumn gap="4px" style={{ opacity: dim ? '0.6' : '1' }}>
|
||||
<AutoRow>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { TokenList } from '@uniswap/token-lists'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import Card from 'components/Card'
|
||||
import { UNSUPPORTED_LIST_URLS } from 'constants/lists'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
@@ -9,7 +10,6 @@ import parseENSAddress from 'lib/utils/parseENSAddress'
|
||||
import uriToHttp from 'lib/utils/uriToHttp'
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { CheckCircle, Settings } from 'react-feather'
|
||||
import ReactGA from 'react-ga4'
|
||||
import { usePopper } from 'react-popper'
|
||||
import { useAppDispatch, useAppSelector } from 'state/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
@@ -26,7 +26,7 @@ import { ButtonEmpty, ButtonPrimary } from '../Button'
|
||||
import Column, { AutoColumn } from '../Column'
|
||||
import ListLogo from '../ListLogo'
|
||||
import Row, { RowBetween, RowFixed } from '../Row'
|
||||
import ListToggle from '../Toggle/ListToggle'
|
||||
import Toggle from '../Toggle'
|
||||
import { CurrencyModalView } from './CurrencySearchModal'
|
||||
import { PaddedColumn, SearchInput, Separator, SeparatorDark } from './styleds'
|
||||
|
||||
@@ -126,7 +126,7 @@ const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) {
|
||||
|
||||
const handleAcceptListUpdate = useCallback(() => {
|
||||
if (!pending) return
|
||||
ReactGA.event({
|
||||
sendEvent({
|
||||
category: 'Lists',
|
||||
action: 'Update List from List Select',
|
||||
label: listUrl,
|
||||
@@ -135,13 +135,13 @@ const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) {
|
||||
}, [dispatch, listUrl, pending])
|
||||
|
||||
const handleRemoveList = useCallback(() => {
|
||||
ReactGA.event({
|
||||
sendEvent({
|
||||
category: 'Lists',
|
||||
action: 'Start Remove List',
|
||||
label: listUrl,
|
||||
})
|
||||
if (window.prompt(t`Please confirm you would like to remove this list by typing REMOVE`) === `REMOVE`) {
|
||||
ReactGA.event({
|
||||
sendEvent({
|
||||
category: 'Lists',
|
||||
action: 'Confirm Remove List',
|
||||
label: listUrl,
|
||||
@@ -151,7 +151,7 @@ const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) {
|
||||
}, [dispatch, listUrl])
|
||||
|
||||
const handleEnableList = useCallback(() => {
|
||||
ReactGA.event({
|
||||
sendEvent({
|
||||
category: 'Lists',
|
||||
action: 'Enable List',
|
||||
label: listUrl,
|
||||
@@ -160,7 +160,7 @@ const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) {
|
||||
}, [dispatch, listUrl])
|
||||
|
||||
const handleDisableList = useCallback(() => {
|
||||
ReactGA.event({
|
||||
sendEvent({
|
||||
category: 'Lists',
|
||||
action: 'Disable List',
|
||||
label: listUrl,
|
||||
@@ -215,7 +215,7 @@ const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) {
|
||||
</StyledMenu>
|
||||
</RowFixed>
|
||||
</Column>
|
||||
<ListToggle
|
||||
<Toggle
|
||||
isActive={isActive}
|
||||
bgColor={listColor}
|
||||
toggle={() => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { LoadingRows as BaseLoadingRows } from 'components/Loader/styled'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { AutoColumn } from '../Column'
|
||||
@@ -72,3 +73,25 @@ export const SeparatorDark = styled.div`
|
||||
height: 1px;
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
`
|
||||
|
||||
export const LoadingRows = styled(BaseLoadingRows)`
|
||||
grid-column-gap: 0.5em;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
max-width: 960px;
|
||||
padding: 12px 20px;
|
||||
|
||||
& > div:nth-child(4n + 1) {
|
||||
grid-column: 1 / 8;
|
||||
height: 1em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
& > div:nth-child(4n + 2) {
|
||||
grid-column: 12;
|
||||
height: 1em;
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
& > div:nth-child(4n + 3) {
|
||||
grid-column: 1 / 4;
|
||||
height: 0.75em;
|
||||
}
|
||||
`
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { AUTO_ROUTER_SUPPORTED_CHAINS } from 'lib/hooks/routing/clientSideSmartOrderRouter'
|
||||
import { useContext, useRef, useState } from 'react'
|
||||
import { Settings, X } from 'react-feather'
|
||||
import ReactGA from 'react-ga4'
|
||||
import { Text } from 'rebass'
|
||||
import styled, { ThemeContext } from 'styled-components/macro'
|
||||
|
||||
@@ -211,7 +211,7 @@ export default function SettingsTab({ placeholderSlippage }: { placeholderSlippa
|
||||
id="toggle-optimized-router-button"
|
||||
isActive={!clientSideRouter}
|
||||
toggle={() => {
|
||||
ReactGA.event({
|
||||
sendEvent({
|
||||
category: 'Routing',
|
||||
action: clientSideRouter ? 'enable routing API' : 'disable routing API',
|
||||
})
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { ThemedText } from '../../theme'
|
||||
|
||||
const Wrapper = styled.button<{ isActive?: boolean; activeElement?: boolean }>`
|
||||
border-radius: 20px;
|
||||
border: none;
|
||||
background: ${({ theme }) => theme.bg1};
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
padding: 0.4rem 0.4rem;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const ToggleElement = styled.span<{ isActive?: boolean; bgColor?: string }>`
|
||||
border-radius: 50%;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
background-color: ${({ isActive, bgColor, theme }) => (isActive ? bgColor : theme.bg4)};
|
||||
:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
`
|
||||
|
||||
const StatusText = styled(ThemedText.Main)<{ isActive?: boolean }>`
|
||||
margin: 0 10px;
|
||||
width: 24px;
|
||||
color: ${({ theme, isActive }) => (isActive ? theme.text1 : theme.text3)};
|
||||
`
|
||||
|
||||
interface ToggleProps {
|
||||
id?: string
|
||||
isActive: boolean
|
||||
bgColor: string
|
||||
toggle: () => void
|
||||
}
|
||||
|
||||
export default function ListToggle({ id, isActive, bgColor, toggle }: ToggleProps) {
|
||||
return (
|
||||
<Wrapper id={id} isActive={isActive} onClick={toggle}>
|
||||
{isActive && (
|
||||
<StatusText fontWeight="600" margin="0 6px" isActive={true}>
|
||||
<Trans>ON</Trans>
|
||||
</StatusText>
|
||||
)}
|
||||
<ToggleElement isActive={isActive} bgColor={bgColor} />
|
||||
{!isActive && (
|
||||
<StatusText fontWeight="600" margin="0 6px" isActive={false}>
|
||||
<Trans>OFF</Trans>
|
||||
</StatusText>
|
||||
)}
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
@@ -1,57 +1,85 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { darken } from 'polished'
|
||||
import { ReactNode } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
import { useState } from 'react'
|
||||
import styled, { keyframes } from 'styled-components/macro'
|
||||
|
||||
const ToggleElement = styled.span<{ isActive?: boolean; isOnSwitch?: boolean }>`
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 9px;
|
||||
background: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.primary1 : theme.bg4) : 'none')};
|
||||
color: ${({ theme, isActive }) => (isActive ? theme.white : theme.text2)};
|
||||
font-size: 14px;
|
||||
font-weight: ${({ isOnSwitch }) => (isOnSwitch ? '500' : '400')};
|
||||
:hover {
|
||||
user-select: ${({ isOnSwitch }) => (isOnSwitch ? 'none' : 'initial')};
|
||||
background: ${({ theme, isActive, isOnSwitch }) =>
|
||||
isActive ? (isOnSwitch ? darken(0.05, theme.primary1) : darken(0.05, theme.bg4)) : 'none'};
|
||||
color: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.white : theme.white) : theme.text3)};
|
||||
const Wrapper = styled.button<{ isActive?: boolean; activeElement?: boolean }>`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.bg1};
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
outline: none;
|
||||
padding: 0.4rem 0.4rem;
|
||||
width: fit-content;
|
||||
`
|
||||
|
||||
const turnOnToggle = keyframes`
|
||||
from {
|
||||
margin-left: 0em;
|
||||
margin-right: 2.2em;
|
||||
}
|
||||
to {
|
||||
margin-left: 2.2em;
|
||||
margin-right: 0em;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledToggle = styled.button<{ isActive?: boolean; activeElement?: boolean }>`
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
background: ${({ theme }) => theme.bg0};
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
padding: 2px;
|
||||
const turnOffToggle = keyframes`
|
||||
from {
|
||||
margin-left: 2.2em;
|
||||
margin-right: 0em;
|
||||
}
|
||||
to {
|
||||
margin-left: 0em;
|
||||
margin-right: 2.2em;
|
||||
}
|
||||
`
|
||||
|
||||
const ToggleElementHoverStyle = (hasBgColor: boolean, theme: any, isActive?: boolean) =>
|
||||
hasBgColor
|
||||
? {
|
||||
opacity: '0.8',
|
||||
}
|
||||
: {
|
||||
background: isActive ? darken(0.05, theme.primary1) : darken(0.05, theme.bg4),
|
||||
color: isActive ? theme.white : theme.text3,
|
||||
}
|
||||
|
||||
const ToggleElement = styled.span<{ isActive?: boolean; bgColor?: string; isInitialToggleLoad?: boolean }>`
|
||||
animation: 0.1s
|
||||
${({ isActive, isInitialToggleLoad }) => (isInitialToggleLoad ? 'none' : isActive ? turnOnToggle : turnOffToggle)}
|
||||
ease-in;
|
||||
background: ${({ theme, bgColor, isActive }) =>
|
||||
isActive ? bgColor ?? theme.primary1 : !!bgColor ? theme.bg4 : theme.text3};
|
||||
border-radius: 50%;
|
||||
height: 24px;
|
||||
:hover {
|
||||
${({ bgColor, theme, isActive }) => ToggleElementHoverStyle(!!bgColor, theme, isActive)}
|
||||
}
|
||||
margin-left: ${({ isActive }) => (isActive ? '2.2em' : '0em')};
|
||||
margin-right: ${({ isActive }) => (!isActive ? '2.2em' : '0em')};
|
||||
width: 24px;
|
||||
`
|
||||
|
||||
interface ToggleProps {
|
||||
id?: string
|
||||
bgColor?: string
|
||||
isActive: boolean
|
||||
toggle: () => void
|
||||
checked?: ReactNode
|
||||
unchecked?: ReactNode
|
||||
}
|
||||
|
||||
export default function Toggle({
|
||||
id,
|
||||
isActive,
|
||||
toggle,
|
||||
checked = <Trans>On</Trans>,
|
||||
unchecked = <Trans>Off</Trans>,
|
||||
}: ToggleProps) {
|
||||
export default function Toggle({ id, bgColor, isActive, toggle }: ToggleProps) {
|
||||
const [isInitialToggleLoad, setIsInitialToggleLoad] = useState(true)
|
||||
|
||||
const switchToggle = () => {
|
||||
toggle()
|
||||
if (isInitialToggleLoad) setIsInitialToggleLoad(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledToggle id={id} isActive={isActive} onClick={toggle}>
|
||||
<ToggleElement isActive={isActive} isOnSwitch={true}>
|
||||
{checked}
|
||||
</ToggleElement>
|
||||
<ToggleElement isActive={!isActive} isOnSwitch={false}>
|
||||
{unchecked}
|
||||
</ToggleElement>
|
||||
</StyledToggle>
|
||||
<Wrapper id={id} isActive={isActive} onClick={switchToggle}>
|
||||
<ToggleElement isActive={isActive} bgColor={bgColor} isInitialToggleLoad={isInitialToggleLoad} />
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,15 +4,14 @@ import Badge from 'components/Badge'
|
||||
import { CHAIN_INFO } from 'constants/chainInfo'
|
||||
import { L2_CHAIN_IDS, SupportedL2ChainId } from 'constants/chains'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import useAddTokenToMetamask from 'hooks/useAddTokenToMetamask'
|
||||
import { ReactNode, useContext } from 'react'
|
||||
import useCurrencyLogoURIs from 'lib/hooks/useCurrencyLogoURIs'
|
||||
import { ReactNode, useCallback, useContext, useState } from 'react'
|
||||
import { AlertCircle, AlertTriangle, ArrowUpCircle, CheckCircle } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
import { useIsTransactionConfirmed, useTransaction } from 'state/transactions/hooks'
|
||||
import styled, { ThemeContext } from 'styled-components/macro'
|
||||
|
||||
import Circle from '../../assets/images/blue-loader.svg'
|
||||
import MetaMaskLogo from '../../assets/images/metamask.png'
|
||||
import { ExternalLink } from '../../theme'
|
||||
import { CloseIcon, CustomLightSpinner } from '../../theme'
|
||||
import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink'
|
||||
@@ -97,9 +96,25 @@ function TransactionSubmittedContent({
|
||||
}) {
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const { library } = useActiveWeb3React()
|
||||
const { connector } = useActiveWeb3React()
|
||||
|
||||
const { addToken, success } = useAddTokenToMetamask(currencyToAdd)
|
||||
const token = currencyToAdd?.wrapped
|
||||
const logoURL = useCurrencyLogoURIs(token)[0]
|
||||
|
||||
const [success, setSuccess] = useState<boolean | undefined>()
|
||||
|
||||
const addToken = useCallback(() => {
|
||||
if (!token?.symbol || !connector.watchAsset) return
|
||||
connector
|
||||
.watchAsset({
|
||||
address: token.address,
|
||||
symbol: token.symbol,
|
||||
decimals: token.decimals,
|
||||
image: logoURL,
|
||||
})
|
||||
.then(() => setSuccess(true))
|
||||
.catch(() => setSuccess(false))
|
||||
}, [connector, logoURL, token])
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
@@ -124,13 +139,11 @@ function TransactionSubmittedContent({
|
||||
</Text>
|
||||
</ExternalLink>
|
||||
)}
|
||||
{currencyToAdd && library?.provider?.isMetaMask && (
|
||||
{currencyToAdd && connector.watchAsset && (
|
||||
<ButtonLight mt="12px" padding="6px 12px" width="fit-content" onClick={addToken}>
|
||||
{!success ? (
|
||||
<RowFixed>
|
||||
<Trans>
|
||||
Add {currencyToAdd.symbol} to Metamask <StyledLogo src={MetaMaskLogo} />
|
||||
</Trans>
|
||||
<Trans>Add {currencyToAdd.symbol}</Trans>
|
||||
</RowFixed>
|
||||
) : (
|
||||
<RowFixed>
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { darken } from 'polished'
|
||||
import { Connector } from '@web3-react/types'
|
||||
import { ButtonEmpty, ButtonPrimary } from 'components/Button'
|
||||
import styled from 'styled-components/macro'
|
||||
import { AbstractConnector } from 'web3-react-abstract-connector'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
import { injected } from '../../connectors'
|
||||
import { SUPPORTED_WALLETS } from '../../constants/wallet'
|
||||
import Loader from '../Loader'
|
||||
import Option from './Option'
|
||||
|
||||
const PendingSection = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
@@ -18,18 +16,18 @@ const PendingSection = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const StyledLoader = styled(Loader)`
|
||||
margin-right: 1rem;
|
||||
`
|
||||
|
||||
const LoadingMessage = styled.div<{ error?: boolean }>`
|
||||
const LoaderContainer = styled.div`
|
||||
margin: 16px 0;
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
const LoadingMessage = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
color: ${({ theme, error }) => (error ? theme.red1 : 'inherit')};
|
||||
border: 1px solid ${({ theme, error }) => (error ? theme.red1 : theme.text4)};
|
||||
|
||||
& > * {
|
||||
padding: 1rem;
|
||||
@@ -37,29 +35,13 @@ const LoadingMessage = styled.div<{ error?: boolean }>`
|
||||
`
|
||||
|
||||
const ErrorGroup = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
`
|
||||
|
||||
const ErrorButton = styled.div`
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
color: ${({ theme }) => theme.text1};
|
||||
background-color: ${({ theme }) => theme.bg4};
|
||||
margin-left: 1rem;
|
||||
padding: 0.5rem;
|
||||
font-weight: 600;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: ${({ theme }) => darken(0.1, theme.text4)};
|
||||
}
|
||||
`
|
||||
|
||||
const LoadingWrapper = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`
|
||||
@@ -67,67 +49,55 @@ const LoadingWrapper = styled.div`
|
||||
export default function PendingView({
|
||||
connector,
|
||||
error = false,
|
||||
setPendingError,
|
||||
tryActivation,
|
||||
openOptions,
|
||||
}: {
|
||||
connector?: AbstractConnector
|
||||
connector: Connector
|
||||
error?: boolean
|
||||
setPendingError: (error: boolean) => void
|
||||
tryActivation: (connector: AbstractConnector) => void
|
||||
tryActivation: (connector: Connector) => void
|
||||
openOptions: () => void
|
||||
}) {
|
||||
const isMetamask = window?.ethereum?.isMetaMask
|
||||
|
||||
return (
|
||||
<PendingSection>
|
||||
<LoadingMessage error={error}>
|
||||
<LoadingMessage>
|
||||
<LoadingWrapper>
|
||||
{error ? (
|
||||
<ErrorGroup>
|
||||
<div>
|
||||
<ThemedText.MediumHeader marginBottom={12}>
|
||||
<Trans>Error connecting</Trans>
|
||||
</div>
|
||||
<ErrorButton
|
||||
</ThemedText.MediumHeader>
|
||||
<ThemedText.Body fontSize={14} marginBottom={36} textAlign="center">
|
||||
<Trans>
|
||||
The connection attempt failed. Please click try again and follow the steps to connect in your wallet.
|
||||
</Trans>
|
||||
</ThemedText.Body>
|
||||
<ButtonPrimary
|
||||
$borderRadius="12px"
|
||||
padding="12px"
|
||||
onClick={() => {
|
||||
setPendingError(false)
|
||||
connector && tryActivation(connector)
|
||||
tryActivation(connector)
|
||||
}}
|
||||
>
|
||||
<Trans>Try Again</Trans>
|
||||
</ErrorButton>
|
||||
</ButtonPrimary>
|
||||
<ButtonEmpty width="fit-content" padding="0" marginTop={20}>
|
||||
<ThemedText.Link fontSize={12} onClick={openOptions}>
|
||||
<Trans>Back to wallet selection</Trans>
|
||||
</ThemedText.Link>
|
||||
</ButtonEmpty>
|
||||
</ErrorGroup>
|
||||
) : (
|
||||
<>
|
||||
<StyledLoader />
|
||||
<Trans>Initializing...</Trans>
|
||||
<ThemedText.Black fontSize={20} marginY={16}>
|
||||
<LoaderContainer>
|
||||
<Loader stroke="currentColor" size="32px" />
|
||||
</LoaderContainer>
|
||||
<Trans>Connecting...</Trans>
|
||||
</ThemedText.Black>
|
||||
</>
|
||||
)}
|
||||
</LoadingWrapper>
|
||||
</LoadingMessage>
|
||||
{Object.keys(SUPPORTED_WALLETS).map((key) => {
|
||||
const option = SUPPORTED_WALLETS[key]
|
||||
if (option.connector === connector) {
|
||||
if (option.connector === injected) {
|
||||
if (isMetamask && option.name !== 'MetaMask') {
|
||||
return null
|
||||
}
|
||||
if (!isMetamask && option.name === 'MetaMask') {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Option
|
||||
id={`connect-${key}`}
|
||||
key={key}
|
||||
clickable={false}
|
||||
color={option.color}
|
||||
header={option.name}
|
||||
subheader={option.description}
|
||||
icon={option.iconURL}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})}
|
||||
</PendingSection>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { Connector } from '@web3-react/types'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { PrivacyPolicy } from 'components/PrivacyPolicy'
|
||||
import Row, { AutoRow, RowBetween } from 'components/Row'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ArrowLeft, ArrowRight, Info } from 'react-feather'
|
||||
import ReactGA from 'react-ga4'
|
||||
import { AutoRow } from 'components/Row'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import { useAppDispatch, useAppSelector } from 'state/hooks'
|
||||
import { updateSelectedWallet } from 'state/user/reducer'
|
||||
import { updateWalletError } from 'state/wallet/reducer'
|
||||
import styled from 'styled-components/macro'
|
||||
import { AbstractConnector } from 'web3-react-abstract-connector'
|
||||
import { UnsupportedChainIdError, useWeb3React } from 'web3-react-core'
|
||||
import { WalletConnectConnector } from 'web3-react-walletconnect-connector'
|
||||
|
||||
import MetamaskIcon from '../../assets/images/metamask.png'
|
||||
import TallyIcon from '../../assets/images/tally.png'
|
||||
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
||||
import { fortmatic, injected } from '../../connectors'
|
||||
import { OVERLAY_READY } from '../../connectors/Fortmatic'
|
||||
import { fortmatic, getWalletForConnector, injected } from '../../connectors'
|
||||
import { SUPPORTED_WALLETS } from '../../constants/wallet'
|
||||
import usePrevious from '../../hooks/usePrevious'
|
||||
import { useModalOpen, useWalletModalToggle } from '../../state/application/hooks'
|
||||
import { ApplicationModal } from '../../state/application/reducer'
|
||||
import { ExternalLink, ThemedText } from '../../theme'
|
||||
import { isMobile } from '../../utils/userAgent'
|
||||
import AccountDetails from '../AccountDetails'
|
||||
import Card, { LightCard } from '../Card'
|
||||
import { LightCard } from '../Card'
|
||||
import Modal from '../Modal'
|
||||
import Option from './Option'
|
||||
import PendingView from './PendingView'
|
||||
@@ -64,24 +64,20 @@ const ContentWrapper = styled.div`
|
||||
padding: 0 1rem 1rem 1rem;
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`padding: 0 1rem 1rem 1rem`};
|
||||
`
|
||||
|
||||
const UpperSection = styled.div`
|
||||
position: relative;
|
||||
|
||||
h5 {
|
||||
margin: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
h5:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
font-weight: 500;
|
||||
@@ -108,22 +104,10 @@ const HoverText = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const LinkCard = styled(Card)`
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
color: ${({ theme }) => theme.text3};
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
`
|
||||
|
||||
const WALLET_VIEWS = {
|
||||
OPTIONS: 'options',
|
||||
OPTIONS_SECONDARY: 'options_secondary',
|
||||
ACCOUNT: 'account',
|
||||
PENDING: 'pending',
|
||||
LEGAL: 'legal',
|
||||
}
|
||||
|
||||
export default function WalletModal({
|
||||
@@ -135,105 +119,99 @@ export default function WalletModal({
|
||||
confirmedTransactions: string[] // hashes of confirmed
|
||||
ENSName?: string
|
||||
}) {
|
||||
// important that these are destructed from the account-specific web3-react context
|
||||
const { active, account, connector, activate, error } = useWeb3React()
|
||||
const dispatch = useAppDispatch()
|
||||
const { connector, account } = useWeb3React()
|
||||
|
||||
const [walletView, setWalletView] = useState(WALLET_VIEWS.ACCOUNT)
|
||||
const previousWalletView = usePrevious(walletView)
|
||||
|
||||
const [pendingWallet, setPendingWallet] = useState<AbstractConnector | undefined>()
|
||||
|
||||
const [pendingError, setPendingError] = useState<boolean>()
|
||||
const [pendingConnector, setPendingConnector] = useState<Connector | undefined>()
|
||||
const pendingError = useAppSelector((state) =>
|
||||
pendingConnector ? state.wallet.errorByWallet[getWalletForConnector(pendingConnector)] : undefined
|
||||
)
|
||||
|
||||
const walletModalOpen = useModalOpen(ApplicationModal.WALLET)
|
||||
const toggleWalletModal = useWalletModalToggle()
|
||||
|
||||
const previousAccount = usePrevious(account)
|
||||
const openOptions = useCallback(() => {
|
||||
setWalletView(WALLET_VIEWS.OPTIONS)
|
||||
}, [setWalletView])
|
||||
|
||||
// close on connection, when logged out before
|
||||
useEffect(() => {
|
||||
if (account && !previousAccount && walletModalOpen) {
|
||||
toggleWalletModal()
|
||||
}
|
||||
}, [account, previousAccount, toggleWalletModal, walletModalOpen])
|
||||
|
||||
// always reset to account view
|
||||
useEffect(() => {
|
||||
if (walletModalOpen) {
|
||||
setPendingError(false)
|
||||
setWalletView(WALLET_VIEWS.ACCOUNT)
|
||||
setWalletView(account ? WALLET_VIEWS.ACCOUNT : WALLET_VIEWS.OPTIONS)
|
||||
}
|
||||
}, [walletModalOpen])
|
||||
}, [walletModalOpen, setWalletView, account])
|
||||
|
||||
// close modal when a connection is successful
|
||||
const activePrevious = usePrevious(active)
|
||||
const connectorPrevious = usePrevious(connector)
|
||||
useEffect(() => {
|
||||
if (walletModalOpen && ((active && !activePrevious) || (connector && connector !== connectorPrevious && !error))) {
|
||||
setWalletView(WALLET_VIEWS.ACCOUNT)
|
||||
if (pendingConnector && walletView !== WALLET_VIEWS.PENDING) {
|
||||
updateWalletError({ wallet: getWalletForConnector(pendingConnector), error: undefined })
|
||||
setPendingConnector(undefined)
|
||||
}
|
||||
}, [setWalletView, active, error, connector, walletModalOpen, activePrevious, connectorPrevious])
|
||||
}, [pendingConnector, walletView])
|
||||
|
||||
const tryActivation = async (connector: AbstractConnector | undefined) => {
|
||||
let name = ''
|
||||
Object.keys(SUPPORTED_WALLETS).map((key) => {
|
||||
if (connector === SUPPORTED_WALLETS[key].connector) {
|
||||
return (name = SUPPORTED_WALLETS[key].name)
|
||||
}
|
||||
return true
|
||||
})
|
||||
// log selected wallet
|
||||
ReactGA.event({
|
||||
category: 'Wallet',
|
||||
action: 'Change Wallet',
|
||||
label: name,
|
||||
})
|
||||
setPendingWallet(connector) // set wallet for pending view
|
||||
setWalletView(WALLET_VIEWS.PENDING)
|
||||
const tryActivation = useCallback(
|
||||
async (connector: Connector) => {
|
||||
const wallet = getWalletForConnector(connector)
|
||||
|
||||
// if the connector is walletconnect and the user has already tried to connect, manually reset the connector
|
||||
if (connector instanceof WalletConnectConnector) {
|
||||
connector.walletConnectProvider = undefined
|
||||
}
|
||||
|
||||
connector &&
|
||||
activate(connector, undefined, true).catch((error) => {
|
||||
if (error instanceof UnsupportedChainIdError) {
|
||||
activate(connector) // a little janky...can't use setError because the connector isn't set
|
||||
} else {
|
||||
setPendingError(true)
|
||||
}
|
||||
// log selected wallet
|
||||
sendEvent({
|
||||
category: 'Wallet',
|
||||
action: 'Change Wallet',
|
||||
label: wallet,
|
||||
})
|
||||
}
|
||||
|
||||
// close wallet modal if fortmatic modal is active
|
||||
useEffect(() => {
|
||||
fortmatic.on(OVERLAY_READY, () => {
|
||||
toggleWalletModal()
|
||||
})
|
||||
}, [toggleWalletModal])
|
||||
try {
|
||||
// Fortmatic opens it's own modal on activation to log in. This modal has a tabIndex
|
||||
// collision into the WalletModal, so we special case by closing the modal.
|
||||
if (connector === fortmatic) {
|
||||
toggleWalletModal()
|
||||
}
|
||||
|
||||
setPendingConnector(connector)
|
||||
setWalletView(WALLET_VIEWS.PENDING)
|
||||
dispatch(updateWalletError({ wallet, error: undefined }))
|
||||
|
||||
await connector.activate()
|
||||
|
||||
dispatch(updateSelectedWallet({ wallet }))
|
||||
} catch (error) {
|
||||
console.debug(`web3-react connection error: ${error}`)
|
||||
dispatch(updateWalletError({ wallet, error: error.message }))
|
||||
}
|
||||
},
|
||||
[dispatch, toggleWalletModal]
|
||||
)
|
||||
|
||||
// get wallets user can switch too, depending on device/browser
|
||||
function getOptions() {
|
||||
const isMetamask = window.ethereum && window.ethereum.isMetaMask
|
||||
const isMetamask = !!window.ethereum?.isMetaMask
|
||||
const isTally = !!window.ethereum?.isTally
|
||||
return Object.keys(SUPPORTED_WALLETS).map((key) => {
|
||||
const option = SUPPORTED_WALLETS[key]
|
||||
const isActive = option.connector === connector
|
||||
|
||||
const optionProps = {
|
||||
active: isActive,
|
||||
id: `connect-${key}`,
|
||||
link: option.href,
|
||||
header: option.name,
|
||||
color: option.color,
|
||||
key,
|
||||
icon: option.iconURL,
|
||||
}
|
||||
|
||||
// check for mobile options
|
||||
if (isMobile) {
|
||||
if (!window.web3 && !window.ethereum && option.mobile) {
|
||||
return (
|
||||
<Option
|
||||
{...optionProps}
|
||||
onClick={() => {
|
||||
option.connector !== connector && !option.href && tryActivation(option.connector)
|
||||
if (!isActive && !option.href && !!option.connector) {
|
||||
tryActivation(option.connector)
|
||||
}
|
||||
}}
|
||||
id={`connect-${key}`}
|
||||
key={key}
|
||||
active={option.connector && option.connector === connector}
|
||||
color={option.color}
|
||||
link={option.href}
|
||||
header={option.name}
|
||||
subheader={null}
|
||||
icon={option.iconURL}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -267,6 +245,24 @@ export default function WalletModal({
|
||||
// likewise for generic
|
||||
else if (option.name === 'Injected' && isMetamask) {
|
||||
return null
|
||||
} else if (option.name === 'Injected' && isTally) {
|
||||
return (
|
||||
<Option
|
||||
id={`connect-${key}`}
|
||||
key={key}
|
||||
onClick={() => {
|
||||
option.connector === connector
|
||||
? setWalletView(WALLET_VIEWS.ACCOUNT)
|
||||
: !option.href && option.connector && tryActivation(option.connector)
|
||||
}}
|
||||
color={'#E8831D'}
|
||||
header={<Trans>Tally</Trans>}
|
||||
active={option.connector === connector}
|
||||
subheader={null}
|
||||
link={null}
|
||||
icon={TallyIcon}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,19 +271,13 @@ export default function WalletModal({
|
||||
!isMobile &&
|
||||
!option.mobileOnly && (
|
||||
<Option
|
||||
id={`connect-${key}`}
|
||||
{...optionProps}
|
||||
onClick={() => {
|
||||
option.connector === connector
|
||||
? setWalletView(WALLET_VIEWS.ACCOUNT)
|
||||
: !option.href && tryActivation(option.connector)
|
||||
: !option.href && option.connector && tryActivation(option.connector)
|
||||
}}
|
||||
key={key}
|
||||
active={option.connector === connector}
|
||||
color={option.color}
|
||||
link={option.href}
|
||||
header={option.name}
|
||||
subheader={null} //use option.descriptio to bring back multi-line
|
||||
icon={option.iconURL}
|
||||
/>
|
||||
)
|
||||
)
|
||||
@@ -295,121 +285,78 @@ export default function WalletModal({
|
||||
}
|
||||
|
||||
function getModalContent() {
|
||||
if (error) {
|
||||
return (
|
||||
<UpperSection>
|
||||
<CloseIcon onClick={toggleWalletModal}>
|
||||
<CloseColor />
|
||||
</CloseIcon>
|
||||
<HeaderRow>
|
||||
{error instanceof UnsupportedChainIdError ? <Trans>Wrong Network</Trans> : <Trans>Error connecting</Trans>}
|
||||
</HeaderRow>
|
||||
<ContentWrapper>
|
||||
{error instanceof UnsupportedChainIdError ? (
|
||||
<h5>
|
||||
<Trans>Please connect to a supported network in the dropdown menu or in your wallet.</Trans>
|
||||
</h5>
|
||||
) : (
|
||||
<Trans>Error connecting. Try refreshing the page.</Trans>
|
||||
)}
|
||||
</ContentWrapper>
|
||||
</UpperSection>
|
||||
)
|
||||
}
|
||||
if (walletView === WALLET_VIEWS.LEGAL) {
|
||||
return (
|
||||
<UpperSection>
|
||||
<HeaderRow>
|
||||
<HoverText
|
||||
onClick={() => {
|
||||
setWalletView(
|
||||
(previousWalletView === WALLET_VIEWS.LEGAL ? WALLET_VIEWS.ACCOUNT : previousWalletView) ??
|
||||
WALLET_VIEWS.ACCOUNT
|
||||
)
|
||||
}}
|
||||
>
|
||||
<ArrowLeft />
|
||||
</HoverText>
|
||||
<Row justify="center">
|
||||
<ThemedText.MediumHeader>
|
||||
<Trans>Legal & Privacy</Trans>
|
||||
</ThemedText.MediumHeader>
|
||||
</Row>
|
||||
</HeaderRow>
|
||||
<PrivacyPolicy />
|
||||
</UpperSection>
|
||||
)
|
||||
}
|
||||
if (account && walletView === WALLET_VIEWS.ACCOUNT) {
|
||||
if (walletView === WALLET_VIEWS.ACCOUNT) {
|
||||
return (
|
||||
<AccountDetails
|
||||
toggleWalletModal={toggleWalletModal}
|
||||
pendingTransactions={pendingTransactions}
|
||||
confirmedTransactions={confirmedTransactions}
|
||||
ENSName={ENSName}
|
||||
openOptions={() => setWalletView(WALLET_VIEWS.OPTIONS)}
|
||||
openOptions={openOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
let headerRow
|
||||
if (walletView === WALLET_VIEWS.PENDING) {
|
||||
headerRow = null
|
||||
} else if (walletView === WALLET_VIEWS.ACCOUNT || !!account) {
|
||||
headerRow = (
|
||||
<HeaderRow color="blue">
|
||||
<HoverText onClick={() => setWalletView(account ? WALLET_VIEWS.ACCOUNT : WALLET_VIEWS.OPTIONS)}>
|
||||
<ArrowLeft />
|
||||
</HoverText>
|
||||
</HeaderRow>
|
||||
)
|
||||
} else {
|
||||
headerRow = (
|
||||
<HeaderRow>
|
||||
<HoverText>
|
||||
<Trans>Connect a wallet</Trans>
|
||||
</HoverText>
|
||||
</HeaderRow>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<UpperSection>
|
||||
<CloseIcon onClick={toggleWalletModal}>
|
||||
<CloseColor />
|
||||
</CloseIcon>
|
||||
{walletView !== WALLET_VIEWS.ACCOUNT ? (
|
||||
<HeaderRow color="blue">
|
||||
<HoverText
|
||||
onClick={() => {
|
||||
setPendingError(false)
|
||||
setWalletView(WALLET_VIEWS.ACCOUNT)
|
||||
}}
|
||||
>
|
||||
<ArrowLeft />
|
||||
</HoverText>
|
||||
</HeaderRow>
|
||||
) : (
|
||||
<HeaderRow>
|
||||
<HoverText>
|
||||
<Trans>Connect a wallet</Trans>
|
||||
</HoverText>
|
||||
</HeaderRow>
|
||||
)}
|
||||
|
||||
{headerRow}
|
||||
<ContentWrapper>
|
||||
<AutoColumn gap="16px">
|
||||
<LightCard>
|
||||
<AutoRow style={{ flexWrap: 'nowrap' }}>
|
||||
<ThemedText.Black fontSize={14}>
|
||||
<Trans>
|
||||
By connecting a wallet, you agree to Uniswap Labs’{' '}
|
||||
<ExternalLink href="https://uniswap.org/terms-of-service/">Terms of Service</ExternalLink> and
|
||||
acknowledge that you have read and understand the Uniswap{' '}
|
||||
<ExternalLink href="https://uniswap.org/disclaimer/">Protocol Disclaimer</ExternalLink>.
|
||||
</Trans>
|
||||
</ThemedText.Black>
|
||||
</AutoRow>
|
||||
</LightCard>
|
||||
{walletView === WALLET_VIEWS.PENDING ? (
|
||||
{walletView === WALLET_VIEWS.PENDING && pendingConnector && (
|
||||
<PendingView
|
||||
connector={pendingWallet}
|
||||
error={pendingError}
|
||||
setPendingError={setPendingError}
|
||||
openOptions={openOptions}
|
||||
connector={pendingConnector}
|
||||
error={!!pendingError}
|
||||
tryActivation={tryActivation}
|
||||
/>
|
||||
) : (
|
||||
<OptionGrid>{getOptions()}</OptionGrid>
|
||||
)}
|
||||
<LinkCard padding=".5rem" $borderRadius=".75rem" onClick={() => setWalletView(WALLET_VIEWS.LEGAL)}>
|
||||
<RowBetween>
|
||||
<AutoRow gap="4px">
|
||||
<Info size={20} />
|
||||
<ThemedText.Label fontSize={14}>
|
||||
<Trans>How this app uses APIs</Trans>
|
||||
</ThemedText.Label>
|
||||
{walletView !== WALLET_VIEWS.PENDING && <OptionGrid data-cy="option-grid">{getOptions()}</OptionGrid>}
|
||||
{!pendingError && (
|
||||
<LightCard>
|
||||
<AutoRow style={{ flexWrap: 'nowrap' }}>
|
||||
<ThemedText.Body fontSize={12}>
|
||||
<Trans>
|
||||
By connecting a wallet, you agree to Uniswap Labs’{' '}
|
||||
<ExternalLink
|
||||
style={{ textDecoration: 'underline' }}
|
||||
href="https://uniswap.org/terms-of-service/"
|
||||
>
|
||||
Terms of Service
|
||||
</ExternalLink>{' '}
|
||||
and acknowledge that you have read and understand the Uniswap{' '}
|
||||
<ExternalLink style={{ textDecoration: 'underline' }} href="https://uniswap.org/disclaimer/">
|
||||
Protocol Disclaimer
|
||||
</ExternalLink>
|
||||
.
|
||||
</Trans>
|
||||
</ThemedText.Body>
|
||||
</AutoRow>
|
||||
<ArrowRight size={16} />
|
||||
</RowBetween>
|
||||
</LinkCard>
|
||||
</LightCard>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</ContentWrapper>
|
||||
</UpperSection>
|
||||
|
||||
44
src/components/Web3Provider/index.tsx
Normal file
44
src/components/Web3Provider/index.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Web3ReactProvider } from '@web3-react/core'
|
||||
import { Connector } from '@web3-react/types'
|
||||
import { BACKFILLABLE_WALLETS, getConnectorForWallet, gnosisSafe, injected, network, useConnectors } from 'connectors'
|
||||
import { ReactNode, useEffect } from 'react'
|
||||
import { useAppSelector } from 'state/hooks'
|
||||
|
||||
import { isMobile } from '../../utils/userAgent'
|
||||
|
||||
const connect = async (connector: Connector) => {
|
||||
try {
|
||||
if (connector.connectEagerly) {
|
||||
await connector.connectEagerly()
|
||||
} else {
|
||||
await connector.activate()
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug(`web3-react eager connection error: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
export default function Web3Provider({ children }: { children: ReactNode }) {
|
||||
const selectedWalletBackfilled = useAppSelector((state) => state.user.selectedWalletBackfilled)
|
||||
const selectedWallet = useAppSelector((state) => state.user.selectedWallet)
|
||||
|
||||
const connectors = useConnectors(selectedWallet)
|
||||
|
||||
const isMetaMask = !!window.ethereum?.isMetaMask
|
||||
|
||||
useEffect(() => {
|
||||
connect(gnosisSafe)
|
||||
connect(network)
|
||||
|
||||
if (isMobile && isMetaMask) {
|
||||
injected.activate()
|
||||
} else if (selectedWallet) {
|
||||
connect(getConnectorForWallet(selectedWallet))
|
||||
} else if (!selectedWalletBackfilled) {
|
||||
BACKFILLABLE_WALLETS.map(getConnectorForWallet).forEach(connect)
|
||||
}
|
||||
// The dependency list is empty so this is only run once on mount
|
||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return <Web3ReactProvider connectors={connectors}>{children}</Web3ReactProvider>
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useEffect } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
import { useWeb3React } from 'web3-react-core'
|
||||
|
||||
import { network } from '../../connectors'
|
||||
import { NetworkContextName } from '../../constants/misc'
|
||||
import { useEagerConnect, useInactiveListener } from '../../hooks/web3'
|
||||
|
||||
const MessageWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 20rem;
|
||||
`
|
||||
|
||||
const Message = styled.h2`
|
||||
color: ${({ theme }) => theme.secondary1};
|
||||
`
|
||||
|
||||
export default function Web3ReactManager({ children }: { children: JSX.Element }) {
|
||||
const { active } = useWeb3React()
|
||||
const { active: networkActive, error: networkError, activate: activateNetwork } = useWeb3React(NetworkContextName)
|
||||
|
||||
// try to eagerly connect to an injected provider, if it exists and has granted access already
|
||||
const triedEager = useEagerConnect()
|
||||
|
||||
// after eagerly trying injected, if the network connect ever isn't active or in an error state, activate itd
|
||||
useEffect(() => {
|
||||
if (triedEager && !networkActive && !networkError && !active) {
|
||||
activateNetwork(network)
|
||||
}
|
||||
}, [triedEager, networkActive, networkError, activateNetwork, active])
|
||||
|
||||
// when there's no account connected, react to logins (broadly speaking) on the injected provider, if it exists
|
||||
useInactiveListener(!triedEager)
|
||||
|
||||
// if the account context isn't active, and there's an error on the network context, it's an irrecoverable error
|
||||
if (triedEager && !active && networkError) {
|
||||
return (
|
||||
<MessageWrapper>
|
||||
<Message>
|
||||
<Trans>
|
||||
Oops! An unknown error occurred. Please refresh the page, or visit from another browser or device.
|
||||
</Trans>
|
||||
</Message>
|
||||
</MessageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { Connector } from '@web3-react/types'
|
||||
import { getWalletForConnector } from 'connectors'
|
||||
import { darken } from 'polished'
|
||||
import { useMemo } from 'react'
|
||||
import { Activity } from 'react-feather'
|
||||
import { useAppSelector } from 'state/hooks'
|
||||
import styled, { css } from 'styled-components/macro'
|
||||
import { AbstractConnector } from 'web3-react-abstract-connector'
|
||||
import { UnsupportedChainIdError, useWeb3React } from 'web3-react-core'
|
||||
import { isChainAllowed } from 'utils/switchChain'
|
||||
|
||||
import { NetworkContextName } from '../../constants/misc'
|
||||
import useENSName from '../../hooks/useENSName'
|
||||
import { useHasSocks } from '../../hooks/useSocksBalance'
|
||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||
import { isTransactionRecent, useAllTransactions } from '../../state/transactions/hooks'
|
||||
@@ -131,7 +131,7 @@ function Sock() {
|
||||
)
|
||||
}
|
||||
|
||||
function WrappedStatusIcon({ connector }: { connector: AbstractConnector | Connector }) {
|
||||
function WrappedStatusIcon({ connector }: { connector: Connector }) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<StatusIcon connector={connector} />
|
||||
@@ -140,9 +140,11 @@ function WrappedStatusIcon({ connector }: { connector: AbstractConnector | Conne
|
||||
}
|
||||
|
||||
function Web3StatusInner() {
|
||||
const { account, connector, error } = useWeb3React()
|
||||
const { account, connector, chainId, ENSName } = useWeb3React()
|
||||
|
||||
const { ENSName } = useENSName(account ?? undefined)
|
||||
const error = useAppSelector((state) => state.wallet.errorByWallet[getWalletForConnector(connector)])
|
||||
|
||||
const chainAllowed = chainId && isChainAllowed(connector, chainId)
|
||||
|
||||
const allTransactions = useAllTransactions()
|
||||
|
||||
@@ -157,7 +159,27 @@ function Web3StatusInner() {
|
||||
const hasSocks = useHasSocks()
|
||||
const toggleWalletModal = useWalletModalToggle()
|
||||
|
||||
if (account) {
|
||||
if (!chainId) {
|
||||
return null
|
||||
} else if (!chainAllowed) {
|
||||
return (
|
||||
<Web3StatusError onClick={toggleWalletModal}>
|
||||
<NetworkIcon />
|
||||
<Text>
|
||||
<Trans>Wrong Network</Trans>
|
||||
</Text>
|
||||
</Web3StatusError>
|
||||
)
|
||||
} else if (error) {
|
||||
return (
|
||||
<Web3StatusError onClick={toggleWalletModal}>
|
||||
<NetworkIcon />
|
||||
<Text>
|
||||
<Trans>Error</Trans>
|
||||
</Text>
|
||||
</Web3StatusError>
|
||||
)
|
||||
} else if (account) {
|
||||
return (
|
||||
<Web3StatusConnected id="web3-status-connected" onClick={toggleWalletModal} pending={hasPendingTransactions}>
|
||||
{hasPendingTransactions ? (
|
||||
@@ -176,13 +198,6 @@ function Web3StatusInner() {
|
||||
{!hasPendingTransactions && connector && <WrappedStatusIcon connector={connector} />}
|
||||
</Web3StatusConnected>
|
||||
)
|
||||
} else if (error) {
|
||||
return (
|
||||
<Web3StatusError onClick={toggleWalletModal}>
|
||||
<NetworkIcon />
|
||||
<Text>{error instanceof UnsupportedChainIdError ? <Trans>Wrong Network</Trans> : <Trans>Error</Trans>}</Text>
|
||||
</Web3StatusError>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Web3StatusConnect id="connect-wallet" onClick={toggleWalletModal} faded={!account}>
|
||||
@@ -195,10 +210,7 @@ function Web3StatusInner() {
|
||||
}
|
||||
|
||||
export default function Web3Status() {
|
||||
const { active, account } = useWeb3React()
|
||||
const contextNetwork = useWeb3React(NetworkContextName)
|
||||
|
||||
const { ENSName } = useENSName(account ?? undefined)
|
||||
const { ENSName } = useWeb3React()
|
||||
|
||||
const allTransactions = useAllTransactions()
|
||||
|
||||
@@ -213,9 +225,7 @@ export default function Web3Status() {
|
||||
return (
|
||||
<>
|
||||
<Web3StatusInner />
|
||||
{(contextNetwork.active || active) && (
|
||||
<WalletModal ENSName={ENSName ?? undefined} pendingTransactions={pending} confirmedTransactions={confirmed} />
|
||||
)}
|
||||
<WalletModal ENSName={ENSName ?? undefined} pendingTransactions={pending} confirmedTransactions={confirmed} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
51
src/components/analytics/GoogleAnalyticsProvider.tsx
Normal file
51
src/components/analytics/GoogleAnalyticsProvider.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import ReactGA from 'react-ga4'
|
||||
import { GaOptions, InitOptions, UaEventOptions } from 'react-ga4/types/ga4'
|
||||
|
||||
/**
|
||||
* Google Analytics Provider containing all methods used throughout app to log events to Google Analytics.
|
||||
*/
|
||||
export default class GoogleAnalyticsProvider {
|
||||
public sendEvent(event: string | UaEventOptions, params?: any) {
|
||||
ReactGA.event(event, params)
|
||||
}
|
||||
|
||||
public initialize(
|
||||
GA_MEASUREMENT_ID: InitOptions[] | string,
|
||||
options?: {
|
||||
legacyDimensionMetric?: boolean
|
||||
nonce?: string
|
||||
testMode?: boolean
|
||||
gaOptions?: GaOptions | any
|
||||
gtagOptions?: any
|
||||
}
|
||||
) {
|
||||
ReactGA.initialize(GA_MEASUREMENT_ID, options)
|
||||
}
|
||||
|
||||
public set(fieldsObject: any) {
|
||||
ReactGA.set(fieldsObject)
|
||||
}
|
||||
|
||||
public outboundLink(
|
||||
{
|
||||
label,
|
||||
}: {
|
||||
label: string
|
||||
},
|
||||
hitCallback: () => unknown
|
||||
) {
|
||||
ReactGA.outboundLink({ label }, hitCallback)
|
||||
}
|
||||
|
||||
public pageview(path?: string, _?: string[], title?: string) {
|
||||
ReactGA.pageview(path, _, title)
|
||||
}
|
||||
|
||||
public ga(...args: any[]) {
|
||||
ReactGA.ga(...args)
|
||||
}
|
||||
|
||||
public gaCommandSendTiming(timingCategory: any, timingVar: any, timingValue: any, timingLabel: any) {
|
||||
ReactGA._gaCommandSendTiming(timingCategory, timingVar, timingValue, timingLabel)
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { useEffect } from 'react'
|
||||
import ReactGA from 'react-ga4'
|
||||
import { RouteComponentProps } from 'react-router-dom'
|
||||
import { getCLS, getFCP, getFID, getLCP, Metric } from 'web-vitals'
|
||||
|
||||
import { GOOGLE_ANALYTICS_CLIENT_ID_STORAGE_KEY } from './index'
|
||||
|
||||
function reportWebVitals({ name, delta, id }: Metric) {
|
||||
ReactGA._gaCommandSendTiming('Web Vitals', name, Math.round(name === 'CLS' ? delta * 1000 : delta), id)
|
||||
}
|
||||
|
||||
// tracks web vitals and pageviews
|
||||
export default function GoogleAnalyticsReporter({ location: { pathname, search } }: RouteComponentProps): null {
|
||||
useEffect(() => {
|
||||
getFCP(reportWebVitals)
|
||||
getFID(reportWebVitals)
|
||||
getLCP(reportWebVitals)
|
||||
getCLS(reportWebVitals)
|
||||
}, [])
|
||||
|
||||
const { chainId } = useActiveWeb3React()
|
||||
useEffect(() => {
|
||||
// cd1 - custom dimension 1 - chainId
|
||||
ReactGA.set({ cd1: chainId ?? 0 })
|
||||
}, [chainId])
|
||||
|
||||
useEffect(() => {
|
||||
ReactGA.pageview(`${pathname}${search}`)
|
||||
}, [pathname, search])
|
||||
|
||||
useEffect(() => {
|
||||
// typed as 'any' in react-ga4 -.-
|
||||
ReactGA.ga((tracker: any) => {
|
||||
if (!tracker) return
|
||||
|
||||
const clientId = tracker.get('clientId')
|
||||
window.localStorage.setItem(GOOGLE_ANALYTICS_CLIENT_ID_STORAGE_KEY, clientId)
|
||||
})
|
||||
}, [])
|
||||
return null
|
||||
}
|
||||
@@ -1,20 +1,47 @@
|
||||
import ReactGA from 'react-ga4'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { useEffect } from 'react'
|
||||
import { UaEventOptions } from 'react-ga4/types/ga4'
|
||||
import { RouteComponentProps } from 'react-router-dom'
|
||||
import { isMobile } from 'utils/userAgent'
|
||||
import { getCLS, getFCP, getFID, getLCP, Metric } from 'web-vitals'
|
||||
|
||||
import GoogleAnalyticsProvider from './GoogleAnalyticsProvider'
|
||||
|
||||
export const GOOGLE_ANALYTICS_CLIENT_ID_STORAGE_KEY = 'ga_client_id'
|
||||
const GOOGLE_ANALYTICS_ID: string | undefined = process.env.REACT_APP_GOOGLE_ANALYTICS_ID
|
||||
|
||||
const storedClientId = window.localStorage.getItem(GOOGLE_ANALYTICS_CLIENT_ID_STORAGE_KEY)
|
||||
|
||||
const googleAnalytics = new GoogleAnalyticsProvider()
|
||||
|
||||
export function sendEvent(event: string | UaEventOptions, params?: any) {
|
||||
return googleAnalytics.sendEvent(event, params)
|
||||
}
|
||||
|
||||
export function outboundLink(
|
||||
{
|
||||
label,
|
||||
}: {
|
||||
label: string
|
||||
},
|
||||
hitCallback: () => unknown
|
||||
) {
|
||||
return googleAnalytics.outboundLink({ label }, hitCallback)
|
||||
}
|
||||
|
||||
export function sendTiming(timingCategory: any, timingVar: any, timingValue: any, timingLabel: any) {
|
||||
return googleAnalytics.gaCommandSendTiming(timingCategory, timingVar, timingValue, timingLabel)
|
||||
}
|
||||
|
||||
if (typeof GOOGLE_ANALYTICS_ID === 'string') {
|
||||
ReactGA.initialize(GOOGLE_ANALYTICS_ID, {
|
||||
googleAnalytics.initialize(GOOGLE_ANALYTICS_ID, {
|
||||
gaOptions: {
|
||||
storage: 'none',
|
||||
storeGac: false,
|
||||
clientId: storedClientId ?? undefined,
|
||||
},
|
||||
})
|
||||
ReactGA.set({
|
||||
googleAnalytics.set({
|
||||
anonymizeIp: true,
|
||||
customBrowserType: !isMobile
|
||||
? 'desktop'
|
||||
@@ -23,5 +50,44 @@ if (typeof GOOGLE_ANALYTICS_ID === 'string') {
|
||||
: 'mobileRegular',
|
||||
})
|
||||
} else {
|
||||
ReactGA.initialize('test', { gtagOptions: { debug_mode: true } })
|
||||
googleAnalytics.initialize('test', { gtagOptions: { debug_mode: true } })
|
||||
}
|
||||
|
||||
const installed = Boolean(window.navigator.serviceWorker?.controller)
|
||||
const hit = Boolean((window as any).__isDocumentCached)
|
||||
const action = installed ? (hit ? 'Cache hit' : 'Cache miss') : 'Not installed'
|
||||
sendEvent({ category: 'Service Worker', action, nonInteraction: true })
|
||||
|
||||
function reportWebVitals({ name, delta, id }: Metric) {
|
||||
sendTiming('Web Vitals', name, Math.round(name === 'CLS' ? delta * 1000 : delta), id)
|
||||
}
|
||||
|
||||
// tracks web vitals and pageviews
|
||||
export function useAnalyticsReporter({ pathname, search }: RouteComponentProps['location']) {
|
||||
useEffect(() => {
|
||||
getFCP(reportWebVitals)
|
||||
getFID(reportWebVitals)
|
||||
getLCP(reportWebVitals)
|
||||
getCLS(reportWebVitals)
|
||||
}, [])
|
||||
|
||||
const { chainId } = useActiveWeb3React()
|
||||
useEffect(() => {
|
||||
// cd1 - custom dimension 1 - chainId
|
||||
googleAnalytics.set({ cd1: chainId ?? 0 })
|
||||
}, [chainId])
|
||||
|
||||
useEffect(() => {
|
||||
googleAnalytics.pageview(`${pathname}${search}`)
|
||||
}, [pathname, search])
|
||||
|
||||
useEffect(() => {
|
||||
// typed as 'any' in react-ga4 -.-
|
||||
googleAnalytics.ga((tracker: any) => {
|
||||
if (!tracker) return
|
||||
|
||||
const clientId = tracker.get('clientId')
|
||||
window.localStorage.setItem(GOOGLE_ANALYTICS_CLIENT_ID_STORAGE_KEY, clientId)
|
||||
})
|
||||
}, [])
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ interface StakingModalProps {
|
||||
}
|
||||
|
||||
export default function StakingModal({ isOpen, onDismiss, stakingInfo, userLiquidityUnstaked }: StakingModalProps) {
|
||||
const { library } = useActiveWeb3React()
|
||||
const { provider } = useActiveWeb3React()
|
||||
|
||||
// track and parse user input
|
||||
const [typedValue, setTypedValue] = useState('')
|
||||
@@ -144,7 +144,7 @@ export default function StakingModal({ isOpen, onDismiss, stakingInfo, userLiqui
|
||||
}, [maxAmountInput, onUserInput])
|
||||
|
||||
async function onAttemptToApprove() {
|
||||
if (!pairContract || !library || !deadline) throw new Error('missing dependencies')
|
||||
if (!pairContract || !provider || !deadline) throw new Error('missing dependencies')
|
||||
if (!parsedAmount) throw new Error('missing liquidity amount')
|
||||
|
||||
if (gatherPermitSignature) {
|
||||
|
||||
@@ -42,7 +42,7 @@ export default function UnstakingModal({ isOpen, onDismiss, stakingInfo }: Staki
|
||||
const [hash, setHash] = useState<string | undefined>()
|
||||
const [attempting, setAttempting] = useState(false)
|
||||
|
||||
function wrappedOndismiss() {
|
||||
function wrappedOnDismiss() {
|
||||
setHash(undefined)
|
||||
setAttempting(false)
|
||||
onDismiss()
|
||||
@@ -79,14 +79,14 @@ export default function UnstakingModal({ isOpen, onDismiss, stakingInfo }: Staki
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={wrappedOndismiss} maxHeight={90}>
|
||||
<Modal isOpen={isOpen} onDismiss={wrappedOnDismiss} maxHeight={90}>
|
||||
{!attempting && !hash && (
|
||||
<ContentWrapper gap="lg">
|
||||
<RowBetween>
|
||||
<ThemedText.MediumHeader>
|
||||
<Trans>Withdraw</Trans>
|
||||
</ThemedText.MediumHeader>
|
||||
<CloseIcon onClick={wrappedOndismiss} />
|
||||
<CloseIcon onClick={wrappedOnDismiss} />
|
||||
</RowBetween>
|
||||
{stakingInfo?.stakedAmount && (
|
||||
<AutoColumn justify="center" gap="md">
|
||||
@@ -117,7 +117,7 @@ export default function UnstakingModal({ isOpen, onDismiss, stakingInfo }: Staki
|
||||
</ContentWrapper>
|
||||
)}
|
||||
{attempting && !hash && (
|
||||
<LoadingView onDismiss={wrappedOndismiss}>
|
||||
<LoadingView onDismiss={wrappedOnDismiss}>
|
||||
<AutoColumn gap="12px" justify={'center'}>
|
||||
<ThemedText.Body fontSize={20}>
|
||||
<Trans>Withdrawing {stakingInfo?.stakedAmount?.toSignificant(4)} UNI-V2</Trans>
|
||||
@@ -129,7 +129,7 @@ export default function UnstakingModal({ isOpen, onDismiss, stakingInfo }: Staki
|
||||
</LoadingView>
|
||||
)}
|
||||
{hash && (
|
||||
<SubmittedView onDismiss={wrappedOndismiss} hash={hash}>
|
||||
<SubmittedView onDismiss={wrappedOnDismiss} hash={hash}>
|
||||
<AutoColumn gap="12px" justify={'center'}>
|
||||
<ThemedText.LargeHeader>
|
||||
<Trans>Transaction Submitted</Trans>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Currency, TradeType } from '@uniswap/sdk-core'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { LoadingOpacityContainer } from 'components/Loader/styled'
|
||||
import { RowFixed } from 'components/Row'
|
||||
import { MouseoverTooltipContent } from 'components/Tooltip'
|
||||
import ReactGA from 'react-ga4'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
@@ -85,7 +85,7 @@ export default function GasEstimateBadge({
|
||||
}
|
||||
placement="bottom"
|
||||
onOpen={() =>
|
||||
ReactGA.event({
|
||||
sendEvent({
|
||||
category: 'Gas',
|
||||
action: 'Gas Details Tooltip Open',
|
||||
})
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Currency, TradeType } from '@uniswap/sdk-core'
|
||||
import { Protocol } from '@uniswap/router-sdk'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { Pair } from '@uniswap/v2-sdk'
|
||||
import { FeeAmount } from '@uniswap/v3-sdk'
|
||||
import AnimatedDropdown from 'components/AnimatedDropdown'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { LoadingRows } from 'components/Loader/styled'
|
||||
@@ -8,7 +11,6 @@ import { AutoRow, RowBetween } from 'components/Row'
|
||||
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import useAutoRouterSupported from 'hooks/useAutoRouterSupported'
|
||||
import { getTokenPath } from 'lib/components/Swap/RoutingDiagram/utils'
|
||||
import { memo, useState } from 'react'
|
||||
import { Plus } from 'react-feather'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
@@ -106,3 +108,41 @@ export default memo(function SwapRoute({ trade, syncing, fixedOpen = false, ...r
|
||||
</Wrapper>
|
||||
)
|
||||
})
|
||||
|
||||
export interface RoutingDiagramEntry {
|
||||
percent: Percent
|
||||
path: [Currency, Currency, FeeAmount][]
|
||||
protocol: Protocol
|
||||
}
|
||||
|
||||
const V2_DEFAULT_FEE_TIER = 3000
|
||||
|
||||
/**
|
||||
* Loops through all routes on a trade and returns an array of diagram entries.
|
||||
*/
|
||||
export function getTokenPath(trade: InterfaceTrade<Currency, Currency, TradeType>): RoutingDiagramEntry[] {
|
||||
return trade.swaps.map(({ route: { path: tokenPath, pools, protocol }, inputAmount, outputAmount }) => {
|
||||
const portion =
|
||||
trade.tradeType === TradeType.EXACT_INPUT
|
||||
? inputAmount.divide(trade.inputAmount)
|
||||
: outputAmount.divide(trade.outputAmount)
|
||||
const percent = new Percent(portion.numerator, portion.denominator)
|
||||
const path: RoutingDiagramEntry['path'] = []
|
||||
for (let i = 0; i < pools.length; i++) {
|
||||
const nextPool = pools[i]
|
||||
const tokenIn = tokenPath[i]
|
||||
const tokenOut = tokenPath[i + 1]
|
||||
const entry: RoutingDiagramEntry['path'][0] = [
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
nextPool instanceof Pair ? V2_DEFAULT_FEE_TIER : nextPool.fee,
|
||||
]
|
||||
path.push(entry)
|
||||
}
|
||||
return {
|
||||
percent,
|
||||
path,
|
||||
protocol,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -33,6 +33,11 @@ export default function TradePrice({ price, showInverted, setShowInverted }: Tra
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const usdcPrice = useUSDCPrice(showInverted ? price.baseCurrency : price.quoteCurrency)
|
||||
/*
|
||||
* calculate needed amount of decimal prices, for prices between 0.95-1.05 use 4 decimal places
|
||||
*/
|
||||
const p = Number(usdcPrice?.toFixed())
|
||||
const visibleDecimalPlaces = p < 1.05 ? 4 : 2
|
||||
|
||||
let formattedPrice: string
|
||||
try {
|
||||
@@ -60,7 +65,7 @@ export default function TradePrice({ price, showInverted, setShowInverted }: Tra
|
||||
</Text>{' '}
|
||||
{usdcPrice && (
|
||||
<ThemedText.DarkGray>
|
||||
<Trans>(${usdcPrice.toSignificant(6, { groupSeparator: ',' })})</Trans>
|
||||
<Trans>(${usdcPrice.toFixed(visibleDecimalPlaces, { groupSeparator: ',' })})</Trans>
|
||||
</ThemedText.DarkGray>
|
||||
)}
|
||||
</StyledPriceContainer>
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function DelegateModal({ isOpen, onDismiss, title }: VoteModalPro
|
||||
const [attempting, setAttempting] = useState(false)
|
||||
|
||||
// wrapper to reset state on modal close
|
||||
function wrappedOndismiss() {
|
||||
function wrappedOnDismiss() {
|
||||
setHash(undefined)
|
||||
setAttempting(false)
|
||||
onDismiss()
|
||||
@@ -90,13 +90,13 @@ export default function DelegateModal({ isOpen, onDismiss, title }: VoteModalPro
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={wrappedOndismiss} maxHeight={90}>
|
||||
<Modal isOpen={isOpen} onDismiss={wrappedOnDismiss} maxHeight={90}>
|
||||
{!attempting && !hash && (
|
||||
<ContentWrapper gap="lg">
|
||||
<AutoColumn gap="lg" justify="center">
|
||||
<RowBetween>
|
||||
<ThemedText.MediumHeader fontWeight={500}>{title}</ThemedText.MediumHeader>
|
||||
<StyledClosed stroke="black" onClick={wrappedOndismiss} />
|
||||
<StyledClosed stroke="black" onClick={wrappedOnDismiss} />
|
||||
</RowBetween>
|
||||
<ThemedText.Body>
|
||||
<Trans>Earned UNI tokens represent voting shares in Uniswap governance.</Trans>
|
||||
@@ -119,7 +119,7 @@ export default function DelegateModal({ isOpen, onDismiss, title }: VoteModalPro
|
||||
</ContentWrapper>
|
||||
)}
|
||||
{attempting && !hash && (
|
||||
<LoadingView onDismiss={wrappedOndismiss}>
|
||||
<LoadingView onDismiss={wrappedOnDismiss}>
|
||||
<AutoColumn gap="12px" justify={'center'}>
|
||||
<ThemedText.LargeHeader>
|
||||
{usingDelegate ? <Trans>Delegating votes</Trans> : <Trans>Unlocking Votes</Trans>}
|
||||
@@ -129,7 +129,7 @@ export default function DelegateModal({ isOpen, onDismiss, title }: VoteModalPro
|
||||
</LoadingView>
|
||||
)}
|
||||
{hash && (
|
||||
<SubmittedView onDismiss={wrappedOndismiss} hash={hash}>
|
||||
<SubmittedView onDismiss={wrappedOnDismiss} hash={hash}>
|
||||
<AutoColumn gap="12px" justify={'center'}>
|
||||
<ThemedText.LargeHeader>
|
||||
<Trans>Transaction Submitted</Trans>
|
||||
|
||||
153
src/components/vote/ExecuteModal.tsx
Normal file
153
src/components/vote/ExecuteModal.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { useContext, useState } from 'react'
|
||||
import { ArrowUpCircle, X } from 'react-feather'
|
||||
import styled, { ThemeContext } from 'styled-components/macro'
|
||||
|
||||
import Circle from '../../assets/images/blue-loader.svg'
|
||||
import { useExecuteCallback } from '../../state/governance/hooks'
|
||||
import { CustomLightSpinner, ThemedText } from '../../theme'
|
||||
import { ExternalLink } from '../../theme'
|
||||
import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink'
|
||||
import { ButtonPrimary } from '../Button'
|
||||
import { AutoColumn, ColumnCenter } from '../Column'
|
||||
import Modal from '../Modal'
|
||||
import { RowBetween } from '../Row'
|
||||
|
||||
const ContentWrapper = styled(AutoColumn)`
|
||||
width: 100%;
|
||||
padding: 24px;
|
||||
`
|
||||
|
||||
const StyledClosed = styled(X)`
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`
|
||||
|
||||
const ConfirmOrLoadingWrapper = styled.div`
|
||||
width: 100%;
|
||||
padding: 24px;
|
||||
`
|
||||
|
||||
const ConfirmedIcon = styled(ColumnCenter)`
|
||||
padding: 60px 0;
|
||||
`
|
||||
|
||||
interface ExecuteModalProps {
|
||||
isOpen: boolean
|
||||
onDismiss: () => void
|
||||
proposalId: string | undefined // id for the proposal to execute
|
||||
}
|
||||
|
||||
export default function ExecuteModal({ isOpen, onDismiss, proposalId }: ExecuteModalProps) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const executeCallback = useExecuteCallback()
|
||||
|
||||
// monitor call to help UI loading state
|
||||
const [hash, setHash] = useState<string | undefined>()
|
||||
const [attempting, setAttempting] = useState<boolean>(false)
|
||||
|
||||
// get theme for colors
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
// wrapper to reset state on modal close
|
||||
function wrappedOnDismiss() {
|
||||
setHash(undefined)
|
||||
setAttempting(false)
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
async function onExecute() {
|
||||
setAttempting(true)
|
||||
|
||||
// if callback not returned properly ignore
|
||||
if (!executeCallback) return
|
||||
|
||||
// try delegation and store hash
|
||||
const hash = await executeCallback(proposalId)?.catch((error) => {
|
||||
setAttempting(false)
|
||||
console.log(error)
|
||||
})
|
||||
|
||||
if (hash) {
|
||||
setHash(hash)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={wrappedOnDismiss} maxHeight={90}>
|
||||
{!attempting && !hash && (
|
||||
<ContentWrapper gap="lg">
|
||||
<AutoColumn gap="lg" justify="center">
|
||||
<RowBetween>
|
||||
<ThemedText.MediumHeader fontWeight={500}>
|
||||
<Trans>Execute Proposal {proposalId}</Trans>
|
||||
</ThemedText.MediumHeader>
|
||||
<StyledClosed onClick={wrappedOnDismiss} />
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<ThemedText.Body>
|
||||
<Trans>Executing this proposal will enact the calldata on-chain.</Trans>
|
||||
</ThemedText.Body>
|
||||
</RowBetween>
|
||||
<ButtonPrimary onClick={onExecute}>
|
||||
<ThemedText.MediumHeader color="white">
|
||||
<Trans>Execute</Trans>
|
||||
</ThemedText.MediumHeader>
|
||||
</ButtonPrimary>
|
||||
</AutoColumn>
|
||||
</ContentWrapper>
|
||||
)}
|
||||
{attempting && !hash && (
|
||||
<ConfirmOrLoadingWrapper>
|
||||
<RowBetween>
|
||||
<div />
|
||||
<StyledClosed onClick={wrappedOnDismiss} />
|
||||
</RowBetween>
|
||||
<ConfirmedIcon>
|
||||
<CustomLightSpinner src={Circle} alt="loader" size={'90px'} />
|
||||
</ConfirmedIcon>
|
||||
<AutoColumn gap="100px" justify={'center'}>
|
||||
<AutoColumn gap="12px" justify={'center'}>
|
||||
<ThemedText.LargeHeader>
|
||||
<Trans>Executing</Trans>
|
||||
</ThemedText.LargeHeader>
|
||||
</AutoColumn>
|
||||
<ThemedText.SubHeader>
|
||||
<Trans>Confirm this transaction in your wallet</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
</AutoColumn>
|
||||
</ConfirmOrLoadingWrapper>
|
||||
)}
|
||||
{hash && (
|
||||
<ConfirmOrLoadingWrapper>
|
||||
<RowBetween>
|
||||
<div />
|
||||
<StyledClosed onClick={wrappedOnDismiss} />
|
||||
</RowBetween>
|
||||
<ConfirmedIcon>
|
||||
<ArrowUpCircle strokeWidth={0.5} size={90} color={theme.primary1} />
|
||||
</ConfirmedIcon>
|
||||
<AutoColumn gap="100px" justify={'center'}>
|
||||
<AutoColumn gap="12px" justify={'center'}>
|
||||
<ThemedText.LargeHeader>
|
||||
<Trans>Execution Submitted</Trans>
|
||||
</ThemedText.LargeHeader>
|
||||
</AutoColumn>
|
||||
{chainId && (
|
||||
<ExternalLink
|
||||
href={getExplorerLink(chainId, hash, ExplorerDataType.TRANSACTION)}
|
||||
style={{ marginLeft: '4px' }}
|
||||
>
|
||||
<ThemedText.SubHeader>
|
||||
<Trans>View transaction on Explorer</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
</ExternalLink>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</ConfirmOrLoadingWrapper>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
153
src/components/vote/QueueModal.tsx
Normal file
153
src/components/vote/QueueModal.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { useContext, useState } from 'react'
|
||||
import { ArrowUpCircle, X } from 'react-feather'
|
||||
import styled, { ThemeContext } from 'styled-components/macro'
|
||||
|
||||
import Circle from '../../assets/images/blue-loader.svg'
|
||||
import { useQueueCallback } from '../../state/governance/hooks'
|
||||
import { CustomLightSpinner, ThemedText } from '../../theme'
|
||||
import { ExternalLink } from '../../theme'
|
||||
import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink'
|
||||
import { ButtonPrimary } from '../Button'
|
||||
import { AutoColumn, ColumnCenter } from '../Column'
|
||||
import Modal from '../Modal'
|
||||
import { RowBetween } from '../Row'
|
||||
|
||||
const ContentWrapper = styled(AutoColumn)`
|
||||
width: 100%;
|
||||
padding: 24px;
|
||||
`
|
||||
|
||||
const StyledClosed = styled(X)`
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`
|
||||
|
||||
const ConfirmOrLoadingWrapper = styled.div`
|
||||
width: 100%;
|
||||
padding: 24px;
|
||||
`
|
||||
|
||||
const ConfirmedIcon = styled(ColumnCenter)`
|
||||
padding: 60px 0;
|
||||
`
|
||||
|
||||
interface QueueModalProps {
|
||||
isOpen: boolean
|
||||
onDismiss: () => void
|
||||
proposalId: string | undefined // id for the proposal to queue
|
||||
}
|
||||
|
||||
export default function QueueModal({ isOpen, onDismiss, proposalId }: QueueModalProps) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const queueCallback = useQueueCallback()
|
||||
|
||||
// monitor call to help UI loading state
|
||||
const [hash, setHash] = useState<string | undefined>()
|
||||
const [attempting, setAttempting] = useState<boolean>(false)
|
||||
|
||||
// get theme for colors
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
// wrapper to reset state on modal close
|
||||
function wrappedOnDismiss() {
|
||||
setHash(undefined)
|
||||
setAttempting(false)
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
async function onQueue() {
|
||||
setAttempting(true)
|
||||
|
||||
// if callback not returned properly ignore
|
||||
if (!queueCallback) return
|
||||
|
||||
// try delegation and store hash
|
||||
const hash = await queueCallback(proposalId)?.catch((error) => {
|
||||
setAttempting(false)
|
||||
console.log(error)
|
||||
})
|
||||
|
||||
if (hash) {
|
||||
setHash(hash)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={wrappedOnDismiss} maxHeight={90}>
|
||||
{!attempting && !hash && (
|
||||
<ContentWrapper gap="lg">
|
||||
<AutoColumn gap="lg" justify="center">
|
||||
<RowBetween>
|
||||
<ThemedText.MediumHeader fontWeight={500}>
|
||||
<Trans>Queue Proposal {proposalId}</Trans>
|
||||
</ThemedText.MediumHeader>
|
||||
<StyledClosed onClick={wrappedOnDismiss} />
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<ThemedText.Body>
|
||||
<Trans>Adding this proposal to the queue will allow it to be executed, after a delay.</Trans>
|
||||
</ThemedText.Body>
|
||||
</RowBetween>
|
||||
<ButtonPrimary onClick={onQueue}>
|
||||
<ThemedText.MediumHeader color="white">
|
||||
<Trans>Queue</Trans>
|
||||
</ThemedText.MediumHeader>
|
||||
</ButtonPrimary>
|
||||
</AutoColumn>
|
||||
</ContentWrapper>
|
||||
)}
|
||||
{attempting && !hash && (
|
||||
<ConfirmOrLoadingWrapper>
|
||||
<RowBetween>
|
||||
<div />
|
||||
<StyledClosed onClick={wrappedOnDismiss} />
|
||||
</RowBetween>
|
||||
<ConfirmedIcon>
|
||||
<CustomLightSpinner src={Circle} alt="loader" size={'90px'} />
|
||||
</ConfirmedIcon>
|
||||
<AutoColumn gap="100px" justify={'center'}>
|
||||
<AutoColumn gap="12px" justify={'center'}>
|
||||
<ThemedText.LargeHeader>
|
||||
<Trans>Queueing</Trans>
|
||||
</ThemedText.LargeHeader>
|
||||
</AutoColumn>
|
||||
<ThemedText.SubHeader>
|
||||
<Trans>Confirm this transaction in your wallet</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
</AutoColumn>
|
||||
</ConfirmOrLoadingWrapper>
|
||||
)}
|
||||
{hash && (
|
||||
<ConfirmOrLoadingWrapper>
|
||||
<RowBetween>
|
||||
<div />
|
||||
<StyledClosed onClick={wrappedOnDismiss} />
|
||||
</RowBetween>
|
||||
<ConfirmedIcon>
|
||||
<ArrowUpCircle strokeWidth={0.5} size={90} color={theme.primary1} />
|
||||
</ConfirmedIcon>
|
||||
<AutoColumn gap="100px" justify={'center'}>
|
||||
<AutoColumn gap="12px" justify={'center'}>
|
||||
<ThemedText.LargeHeader>
|
||||
<Trans>Transaction Submitted</Trans>
|
||||
</ThemedText.LargeHeader>
|
||||
</AutoColumn>
|
||||
{chainId && (
|
||||
<ExternalLink
|
||||
href={getExplorerLink(chainId, hash, ExplorerDataType.TRANSACTION)}
|
||||
style={{ marginLeft: '4px' }}
|
||||
>
|
||||
<ThemedText.SubHeader>
|
||||
<Trans>View transaction on Explorer</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
</ExternalLink>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</ConfirmOrLoadingWrapper>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -45,7 +45,7 @@ interface VoteModalProps {
|
||||
|
||||
export default function VoteModal({ isOpen, onDismiss, proposalId, voteOption }: VoteModalProps) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const { voteCallback } = useVoteCallback()
|
||||
const voteCallback = useVoteCallback()
|
||||
const { votes: availableVotes } = useUserVotes()
|
||||
|
||||
// monitor call to help UI loading state
|
||||
@@ -56,7 +56,7 @@ export default function VoteModal({ isOpen, onDismiss, proposalId, voteOption }:
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
// wrapper to reset state on modal close
|
||||
function wrappedOndismiss() {
|
||||
function wrappedOnDismiss() {
|
||||
setHash(undefined)
|
||||
setAttempting(false)
|
||||
onDismiss()
|
||||
@@ -80,7 +80,7 @@ export default function VoteModal({ isOpen, onDismiss, proposalId, voteOption }:
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={wrappedOndismiss} maxHeight={90}>
|
||||
<Modal isOpen={isOpen} onDismiss={wrappedOnDismiss} maxHeight={90}>
|
||||
{!attempting && !hash && (
|
||||
<ContentWrapper gap="lg">
|
||||
<AutoColumn gap="lg" justify="center">
|
||||
@@ -94,7 +94,7 @@ export default function VoteModal({ isOpen, onDismiss, proposalId, voteOption }:
|
||||
<Trans>Vote to abstain on proposal {proposalId}</Trans>
|
||||
)}
|
||||
</ThemedText.MediumHeader>
|
||||
<StyledClosed stroke="black" onClick={wrappedOndismiss} />
|
||||
<StyledClosed onClick={wrappedOnDismiss} />
|
||||
</RowBetween>
|
||||
<ThemedText.LargeHeader>
|
||||
<Trans>{formatCurrencyAmount(availableVotes, 4)} Votes</Trans>
|
||||
@@ -117,7 +117,7 @@ export default function VoteModal({ isOpen, onDismiss, proposalId, voteOption }:
|
||||
<ConfirmOrLoadingWrapper>
|
||||
<RowBetween>
|
||||
<div />
|
||||
<StyledClosed onClick={wrappedOndismiss} />
|
||||
<StyledClosed onClick={wrappedOnDismiss} />
|
||||
</RowBetween>
|
||||
<ConfirmedIcon>
|
||||
<CustomLightSpinner src={Circle} alt="loader" size={'90px'} />
|
||||
@@ -138,7 +138,7 @@ export default function VoteModal({ isOpen, onDismiss, proposalId, voteOption }:
|
||||
<ConfirmOrLoadingWrapper>
|
||||
<RowBetween>
|
||||
<div />
|
||||
<StyledClosed onClick={wrappedOndismiss} />
|
||||
<StyledClosed onClick={wrappedOnDismiss} />
|
||||
</RowBetween>
|
||||
<ConfirmedIcon>
|
||||
<ArrowUpCircle strokeWidth={0.5} size={90} color={theme.primary1} />
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import { FortmaticConnector as FortmaticConnectorCore } from 'web3-react-fortmatic-connector'
|
||||
|
||||
export const OVERLAY_READY = 'OVERLAY_READY'
|
||||
|
||||
type FormaticSupportedChains = 1 | 3 | 4 | 42
|
||||
|
||||
const CHAIN_ID_NETWORK_ARGUMENT: { readonly [chainId in FormaticSupportedChains]: string | undefined } = {
|
||||
1: undefined,
|
||||
3: 'ropsten',
|
||||
4: 'rinkeby',
|
||||
42: 'kovan',
|
||||
}
|
||||
|
||||
export class FortmaticConnector extends FortmaticConnectorCore {
|
||||
async activate() {
|
||||
if (!this.fortmatic) {
|
||||
const { default: Fortmatic } = await import('fortmatic')
|
||||
|
||||
const { apiKey, chainId } = this as any
|
||||
if (chainId in CHAIN_ID_NETWORK_ARGUMENT) {
|
||||
this.fortmatic = new Fortmatic(apiKey, CHAIN_ID_NETWORK_ARGUMENT[chainId as FormaticSupportedChains])
|
||||
} else {
|
||||
throw new Error(`Unsupported network ID: ${chainId}`)
|
||||
}
|
||||
}
|
||||
|
||||
const provider = this.fortmatic.getProvider()
|
||||
|
||||
const pollForOverlayReady = new Promise<void>((resolve) => {
|
||||
const interval = setInterval(() => {
|
||||
if (provider.overlayReady) {
|
||||
clearInterval(interval)
|
||||
this.emit(OVERLAY_READY)
|
||||
resolve()
|
||||
}
|
||||
}, 200)
|
||||
})
|
||||
|
||||
const [account] = await Promise.all([
|
||||
provider.enable().then((accounts: string[]) => accounts[0]),
|
||||
pollForOverlayReady,
|
||||
])
|
||||
|
||||
return { provider: this.fortmatic.getProvider(), chainId: (this as any).chainId, account }
|
||||
}
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
import invariant from 'tiny-invariant'
|
||||
import { AbstractConnector } from 'web3-react-abstract-connector'
|
||||
import { ConnectorUpdate } from 'web3-react-types'
|
||||
|
||||
interface NetworkConnectorArguments {
|
||||
urls: { [chainId: number]: string }
|
||||
defaultChainId?: number
|
||||
}
|
||||
|
||||
// taken from ethers.js, compatible interface with web3 provider
|
||||
type AsyncSendable = {
|
||||
isMetaMask?: boolean
|
||||
host?: string
|
||||
path?: string
|
||||
sendAsync?: (request: any, callback: (error: any, response: any) => void) => void
|
||||
send?: (request: any, callback: (error: any, response: any) => void) => void
|
||||
}
|
||||
|
||||
class RequestError extends Error {
|
||||
constructor(message: string, public code: number, public data?: unknown) {
|
||||
super(message)
|
||||
}
|
||||
}
|
||||
|
||||
interface BatchItem {
|
||||
request: { jsonrpc: '2.0'; id: number; method: string; params: unknown }
|
||||
resolve: (result: any) => void
|
||||
reject: (error: Error) => void
|
||||
}
|
||||
|
||||
class MiniRpcProvider implements AsyncSendable {
|
||||
public readonly isMetaMask: false = false
|
||||
public readonly chainId: number
|
||||
public readonly url: string
|
||||
public readonly host: string
|
||||
public readonly path: string
|
||||
public readonly batchWaitTimeMs: number
|
||||
|
||||
private readonly connector: NetworkConnector
|
||||
|
||||
private nextId = 1
|
||||
private batchTimeoutId: ReturnType<typeof setTimeout> | null = null
|
||||
private batch: BatchItem[] = []
|
||||
|
||||
constructor(connector: NetworkConnector, chainId: number, url: string, batchWaitTimeMs?: number) {
|
||||
this.connector = connector
|
||||
this.chainId = chainId
|
||||
this.url = url
|
||||
const parsed = new URL(url)
|
||||
this.host = parsed.host
|
||||
this.path = parsed.pathname
|
||||
// how long to wait to batch calls
|
||||
this.batchWaitTimeMs = batchWaitTimeMs ?? 50
|
||||
}
|
||||
|
||||
public readonly clearBatch = async () => {
|
||||
console.debug('Clearing batch', this.batch)
|
||||
let batch = this.batch
|
||||
|
||||
batch = batch.filter((b) => {
|
||||
if (b.request.method === 'wallet_switchEthereumChain') {
|
||||
try {
|
||||
this.connector.changeChainId(parseInt((b.request.params as [{ chainId: string }])[0].chainId))
|
||||
b.resolve({ id: b.request.id })
|
||||
} catch (error) {
|
||||
b.reject(error)
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
this.batch = []
|
||||
this.batchTimeoutId = null
|
||||
let response: Response
|
||||
try {
|
||||
response = await fetch(this.url, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
||||
body: JSON.stringify(batch.map((item) => item.request)),
|
||||
})
|
||||
} catch (error) {
|
||||
batch.forEach(({ reject }) => reject(new Error('Failed to send batch call')))
|
||||
return
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
batch.forEach(({ reject }) => reject(new RequestError(`${response.status}: ${response.statusText}`, -32000)))
|
||||
return
|
||||
}
|
||||
|
||||
let json
|
||||
try {
|
||||
json = await response.json()
|
||||
} catch (error) {
|
||||
batch.forEach(({ reject }) => reject(new Error('Failed to parse JSON response')))
|
||||
return
|
||||
}
|
||||
const byKey = batch.reduce<{ [id: number]: BatchItem }>((memo, current) => {
|
||||
memo[current.request.id] = current
|
||||
return memo
|
||||
}, {})
|
||||
for (const result of json) {
|
||||
const {
|
||||
resolve,
|
||||
reject,
|
||||
request: { method },
|
||||
} = byKey[result.id]
|
||||
if ('error' in result) {
|
||||
reject(new RequestError(result?.error?.message, result?.error?.code, result?.error?.data))
|
||||
} else if ('result' in result && resolve) {
|
||||
resolve(result.result)
|
||||
} else {
|
||||
reject(new RequestError(`Received unexpected JSON-RPC response to ${method} request.`, -32000, result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public readonly sendAsync = (
|
||||
request: {
|
||||
jsonrpc: '2.0'
|
||||
id: number | string | null
|
||||
method: string
|
||||
params?: unknown[] | Record<string, unknown>
|
||||
},
|
||||
callback: (error: any, response: any) => void
|
||||
): void => {
|
||||
this.request(request.method, request.params)
|
||||
.then((result) => callback(null, { jsonrpc: '2.0', id: request.id, result }))
|
||||
.catch((error) => callback(error, null))
|
||||
}
|
||||
|
||||
public readonly request = async (
|
||||
method: string | { method: string; params: unknown[] },
|
||||
params?: unknown[] | Record<string, unknown>
|
||||
): Promise<unknown> => {
|
||||
if (typeof method !== 'string') {
|
||||
return this.request(method.method, method.params)
|
||||
}
|
||||
if (method === 'eth_chainId') {
|
||||
return `0x${this.chainId.toString(16)}`
|
||||
}
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
this.batch.push({
|
||||
request: {
|
||||
jsonrpc: '2.0',
|
||||
id: this.nextId++,
|
||||
method,
|
||||
params,
|
||||
},
|
||||
resolve,
|
||||
reject,
|
||||
})
|
||||
})
|
||||
this.batchTimeoutId = this.batchTimeoutId ?? setTimeout(this.clearBatch, this.batchWaitTimeMs)
|
||||
return promise
|
||||
}
|
||||
}
|
||||
|
||||
export class NetworkConnector extends AbstractConnector {
|
||||
private readonly providers: { [chainId: number]: MiniRpcProvider }
|
||||
private currentChainId: number
|
||||
|
||||
constructor({ urls, defaultChainId }: NetworkConnectorArguments) {
|
||||
invariant(defaultChainId || Object.keys(urls).length === 1, 'defaultChainId is a required argument with >1 url')
|
||||
super({ supportedChainIds: Object.keys(urls).map((k): number => Number(k)) })
|
||||
|
||||
this.currentChainId = defaultChainId ?? Number(Object.keys(urls)[0])
|
||||
this.providers = Object.keys(urls).reduce<{ [chainId: number]: MiniRpcProvider }>((accumulator, chainId) => {
|
||||
accumulator[Number(chainId)] = new MiniRpcProvider(this, Number(chainId), urls[Number(chainId)])
|
||||
return accumulator
|
||||
}, {})
|
||||
}
|
||||
|
||||
public get provider(): MiniRpcProvider {
|
||||
return this.providers[this.currentChainId]
|
||||
}
|
||||
|
||||
public async activate(): Promise<ConnectorUpdate> {
|
||||
return { provider: this.providers[this.currentChainId], chainId: this.currentChainId, account: null }
|
||||
}
|
||||
|
||||
public async getProvider(): Promise<MiniRpcProvider> {
|
||||
return this.providers[this.currentChainId]
|
||||
}
|
||||
|
||||
public async getChainId(): Promise<number> {
|
||||
return this.currentChainId
|
||||
}
|
||||
|
||||
public async getAccount(): Promise<null> {
|
||||
return null
|
||||
}
|
||||
|
||||
public deactivate() {
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* Meant to be called only by MiniRpcProvider
|
||||
* @param chainId the new chain id
|
||||
*/
|
||||
public changeChainId(chainId: number) {
|
||||
if (chainId in this.providers) {
|
||||
this.currentChainId = chainId
|
||||
this.emitUpdate({
|
||||
chainId,
|
||||
account: null,
|
||||
provider: this.providers[chainId],
|
||||
})
|
||||
} else {
|
||||
throw new Error(`Unsupported chain ID: ${chainId}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
1
src/connectors/fortmatic.d.ts
vendored
1
src/connectors/fortmatic.d.ts
vendored
@@ -1 +0,0 @@
|
||||
declare module 'formatic'
|
||||
@@ -1,49 +1,150 @@
|
||||
import { Web3Provider } from '@ethersproject/providers'
|
||||
import { SafeAppConnector } from '@gnosis.pm/safe-apps-web3-react'
|
||||
import { ALL_SUPPORTED_CHAIN_IDS, SupportedChainId } from 'constants/chains'
|
||||
import { CoinbaseWallet } from '@web3-react/coinbase-wallet'
|
||||
import { initializeConnector, Web3ReactHooks } from '@web3-react/core'
|
||||
import { EIP1193 } from '@web3-react/eip1193'
|
||||
import { GnosisSafe } from '@web3-react/gnosis-safe'
|
||||
import { MetaMask } from '@web3-react/metamask'
|
||||
import { Network } from '@web3-react/network'
|
||||
import { Connector } from '@web3-react/types'
|
||||
import { WalletConnect } from '@web3-react/walletconnect'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { INFURA_NETWORK_URLS } from 'constants/infura'
|
||||
import { InjectedConnector } from 'web3-react-injected-connector'
|
||||
import { WalletConnectConnector } from 'web3-react-walletconnect-connector'
|
||||
import { WalletLinkConnector } from 'web3-react-walletlink-connector'
|
||||
import Fortmatic from 'fortmatic'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import UNISWAP_LOGO_URL from '../assets/svg/logo.svg'
|
||||
import getLibrary from '../utils/getLibrary'
|
||||
import { FortmaticConnector } from './Fortmatic'
|
||||
import { NetworkConnector } from './NetworkConnector'
|
||||
|
||||
const FORMATIC_KEY = process.env.REACT_APP_FORTMATIC_KEY
|
||||
|
||||
export const network = new NetworkConnector({
|
||||
urls: INFURA_NETWORK_URLS,
|
||||
defaultChainId: 1,
|
||||
})
|
||||
|
||||
let networkLibrary: Web3Provider | undefined
|
||||
export function getNetworkLibrary(): Web3Provider {
|
||||
return (networkLibrary = networkLibrary ?? getLibrary(network.provider))
|
||||
export enum Wallet {
|
||||
INJECTED = 'INJECTED',
|
||||
COINBASE_WALLET = 'COINBASE_WALLET',
|
||||
WALLET_CONNECT = 'WALLET_CONNECT',
|
||||
FORTMATIC = 'FORTMATIC',
|
||||
NETWORK = 'NETWORK',
|
||||
GNOSIS_SAFE = 'GNOSIS_SAFE',
|
||||
}
|
||||
|
||||
export const injected = new InjectedConnector({
|
||||
supportedChainIds: ALL_SUPPORTED_CHAIN_IDS,
|
||||
})
|
||||
export const BACKFILLABLE_WALLETS = [Wallet.COINBASE_WALLET, Wallet.WALLET_CONNECT, Wallet.INJECTED]
|
||||
export const SELECTABLE_WALLETS = [...BACKFILLABLE_WALLETS, Wallet.FORTMATIC]
|
||||
|
||||
export const gnosisSafe = new SafeAppConnector()
|
||||
function onError(error: Error) {
|
||||
console.debug(`web3-react error: ${error}`)
|
||||
}
|
||||
|
||||
export const walletconnect = new WalletConnectConnector({
|
||||
supportedChainIds: ALL_SUPPORTED_CHAIN_IDS,
|
||||
rpc: INFURA_NETWORK_URLS,
|
||||
qrcode: true,
|
||||
})
|
||||
export function getWalletForConnector(connector: Connector) {
|
||||
switch (connector) {
|
||||
case injected:
|
||||
return Wallet.INJECTED
|
||||
case coinbaseWallet:
|
||||
return Wallet.COINBASE_WALLET
|
||||
case walletConnect:
|
||||
return Wallet.WALLET_CONNECT
|
||||
case fortmatic:
|
||||
return Wallet.FORTMATIC
|
||||
case network:
|
||||
return Wallet.NETWORK
|
||||
case gnosisSafe:
|
||||
return Wallet.GNOSIS_SAFE
|
||||
default:
|
||||
throw Error('unsupported connector')
|
||||
}
|
||||
}
|
||||
|
||||
// mainnet only
|
||||
export const fortmatic = new FortmaticConnector({
|
||||
apiKey: FORMATIC_KEY ?? '',
|
||||
chainId: 1,
|
||||
})
|
||||
export function getConnectorForWallet(wallet: Wallet) {
|
||||
switch (wallet) {
|
||||
case Wallet.INJECTED:
|
||||
return injected
|
||||
case Wallet.COINBASE_WALLET:
|
||||
return coinbaseWallet
|
||||
case Wallet.WALLET_CONNECT:
|
||||
return walletConnect
|
||||
case Wallet.FORTMATIC:
|
||||
return fortmatic
|
||||
case Wallet.NETWORK:
|
||||
return network
|
||||
case Wallet.GNOSIS_SAFE:
|
||||
return gnosisSafe
|
||||
}
|
||||
}
|
||||
|
||||
export const walletlink = new WalletLinkConnector({
|
||||
url: INFURA_NETWORK_URLS[SupportedChainId.MAINNET],
|
||||
appName: 'Uniswap',
|
||||
appLogoUrl: UNISWAP_LOGO_URL,
|
||||
supportedChainIds: ALL_SUPPORTED_CHAIN_IDS,
|
||||
})
|
||||
function getHooksForWallet(wallet: Wallet) {
|
||||
switch (wallet) {
|
||||
case Wallet.INJECTED:
|
||||
return injectedHooks
|
||||
case Wallet.COINBASE_WALLET:
|
||||
return coinbaseWalletHooks
|
||||
case Wallet.WALLET_CONNECT:
|
||||
return walletConnectHooks
|
||||
case Wallet.FORTMATIC:
|
||||
return fortmaticHooks
|
||||
case Wallet.NETWORK:
|
||||
return networkHooks
|
||||
case Wallet.GNOSIS_SAFE:
|
||||
return gnosisSafeHooks
|
||||
}
|
||||
}
|
||||
|
||||
export const [network, networkHooks] = initializeConnector<Network>(
|
||||
(actions) => new Network({ actions, urlMap: INFURA_NETWORK_URLS, defaultChainId: 1 })
|
||||
)
|
||||
|
||||
export const [injected, injectedHooks] = initializeConnector<MetaMask>((actions) => new MetaMask({ actions, onError }))
|
||||
|
||||
export const [gnosisSafe, gnosisSafeHooks] = initializeConnector<GnosisSafe>((actions) => new GnosisSafe({ actions }))
|
||||
|
||||
export const [walletConnect, walletConnectHooks] = initializeConnector<WalletConnect>(
|
||||
(actions) =>
|
||||
new WalletConnect({
|
||||
actions,
|
||||
options: {
|
||||
rpc: INFURA_NETWORK_URLS,
|
||||
qrcode: true,
|
||||
},
|
||||
onError,
|
||||
})
|
||||
)
|
||||
|
||||
export const [fortmatic, fortmaticHooks] = initializeConnector<EIP1193>(
|
||||
(actions) => new EIP1193({ actions, provider: new Fortmatic(process.env.REACT_APP_FORTMATIC_KEY).getProvider() })
|
||||
)
|
||||
|
||||
export const [coinbaseWallet, coinbaseWalletHooks] = initializeConnector<CoinbaseWallet>(
|
||||
(actions) =>
|
||||
new CoinbaseWallet({
|
||||
actions,
|
||||
options: {
|
||||
url: INFURA_NETWORK_URLS[SupportedChainId.MAINNET],
|
||||
appName: 'Uniswap',
|
||||
appLogoUrl: UNISWAP_LOGO_URL,
|
||||
},
|
||||
onError,
|
||||
})
|
||||
)
|
||||
|
||||
interface ConnectorListItem {
|
||||
connector: Connector
|
||||
hooks: Web3ReactHooks
|
||||
}
|
||||
|
||||
function getConnectorListItemForWallet(wallet: Wallet) {
|
||||
return {
|
||||
connector: getConnectorForWallet(wallet),
|
||||
hooks: getHooksForWallet(wallet),
|
||||
}
|
||||
}
|
||||
|
||||
export function useConnectors(selectedWallet: Wallet | undefined) {
|
||||
return useMemo(() => {
|
||||
const connectors: ConnectorListItem[] = [{ connector: gnosisSafe, hooks: gnosisSafeHooks }]
|
||||
if (selectedWallet) {
|
||||
connectors.push(getConnectorListItemForWallet(selectedWallet))
|
||||
}
|
||||
connectors.push(
|
||||
...SELECTABLE_WALLETS.filter((wallet) => wallet !== selectedWallet).map(getConnectorListItemForWallet)
|
||||
)
|
||||
connectors.push({ connector: network, hooks: networkHooks })
|
||||
const web3ReactConnectors: [Connector, Web3ReactHooks][] = connectors.map(({ connector, hooks }) => [
|
||||
connector,
|
||||
hooks,
|
||||
])
|
||||
return web3ReactConnectors
|
||||
}, [selectedWallet])
|
||||
}
|
||||
|
||||
@@ -46,6 +46,15 @@ export const SUPPORTED_GAS_ESTIMATE_CHAIN_IDS = [
|
||||
SupportedChainId.ARBITRUM_ONE,
|
||||
]
|
||||
|
||||
/**
|
||||
* Unsupported networks for V2 pool behavior.
|
||||
*/
|
||||
export const UNSUPPORTED_V2POOL_CHAIN_IDS = [
|
||||
SupportedChainId.POLYGON,
|
||||
SupportedChainId.OPTIMISM,
|
||||
SupportedChainId.ARBITRUM_ONE,
|
||||
]
|
||||
|
||||
/**
|
||||
* All the chain IDs that are running the Ethereum protocol.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
GOVERNANCE_ALPHA_V0_ADDRESSES,
|
||||
GOVERNANCE_ALPHA_V1_ADDRESSES,
|
||||
GOVERNANCE_BRAVO_ADDRESSES,
|
||||
TIMELOCK_ADDRESS,
|
||||
UNI_ADDRESS,
|
||||
} from './addresses'
|
||||
@@ -11,7 +12,8 @@ export const COMMON_CONTRACT_NAMES: Record<number, { [address: string]: string }
|
||||
[UNI_ADDRESS[SupportedChainId.MAINNET]]: 'UNI',
|
||||
[TIMELOCK_ADDRESS[SupportedChainId.MAINNET]]: 'Timelock',
|
||||
[GOVERNANCE_ALPHA_V0_ADDRESSES[SupportedChainId.MAINNET]]: 'Governance (V0)',
|
||||
[GOVERNANCE_ALPHA_V1_ADDRESSES[SupportedChainId.MAINNET]]: 'Governance',
|
||||
[GOVERNANCE_ALPHA_V1_ADDRESSES[SupportedChainId.MAINNET]]: 'Governance (V1)',
|
||||
[GOVERNANCE_BRAVO_ADDRESSES[SupportedChainId.MAINNET]]: 'Governance',
|
||||
'0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e': 'ENS Registry',
|
||||
'0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41': 'ENS Public Resolver',
|
||||
},
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { JsonRpcProvider } from '@ethersproject/providers'
|
||||
|
||||
import { SupportedChainId } from './chains'
|
||||
|
||||
const INFURA_KEY = process.env.REACT_APP_INFURA_KEY
|
||||
@@ -5,6 +7,8 @@ if (typeof INFURA_KEY === 'undefined') {
|
||||
throw new Error(`REACT_APP_INFURA_KEY must be a defined environment variable`)
|
||||
}
|
||||
|
||||
export const MAINNET_PROVIDER = new JsonRpcProvider(`https://mainnet.infura.io/v3/${INFURA_KEY}`)
|
||||
|
||||
/**
|
||||
* These are the network URLs used by the interface when there is not another available source of chain data
|
||||
*/
|
||||
|
||||
@@ -2,3 +2,4 @@ export const UNISWAP_GRANTS_START_BLOCK = 11473815
|
||||
export const BRAVO_START_BLOCK = 13059344
|
||||
export const ONE_BIP_START_BLOCK = 13551293
|
||||
export const POLYGON_START_BLOCK = 13786993
|
||||
export const MOONBEAN_START_BLOCK = 14732457
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { AbstractConnector } from 'web3-react-abstract-connector'
|
||||
import { Connector } from '@web3-react/types'
|
||||
|
||||
import INJECTED_ICON_URL from '../assets/images/arrow-right.svg'
|
||||
import COINBASE_ICON_URL from '../assets/images/coinbaseWalletIcon.svg'
|
||||
import FORTMATIC_ICON_URL from '../assets/images/fortmaticIcon.png'
|
||||
import METAMASK_ICON_URL from '../assets/images/metamask.png'
|
||||
import WALLETCONNECT_ICON_URL from '../assets/images/walletConnectIcon.svg'
|
||||
import { fortmatic, injected, walletconnect, walletlink } from '../connectors'
|
||||
import { coinbaseWallet, fortmatic, injected, Wallet, walletConnect } from '../connectors'
|
||||
|
||||
interface WalletInfo {
|
||||
connector?: AbstractConnector
|
||||
connector?: Connector
|
||||
wallet?: Wallet
|
||||
name: string
|
||||
iconURL: string
|
||||
description: string
|
||||
@@ -22,6 +23,7 @@ interface WalletInfo {
|
||||
export const SUPPORTED_WALLETS: { [key: string]: WalletInfo } = {
|
||||
INJECTED: {
|
||||
connector: injected,
|
||||
wallet: Wallet.INJECTED,
|
||||
name: 'Injected',
|
||||
iconURL: INJECTED_ICON_URL,
|
||||
description: 'Injected web3 provider.',
|
||||
@@ -31,6 +33,7 @@ export const SUPPORTED_WALLETS: { [key: string]: WalletInfo } = {
|
||||
},
|
||||
METAMASK: {
|
||||
connector: injected,
|
||||
wallet: Wallet.INJECTED,
|
||||
name: 'MetaMask',
|
||||
iconURL: METAMASK_ICON_URL,
|
||||
description: 'Easy-to-use browser extension.',
|
||||
@@ -38,7 +41,8 @@ export const SUPPORTED_WALLETS: { [key: string]: WalletInfo } = {
|
||||
color: '#E8831D',
|
||||
},
|
||||
WALLET_CONNECT: {
|
||||
connector: walletconnect,
|
||||
connector: walletConnect,
|
||||
wallet: Wallet.WALLET_CONNECT,
|
||||
name: 'WalletConnect',
|
||||
iconURL: WALLETCONNECT_ICON_URL,
|
||||
description: 'Connect to Trust Wallet, Rainbow Wallet and more...',
|
||||
@@ -46,8 +50,9 @@ export const SUPPORTED_WALLETS: { [key: string]: WalletInfo } = {
|
||||
color: '#4196FC',
|
||||
mobile: true,
|
||||
},
|
||||
WALLET_LINK: {
|
||||
connector: walletlink,
|
||||
COINBASE_WALLET: {
|
||||
connector: coinbaseWallet,
|
||||
wallet: Wallet.COINBASE_WALLET,
|
||||
name: 'Coinbase Wallet',
|
||||
iconURL: COINBASE_ICON_URL,
|
||||
description: 'Use Coinbase Wallet app on mobile device',
|
||||
@@ -65,6 +70,7 @@ export const SUPPORTED_WALLETS: { [key: string]: WalletInfo } = {
|
||||
},
|
||||
FORTMATIC: {
|
||||
connector: fortmatic,
|
||||
wallet: Wallet.FORTMATIC,
|
||||
name: 'Fortmatic',
|
||||
iconURL: FORTMATIC_ICON_URL,
|
||||
description: 'Login using Fortmatic hosted wallet',
|
||||
|
||||
@@ -120,11 +120,15 @@ export function useSearchInactiveTokenLists(search: string | undefined, minResul
|
||||
if (!list) continue
|
||||
for (const tokenInfo of list.tokens) {
|
||||
if (tokenInfo.chainId === chainId && tokenFilter(tokenInfo)) {
|
||||
const wrapped: WrappedTokenInfo = new WrappedTokenInfo(tokenInfo, list)
|
||||
if (!(wrapped.address in activeTokens) && !addressSet[wrapped.address]) {
|
||||
addressSet[wrapped.address] = true
|
||||
result.push(wrapped)
|
||||
if (result.length >= minResults) return result
|
||||
try {
|
||||
const wrapped: WrappedTokenInfo = new WrappedTokenInfo(tokenInfo, list)
|
||||
if (!(wrapped.address in activeTokens) && !addressSet[wrapped.address]) {
|
||||
addressSet[wrapped.address] = true
|
||||
result.push(wrapped)
|
||||
if (result.length >= minResults) return result
|
||||
}
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import ms from 'ms.macro'
|
||||
import { useEffect } from 'react'
|
||||
import ReactGA from 'react-ga4'
|
||||
import { ApplicationModal, setOpenModal } from 'state/application/reducer'
|
||||
import { useAppDispatch } from 'state/hooks'
|
||||
|
||||
@@ -7,27 +8,35 @@ export default function useAccountRiskCheck(account: string | null | undefined)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
if (account && window.location.hostname === 'app.uniswap.org') {
|
||||
const headers = new Headers({ 'Content-Type': 'application/json' })
|
||||
fetch('https://screening-worker.uniswap.workers.dev', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ address: account }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.block) {
|
||||
dispatch(setOpenModal(ApplicationModal.BLOCKED_ACCOUNT))
|
||||
ReactGA.event({
|
||||
category: 'Address Screening',
|
||||
action: 'blocked',
|
||||
label: account,
|
||||
if (account) {
|
||||
const riskCheckLocalStorageKey = `risk-check-${account}`
|
||||
const now = Date.now()
|
||||
try {
|
||||
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://screening-worker.uniswap.workers.dev', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ address: account }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.block) {
|
||||
dispatch(setOpenModal(ApplicationModal.BLOCKED_ACCOUNT))
|
||||
sendEvent({
|
||||
category: 'Address Screening',
|
||||
action: 'blocked',
|
||||
label: account,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(setOpenModal(null))
|
||||
})
|
||||
.catch(() => dispatch(setOpenModal(null)))
|
||||
}
|
||||
} finally {
|
||||
localStorage.setItem(riskCheckLocalStorageKey, (now + ms`7 days`).toString())
|
||||
}
|
||||
}
|
||||
}, [account, dispatch])
|
||||
}
|
||||
|
||||
@@ -1,23 +1,6 @@
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import { Web3Provider } from '@ethersproject/providers'
|
||||
import { default as useWidgetsWeb3React } from 'lib/hooks/useActiveWeb3React'
|
||||
import { useWeb3React } from 'web3-react-core'
|
||||
|
||||
import { NetworkContextName } from '../constants/misc'
|
||||
// TODO(vm): Rm this file once #3759 is merged.
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
|
||||
export default function useActiveWeb3React() {
|
||||
if (process.env.REACT_APP_IS_WIDGET) {
|
||||
return useWidgetsWeb3React()
|
||||
}
|
||||
|
||||
const interfaceContext = useWeb3React<Web3Provider>()
|
||||
const interfaceNetworkContext = useWeb3React<Web3Provider>(
|
||||
process.env.REACT_APP_IS_WIDGET ? undefined : NetworkContextName
|
||||
)
|
||||
|
||||
if (interfaceContext.active) {
|
||||
return interfaceContext
|
||||
}
|
||||
|
||||
return interfaceNetworkContext
|
||||
return useWeb3React()
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user