Compare commits

...

72 Commits

Author SHA1 Message Date
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
Crowdin Bot
f4f0f29409 chore(i18n): synchronize translations from crowdin [skip ci] 2022-06-06 00:15:16 +00:00
Lynn
fa25e3c3e5 fix: use simple toggle instead of toggle with text init commit (#3884)
* 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

Co-authored-by: Lynn Yu <lynn.yu@UNISWAP-MAC-015.local>
Co-authored-by: Lynn Yu <lynn.yu@uniswap.org>
2022-06-05 19:13:05 -04:00
Crowdin Bot
51d2b3792f chore(i18n): synchronize translations from crowdin [skip ci] 2022-06-03 22:06:41 +00:00
Lynn
04ded04e74 fix: add animated loading bars and ensure sorted tokens upon load (#3874)
* fix: add animated loading bars and ensure sorted tokens upon load

* refactor: undo refactor of token selector component in CurrencySearch

* fix: fix styling as per design, still need to respond to other comments in review

* fix: add timeout to token loader of 2 seconds

* fix: add snapshot test and styling changes as per fred rec

* refactor: simplify function in currency list test

* fix: increase loading bars time from 2 seconds to 3 sec

* fix: respond to zach's comments

* fix: fix import errors

Co-authored-by: Lynn Yu <lynn.yu@UNISWAP-MAC-015.local>
2022-06-03 17:09:16 -04:00
Crowdin Bot
cf188a86de chore(i18n): synchronize translations from crowdin [skip ci] 2022-06-03 15:09:34 +00:00
hunter
f7e2435868 fix: Mobile layout: hide closed positions disappears #3344 (#3858)
* Add 'hide closed positions' to mobile pool view

* Update 'Hide closed positions' mobile font size

* add new toggle from design spec

* change off toggle button color

* Update SimpleToggle.tsx

* update wrapping position for toggle, improve component naming
2022-06-03 10:53:09 -04:00
Vignesh Mohankumar
5817d3bbdb style: simplify otherAmount calculation (#3880)
* simplify otherAmount calculation

* possibly not needed, but adding to keep logic the same
2022-06-01 14:22:38 -04:00
Vignesh Mohankumar
bdeb62ad64 fix: center NetworkSelector circles (#3867)
center NetworkSelector circles
2022-06-01 11:46:09 -04:00
Rachel-Eichenberger
e42effdbfd fix: Update graph link (#3876)
Update graph link
2022-06-01 11:34:37 -04:00
Clayton Lin
c0a5adf43d docs: remove unnecessary WalletModal comment (#3879)
fix: remove unnecessary comment
2022-06-01 11:25:48 -04:00
Clayton Lin
fd0489e654 feat: Suppress the account modal after connect (#3848)
fix: Suppress the account modal after connect
2022-06-01 11:01:11 -04:00
Crowdin Bot
caa0a2967d chore(i18n): synchronize translations from crowdin [skip ci] 2022-05-24 21:07:29 +00:00
Clayton Lin
11ec2333dd style: standardize fonts (#3844) 2022-05-23 13:48:04 -05:00
Tott0
251339a9ef feat: changes usdcPrice formatting to fixed decimals (#3849)
* change usdcPrice in swap modal to use fixed decimals instead of significant formatting

* change text for fiatValue as well

* change decimal points condition to 1.05

* (m) missed one value
2022-05-23 13:47:18 -05:00
Eugene Sokovikov
148e415fe8 fix: handle non existing inputCurrency from url #3056 (#3857)
- fix: handle non existing inputCurrency from url

oppening an url swap?inputCurrency=not_existing_address
cause a state where you are not able to change input currency
2022-05-23 08:59:49 -05:00
Jordan Frankfurt
fd964c5b74 feat(google-analytics): add tx hash to swap events (#3854) 2022-05-19 15:30:04 -05:00
Jordan Frankfurt
83b6eec271 fix(copy-icon): makes copy icon configurable for different uses (#3853) 2022-05-19 15:09:29 -05:00
Noah Zinsmeister
cd76fffbbe fix build warning 2022-05-17 10:53:48 -04:00
Noah Zinsmeister
2c0ac56296 Revert "fix build warning"
This reverts commit f836e3ca32.
2022-05-17 10:53:01 -04:00
Noah Zinsmeister
f836e3ca32 fix build warning 2022-05-17 10:48:29 -04:00
Crowdin Bot
1733fbb378 chore(i18n): synchronize translations from crowdin [skip ci] 2022-05-17 10:11:18 +00:00
Crowdin Bot
78142270a8 chore(i18n): synchronize translations from crowdin [skip ci] 2022-05-16 21:07:07 +00:00
dependabot[bot]
db4e2a9bee chore(deps-dev): bump @uniswap/default-token-list from 3.1.0 to 3.2.0 (#3755)
Bumps [@uniswap/default-token-list](https://github.com/Uniswap/default-token-list) from 3.1.0 to 3.2.0.
- [Release notes](https://github.com/Uniswap/default-token-list/releases)
- [Commits](https://github.com/Uniswap/default-token-list/compare/v3.1.0...v3.2.0)

---
updated-dependencies:
- dependency-name: "@uniswap/default-token-list"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-16 17:00:10 -04:00
Michael Wei
9cee94a473 fix: ignore dbg.json files during typechain (#3751) 2022-05-16 16:55:37 -04:00
Noah Zinsmeister
db5a14387f perf: constrain log-fetching block ranges (#3846)
* first pass

* don't re-fetch historical logs

* hide cancelled proposals by default
2022-05-16 16:45:27 -04:00
Crowdin Bot
5dc7d36669 chore(i18n): synchronize translations from crowdin [skip ci] 2022-05-13 23:06:42 +00:00
Tott0
a3cbe672c7 feat: changes wallet connect terms of service layout (#3835)
Reformat ToS text and remove unneeded links
2022-05-13 18:44:39 -04:00
DhruvJain1122
dc368ed7ac feat: Adding Tally Ho wallet with name & logo for Tally Ho Users (#3820)
* changes

* add tally
2022-05-13 18:38:35 -04:00
Crowdin Bot
b109248b4c chore(i18n): synchronize translations from crowdin [skip ci] 2022-05-12 18:11:13 +00:00
Vignesh Mohankumar
64cc6fb88c chore: initial PR template (#3832) 2022-05-12 08:43:56 -04:00
Crowdin Bot
74f6a4ef3f chore(i18n): synchronize translations from crowdin [skip ci] 2022-05-11 16:12:13 +00:00
Clayton Lin
f468001404 style: build new error connect state (#3831)
* style: build new error connect state

* use usecallback for resetAcountView

* remove fontSize props
2022-05-11 11:48:53 -04:00
Crowdin Bot
f26ec2ff1b chore(i18n): synchronize translations from crowdin [skip ci] 2022-05-10 21:06:48 +00:00
Zach Pomerantz
61d1036d28 chore: rm widget code (#3810)
* chore: rm widget tooling

* chore: rm widget components

* chore: rm widget theme

* chore: rm widget assets

* chore: rm widget business logic

* chore: rm widget meta

* chore: update yarn.lock

* chore: mv type to usage
2022-05-10 13:49:04 -07:00
Crowdin Bot
e11d2080a4 chore(i18n): synchronize translations from crowdin [skip ci] 2022-05-10 15:12:51 +00:00
Clayton Lin
da33423719 style: build new connecting pending state (#3825)
* style: build new connecting pending state

* use currentcolor rather than direct theme text1

* remove unnecessary margin usage
2022-05-10 10:18:51 -04:00
Simeon Kerkola
bd4545538d Chore: Use optional chaining (#3795)
Use optional chaining to check `window.ethereum` object chain.
2022-05-09 14:57:55 -05:00
Crowdin Bot
4274db67d5 chore(i18n): synchronize translations from crowdin [skip ci] 2022-05-09 19:06:52 +00:00
Clayton Lin
28498706cb style: add learn more about wallets link (#3821)
* web-91: add learn more about wallets link

* move externallink outside

* fix trans usage
2022-05-09 14:50:11 -04:00
Crowdin Bot
68c71a67dd chore(i18n): synchronize translations from crowdin [skip ci] 2022-05-09 17:18:03 +00:00
Moody Salem
fe195b63f7 chore: fix translations download CI 2022-05-09 12:05:19 -04:00
Noah Zinsmeister
86b85e25a5 fix proposal formatting 2022-05-09 10:34:25 -04:00
Jordan Frankfurt
0ea635ce15 chore: remove hostname check on risk screen (#3805) 2022-05-04 21:35:24 -05:00
Christine Legge
99ab581a87 refactor: migrate state/user to createSlice (#3779)
* use slice in transactions reducer

* update transaction reducer tests

* update user reducer to use slice

* fix merge conflicts
2022-05-02 15:37:44 -04:00
Will Hennessy
fc571d0f63 chore: update compliance email test (#3788) 2022-05-02 14:37:51 -04:00
David Mihal
2de43a8cdb feat: take tick range from URL (#3208)
* Take tick range from URL

* Keep minPrice/maxPrice in the URL
2022-05-02 12:10:27 -05:00
Jordan Frankfurt
5383436c88 feat(widgets): empty token list on network alert (#3627)
* feat(widgets): empty token list on network alert

* make it work

* pr review

* split dialog header out of tokenselect

* correctly filter token list case

* find -> some

* pr feedback

* clean up query hooks
2022-05-02 10:47:27 -05:00
Jordan Frankfurt
521f3aae04 chore(monitoring): trm cleanup (#3783)
* remove old monitoring code

* cleanup

* remove unneeded .then
2022-04-29 15:42:09 -05:00
0xlucius
9318c1204b feat: Add on-hover tooltips for tx details (#3178)
* Add on-hover tooltips for tx details

* Change tooltips to use <Trans> macro instead of t

* fix: remove info tooltip on transaction popup

* fix: update getting the nativeCurrencyVariable

* use getNativeCurrency() instead of chainInfo const

Co-authored-by: Christine Legge <christine.legge@uniswap.org>
2022-04-25 12:43:41 -04:00
Jordan Frankfurt
5055695b9b feat(optimism): update to new bridge app (#3771) 2022-04-25 10:44:00 -05:00
Christine Legge
ae8c0377de refactor: move state/transactions to createSlice (#3758)
* use slice in transactions reducer

* update transaction reducer tests

* chore: move state/transactions types into their own folder

* fix: fix broken transaction/reducer tests
2022-04-25 09:22:31 -04:00
Jordan Frankfurt
8eaf1f4964 feat(analytics): add a GA event on risk block (#3768)
* feat(analytics): add a GA event on risk block

* Update src/hooks/useAccountRiskCheck.ts

Co-authored-by: Will Hennessy <hennessywill@gmail.com>

Co-authored-by: Will Hennessy <hennessywill@gmail.com>
2022-04-21 21:44:34 -05:00
Zach Pomerantz
f717bf4a49 chore: bump to 1.0.7 (#3753) 2022-04-19 13:14:58 -04:00
Zach Pomerantz
dcbd4e475d chore: rm "with no slippage" (#3752) 2022-04-19 13:13:33 -04:00
Jordan Frankfurt
b704bdac94 feat(compliance): risk screening (#3714)
* feat(compliance): risk screening

* add api endpoint

* hosted app only

* add help center link and click-to-copy email address

* only show on app.uniswap.org and fix spacing nits

* 12px for bottom section
2022-04-19 10:12:28 -05:00
Zach Pomerantz
00d3df95c0 fix: rm console logs (#3743) 2022-04-15 15:19:34 -04:00
Ian Lapham
251b8b703a update list (#3737) 2022-04-15 15:19:11 -04:00
Zach Pomerantz
ef8432437d fix: missing token img (#3727) 2022-04-15 14:19:38 -04:00
281 changed files with 4777 additions and 19023 deletions

24
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,24 @@
Your PR title must follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary), and should start with one of the following [types](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#type):
- build: Changes that affect the build system or external dependencies (example scopes: yarn, eslint, typescript)
- ci: Changes to our CI configuration files and scripts (example scopes: vercel, github, cypress)
- docs: Documentation only changes
- feat: A new feature
- fix: A bug fix
- perf: A code change that improves performance
- refactor: A code change that neither fixes a bug nor adds a feature
- style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
- test: Adding missing tests or correcting existing tests
Example commit messages:
- feat: adds support for gnosis safe wallet
- fix: removes a polling memory leak
- chore: bumps redux version
Other things to note:
- Please describe the change using verb statements (ex: Removes X from Y)
- PRs with multiple changes should use a list of verb statements
- Add any relevant unit / integration tests
- Changes will be previewable via vercel. Non-obvious changes should include instructions for how to reproduce them

View File

@@ -1,40 +0,0 @@
name: Widgets
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up node
uses: actions/setup-node@v2
with:
node-version: 14
registry-url: https://registry.npmjs.org
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Build
run: yarn widgets:build

View File

@@ -42,7 +42,7 @@ jobs:
run: "yarn i18n:extract"
- name: Synchronize
uses: crowdin/github-action@1.1.0
uses: crowdin/github-action@1.4.9
with:
upload_sources: false
download_translations: true

View File

@@ -37,14 +37,13 @@ jobs:
- name: Install dependencies
run: yarn install --frozen-lockfile
- run: yarn cypress install
- run: yarn build
env:
CI: false
REACT_APP_SERVICE_WORKER: false
CI: false # disables lint checks when building
- run: yarn test:e2e
- run: yarn serve &
- run: yarn cypress:run --record
env:
CYPRESS_INTEGRATION_TEST_PRIVATE_KEY: ${{ secrets.CYPRESS_INTEGRATION_TEST_PRIVATE_KEY }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}

4
.gitignore vendored
View File

@@ -19,8 +19,6 @@
# builds
/build
/cosmos-export
/dist
/dts
# misc
@@ -39,8 +37,6 @@ yarn-error.log*
notes.txt
.idea/
.vscode/
package-lock.json
cypress/videos

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,45 +0,0 @@
# Uniswap Interface
An open source interface for Uniswap -- a protocol for decentralized exchange of Ethereum tokens.
- Website: [uniswap.org](https://uniswap.org/)
- Interface: [app.uniswap.org](https://app.uniswap.org)
- Docs: [uniswap.org/docs/](https://docs.uniswap.org/)
- Twitter: [@Uniswap](https://twitter.com/Uniswap)
- Reddit: [/r/Uniswap](https://www.reddit.com/r/Uniswap/)
- Email: [contact@uniswap.org](mailto:contact@uniswap.org)
- Discord: [Uniswap](https://discord.gg/FCfyBSbCU5)
- Whitepapers:
- [V1](https://hackmd.io/C-DvwDSfSxuh-Gd4WKE_ig)
- [V2](https://uniswap.org/whitepaper.pdf)
- [V3](https://uniswap.org/whitepaper-v3.pdf)
## Accessing the Uniswap Interface
To access the Uniswap Interface, use an IPFS gateway link from the
[latest release](https://github.com/Uniswap/uniswap-interface/releases/latest),
or visit [app.uniswap.org](https://app.uniswap.org).
## Unsupported tokens
Check out `useUnsupportedTokenList()` in [src/state/lists/hooks.ts](./src/state/lists/hooks.ts) for blocking tokens in your instance of the interface.
You can block an entire list of tokens by passing in a tokenlist like [here](./src/constants/lists.ts) or you can block specific tokens by adding them to [unsupported.tokenlist.json](./src/constants/tokenLists/unsupported.tokenlist.json).
## Contributions
For steps on local deployment, development, and code contribution, please see [CONTRIBUTING](./CONTRIBUTING.md).
## Accessing Uniswap V2
The Uniswap Interface supports swapping, adding liquidity, removing liquidity and migrating liquidity for Uniswap protocol V2.
- Swap on Uniswap V2: https://app.uniswap.org/#/swap?use=v2
- View V2 liquidity: https://app.uniswap.org/#/pool/v2
- Add V2 liquidity: https://app.uniswap.org/#/add/v2
- Migrate V2 liquidity to V3: https://app.uniswap.org/#/migrate/v2
## Accessing Uniswap V1
The Uniswap V1 interface for mainnet and testnets is accessible via IPFS gateways
linked from the [v1.0.0 release](https://github.com/Uniswap/uniswap-interface/releases/tag/v1.0.0).

View File

@@ -1,5 +1,3 @@
This repo is home to the Uniswap Widgets package and the web app interface [app.uniswap.org](https://app.uniswap.org).
# Uniswap Labs Interface
[![Unit Tests](https://github.com/Uniswap/interface/actions/workflows/unit-tests.yaml/badge.svg)](https://github.com/Uniswap/interface/actions/workflows/unit-tests.yaml)
@@ -8,14 +6,46 @@ This repo is home to the Uniswap Widgets package and the web app interface [app.
[![Release](https://github.com/Uniswap/interface/actions/workflows/release.yaml/badge.svg)](https://github.com/Uniswap/interface/actions/workflows/release.yaml)
[![Crowdin](https://badges.crowdin.net/uniswap-interface/localized.svg)](https://crowdin.com/project/uniswap-interface)
The web application hosted at https://app.uniswap.org is a convenient way to access the core functionality of the Uniswap Protocol.
An open source interface for Uniswap -- a protocol for decentralized exchange of Ethereum tokens.
For documentation of the interface including how to contribute or access prior builds, please view the README here: [INTERFACE_README.md](./INTERFACE_README.md)
- Website: [uniswap.org](https://uniswap.org/)
- Interface: [app.uniswap.org](https://app.uniswap.org)
- Docs: [uniswap.org/docs/](https://docs.uniswap.org/)
- Twitter: [@Uniswap](https://twitter.com/Uniswap)
- Reddit: [/r/Uniswap](https://www.reddit.com/r/Uniswap/)
- Email: [contact@uniswap.org](mailto:contact@uniswap.org)
- Discord: [Uniswap](https://discord.gg/FCfyBSbCU5)
- Whitepapers:
- [V1](https://hackmd.io/C-DvwDSfSxuh-Gd4WKE_ig)
- [V2](https://uniswap.org/whitepaper.pdf)
- [V3](https://uniswap.org/whitepaper-v3.pdf)
# Uniswap Labs Widgets
## Accessing the Uniswap Interface
The `@uniswap/widgets` package is an npm package of React components used to provide subsets of the Uniswap Protocol functionality in a small and configurable user interface element.
To access the Uniswap Interface, use an IPFS gateway link from the
[latest release](https://github.com/Uniswap/uniswap-interface/releases/latest),
or visit [app.uniswap.org](https://app.uniswap.org).
The npm package can be found here. [@uniswap/widgets](https://www.npmjs.com/package/@uniswap/widgets)
## Unsupported tokens
For documentation of the widgets package, please view the README here: [WIDGETS_README.md](./WIDGETS_README.md).
Check out `useUnsupportedTokenList()` in [src/state/lists/hooks.ts](./src/state/lists/hooks.ts) for blocking tokens in your instance of the interface.
You can block an entire list of tokens by passing in a tokenlist like [here](./src/constants/lists.ts) or you can block specific tokens by adding them to [unsupported.tokenlist.json](./src/constants/tokenLists/unsupported.tokenlist.json).
## Contributions
For steps on local deployment, development, and code contribution, please see [CONTRIBUTING](./CONTRIBUTING.md).
## Accessing Uniswap V2
The Uniswap Interface supports swapping, adding liquidity, removing liquidity and migrating liquidity for Uniswap protocol V2.
- Swap on Uniswap V2: https://app.uniswap.org/#/swap?use=v2
- View V2 liquidity: https://app.uniswap.org/#/pool/v2
- Add V2 liquidity: https://app.uniswap.org/#/add/v2
- Migrate V2 liquidity to V3: https://app.uniswap.org/#/migrate/v2
## Accessing Uniswap V1
The Uniswap V1 interface for mainnet and testnets is accessible via IPFS gateways
linked from the [v1.0.0 release](https://github.com/Uniswap/uniswap-interface/releases/tag/v1.0.0).

View File

@@ -1,40 +0,0 @@
# Uniswap Labs Swap Widget
The Swap Widget bundles the whole swapping experience into a single React component that developers can easily embed in their app with one line of code.
![swap widget screenshot](https://raw.githubusercontent.com/Uniswap/interface/main/src/assets/images/widget-screenshot.png)
You can customize the theme (colors, fonts, border radius, and more) to match the style of your application. You can also configure your own default token list and optionally set a convenience fee on swaps executed through the widget on your site.
## Installation
Install the widgets library via `npm` or `yarn`. If you do not already use the widget's peerDependencies `redux` and `react-redux`, then you'll need to add them as well.
```js
yarn add @uniswap/widgets redux react-redux
```
```js
npm i --save @uniswap/widgets redux react-redux
```
## Documentation
- [overview](https://docs.uniswap.org/sdk/widgets/swap-widget)
- [api reference](https://docs.uniswap.org/sdk/widgets/swap-widget/api)
## Example Apps
Uniswap Labs maintains two demo apps in branches of the [widgets-demo](https://github.com/Uniswap/widgets-demo) repo:
- [NextJS](https://github.com/Uniswap/widgets-demo/tree/nextjs)
- [Create React App](https://github.com/Uniswap/widgets-demo/tree/cra)
Others have also also released the widget in production to their userbase:
- [OpenSea](https://opensea.io/)
- [Friends With Benefits](https://www.fwb.help/)
- [Oasis](https://oasis.app/)
## Legal notice
Uniswap Labs encourages integrators to evaluate their own regulatory obligations when integrating this widget into their products, including, but not limited to, those related to economic or trade sanctions compliance.

View File

@@ -1,10 +0,0 @@
{
"watchDirs": [
"src"
],
"webpack": {
"configPath": "react-scripts/config/webpack.config",
"overridePath": "cosmos.override.cjs"
},
"port": 5001
}

View File

@@ -1,26 +0,0 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { DefinePlugin } = require('webpack')
// Renders the cosmos fixtures in isolation, instead of using public/index.html.
module.exports = (webpackConfig) => ({
...webpackConfig,
plugins: webpackConfig.plugins.map((plugin) => {
if (plugin instanceof HtmlWebpackPlugin) {
return new HtmlWebpackPlugin({
templateContent: '<body></body>',
})
}
if (plugin instanceof DefinePlugin) {
return new DefinePlugin({
...plugin.definitions,
'process.env': {
...plugin.definitions['process.env'],
REACT_APP_IS_WIDGET: true,
REACT_APP_LOCALES: '"../locales"',
},
})
}
return plugin
}),
})

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',
video: 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,4 +1,4 @@
import { TEST_ADDRESS_NEVER_USE_SHORTENED } from '../support/commands'
import { TEST_ADDRESS_NEVER_USE_SHORTENED } from '../support/ethereum'
describe('Landing Page', () => {
beforeEach(() => cy.visit('/'))

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

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

@@ -1,36 +1,62 @@
{
"name": "@uniswap/widgets",
"version": "1.0.6",
"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": {
"@ethersproject/experimental": "^5.4.0",
"@fontsource/ibm-plex-mono": "^4.5.1",
"@fontsource/inter": "^4.5.1",
"@gnosis.pm/safe-apps-web3-react": "^0.6.0",
"@graphql-codegen/cli": "1.21.5",
"@graphql-codegen/typescript": "1.22.3",
@@ -41,19 +67,11 @@
"@lingui/macro": "^3.9.0",
"@lingui/react": "^3.9.0",
"@metamask/jazzicon": "^2.0.0",
"@popperjs/core": "^2.4.4",
"@reach/dialog": "^0.10.3",
"@reach/portal": "^0.10.3",
"@react-hook/window-scroll": "^1.3.0",
"@rollup/plugin-alias": "^3.1.9",
"@rollup/plugin-babel": "^5.3.0",
"@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-eslint": "^8.0.1",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.1.3",
"@rollup/plugin-replace": "^3.0.1",
"@rollup/plugin-typescript": "^8.3.0",
"@rollup/plugin-url": "^6.1.0",
"@svgr/rollup": "^6.2.0",
"@reduxjs/toolkit": "^1.6.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@testing-library/react-hooks": "^7.0.2",
@@ -86,19 +104,32 @@
"@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.27",
"@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",
"@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/metamask": "^8.0.19-beta.0",
"@web3-react/types": "^8.0.12-beta.0",
"@web3-react/url": "^8.0.17-beta.0",
"@web3-react/walletconnect": "^8.0.26-beta.0",
"ajv": "^6.12.3",
"array.prototype.flat": "^1.2.4",
"array.prototype.flatmap": "^1.2.4",
"babel-plugin-macros": "^3.1.0",
"cids": "^1.0.0",
"copy-to-clipboard": "^3.2.0",
"cross-env": "^7.0.3",
"cypress": "^7.7.0",
"cypress": "^10.1.0",
"d3": "^7.0.0",
"env-cmd": "^10.1.0",
"eslint": "^7.11.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-better-styled-components": "^1.1.2",
@@ -107,112 +138,13 @@
"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",
"graphql": "^15.5.0",
"graphql-request": "^3.4.0",
"immer": "^9.0.6",
"inter-ui": "^3.13.1",
"jest-styled-components": "^7.0.5",
"polyfill-object.fromentries": "^1.0.1",
"prettier": "^2.2.1",
"qs": "^6.9.4",
"react": "^17.0.1",
"react-confetti": "^6.0.0",
"react-cosmos": "^5.6.6",
"react-dom": "^17.0.1",
"react-ga4": "^1.4.1",
"react-is": "^17.0.2",
"react-markdown": "^4.3.1",
"react-redux": "^7.2.2",
"react-router-dom": "^5.0.0",
"react-scripts": "^4.0.3",
"react-spring": "^8.0.27",
"react-use-gesture": "^6.0.14",
"redux": "^4.1.2",
"redux-localstorage-simple": "^2.3.1",
"rollup": "^2.63.0",
"rollup-plugin-copy": "^3.4.0",
"rollup-plugin-delete": "^2.0.0",
"rollup-plugin-dts": "^4.1.0",
"rollup-plugin-multi-input": "^1.3.1",
"rollup-plugin-node-externals": "^3.1.2",
"rollup-plugin-scss": "^3.0.0",
"rollup-plugin-typescript2": "^0.31.1",
"sass": "^1.45.1",
"serve": "^11.3.2",
"start-server-and-test": "^1.11.0",
"typechain": "^5.0.0",
"typescript": "^4.4.3",
"ua-parser-js": "^0.7.28",
"use-count-up": "^2.2.5",
"use-resize-observer": "^8.0.0",
"wcag-contrast": "^3.0.0",
"web-vitals": "^2.1.0",
"web3-react-abstract-connector": "npm:@web3-react/abstract-connector@^6.0.7",
"web3-react-fortmatic-connector": "npm:@web3-react/fortmatic-connector@^6.0.9",
"web3-react-injected-connector": "npm:@web3-react/injected-connector@^6.0.7",
"web3-react-types": "npm:@web3-react/types@^6.0.7",
"web3-react-walletconnect-connector": "npm:@web3-react/walletconnect-connector@^7.0.2-alpha.0",
"web3-react-walletlink-connector": "npm:@web3-react/walletlink-connector@^6.2.13",
"workbox-core": "^6.1.0",
"workbox-precaching": "^6.1.0",
"workbox-routing": "^6.1.0"
},
"resolutions": {
"@walletconnect/ethereum-provider": "1.7.1"
},
"scripts": {
"contracts:compile:abi": "typechain --target ethers-v5 --out-dir src/abis/types \"./src/abis/**/*.json\"",
"contracts:compile:v3": "typechain --target ethers-v5 --out-dir src/types/v3 \"./node_modules/@uniswap/**/artifacts/contracts/**/*.json\"",
"contracts:compile": "yarn contracts:compile:abi && yarn contracts:compile:v3",
"graphql:generate": "graphql-codegen --config codegen.yml",
"prei18n:extract": "touch src/locales/en-US.po",
"i18n:extract": "lingui extract --locale en-US",
"i18n:compile": "yarn i18n:extract && lingui compile",
"i18n:pseudo": "lingui extract --locale pseudo && lingui compile",
"prepare": "yarn contracts:compile && yarn graphql:generate && yarn i18n:compile",
"prepublishOnly": "yarn widgets:build",
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=./custom-test-env.cjs",
"test:e2e": "start-server-and-test 'serve build -l 3000' http://localhost:3000 'cypress run --record'",
"widgets:start": "cosmos",
"widgets:build": "rollup --config --failAfterWarnings --configPlugin typescript2"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"license": "GPL-3.0-or-later",
"dependencies": {
"@babel/runtime": "^7.17.0",
"@fontsource/ibm-plex-mono": "^4.5.1",
"@fontsource/inter": "^4.5.1",
"@popperjs/core": "^2.4.4",
"@reduxjs/toolkit": "^1.6.1",
"@uniswap/redux-multicall": "^1.1.1",
"@uniswap/router-sdk": "^1.0.3",
"@uniswap/sdk-core": "^3.0.1",
"@uniswap/smart-order-router": "^2.5.26",
"@uniswap/token-lists": "^1.0.0-beta.27",
"@uniswap/v2-sdk": "^3.0.1",
"@uniswap/v3-sdk": "^3.8.2",
"@web3-react/core": "^8.0.23-beta.0",
"@web3-react/eip1193": "^8.0.18-beta.0",
"@web3-react/empty": "^8.0.12-beta.0",
"@web3-react/types": "^8.0.12-beta.0",
"@web3-react/url": "^8.0.17-beta.0",
"ajv": "^6.12.3",
"cids": "^1.0.0",
"ethers": "^5.1.4",
"immer": "^9.0.6",
"jotai": "^1.3.7",
"jsbi": "^3.1.4",
"make-plural": "^7.0.0",
@@ -221,29 +153,55 @@
"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",
"start-server-and-test": "^1.11.0",
"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",
"web-vitals": "^2.1.0",
"web3-react-abstract-connector": "npm:@web3-react/abstract-connector@^6.0.7",
"web3-react-core": "npm:@web3-react/core@^6.0.9",
"wicg-inert": "^3.1.1"
"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",
"wicg-inert": "^3.1.1",
"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

@@ -1,182 +0,0 @@
/**
* Bundles the widgets library, which is released independently of the interface application.
* This library lives in src/lib, but shares code with the interface application.
*/
import alias from '@rollup/plugin-alias'
import babel from '@rollup/plugin-babel'
import commonjs from '@rollup/plugin-commonjs'
import json from '@rollup/plugin-json'
import resolve from '@rollup/plugin-node-resolve'
import replace from '@rollup/plugin-replace'
import typescript from '@rollup/plugin-typescript'
import url from '@rollup/plugin-url'
import svgr from '@svgr/rollup'
import path from 'path'
import { RollupWarning } from 'rollup'
import copy from 'rollup-plugin-copy'
import del from 'rollup-plugin-delete'
import dts from 'rollup-plugin-dts'
// @ts-ignore // missing types
import multi from 'rollup-plugin-multi-input'
import externals from 'rollup-plugin-node-externals'
import sass from 'rollup-plugin-scss'
import { CompilerOptions } from 'typescript'
const REPLACEMENTS = {
'process.env.REACT_APP_IS_WIDGET': true,
'process.env.REACT_APP_LOCALES': '"./locales"',
// esm requires fully-specified paths:
'react/jsx-runtime': 'react/jsx-runtime.js',
}
const EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx']
const ASSET_EXTENSIONS = ['.png', '.svg']
function isAsset(source: string) {
const extname = path.extname(source)
return extname && [...ASSET_EXTENSIONS, '.css', '.scss'].includes(extname)
}
function isEthers(source: string) {
// @ethersproject/* modules are provided by ethers, with the exception of experimental.
return source.startsWith('@ethersproject/') && !source.endsWith('experimental')
}
const TS_CONFIG = './tsconfig.lib.json'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { baseUrl, paths }: CompilerOptions = require(TS_CONFIG).compilerOptions
const aliases = Object.entries({ ...paths }).flatMap(([find, replacements]) => {
return replacements.map((replacement) => ({
find: path.dirname(find),
replacement: path.join(__dirname, baseUrl || '.', path.dirname(replacement)),
}))
})
const plugins = [
// Dependency resolution
resolve({ extensions: EXTENSIONS }), // resolves third-party modules within node_modules/
alias({ entries: aliases }), // resolves paths aliased through the tsconfig (babel does not use tsconfig path resolution)
// Source code transformation
replace({ ...REPLACEMENTS, preventAssignment: true }),
json(), // imports json as ES6; doing so enables type-checking and module resolution
]
const check = {
input: 'src/lib/index.tsx',
output: { file: 'dist/widgets.tsc', inlineDynamicImports: true },
external: (source: string) => isAsset(source) || isEthers(source),
plugins: [
externals({ exclude: ['constants'], deps: true, peerDeps: true }), // marks builtins, dependencies, and peerDependencies external
...plugins,
typescript({ tsconfig: TS_CONFIG }),
],
onwarn: squelchTranspilationWarnings, // this pipeline is only for typechecking and generating definitions
}
const type = {
input: 'dist/dts/lib/index.d.ts',
output: { file: 'dist/index.d.ts' },
external: (source: string) => isAsset(source) || isEthers(source),
plugins: [
externals({ exclude: ['constants'], deps: true, peerDeps: true }),
dts({ compilerOptions: { baseUrl: 'dist/dts' } }),
process.env.ROLLUP_WATCH ? undefined : del({ hook: 'buildEnd', targets: ['dist/widgets.tsc', 'dist/dts'] }),
],
}
/**
* This exports scheme works for nextjs and for CRA5.
*
* It will also work for CRA4 if you use direct imports:
* instead of `import { SwapWidget } from '@uniswap/widgets'`,
* `import { SwapWidget } from '@uniswap/widgets/dist/index.js'`.
* I do not know why CRA4 does not seem to use exports for resolution.
*
* Note that chunks are enabled. This is so the tokenlist spec can be loaded async,
* to improve first load time (due to ajv). Locales are also in separate chunks.
*
* Lastly, note that JSON and lingui are bundled into the library, as neither are fully
* supported/compatible with ES Modules. Both _could_ be bundled only with esm, but this
* yields a less complex pipeline.
*/
const transpile = {
input: 'src/lib/index.tsx',
output: [
{
dir: 'dist',
format: 'esm',
sourcemap: false,
},
{
dir: 'dist/cjs',
entryFileNames: '[name].cjs',
chunkFileNames: '[name]-[hash].cjs',
format: 'cjs',
sourcemap: false,
},
],
external: isEthers,
plugins: [
externals({
exclude: [
'constants',
/@lingui\/(core|react)/, // @lingui incorrectly exports esm, so it must be bundled in
/\.json$/, // esm does not support JSON loading, so it must be bundled in
],
deps: true,
peerDeps: true,
}),
...plugins,
// Source code transformation
url({ include: ASSET_EXTENSIONS.map((extname) => '**/*' + extname), limit: Infinity }), // imports assets as data URIs
svgr({ exportType: 'named', svgo: false }), // imports svgs as React components
sass({ output: 'dist/fonts.css' }), // generates fonts.css
commonjs(), // transforms cjs dependencies into tree-shakeable ES modules
babel({
babelHelpers: 'runtime',
presets: ['@babel/preset-env', ['@babel/preset-react', { runtime: 'automatic' }], '@babel/preset-typescript'],
extensions: EXTENSIONS,
plugins: [
'macros', // enables @lingui and styled-components macros
'@babel/plugin-transform-runtime', // embeds the babel runtime for library distribution
],
}),
],
onwarn: squelchTypeWarnings, // this pipeline is only for transpilation
}
const locales = {
input: 'src/locales/*.js',
output: [
{
dir: 'dist',
format: 'esm',
sourcemap: false,
},
],
plugins: [
copy({
copyOnce: true,
targets: [{ src: 'src/locales/*.js', dest: 'dist/cjs/locales', rename: (name) => `${name}.cjs` }],
}),
commonjs(),
multi(),
],
}
const config = [check, type, transpile, locales]
export default config
function squelchTranspilationWarnings(warning: RollupWarning, warn: (warning: RollupWarning) => void) {
if (warning.pluginCode === 'TS5055') return
warn(warning)
}
function squelchTypeWarnings(warning: RollupWarning, warn: (warning: RollupWarning) => void) {
if (warning.code === 'UNUSED_EXTERNAL_IMPORT') return
warn(warning)
}

BIN
src/assets/images/tally.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,49 +1,64 @@
import { Trans } from '@lingui/macro'
import React from 'react'
import useCopyClipboard from 'hooks/useCopyClipboard'
import React, { useCallback } from 'react'
import { CheckCircle, Copy } from 'react-feather'
import styled from 'styled-components/macro'
import useCopyClipboard from '../../hooks/useCopyClipboard'
import { LinkStyledButton } from '../../theme'
import { LinkStyledButton } from 'theme'
const CopyIcon = styled(LinkStyledButton)`
color: ${({ theme }) => theme.text3};
color: ${({ color, theme }) => color || theme.text3};
flex-shrink: 0;
display: flex;
text-decoration: none;
font-size: 0.825rem;
:hover,
:active,
:focus {
text-decoration: none;
color: ${({ theme }) => theme.text2};
color: ${({ color, theme }) => color || theme.text2};
}
`
const TransactionStatusText = styled.span`
const StyledText = styled.span`
margin-left: 0.25rem;
font-size: 0.825rem;
${({ theme }) => theme.flexRowNoWrap};
align-items: center;
`
export default function CopyHelper(props: { toCopy: string; children?: React.ReactNode }) {
const Copied = ({ iconSize }: { iconSize?: number }) => (
<StyledText>
<CheckCircle size={iconSize ?? '16'} />
<StyledText>
<Trans>Copied</Trans>
</StyledText>
</StyledText>
)
const Icon = ({ iconSize }: { iconSize?: number }) => (
<StyledText>
<Copy size={iconSize ?? '16'} />
</StyledText>
)
interface BaseProps {
toCopy: string
color?: string
iconSize?: number
iconPosition?: 'left' | 'right'
}
export type CopyHelperProps = BaseProps & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, keyof BaseProps>
export default function CopyHelper({ color, toCopy, children, iconSize, iconPosition }: CopyHelperProps) {
const [isCopied, setCopied] = useCopyClipboard()
const copy = useCallback(() => {
setCopied(toCopy)
}, [toCopy, setCopied])
return (
<CopyIcon onClick={() => setCopied(props.toCopy)}>
{isCopied ? (
<TransactionStatusText>
<CheckCircle size={'16'} />
<TransactionStatusText>
<Trans>Copied</Trans>
</TransactionStatusText>
</TransactionStatusText>
) : (
<TransactionStatusText>
<Copy size={'16'} />
</TransactionStatusText>
)}
{isCopied ? '' : props.children}
<CopyIcon onClick={copy} color={color}>
{iconPosition === 'left' ? isCopied ? <Copied iconSize={iconSize} /> : <Icon iconSize={iconSize} /> : null}
{iconPosition === 'left' && <>&nbsp;</>}
{isCopied ? '' : children}
{iconPosition === 'right' && <>&nbsp;</>}
{iconPosition === 'right' ? isCopied ? <Copied iconSize={iconSize} /> : <Icon iconSize={iconSize} /> : null}
</CopyIcon>
)
}

View File

@@ -17,7 +17,9 @@ import {
DepositLiquidityStakingTransactionInfo,
ExactInputSwapTransactionInfo,
ExactOutputSwapTransactionInfo,
ExecuteTransactionInfo,
MigrateV2LiquidityToV3TransactionInfo,
QueueTransactionInfo,
RemoveLiquidityV3TransactionInfo,
SubmitProposalTransactionInfo,
TransactionInfo,
@@ -25,7 +27,7 @@ import {
VoteTransactionInfo,
WithdrawLiquidityStakingTransactionInfo,
WrapTransactionInfo,
} from '../../state/transactions/actions'
} from '../../state/transactions/types'
function formatAmount(amountRaw: string, decimals: number, sigFigs: number): string {
return new Fraction(amountRaw, JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(decimals))).toSignificant(sigFigs)
@@ -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

@@ -1,5 +1,6 @@
import { Trans } from '@lingui/macro'
import { Connector } from '@web3-react/types'
import CopyHelper from 'components/AccountDetails/Copy'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useCallback, useContext } from 'react'
import { ExternalLink as LinkIcon } from 'react-feather'
@@ -10,14 +11,13 @@ import { AbstractConnector } from 'web3-react-abstract-connector'
import { ReactComponent as Close } from '../../assets/images/x.svg'
import { injected, walletlink } from '../../connectors'
import { SUPPORTED_WALLETS } from '../../constants/wallet'
import { clearAllTransactions } from '../../state/transactions/actions'
import { clearAllTransactions } from '../../state/transactions/reducer'
import { ExternalLink, LinkStyledButton, ThemedText } from '../../theme'
import { shortenAddress } from '../../utils'
import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink'
import { ButtonSecondary } from '../Button'
import StatusIcon from '../Identicon/StatusIcon'
import { AutoRow } from '../Row'
import Copy from './Copy'
import Transaction from './Transaction'
const HeaderRow = styled.div`
@@ -310,11 +310,11 @@ export default function AccountDetails({
<AccountControl>
<div>
{account && (
<Copy toCopy={account}>
<CopyHelper toCopy={account} iconPosition="left">
<span style={{ marginLeft: '4px' }}>
<Trans>Copy Address</Trans>
</span>
</Copy>
</CopyHelper>
)}
{chainId && account && (
<AddressLink
@@ -336,11 +336,11 @@ export default function AccountDetails({
<AccountControl>
<div>
{account && (
<Copy toCopy={account}>
<CopyHelper toCopy={account} iconPosition="left">
<span style={{ marginLeft: '4px' }}>
<Trans>Copy Address</Trans>
</span>
</Copy>
</CopyHelper>
)}
{chainId && account && (
<AddressLink

View File

@@ -2,7 +2,6 @@ import { Trans } from '@lingui/macro'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { ReactNode, useMemo } from 'react'
// SDN OFAC addresses
const BLOCKED_ADDRESSES: string[] = [
'0x7Db418b5D567A4e0E8c59Ad71BE1FcE48f3E6107',
'0x72a5843cc08275C8171E582972Aa4fDa8C397B2A',
@@ -24,16 +23,18 @@ const BLOCKED_ADDRESSES: string[] = [
'0x6acdfba02d390b97ac2b2d42a63e85293bcc1',
'0x48549a34ae37b12f6a30566245176994e17c6',
'0x5512d943ed1f7c8a43f3435c85f7ab68b30121',
'0xc455f7fd3e0e12afd51fba5c106909934d8a0e',
'0x3cbded43efdaf0fc77b9c55f6fc9988fcc9b757d',
'0xC455f7fd3e0e12afd51fba5c106909934D8A0e4a',
'0x3CBdeD43EFdAf0FC77b9C55F6fC9988fCC9b757d',
'0x67d40EE1A85bf4a4Bb7Ffae16De985e8427B6b45',
'0x6f1ca141a28907f78ebaa64fb83a9088b02a8352',
'0x6acdfba02d390b97ac2b2d42a63e85293bcc160e',
'0x6F1cA141A28907F78Ebaa64fb83A9088b02A8352',
'0x6aCDFBA02D390b97Ac2b2d42A63E85293BCc160e',
'0x48549a34ae37b12f6a30566245176994e17c6b4a',
'0x5512d943ed1f7c8a43f3435c85f7ab68b30121b0',
'0xc455f7fd3e0e12afd51fba5c106909934d8a0e4a',
'0xC455f7fd3e0e12afd51fba5c106909934D8A0e4a',
'0x629e7Da20197a5429d30da36E77d06CdF796b71A',
'0x7FF9cFad3877F21d41Da833E2F775dB0569eE3D9',
'0x098B716B8Aaf21512996dC57EB0615e2383E2f96',
'0xfEC8A60023265364D066a1212fDE3930F6Ae8da7',
]
export default function Blocklist({ children }: { children: ReactNode }) {

View File

@@ -0,0 +1,58 @@
import { Trans } from '@lingui/macro'
import CopyHelper from 'components/AccountDetails/Copy'
import Column from 'components/Column'
import useTheme from 'hooks/useTheme'
import { AlertOctagon } from 'react-feather'
import styled from 'styled-components/macro'
import { ExternalLink, ThemedText } from 'theme'
import Modal from '../Modal'
const ContentWrapper = styled(Column)`
align-items: center;
margin: 32px;
text-align: center;
`
const WarningIcon = styled(AlertOctagon)`
min-height: 22px;
min-width: 22px;
color: ${({ theme }) => theme.warning};
`
const Copy = styled(CopyHelper)`
font-size: 12px;
`
interface ConnectedAccountBlockedProps {
account: string | null | undefined
isOpen: boolean
}
export default function ConnectedAccountBlocked(props: ConnectedAccountBlockedProps) {
const theme = useTheme()
return (
<Modal isOpen={props.isOpen} onDismiss={Function.prototype()}>
<ContentWrapper>
<WarningIcon />
<ThemedText.LargeHeader lineHeight={2} marginBottom={1} marginTop={1}>
<Trans>Blocked Address</Trans>
</ThemedText.LargeHeader>
<ThemedText.DarkGray fontSize={12} marginBottom={12}>
{props.account}
</ThemedText.DarkGray>
<ThemedText.Main fontSize={14} marginBottom={12}>
<Trans>This address is blocked on the Uniswap Labs interface because it is associated with one or more</Trans>{' '}
<ExternalLink href="https://help.uniswap.org/en/articles/6149816">
<Trans>blocked activities</Trans>
</ExternalLink>
.
</ThemedText.Main>
<ThemedText.Main fontSize={12}>
<Trans>If you believe this is an error, please send an email including your address to </Trans>{' '}
</ThemedText.Main>
<Copy iconSize={12} toCopy="compliance@uniswap.org" color={theme.primary1} iconPosition="right">
compliance@uniswap.org
</Copy>
</ContentWrapper>
</Modal>
)
}

View File

@@ -27,13 +27,16 @@ export function FiatValue({
return theme.red1
}, [priceImpact, theme.green1, theme.red1, theme.text3, theme.yellow1])
const p = Number(fiatValue?.toFixed())
const visibleDecimalPlaces = p < 1.05 ? 4 : 2
return (
<ThemedText.Body fontSize={14} color={fiatValue ? theme.text3 : theme.text4}>
{fiatValue ? (
<Trans>
$
<HoverInlineText
text={fiatValue?.toSignificant(6, { groupSeparator: ',' })}
text={fiatValue?.toFixed(visibleDecimalPlaces, { groupSeparator: ',' })}
textColor={fiatValue ? theme.text3 : theme.text4}
/>
</Trans>

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

@@ -93,6 +93,13 @@ const FlyoutRowActiveIndicator = styled.div`
height: 9px;
width: 9px;
`
const CircleContainer = styled.div`
width: 20px;
display: flex;
justify-content: center;
`
const LinkOutCircle = styled(ArrowDownCircle)`
transform: rotate(230deg);
width: 16px;
@@ -146,7 +153,7 @@ const BridgeLabel = ({ chainId }: { chainId: SupportedChainId }) => {
return <Trans>Arbitrum Bridge</Trans>
case SupportedChainId.OPTIMISM:
case SupportedChainId.OPTIMISTIC_KOVAN:
return <Trans>Optimism Gateway</Trans>
return <Trans>Optimism Bridge</Trans>
case SupportedChainId.POLYGON:
case SupportedChainId.POLYGON_MUMBAI:
return <Trans>Polygon Bridge</Trans>
@@ -188,7 +195,11 @@ function Row({
<FlyoutRow onClick={() => onSelectChain(targetChain)} active={active}>
<Logo src={logoUrl} />
<NetworkLabel>{label}</NetworkLabel>
{chainId === targetChain && <FlyoutRowActiveIndicator />}
{chainId === targetChain && (
<CircleContainer>
<FlyoutRowActiveIndicator />
</CircleContainer>
)}
</FlyoutRow>
)
@@ -197,21 +208,30 @@ function Row({
<ActiveRowWrapper>
{rowContent}
<ActiveRowLinkList>
{bridge ? (
{bridge && (
<ExternalLink href={bridge}>
<BridgeLabel chainId={chainId} /> <LinkOutCircle />
<BridgeLabel chainId={chainId} />
<CircleContainer>
<LinkOutCircle />
</CircleContainer>
</ExternalLink>
) : null}
{explorer ? (
)}
{explorer && (
<ExternalLink href={explorer}>
<ExplorerLabel chainId={chainId} /> <LinkOutCircle />
<ExplorerLabel chainId={chainId} />
<CircleContainer>
<LinkOutCircle />
</CircleContainer>
</ExternalLink>
) : null}
{helpCenterUrl ? (
)}
{helpCenterUrl && (
<ExternalLink href={helpCenterUrl}>
<Trans>Help Center</Trans> <LinkOutCircle />
<Trans>Help Center</Trans>
<CircleContainer>
<LinkOutCircle />
</CircleContainer>
</ExternalLink>
) : null}
)}
</ActiveRowLinkList>
</ActiveRowWrapper>
)

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,6 +1,6 @@
import { Trans } from '@lingui/macro'
import { ButtonText } from 'components/Button'
import PositionListItem from 'components/PositionListItem'
import Toggle from 'components/Toggle'
import React from 'react'
import styled from 'styled-components/macro'
import { MEDIA_WIDTHS } from 'theme'
@@ -28,9 +28,37 @@ const MobileHeader = styled.div`
font-size: 16px;
font-weight: 500;
padding: 8px;
display: flex;
justify-content: space-between;
align-items: center;
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
display: none;
}
@media screen and (max-width: ${MEDIA_WIDTHS.upToExtraSmall}px) {
display: flex;
flex-direction: column;
align-items: start;
}
`
const ToggleWrap = styled.div`
display: flex;
flex-direction: row;
align-items: center;
`
const ToggleLabel = styled.div`
opacity: 0.6;
margin-right: 10px;
`
const MobileTogglePosition = styled.div`
@media screen and (max-width: ${MEDIA_WIDTHS.upToExtraSmall}px) {
position: absolute;
right: 20px;
}
`
type PositionListProps = React.PropsWithChildren<{
@@ -51,12 +79,35 @@ export default function PositionList({
<Trans>Your positions</Trans>
{positions && ' (' + positions.length + ')'}
</div>
<ButtonText style={{ opacity: 0.6 }} onClick={() => setUserHideClosedPositions(!userHideClosedPositions)}>
<Trans>Hide closed positions</Trans>
</ButtonText>
<ToggleWrap>
<ToggleLabel>
<Trans>Show closed positions</Trans>
</ToggleLabel>
<Toggle
id="desktop-hide-closed-positions"
isActive={!userHideClosedPositions}
toggle={() => {
setUserHideClosedPositions(!userHideClosedPositions)
}}
/>
</ToggleWrap>
</DesktopHeader>
<MobileHeader>
<Trans>Your positions</Trans>
<ToggleWrap>
<ToggleLabel>
<Trans>Show closed positions</Trans>
</ToggleLabel>
<MobileTogglePosition>
<Toggle
id="mobile-hide-closed-positions"
isActive={!userHideClosedPositions}
toggle={() => {
setUserHideClosedPositions(!userHideClosedPositions)
}}
/>
</MobileTogglePosition>
</ToggleWrap>
</MobileHeader>
{positions.map((p) => {
return <PositionListItem key={p.tokenId.toString()} positionDetails={p} />

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',
})
@@ -167,11 +167,13 @@ export function PrivacyPolicy() {
</AutoColumn>
</DarkGreyCard>
))}
<Row justify="center" marginBottom="1rem">
<ExternalLink href="https://help.uniswap.org/en/articles/5675203-terms-of-service-faq">
<Trans>Learn more</Trans>
</ExternalLink>
</Row>
<ThemedText.Body fontSize={12}>
<Row justify="center" marginBottom="1rem">
<ExternalLink href="https://help.uniswap.org/en/articles/5675203-terms-of-service-faq">
<Trans>Learn more</Trans>
</ExternalLink>
</Row>
</ThemedText.Body>
</AutoColumn>
</AutoColumn>
</Wrapper>

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

@@ -5,8 +5,8 @@ import Badge from 'components/Badge'
import CurrencyLogo from 'components/CurrencyLogo'
import DoubleCurrencyLogo from 'components/DoubleLogo'
import Row, { AutoRow } from 'components/Row'
import { RoutingDiagramEntry } from 'components/swap/SwapRoute'
import { useTokenInfoFromActiveList } from 'hooks/useTokenInfoFromActiveList'
import { RoutingDiagramEntry } from 'lib/components/Swap/RoutingDiagram/utils'
import { Box } from 'rebass'
import styled from 'styled-components/macro'
import { ThemedText, Z_INDEX } from 'theme'

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

@@ -0,0 +1,95 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders currency rows correctly when currencies list is non-empty 1`] = `
<DocumentFragment>
<div
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
>
<div
style="height: 168px; width: 100%;"
>
<div
class="sc-bdnxRM Row-sc-u7azg8-0 Row__RowBetween-sc-u7azg8-1 styleds__MenuItem-sc-muzgnq-3 lmTMKd hLLNig hzJkYd firMKT token-item-0x6B175474E89094C44Da98b954EedeAC495271d0F"
style="position: absolute; left: 0px; top: 0px; height: 56px; width: 100%;"
tabindex="0"
>
CurrencyLogo currency=DAI
<div
class="Column-sc-1r2yyln-0 cYEAJI"
>
<div
class="css-8mokm4"
title="Dai Stablecoin"
>
DAI
</div>
<div
class="theme__TextWrapper-sc-5lu8um-0 gVIOIC css-165qfk5"
>
Dai Stablecoin
</div>
</div>
<span />
</div>
<div
class="sc-bdnxRM Row-sc-u7azg8-0 Row__RowBetween-sc-u7azg8-1 styleds__MenuItem-sc-muzgnq-3 lmTMKd hLLNig hzJkYd firMKT token-item-0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
style="position: absolute; left: 0px; top: 56px; height: 56px; width: 100%;"
tabindex="0"
>
CurrencyLogo currency=USDC
<div
class="Column-sc-1r2yyln-0 cYEAJI"
>
<div
class="css-8mokm4"
title="USD//C"
>
USDC
</div>
<div
class="theme__TextWrapper-sc-5lu8um-0 gVIOIC css-165qfk5"
>
USD//C
</div>
</div>
<span />
</div>
<div
class="sc-bdnxRM Row-sc-u7azg8-0 Row__RowBetween-sc-u7azg8-1 styleds__MenuItem-sc-muzgnq-3 lmTMKd hLLNig hzJkYd firMKT token-item-0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
style="position: absolute; left: 0px; top: 112px; height: 56px; width: 100%;"
tabindex="0"
>
CurrencyLogo currency=WBTC
<div
class="Column-sc-1r2yyln-0 cYEAJI"
>
<div
class="css-8mokm4"
title="Wrapped BTC"
>
WBTC
</div>
<div
class="theme__TextWrapper-sc-5lu8um-0 gVIOIC css-165qfk5"
>
Wrapped BTC
</div>
</div>
<span />
</div>
</div>
</div>
</DocumentFragment>
`;
exports[`renders loading rows when isLoading is true 1`] = `
<DocumentFragment>
<div
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
>
<div
style="height: 0px; width: 100%;"
/>
</div>
</DocumentFragment>
`;

View File

@@ -0,0 +1,73 @@
import { Currency, CurrencyAmount as mockCurrencyAmount, Token as mockToken } from '@uniswap/sdk-core'
import { DAI, USDC_MAINNET, WBTC } from 'constants/tokens'
import * as mockJSBI from 'jsbi'
import { render } from 'test-utils'
import CurrencyList from '.'
const noOp = function () {
// do nothing
}
const mockCurrencyAmt = {
[DAI.address]: mockCurrencyAmount.fromRawAmount(DAI, mockJSBI.default.BigInt(100)),
[USDC_MAINNET.address]: mockCurrencyAmount.fromRawAmount(USDC_MAINNET, mockJSBI.default.BigInt(10)),
[WBTC.address]: mockCurrencyAmount.fromRawAmount(WBTC, mockJSBI.default.BigInt(1)),
}
jest.mock(
'components/CurrencyLogo',
() =>
({ currency }: { currency: Currency }) =>
`CurrencyLogo currency=${currency.symbol}`
)
jest.mock('hooks/useActiveWeb3React', () => {
return {
__esModule: true,
default: () => ({
account: '123',
active: true,
}),
}
})
jest.mock('../../../state/wallet/hooks', () => {
return {
useCurrencyBalance: (currency: Currency) => {
return mockCurrencyAmt[(currency as mockToken).address]
},
}
})
it('renders loading rows when isLoading is true', () => {
const { asFragment } = render(
<CurrencyList
height={10}
currencies={[]}
otherListTokens={[]}
selectedCurrency={null}
onCurrencySelect={noOp}
showImportView={noOp}
setImportToken={noOp}
isLoading={true}
/>
)
expect(asFragment()).toMatchSnapshot()
})
it('renders currency rows correctly when currencies list is non-empty', () => {
const { asFragment } = render(
<CurrencyList
height={10}
currencies={[DAI, USDC_MAINNET, WBTC]}
otherListTokens={[]}
selectedCurrency={null}
onCurrencySelect={noOp}
showImportView={noOp}
setImportToken={noOp}
isLoading={false}
/>
)
expect(asFragment()).toMatchSnapshot()
})

View File

@@ -9,20 +9,20 @@ import { FixedSizeList } from 'react-window'
import { Text } from 'rebass'
import styled from 'styled-components/macro'
import TokenListLogo from '../../assets/svg/tokenlist.svg'
import { useIsUserAddedToken } from '../../hooks/Tokens'
import { useCombinedActiveList } from '../../state/lists/hooks'
import { WrappedTokenInfo } from '../../state/lists/wrappedTokenInfo'
import { useCurrencyBalance } from '../../state/wallet/hooks'
import { ThemedText } from '../../theme'
import { isTokenOnList } from '../../utils'
import Column from '../Column'
import CurrencyLogo from '../CurrencyLogo'
import Loader from '../Loader'
import { RowBetween, RowFixed } from '../Row'
import { MouseoverTooltip } from '../Tooltip'
import ImportRow from './ImportRow'
import { MenuItem } from './styleds'
import TokenListLogo from '../../../assets/svg/tokenlist.svg'
import { useIsUserAddedToken } from '../../../hooks/Tokens'
import { useCombinedActiveList } from '../../../state/lists/hooks'
import { WrappedTokenInfo } from '../../../state/lists/wrappedTokenInfo'
import { useCurrencyBalance } from '../../../state/wallet/hooks'
import { ThemedText } from '../../../theme'
import { isTokenOnList } from '../../../utils'
import Column from '../../Column'
import CurrencyLogo from '../../CurrencyLogo'
import Loader from '../../Loader'
import { RowBetween, RowFixed } from '../../Row'
import { MouseoverTooltip } from '../../Tooltip'
import ImportRow from '../ImportRow'
import { LoadingRows, MenuItem } from '../styleds'
function currencyKey(currency: Currency): string {
return currency.isToken ? currency.address : 'ETHER'
@@ -124,8 +124,10 @@ function CurrencyRow({
// only show add or remove buttons if not on selected list
return (
<MenuItem
tabIndex={0}
style={style}
className={`token-item-${key}`}
onKeyPress={(e) => (!isSelected && e.key === 'Enter' ? onSelect() : null)}
onClick={() => (isSelected ? null : onSelect())}
disabled={isSelected}
selected={otherSelected}
@@ -195,6 +197,7 @@ export default function CurrencyList({
showImportView,
setImportToken,
showCurrencyAmount,
isLoading,
}: {
height: number
currencies: Currency[]
@@ -206,6 +209,7 @@ export default function CurrencyList({
showImportView: () => void
setImportToken: (token: Token) => void
showCurrencyAmount?: boolean
isLoading: boolean
}) {
const itemData: (Currency | BreakLine)[] = useMemo(() => {
if (otherListTokens && otherListTokens?.length > 0) {
@@ -232,7 +236,15 @@ export default function CurrencyList({
const showImport = index > currencies.length
if (showImport && token) {
if (isLoading) {
return (
<LoadingRows>
<div />
<div />
<div />
</LoadingRows>
)
} else if (showImport && token) {
return (
<ImportRow style={style} token={token} showImportView={showImportView} setImportToken={setImportToken} dim />
)
@@ -259,6 +271,7 @@ export default function CurrencyList({
setImportToken,
showImportView,
showCurrencyAmount,
isLoading,
]
)

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'
@@ -74,6 +74,8 @@ export function CurrencySearch({
const { chainId } = useActiveWeb3React()
const theme = useTheme()
const [tokenLoaderTimerElapsed, setTokenLoaderTimerElapsed] = useState(false)
// refs for fixed size lists
const fixedList = useRef<FixedSizeList>()
@@ -91,7 +93,7 @@ export function CurrencySearch({
useEffect(() => {
if (isAddressSearch) {
ReactGA.event({
sendEvent({
category: 'Currency Select',
action: 'Search by address',
label: isAddressSearch,
@@ -103,10 +105,11 @@ export function CurrencySearch({
return Object.values(allTokens).filter(getTokenFilter(debouncedQuery))
}, [allTokens, debouncedQuery])
const balances = useAllTokenBalances()
const [balances, balancesIsLoading] = useAllTokenBalances()
const sortedTokens: Token[] = useMemo(() => {
return filteredTokens.sort(tokenComparator.bind(null, balances))
}, [balances, filteredTokens])
void balancesIsLoading // creates a new array once balances load to update hooks
return [...filteredTokens].sort(tokenComparator.bind(null, balances))
}, [balances, filteredTokens, balancesIsLoading])
const filteredSortedTokens = useSortTokensByQuery(debouncedQuery, sortedTokens)
@@ -173,6 +176,14 @@ export function CurrencySearch({
filteredTokens.length === 0 || (debouncedQuery.length > 2 && !isAddressSearch) ? debouncedQuery : undefined
)
// Timeout token loader after 3 seconds to avoid hanging in a loading state.
useEffect(() => {
const tokenLoaderTimer = setTimeout(() => {
setTokenLoaderTimerElapsed(true)
}, 3000)
return () => clearTimeout(tokenLoaderTimer)
}, [])
return (
<ContentWrapper>
<PaddedColumn gap="16px">
@@ -218,6 +229,7 @@ export function CurrencySearch({
showImportView={showImportView}
setImportToken={setImportToken}
showCurrencyAmount={showCurrencyAmount}
isLoading={balancesIsLoading && !tokenLoaderTimerElapsed}
/>
)}
</AutoSizer>

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'
@@ -26,7 +26,7 @@ import { ButtonEmpty, ButtonPrimary } from '../Button'
import Column, { AutoColumn } from '../Column'
import ListLogo from '../ListLogo'
import Row, { RowBetween, RowFixed } from '../Row'
import ListToggle from '../Toggle/ListToggle'
import Toggle from '../Toggle'
import { CurrencyModalView } from './CurrencySearchModal'
import { PaddedColumn, SearchInput, Separator, SeparatorDark } from './styleds'
@@ -126,7 +126,7 @@ const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) {
const handleAcceptListUpdate = useCallback(() => {
if (!pending) return
ReactGA.event({
sendEvent({
category: 'Lists',
action: 'Update List from List Select',
label: listUrl,
@@ -135,13 +135,13 @@ const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) {
}, [dispatch, listUrl, pending])
const handleRemoveList = useCallback(() => {
ReactGA.event({
sendEvent({
category: 'Lists',
action: 'Start Remove List',
label: listUrl,
})
if (window.prompt(t`Please confirm you would like to remove this list by typing REMOVE`) === `REMOVE`) {
ReactGA.event({
sendEvent({
category: 'Lists',
action: 'Confirm Remove List',
label: listUrl,
@@ -151,7 +151,7 @@ const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) {
}, [dispatch, listUrl])
const handleEnableList = useCallback(() => {
ReactGA.event({
sendEvent({
category: 'Lists',
action: 'Enable List',
label: listUrl,
@@ -160,7 +160,7 @@ const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) {
}, [dispatch, listUrl])
const handleDisableList = useCallback(() => {
ReactGA.event({
sendEvent({
category: 'Lists',
action: 'Disable List',
label: listUrl,
@@ -215,7 +215,7 @@ const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) {
</StyledMenu>
</RowFixed>
</Column>
<ListToggle
<Toggle
isActive={isActive}
bgColor={listColor}
toggle={() => {

View File

@@ -1,3 +1,4 @@
import { LoadingRows as BaseLoadingRows } from 'components/Loader/styled'
import styled from 'styled-components/macro'
import { AutoColumn } from '../Column'
@@ -72,3 +73,25 @@ export const SeparatorDark = styled.div`
height: 1px;
background-color: ${({ theme }) => theme.bg3};
`
export const LoadingRows = styled(BaseLoadingRows)`
grid-column-gap: 0.5em;
grid-template-columns: repeat(12, 1fr);
max-width: 960px;
padding: 12px 20px;
& > div:nth-child(4n + 1) {
grid-column: 1 / 8;
height: 1em;
margin-bottom: 0.25em;
}
& > div:nth-child(4n + 2) {
grid-column: 12;
height: 1em;
margin-top: 0.25em;
}
& > div:nth-child(4n + 3) {
grid-column: 1 / 4;
height: 0.75em;
}
`

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,57 +0,0 @@
import { Trans } from '@lingui/macro'
import styled from 'styled-components/macro'
import { ThemedText } from '../../theme'
const Wrapper = styled.button<{ isActive?: boolean; activeElement?: boolean }>`
border-radius: 20px;
border: none;
background: ${({ theme }) => theme.bg1};
display: flex;
width: fit-content;
cursor: pointer;
outline: none;
padding: 0.4rem 0.4rem;
align-items: center;
`
const ToggleElement = styled.span<{ isActive?: boolean; bgColor?: string }>`
border-radius: 50%;
height: 24px;
width: 24px;
background-color: ${({ isActive, bgColor, theme }) => (isActive ? bgColor : theme.bg4)};
:hover {
opacity: 0.8;
}
`
const StatusText = styled(ThemedText.Main)<{ isActive?: boolean }>`
margin: 0 10px;
width: 24px;
color: ${({ theme, isActive }) => (isActive ? theme.text1 : theme.text3)};
`
interface ToggleProps {
id?: string
isActive: boolean
bgColor: string
toggle: () => void
}
export default function ListToggle({ id, isActive, bgColor, toggle }: ToggleProps) {
return (
<Wrapper id={id} isActive={isActive} onClick={toggle}>
{isActive && (
<StatusText fontWeight="600" margin="0 6px" isActive={true}>
<Trans>ON</Trans>
</StatusText>
)}
<ToggleElement isActive={isActive} bgColor={bgColor} />
{!isActive && (
<StatusText fontWeight="600" margin="0 6px" isActive={false}>
<Trans>OFF</Trans>
</StatusText>
)}
</Wrapper>
)
}

View File

@@ -1,57 +1,85 @@
import { Trans } from '@lingui/macro'
import { darken } from 'polished'
import { ReactNode } from 'react'
import styled from 'styled-components/macro'
import { useState } from 'react'
import styled, { keyframes } from 'styled-components/macro'
const ToggleElement = styled.span<{ isActive?: boolean; isOnSwitch?: boolean }>`
padding: 0.25rem 0.6rem;
border-radius: 9px;
background: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.primary1 : theme.bg4) : 'none')};
color: ${({ theme, isActive }) => (isActive ? theme.white : theme.text2)};
font-size: 14px;
font-weight: ${({ isOnSwitch }) => (isOnSwitch ? '500' : '400')};
:hover {
user-select: ${({ isOnSwitch }) => (isOnSwitch ? 'none' : 'initial')};
background: ${({ theme, isActive, isOnSwitch }) =>
isActive ? (isOnSwitch ? darken(0.05, theme.primary1) : darken(0.05, theme.bg4)) : 'none'};
color: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.white : theme.white) : theme.text3)};
const Wrapper = styled.button<{ isActive?: boolean; activeElement?: boolean }>`
align-items: center;
background: ${({ theme }) => theme.bg1};
border: none;
border-radius: 20px;
cursor: pointer;
display: flex;
outline: none;
padding: 0.4rem 0.4rem;
width: fit-content;
`
const turnOnToggle = keyframes`
from {
margin-left: 0em;
margin-right: 2.2em;
}
to {
margin-left: 2.2em;
margin-right: 0em;
}
`
const StyledToggle = styled.button<{ isActive?: boolean; activeElement?: boolean }>`
border-radius: 12px;
border: none;
background: ${({ theme }) => theme.bg0};
display: flex;
width: fit-content;
cursor: pointer;
outline: none;
padding: 2px;
const turnOffToggle = keyframes`
from {
margin-left: 2.2em;
margin-right: 0em;
}
to {
margin-left: 0em;
margin-right: 2.2em;
}
`
const ToggleElementHoverStyle = (hasBgColor: boolean, theme: any, isActive?: boolean) =>
hasBgColor
? {
opacity: '0.8',
}
: {
background: isActive ? darken(0.05, theme.primary1) : darken(0.05, theme.bg4),
color: isActive ? theme.white : theme.text3,
}
const ToggleElement = styled.span<{ isActive?: boolean; bgColor?: string; isInitialToggleLoad?: boolean }>`
animation: 0.1s
${({ isActive, isInitialToggleLoad }) => (isInitialToggleLoad ? 'none' : isActive ? turnOnToggle : turnOffToggle)}
ease-in;
background: ${({ theme, bgColor, isActive }) =>
isActive ? bgColor ?? theme.primary1 : !!bgColor ? theme.bg4 : theme.text3};
border-radius: 50%;
height: 24px;
:hover {
${({ bgColor, theme, isActive }) => ToggleElementHoverStyle(!!bgColor, theme, isActive)}
}
margin-left: ${({ isActive }) => (isActive ? '2.2em' : '0em')};
margin-right: ${({ isActive }) => (!isActive ? '2.2em' : '0em')};
width: 24px;
`
interface ToggleProps {
id?: string
bgColor?: string
isActive: boolean
toggle: () => void
checked?: ReactNode
unchecked?: ReactNode
}
export default function Toggle({
id,
isActive,
toggle,
checked = <Trans>On</Trans>,
unchecked = <Trans>Off</Trans>,
}: ToggleProps) {
export default function Toggle({ id, bgColor, isActive, toggle }: ToggleProps) {
const [isInitialToggleLoad, setIsInitialToggleLoad] = useState(true)
const switchToggle = () => {
toggle()
if (isInitialToggleLoad) setIsInitialToggleLoad(false)
}
return (
<StyledToggle id={id} isActive={isActive} onClick={toggle}>
<ToggleElement isActive={isActive} isOnSwitch={true}>
{checked}
</ToggleElement>
<ToggleElement isActive={!isActive} isOnSwitch={false}>
{unchecked}
</ToggleElement>
</StyledToggle>
<Wrapper id={id} isActive={isActive} onClick={switchToggle}>
<ToggleElement isActive={isActive} bgColor={bgColor} isInitialToggleLoad={isInitialToggleLoad} />
</Wrapper>
)
}

View File

@@ -18,6 +18,7 @@ export const TooltipContainer = styled.div`
interface TooltipProps extends Omit<PopoverProps, 'content'> {
text: ReactNode
disableHover?: boolean // disable the hover and content display
}
interface TooltipContentProps extends Omit<PopoverProps, 'content'> {
@@ -29,19 +30,20 @@ interface TooltipContentProps extends Omit<PopoverProps, 'content'> {
}
export default function Tooltip({ text, ...rest }: TooltipProps) {
return <Popover content={<TooltipContainer>{text}</TooltipContainer>} {...rest} />
return <Popover content={text && <TooltipContainer>{text}</TooltipContainer>} {...rest} />
}
function TooltipContent({ content, wrap = false, ...rest }: TooltipContentProps) {
return <Popover content={wrap ? <TooltipContainer>{content}</TooltipContainer> : content} {...rest} />
}
export function MouseoverTooltip({ children, ...rest }: Omit<TooltipProps, 'show'>) {
/** Standard text tooltip. */
export function MouseoverTooltip({ text, disableHover, children, ...rest }: Omit<TooltipProps, 'show'>) {
const [show, setShow] = useState(false)
const open = useCallback(() => setShow(true), [setShow])
const close = useCallback(() => setShow(false), [setShow])
return (
<Tooltip {...rest} show={show}>
<Tooltip {...rest} show={show} text={disableHover ? null : text}>
<div onMouseEnter={open} onMouseLeave={close}>
{children}
</div>
@@ -49,6 +51,7 @@ export function MouseoverTooltip({ children, ...rest }: Omit<TooltipProps, 'show
)
}
/** Tooltip that displays custom content. */
export function MouseoverTooltipContent({
content,
children,

View File

@@ -0,0 +1,23 @@
import AddressClaimModal from 'components/claim/AddressClaimModal'
import ConnectedAccountBlocked from 'components/ConnectedAccountBlocked'
import useAccountRiskCheck from 'hooks/useAccountRiskCheck'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useModalOpen, useToggleModal } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
export default function TopLevelModals() {
const addressClaimOpen = useModalOpen(ApplicationModal.ADDRESS_CLAIM)
const addressClaimToggle = useToggleModal(ApplicationModal.ADDRESS_CLAIM)
const blockedAccountModalOpen = useModalOpen(ApplicationModal.BLOCKED_ACCOUNT)
const { account } = useActiveWeb3React()
useAccountRiskCheck(account)
const open = Boolean(blockedAccountModalOpen && account)
return (
<>
<AddressClaimModal isOpen={addressClaimOpen} onDismiss={addressClaimToggle} />
<ConnectedAccountBlocked account={account} isOpen={open} />
</>
)
}

View File

@@ -1,12 +1,10 @@
import { Trans } from '@lingui/macro'
import { darken } from 'polished'
import { ButtonEmpty, ButtonPrimary } from 'components/Button'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { AbstractConnector } from 'web3-react-abstract-connector'
import { injected } from '../../connectors'
import { SUPPORTED_WALLETS } from '../../constants/wallet'
import Loader from '../Loader'
import Option from './Option'
const PendingSection = styled.div`
${({ theme }) => theme.flexColumnNoWrap};
@@ -18,18 +16,18 @@ const PendingSection = styled.div`
}
`
const StyledLoader = styled(Loader)`
margin-right: 1rem;
`
const LoadingMessage = styled.div<{ error?: boolean }>`
const LoaderContainer = styled.div`
margin: 16px 0;
${({ theme }) => theme.flexRowNoWrap};
align-items: center;
justify-content: flex-start;
justify-content: center;
`
const LoadingMessage = styled.div`
${({ theme }) => theme.flexRowNoWrap};
align-items: center;
justify-content: center;
border-radius: 12px;
margin-bottom: 20px;
color: ${({ theme, error }) => (error ? theme.red1 : 'inherit')};
border: 1px solid ${({ theme, error }) => (error ? theme.red1 : theme.text4)};
& > * {
padding: 1rem;
@@ -37,29 +35,13 @@ const LoadingMessage = styled.div<{ error?: boolean }>`
`
const ErrorGroup = styled.div`
${({ theme }) => theme.flexRowNoWrap};
${({ theme }) => theme.flexColumnNoWrap};
align-items: center;
justify-content: flex-start;
`
const ErrorButton = styled.div`
border-radius: 8px;
font-size: 12px;
color: ${({ theme }) => theme.text1};
background-color: ${({ theme }) => theme.bg4};
margin-left: 1rem;
padding: 0.5rem;
font-weight: 600;
user-select: none;
&:hover {
cursor: pointer;
background-color: ${({ theme }) => darken(0.1, theme.text4)};
}
`
const LoadingWrapper = styled.div`
${({ theme }) => theme.flexRowNoWrap};
${({ theme }) => theme.flexColumnNoWrap};
align-items: center;
justify-content: center;
`
@@ -69,65 +51,56 @@ export default function PendingView({
error = false,
setPendingError,
tryActivation,
resetAccountView,
}: {
connector?: AbstractConnector
error?: boolean
setPendingError: (error: boolean) => void
tryActivation: (connector: AbstractConnector) => void
resetAccountView: () => void
}) {
const isMetamask = window?.ethereum?.isMetaMask
return (
<PendingSection>
<LoadingMessage error={error}>
<LoadingMessage>
<LoadingWrapper>
{error ? (
<ErrorGroup>
<div>
<ThemedText.MediumHeader marginBottom={12}>
<Trans>Error connecting</Trans>
</div>
<ErrorButton
</ThemedText.MediumHeader>
<ThemedText.Body fontSize={14} marginBottom={36} textAlign="center">
<Trans>
The connection attempt failed. Please click try again and follow the steps to connect in your wallet.
</Trans>
</ThemedText.Body>
<ButtonPrimary
$borderRadius="12px"
padding="12px"
onClick={() => {
setPendingError(false)
connector && tryActivation(connector)
}}
>
<Trans>Try Again</Trans>
</ErrorButton>
</ButtonPrimary>
<ButtonEmpty width="fit-content" padding="0" marginTop={20}>
<ThemedText.Link fontSize={12} onClick={resetAccountView}>
<Trans>Back to wallet selection</Trans>
</ThemedText.Link>
</ButtonEmpty>
</ErrorGroup>
) : (
<>
<StyledLoader />
<Trans>Initializing...</Trans>
<ThemedText.Black fontSize={20} marginY={16}>
<LoaderContainer>
<Loader stroke="currentColor" size="32px" />
</LoaderContainer>
<Trans>Connecting...</Trans>
</ThemedText.Black>
</>
)}
</LoadingWrapper>
</LoadingMessage>
{Object.keys(SUPPORTED_WALLETS).map((key) => {
const option = SUPPORTED_WALLETS[key]
if (option.connector === connector) {
if (option.connector === injected) {
if (isMetamask && option.name !== 'MetaMask') {
return null
}
if (!isMetamask && option.name === 'MetaMask') {
return null
}
}
return (
<Option
id={`connect-${key}`}
key={key}
clickable={false}
color={option.color}
header={option.name}
subheader={option.description}
icon={option.iconURL}
/>
)
}
return null
})}
</PendingSection>
)
}

View File

@@ -1,17 +1,17 @@
import { Trans } from '@lingui/macro'
import { sendEvent } from 'components/analytics'
import { AutoColumn } from 'components/Column'
import { PrivacyPolicy } from 'components/PrivacyPolicy'
import Row, { AutoRow, RowBetween } from 'components/Row'
import { useWalletConnectMonitoringEventCallback } from 'hooks/useMonitoringEventCallback'
import { useEffect, useState } from 'react'
import { ArrowLeft, ArrowRight, Info } from 'react-feather'
import ReactGA from 'react-ga4'
import Row, { AutoRow } from 'components/Row'
import { useCallback, useEffect, useState } from 'react'
import { ArrowLeft } from 'react-feather'
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'
@@ -22,7 +22,7 @@ import { ApplicationModal } from '../../state/application/reducer'
import { ExternalLink, ThemedText } from '../../theme'
import { isMobile } from '../../utils/userAgent'
import AccountDetails from '../AccountDetails'
import Card, { LightCard } from '../Card'
import { LightCard } from '../Card'
import Modal from '../Modal'
import Option from './Option'
import PendingView from './PendingView'
@@ -109,16 +109,6 @@ const HoverText = styled.div`
}
`
const LinkCard = styled(Card)`
background-color: ${({ theme }) => theme.bg1};
color: ${({ theme }) => theme.text3};
:hover {
cursor: pointer;
filter: brightness(0.9);
}
`
const WALLET_VIEWS = {
OPTIONS: 'options',
OPTIONS_SECONDARY: 'options_secondary',
@@ -137,7 +127,7 @@ export default function WalletModal({
ENSName?: string
}) {
// important that these are destructed from the account-specific web3-react context
const { active, account, connector, activate, error } = useWeb3React()
const { account, connector, activate, error } = useWeb3React()
const [walletView, setWalletView] = useState(WALLET_VIEWS.ACCOUNT)
const previousWalletView = usePrevious(walletView)
@@ -151,7 +141,10 @@ export default function WalletModal({
const previousAccount = usePrevious(account)
const logMonitoringEvent = useWalletConnectMonitoringEventCallback()
const resetAccountView = useCallback(() => {
setPendingError(false)
setWalletView(WALLET_VIEWS.ACCOUNT)
}, [setPendingError, setWalletView])
// close on connection, when logged out before
useEffect(() => {
@@ -163,19 +156,9 @@ export default function WalletModal({
// always reset to account view
useEffect(() => {
if (walletModalOpen) {
setPendingError(false)
setWalletView(WALLET_VIEWS.ACCOUNT)
resetAccountView()
}
}, [walletModalOpen])
// close modal when a connection is successful
const activePrevious = usePrevious(active)
const connectorPrevious = usePrevious(connector)
useEffect(() => {
if (walletModalOpen && ((active && !activePrevious) || (connector && connector !== connectorPrevious && !error))) {
setWalletView(WALLET_VIEWS.ACCOUNT)
}
}, [setWalletView, active, error, connector, walletModalOpen, activePrevious, connectorPrevious])
}, [walletModalOpen, resetAccountView])
const tryActivation = async (connector: AbstractConnector | undefined) => {
let name = ''
@@ -186,7 +169,7 @@ export default function WalletModal({
return true
})
// log selected wallet
ReactGA.event({
sendEvent({
category: 'Wallet',
action: 'Change Wallet',
label: name,
@@ -200,18 +183,13 @@ export default function WalletModal({
}
connector &&
activate(connector, undefined, true)
.then(async () => {
const walletAddress = await connector.getAccount()
logMonitoringEvent({ walletAddress })
})
.catch((error) => {
if (error instanceof UnsupportedChainIdError) {
activate(connector) // a little janky...can't use setError because the connector isn't set
} else {
setPendingError(true)
}
})
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
@@ -223,7 +201,8 @@ export default function WalletModal({
// get wallets user can switch too, depending on device/browser
function getOptions() {
const isMetamask = window.ethereum && window.ethereum.isMetaMask
const isMetamask = !!window.ethereum?.isMetaMask
const isTally = !!window.ethereum?.isTally
return Object.keys(SUPPORTED_WALLETS).map((key) => {
const option = SUPPORTED_WALLETS[key]
// check for mobile options
@@ -275,6 +254,24 @@ export default function WalletModal({
// likewise for generic
else if (option.name === 'Injected' && isMetamask) {
return null
} else if (option.name === 'Injected' && isTally) {
return (
<Option
id={`connect-${key}`}
key={key}
onClick={() => {
option.connector === connector
? setWalletView(WALLET_VIEWS.ACCOUNT)
: !option.href && tryActivation(option.connector)
}}
color={'#E8831D'}
header={<Trans>Tally</Trans>}
active={option.connector === connector}
subheader={null}
link={null}
icon={TallyIcon}
/>
)
}
}
@@ -366,12 +363,7 @@ export default function WalletModal({
</CloseIcon>
{walletView !== WALLET_VIEWS.ACCOUNT ? (
<HeaderRow color="blue">
<HoverText
onClick={() => {
setPendingError(false)
setWalletView(WALLET_VIEWS.ACCOUNT)
}}
>
<HoverText onClick={resetAccountView}>
<ArrowLeft />
</HoverText>
</HeaderRow>
@@ -385,39 +377,38 @@ export default function WalletModal({
<ContentWrapper>
<AutoColumn gap="16px">
<LightCard>
<AutoRow style={{ flexWrap: 'nowrap' }}>
<ThemedText.Black fontSize={14}>
<Trans>
By connecting a wallet, you agree to Uniswap Labs{' '}
<ExternalLink href="https://uniswap.org/terms-of-service/">Terms of Service</ExternalLink> and
acknowledge that you have read and understand the Uniswap{' '}
<ExternalLink href="https://uniswap.org/disclaimer/">Protocol Disclaimer</ExternalLink>.
</Trans>
</ThemedText.Black>
</AutoRow>
</LightCard>
{walletView === WALLET_VIEWS.PENDING ? (
{walletView === WALLET_VIEWS.PENDING && (
<PendingView
connector={pendingWallet}
error={pendingError}
setPendingError={setPendingError}
tryActivation={tryActivation}
resetAccountView={resetAccountView}
/>
) : (
<OptionGrid>{getOptions()}</OptionGrid>
)}
<LinkCard padding=".5rem" $borderRadius=".75rem" onClick={() => setWalletView(WALLET_VIEWS.LEGAL)}>
<RowBetween>
<AutoRow gap="4px">
<Info size={20} />
<ThemedText.Label fontSize={14}>
<Trans>How this app uses APIs</Trans>
</ThemedText.Label>
{walletView !== WALLET_VIEWS.PENDING && <OptionGrid>{getOptions()}</OptionGrid>}
{!pendingError && (
<LightCard>
<AutoRow style={{ flexWrap: 'nowrap' }}>
<ThemedText.Body fontSize={12}>
<Trans>
By connecting a wallet, you agree to Uniswap Labs{' '}
<ExternalLink
style={{ textDecoration: 'underline' }}
href="https://uniswap.org/terms-of-service/"
>
Terms of Service
</ExternalLink>{' '}
and acknowledge that you have read and understand the Uniswap{' '}
<ExternalLink style={{ textDecoration: 'underline' }} href="https://uniswap.org/disclaimer/">
Protocol Disclaimer
</ExternalLink>
.
</Trans>
</ThemedText.Body>
</AutoRow>
<ArrowRight size={16} />
</RowBetween>
</LinkCard>
</LightCard>
)}
</AutoColumn>
</ContentWrapper>
</UpperSection>

View File

@@ -13,7 +13,7 @@ import useENSName from '../../hooks/useENSName'
import { useHasSocks } from '../../hooks/useSocksBalance'
import { useWalletModalToggle } from '../../state/application/hooks'
import { isTransactionRecent, useAllTransactions } from '../../state/transactions/hooks'
import { TransactionDetails } from '../../state/transactions/reducer'
import { TransactionDetails } from '../../state/transactions/types'
import { shortenAddress } from '../../utils'
import { ButtonSecondary } from '../Button'
import StatusIcon from '../Identicon/StatusIcon'

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

@@ -7,8 +7,8 @@ import styled from 'styled-components/macro'
import { useContract } from '../../hooks/useContract'
import { StakingInfo } from '../../state/stake/hooks'
import { TransactionType } from '../../state/transactions/actions'
import { useTransactionAdder } from '../../state/transactions/hooks'
import { TransactionType } from '../../state/transactions/types'
import { CloseIcon, ThemedText } from '../../theme'
import { ButtonError } from '../Button'
import { AutoColumn } from '../Column'

View File

@@ -12,8 +12,8 @@ import { ApprovalState, useApproveCallback } from '../../hooks/useApproveCallbac
import { useContract, usePairContract, useV2RouterContract } from '../../hooks/useContract'
import useTransactionDeadline from '../../hooks/useTransactionDeadline'
import { StakingInfo, useDerivedStakeInfo } from '../../state/stake/hooks'
import { TransactionType } from '../../state/transactions/actions'
import { useTransactionAdder } from '../../state/transactions/hooks'
import { TransactionType } from '../../state/transactions/types'
import { CloseIcon, ThemedText } from '../../theme'
import { formatCurrencyAmount } from '../../utils/formatCurrencyAmount'
import { maxAmountSpend } from '../../utils/maxAmountSpend'

View File

@@ -7,8 +7,8 @@ import styled from 'styled-components/macro'
import { useContract } from '../../hooks/useContract'
import { StakingInfo } from '../../state/stake/hooks'
import { TransactionType } from '../../state/transactions/actions'
import { useTransactionAdder } from '../../state/transactions/hooks'
import { TransactionType } from '../../state/transactions/types'
import { CloseIcon, ThemedText } from '../../theme'
import { ButtonError } from '../Button'
import { AutoColumn } from '../Column'
@@ -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

@@ -4,6 +4,7 @@ import Card from 'components/Card'
import { LoadingRows } from 'components/Loader/styled'
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { useContext, useMemo } from 'react'
import { InterfaceTrade } from 'state/routing/types'
import styled, { ThemeContext } from 'styled-components/macro'
@@ -12,6 +13,7 @@ import { Separator, ThemedText } from '../../theme'
import { computeRealizedLPFeePercent } from '../../utils/prices'
import { AutoColumn } from '../Column'
import { RowBetween, RowFixed } from '../Row'
import { MouseoverTooltip } from '../Tooltip'
import FormattedPriceImpact from './FormattedPriceImpact'
const StyledCard = styled(Card)`
@@ -23,6 +25,7 @@ interface AdvancedSwapDetailsProps {
allowedSlippage: Percent
syncing?: boolean
hideRouteDiagram?: boolean
hideInfoTooltips?: boolean
}
function TextWithLoadingPlaceholder({
@@ -43,9 +46,15 @@ function TextWithLoadingPlaceholder({
)
}
export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }: AdvancedSwapDetailsProps) {
export function AdvancedSwapDetails({
trade,
allowedSlippage,
syncing = false,
hideInfoTooltips = false,
}: AdvancedSwapDetailsProps) {
const theme = useContext(ThemeContext)
const { chainId } = useActiveWeb3React()
const nativeCurrency = useNativeCurrency()
const { expectedOutputAmount, priceImpact } = useMemo(() => {
if (!trade) return { expectedOutputAmount: undefined, priceImpact: undefined }
@@ -60,9 +69,19 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
<AutoColumn gap="8px">
<RowBetween>
<RowFixed>
<ThemedText.SubHeader color={theme.text1}>
<Trans>Expected Output</Trans>
</ThemedText.SubHeader>
<MouseoverTooltip
text={
<Trans>
The amount you expect to receive at the current market price. You may receive less or more if the
market price changes while your transaction is pending.
</Trans>
}
disableHover={hideInfoTooltips}
>
<ThemedText.SubHeader color={theme.text1}>
<Trans>Expected Output</Trans>
</ThemedText.SubHeader>
</MouseoverTooltip>
</RowFixed>
<TextWithLoadingPlaceholder syncing={syncing} width={65}>
<ThemedText.Black textAlign="right" fontSize={14}>
@@ -74,9 +93,14 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
</RowBetween>
<RowBetween>
<RowFixed>
<ThemedText.SubHeader color={theme.text1}>
<Trans>Price Impact</Trans>
</ThemedText.SubHeader>
<MouseoverTooltip
text={<Trans>The impact your trade has on the market price of this pool.</Trans>}
disableHover={hideInfoTooltips}
>
<ThemedText.SubHeader color={theme.text1}>
<Trans>Price Impact</Trans>
</ThemedText.SubHeader>
</MouseoverTooltip>
</RowFixed>
<TextWithLoadingPlaceholder syncing={syncing} width={50}>
<ThemedText.Black textAlign="right" fontSize={14}>
@@ -87,14 +111,24 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
<Separator />
<RowBetween>
<RowFixed style={{ marginRight: '20px' }}>
<ThemedText.SubHeader color={theme.text3}>
{trade.tradeType === TradeType.EXACT_INPUT ? (
<Trans>Minimum received</Trans>
) : (
<Trans>Maximum sent</Trans>
)}{' '}
<Trans>after slippage</Trans> ({allowedSlippage.toFixed(2)}%)
</ThemedText.SubHeader>
<MouseoverTooltip
text={
<Trans>
The minimum amount you are guaranteed to receive. If the price slips any further, your transaction
will revert.
</Trans>
}
disableHover={hideInfoTooltips}
>
<ThemedText.SubHeader color={theme.text3}>
{trade.tradeType === TradeType.EXACT_INPUT ? (
<Trans>Minimum received</Trans>
) : (
<Trans>Maximum sent</Trans>
)}{' '}
<Trans>after slippage</Trans> ({allowedSlippage.toFixed(2)}%)
</ThemedText.SubHeader>
</MouseoverTooltip>
</RowFixed>
<TextWithLoadingPlaceholder syncing={syncing} width={70}>
<ThemedText.Black textAlign="right" fontSize={14} color={theme.text3}>
@@ -106,9 +140,18 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
</RowBetween>
{!trade?.gasUseEstimateUSD || !chainId || !SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) ? null : (
<RowBetween>
<ThemedText.SubHeader color={theme.text3}>
<Trans>Network Fee</Trans>
</ThemedText.SubHeader>
<MouseoverTooltip
text={
<Trans>
The fee paid to miners who process your transaction. This must be paid in {nativeCurrency.symbol}.
</Trans>
}
disableHover={hideInfoTooltips}
>
<ThemedText.SubHeader color={theme.text3}>
<Trans>Network Fee</Trans>
</ThemedText.SubHeader>
</MouseoverTooltip>
<TextWithLoadingPlaceholder syncing={syncing} width={50}>
<ThemedText.Black textAlign="right" fontSize={14} color={theme.text3}>
~${trade.gasUseEstimateUSD.toFixed(2)}

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

@@ -147,7 +147,12 @@ export default function SwapDetailsDropdown({
content={
<ResponsiveTooltipContainer origin="top right" style={{ padding: '0' }}>
<Card padding="12px">
<AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} syncing={syncing} />
<AdvancedSwapDetails
trade={trade}
allowedSlippage={allowedSlippage}
syncing={syncing}
hideInfoTooltips={true}
/>
</Card>
</ResponsiveTooltipContainer>
}

View File

@@ -1,5 +1,8 @@
import { Trans } from '@lingui/macro'
import { Currency, TradeType } from '@uniswap/sdk-core'
import { Protocol } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { Pair } from '@uniswap/v2-sdk'
import { FeeAmount } from '@uniswap/v3-sdk'
import AnimatedDropdown from 'components/AnimatedDropdown'
import { AutoColumn } from 'components/Column'
import { LoadingRows } from 'components/Loader/styled'
@@ -8,7 +11,6 @@ import { AutoRow, RowBetween } from 'components/Row'
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import useAutoRouterSupported from 'hooks/useAutoRouterSupported'
import { getTokenPath } from 'lib/components/Swap/RoutingDiagram/utils'
import { memo, useState } from 'react'
import { Plus } from 'react-feather'
import { InterfaceTrade } from 'state/routing/types'
@@ -106,3 +108,41 @@ export default memo(function SwapRoute({ trade, syncing, fixedOpen = false, ...r
</Wrapper>
)
})
export interface RoutingDiagramEntry {
percent: Percent
path: [Currency, Currency, FeeAmount][]
protocol: Protocol
}
const V2_DEFAULT_FEE_TIER = 3000
/**
* Loops through all routes on a trade and returns an array of diagram entries.
*/
export function getTokenPath(trade: InterfaceTrade<Currency, Currency, TradeType>): RoutingDiagramEntry[] {
return trade.swaps.map(({ route: { path: tokenPath, pools, protocol }, inputAmount, outputAmount }) => {
const portion =
trade.tradeType === TradeType.EXACT_INPUT
? inputAmount.divide(trade.inputAmount)
: outputAmount.divide(trade.outputAmount)
const percent = new Percent(portion.numerator, portion.denominator)
const path: RoutingDiagramEntry['path'] = []
for (let i = 0; i < pools.length; i++) {
const nextPool = pools[i]
const tokenIn = tokenPath[i]
const tokenOut = tokenPath[i + 1]
const entry: RoutingDiagramEntry['path'][0] = [
tokenIn,
tokenOut,
nextPool instanceof Pair ? V2_DEFAULT_FEE_TIER : nextPool.fee,
]
path.push(entry)
}
return {
percent,
path,
protocol,
}
})
}

View File

@@ -33,6 +33,11 @@ export default function TradePrice({ price, showInverted, setShowInverted }: Tra
const theme = useContext(ThemeContext)
const usdcPrice = useUSDCPrice(showInverted ? price.baseCurrency : price.quoteCurrency)
/*
* calculate needed amount of decimal prices, for prices between 0.95-1.05 use 4 decimal places
*/
const p = Number(usdcPrice?.toFixed())
const visibleDecimalPlaces = p < 1.05 ? 4 : 2
let formattedPrice: string
try {
@@ -60,7 +65,7 @@ export default function TradePrice({ price, showInverted, setShowInverted }: Tra
</Text>{' '}
{usdcPrice && (
<ThemedText.DarkGray>
<Trans>(${usdcPrice.toSignificant(6, { groupSeparator: ',' })})</Trans>
<Trans>(${usdcPrice.toFixed(visibleDecimalPlaces, { groupSeparator: ',' })})</Trans>
</ThemedText.DarkGray>
)}
</StyledPriceContainer>

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

@@ -94,7 +94,7 @@ export const CHAIN_INFO: ChainInfoMap = {
[SupportedChainId.OPTIMISM]: {
networkType: NetworkType.L2,
blockWaitMsBeforeWarning: ms`25m`,
bridge: 'https://gateway.optimism.io/?chainId=1',
bridge: 'https://app.optimism.io/bridge',
defaultListUrl: OPTIMISM_LIST,
docs: 'https://optimism.io/',
explorer: 'https://optimistic.etherscan.io/',
@@ -108,7 +108,7 @@ export const CHAIN_INFO: ChainInfoMap = {
[SupportedChainId.OPTIMISTIC_KOVAN]: {
networkType: NetworkType.L2,
blockWaitMsBeforeWarning: ms`25m`,
bridge: 'https://gateway.optimism.io/',
bridge: 'https://app.optimism.io/bridge',
defaultListUrl: OPTIMISM_LIST,
docs: 'https://optimism.io/',
explorer: 'https://optimistic.etherscan.io/',

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,6 +1,7 @@
import {
GOVERNANCE_ALPHA_V0_ADDRESSES,
GOVERNANCE_ALPHA_V1_ADDRESSES,
GOVERNANCE_BRAVO_ADDRESSES,
TIMELOCK_ADDRESS,
UNI_ADDRESS,
} from './addresses'
@@ -11,7 +12,8 @@ export const COMMON_CONTRACT_NAMES: Record<number, { [address: string]: string }
[UNI_ADDRESS[SupportedChainId.MAINNET]]: 'UNI',
[TIMELOCK_ADDRESS[SupportedChainId.MAINNET]]: 'Timelock',
[GOVERNANCE_ALPHA_V0_ADDRESSES[SupportedChainId.MAINNET]]: 'Governance (V0)',
[GOVERNANCE_ALPHA_V1_ADDRESSES[SupportedChainId.MAINNET]]: 'Governance',
[GOVERNANCE_ALPHA_V1_ADDRESSES[SupportedChainId.MAINNET]]: 'Governance (V1)',
[GOVERNANCE_BRAVO_ADDRESSES[SupportedChainId.MAINNET]]: 'Governance',
'0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e': 'ENS Registry',
'0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41': 'ENS Public Resolver',
},

View File

@@ -2,3 +2,4 @@ export const UNISWAP_GRANTS_START_BLOCK = 11473815
export const BRAVO_START_BLOCK = 13059344
export const ONE_BIP_START_BLOCK = 13551293
export const POLYGON_START_BLOCK = 13786993
export const MOONBEAN_START_BLOCK = 14732457

View File

@@ -0,0 +1,33 @@
import { sendEvent } from 'components/analytics'
import { useEffect } from 'react'
import { ApplicationModal, setOpenModal } from 'state/application/reducer'
import { useAppDispatch } from 'state/hooks'
export default function useAccountRiskCheck(account: string | null | undefined) {
const dispatch = useAppDispatch()
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))
sendEvent({
category: 'Address Screening',
action: 'blocked',
label: account,
})
}
})
.catch(() => {
dispatch(setOpenModal(null))
})
}
}, [account, dispatch])
}

View File

@@ -1,15 +1,10 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { Web3Provider } from '@ethersproject/providers'
import { default as useWidgetsWeb3React } from 'lib/hooks/useActiveWeb3React'
import { useWeb3React } from 'web3-react-core'
import { NetworkContextName } from '../constants/misc'
export default function useActiveWeb3React() {
if (process.env.REACT_APP_IS_WIDGET) {
return useWidgetsWeb3React()
}
const interfaceContext = useWeb3React<Web3Provider>()
const interfaceNetworkContext = useWeb3React<Web3Provider>(
process.env.REACT_APP_IS_WIDGET ? undefined : NetworkContextName

View File

@@ -1,7 +1,7 @@
import { useEffect } from 'react'
import { useAppDispatch } from 'state/hooks'
import { updateUserExpertMode } from '../state/user/actions'
import { updateUserExpertMode } from '../state/user/reducer'
import useParsedQueryString from './useParsedQueryString'
export default function ApeModeQueryParamReader(): null {

View File

@@ -6,8 +6,8 @@ import useSwapApproval, { useSwapApprovalOptimizedTrade } from 'lib/hooks/swap/u
import { ApprovalState, useApproval } from 'lib/hooks/useApproval'
import { useCallback } from 'react'
import { TransactionType } from '../state/transactions/actions'
import { useHasPendingApproval, useTransactionAdder } from '../state/transactions/hooks'
import { TransactionType } from '../state/transactions/types'
export { ApprovalState } from 'lib/hooks/useApproval'
function useGetAndTrackApproval(getApproval: ReturnType<typeof useApproval>[1]) {

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

@@ -1,98 +0,0 @@
import { TransactionResponse } from '@ethersproject/providers'
import { initializeApp } from 'firebase/app'
import { getDatabase, push, ref } from 'firebase/database'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useCallback } from 'react'
import { TransactionInfo, TransactionType } from 'state/transactions/actions'
type PartialTransactionResponse = Pick<TransactionResponse, 'hash' | 'v' | 'r' | 's'>
const SUPPORTED_TRANSACTION_TYPES = [
TransactionType.ADD_LIQUIDITY_V2_POOL,
TransactionType.ADD_LIQUIDITY_V3_POOL,
TransactionType.CREATE_V3_POOL,
TransactionType.REMOVE_LIQUIDITY_V3,
TransactionType.SWAP,
]
const FIREBASE_API_KEY = process.env.REACT_APP_FIREBASE_KEY
const firebaseEnabled = typeof FIREBASE_API_KEY !== 'undefined'
if (firebaseEnabled) initializeFirebase()
function useMonitoringEventCallback() {
const { chainId } = useActiveWeb3React()
return useCallback(
async function log(
type: string,
{
transactionResponse,
walletAddress,
}: { transactionResponse: PartialTransactionResponse; walletAddress: string | undefined }
) {
if (!firebaseEnabled) return
const db = getDatabase()
if (!walletAddress) {
console.debug('Wallet address required to log monitoring events.')
return
}
try {
push(ref(db, 'trm'), {
chainId,
origin: window.location.origin,
timestamp: Date.now(),
tx: transactionResponse,
type,
walletAddress,
})
} catch (e) {
console.debug('Error adding document: ', e)
}
},
[chainId]
)
}
export function useTransactionMonitoringEventCallback() {
const { account } = useActiveWeb3React()
const log = useMonitoringEventCallback()
return useCallback(
(info: TransactionInfo, transactionResponse: TransactionResponse) => {
if (SUPPORTED_TRANSACTION_TYPES.includes(info.type)) {
log(TransactionType[info.type], {
transactionResponse: (({ hash, v, r, s }: PartialTransactionResponse) => ({ hash, v, r, s }))(
transactionResponse
),
walletAddress: account ?? undefined,
})
}
},
[account, log]
)
}
export function useWalletConnectMonitoringEventCallback() {
const log = useMonitoringEventCallback()
return useCallback(
(walletAddress) => {
log('WALLET_CONNECTED', { transactionResponse: { hash: '', r: '', s: '', v: -1 }, walletAddress })
},
[log]
)
}
function initializeFirebase() {
initializeApp({
apiKey: process.env.REACT_APP_FIREBASE_KEY,
authDomain: 'interface-monitoring.firebaseapp.com',
databaseURL: 'https://interface-monitoring-default-rtdb.firebaseio.com',
projectId: 'interface-monitoring',
storageBucket: 'interface-monitoring.appspot.com',
messagingSenderId: '968187720053',
appId: '1:968187720053:web:acedf72dce629d470be33c',
})
}

View File

@@ -4,8 +4,8 @@ import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { SwapCallbackState, useSwapCallback as useLibSwapCallBack } from 'lib/hooks/swap/useSwapCallback'
import { ReactNode, useMemo } from 'react'
import { TransactionType } from '../state/transactions/actions'
import { useTransactionAdder } from '../state/transactions/hooks'
import { TransactionType } from '../state/transactions/types'
import { currencyId } from '../utils/currencyId'
import useENS from './useENS'
import { SignatureData } from './useERC20Permit'

View File

@@ -6,8 +6,8 @@ import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { useMemo } from 'react'
import { WRAPPED_NATIVE_CURRENCY } from '../constants/tokens'
import { TransactionType } from '../state/transactions/actions'
import { useTransactionAdder } from '../state/transactions/hooks'
import { TransactionType } from '../state/transactions/types'
import { useCurrencyBalance } from '../state/wallet/hooks'
import { useWETHContract } from './useContract'

View File

@@ -1,28 +0,0 @@
{
"extends": ["../../.eslintrc.json"],
"plugins": ["better-styled-components"],
"rules": {
"better-styled-components/sort-declarations-alphabetically": "error",
"no-restricted-imports": [
"error",
{
"paths": [
{
"name": "react-feather",
"message": "Please import from lib/icons to ensure performant usage."
},
{
"name": "@uniswap/smart-order-router",
"message": "Forbidden import; smart-order-router is lazy-loaded."
}
],
"patterns": [
{
"group": ["styled-components"],
"message": "Please import styled from lib/theme to get the correct typings."
}
]
}
]
}
}

View File

@@ -1,22 +0,0 @@
// Use Inter mixin to set font-display: block.
@use "@fontsource/inter/scss/mixins" as Inter;
@include Inter.fontFace(
$fontName: 'Inter',
$weight: 400,
$display: block,
);
@include Inter.fontFace(
$fontName: 'Inter',
$weight: 500,
$display: block,
);
@include Inter.fontFace(
$fontName: 'Inter',
$weight: 600,
$display: block,
);
@include Inter.fontFaceVariable(
$display: block,
);
@import "~@fontsource/ibm-plex-mono/400.css";

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,10 +0,0 @@
<svg width="23" height="20" viewBox="0 0 23 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="gradient" x1="0" y1="0" x2="1" y2="0" gradientTransform="rotate(95)">
<stop id="stop1" offset="0" stop-color="#2274E2"/>
<stop id="stop1" offset="0.5" stop-color="#2274E2"/>
<stop id="stop2" offset="1" stop-color="#3FB672" />
</linearGradient>
</defs>
<path d="M16 16C10 16 9 10 5 10M16 16C16 17.6569 17.3431 19 19 19C20.6569 19 22 17.6569 22 16C22 14.3431 20.6569 13 19 13C17.3431 13 16 14.3431 16 16ZM5 10C9 10 10 4 16 4M5 10H1.5M16 4C16 5.65685 17.3431 7 19 7C20.6569 7 22 5.65685 22 4C22 2.34315 20.6569 1 19 1C17.3431 1 16 2.34315 16 4Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke="url(#gradient)" />
</svg>

Before

Width:  |  Height:  |  Size: 780 B

View File

@@ -1,4 +0,0 @@
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="10" cy="10" r="10" />
<path d="M14 7L8.5 12.5L6 10" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 233 B

View File

@@ -1,4 +0,0 @@
<svg viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" xmlns="http://www.w3.org/2000/svg">
<polyline class="left" points="18 15 12 9"></polyline>
<polyline class="right" points="12 9 6 15"></polyline>
</svg>

Before

Width:  |  Height:  |  Size: 238 B

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