Compare commits

...

27 Commits

Author SHA1 Message Date
Zach Pomerantz
2c6757ff62 fix: set content-type on cached document (#3990)
* fix: set content-type on cached document

* fix: delete old content-types

* fix: avoid immutable headers

* test: content-type

* fix: do not destructure response

* test: serve from cache with vercel

* fix: inject cache marker into body
2022-06-28 16:31:05 -07:00
Zach Pomerantz
0d03b09ae9 build: sleep for 10m after pinning (#3991) 2022-06-28 15:40:33 -07:00
Jordan Frankfurt
566da07448 feat(risk): cache risk check with ttl (#3965) 2022-06-24 11:11:32 -05:00
Vignesh Mohankumar
31a3840b1f feat: fix metamask mobile browser connection (#3964)
* fix metamask

* forceActivate

* remove forceActivate

* unused change
2022-06-23 16:50:04 -04:00
Vignesh Mohankumar
f89d7ccd5e feat: empty to deploy 628417f696 (#3962)
feat: empty to deploy
2022-06-23 12:29:46 -04:00
matteenm
628417f696 chore(deps): bump token-lists (#3929) 2022-06-21 15:52:09 -04:00
Kaylee George
ea8c7326d6 fix: crash on HOP token search (#3904) (#3928)
fix: ensure token address is checksummed on construction
2022-06-17 11:54:07 -04:00
Zach Pomerantz
dd5feaacb2 fix: serviceworker request path (#3926)
* fix: serviceworker request path

Always requests the app-shell from the same path as the cache key, in
order to guarantee that the etags will match should the cache be valid.

* fix: avoid returning redirects
2022-06-16 17:51:46 -04:00
Zach Pomerantz
cc919ab3df build: optimize github actions (#3922)
* build: parallelize cypress

- Parallelizes cypress CI runs
- Cleans up CI workflow files

* build: fix typo

* build: cache node_modules

* build: cache node_modules everywhere

* fix: action/cache usage

* fix: do not cache dynamically built files

* build: use standard container for cypress

* fix: cache cypress

* fix: cache cypress
2022-06-16 17:42:18 -04:00
Kaylee George
53d6eb0922 fix: show no price impact eth to weth (#3923)
* fix: show no price impact eth to weth

Fixed the price estimate values to reflect the correct price estimates depending on whether it is a wrapped trade

* fix: show no price impact eth to weth

Fixed to display the correct price estimates depending on whether it is a wrapped trade (eth -> weth should show no price impact)

* Added ETH->wETH testing

Added a Cypress test to check wrapped value swap has no price impact

* make Cypress test cleaner
2022-06-16 15:09:57 -04:00
Vignesh Mohankumar
db0d3cf3fa feat: upgrade to web3-react v8 (#3759)
* initial

* comment more stuff out for now

* more changes

* more temp

* remove walletconnect bug logic

* switch to provider not connector

* remove fortmatic

* remove some usage of network connector

* fix initialize connector

* more changes

* remove switch to network

* connect eagerly

* active -> isActive

* add initial option cards

* upgrade web3-react

* delete tryActivation

* delete pending view, reset option code

* fix hooks

* library -> provider

* rm getLibrary

* eagerly connect

* comment all this code for now

* add back app

* dont connect eagerly here

* deactivate

* switchToNetwork

* switch to useWeb3React

* rm Web3ReactManager

* add back og wallet modal code

* switch back to old option logic

* add account logic back

* add back more network switch logic

* Revert "switch to useWeb3React"

This reverts commit 08ac6319d4.

* add back skip disconnect logic

* check for network connector

* use promise.then again

* remove unnecessary pending error logic

* reset useAddTokenToMetamask

* upgrade packages

* use watchAsset

* add gnosis

* rm fortmatic

* close on disconnection

* add Wallet enum

* remove fortmatic imports

* add wallet state

* set/clear override wallet

* resolve empty

* remove some wallet modal view logic

* useWeb3ReactListener

* move to use effect

* add setwalletoverride in deactivate for now

* start to fix the wallet modal bug

* back button should open options

* connect eagerly to all

* Revert "add setwalletoverride in deactivate for now"

This reverts commit fbc990a924.

* useSelectedIsActive

* switch the enum to not be a bug

* actually dispatch the wallet override

* remove connection useEffect for now

* Revert "remove connection useEffect for now"

This reverts commit 0b92eee689.

* add back the activation useeffect

* handle resetting eagerly connecting

* dont disconnect from coinbase wallet

* disconnect except for coinbase wallet, bc their reload breaks things

* handle eager activation edge case

* backfill wallet override

* rename wrapper components

* update test

* network if override undefined

* npx deduplicate

* comment for why coinbase wallet special cased

* connectorPrevious -> previousConnector

* Array.find instead of forEach

* useState instead of useReducer

* add comments and simplify

* Web3Wrapper component

* add type guard

* check for watchAsset

* revert Option.tsx changes

* set -> updateWalletOverride

* generalize connector type usage

* rm comment

* eagerlyConnect comment

* null -> undefined

* add comment for wallet override

* add back pendingError logic

* merge conflicts

* remove provider dep

* add back connect a wallet

* move active prop out of base props

* add back account details test

* add type of isActiveMap

* add back eslint

* add TODO

* Web3Provider

* return null from Updater

* update comment

* integration tests initial

* try updating test

* check for gnosis safe

* fix gnosis safe check

* pr comments

* pr comments

* don't eagerly connect to any wallets other than gnosis or walletOverride

* remove unused branch

* pendingError from hook

* eslint-disable-line

* try connecting to wallets if not backfilled

* move eager connection logic

* remove connect eagerly set logic

* disconnect on change

* simplify ConnectorState

* better solution for changing wallet priority

* merge fixes

* fix tests

* try fixing test again

* add comment

* add fortmatic back

* set walletOverride for fortmatic

* hide other chains

* handle eager connection

* connect everything eagerly if not backfilled

* fix chain switching

* async

* rm error console

* fortmatic update

* log errors

* don't eagerly connect to fortmatic

* onSelectChain + switchChain

* typo

* don't disconnect from coinbase wallet for now

* upgrade web3-react

* close on disconnection/connection again

* simplify account change check

* comment fix

* comment

* fortmatic icon

* comment for fortmatic in network selector

* consolidate useEffect hooks in walletmodal for connection/disconnection

* switchToChain

* comment

* isEagerlyConnecting instead of eagerlyConnectingWallets

* update web3-react

* close modal fortmatic

* remove error log

* chainIdNotAllowed

* handle useToken

* update SupportedChainId

* move if statements around

* move to wallet reducer

* close as error

* export fix

* add back history change

* add back popular

* fortmatic key

* persist wallet

* remove eagerly connect

* call connect eagerly

* handle modal errors

* handle fortmatic close properly

* connector error changes

* go back to options

* change redux wallets

* simplify reducer

* fix eagerly connect / disconnect

* remove account change hook

* simplify connect eagerly

* remove unused var

* revert chain

* walletOverride reducer

* update web3-react

* fix compile errors for now

* show disconnect button

* clear pending connector

* clear error state

* add back skip toggle check

* MAINNET provider for ENS

* add coinbase wallet sdk

* fix test

* add back style but fix syntax highlighting

* dont create separate json rpc provider

* don't use selected hooks

* dont export

* dispatch first

* useConnectors

* comment

* simplify activeMap

* useIsActiveMap

* prettier

* prop change

* move comment

* useCallback

* coinbase wallet link fix

* rm ModalWallet type

* reportError

* isChainAllowed

* NETWORK_SELECTOR_CHAINS

* mainnet provider

* remove unused wallet views

* add back default case

* selected wallet

* comment change

* !chainAllowed

* rm ensResolver

* rm forEach

* re-define reportError

* move effects arounds

* change error message for switching chain

* simplify Web3Provider

* delete use isActive map

* fix test?

* rm disconnect test for now

* error message updates

* const -> function

* move fn

* undo changes for showing connect wallet state

* clear error before activating

* remove special case for fortmatic error

* backfillable/selectable wallets

* log wallet

* Revert "rm disconnect test for now"

This reverts commit 225bc7dc56.

* check if account exists

* unused dep

* remove reload piece of test

* update connect a wallet default state

* headerRow
2022-06-16 14:39:23 -04:00
Zach Pomerantz
ace4276bcb build: revert cypress projectId (#3921) 2022-06-16 10:08:25 -04:00
Zach Pomerantz
50a2dc9560 build: update cypress projectId (#3918)
* build: update cypress projectId

* build: rm unused cypress key

* test: save cypress videos

* build: rm start-server-and-test

* build: update github action cache usage

* build: use cypress action
2022-06-16 09:17:48 -04:00
Jordan Frankfurt
8cdec6188c chore(vscode): add workspace settings.json (#3914)
* chore(vscode): add workspace settings.json

* add newline

* no bad things allowed

* drop hiding . dirs
2022-06-14 17:39:12 -04:00
Kaylee George
5325d0cbe5 fix: adds messaging for unsupported V2 pool networks (#3762 #3777) (#3913)
Fix: Unsupported V2 Pool network messaging

#3762: Added error messaging for unsupported V2 pool networks (Polygon, Optimism, Arbitrum)
2022-06-14 16:20:31 -04:00
Zach Pomerantz
c16e49e774 feat: service worker with etag cache (#3897)
* fix: always-fresh service worker cache

* chore: clarify service-worker

* fix: cache in CacheStorage

* feat: set __isDocumentCached

* add back in manifest precaching

* add unit tests (incomplete)

* test: simplify test env

* test: add service-worker cypress test

* test: service-worker document handler

* fix: CachedDocument ctor

* fix: Readable for ReadableStream in jest

* build: clean up module loading

* fix: rename commands->ethereum

* build: simplify package.json deps

* build: clean up cypress usage

* build: clean up yarn.lock

* build: record cypress runs

* build: disable chromeWebSecurity in cypress tests

* build: rm babel

* build: disable sw in ci cypress

* build: nits

* build: update workbox version

* chore: fix merge

* test: cache

* test: cypress-ify the before hook

* test: clear sw before each test

* fix: cy then

* test: cypress shenanigans

* style: lint

* chore: rm todo

* test: fail fast for service worker with dev builds

* docs: update contributing to tests

* fix: clean up tests after merge

- Add fast fail in case of dev server, which lacks ServiceWorker

* fix: inject ethereum

* test: service worker

* test: increase sw timeout

* test: sw state

* test: run cypress in chrome

* feat: add on-demand caching to improve sw startup time

* test: test dynamically

* fix: simplify cached doc

* fix: optional sw

* fix: expose response on cached doc

* fix: stub out sw req

* fix: intercept

Co-authored-by: Christine Legge <christine.legge@uniswap.org>
2022-06-14 15:40:52 -04:00
Jordan Frankfurt
7e709e10db fix(L2): removes network-specific polling (#3912) 2022-06-14 14:51:24 -04:00
gzeon
7389b178fd perf: remove Arbitrum polling override (#3907) 2022-06-14 12:45:35 -04:00
Zach Pomerantz
48f8c6a141 test: update cypress (#3908)
* test: update cypress

* chore: comment on infura origin

* test: split build and serve

* chore: rm setupNodeEvents
2022-06-13 17:43:58 -04:00
Noah Zinsmeister
091876a374 feat: add Queue and Execute buttons (#3905)
* add queue and execute buttons

* eta is timestamp not block number

* address comments

* add execute text

* address comments
2022-06-13 13:23:15 -04:00
Lynn
d0e4aa832a fix: add jest coverage config to package.json (#3896)
* fix: add jest coverage config to package.json

* fix: add running test instructions to CONTRIBUTING.md

* fix: respond to zzmp comments

* fix: lower test thresholds

Co-authored-by: Lynn Yu <lynn.yu@uniswap.org>
2022-06-10 16:39:38 -04:00
Lynn
b17a38d94b feat: abstract analytics logging (#3892)
* fix: init commit

* fix: replace ReactGA.event with GoogleAnalyticsProvider.sentEvent

* fix: use GoogleAnalyticsProvider for all other ReactGA usages

* fix: add missing GoogleAnalyticsProvider import

* fix: incorporate zzmp's suggestions

* fix: add import I forgot

* fix: add another import I forgot

* fix: respond to zzmp comments

Co-authored-by: Lynn Yu <lynn.yu@uniswap.org>
2022-06-10 16:36:44 -04:00
Zach Pomerantz
22136b2708 build: clean up module loading (#3898)
* build: clean up module loading

* fix: rename commands->ethereum

* build: simplify package.json deps

* build: clean up cypress usage

* build: clean up yarn.lock

* build: record cypress runs

* build: disable chromeWebSecurity in cypress tests

* build: rm babel

* build: disable sw in ci cypress

* build: use dev env for e2e

* build: reenable web security
2022-06-09 14:39:38 -04:00
Zach Pomerantz
1897330ffc fix: omit native from DOM props (#3902)
fix: omit native from dom
2022-06-09 12:42:44 -04:00
Lynn
6131d0079f fix: fix scroll on page navigation (#3893)
* fix: fix scroll on page navigation

* fix: scroll to top when navigating in whole app

* fix: refactor to use useHistory hook

* fix: change scrollToTop component to hook

Co-authored-by: Lynn Yu <lynn.yu@uniswap.org>
2022-06-08 11:25:46 -04:00
Lynn
e6814994f6 fix: add keyboard accessibility to token selector (#3887)
* fix: add keyboard accessibility to token selector

* fix: update snapshot test

* fix: enable selected of suggested tokens by enter key

Co-authored-by: Lynn Yu <lynn.yu@uniswap.org>
2022-06-07 10:50:08 -04:00
Lynn
fea7d3a867 fix: remove animation from toggles on initial page load (#3886)
* fix: use simple toggle instead of toggle with text init commit

* fix: also change toggle in voting page and list toggle used in manage token list

* fix: simplify all toggle components into one configurable toggle

* fix: add ease-in animations for toggle

* fix: remove animation from toggle on initial page load to reduce thrash

Co-authored-by: Lynn Yu <lynn.yu@UNISWAP-MAC-015.local>
Co-authored-by: Lynn Yu <lynn.yu@uniswap.org>
2022-06-06 11:27:08 -04:00
127 changed files with 3293 additions and 2068 deletions

3
.env
View File

@@ -1,2 +1,3 @@
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
REACT_APP_LOCALES="locales"
REACT_APP_FORTMATIC_KEY="pk_live_357F77728B8EB880"
REACT_APP_LOCALES="locales"

View File

@@ -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,34 +13,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: Download Crowdin translations
uses: crowdin/github-action@1.4.9
with:
upload_sources: false

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -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
@@ -68,6 +73,8 @@ jobs:
uses: uniswap/convert-cidv0-cidv1@v1.0.0
with:
cidv0: ${{ steps.upload.outputs.hash }}
- run: sleep 600
- name: Update DNS with new IPFS hash
env:

103
.github/workflows/tests-e2e.yaml vendored Normal file
View 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
View 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

View File

@@ -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

7
.gitignore vendored
View File

@@ -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
@@ -37,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
View 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"
}
}

View File

@@ -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).

View File

@@ -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
View 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}',
},
})

View File

@@ -1,8 +0,0 @@
{
"projectId": "yp82ef",
"baseUrl": "http://localhost:3000",
"pluginsFile": false,
"supportFile": "cypress/support/index.js",
"video": false,
"defaultCommandTimeout": 10000
}

View File

@@ -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)
})
})

View File

@@ -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')

View 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 })
})
})

View File

@@ -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) => {

View 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')
})
})

View File

@@ -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
View 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()
})
})

View File

@@ -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)

View File

@@ -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'

View File

@@ -1,7 +1,6 @@
{
"compilerOptions": {
"strict": true,
"baseUrl": "../node_modules",
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress"]

View File

@@ -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,9 +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",
"@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",
@@ -76,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",
@@ -97,100 +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-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",
"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/**/*[!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",
"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'"
},
"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",
@@ -199,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"
}
}

View File

@@ -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} />
}

View File

@@ -5,11 +5,11 @@ 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'
@@ -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>

View File

@@ -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} />
}

View File

@@ -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() {

View File

@@ -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',
})

View File

@@ -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;
@@ -184,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
@@ -257,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)
@@ -271,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(() => {
@@ -312,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(() => {
@@ -323,7 +334,7 @@ export default function NetworkSelector() {
}
}, [chainId, history, urlChainId, urlChain])
if (!chainId || !info || !library) {
if (!chainId || !info || !provider) {
return null
}
@@ -340,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>
)}

View File

@@ -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}>

View File

@@ -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
}

View File

@@ -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 (

View File

@@ -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',
})

View File

@@ -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',
})

View File

@@ -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',
})

View File

@@ -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',
})

View File

@@ -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)}

View File

@@ -11,6 +11,7 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
<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
@@ -33,6 +34,7 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
<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
@@ -55,6 +57,7 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
<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

View File

@@ -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}

View File

@@ -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'
@@ -93,7 +93,7 @@ export function CurrencySearch({
useEffect(() => {
if (isAddressSearch) {
ReactGA.event({
sendEvent({
category: 'Currency Select',
action: 'Search by address',
label: isAddressSearch,

View File

@@ -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,

View File

@@ -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>

View File

@@ -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'
@@ -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,

View File

@@ -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',
})

View File

@@ -1,4 +1,5 @@
import { darken } from 'polished'
import { useState } from 'react'
import styled, { keyframes } from 'styled-components/macro'
const Wrapper = styled.button<{ isActive?: boolean; activeElement?: boolean }>`
@@ -45,8 +46,10 @@ const ToggleElementHoverStyle = (hasBgColor: boolean, theme: any, isActive?: boo
color: isActive ? theme.white : theme.text3,
}
const ToggleElement = styled.span<{ isActive?: boolean; bgColor?: string }>`
animation: 0.1s ${({ isActive }) => (isActive ? turnOnToggle : turnOffToggle)} ease-in;
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%;
@@ -67,9 +70,16 @@ interface ToggleProps {
}
export default function Toggle({ id, bgColor, isActive, toggle }: ToggleProps) {
const [isInitialToggleLoad, setIsInitialToggleLoad] = useState(true)
const switchToggle = () => {
toggle()
if (isInitialToggleLoad) setIsInitialToggleLoad(false)
}
return (
<Wrapper id={id} isActive={isActive} onClick={toggle}>
<ToggleElement isActive={isActive} bgColor={bgColor} />
<Wrapper id={id} isActive={isActive} onClick={switchToggle}>
<ToggleElement isActive={isActive} bgColor={bgColor} isInitialToggleLoad={isInitialToggleLoad} />
</Wrapper>
)
}

View File

@@ -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>

View File

@@ -1,8 +1,8 @@
import { Trans } from '@lingui/macro'
import { Connector } from '@web3-react/types'
import { ButtonEmpty, ButtonPrimary } from 'components/Button'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { AbstractConnector } from 'web3-react-abstract-connector'
import Loader from '../Loader'
@@ -49,15 +49,13 @@ const LoadingWrapper = styled.div`
export default function PendingView({
connector,
error = false,
setPendingError,
tryActivation,
resetAccountView,
openOptions,
}: {
connector?: AbstractConnector
connector: Connector
error?: boolean
setPendingError: (error: boolean) => void
tryActivation: (connector: AbstractConnector) => void
resetAccountView: () => void
tryActivation: (connector: Connector) => void
openOptions: () => void
}) {
return (
<PendingSection>
@@ -77,14 +75,13 @@ export default function PendingView({
$borderRadius="12px"
padding="12px"
onClick={() => {
setPendingError(false)
connector && tryActivation(connector)
tryActivation(connector)
}}
>
<Trans>Try Again</Trans>
</ButtonPrimary>
<ButtonEmpty width="fit-content" padding="0" marginTop={20}>
<ThemedText.Link fontSize={12} onClick={resetAccountView}>
<ThemedText.Link fontSize={12} onClick={openOptions}>
<Trans>Back to wallet selection</Trans>
</ThemedText.Link>
</ButtonEmpty>

View File

@@ -1,22 +1,21 @@
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 } from 'components/Row'
import { AutoRow } from 'components/Row'
import { useCallback, useEffect, useState } from 'react'
import { ArrowLeft } from 'react-feather'
import ReactGA from 'react-ga4'
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'
@@ -65,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;
@@ -111,10 +106,8 @@ const HoverText = styled.div`
const WALLET_VIEWS = {
OPTIONS: 'options',
OPTIONS_SECONDARY: 'options_secondary',
ACCOUNT: 'account',
PENDING: 'pending',
LEGAL: 'legal',
}
export default function WalletModal({
@@ -126,78 +119,68 @@ export default function WalletModal({
confirmedTransactions: string[] // hashes of confirmed
ENSName?: string
}) {
// important that these are destructed from the account-specific web3-react context
const { 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])
const resetAccountView = useCallback(() => {
setPendingError(false)
setWalletView(WALLET_VIEWS.ACCOUNT)
}, [setPendingError, 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) {
resetAccountView()
setWalletView(account ? WALLET_VIEWS.ACCOUNT : WALLET_VIEWS.OPTIONS)
}
}, [walletModalOpen, resetAccountView])
}, [walletModalOpen, setWalletView, account])
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)
// 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)
}
})
}
// close wallet modal if fortmatic modal is active
useEffect(() => {
fortmatic.on(OVERLAY_READY, () => {
toggleWalletModal()
})
}, [toggleWalletModal])
if (pendingConnector && walletView !== WALLET_VIEWS.PENDING) {
updateWalletError({ wallet: getWalletForConnector(pendingConnector), error: undefined })
setPendingConnector(undefined)
}
}, [pendingConnector, walletView])
const tryActivation = useCallback(
async (connector: Connector) => {
const wallet = getWalletForConnector(connector)
// log selected wallet
sendEvent({
category: 'Wallet',
action: 'Change Wallet',
label: wallet,
})
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() {
@@ -205,22 +188,30 @@ export default function WalletModal({
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}
/>
)
}
@@ -262,7 +253,7 @@ export default function WalletModal({
onClick={() => {
option.connector === connector
? setWalletView(WALLET_VIEWS.ACCOUNT)
: !option.href && tryActivation(option.connector)
: !option.href && option.connector && tryActivation(option.connector)
}}
color={'#E8831D'}
header={<Trans>Tally</Trans>}
@@ -280,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}
/>
)
)
@@ -300,93 +285,56 @@ 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={resetAccountView}>
<ArrowLeft />
</HoverText>
</HeaderRow>
) : (
<HeaderRow>
<HoverText>
<Trans>Connect a wallet</Trans>
</HoverText>
</HeaderRow>
)}
{headerRow}
<ContentWrapper>
<AutoColumn gap="16px">
{walletView === WALLET_VIEWS.PENDING && (
{walletView === WALLET_VIEWS.PENDING && pendingConnector && (
<PendingView
connector={pendingWallet}
error={pendingError}
setPendingError={setPendingError}
openOptions={openOptions}
connector={pendingConnector}
error={!!pendingError}
tryActivation={tryActivation}
resetAccountView={resetAccountView}
/>
)}
{walletView !== WALLET_VIEWS.PENDING && <OptionGrid>{getOptions()}</OptionGrid>}
{walletView !== WALLET_VIEWS.PENDING && <OptionGrid data-cy="option-grid">{getOptions()}</OptionGrid>}
{!pendingError && (
<LightCard>
<AutoRow style={{ flexWrap: 'nowrap' }}>

View 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>
}

View File

@@ -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
}

View File

@@ -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} />
</>
)
}

View 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)
}
}

View File

@@ -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
}

View File

@@ -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)
})
}, [])
}

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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',
})

View File

@@ -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>

View 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>
)
}

View 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>
)
}

View File

@@ -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} />

View File

@@ -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 }
}
}

View File

@@ -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}`)
}
}
}

View File

@@ -1 +0,0 @@
declare module 'formatic'

View File

@@ -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])
}

View File

@@ -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.
*/

View File

@@ -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
*/

View File

@@ -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',

View File

@@ -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
}
}
}

View File

@@ -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'
@@ -8,26 +9,34 @@ export default function useAccountRiskCheck(account: string | null | undefined)
useEffect(() => {
if (account) {
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,
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])
}

View File

@@ -1,18 +1,6 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { Web3Provider } from '@ethersproject/providers'
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() {
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()
}

View File

@@ -1,43 +0,0 @@
import { Currency, Token } from '@uniswap/sdk-core'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import useCurrencyLogoURIs from 'lib/hooks/useCurrencyLogoURIs'
import { useCallback, useState } from 'react'
export default function useAddTokenToMetamask(currencyToAdd: Currency | undefined): {
addToken: () => void
success: boolean | undefined
} {
const { library } = useActiveWeb3React()
const token: Token | undefined = currencyToAdd?.wrapped
const [success, setSuccess] = useState<boolean | undefined>()
const logoURL = useCurrencyLogoURIs(token)[0]
const addToken = useCallback(() => {
if (library && library?.provider?.isMetaMask && library.provider.request && token) {
library.provider
.request({
method: 'wallet_watchAsset',
params: {
//@ts-ignore // need this for incorrect ethers provider type
type: 'ERC20',
options: {
address: token.address,
symbol: token.symbol,
decimals: token.decimals,
image: logoURL,
},
},
})
.then((success) => {
setSuccess(success)
})
.catch(() => setSuccess(false))
} else {
setSuccess(false)
}
}, [library, logoURL, token])
return { addToken, success }
}

View File

@@ -48,21 +48,21 @@ export function useContract<T extends Contract = Contract>(
ABI: any,
withSignerIfPossible = true
): T | null {
const { library, account, chainId } = useActiveWeb3React()
const { provider, account, chainId } = useActiveWeb3React()
return useMemo(() => {
if (!addressOrAddressMap || !ABI || !library || !chainId) return null
if (!addressOrAddressMap || !ABI || !provider || !chainId) return null
let address: string | undefined
if (typeof addressOrAddressMap === 'string') address = addressOrAddressMap
else address = addressOrAddressMap[chainId]
if (!address) return null
try {
return getContract(address, ABI, library, withSignerIfPossible && account ? account : undefined)
return getContract(address, ABI, provider, withSignerIfPossible && account ? account : undefined)
} catch (error) {
console.error('Failed to get contract', error)
return null
}
}, [addressOrAddressMap, ABI, library, chainId, withSignerIfPossible, account]) as T
}, [addressOrAddressMap, ABI, provider, chainId, withSignerIfPossible, account]) as T
}
export function useV2MigratorContract() {

View File

@@ -126,7 +126,7 @@ export function useERC20Permit(
state: UseERC20PermitState
gatherPermitSignature: null | (() => Promise<void>)
} {
const { account, chainId, library } = useActiveWeb3React()
const { account, chainId, provider } = useActiveWeb3React()
const tokenAddress = currencyAmount?.currency?.isToken ? currencyAmount.currency.address : undefined
const eip2612Contract = useEIP2612Contract(tokenAddress)
const isArgentWallet = useIsArgentWallet()
@@ -145,7 +145,7 @@ export function useERC20Permit(
!account ||
!chainId ||
!transactionDeadline ||
!library ||
!provider ||
!tokenNonceState.valid ||
!tokenAddress ||
!spender ||
@@ -221,7 +221,7 @@ export function useERC20Permit(
message,
})
return library
return provider
.send('eth_signTypedData_v4', [account, data])
.then(splitSignature)
.then((signature) => {
@@ -248,7 +248,7 @@ export function useERC20Permit(
chainId,
isArgentWallet,
transactionDeadline,
library,
provider,
tokenNonceState.loading,
tokenNonceState.valid,
tokenNonceState.result,

View File

@@ -1,10 +1,10 @@
import { skipToken } from '@reduxjs/toolkit/query/react'
import { Currency, Token } from '@uniswap/sdk-core'
import { FeeAmount } from '@uniswap/v3-sdk'
import { sendEvent } from 'components/analytics'
import useBlockNumber from 'lib/hooks/useBlockNumber'
import ms from 'ms.macro'
import { useMemo } from 'react'
import ReactGA from 'react-ga4'
import { useFeeTierDistributionQuery } from 'state/data/enhanced'
import { FeeTierDistributionQuery } from 'state/data/generated'
@@ -112,7 +112,7 @@ function usePoolTVL(token0: Token | undefined, token1: Token | undefined) {
}
if (latestBlock - (_meta?.block?.number ?? 0) > MAX_DATA_BLOCK_AGE) {
ReactGA.event('exception', { description: `Graph stale (latest block: ${latestBlock})` })
sendEvent('exception', { description: `Graph stale (latest block: ${latestBlock})` })
return {
isLoading,

View File

@@ -1,39 +1,22 @@
import { nanoid } from '@reduxjs/toolkit'
import { TokenList } from '@uniswap/token-lists'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { MAINNET_PROVIDER } from 'constants/infura'
import getTokenList from 'lib/hooks/useTokenList/fetchTokenList'
import resolveENSContentHash from 'lib/utils/resolveENSContentHash'
import { useCallback } from 'react'
import { useAppDispatch } from 'state/hooks'
import { getNetworkLibrary } from '../connectors'
import { fetchTokenList } from '../state/lists/actions'
export function useFetchListCallback(): (listUrl: string, sendDispatch?: boolean) => Promise<TokenList> {
const { chainId, library } = useActiveWeb3React()
const dispatch = useAppDispatch()
const ensResolver = useCallback(
async (ensName: string) => {
if (!library || chainId !== 1) {
const networkLibrary = getNetworkLibrary()
const network = await networkLibrary.getNetwork()
if (networkLibrary && network.chainId === 1) {
return resolveENSContentHash(ensName, networkLibrary)
}
throw new Error('Could not construct mainnet ENS resolver')
}
return resolveENSContentHash(ensName, library)
},
[chainId, library]
)
// note: prevent dispatch if using for list search or unsupported list
return useCallback(
async (listUrl: string, sendDispatch = true) => {
const requestId = nanoid()
sendDispatch && dispatch(fetchTokenList.pending({ requestId, url: listUrl }))
return getTokenList(listUrl, ensResolver)
return getTokenList(listUrl, (ensName: string) => resolveENSContentHash(ensName, MAINNET_PROVIDER))
.then((tokenList) => {
sendDispatch && dispatch(fetchTokenList.fulfilled({ url: listUrl, tokenList, requestId }))
return tokenList
@@ -44,6 +27,6 @@ export function useFetchListCallback(): (listUrl: string, sendDispatch?: boolean
throw error
})
},
[dispatch, ensResolver]
[dispatch]
)
}

View File

@@ -1,9 +1,9 @@
import { sendEvent } from 'components/analytics'
import { SupportedLocale } from 'constants/locales'
import { LocationDescriptor } from 'history'
import useParsedQueryString from 'hooks/useParsedQueryString'
import { stringify } from 'qs'
import { useMemo } from 'react'
import ReactGA from 'react-ga4'
import { useLocation } from 'react-router-dom'
import { useActiveLocale } from './useActiveLocale'
@@ -26,7 +26,7 @@ export function useLocationLinkProps(locale: SupportedLocale | null): {
search: stringify({ ...qs, lng: locale }),
},
onClick: () => {
ReactGA.event({
sendEvent({
category: 'Localization',
action: 'Switch Locale',
label: `${activeLocale} -> ${locale}`,

View File

@@ -39,7 +39,7 @@ export function useSwapCallArguments(
deadline: BigNumber | undefined,
feeOptions: FeeOptions | undefined
): SwapCall[] {
const { account, chainId, library } = useActiveWeb3React()
const { account, chainId, provider } = useActiveWeb3React()
const { address: recipientAddress } = useENS(recipientAddressOrName)
const recipient = recipientAddressOrName === null ? account : recipientAddress
@@ -47,7 +47,7 @@ export function useSwapCallArguments(
const argentWalletContract = useArgentWalletContract()
return useMemo(() => {
if (!trade || !recipient || !library || !account || !chainId || !deadline) return []
if (!trade || !recipient || !provider || !account || !chainId || !deadline) return []
if (trade instanceof V2Trade) {
if (!routerContract) return []
@@ -175,7 +175,7 @@ export function useSwapCallArguments(
chainId,
deadline,
feeOptions,
library,
provider,
recipient,
routerContract,
signatureData,

View File

@@ -1,102 +0,0 @@
import type { EthereumProvider } from 'lib/ethereum'
import { useEffect, useState } from 'react'
import { useWeb3React } from 'web3-react-core'
import { gnosisSafe, injected } from '../connectors'
import { IS_IN_IFRAME } from '../constants/misc'
import { isMobile } from '../utils/userAgent'
export function useEagerConnect() {
const { activate, active } = useWeb3React()
const [tried, setTried] = useState(false)
// gnosisSafe.isSafeApp() races a timeout against postMessage, so it delays pageload if we are not in a safe app;
// if we are not embedded in an iframe, it is not worth checking
const [triedSafe, setTriedSafe] = useState(!IS_IN_IFRAME)
// first, try connecting to a gnosis safe
useEffect(() => {
if (!triedSafe) {
gnosisSafe.isSafeApp().then((loadedInSafe) => {
if (loadedInSafe) {
activate(gnosisSafe, undefined, true).catch(() => {
setTriedSafe(true)
})
} else {
setTriedSafe(true)
}
})
}
}, [activate, setTriedSafe, triedSafe])
// then, if that fails, try connecting to an injected connector
useEffect(() => {
if (!active && triedSafe) {
injected.isAuthorized().then((isAuthorized) => {
if (isAuthorized) {
activate(injected, undefined, true).catch(() => {
setTried(true)
})
} else {
if (isMobile && window.ethereum) {
activate(injected, undefined, true).catch(() => {
setTried(true)
})
} else {
setTried(true)
}
}
})
}
}, [activate, active, triedSafe])
// wait until we get confirmation of a connection to flip the flag
useEffect(() => {
if (active) {
setTried(true)
}
}, [active])
return tried
}
/**
* Use for network and injected - logs user in
* and out after checking what network theyre on
*/
export function useInactiveListener(suppress = false) {
const { active, error, activate } = useWeb3React()
useEffect(() => {
const ethereum = window.ethereum as EthereumProvider | undefined
if (ethereum && ethereum.on && !active && !error && !suppress) {
const handleChainChanged = () => {
// eat errors
activate(injected, undefined, true).catch((error) => {
console.error('Failed to activate after chain changed', error)
})
}
const handleAccountsChanged = (accounts: string[]) => {
if (accounts.length > 0) {
// eat errors
activate(injected, undefined, true).catch((error) => {
console.error('Failed to activate after accounts changed', error)
})
}
}
ethereum.on('chainChanged', handleChainChanged)
ethereum.on('accountsChanged', handleAccountsChanged)
return () => {
if (ethereum.removeListener) {
ethereum.removeListener('chainChanged', handleChainChanged)
ethereum.removeListener('accountsChanged', handleAccountsChanged)
}
}
}
return undefined
}, [active, error, suppress, activate])
}

View File

@@ -9,10 +9,9 @@ import { StrictMode } from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { HashRouter } from 'react-router-dom'
import { createWeb3ReactRoot, Web3ReactProvider } from 'web3-react-core'
import Blocklist from './components/Blocklist'
import { NetworkContextName } from './constants/misc'
import Web3Provider from './components/Web3Provider'
import { LanguageProvider } from './i18n'
import App from './pages/App'
import * as serviceWorkerRegistration from './serviceWorkerRegistration'
@@ -24,9 +23,6 @@ import TransactionUpdater from './state/transactions/updater'
import UserUpdater from './state/user/updater'
import ThemeProvider, { ThemedGlobalStyle } from './theme'
import RadialGradientByChainUpdater from './theme/RadialGradientByChainUpdater'
import getLibrary from './utils/getLibrary'
const Web3ProviderNetwork = createWeb3ReactRoot(NetworkContextName)
if (!!window.ethereum) {
window.ethereum.autoRefreshOnNetworkChange = false
@@ -51,19 +47,17 @@ ReactDOM.render(
<Provider store={store}>
<HashRouter>
<LanguageProvider>
<Web3ReactProvider getLibrary={getLibrary}>
<Web3ProviderNetwork getLibrary={getLibrary}>
<Blocklist>
<BlockNumberProvider>
<Updaters />
<ThemeProvider>
<ThemedGlobalStyle />
<App />
</ThemeProvider>
</BlockNumberProvider>
</Blocklist>
</Web3ProviderNetwork>
</Web3ReactProvider>
<Web3Provider>
<Blocklist>
<BlockNumberProvider>
<Updaters />
<ThemeProvider>
<ThemedGlobalStyle />
<App />
</ThemeProvider>
</BlockNumberProvider>
</Blocklist>
</Web3Provider>
</LanguageProvider>
</HashRouter>
</Provider>

View File

@@ -42,7 +42,7 @@ export function useSwapCallback({
deadline,
feeOptions,
}: UseSwapCallbackArgs): UseSwapCallbackReturns {
const { account, chainId, library } = useActiveWeb3React()
const { account, chainId, provider } = useActiveWeb3React()
const swapCalls = useSwapCallArguments(
trade,
@@ -52,13 +52,13 @@ export function useSwapCallback({
deadline,
feeOptions
)
const { callback } = useSendSwapTransaction(account, chainId, library, trade, swapCalls)
const { callback } = useSendSwapTransaction(account, chainId, provider, trade, swapCalls)
const { address: recipientAddress } = useENS(recipientAddressOrName)
const recipient = recipientAddressOrName === null ? account : recipientAddress
return useMemo(() => {
if (!trade || !library || !account || !chainId || !callback) {
if (!trade || !provider || !account || !chainId || !callback) {
return { state: SwapCallbackState.INVALID, error: <Trans>Missing dependencies</Trans> }
}
if (!recipient) {
@@ -73,5 +73,5 @@ export function useSwapCallback({
state: SwapCallbackState.VALID,
callback: async () => callback(),
}
}, [trade, library, account, chainId, callback, recipient, recipientAddressOrName])
}, [trade, provider, account, chainId, callback, recipient, recipientAddressOrName])
}

View File

@@ -45,18 +45,18 @@ interface UpdaterProps {
}
export default function Updater({ pendingTransactions, onCheck, onReceipt }: UpdaterProps): null {
const { chainId, library } = useActiveWeb3React()
const { chainId, provider } = useActiveWeb3React()
const lastBlockNumber = useBlockNumber()
const fastForwardBlockNumber = useFastForwardBlockNumber()
const getReceipt = useCallback(
(hash: string) => {
if (!library || !chainId) throw new Error('No library or chainId')
if (!provider || !chainId) throw new Error('No provider or chainId')
const retryOptions = RETRY_OPTIONS_BY_CHAIN_ID[chainId] ?? DEFAULT_RETRY_OPTIONS
return retry(
() =>
library.getTransactionReceipt(hash).then((receipt) => {
provider.getTransactionReceipt(hash).then((receipt) => {
if (receipt === null) {
console.debug(`Retrying tranasaction receipt for ${hash}`)
throw new RetryableError()
@@ -66,11 +66,11 @@ export default function Updater({ pendingTransactions, onCheck, onReceipt }: Upd
retryOptions
)
},
[chainId, library]
[chainId, provider]
)
useEffect(() => {
if (!chainId || !library || !lastBlockNumber) return
if (!chainId || !provider || !lastBlockNumber) return
const cancels = Object.keys(pendingTransactions)
.filter((hash) => shouldCheck(lastBlockNumber, pendingTransactions[hash]))
@@ -95,7 +95,7 @@ export default function Updater({ pendingTransactions, onCheck, onReceipt }: Upd
return () => {
cancels.forEach((cancel) => cancel())
}
}, [chainId, library, lastBlockNumber, getReceipt, fastForwardBlockNumber, onReceipt, onCheck, pendingTransactions])
}, [chainId, provider, lastBlockNumber, getReceipt, fastForwardBlockNumber, onReceipt, onCheck, pendingTransactions])
return null
}

View File

@@ -29,7 +29,7 @@ export function useFastForwardBlockNumber(): (block: number) => void {
}
export function BlockNumberProvider({ children }: { children: ReactNode }) {
const { chainId: activeChainId, library } = useActiveWeb3React()
const { chainId: activeChainId, provider } = useActiveWeb3React()
const [{ chainId, block }, setChainBlock] = useState<{ chainId?: number; block?: number }>({ chainId: activeChainId })
const onBlock = useCallback(
@@ -48,24 +48,24 @@ export function BlockNumberProvider({ children }: { children: ReactNode }) {
const windowVisible = useIsWindowVisible()
useEffect(() => {
if (library && activeChainId && windowVisible) {
if (provider && activeChainId && windowVisible) {
// If chainId hasn't changed, don't clear the block. This prevents re-fetching still valid data.
setChainBlock((chainBlock) => (chainBlock.chainId === activeChainId ? chainBlock : { chainId: activeChainId }))
library
provider
.getBlockNumber()
.then(onBlock)
.catch((error) => {
console.error(`Failed to get block number for chainId ${activeChainId}`, error)
})
library.on('block', onBlock)
provider.on('block', onBlock)
return () => {
library.removeListener('block', onBlock)
provider.removeListener('block', onBlock)
}
}
return undefined
}, [activeChainId, library, onBlock, setChainBlock, windowVisible])
}, [activeChainId, provider, onBlock, setChainBlock, windowVisible])
const value = useMemo(
() => ({

View File

@@ -6,6 +6,7 @@ import { useBytes32TokenContract, useTokenContract } from 'hooks/useContract'
import { NEVER_RELOAD, useSingleCallResult } from 'lib/hooks/multicall'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { useMemo } from 'react'
import { isChainAllowed } from 'utils/switchChain'
import { TOKEN_SHORTHANDS } from '../../constants/tokens'
import { isAddress } from '../../utils'
@@ -29,7 +30,8 @@ function parseStringOrBytes32(str: string | undefined, bytes32: string | undefin
* Returns undefined if tokenAddress is invalid or token does not exist.
*/
export function useTokenFromNetwork(tokenAddress: string | null | undefined): Token | null | undefined {
const { chainId } = useActiveWeb3React()
const { chainId, connector } = useActiveWeb3React()
const chainAllowed = chainId && isChainAllowed(connector, chainId)
const formattedAddress = isAddress(tokenAddress)
@@ -43,7 +45,7 @@ export function useTokenFromNetwork(tokenAddress: string | null | undefined): To
const decimals = useSingleCallResult(tokenContract, 'decimals', undefined, NEVER_RELOAD)
return useMemo(() => {
if (typeof tokenAddress !== 'string' || !chainId || !formattedAddress) return undefined
if (typeof tokenAddress !== 'string' || !chainAllowed || !formattedAddress) return undefined
if (decimals.loading || symbol.loading || tokenName.loading) return null
if (decimals.result) {
return new Token(
@@ -58,6 +60,7 @@ export function useTokenFromNetwork(tokenAddress: string | null | undefined): To
}, [
formattedAddress,
chainId,
chainAllowed,
decimals.loading,
decimals.result,
symbol.loading,
@@ -93,7 +96,7 @@ export function useTokenFromMapOrNetwork(tokens: TokenMap, tokenAddress?: string
*/
export function useCurrencyFromMap(tokens: TokenMap, currencyId?: string | null): Currency | null | undefined {
const nativeCurrency = useNativeCurrency()
const { chainId } = useActiveWeb3React()
const { chainId, connector } = useActiveWeb3React()
const isNative = Boolean(nativeCurrency && currencyId?.toUpperCase() === 'ETH')
const shorthandMatchAddress = useMemo(() => {
const chain = supportedChainId(chainId)
@@ -102,7 +105,8 @@ export function useCurrencyFromMap(tokens: TokenMap, currencyId?: string | null)
const token = useTokenFromMapOrNetwork(tokens, isNative ? undefined : shorthandMatchAddress ?? currencyId)
if (currencyId === null || currencyId === undefined) return currencyId
const chainAllowed = chainId && isChainAllowed(connector, chainId)
if (currencyId === null || currencyId === undefined || !chainAllowed) return null
// this case so we use our builtin wrapped token instead of wrapped tokens on token lists
const wrappedNative = nativeCurrency?.wrapped

View File

@@ -16,16 +16,20 @@ export function tokensToChainTokenMap(tokens: TokenList | TokenInfo[]): ChainTok
const [list, infos] = Array.isArray(tokens) ? [undefined, tokens] : [tokens, tokens.tokens]
const map = infos.reduce<Mutable<ChainTokenMap>>((map, info) => {
const token = new WrappedTokenInfo(info, list)
if (map[token.chainId]?.[token.address] !== undefined) {
console.warn(`Duplicate token skipped: ${token.address}`)
try {
const token = new WrappedTokenInfo(info, list)
if (map[token.chainId]?.[token.address] !== undefined) {
console.warn(`Duplicate token skipped: ${token.address}`)
return map
}
if (!map[token.chainId]) {
map[token.chainId] = {}
}
map[token.chainId][token.address] = { token, list }
return map
} catch {
return map
}
if (!map[token.chainId]) {
map[token.chainId] = {}
}
map[token.chainId][token.address] = { token, list }
return map
}, {}) as ChainTokenMap
mapCache?.set(tokens, map)
return map

View File

@@ -3,12 +3,12 @@ import { TransactionResponse } from '@ethersproject/providers'
import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core'
import { FeeAmount, NonfungiblePositionManager } from '@uniswap/v3-sdk'
import { sendEvent } from 'components/analytics'
import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import useParsedQueryString from 'hooks/useParsedQueryString'
import { useCallback, useContext, useEffect, useState } from 'react'
import { AlertTriangle } from 'react-feather'
import ReactGA from 'react-ga4'
import { RouteComponentProps } from 'react-router-dom'
import { Text } from 'rebass'
import {
@@ -81,7 +81,7 @@ export default function AddLiquidity({
},
history,
}: RouteComponentProps<{ currencyIdA?: string; currencyIdB?: string; feeAmount?: string; tokenId?: string }>) {
const { account, chainId, library } = useActiveWeb3React()
const { account, chainId, provider } = useActiveWeb3React()
const theme = useContext(ThemeContext)
const toggleWalletModal = useWalletModalToggle() // toggle wallet when disconnected
const expertMode = useIsExpertMode()
@@ -225,7 +225,7 @@ export default function AddLiquidity({
)
async function onAdd() {
if (!chainId || !library || !account) return
if (!chainId || !provider || !account) return
if (!positionManager || !baseCurrency || !quoteCurrency) {
return
@@ -281,7 +281,7 @@ export default function AddLiquidity({
setAttemptingTxn(true)
library
provider
.getSigner()
.estimateGas(txn)
.then((estimate) => {
@@ -290,7 +290,7 @@ export default function AddLiquidity({
gasLimit: calculateGasMargin(estimate),
}
return library
return provider
.getSigner()
.sendTransaction(newTxn)
.then((response: TransactionResponse) => {
@@ -305,7 +305,7 @@ export default function AddLiquidity({
feeAmount: position.pool.fee,
})
setTxHash(response.hash)
ReactGA.event({
sendEvent({
category: 'Liquidity',
action: 'Add',
label: [currencies[Field.CURRENCY_A]?.symbol, currencies[Field.CURRENCY_B]?.symbol].join('/'),

View File

@@ -2,12 +2,12 @@ import { BigNumber } from '@ethersproject/bignumber'
import { TransactionResponse } from '@ethersproject/providers'
import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core'
import { sendEvent } from 'components/analytics'
import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter'
import { SwitchLocaleLink } from 'components/SwitchLocaleLink'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useCallback, useContext, useState } from 'react'
import { Plus } from 'react-feather'
import ReactGA from 'react-ga4'
import { RouteComponentProps } from 'react-router-dom'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components/macro'
@@ -53,7 +53,7 @@ export default function AddLiquidity({
},
history,
}: RouteComponentProps<{ currencyIdA?: string; currencyIdB?: string }>) {
const { account, chainId, library } = useActiveWeb3React()
const { account, chainId, provider } = useActiveWeb3React()
const theme = useContext(ThemeContext)
@@ -137,7 +137,7 @@ export default function AddLiquidity({
const addTransaction = useTransactionAdder()
async function onAdd() {
if (!chainId || !library || !account || !router) return
if (!chainId || !provider || !account || !router) return
const { [Field.CURRENCY_A]: parsedAmountA, [Field.CURRENCY_B]: parsedAmountB } = parsedAmounts
if (!parsedAmountA || !parsedAmountB || !currencyA || !currencyB || !deadline) {
@@ -201,7 +201,7 @@ export default function AddLiquidity({
setTxHash(response.hash)
ReactGA.event({
sendEvent({
category: 'Liquidity',
action: 'Add',
label: [currencies[Field.CURRENCY_A]?.symbol, currencies[Field.CURRENCY_B]?.symbol].join('/'),

View File

@@ -2,15 +2,15 @@ import Loader from 'components/Loader'
import TopLevelModals from 'components/TopLevelModals'
import ApeModeQueryParamReader from 'hooks/useApeModeQueryParamReader'
import { lazy, Suspense } from 'react'
import { Redirect, Route, Switch } from 'react-router-dom'
import { useEffect } from 'react'
import { Redirect, Route, Switch, useHistory, useLocation } from 'react-router-dom'
import styled from 'styled-components/macro'
import GoogleAnalyticsReporter from '../components/analytics/GoogleAnalyticsReporter'
import { useAnalyticsReporter } from '../components/analytics'
import ErrorBoundary from '../components/ErrorBoundary'
import Header from '../components/Header'
import Polling from '../components/Header/Polling'
import Popups from '../components/Popups'
import Web3ReactManager from '../components/Web3ReactManager'
import DarkModeQueryParamReader from '../theme/DarkModeQueryParamReader'
import AddLiquidity from './AddLiquidity'
import { RedirectDuplicateTokenIds } from './AddLiquidity/redirects'
@@ -64,72 +64,76 @@ const Marginer = styled.div`
`
export default function App() {
const history = useHistory()
useAnalyticsReporter(useLocation())
useEffect(() => {
const unlisten = history.listen(() => {
window.scrollTo(0, 0)
})
return () => {
unlisten()
}
}, [history])
return (
<ErrorBoundary>
<Route component={GoogleAnalyticsReporter} />
<Route component={DarkModeQueryParamReader} />
<Route component={ApeModeQueryParamReader} />
<Web3ReactManager>
<AppWrapper>
<HeaderWrapper>
<Header />
</HeaderWrapper>
<BodyWrapper>
<Popups />
<Polling />
<TopLevelModals />
<Suspense fallback={<Loader />}>
<Switch>
<Route strict path="/vote" component={Vote} />
<Route exact strict path="/create-proposal">
<Redirect to="/vote/create-proposal" />
</Route>
<Route exact strict path="/claim" component={OpenClaimAddressModalAndRedirectToSwap} />
<Route exact strict path="/uni" component={Earn} />
<Route exact strict path="/uni/:currencyIdA/:currencyIdB" component={Manage} />
<AppWrapper>
<HeaderWrapper>
<Header />
</HeaderWrapper>
<BodyWrapper>
<Popups />
<Polling />
<TopLevelModals />
<Suspense fallback={<Loader />}>
<Switch>
<Route strict path="/vote" component={Vote} />
<Route exact strict path="/create-proposal">
<Redirect to="/vote/create-proposal" />
</Route>
<Route exact strict path="/claim" component={OpenClaimAddressModalAndRedirectToSwap} />
<Route exact strict path="/uni" component={Earn} />
<Route exact strict path="/uni/:currencyIdA/:currencyIdB" component={Manage} />
<Route exact strict path="/send" component={RedirectPathToSwapOnly} />
<Route exact strict path="/swap/:outputCurrency" component={RedirectToSwap} />
<Route exact strict path="/swap" component={Swap} />
<Route exact strict path="/send" component={RedirectPathToSwapOnly} />
<Route exact strict path="/swap/:outputCurrency" component={RedirectToSwap} />
<Route exact strict path="/swap" component={Swap} />
<Route exact strict path="/pool/v2/find" component={PoolFinder} />
<Route exact strict path="/pool/v2" component={PoolV2} />
<Route exact strict path="/pool" component={Pool} />
<Route exact strict path="/pool/:tokenId" component={PositionPage} />
<Route exact strict path="/pool/v2/find" component={PoolFinder} />
<Route exact strict path="/pool/v2" component={PoolV2} />
<Route exact strict path="/pool" component={Pool} />
<Route exact strict path="/pool/:tokenId" component={PositionPage} />
<Route
exact
strict
path="/add/v2/:currencyIdA?/:currencyIdB?"
component={RedirectDuplicateTokenIdsV2}
/>
<Route
exact
strict
path="/add/:currencyIdA?/:currencyIdB?/:feeAmount?"
component={RedirectDuplicateTokenIds}
/>
<Route exact strict path="/add/v2/:currencyIdA?/:currencyIdB?" component={RedirectDuplicateTokenIdsV2} />
<Route
exact
strict
path="/add/:currencyIdA?/:currencyIdB?/:feeAmount?"
component={RedirectDuplicateTokenIds}
/>
<Route
exact
strict
path="/increase/:currencyIdA?/:currencyIdB?/:feeAmount?/:tokenId?"
component={AddLiquidity}
/>
<Route
exact
strict
path="/increase/:currencyIdA?/:currencyIdB?/:feeAmount?/:tokenId?"
component={AddLiquidity}
/>
<Route exact strict path="/remove/v2/:currencyIdA/:currencyIdB" component={RemoveLiquidity} />
<Route exact strict path="/remove/:tokenId" component={RemoveLiquidityV3} />
<Route exact strict path="/remove/v2/:currencyIdA/:currencyIdB" component={RemoveLiquidity} />
<Route exact strict path="/remove/:tokenId" component={RemoveLiquidityV3} />
<Route exact strict path="/migrate/v2" component={MigrateV2} />
<Route exact strict path="/migrate/v2/:address" component={MigrateV2Pair} />
<Route exact strict path="/migrate/v2" component={MigrateV2} />
<Route exact strict path="/migrate/v2/:address" component={MigrateV2Pair} />
<Route component={RedirectPathToSwapOnly} />
</Switch>
</Suspense>
<Marginer />
</BodyWrapper>
</AppWrapper>
</Web3ReactManager>
<Route component={RedirectPathToSwapOnly} />
</Switch>
</Suspense>
<Marginer />
</BodyWrapper>
</AppWrapper>
</ErrorBoundary>
)
}

View File

@@ -3,6 +3,7 @@ import { TransactionResponse } from '@ethersproject/providers'
import { Trans } from '@lingui/macro'
import { CurrencyAmount, Fraction, Percent, Price, Token } from '@uniswap/sdk-core'
import { FeeAmount, Pool, Position, priceToClosestTick, TickMath } from '@uniswap/v3-sdk'
import { sendEvent } from 'components/analytics'
import Badge, { BadgeVariant } from 'components/Badge'
import { ButtonConfirmed } from 'components/Button'
import { BlueCard, DarkGreyCard, LightCard, YellowCard } from 'components/Card'
@@ -23,7 +24,6 @@ import JSBI from 'jsbi'
import { NEVER_RELOAD, useSingleCallResult } from 'lib/hooks/multicall'
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
import { AlertCircle, AlertTriangle, ArrowDown } from 'react-feather'
import ReactGA from 'react-ga4'
import { Redirect, RouteComponentProps } from 'react-router'
import { Text } from 'rebass'
import { useAppDispatch } from 'state/hooks'
@@ -336,7 +336,7 @@ function V2PairMigration({
return migrator
.multicall(data, { gasLimit: calculateGasMargin(gasEstimate) })
.then((response: TransactionResponse) => {
ReactGA.event({
sendEvent({
category: 'Migrate',
action: `${isNotUniswap ? 'SushiSwap' : 'V2'}->V3`,
label: `${currency0.symbol}/${currency1.symbol}`,

View File

@@ -3,6 +3,7 @@ import { TransactionResponse } from '@ethersproject/providers'
import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Fraction, Percent, Price, Token } from '@uniswap/sdk-core'
import { NonfungiblePositionManager, Pool, Position } from '@uniswap/v3-sdk'
import { sendEvent } from 'components/analytics'
import Badge from 'components/Badge'
import { ButtonConfirmed, ButtonGray, ButtonPrimary } from 'components/Button'
import { DarkCard, LightCard } from 'components/Card'
@@ -25,7 +26,6 @@ import { useV3PositionFromTokenId } from 'hooks/useV3Positions'
import { useSingleCallResult } from 'lib/hooks/multicall'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { useCallback, useMemo, useRef, useState } from 'react'
import ReactGA from 'react-ga4'
import { Link, RouteComponentProps } from 'react-router-dom'
import { Bound } from 'state/mint/v3/actions'
import { useIsTransactionPending, useTransactionAdder } from 'state/transactions/hooks'
@@ -318,7 +318,7 @@ export function PositionPage({
params: { tokenId: tokenIdFromUrl },
},
}: RouteComponentProps<{ tokenId?: string }>) {
const { chainId, account, library } = useActiveWeb3React()
const { chainId, account, provider } = useActiveWeb3React()
const theme = useTheme()
const parsedTokenId = tokenIdFromUrl ? BigNumber.from(tokenIdFromUrl) : undefined
@@ -433,7 +433,7 @@ export function PositionPage({
!positionManager ||
!account ||
!tokenId ||
!library
!provider
)
return
@@ -454,7 +454,7 @@ export function PositionPage({
value,
}
library
provider
.getSigner()
.estimateGas(txn)
.then((estimate) => {
@@ -463,14 +463,14 @@ export function PositionPage({
gasLimit: calculateGasMargin(estimate),
}
return library
return provider
.getSigner()
.sendTransaction(newTxn)
.then((response: TransactionResponse) => {
setCollectMigrationHash(response.hash)
setCollecting(false)
ReactGA.event({
sendEvent({
category: 'Liquidity',
action: 'CollectV3',
label: [currency0ForFeeCollectionPurposes.symbol, currency1ForFeeCollectionPurposes.symbol].join('/'),
@@ -497,7 +497,7 @@ export function PositionPage({
account,
tokenId,
addTransaction,
library,
provider,
])
const owner = useSingleCallResult(!!tokenId ? positionManager : null, 'ownerOf', [tokenId]).result?.[0]

View File

@@ -1,6 +1,6 @@
import { Trans } from '@lingui/macro'
import { Pair } from '@uniswap/v2-sdk'
import { L2_CHAIN_IDS } from 'constants/chains'
import { UNSUPPORTED_V2POOL_CHAIN_IDS } from 'constants/chains'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import JSBI from 'jsbi'
import { useContext, useMemo } from 'react'
@@ -85,9 +85,11 @@ const Layer2Prompt = styled(EmptyProposals)`
export default function Pool() {
const theme = useContext(ThemeContext)
const { account, chainId } = useActiveWeb3React()
const unsupportedV2Network = chainId && UNSUPPORTED_V2POOL_CHAIN_IDS.includes(chainId)
// fetch the user's balances of all tracked V2 LP tokens
const trackedTokenPairs = useTrackedTokenPairs()
let trackedTokenPairs = useTrackedTokenPairs()
if (unsupportedV2Network) trackedTokenPairs = []
const tokenPairsWithLiquidityTokens = useMemo(
() => trackedTokenPairs.map((tokens) => ({ liquidityToken: toV2LiquidityToken(tokens), tokens })),
[trackedTokenPairs]
@@ -132,8 +134,6 @@ export default function Pool() {
)
})
const ON_L2 = chainId && L2_CHAIN_IDS.includes(chainId)
return (
<>
<PageWrapper>
@@ -171,12 +171,12 @@ export default function Pool() {
<CardNoise />
</VoteCard>
{ON_L2 ? (
{unsupportedV2Network ? (
<AutoColumn gap="lg" justify="center">
<AutoColumn gap="md" style={{ width: '100%' }}>
<Layer2Prompt>
<ThemedText.Body color={theme.text3} textAlign="center">
<Trans>V2 is not available on Layer 2. Switch to Layer 1 Ethereum.</Trans>
<Trans>V2 Pool is not available on Layer 2. Switch to Layer 1 Ethereum.</Trans>
</ThemedText.Body>
</Layer2Prompt>
</AutoColumn>

View File

@@ -3,6 +3,7 @@ import { TransactionResponse } from '@ethersproject/providers'
import { Trans } from '@lingui/macro'
import { CurrencyAmount, Percent } from '@uniswap/sdk-core'
import { NonfungiblePositionManager } from '@uniswap/v3-sdk'
import { sendEvent } from 'components/analytics'
import RangeBadge from 'components/Badge/RangeBadge'
import { ButtonConfirmed, ButtonPrimary } from 'components/Button'
import { LightCard } from 'components/Card'
@@ -24,7 +25,6 @@ import useTransactionDeadline from 'hooks/useTransactionDeadline'
import { useV3PositionFromTokenId } from 'hooks/useV3Positions'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { useCallback, useMemo, useState } from 'react'
import ReactGA from 'react-ga4'
import { Redirect, RouteComponentProps } from 'react-router-dom'
import { Text } from 'rebass'
import { useBurnV3ActionHandlers, useBurnV3State, useDerivedV3BurnInfo } from 'state/burn/v3/hooks'
@@ -66,7 +66,7 @@ export default function RemoveLiquidityV3({
function Remove({ tokenId }: { tokenId: BigNumber }) {
const { position } = useV3PositionFromTokenId(tokenId)
const theme = useTheme()
const { account, chainId, library } = useActiveWeb3React()
const { account, chainId, provider } = useActiveWeb3React()
// flag for receiving WETH
const [receiveWETH, setReceiveWETH] = useState(false)
@@ -111,7 +111,7 @@ function Remove({ tokenId }: { tokenId: BigNumber }) {
!chainId ||
!positionSDK ||
!liquidityPercentage ||
!library
!provider
) {
return
}
@@ -136,7 +136,7 @@ function Remove({ tokenId }: { tokenId: BigNumber }) {
value,
}
library
provider
.getSigner()
.estimateGas(txn)
.then((estimate) => {
@@ -145,11 +145,11 @@ function Remove({ tokenId }: { tokenId: BigNumber }) {
gasLimit: calculateGasMargin(estimate),
}
return library
return provider
.getSigner()
.sendTransaction(newTxn)
.then((response: TransactionResponse) => {
ReactGA.event({
sendEvent({
category: 'Liquidity',
action: 'RemoveV3',
label: [liquidityValue0.currency.symbol, liquidityValue1.currency.symbol].join('/'),
@@ -180,7 +180,7 @@ function Remove({ tokenId }: { tokenId: BigNumber }) {
feeValue1,
positionSDK,
liquidityPercentage,
library,
provider,
tokenId,
allowedSlippage,
addTransaction,

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