Compare commits

...

74 Commits

Author SHA1 Message Date
Moody Salem
b08bb7eaff add an integration test 2020-07-27 13:29:25 -05:00
Moody Salem
3a36ac5538 chore(release): update dns again 2020-07-27 12:33:31 -05:00
Moody Salem
2962cd0e14 fix(migrate v1): migrate v1 pages and formatting 2020-07-27 12:33:02 -05:00
Moody Salem
6a311aa6d7 fix(v1 swap): exact out swaps not working 2020-07-27 08:45:48 -05:00
Moody Salem
e78b6d61f2 improvement(transactions): some clean up and unit tests
- fetch transaction state less often for old transactions
- fix a bug calling non payable methods with value 0
2020-07-27 08:45:48 -05:00
Moody Salem
365b429c0b feat(token lists): implement the uniswap default list as a token list (#983)
* load tokens from url `useTokenList`

* improve performance of the loading

* move the loading to redux and save loaded lists

* lint error

* move the list fetching code to a separate component

* change how token lists are fetched to use the updater and add unit tests

* fix a crash with currencyEquals

* bump sdk version

* token lists should automatically update for minor/patch changes

* nit

* show popups for list updates

* support pointing at localhost

* spuport ipfs/ipns logos

* use the updater to bump list versions

* save the old/new list in the popup for viewing diffs

* improve the list popup

* fix linter error, make sure visibility checking is working

* show list update notifications

* address a couple metamask warnings, linter error

* fix the custom added/default tokens

* refactor some popup stuff to reuse the fader

* linter error

* Revert: refactor some popup stuff to reuse the fader (a7b0f752)

* style improvements, linter

* add to the readme, drop the token-request template

* back to the beta that works with wallet connect

* get the dependencies to a state that works with wallet connect and passes integration tests
2020-07-25 10:41:03 -05:00
Moody Salem
32d300009e just bump the polling interval, aiming to have same # of blockNumber requests as calls or less 2020-07-21 15:57:09 -05:00
Moody Salem
806623c602 save some calls on the redundant chain id requests 2020-07-21 15:43:52 -05:00
Moody Salem
3272f8e9db chore(infura): rotate keys (complete) 2020-07-21 11:07:10 -05:00
Moody Salem
010ef108eb chore(infura): rotate keys 2020-07-21 11:05:55 -05:00
Moody Salem
19b1e9e399 feat(weth): support WETH across the site and use sdk 3.0 (#947)
* first pass of sdk 3.0

* second pass using weth

* kill unused pool popup

* get it compiling again

* first pass of sdk 3.0

* switch to currencies

* get it compiling after the big move merge

* restore margin

* clean up add liquidity more

* fix a bunch of bugs

* todo trade on v1

* show eth in currency list

* allow selecting eth in the swap page

* fix unit tests for swap page

* test lint errors

* fix failing integration tests

* fix another couple of failing unit tests

* handle selecting currency b when no currency a

* improve the import pool page

* clean up add liquidity for invalid pairs

* bold

* first pass at swap arguments for v1, some unit tests

* fix some bugs in add liquidity, burn hook

* fix last of ts errors in remove liquidity

* support wrapping/unwrapping weth

* kill a bunch of code including the dummy pairs

* required pair prop in the position card

* tests for the v1 swap arguments

* do not say estimated on the wrap ui

* show ETH instead of WETH in the pool summaries

* small size socks

* fix lint error

* in burn, use currencies from the URL

* fix some integration tests

* both contain weth

* receive eth/weth link

* fix empty row

* show wrapped only if one currency is weth

* currency selects in the remove liquidity page
2020-07-20 06:48:42 -05:00
Moody Salem
6287b95b92 fix(#961): change send copy 2020-07-17 09:05:20 -05:00
Ian Lapham
4e8a6e2a4c change copy on confirm view (#969) 2020-07-16 11:59:37 -04:00
Moody Salem
848c7b418b skip dns update while we work out cloudflare caching issues 2020-07-14 19:37:03 -04:00
Moody Salem
f619cf4353 fix the cf-ipfs url 2020-07-14 11:06:52 -04:00
Moody Salem
877db71e2a improvement(analytics): add exception reporting 2020-07-14 10:57:28 -04:00
Moody Salem
f4b5727fdb longer wait between retries 2020-07-13 18:03:12 -04:00
Moody Salem
1fd6b1e659 tweaking the slippage tabs for mobile again 2020-07-13 10:49:04 -04:00
Moody Salem
6570beef32 add BZRX token 2020-07-13 10:46:32 -04:00
Moody Salem
b57f58ab35 fix(title): link to relative path 2020-07-13 10:21:46 -04:00
Moody Salem
2f40c4f614 fix(settings): smaller slippage tabs for small screens 2020-07-12 12:57:06 -04:00
Moody Salem
3f9c34d37d always render the wordmark in the header 2020-07-12 12:53:05 -04:00
Moody Salem
1d5c6530e3 fix(header): some responsive style changes to the header 2020-07-12 12:50:53 -04:00
Moody Salem
78f294c340 more retries since metamask nodes often return old data 2020-07-12 12:43:36 -04:00
Moody Salem
90d24a26f3 retry tests 2020-07-11 15:25:33 -04:00
Moody Salem
7a3a5bd546 nit 2020-07-11 11:51:22 -04:00
Moody Salem
081ae15aa8 retry failed requests up to 3 times 2020-07-11 11:42:27 -04:00
Moody Salem
f5a5c5e70d fix(rpc spam): retries while remote node is out of sync 2020-07-11 11:09:45 -04:00
Moody Salem
e05e0206b7 fix a warning with add liquidity button 2020-07-10 15:31:58 -04:00
Moody Salem
344b4340ae improvement(pool): simplify pool flow, remove pool search modal (#941)
* deleting some code first

* strict, some refactoring

* denser common bases

* more add liquidity refactoring

* add liquidity paths working

* show common bases in the token selects

* fix the ability to select duplicate tokens

* useless rename

* try to handle alllll the duplicate token edge cases

* think i got them all lol

* remove common bases header

* Revert "remove common bases header"

This reverts commit 6ac4565d

* fix and add integration tests

* make gap between rows smaller

* get integration tests actually running again

* try another format of the command, upgrade serve

* frozen lockfile on install

* try the cypress github action

* install cypress in ci

* remove redundant ignore-scripts command

* use a specific github commit for the pinata action

* fix a bug in the multicall reducer, improve token list rendering performance

* improve the enter key on the token search modal

* stop using history.push

* fix linting errors

* position card cleanup before updating to match mock
2020-07-10 15:25:15 -04:00
Moody Salem
eeef306bdd fix(🧦): 🧦 2020-07-10 12:42:24 -04:00
Moody Salem
63a491d4b1 improvement(approval): show approval state approved if allowance exceeds amount to approve, even when pending 2020-07-09 13:34:29 -04:00
Moody Salem
6831a73fdf fix(swap): revert the change to reload query parameters on every url change 2020-07-09 10:35:14 -04:00
Moody Salem
a4aef02747 nit(swap): add "(optional)" to add recipient button 2020-07-09 10:28:58 -04:00
Moody Salem
c26716047f chore(release): allow (new) manual trigger of release 2020-07-09 09:57:17 -04:00
Moody Salem
0fa238af0b fix(swap): swap to account if recipient is null (#940)
* fix(swap): swap to account if recipient is null

* fix naming and strict ts error
2020-07-09 09:55:20 -04:00
Moody Salem
21c1484c0e feat(send page): remove send page, implement recipient feature in swap page (#934)
* quick poc for removing swap page

* accidental import

* error for recipient field

* query parameter working

* undo id change

* tweaks to match mocks better

* tweaks to match mocks better

* some extra integration tests

* clean up nav tabs a bit

* clean up nav tabs a bit

* space swap/pool better

* stop selecting button text when double clicking

* remove unused transfer modal header

* add info to swap confirm modal

* shorten address

* improve summary message, remove unused send callback, fix react ga event

* fix lint errors

* arrow color
2020-07-08 23:06:29 -04:00
Antonio Savage
8a845ee0e9 fix(discord invite link): working discord invite link (#929) 2020-07-06 22:59:14 -04:00
Moody Salem
f5229ca838 linter error 2020-07-06 21:31:08 -04:00
Moody Salem
875203f0ef fix(responsiveness): small tweaks for mobile 2020-07-06 21:26:38 -04:00
Moody Salem
91a8202737 fix(send page): support swap + send query parameters on send page (#921)
* support swap + send query parameters on send page

* revert the unfinished portis logic

Co-authored-by: ianlapham <ianlapham@gmail.com>
2020-07-05 22:32:54 -04:00
Moody Salem
0b4819d165 fix(#899): Add PieDAO USD++ 2020-07-05 22:29:49 -04:00
Jonathan Diep
e7d3289754 improvement(token warning card): link to the token page on etherscan instead of the address page (#914) 2020-07-02 08:27:19 -04:00
Moody Salem
0698e0f82a Update README.md 2020-07-01 13:09:37 -04:00
Micah Zoltu
0350cc4701 fix(REP token): renames REP to REPv1 (#915)
https://www.augur.net/blog/v2-launch/

TL;DR: Augur v2 launch is coming up and will introduce a new REP token.  FF has requested all exchanges rename REP to REPv1 to avoid confusion.

Going forward, REP tokens will contain versioning in their name/symbol on chain so this should be a one-time "fix" for Augur v1 REP.
2020-07-01 10:19:28 -04:00
Moody Salem
997052869d fix(lint): linter error 2020-06-30 16:50:19 -04:00
Moody Salem
9ec16c2ba8 actually add the inter-ui dependency 2020-06-30 16:47:16 -04:00
Moody Salem
e2cf8f1642 fix(font): do not load font from remote 2020-06-30 16:43:21 -04:00
Moody Salem
ed6952d1f7 readme cleanup 2020-06-30 14:13:27 -04:00
Moody Salem
3277d70e93 fix all tests 2020-06-30 14:02:09 -04:00
Moody Salem
d1a31fe763 old link 2020-06-30 13:53:18 -04:00
Moody Salem
f88af029ae chore(tests): fix integration tests 2020-06-30 13:51:20 -04:00
Moody Salem
9f3e49b4d8 chore(ipfs migration): point at master branch instead of v2 branch 2020-06-30 13:49:38 -04:00
Moody Salem
d4911d1054 chore(ipfs migration): changes for ipfs url migration
- remove netlify stuff
- update rename to uniswap-interface
- always use hash router
2020-06-30 13:41:51 -04:00
Moody Salem
90df9c4ced improvement(layout): move header version switch, drop footer for mobile (#910)
* version switch tweaks

* Mobile layout and toggle tweaks

* Remove the entire footer

Co-authored-by: Callil Capuozzo <callil.capuozzo@gmail.com>
2020-06-29 16:55:33 -04:00
Callil Capuozzo
14f15d1fd6 fix(i18n): Fix return characters and remove uneeded file (#912) 2020-06-29 14:15:45 -04:00
Moody Salem
69818ace1f fix(popover): animation getting stuck open on firefox 2020-06-28 13:55:54 -04:00
Moody Salem
42906d6709 add BAL 2020-06-27 12:27:10 -04:00
Moody Salem
2f8936a980 unused keys 2020-06-26 14:46:22 -04:00
Moody Salem
f5c4468c3c fix(token logo): fix persistent error state in token logo, clean up swap route code 2020-06-26 14:44:33 -04:00
Moody Salem
852e8f749f fix(swap routing): max hops back to 3 2020-06-26 14:12:12 -04:00
Moody Salem
6694e5e398 improvement(swap routing): consider more bases in the swap (#909)
* consider more bases in the swap

* all match type

* max hops 2, only 1 result
2020-06-26 13:27:38 -04:00
Noah Zinsmeister
2c9a50a372 remove trust deep link 2020-06-25 10:20:27 -04:00
Noah Zinsmeister
0fc0cba6de bump walletconnect 2020-06-25 10:17:49 -04:00
Moody Salem
041c86c04d fix dns variable 2020-06-24 20:07:50 -05:00
Moody Salem
123373e671 docs in release, trigger another release 2020-06-24 19:33:26 -05:00
Moody Salem
eb1732deee release text 2020-06-24 19:15:18 -05:00
Moody Salem
3c13321a71 point at a specific audited commit for the cloudflare update action 2020-06-24 19:12:26 -05:00
Moody Salem
58703f31a0 chore(release): update cloudflare's DNS instead of vercel's DNS 2020-06-24 19:09:02 -05:00
Moody Salem
58721fb191 improvement(remove liquidity): fix width of buttons on small screens 2020-06-24 11:55:19 -05:00
Noah Zinsmeister
678cd1a06f upgrade to walletconnect v1 (#903) 2020-06-23 16:18:04 -04:00
Moody Salem
a5ff3beb92 add a comment for the previous change 2020-06-22 16:12:02 -05:00
Moody Salem
35ccf425f6 improvement(modals): do not focus inputs automatically on mobile 2020-06-22 16:11:32 -05:00
Moody Salem
fe030412cd improvement(pair search modal): show exact symbol match pairs first, filter before sorting 2020-06-22 14:37:52 -05:00
166 changed files with 7474 additions and 6299 deletions

2
.env
View File

@@ -1,2 +1,2 @@
REACT_APP_CHAIN_ID="1" REACT_APP_CHAIN_ID="1"
REACT_APP_NETWORK_URL="https://mainnet.infura.io/v3/acb7e55995d04c49bfb52b7141599467" REACT_APP_NETWORK_URL="https://mainnet.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847"

View File

@@ -1,5 +1,5 @@
REACT_APP_CHAIN_ID="1" REACT_APP_CHAIN_ID="1"
REACT_APP_NETWORK_URL="https://mainnet.infura.io/v3/febcb10ca2754433a61e0805bc6c047d" REACT_APP_NETWORK_URL="https://mainnet.infura.io/v3/099fc58e0de9451d80b18d7c74caa7c1"
REACT_APP_PORTIS_ID="c0e2bf01-4b08-4fd5-ac7b-8e26b58cd236" REACT_APP_PORTIS_ID="c0e2bf01-4b08-4fd5-ac7b-8e26b58cd236"
REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF" REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF"
REACT_APP_GOOGLE_ANALYTICS_ID="UA-128182339-4" REACT_APP_GOOGLE_ANALYTICS_ID="UA-128182339-4"

View File

@@ -1,27 +0,0 @@
---
name: Token Request
about: Request a token addition
title: ''
labels: token request
assignees: ''
---
**Please provide the following information for your token.**
Token Address:
Token Name (from contract):
Token Decimals (from contract):
Token Symbol (from contract):
Uniswap Exchange Address of Token:
Link to the official homepage of token:
Link to CoinMarketCap or CoinGecko page of token:
Some tokens (e.g. BNB) do not work with Uniswap v1. In order to assess if your token works correctly, please complete small-value transactions of each of the types below, and submit the Etherscan transaction links for our review.
Test `addLiquidity` transaction:
Test `swap` transaction:
Test `removeLiquidity` transaction:
Are you willing to add liquidity to the liquidity pool for this token? (Y/N):
If so, how much liquidity are you willing to add?:

View File

@@ -4,13 +4,8 @@ on:
schedule: schedule:
- cron: '0 12 * * 1-4' - cron: '0 12 * * 1-4'
# releases are triggered on changes to this file # manual trigger
push: workflow_dispatch:
branches:
- v2
paths:
- '.github/workflows/release.yaml'
- '.env.production'
jobs: jobs:
bump_version: bump_version:
@@ -44,14 +39,14 @@ jobs:
node-version: '12' node-version: '12'
- name: Install dependencies - name: Install dependencies
run: yarn install --ignore-scripts --frozen-lockfile run: yarn install --frozen-lockfile
- name: Build the IPFS bundle - name: Build the IPFS bundle
run: yarn ipfs-build run: yarn build
- name: Pin to IPFS - name: Pin to IPFS
id: upload id: upload
uses: anantaramdas/ipfs-pinata-deploy-action@v1.5.2 uses: anantaramdas/ipfs-pinata-deploy-action@39bbda1ce1fe24c69c6f57861b8038278d53688d
with: with:
pin-name: Uniswap ${{ needs.bump_version.outputs.new_tag }} pin-name: Uniswap ${{ needs.bump_version.outputs.new_tag }}
path: './build' path: './build'
@@ -65,14 +60,14 @@ jobs:
cidv0: ${{ steps.upload.outputs.hash }} cidv0: ${{ steps.upload.outputs.hash }}
- name: Update DNS with new IPFS hash - name: Update DNS with new IPFS hash
uses: uniswap/replace-vercel-dns-records@v1.0.0 env:
CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_TOKEN }}
RECORD_DOMAIN: 'uniswap.org'
RECORD_NAME: '_dnslink.app'
CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}
uses: textileio/cloudflare-update-dnslink@0fe7b7a1ffc865db3a4da9773f0f987447ad5848
with: with:
domain: 'uniswap.org' cid: ${{ steps.upload.outputs.hash }}
subdomain: '_dnslink.app'
record-type: 'TXT'
value: dnslink=/ipfs/${{ steps.upload.outputs.hash }}
token: ${{ secrets.VERCEL_TOKEN }}
team-name: 'uniswap'
- name: Create GitHub Release - name: Create GitHub Release
id: create_release id: create_release
@@ -87,13 +82,17 @@ jobs:
- CIDv0: `${{ steps.upload.outputs.hash }}` - CIDv0: `${{ steps.upload.outputs.hash }}`
- CIDv1: `${{ steps.convert_cidv0.outputs.cidv1 }}` - CIDv1: `${{ steps.convert_cidv0.outputs.cidv1 }}`
The latest release is always accessible via our alias to the Cloudflare IPFS gateway at [app.uniswap.org](https://app.uniswap.org).
You can also access the Uniswap Interface directly from an IPFS gateway.
The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to store your settings. The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to store your settings.
**Beware** that other sites you access via the _same_ IPFS gateway can read and modify your settings on the Uniswap interface without your permission. **Beware** that other sites you access via the _same_ IPFS gateway can read and modify your settings on the Uniswap interface without your permission.
You can avoid this issue by using a subdomain IPFS gateway. The preferred gateway URLs below utilize the CIDv1 of the release in the subdomain, and are relatively safer. You can avoid this issue by using a subdomain IPFS gateway, or our alias to the latest release at [app.uniswap.org](https://app.uniswap.org).
The preferred URLs below are safe to use to access this specific release.
Preferred URLs: Preferred URLs:
- https://${{ steps.convert_cidv0.outputs.cidv1 }}.ipfs.dweb.link/ - https://${{ steps.convert_cidv0.outputs.cidv1 }}.ipfs.dweb.link/
- https://${{ steps.convert_cidv0.outputs.cidv1 }}.cf-ipfs.com/ - https://${{ steps.convert_cidv0.outputs.cidv1 }}.ipfs.cf-ipfs.com/
- [ipfs://${{ steps.upload.outputs.hash }}/](ipfs://${{ steps.upload.outputs.hash }}/) - [ipfs://${{ steps.upload.outputs.hash }}/](ipfs://${{ steps.upload.outputs.hash }}/)
Other IPFS gateways: Other IPFS gateways:

View File

@@ -2,10 +2,10 @@ name: Tests
on: on:
push: push:
branches: branches:
- v2 - master
pull_request: pull_request:
branches: branches:
- v2 - master
jobs: jobs:
integration-tests: integration-tests:
name: Integration tests name: Integration tests
@@ -26,7 +26,9 @@ jobs:
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: | restore-keys: |
${{ runner.os }}-yarn- ${{ runner.os }}-yarn-
- run: yarn install - run: yarn install --frozen-lockfile
- run: yarn cypress install
- run: yarn build
- run: yarn integration-test - run: yarn integration-test
unit-tests: unit-tests:
@@ -48,7 +50,7 @@ jobs:
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: | restore-keys: |
${{ runner.os }}-yarn- ${{ runner.os }}-yarn-
- run: yarn install --ignore-scripts --frozen-lockfile - run: yarn install --frozen-lockfile
- run: yarn test - run: yarn test
lint: lint:
@@ -70,6 +72,6 @@ jobs:
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: | restore-keys: |
${{ runner.os }}-yarn- ${{ runner.os }}-yarn-
- run: yarn install --ignore-scripts --frozen-lockfile - run: yarn install --frozen-lockfile
- run: yarn lint - run: yarn lint

1
.yarnrc Normal file
View File

@@ -0,0 +1 @@
ignore-scripts true

View File

@@ -1,11 +1,12 @@
# Uniswap Frontend # Uniswap Interface
[![Tests](https://github.com/Uniswap/uniswap-frontend/workflows/Tests/badge.svg)](https://github.com/Uniswap/uniswap-frontend/actions?query=workflow%3ATests) [![Tests](https://github.com/Uniswap/uniswap-interface/workflows/Tests/badge.svg)](https://github.com/Uniswap/uniswap-interface/actions?query=workflow%3ATests)
[![Styled With Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://prettier.io/) [![Styled With Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://prettier.io/)
An open source interface for Uniswap -- a protocol for decentralized exchange of Ethereum tokens. An open source interface for Uniswap -- a protocol for decentralized exchange of Ethereum tokens.
- Website: [uniswap.org](https://uniswap.org/) - Website: [uniswap.org](https://uniswap.org/)
- Interface: [app.uniswap.org](https://app.uniswap.org)
- Docs: [uniswap.org/docs/](https://uniswap.org/docs/) - Docs: [uniswap.org/docs/](https://uniswap.org/docs/)
- Twitter: [@UniswapProtocol](https://twitter.com/UniswapProtocol) - Twitter: [@UniswapProtocol](https://twitter.com/UniswapProtocol)
- Reddit: [/r/Uniswap](https://www.reddit.com/r/Uniswap/) - Reddit: [/r/Uniswap](https://www.reddit.com/r/Uniswap/)
@@ -13,11 +14,17 @@ An open source interface for Uniswap -- a protocol for decentralized exchange of
- Discord: [Uniswap](https://discord.gg/Y7TF6QA) - Discord: [Uniswap](https://discord.gg/Y7TF6QA)
- Whitepaper: [Link](https://hackmd.io/C-DvwDSfSxuh-Gd4WKE_ig) - Whitepaper: [Link](https://hackmd.io/C-DvwDSfSxuh-Gd4WKE_ig)
## Accessing the frontend ## Accessing the Uniswap Interface
To access the front end, use an IPFS gateway link from the To access the Uniswap Interface, use an IPFS gateway link from the
[latest release](https://github.com/Uniswap/uniswap-frontend/releases/latest) [latest release](https://github.com/Uniswap/uniswap-interface/releases/latest),
or visit [uniswap.exchange](https://uniswap.exchange). or visit [app.uniswap.org](https://app.uniswap.org).
## Listing a token
Please see the
[@uniswap/default-token-list](https://github.com/uniswap/default-token-list)
repository.
## Development ## Development
@@ -27,31 +34,32 @@ or visit [uniswap.exchange](https://uniswap.exchange).
yarn yarn
``` ```
### Configure Environment (optional)
Copy `.env` to `.env.local` and change the appropriate variables.
### Run ### Run
```bash ```bash
yarn start yarn start
``` ```
To have the frontend default to a different network, make a copy of `.env` named `.env.local`, ### Configuring the environment (optional)
change `REACT_APP_NETWORK_ID` to `"{yourNetworkId}"`, and change `REACT_APP_NETWORK_URL` to e.g.
`"https://{yourNetwork}.infura.io/v3/{yourKey}"`.
Note that the front end only works properly on testnets where both To have the interface default to a different network when a wallet is not connected:
1. Make a copy of `.env` named `.env.local`
2. Change `REACT_APP_NETWORK_ID` to `"{YOUR_NETWORK_ID}"`
3. Change `REACT_APP_NETWORK_URL` to e.g. `"https://{YOUR_NETWORK_ID}.infura.io/v3/{YOUR_INFURA_KEY}"`
Note that the interface only works on testnets where both
[Uniswap V2](https://uniswap.org/docs/v2/smart-contracts/factory/) and [Uniswap V2](https://uniswap.org/docs/v2/smart-contracts/factory/) and
[multicall](https://github.com/makerdao/multicall) are deployed. [multicall](https://github.com/makerdao/multicall) are deployed.
The frontend will not work on other networks. The interface will not work on other networks.
## Contributions ## Contributions
**Please open all pull requests against the `v2` branch.** **Please open all pull requests against the `master` branch.**
CI checks will run against all PRs. CI checks will run against all PRs.
## Accessing Uniswap V1 interface ## Accessing Uniswap Interface V1
The Uniswap V1 interface for mainnet and testnets is accessible via IPFS gateways linked The Uniswap Interface supports swapping against, and migrating or removing liquidity from Uniswap V1. However,
from the [v1.0.0 release](https://github.com/Uniswap/uniswap-frontend/releases/tag/v1.0.0). if you would like to use 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

@@ -16,4 +16,35 @@ describe('Add Liquidity', () => {
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'SKL') cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'SKL')
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'MKR') cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'MKR')
}) })
it('single token can be selected', () => {
cy.visit('/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d')
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'SKL')
cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'MKR')
})
it('redirects /add/token-token to add/token/token', () => {
cy.visit('/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
cy.url().should(
'contain',
'/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85'
)
})
it('redirects /add/WETH-token to /add/WETH-address/token', () => {
cy.visit('/add/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
cy.url().should(
'contain',
'/add/0xc778417E063141139Fce010982780140Aa0cD5Ab/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85'
)
})
it('redirects /add/token-WETH to /add/token/WETH-address', () => {
cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85-0xc778417E063141139Fce010982780140Aa0cD5Ab')
cy.url().should(
'contain',
'/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85/0xc778417E063141139Fce010982780140Aa0cD5Ab'
)
})
}) })

View File

@@ -10,11 +10,6 @@ describe('Landing Page', () => {
cy.url().should('include', '/swap') cy.url().should('include', '/swap')
}) })
it('allows navigation to send', () => {
cy.get('#send-nav-link').click()
cy.url().should('include', '/send')
})
it('allows navigation to pool', () => { it('allows navigation to pool', () => {
cy.get('#pool-nav-link').click() cy.get('#pool-nav-link').click()
cy.url().should('include', '/pool') cy.url().should('include', '/pool')

View File

@@ -0,0 +1,8 @@
describe('Migrate V1 Liquidity', () => {
describe('Remove V1 liquidity', () => {
it('renders the correct page', () => {
cy.visit('/remove/v1/0x93bB63aFe1E0180d0eF100D774B473034fd60C36')
cy.get('#remove-v1-exchange').should('contain', 'MKR/ETH')
})
})
})

View File

@@ -1,13 +1,12 @@
describe('Pool', () => { describe('Pool', () => {
beforeEach(() => cy.visit('/pool')) beforeEach(() => cy.visit('/pool'))
it('can search for a pool', () => { it('add liquidity links to /add/ETH', () => {
cy.get('#join-pool-button').click() cy.get('#join-pool-button').click()
cy.get('#token-search-input').type('DAI', { delay: 200 }) cy.url().should('contain', '/add/ETH')
}) })
it('can import a pool', () => { it('import pool links to /import', () => {
cy.get('#join-pool-button').click() cy.get('#import-pool-link').click()
cy.get('#import-pool-link').click({ force: true }) // blocked by the grid element in the search box cy.url().should('contain', '/find')
cy.url().should('include', '/find')
}) })
}) })

View File

@@ -1,14 +1,34 @@
describe('Remove Liquidity', () => { describe('Remove Liquidity', () => {
it('redirects', () => {
cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
cy.url().should(
'contain',
'/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85'
)
})
it('eth remove', () => {
cy.visit('/remove/ETH/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'MKR')
})
it('eth remove swap order', () => {
cy.visit('/remove/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85/ETH')
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'MKR')
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'ETH')
})
it('loads the two correct tokens', () => { it('loads the two correct tokens', () => {
cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85') cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH') cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'WETH')
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'MKR') cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'MKR')
}) })
it('does not crash if ETH is duplicated', () => { it('does not crash if ETH is duplicated', () => {
cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xc778417E063141139Fce010982780140Aa0cD5Ab') cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xc778417E063141139Fce010982780140Aa0cD5Ab')
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH') cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'WETH')
cy.get('#remove-liquidity-tokenb-symbol').should('not.contain.text', 'ETH') cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'WETH')
}) })
it('token not in storage is loaded', () => { it('token not in storage is loaded', () => {

View File

@@ -1,9 +1,11 @@
describe('Send', () => { describe('Send', () => {
beforeEach(() => cy.visit('/send')) it('should redirect', () => {
cy.visit('/send')
cy.url().should('include', '/swap')
})
it('can enter an amount into input', () => { it('should redirect with url params', () => {
cy.get('#sending-no-swap-input') cy.visit('/send?outputCurrency=ETH&recipient=bob.argent.xyz')
.type('0.001', { delay: 200 }) cy.url().should('contain', '/swap?outputCurrency=ETH&recipient=bob.argent.xyz')
.should('have.value', '0.001')
}) })
}) })

View File

@@ -40,4 +40,15 @@ describe('Swap', () => {
cy.get('#swap-button').click() cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').should('contain', 'Confirm Swap') cy.get('#confirm-swap-or-send').should('contain', 'Confirm Swap')
}) })
it('add a recipient', () => {
cy.get('#add-recipient-button').click()
cy.get('#recipient').should('exist')
})
it('remove recipient', () => {
cy.get('#add-recipient-button').click()
cy.get('#remove-recipient-button').click()
cy.get('#recipient').should('not.exist')
})
}) })

View File

@@ -69,11 +69,11 @@ class CustomizedBridge extends _Eip1193Bridge {
// sets up the injected provider to be a mock ethereum provider with the given mnemonic/index // sets up the injected provider to be a mock ethereum provider with the given mnemonic/index
Cypress.Commands.overwrite('visit', (original, url, options) => { Cypress.Commands.overwrite('visit', (original, url, options) => {
return original(url, { return original(url.startsWith('/') && url.length > 2 && !url.startsWith('/#') ? `/#${url}` : url, {
...options, ...options,
onBeforeLoad(win) { onBeforeLoad(win) {
options && options.onBeforeLoad && options.onBeforeLoad(win) options && options.onBeforeLoad && options.onBeforeLoad(win)
const provider = new JsonRpcProvider('https://rinkeby.infura.io/v3/acb7e55995d04c49bfb52b7141599467', 4) const provider = new JsonRpcProvider('https://rinkeby.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847', 4)
const signer = new Wallet(PRIVATE_KEY_TEST_NEVER_USE, provider) const signer = new Wallet(PRIVATE_KEY_TEST_NEVER_USE, provider)
win.ethereum = new CustomizedBridge(signer, provider) win.ethereum = new CustomizedBridge(signer, provider)
} }

View File

@@ -1,27 +0,0 @@
# block some countries
[[redirects]]
from = "/*"
to = "/451.html"
status = 451
force = true
conditions = {Country=["BY","CU","IR","IQ","CI","LR","KP","SD","SY","ZW"]}
headers = {Link="<https://uniswap.exchange>"}
# forward migrate
[[redirects]]
from = "https://migrate.uniswap.exchange/*"
to = "https://uniswap.exchange/migrate/v1"
status = 301
force = true
# forward v2 subdomain to apex
[[redirects]]
from = "https://v2.uniswap.exchange/*"
to = "https://uniswap.exchange/:splat"
status = 301
# support SPA setup
[[redirects]]
from = "/*"
to = "/index.html"
status = 200

View File

@@ -1,19 +1,21 @@
{ {
"name": "@uniswap/interface", "name": "@uniswap/interface",
"description": "Uniswap Interface", "description": "Uniswap Interface",
"homepage": "https://uniswap.exchange", "homepage": ".",
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"@ethersproject/address": "^5.0.0-beta.134", "@ethersproject/address": "5.0.0-beta.134",
"@ethersproject/bignumber": "^5.0.0-beta.138", "@ethersproject/bignumber": "5.0.0-beta.138",
"@ethersproject/constants": "^5.0.0-beta.133", "@ethersproject/constants": "5.0.0-beta.133",
"@ethersproject/contracts": "^5.0.0-beta.151", "@ethersproject/contracts": "5.0.0-beta.151",
"@ethersproject/experimental": "^5.0.0-beta.141", "@ethersproject/experimental": "5.0.0-beta.141",
"@ethersproject/networks": "5.0.0-beta.136",
"@ethersproject/providers": "5.0.0-beta.162", "@ethersproject/providers": "5.0.0-beta.162",
"@ethersproject/strings": "^5.0.0-beta.136", "@ethersproject/solidity": "5.0.2",
"@ethersproject/units": "^5.0.0-beta.132", "@ethersproject/strings": "5.0.0-beta.136",
"@ethersproject/wallet": "^5.0.0-beta.141", "@ethersproject/units": "5.0.0-beta.132",
"@popperjs/core": "^2.4.0", "@ethersproject/wallet": "5.0.0-beta.141",
"@popperjs/core": "^2.4.4",
"@reach/dialog": "^0.10.3", "@reach/dialog": "^0.10.3",
"@reach/portal": "^0.10.3", "@reach/portal": "^0.10.3",
"@reduxjs/toolkit": "^1.3.5", "@reduxjs/toolkit": "^1.3.5",
@@ -27,19 +29,21 @@
"@types/react-router-dom": "^5.0.0", "@types/react-router-dom": "^5.0.0",
"@types/react-window": "^1.8.2", "@types/react-window": "^1.8.2",
"@types/rebass": "^4.0.5", "@types/rebass": "^4.0.5",
"@types/styled-components": "^4.2.0", "@types/styled-components": "^5.1.0",
"@types/testing-library__cypress": "^5.0.5", "@types/testing-library__cypress": "^5.0.5",
"@typescript-eslint/eslint-plugin": "^2.31.0", "@typescript-eslint/eslint-plugin": "^2.31.0",
"@typescript-eslint/parser": "^2.31.0", "@typescript-eslint/parser": "^2.31.0",
"@uniswap/sdk": "^2.0.5", "@uniswap/sdk": "3.0.3-beta.1",
"@uniswap/token-lists": "^1.0.0-beta.9",
"@uniswap/v2-core": "1.0.0", "@uniswap/v2-core": "1.0.0",
"@uniswap/v2-periphery": "^1.1.0-beta.0", "@uniswap/v2-periphery": "^1.1.0-beta.0",
"@web3-react/core": "^6.0.9", "@web3-react/core": "^6.0.9",
"@web3-react/fortmatic-connector": "^6.0.9", "@web3-react/fortmatic-connector": "^6.0.9",
"@web3-react/injected-connector": "^6.0.7", "@web3-react/injected-connector": "^6.0.7",
"@web3-react/portis-connector": "^6.0.9", "@web3-react/portis-connector": "^6.0.9",
"@web3-react/walletconnect-connector": "^6.0.9", "@web3-react/walletconnect-connector": "^6.1.1",
"@web3-react/walletlink-connector": "^6.0.9", "@web3-react/walletlink-connector": "^6.0.9",
"ajv": "^6.12.3",
"copy-to-clipboard": "^3.2.0", "copy-to-clipboard": "^3.2.0",
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"cypress": "^4.5.0", "cypress": "^4.5.0",
@@ -51,6 +55,7 @@
"i18next": "^15.0.9", "i18next": "^15.0.9",
"i18next-browser-languagedetector": "^3.0.1", "i18next-browser-languagedetector": "^3.0.1",
"i18next-xhr-backend": "^2.0.1", "i18next-xhr-backend": "^2.0.1",
"inter-ui": "^3.13.1",
"jazzicon": "^1.5.0", "jazzicon": "^1.5.0",
"lodash.flatmap": "^4.5.0", "lodash.flatmap": "^4.5.0",
"polished": "^3.3.2", "polished": "^3.3.2",
@@ -81,14 +86,10 @@
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
"build": "react-scripts build", "build": "react-scripts build",
"ipfs-build": "cross-env PUBLIC_URL=\".\" react-scripts build",
"test": "react-scripts test --env=jsdom", "test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'", "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
"lint:fix": "eslint 'src/**/*.{js,jsx,ts,tsx}' --fix", "integration-test": "start-server-and-test 'serve build -l 3000' http://localhost:3000 'cypress run'"
"cy:run": "cypress run",
"serve:build": "serve -s build -l 3000",
"integration-test": "yarn build && start-server-and-test 'yarn run serve:build' http://localhost:3000 cy:run"
}, },
"eslintConfig": { "eslintConfig": {
"extends": "react-app" "extends": "react-app"

View File

@@ -1,75 +0,0 @@
{
"noWallet": "לא נמצא ארנק",
"wrongNetwork": "נבחרה רשת לא נכונה",
"switchNetwork": "{{ correctNetwork }} יש צורך לשנות את הרשת ל",
"installWeb3MobileBrowser": "יש צורך בארנק ווב3.0, תתקין מטאמאסק או ארנק דומה",
"installMetamask": " Metamask יש צורך להתקין תוסף מטאמאסק לדפדפן, חפשו בגוגל ",
"disconnected": "מנותק",
"swap": "המרה",
"send": "שליחה",
"pool": "להפקיד",
"betaWarning": "הפרויקט נמצא בשלב בטא, השתמשו באחריות",
"input": "מוכר",
"output": "אקבל",
"estimated": "הערכה",
"balance": "בארנק שלי {{ balanceInput }}",
"unlock": "שחרור נעילת ארנק",
"pending": "ממתין לאישור",
"selectToken": "בחרו את הטוקן להמרה",
"searchOrPaste": "הכניסו שם או כתובת של טוקן לחיפוש",
"noExchange": "לא מתאפשרת המרה",
"exchangeRate": "שער המרה",
"enterValueCont": "כדי להמשיך {{ missingCurrencyValue }} הזינו ",
"selectTokenCont": "בחרו טוקן כדי להמשיך",
"noLiquidity": "אין נזילות",
"unlockTokenCont": "יש צורך לאשר את הטוקן למסחר",
"transactionDetails": "פרטי הטרנזקציה",
"hideDetails": "הסתר פרטים נוספים",
"youAreSelling": "למכירה",
"orTransFail": "או שהטרנזקציה תיכשל",
"youWillReceive": "תוצר המרה מינימלי",
"youAreBuying": "קונה",
"itWillCost": "זה יעלה",
"insufficientBalance": "אין בחשבון מספיק מטבעות",
"inputNotValid": "קלט לא תקין",
"differentToken": "יש צורך בטוקנים שונים",
"noRecipient": "לא הוכנסה כתובת ארנק יעד",
"invalidRecipient": "לא הוכנסה כתובת תקינה",
"recipientAddress": "כתובת יעד",
"youAreSending": "כמות לשליחה",
"willReceive": "יתקבל לכל הפחות",
"to": "אל",
"addLiquidity": "להוספת נזילות למאגר",
"deposit": "הפקדה",
"currentPoolSize": "גודל מאגר הנזילות הכולל",
"yourPoolShare": "חלקך במאגר הנזילות",
"noZero": "אפס אינו ערך תקין",
"mustBeETH": "ETH חייב להופיע באחד מהצדדים",
"enterCurrencyOrLabelCont": "כדי להמשיך {{ inputCurrency }} או {{ label }} הכנס",
"youAreAdding": "מתווספים למאגר",
"and": "וגם",
"intoPool": "לתוך הנזילות",
"outPool": "מתוך",
"youWillMint": "יונפקו לכם",
"liquidityTokens": "טוקנים של נזילות",
"totalSupplyIs": "חלקך במאגר הנזילות",
"youAreSettingExRate": "שער ההמרה יקבע על ידך",
"totalSupplyIs0": "אין לך טוקנים של נזילות",
"tokenWorth": "שווי כל טוקן נזילות הינו",
"firstLiquidity": "את\ה הראשון\ה שמזרים נזילות למאגר",
"initialExchangeRate": "ושל האית'ר הינן בערך שווה {{ label }} תוודאו שההפקדה של הטוקן",
"removeLiquidity": "הוצאה של נזילות",
"poolTokens": "טוקנים של מאגר הנזילות",
"enterLabelCont": "כדי להמשיך {{ label }} הכנס ",
"youAreRemoving": "יוסרו",
"youWillRemove": "יוסרו",
"createExchange": "ליצירת זוג מסחר",
"invalidTokenAddress": "כתובת טוקן לא נכונה",
"exchangeExists": "{{ label }} כבר קיים זוג המרה עבור",
"invalidSymbol": "תו שגוי",
"invalidDecimals": "ספרות עשרוניות שגויות",
"tokenAddress": "כתובת הטוקן",
"label": "שם",
"decimals": "ספרות עשרויות",
"enterTokenCont": "הכניסו כתובת טוקן כדי להמשיך"
}

View File

@@ -56,7 +56,7 @@
"youAreSettingExRate": "שער ההמרה יקבע על ידך", "youAreSettingExRate": "שער ההמרה יקבע על ידך",
"totalSupplyIs0": "אין לך טוקנים של נזילות", "totalSupplyIs0": "אין לך טוקנים של נזילות",
"tokenWorth": "שווי כל טוקן נזילות הינו", "tokenWorth": "שווי כל טוקן נזילות הינו",
"firstLiquidity": "את\ה הראשון\ה שמזרים נזילות למאגר", "firstLiquidity": "אתה הראשוןה שמזרים נזילות למאגר",
"initialExchangeRate": "ושל האית'ר הינן בערך שווה {{ label }} תוודאו שההפקדה של הטוקן", "initialExchangeRate": "ושל האית'ר הינן בערך שווה {{ label }} תוודאו שההפקדה של הטוקן",
"removeLiquidity": "הוצאה של נזילות", "removeLiquidity": "הוצאה של נזילות",
"poolTokens": "טוקנים של מאגר הנזילות", "poolTokens": "טוקנים של מאגר הנזילות",

View File

@@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { CheckCircle, Triangle, ExternalLink as LinkIcon } from 'react-feather' import { CheckCircle, Triangle } from 'react-feather'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { getEtherscanLink } from '../../utils' import { getEtherscanLink } from '../../utils'
@@ -50,8 +50,7 @@ export default function Transaction({ hash }: { hash: string }) {
<TransactionWrapper> <TransactionWrapper>
<TransactionState href={getEtherscanLink(chainId, hash, 'transaction')} pending={pending} success={success}> <TransactionState href={getEtherscanLink(chainId, hash, 'transaction')} pending={pending} success={success}>
<RowFixed> <RowFixed>
<TransactionStatusText>{summary ? summary : hash}</TransactionStatusText> <TransactionStatusText>{summary ?? hash} </TransactionStatusText>
<LinkIcon size={16} />
</RowFixed> </RowFixed>
<IconWrapper pending={pending} success={success}> <IconWrapper pending={pending} success={success}>
{pending ? <Loader /> : success ? <CheckCircle size="16" /> : <Triangle size="16" />} {pending ? <Loader /> : success ? <CheckCircle size="16" /> : <Triangle size="16" />}

View File

@@ -1,8 +1,6 @@
import React, { useState, useEffect, useContext } from 'react' import React, { useContext, useCallback } from 'react'
import styled, { ThemeContext } from 'styled-components' import styled, { ThemeContext } from 'styled-components'
import useDebounce from '../../hooks/useDebounce' import useENS from '../../hooks/useENS'
import { isAddress } from '../../utils'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { ExternalLink, TYPE } from '../../theme' import { ExternalLink, TYPE } from '../../theme'
import { AutoColumn } from '../Column' import { AutoColumn } from '../Column'
@@ -24,6 +22,8 @@ const ContainerRow = styled.div<{ error: boolean }>`
align-items: center; align-items: center;
border-radius: 1.25rem; border-radius: 1.25rem;
border: 1px solid ${({ error, theme }) => (error ? theme.red1 : theme.bg2)}; border: 1px solid ${({ error, theme }) => (error ? theme.red1 : theme.bg2)};
transition: border-color 300ms ${({ error }) => (error ? 'step-end' : 'step-start')},
color 500ms ${({ error }) => (error ? 'step-end' : 'step-start')};
background-color: ${({ theme }) => theme.bg1}; background-color: ${({ theme }) => theme.bg1};
` `
@@ -39,6 +39,7 @@ const Input = styled.input<{ error?: boolean }>`
flex: 1 1 auto; flex: 1 1 auto;
width: 0; width: 0;
background-color: ${({ theme }) => theme.bg1}; background-color: ${({ theme }) => theme.bg1};
transition: color 300ms ${({ error }) => (error ? 'step-end' : 'step-start')};
color: ${({ error, theme }) => (error ? theme.red1 : theme.primary1)}; color: ${({ error, theme }) => (error ? theme.red1 : theme.primary1)};
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -64,120 +65,65 @@ const Input = styled.input<{ error?: boolean }>`
} }
` `
interface Value {
address: string
name?: string
}
export default function AddressInputPanel({ export default function AddressInputPanel({
initialInput = '', id,
onChange, value,
onError onChange
}: { }: {
initialInput?: string id?: string
onChange: (val: { address: string; name?: string }) => void // the typed string value
onError: (error: boolean, input: string) => void value: string
// triggers whenever the typed value changes
onChange: (value: string) => void
}) { }) {
const { chainId, library } = useActiveWeb3React() const { chainId } = useActiveWeb3React()
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
const [input, setInput] = useState(initialInput ? initialInput : '') const { address, loading, name } = useENS(value)
const debouncedInput = useDebounce(input, 200)
const [data, setData] = useState<{ address: string; name: string }>({ address: undefined, name: undefined }) const handleInput = useCallback(
const [error, setError] = useState<boolean>(false) event => {
const input = event.target.value
const withoutSpaces = input.replace(/\s+/g, '')
onChange(withoutSpaces)
},
[onChange]
)
// keep data and errors in sync const error = Boolean(value.length > 0 && !loading && !address)
useEffect(() => {
onChange({ address: data.address, name: data.name })
}, [onChange, data.address, data.name])
useEffect(() => {
onError(error, input)
}, [onError, error, input])
// run parser on debounced input
useEffect(() => {
let stale = false
// if the input is an address, try to look up its name
if (isAddress(debouncedInput)) {
library
.lookupAddress(debouncedInput)
.then(name => {
if (stale) return
// if an ENS name exists, set it as the destination
if (name) {
setInput(name)
} else {
setData({ address: debouncedInput, name: '' })
setError(null)
}
})
.catch(() => {
if (stale) return
setData({ address: debouncedInput, name: '' })
setError(null)
})
}
// otherwise try to look up the address of the input, treated as an ENS name
else {
if (debouncedInput !== '') {
library
.resolveName(debouncedInput)
.then(address => {
if (stale) return
// if the debounced input name resolves to an address
if (address) {
setData({ address: address, name: debouncedInput })
setError(null)
} else {
setError(true)
}
})
.catch(() => {
if (stale) return
setError(true)
})
} else if (debouncedInput === '') {
setError(true)
}
}
return () => {
stale = true
}
}, [debouncedInput, library])
function onInput(event) {
setData({ address: undefined, name: undefined })
setError(false)
const input = event.target.value
const checksummedInput = isAddress(input.replace(/\s/g, '')) // delete whitespace
setInput(checksummedInput || input)
}
return ( return (
<InputPanel> <InputPanel id={id}>
<ContainerRow error={input !== '' && error}> <ContainerRow error={error}>
<InputContainer> <InputContainer>
<AutoColumn gap="md"> <AutoColumn gap="md">
<RowBetween> <RowBetween>
<TYPE.black color={theme.text2} fontWeight={500} fontSize={14}> <TYPE.black color={theme.text2} fontWeight={500} fontSize={14}>
Recipient Recipient
</TYPE.black> </TYPE.black>
{data.address && ( {address && (
<ExternalLink <ExternalLink href={getEtherscanLink(chainId, name ?? address, 'address')} style={{ fontSize: '14px' }}>
href={getEtherscanLink(chainId, data.name || data.address, 'address')}
style={{ fontSize: '14px' }}
>
(View on Etherscan) (View on Etherscan)
</ExternalLink> </ExternalLink>
)} )}
</RowBetween> </RowBetween>
<Input <Input
className="recipient-address-input"
type="text" type="text"
autoComplete="off" autoComplete="off"
autoCorrect="off" autoCorrect="off"
autoCapitalize="off" autoCapitalize="off"
spellCheck="false" spellCheck="false"
placeholder="Wallet Address or ENS name" placeholder="Wallet Address or ENS name"
error={input !== '' && error} error={error}
onChange={onInput} pattern="^(0x[a-fA-F0-9]{40})$"
value={input} onChange={handleInput}
value={value}
/> />
</AutoColumn> </AutoColumn>
</InputContainer> </InputContainer>

View File

@@ -21,6 +21,7 @@ const Base = styled(RebassButton)<{
outline: none; outline: none;
border: 1px solid transparent; border: 1px solid transparent;
color: white; color: white;
text-decoration: none;
display: flex; display: flex;
justify-content: center; justify-content: center;
flex-wrap: nowrap; flex-wrap: nowrap;

View File

@@ -1,12 +1,11 @@
import { Pair, Token } from '@uniswap/sdk' import { Currency, Pair } from '@uniswap/sdk'
import React, { useState, useContext, useCallback } from 'react' import React, { useState, useContext, useCallback } from 'react'
import styled, { ThemeContext } from 'styled-components' import styled, { ThemeContext } from 'styled-components'
import { darken } from 'polished' import { darken } from 'polished'
import { Field } from '../../state/swap/actions' import { useCurrencyBalance } from '../../state/wallet/hooks'
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks' import CurrencySearchModal from '../SearchModal/CurrencySearchModal'
import TokenSearchModal from '../SearchModal/TokenSearchModal' import CurrencyLogo from '../CurrencyLogo'
import TokenLogo from '../TokenLogo' import DoubleCurrencyLogo from '../DoubleLogo'
import DoubleLogo from '../DoubleLogo'
import { RowBetween } from '../Row' import { RowBetween } from '../Row'
import { TYPE, CursorPointer } from '../../theme' import { TYPE, CursorPointer } from '../../theme'
import { Input as NumericalInput } from '../NumericalInput' import { Input as NumericalInput } from '../NumericalInput'
@@ -117,46 +116,44 @@ const StyledBalanceMax = styled.button`
interface CurrencyInputPanelProps { interface CurrencyInputPanelProps {
value: string value: string
field: string onUserInput: (value: string) => void
onUserInput: (field: string, val: string) => void
onMax?: () => void onMax?: () => void
showMaxButton: boolean showMaxButton: boolean
label?: string label?: string
onTokenSelection?: (tokenAddress: string) => void onCurrencySelect?: (currency: Currency) => void
token?: Token | null currency?: Currency | null
disableTokenSelect?: boolean disableCurrencySelect?: boolean
hideBalance?: boolean hideBalance?: boolean
isExchange?: boolean
pair?: Pair | null pair?: Pair | null
hideInput?: boolean hideInput?: boolean
showSendWithSwap?: boolean showSendWithSwap?: boolean
otherSelectedTokenAddress?: string | null otherCurrency?: Currency | null
id: string id: string
showCommonBases?: boolean
} }
export default function CurrencyInputPanel({ export default function CurrencyInputPanel({
value, value,
field,
onUserInput, onUserInput,
onMax, onMax,
showMaxButton, showMaxButton,
label = 'Input', label = 'Input',
onTokenSelection = null, onCurrencySelect = null,
token = null, currency = null,
disableTokenSelect = false, disableCurrencySelect = false,
hideBalance = false, hideBalance = false,
isExchange = false,
pair = null, // used for double token logo pair = null, // used for double token logo
hideInput = false, hideInput = false,
showSendWithSwap = false, showSendWithSwap = false,
otherSelectedTokenAddress = null, otherCurrency = null,
id id,
showCommonBases
}: CurrencyInputPanelProps) { }: CurrencyInputPanelProps) {
const { t } = useTranslation() const { t } = useTranslation()
const [modalOpen, setModalOpen] = useState(false) const [modalOpen, setModalOpen] = useState(false)
const { account } = useActiveWeb3React() const { account } = useActiveWeb3React()
const userTokenBalance = useTokenBalanceTreatingWETHasETH(account, token) const selectedCurrencyBalance = useCurrencyBalance(account, currency)
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
const handleDismissSearch = useCallback(() => { const handleDismissSearch = useCallback(() => {
@@ -181,8 +178,8 @@ export default function CurrencyInputPanel({
fontSize={14} fontSize={14}
style={{ display: 'inline' }} style={{ display: 'inline' }}
> >
{!hideBalance && !!token && userTokenBalance {!hideBalance && !!currency && selectedCurrencyBalance
? 'Balance: ' + userTokenBalance?.toSignificant(6) ? 'Balance: ' + selectedCurrencyBalance?.toSignificant(6)
: ' -'} : ' -'}
</TYPE.body> </TYPE.body>
</CursorPointer> </CursorPointer>
@@ -190,63 +187,63 @@ export default function CurrencyInputPanel({
</RowBetween> </RowBetween>
</LabelRow> </LabelRow>
)} )}
<InputRow style={hideInput ? { padding: '0', borderRadius: '8px' } : {}} selected={disableTokenSelect}> <InputRow style={hideInput ? { padding: '0', borderRadius: '8px' } : {}} selected={disableCurrencySelect}>
{!hideInput && ( {!hideInput && (
<> <>
<NumericalInput <NumericalInput
className="token-amount-input" className="token-amount-input"
value={value} value={value}
onUserInput={val => { onUserInput={val => {
onUserInput(field, val) onUserInput(val)
}} }}
/> />
{account && !!token?.address && showMaxButton && label !== 'To' && ( {account && currency && showMaxButton && label !== 'To' && (
<StyledBalanceMax onClick={onMax}>MAX</StyledBalanceMax> <StyledBalanceMax onClick={onMax}>MAX</StyledBalanceMax>
)} )}
</> </>
)} )}
<CurrencySelect <CurrencySelect
selected={!!token} selected={!!currency}
className="open-currency-select-button" className="open-currency-select-button"
onClick={() => { onClick={() => {
if (!disableTokenSelect) { if (!disableCurrencySelect) {
setModalOpen(true) setModalOpen(true)
} }
}} }}
> >
<Aligner> <Aligner>
{isExchange ? ( {pair ? (
<DoubleLogo a0={pair?.token0.address} a1={pair?.token1.address} size={24} margin={true} /> <DoubleCurrencyLogo currency0={pair.token0} currency1={pair.token1} size={24} margin={true} />
) : token?.address ? ( ) : currency ? (
<TokenLogo address={token?.address} size={'24px'} /> <CurrencyLogo currency={currency} size={'24px'} />
) : null} ) : null}
{isExchange ? ( {pair ? (
<StyledTokenName className="pair-name-container"> <StyledTokenName className="pair-name-container">
{pair?.token0.symbol}:{pair?.token1.symbol} {pair?.token0.symbol}:{pair?.token1.symbol}
</StyledTokenName> </StyledTokenName>
) : ( ) : (
<StyledTokenName className="token-symbol-container" active={Boolean(token && token.symbol)}> <StyledTokenName className="token-symbol-container" active={Boolean(currency && currency.symbol)}>
{(token && token.symbol && token.symbol.length > 20 {(currency && currency.symbol && currency.symbol.length > 20
? token.symbol.slice(0, 4) + ? currency.symbol.slice(0, 4) +
'...' + '...' +
token.symbol.slice(token.symbol.length - 5, token.symbol.length) currency.symbol.slice(currency.symbol.length - 5, currency.symbol.length)
: token?.symbol) || t('selectToken')} : currency?.symbol) || t('selectToken')}
</StyledTokenName> </StyledTokenName>
)} )}
{!disableTokenSelect && <StyledDropDown selected={!!token?.address} />} {!disableCurrencySelect && <StyledDropDown selected={!!currency} />}
</Aligner> </Aligner>
</CurrencySelect> </CurrencySelect>
</InputRow> </InputRow>
</Container> </Container>
{!disableTokenSelect && ( {!disableCurrencySelect && (
<TokenSearchModal <CurrencySearchModal
isOpen={modalOpen} isOpen={modalOpen}
onDismiss={handleDismissSearch} onDismiss={handleDismissSearch}
onTokenSelect={onTokenSelection} onCurrencySelect={onCurrencySelect}
showSendWithSwap={showSendWithSwap} showSendWithSwap={showSendWithSwap}
hiddenToken={token?.address} hiddenCurrency={currency}
otherSelectedTokenAddress={otherSelectedTokenAddress} otherSelectedCurrency={otherCurrency}
otherSelectedText={field === Field.INPUT ? 'Selected as output' : 'Selected as input'} showCommonBases={showCommonBases}
/> />
)} )}
</InputPanel> </InputPanel>

View File

@@ -0,0 +1,94 @@
import { Currency, ETHER, Token } from '@uniswap/sdk'
import React, { useState } from 'react'
import styled from 'styled-components'
import EthereumLogo from '../../assets/images/ethereum-logo.png'
import { WrappedTokenInfo } from '../../state/lists/hooks'
import uriToHttp from '../../utils/uriToHttp'
const getTokenLogoURL = address =>
`https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${address}/logo.png`
const BAD_URIS: { [tokenAddress: string]: true } = {}
const Image = styled.img<{ size: string }>`
width: ${({ size }) => size};
height: ${({ size }) => size};
background-color: white;
border-radius: 1rem;
box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075);
`
const Emoji = styled.span<{ size?: string }>`
display: flex;
align-items: center;
justify-content: center;
font-size: ${({ size }) => size};
width: ${({ size }) => size};
height: ${({ size }) => size};
margin-bottom: -4px;
`
const StyledEthereumLogo = styled.img<{ size: string }>`
width: ${({ size }) => size};
height: ${({ size }) => size};
box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075);
border-radius: 24px;
`
export default function CurrencyLogo({
currency,
size = '24px',
...rest
}: {
currency?: Currency
size?: string
style?: React.CSSProperties
}) {
const [, refresh] = useState<number>(0)
if (currency === ETHER) {
return <StyledEthereumLogo src={EthereumLogo} size={size} {...rest} />
}
if (currency instanceof Token) {
let uri: string | undefined
if (currency instanceof WrappedTokenInfo) {
if (currency.logoURI && !BAD_URIS[currency.logoURI]) {
uri = uriToHttp(currency.logoURI).filter(s => !BAD_URIS[s])[0]
}
}
if (!uri) {
const defaultUri = getTokenLogoURL(currency.address)
if (!BAD_URIS[defaultUri]) {
uri = defaultUri
}
}
if (uri) {
return (
<Image
{...rest}
alt={`${currency.name} Logo`}
src={uri}
size={size}
onError={() => {
if (currency instanceof Token) {
BAD_URIS[uri] = true
}
refresh(i => i + 1)
}}
/>
)
}
}
return (
<Emoji {...rest} size={size}>
<span role="img" aria-label="Thinking">
🤔
</span>
</Emoji>
)
}

View File

@@ -1,34 +1,40 @@
import { Currency } from '@uniswap/sdk'
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import TokenLogo from '../TokenLogo' import CurrencyLogo from '../CurrencyLogo'
const TokenWrapper = styled.div<{ margin: boolean; sizeraw: number }>` const Wrapper = styled.div<{ margin: boolean; sizeraw: number }>`
position: relative; position: relative;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
margin-right: ${({ sizeraw, margin }) => margin && (sizeraw / 3 + 8).toString() + 'px'}; margin-right: ${({ sizeraw, margin }) => margin && (sizeraw / 3 + 8).toString() + 'px'};
` `
interface DoubleTokenLogoProps { interface DoubleCurrencyLogoProps {
margin?: boolean margin?: boolean
size?: number size?: number
a0: string currency0?: Currency
a1?: string currency1?: Currency
} }
const HigherLogo = styled(TokenLogo)` const HigherLogo = styled(CurrencyLogo)`
z-index: 2; z-index: 2;
` `
const CoveredLogo = styled(TokenLogo)<{ sizeraw: number }>` const CoveredLogo = styled(CurrencyLogo)<{ sizeraw: number }>`
position: absolute; position: absolute;
left: ${({ sizeraw }) => (sizeraw / 2).toString() + 'px'}; left: ${({ sizeraw }) => (sizeraw / 2).toString() + 'px'};
` `
export default function DoubleTokenLogo({ a0, a1, size = 16, margin = false }: DoubleTokenLogoProps) { export default function DoubleCurrencyLogo({
currency0,
currency1,
size = 16,
margin = false
}: DoubleCurrencyLogoProps) {
return ( return (
<TokenWrapper sizeraw={size} margin={margin}> <Wrapper sizeraw={size} margin={margin}>
<HigherLogo address={a0} size={size.toString() + 'px'} /> {currency0 && <HigherLogo currency={currency0} size={size.toString() + 'px'} />}
{a1 && <CoveredLogo address={a1} size={size.toString() + 'px'} sizeraw={size} />} {currency1 && <CoveredLogo currency={currency1} size={size.toString() + 'px'} sizeraw={size} />}
</TokenWrapper> </Wrapper>
) )
} }

View File

@@ -1,35 +0,0 @@
import React from 'react'
import styled from 'styled-components'
import { Send, Sun, Moon } from 'react-feather'
import { useDarkModeManager } from '../../state/user/hooks'
import { ButtonSecondary } from '../Button'
const FooterFrame = styled.div`
display: flex;
align-items: center;
justify-content: flex-end;
position: fixed;
right: 1rem;
bottom: 1rem;
${({ theme }) => theme.mediaWidth.upToExtraSmall`
display: none;
`};
`
export default function Footer() {
const [darkMode, toggleDarkMode] = useDarkModeManager()
return (
<FooterFrame>
<form action="https://forms.gle/DaLuqvJsVhVaAM3J9" target="_blank">
<ButtonSecondary p="8px 12px">
<Send size={16} style={{ marginRight: '8px' }} /> Feedback
</ButtonSecondary>
</form>
<ButtonSecondary onClick={toggleDarkMode} p="8px 12px" ml="0.5rem" width="min-content">
{darkMode ? <Sun size={16} /> : <Moon size={16} />}
</ButtonSecondary>
</FooterFrame>
)
}

View File

@@ -4,18 +4,19 @@ import { Link, useLocation } from 'react-router-dom'
import styled from 'styled-components' import styled from 'styled-components'
import useParsedQueryString from '../../hooks/useParsedQueryString' import useParsedQueryString from '../../hooks/useParsedQueryString'
import useToggledVersion, { Version } from '../../hooks/useToggledVersion' import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
import { MouseoverTooltip } from '../Tooltip'
const VersionLabel = styled.span<{ enabled: boolean }>` const VersionLabel = styled.span<{ enabled: boolean }>`
padding: ${({ enabled }) => (enabled ? '0.15rem 0.5rem 0.16rem 0.45rem' : '0.15rem 0.5rem 0.16rem 0.35rem')}; padding: 0.35rem 0.6rem;
border-radius: 14px; border-radius: 12px;
background: ${({ theme, enabled }) => (enabled ? theme.primary1 : 'none')}; background: ${({ theme, enabled }) => (enabled ? theme.primary1 : 'none')};
color: ${({ theme, enabled }) => (enabled ? theme.white : theme.primary1)}; color: ${({ theme, enabled }) => (enabled ? theme.white : theme.text1)};
font-size: 0.825rem; font-size: 1rem;
font-weight: 400; font-weight: ${({ enabled }) => (enabled ? '500' : '400')};
:hover { :hover {
user-select: ${({ enabled }) => (enabled ? 'none' : 'initial')}; user-select: ${({ enabled }) => (enabled ? 'none' : 'initial')};
background: ${({ theme, enabled }) => (enabled ? theme.primary1 : 'none')}; background: ${({ theme, enabled }) => (enabled ? theme.primary1 : 'none')};
color: ${({ theme, enabled }) => (enabled ? theme.white : theme.primary3)}; color: ${({ theme, enabled }) => (enabled ? theme.white : theme.text1)};
} }
` `
@@ -25,21 +26,21 @@ interface VersionToggleProps extends React.ComponentProps<typeof Link> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const VersionToggle = styled(({ enabled, ...rest }: VersionToggleProps) => <Link {...rest} />)<VersionToggleProps>` const VersionToggle = styled(({ enabled, ...rest }: VersionToggleProps) => <Link {...rest} />)<VersionToggleProps>`
border-radius: 16px; border-radius: 12px;
opacity: ${({ enabled }) => (enabled ? 1 : 0.5)}; opacity: ${({ enabled }) => (enabled ? 1 : 0.5)};
cursor: ${({ enabled }) => (enabled ? 'pointer' : 'default')}; cursor: ${({ enabled }) => (enabled ? 'pointer' : 'default')};
background: ${({ theme }) => theme.primary5}; background: ${({ theme }) => theme.bg3};
border: 1px solid ${({ theme }) => theme.primary4};
color: ${({ theme }) => theme.primary1}; color: ${({ theme }) => theme.primary1};
display: flex; display: flex;
width: fit-content; width: fit-content;
margin-left: 0.5rem;
text-decoration: none; text-decoration: none;
:hover { :hover {
text-decoration: none; text-decoration: none;
} }
` `
export function VersionSwitch() { export default function VersionSwitch() {
const version = useToggledVersion() const version = useToggledVersion()
const location = useLocation() const location = useLocation()
const query = useParsedQueryString() const query = useParsedQueryString()
@@ -61,10 +62,15 @@ export function VersionSwitch() {
[versionSwitchAvailable] [versionSwitchAvailable]
) )
return ( const toggle = (
<VersionToggle enabled={versionSwitchAvailable} to={toggleDest} onClick={handleClick}> <VersionToggle enabled={versionSwitchAvailable} to={toggleDest} onClick={handleClick}>
<VersionLabel enabled={version === Version.v2 || !versionSwitchAvailable}>V2</VersionLabel> <VersionLabel enabled={version === Version.v2 || !versionSwitchAvailable}>V2</VersionLabel>
<VersionLabel enabled={version === Version.v1 && versionSwitchAvailable}>V1</VersionLabel> <VersionLabel enabled={version === Version.v1 && versionSwitchAvailable}>V1</VersionLabel>
</VersionToggle> </VersionToggle>
) )
return versionSwitchAvailable ? (
toggle
) : (
<MouseoverTooltip text="This page is only compatible with Uniswap V2.">{toggle}</MouseoverTooltip>
)
} }

View File

@@ -1,7 +1,6 @@
import { ChainId, WETH } from '@uniswap/sdk' import { ChainId } from '@uniswap/sdk'
import React from 'react' import React from 'react'
import { isMobile } from 'react-device-detect' import { isMobile } from 'react-device-detect'
import { Link as HistoryLink } from 'react-router-dom'
import { Text } from 'rebass' import { Text } from 'rebass'
import styled from 'styled-components' import styled from 'styled-components'
@@ -12,17 +11,15 @@ import Wordmark from '../../assets/svg/wordmark.svg'
import WordmarkDark from '../../assets/svg/wordmark_white.svg' import WordmarkDark from '../../assets/svg/wordmark_white.svg'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { useDarkModeManager } from '../../state/user/hooks' import { useDarkModeManager } from '../../state/user/hooks'
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks' import { useETHBalances } from '../../state/wallet/hooks'
import { ExternalLink, StyledInternalLink } from '../../theme'
import { YellowCard } from '../Card' import { YellowCard } from '../Card'
import { AutoColumn } from '../Column'
import Settings from '../Settings' import Settings from '../Settings'
import Menu from '../Menu' import Menu from '../Menu'
import Row, { RowBetween } from '../Row' import Row, { RowBetween } from '../Row'
import Web3Status from '../Web3Status' import Web3Status from '../Web3Status'
import { VersionSwitch } from './VersionSwitch' import VersionSwitch from './VersionSwitch'
const HeaderFrame = styled.div` const HeaderFrame = styled.div`
display: flex; display: flex;
@@ -45,7 +42,16 @@ const HeaderElement = styled.div`
align-items: center; align-items: center;
` `
const Title = styled.div` const HeaderElementWrap = styled.div`
display: flex;
align-items: center;
${({ theme }) => theme.mediaWidth.upToSmall`
margin-top: 0.5rem;
`};
`
const Title = styled.a`
display: flex; display: flex;
align-items: center; align-items: center;
pointer-events: auto; pointer-events: auto;
@@ -70,6 +76,7 @@ const AccountElement = styled.div<{ active: boolean }>`
background-color: ${({ theme, active }) => (!active ? theme.bg1 : theme.bg3)}; background-color: ${({ theme, active }) => (!active ? theme.bg1 : theme.bg3)};
border-radius: 12px; border-radius: 12px;
white-space: nowrap; white-space: nowrap;
width: 100%;
:focus { :focus {
border: 1px solid blue; border: 1px solid blue;
@@ -80,10 +87,7 @@ const TestnetWrapper = styled.div`
white-space: nowrap; white-space: nowrap;
width: fit-content; width: fit-content;
margin-left: 10px; margin-left: 10px;
pointer-events: auto;
${({ theme }) => theme.mediaWidth.upToSmall`
display: none;
`};
` `
const NetworkCard = styled(YellowCard)` const NetworkCard = styled(YellowCard)`
@@ -93,29 +97,31 @@ const NetworkCard = styled(YellowCard)`
padding: 8px 12px; padding: 8px 12px;
` `
const UniIcon = styled(HistoryLink)<{ to: string }>` const UniIcon = styled.div`
transition: transform 0.3s ease; transition: transform 0.3s ease;
:hover { :hover {
transform: rotate(-5deg); transform: rotate(-5deg);
} }
${({ theme }) => theme.mediaWidth.upToSmall`
img {
width: 4.5rem;
}
`};
` `
const MigrateBanner = styled(AutoColumn)` const HeaderControls = styled.div`
width: 100%;
padding: 12px 0;
display: flex; display: flex;
justify-content: center; flex-direction: row;
background-color: ${({ theme }) => theme.primary5}; align-items: center;
color: ${({ theme }) => theme.primaryText1};
font-weight: 400;
text-align: center;
pointer-events: auto;
a {
color: ${({ theme }) => theme.primaryText1};
}
${({ theme }) => theme.mediaWidth.upToSmall` ${({ theme }) => theme.mediaWidth.upToSmall`
padding: 0; flex-direction: column;
align-items: flex-end;
`};
`
const BalanceText = styled(Text)`
${({ theme }) => theme.mediaWidth.upToExtraSmall`
display: none; display: none;
`}; `};
` `
@@ -128,68 +134,45 @@ const NETWORK_LABELS: { [chainId in ChainId]: string | null } = {
[ChainId.KOVAN]: 'Kovan' [ChainId.KOVAN]: 'Kovan'
} }
const BalanceWrapper = styled.div`
${({ theme }) => theme.mediaWidth.upToExtraSmall`
display: none;
`};
`
export default function Header() { export default function Header() {
const { account, chainId } = useActiveWeb3React() const { account, chainId } = useActiveWeb3React()
const userEthBalance = useTokenBalanceTreatingWETHasETH(account, WETH[chainId]) const userEthBalance = useETHBalances([account])[account]
const [isDark] = useDarkModeManager() const [isDark] = useDarkModeManager()
return ( return (
<HeaderFrame> <HeaderFrame>
<MigrateBanner> <RowBetween style={{ alignItems: 'flex-start' }} padding="1rem 1rem 0 1rem">
Uniswap V2 is live! Read the&nbsp;
<ExternalLink href="https://uniswap.org/blog/launch-uniswap-v2/">
<b>blog post </b>
</ExternalLink>
&nbsp;or&nbsp;
<StyledInternalLink to="/migrate/v1">
<b>migrate your liquidity </b>
</StyledInternalLink>
.
</MigrateBanner>
<RowBetween padding="1rem">
<HeaderElement> <HeaderElement>
<Title> <Title href=".">
<UniIcon id="link" to="/"> <UniIcon>
<img src={isDark ? LogoDark : Logo} alt="logo" /> <img src={isDark ? LogoDark : Logo} alt="logo" />
</UniIcon> </UniIcon>
{!isMobile && ( <TitleText>
<TitleText> <img style={{ marginLeft: '4px', marginTop: '4px' }} src={isDark ? WordmarkDark : Wordmark} alt="logo" />
<HistoryLink id="link" to="/"> </TitleText>
<img
style={{ marginLeft: '4px', marginTop: '4px' }}
src={isDark ? WordmarkDark : Wordmark}
alt="logo"
/>
</HistoryLink>
</TitleText>
)}
</Title> </Title>
<TestnetWrapper style={{ pointerEvents: 'auto' }}>{!isMobile && <VersionSwitch />}</TestnetWrapper>
</HeaderElement> </HeaderElement>
<HeaderElement> <HeaderControls>
<TestnetWrapper> <HeaderElement>
{!isMobile && NETWORK_LABELS[chainId] && <NetworkCard>{NETWORK_LABELS[chainId]}</NetworkCard>} <TestnetWrapper>
</TestnetWrapper> {!isMobile && NETWORK_LABELS[chainId] && <NetworkCard>{NETWORK_LABELS[chainId]}</NetworkCard>}
<AccountElement active={!!account} style={{ pointerEvents: 'auto' }}> </TestnetWrapper>
<BalanceWrapper> <AccountElement active={!!account} style={{ pointerEvents: 'auto' }}>
{account && userEthBalance ? ( {account && userEthBalance ? (
<Text style={{ flexShrink: 0 }} pl="0.75rem" pr="0.5rem" fontWeight={500}> <BalanceText style={{ flexShrink: 0 }} pl="0.75rem" pr="0.5rem" fontWeight={500}>
{userEthBalance?.toSignificant(4)} ETH {userEthBalance?.toSignificant(4)} ETH
</Text> </BalanceText>
) : null} ) : null}
</BalanceWrapper> <Web3Status />
<Web3Status /> </AccountElement>
</AccountElement> </HeaderElement>
<Settings /> <HeaderElementWrap>
<Menu /> <VersionSwitch />
</HeaderElement> <Settings />
<Menu />
</HeaderElementWrap>
</HeaderControls>
</RowBetween> </RowBetween>
</HeaderFrame> </HeaderFrame>
) )

View File

@@ -77,7 +77,7 @@ const MenuItem = styled(ExternalLink)`
} }
` `
const CODE_LINK = 'https://github.com/Uniswap/uniswap-frontend' const CODE_LINK = 'https://github.com/Uniswap/uniswap-interface'
export default function Menu() { export default function Menu() {
const node = useRef<HTMLDivElement>() const node = useRef<HTMLDivElement>()
@@ -121,7 +121,7 @@ export default function Menu() {
<Code size={14} /> <Code size={14} />
Code Code
</MenuItem> </MenuItem>
<MenuItem id="link" href="https://discord.gg/vXCdddD"> <MenuItem id="link" href="https://discord.gg/EwFs3Pp">
<MessageCircle size={14} /> <MessageCircle size={14} />
Discord Discord
</MenuItem> </MenuItem>

View File

@@ -112,17 +112,10 @@ export default function Modal({
const [{ y }, set] = useSpring(() => ({ y: 0, config: { mass: 1, tension: 210, friction: 20 } })) const [{ y }, set] = useSpring(() => ({ y: 0, config: { mass: 1, tension: 210, friction: 20 } }))
const bind = useGesture({ const bind = useGesture({
onDrag: state => { onDrag: state => {
let velocity = state.velocity
if (velocity < 1) {
velocity = 1
}
if (velocity > 8) {
velocity = 8
}
set({ set({
y: state.down ? state.movement[1] : 0 y: state.down ? state.movement[1] : 0
}) })
if (velocity > 3 && state.direction[1] > 0) { if (state.velocity > 3 && state.direction[1] > 0) {
onDismiss() onDismiss()
} }
} }
@@ -141,6 +134,8 @@ export default function Modal({
initialFocusRef={initialFocusRef} initialFocusRef={initialFocusRef}
mobile={true} mobile={true}
> >
{/* prevents the automatic focusing of inputs on mobile by the reach dialog */}
{initialFocusRef ? null : <div tabIndex={1} />}
<Spring // animation for entrance and exit <Spring // animation for entrance and exit
from={{ from={{
transform: isOpen ? 'translateY(200px)' : 'translateY(100px)' transform: isOpen ? 'translateY(200px)' : 'translateY(100px)'

View File

@@ -1,37 +1,18 @@
import React, { useCallback } from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { darken } from 'polished' import { darken } from 'polished'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { withRouter, NavLink, Link as HistoryLink, RouteComponentProps } from 'react-router-dom' import { NavLink, Link as HistoryLink } from 'react-router-dom'
import useBodyKeyDown from '../../hooks/useBodyKeyDown'
import { CursorPointer } from '../../theme'
import { ArrowLeft } from 'react-feather' import { ArrowLeft } from 'react-feather'
import { RowBetween } from '../Row' import { RowBetween } from '../Row'
import QuestionHelper from '../QuestionHelper' import QuestionHelper from '../QuestionHelper'
const tabOrder = [
{
path: '/swap',
textKey: 'swap',
regex: /\/swap/
},
{
path: '/send',
textKey: 'send',
regex: /\/send/
},
{
path: '/pool',
textKey: 'pool',
regex: /\/pool/
}
]
const Tabs = styled.div` const Tabs = styled.div`
${({ theme }) => theme.flexRowNoWrap} ${({ theme }) => theme.flexRowNoWrap}
align-items: center; align-items: center;
border-radius: 3rem; border-radius: 3rem;
justify-content: space-evenly;
` `
const activeClassName = 'ACTIVE' const activeClassName = 'ACTIVE'
@@ -43,7 +24,6 @@ const StyledNavLink = styled(NavLink).attrs({
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 3rem; height: 3rem;
flex: 1 0 auto;
border-radius: 3rem; border-radius: 3rem;
outline: none; outline: none;
cursor: pointer; cursor: pointer;
@@ -68,89 +48,54 @@ const ActiveText = styled.div`
font-size: 20px; font-size: 20px;
` `
const ArrowLink = styled(ArrowLeft)` const StyledArrowLeft = styled(ArrowLeft)`
color: ${({ theme }) => theme.text1}; color: ${({ theme }) => theme.text1};
` `
function NavigationTabs({ location: { pathname }, history }: RouteComponentProps<{}>) { export function SwapPoolTabs({ active }: { active: 'swap' | 'pool' }) {
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useCallback(
direction => {
const tabIndex = tabOrder.findIndex(({ regex }) => pathname.match(regex))
history.push(tabOrder[(tabIndex + tabOrder.length + direction) % tabOrder.length].path)
},
[pathname, history]
)
const navigateRight = useCallback(() => {
navigate(1)
}, [navigate])
const navigateLeft = useCallback(() => {
navigate(-1)
}, [navigate])
useBodyKeyDown('ArrowRight', navigateRight)
useBodyKeyDown('ArrowLeft', navigateLeft)
const adding = pathname.match('/add')
const removing = pathname.match('/remove')
const finding = pathname.match('/find')
const creating = pathname.match('/create')
return ( return (
<> <Tabs style={{ marginBottom: '20px' }}>
{adding || removing ? ( <StyledNavLink id={`swap-nav-link`} to={'/swap'} isActive={() => active === 'swap'}>
<Tabs> {t('swap')}
<RowBetween style={{ padding: '1rem' }}> </StyledNavLink>
<CursorPointer onClick={() => history.push('/pool')}> <StyledNavLink id={`pool-nav-link`} to={'/pool'} isActive={() => active === 'pool'}>
<ArrowLink /> {t('pool')}
</CursorPointer> </StyledNavLink>
<ActiveText>{adding ? 'Add' : 'Remove'} Liquidity</ActiveText> </Tabs>
<QuestionHelper
text={
adding
? 'When you add liquidity, you are given pool tokens representing your position. These tokens automatically earn fees proportional to your share of the pool, and can be redeemed at any time.'
: 'Removing pool tokens converts your position back into underlying tokens at the current rate, proportional to your share of the pool. Accrued fees are included in the amounts you receive.'
}
/>
</RowBetween>
</Tabs>
) : finding ? (
<Tabs>
<RowBetween style={{ padding: '1rem' }}>
<HistoryLink to="/pool">
<ArrowLink />
</HistoryLink>
<ActiveText>Import Pool</ActiveText>
<QuestionHelper text={"Use this tool to find pairs that don't automatically appear in the interface."} />
</RowBetween>
</Tabs>
) : creating ? (
<Tabs>
<RowBetween style={{ padding: '1rem' }}>
<HistoryLink to="/pool">
<ArrowLink />
</HistoryLink>
<ActiveText>Create Pool</ActiveText>
<QuestionHelper text={'Use this interface to create a new pool.'} />
</RowBetween>
</Tabs>
) : (
<Tabs style={{ marginBottom: '20px' }}>
{tabOrder.map(({ path, textKey, regex }) => (
<StyledNavLink
id={`${textKey}-nav-link`}
key={path}
to={path}
isActive={(_, { pathname }) => !!pathname.match(regex)}
>
{t(textKey)}
</StyledNavLink>
))}
</Tabs>
)}
</>
) )
} }
export default withRouter(NavigationTabs) export function FindPoolTabs() {
return (
<Tabs>
<RowBetween style={{ padding: '1rem' }}>
<HistoryLink to="/pool">
<StyledArrowLeft />
</HistoryLink>
<ActiveText>Import Pool</ActiveText>
<QuestionHelper text={"Use this tool to find pairs that don't automatically appear in the interface."} />
</RowBetween>
</Tabs>
)
}
export function AddRemoveTabs({ adding }: { adding: boolean }) {
return (
<Tabs>
<RowBetween style={{ padding: '1rem' }}>
<HistoryLink to="/pool">
<StyledArrowLeft />
</HistoryLink>
<ActiveText>{adding ? 'Add' : 'Remove'} Liquidity</ActiveText>
<QuestionHelper
text={
adding
? 'When you add liquidity, you are given pool tokens representing your position. These tokens automatically earn fees proportional to your share of the pool, and can be redeemed at any time.'
: 'Removing pool tokens converts your position back into underlying tokens at the current rate, proportional to your share of the pool. Accrued fees are included in the amounts you receive.'
}
/>
</RowBetween>
</Tabs>
)
}

View File

@@ -2,36 +2,16 @@ import { Placement } from '@popperjs/core'
import { transparentize } from 'polished' import { transparentize } from 'polished'
import React, { useState } from 'react' import React, { useState } from 'react'
import { usePopper } from 'react-popper' import { usePopper } from 'react-popper'
import styled, { keyframes } from 'styled-components' import styled from 'styled-components'
import useInterval from '../../hooks/useInterval' import useInterval from '../../hooks/useInterval'
import Portal from '@reach/portal' import Portal from '@reach/portal'
const fadeIn = keyframes`
from {
opacity : 0;
}
to {
opacity : 1;
}
`
const fadeOut = keyframes`
from {
opacity : 1;
}
to {
opacity : 0;
}
`
const PopoverContainer = styled.div<{ show: boolean }>` const PopoverContainer = styled.div<{ show: boolean }>`
z-index: 9999; z-index: 9999;
visibility: ${props => (!props.show ? 'hidden' : 'visible')}; visibility: ${props => (props.show ? 'visible' : 'hidden')};
animation: ${props => (!props.show ? fadeOut : fadeIn)} 150ms linear; opacity: ${props => (props.show ? 1 : 0)};
transition: visibility 150ms linear; transition: visibility 150ms linear, opacity 150ms linear;
background: ${({ theme }) => theme.bg2}; background: ${({ theme }) => theme.bg2};
border: 1px solid ${({ theme }) => theme.bg3}; border: 1px solid ${({ theme }) => theme.bg3};

View File

@@ -0,0 +1,72 @@
import { TokenList, Version } from '@uniswap/token-lists'
import React, { useCallback, useContext } from 'react'
import { AlertCircle, Info } from 'react-feather'
import { useDispatch } from 'react-redux'
import { ThemeContext } from 'styled-components'
import { AppDispatch } from '../../state'
import { useRemovePopup } from '../../state/application/hooks'
import { acceptListUpdate } from '../../state/lists/actions'
import { TYPE } from '../../theme'
import { ButtonPrimary, ButtonSecondary } from '../Button'
import { AutoColumn } from '../Column'
import { AutoRow } from '../Row'
function versionLabel(version: Version): string {
return `v${version.major}.${version.minor}.${version.patch}`
}
export default function ListUpdatePopup({
popKey,
listUrl,
oldList,
newList,
auto
}: {
popKey: string
listUrl: string
oldList: TokenList
newList: TokenList
auto: boolean
}) {
const removePopup = useRemovePopup()
const removeThisPopup = useCallback(() => removePopup(popKey), [popKey, removePopup])
const dispatch = useDispatch<AppDispatch>()
const theme = useContext(ThemeContext)
const updateList = useCallback(() => {
if (auto) return
dispatch(acceptListUpdate(listUrl))
removeThisPopup()
}, [auto, dispatch, listUrl, removeThisPopup])
return (
<AutoRow>
<div style={{ paddingRight: 16 }}>
{auto ? <Info color={theme.text2} size={24} /> : <AlertCircle color={theme.red1} size={24} />}{' '}
</div>
<AutoColumn style={{ flex: '1' }} gap="8px">
{auto ? (
<TYPE.body fontWeight={500}>
The token list &quot;{oldList.name}&quot; has been updated to{' '}
<strong>{versionLabel(newList.version)}</strong>.
</TYPE.body>
) : (
<>
<div>
A token list update is available for the list &quot;{oldList.name}&quot; ({versionLabel(oldList.version)}{' '}
to {versionLabel(newList.version)}).
</div>
<AutoRow>
<div style={{ flexGrow: 1, marginRight: 6 }}>
<ButtonPrimary onClick={updateList}>Update list</ButtonPrimary>
</div>
<div style={{ flexGrow: 1 }}>
<ButtonSecondary onClick={removeThisPopup}>Dismiss</ButtonSecondary>
</div>
</AutoRow>
</>
)}
</AutoColumn>
</AutoRow>
)
}

View File

@@ -0,0 +1,86 @@
import React, { useCallback, useContext, useState } from 'react'
import { X } from 'react-feather'
import styled, { ThemeContext } from 'styled-components'
import useInterval from '../../hooks/useInterval'
import { PopupContent } from '../../state/application/actions'
import { useRemovePopup } from '../../state/application/hooks'
import ListUpdatePopup from './ListUpdatePopup'
import TxnPopup from './TxnPopup'
export const StyledClose = styled(X)`
position: absolute;
right: 10px;
top: 10px;
:hover {
cursor: pointer;
}
`
export const Popup = styled.div`
display: inline-block;
width: 100%;
padding: 1em;
background-color: ${({ theme }) => theme.bg1};
position: relative;
border-radius: 10px;
padding: 20px;
padding-right: 35px;
z-index: 2;
overflow: hidden;
${({ theme }) => theme.mediaWidth.upToSmall`
min-width: 290px;
`}
`
const DELAY = 100
const Fader = styled.div<{ count: number }>`
position: absolute;
bottom: 0px;
left: 0px;
width: ${({ count }) => `calc(100% - (100% / ${150 / count}))`};
height: 2px;
background-color: ${({ theme }) => theme.bg3};
transition: width 100ms linear;
`
export default function PopupItem({ content, popKey }: { content: PopupContent; popKey: string }) {
const [count, setCount] = useState(1)
const [isRunning, setIsRunning] = useState(true)
const removePopup = useRemovePopup()
const removeThisPopup = useCallback(() => removePopup(popKey), [popKey, removePopup])
useInterval(
() => {
count > 150 ? removeThisPopup() : setCount(count + 1)
},
isRunning ? DELAY : null
)
const theme = useContext(ThemeContext)
const handleMouseEnter = useCallback(() => setIsRunning(false), [])
const handleMouseLeave = useCallback(() => setIsRunning(true), [])
let popupContent
if ('txn' in content) {
const {
txn: { hash, success, summary }
} = content
popupContent = <TxnPopup hash={hash} success={success} summary={summary} />
} else if ('listUpdate' in content) {
const {
listUpdate: { listUrl, oldList, newList, auto }
} = content
popupContent = <ListUpdatePopup popKey={popKey} listUrl={listUrl} oldList={oldList} newList={newList} auto={auto} />
}
return (
<Popup onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<StyledClose color={theme.text2} onClick={() => removePopup(popKey)} />
{popupContent}
<Fader count={count} />
</Popup>
)
}

View File

@@ -0,0 +1,27 @@
import React, { useContext } from 'react'
import { AlertCircle, CheckCircle } from 'react-feather'
import { ThemeContext } from 'styled-components'
import { useActiveWeb3React } from '../../hooks'
import { TYPE } from '../../theme'
import { ExternalLink } from '../../theme/components'
import { getEtherscanLink } from '../../utils'
import { AutoColumn } from '../Column'
import { AutoRow } from '../Row'
export default function TxnPopup({ hash, success, summary }: { hash: string; success?: boolean; summary?: string }) {
const { chainId } = useActiveWeb3React()
const theme = useContext(ThemeContext)
return (
<AutoRow>
<div style={{ paddingRight: 16 }}>
{success ? <CheckCircle color={theme.green1} size={24} /> : <AlertCircle color={theme.red1} size={24} />}
</div>
<AutoColumn gap="8px">
<TYPE.body fontWeight={500}>{summary ?? 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}</TYPE.body>
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>View on Etherscan</ExternalLink>
</AutoColumn>
</AutoRow>
)
}

View File

@@ -1,27 +1,9 @@
import { ChainId, Pair, Token } from '@uniswap/sdk' import React from 'react'
import React, { useContext, useMemo } from 'react' import styled from 'styled-components'
import styled, { ThemeContext } from 'styled-components'
import { useMediaLayout } from 'use-media' import { useMediaLayout } from 'use-media'
import { useActivePopups } from '../../state/application/hooks'
import { X } from 'react-feather'
import { PopupContent } from '../../state/application/actions'
import { useActivePopups, useRemovePopup } from '../../state/application/hooks'
import { ExternalLink } from '../../theme'
import { AutoColumn } from '../Column' import { AutoColumn } from '../Column'
import DoubleTokenLogo from '../DoubleLogo' import PopupItem from './PopupItem'
import Row from '../Row'
import TxnPopup from '../TxnPopup'
import { Text } from 'rebass'
const StyledClose = styled(X)`
position: absolute;
right: 10px;
top: 10px;
:hover {
cursor: pointer;
}
`
const MobilePopupWrapper = styled.div<{ height: string | number }>` const MobilePopupWrapper = styled.div<{ height: string | number }>`
position: relative; position: relative;
@@ -55,77 +37,9 @@ const FixedPopupColumn = styled(AutoColumn)`
`}; `};
` `
const Popup = styled.div`
display: inline-block;
width: 100%;
padding: 1em;
background-color: ${({ theme }) => theme.bg1};
position: relative;
border-radius: 10px;
padding: 20px;
padding-right: 35px;
z-index: 2;
overflow: hidden;
${({ theme }) => theme.mediaWidth.upToSmall`
min-width: 290px;
`}
`
function PoolPopup({
token0,
token1
}: {
token0: { address?: string; symbol?: string }
token1: { address?: string; symbol?: string }
}) {
const pairAddress: string | null = useMemo(() => {
if (!token0 || !token1) return null
// just mock it out
return Pair.getAddress(
new Token(ChainId.MAINNET, token0.address, 18),
new Token(ChainId.MAINNET, token1.address, 18)
)
}, [token0, token1])
return (
<AutoColumn gap={'10px'}>
<Text fontSize={20} fontWeight={500}>
Pool Imported
</Text>
<Row>
<DoubleTokenLogo a0={token0?.address ?? ''} a1={token1?.address ?? ''} margin={true} />
<Text fontSize={16} fontWeight={500}>
UNI {token0?.symbol} / {token1?.symbol}
</Text>
</Row>
{pairAddress ? (
<ExternalLink href={`https://uniswap.info/pair/${pairAddress}`}>View on Uniswap Info.</ExternalLink>
) : null}
</AutoColumn>
)
}
function PopupItem({ content, popKey }: { content: PopupContent; popKey: string }) {
if ('txn' in content) {
const {
txn: { hash, success, summary }
} = content
return <TxnPopup popKey={popKey} hash={hash} success={success} summary={summary} />
} else if ('poolAdded' in content) {
const {
poolAdded: { token0, token1 }
} = content
return <PoolPopup token0={token0} token1={token1} />
}
}
export default function Popups() { export default function Popups() {
const theme = useContext(ThemeContext)
// get all popups // get all popups
const activePopups = useActivePopups() const activePopups = useActivePopups()
const removePopup = useRemovePopup()
// switch view settings on mobile // switch view settings on mobile
const isMobile = useMediaLayout({ maxWidth: '600px' }) const isMobile = useMediaLayout({ maxWidth: '600px' })
@@ -133,14 +47,9 @@ export default function Popups() {
if (!isMobile) { if (!isMobile) {
return ( return (
<FixedPopupColumn gap="20px"> <FixedPopupColumn gap="20px">
{activePopups.map(item => { {activePopups.map(item => (
return ( <PopupItem key={item.key} content={item.content} popKey={item.key} />
<Popup key={item.key}> ))}
<StyledClose color={theme.text2} onClick={() => removePopup(item.key)} />
<PopupItem content={item.content} popKey={item.key} />
</Popup>
)
})}
</FixedPopupColumn> </FixedPopupColumn>
) )
} }
@@ -152,14 +61,9 @@ export default function Popups() {
{activePopups // reverse so new items up front {activePopups // reverse so new items up front
.slice(0) .slice(0)
.reverse() .reverse()
.map(item => { .map(item => (
return ( <PopupItem key={item.key} content={item.content} popKey={item.key} />
<Popup key={item.key}> ))}
<StyledClose color={theme.text2} onClick={() => removePopup(item.key)} />
<PopupItem content={item.content} popKey={item.key} />
</Popup>
)
})}
</MobilePopupInner> </MobilePopupInner>
</MobilePopupWrapper> </MobilePopupWrapper>
) )

View File

@@ -1,5 +1,5 @@
import React, { useContext } from 'react' import React, { useContext } from 'react'
import { RouteComponentProps, withRouter } from 'react-router-dom' import { Link, RouteComponentProps, withRouter } from 'react-router-dom'
import { Token, TokenAmount, WETH } from '@uniswap/sdk' import { Token, TokenAmount, WETH } from '@uniswap/sdk'
import { Text } from 'rebass' import { Text } from 'rebass'
@@ -7,7 +7,7 @@ import { AutoColumn } from '../Column'
import { ButtonSecondary } from '../Button' import { ButtonSecondary } from '../Button'
import { RowBetween, RowFixed } from '../Row' import { RowBetween, RowFixed } from '../Row'
import { FixedHeightRow, HoverCard } from './index' import { FixedHeightRow, HoverCard } from './index'
import DoubleTokenLogo from '../DoubleLogo' import DoubleCurrencyLogo from '../DoubleLogo'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { ThemeContext } from 'styled-components' import { ThemeContext } from 'styled-components'
@@ -16,7 +16,7 @@ interface PositionCardProps extends RouteComponentProps<{}> {
V1LiquidityBalance: TokenAmount V1LiquidityBalance: TokenAmount
} }
function V1PositionCard({ token, V1LiquidityBalance, history }: PositionCardProps) { function V1PositionCard({ token, V1LiquidityBalance }: PositionCardProps) {
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
const { chainId } = useActiveWeb3React() const { chainId } = useActiveWeb3React()
@@ -26,7 +26,7 @@ function V1PositionCard({ token, V1LiquidityBalance, history }: PositionCardProp
<AutoColumn gap="12px"> <AutoColumn gap="12px">
<FixedHeightRow> <FixedHeightRow>
<RowFixed> <RowFixed>
<DoubleTokenLogo a0={token.address} margin={true} size={20} /> <DoubleCurrencyLogo currency0={token} margin={true} size={20} />
<Text fontWeight={500} fontSize={20} style={{ marginLeft: '' }}> <Text fontWeight={500} fontSize={20} style={{ marginLeft: '' }}>
{`${token.equals(WETH[chainId]) ? 'WETH' : token.symbol}/ETH`} {`${token.equals(WETH[chainId]) ? 'WETH' : token.symbol}/ETH`}
</Text> </Text>
@@ -47,21 +47,15 @@ function V1PositionCard({ token, V1LiquidityBalance, history }: PositionCardProp
<AutoColumn gap="8px"> <AutoColumn gap="8px">
<RowBetween marginTop="10px"> <RowBetween marginTop="10px">
<ButtonSecondary <ButtonSecondary width="68%" as={Link} to={`/migrate/v1/${V1LiquidityBalance.token.address}`}>
width="68%"
onClick={() => {
history.push(`/migrate/v1/${V1LiquidityBalance.token.address}`)
}}
>
Migrate Migrate
</ButtonSecondary> </ButtonSecondary>
<ButtonSecondary <ButtonSecondary
style={{ backgroundColor: 'transparent' }} style={{ backgroundColor: 'transparent' }}
width="28%" width="28%"
onClick={() => { as={Link}
history.push(`/remove/v1/${V1LiquidityBalance.token.address}`) to={`/remove/v1/${V1LiquidityBalance.token.address}`}
}}
> >
Remove Remove
</ButtonSecondary> </ButtonSecondary>

View File

@@ -1,22 +1,24 @@
import React, { useState } from 'react' import { JSBI, Pair, Percent } from '@uniswap/sdk'
import styled from 'styled-components'
import { darken } from 'polished' import { darken } from 'polished'
import { RouteComponentProps, withRouter } from 'react-router-dom' import React, { useState } from 'react'
import { Percent, Pair, JSBI } from '@uniswap/sdk' import { ChevronDown, ChevronUp } from 'react-feather'
import { Link } from 'react-router-dom'
import { Text } from 'rebass'
import styled from 'styled-components'
import { useTotalSupply } from '../../data/TotalSupply'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { useTotalSupply } from '../../data/TotalSupply'
import { useTokenBalance } from '../../state/wallet/hooks' import { useTokenBalance } from '../../state/wallet/hooks'
import { ExternalLink } from '../../theme'
import { currencyId } from '../../utils/currencyId'
import { unwrappedToken } from '../../utils/wrappedCurrency'
import { ButtonSecondary } from '../Button'
import Card, { GreyCard } from '../Card' import Card, { GreyCard } from '../Card'
import TokenLogo from '../TokenLogo'
import DoubleLogo from '../DoubleLogo'
import { Text } from 'rebass'
import { ExternalLink } from '../../theme/components'
import { AutoColumn } from '../Column' import { AutoColumn } from '../Column'
import { ChevronDown, ChevronUp } from 'react-feather' import CurrencyLogo from '../CurrencyLogo'
import { ButtonSecondary } from '../Button' import DoubleCurrencyLogo from '../DoubleLogo'
import { RowBetween, RowFixed, AutoRow } from '../Row' import { AutoRow, RowBetween, RowFixed } from '../Row'
import { Dots } from '../swap/styleds' import { Dots } from '../swap/styleds'
export const FixedHeightRow = styled(RowBetween)` export const FixedHeightRow = styled(RowBetween)`
@@ -30,22 +32,107 @@ export const HoverCard = styled(Card)`
} }
` `
interface PositionCardProps extends RouteComponentProps<{}> { interface PositionCardProps {
pair: Pair pair: Pair
minimal?: boolean showUnwrapped?: boolean
border?: string border?: string
} }
function PositionCard({ pair, history, border, minimal = false }: PositionCardProps) { export function MinimalPositionCard({ pair, showUnwrapped = false, border }: PositionCardProps) {
const { account } = useActiveWeb3React() const { account } = useActiveWeb3React()
const token0 = pair?.token0 const currency0 = showUnwrapped ? pair.token0 : unwrappedToken(pair.token0)
const token1 = pair?.token1 const currency1 = showUnwrapped ? pair.token1 : unwrappedToken(pair.token1)
const [showMore, setShowMore] = useState(false) const [showMore, setShowMore] = useState(false)
const userPoolBalance = useTokenBalance(account, pair?.liquidityToken) const userPoolBalance = useTokenBalance(account, pair.liquidityToken)
const totalPoolTokens = useTotalSupply(pair?.liquidityToken) const totalPoolTokens = useTotalSupply(pair.liquidityToken)
const [token0Deposited, token1Deposited] =
!!pair &&
!!totalPoolTokens &&
!!userPoolBalance &&
// this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
JSBI.greaterThanOrEqual(totalPoolTokens.raw, userPoolBalance.raw)
? [
pair.getLiquidityValue(pair.token0, totalPoolTokens, userPoolBalance, false),
pair.getLiquidityValue(pair.token1, totalPoolTokens, userPoolBalance, false)
]
: [undefined, undefined]
return (
<>
{userPoolBalance && (
<GreyCard border={border}>
<AutoColumn gap="12px">
<FixedHeightRow>
<RowFixed>
<Text fontWeight={500} fontSize={16}>
Your position
</Text>
</RowFixed>
</FixedHeightRow>
<FixedHeightRow onClick={() => setShowMore(!showMore)}>
<RowFixed>
<DoubleCurrencyLogo currency0={currency0} currency1={currency1} margin={true} size={20} />
<Text fontWeight={500} fontSize={20}>
{currency0.symbol}/{currency1.symbol}
</Text>
</RowFixed>
<RowFixed>
<Text fontWeight={500} fontSize={20}>
{userPoolBalance ? userPoolBalance.toSignificant(4) : '-'}
</Text>
</RowFixed>
</FixedHeightRow>
<AutoColumn gap="4px">
<FixedHeightRow>
<Text color="#888D9B" fontSize={16} fontWeight={500}>
{currency0.symbol}:
</Text>
{token0Deposited ? (
<RowFixed>
<Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}>
{token0Deposited?.toSignificant(6)}
</Text>
</RowFixed>
) : (
'-'
)}
</FixedHeightRow>
<FixedHeightRow>
<Text color="#888D9B" fontSize={16} fontWeight={500}>
{currency1.symbol}:
</Text>
{token1Deposited ? (
<RowFixed>
<Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}>
{token1Deposited?.toSignificant(6)}
</Text>
</RowFixed>
) : (
'-'
)}
</FixedHeightRow>
</AutoColumn>
</AutoColumn>
</GreyCard>
)}
</>
)
}
export default function FullPositionCard({ pair, border }: PositionCardProps) {
const { account } = useActiveWeb3React()
const currency0 = unwrappedToken(pair.token0)
const currency1 = unwrappedToken(pair.token1)
const [showMore, setShowMore] = useState(false)
const userPoolBalance = useTokenBalance(account, pair.liquidityToken)
const totalPoolTokens = useTotalSupply(pair.liquidityToken)
const poolTokenPercentage = const poolTokenPercentage =
!!userPoolBalance && !!totalPoolTokens && JSBI.greaterThanOrEqual(totalPoolTokens.raw, userPoolBalance.raw) !!userPoolBalance && !!totalPoolTokens && JSBI.greaterThanOrEqual(totalPoolTokens.raw, userPoolBalance.raw)
@@ -59,173 +146,99 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
// this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply // this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
JSBI.greaterThanOrEqual(totalPoolTokens.raw, userPoolBalance.raw) JSBI.greaterThanOrEqual(totalPoolTokens.raw, userPoolBalance.raw)
? [ ? [
pair.getLiquidityValue(token0, totalPoolTokens, userPoolBalance, false), pair.getLiquidityValue(pair.token0, totalPoolTokens, userPoolBalance, false),
pair.getLiquidityValue(token1, totalPoolTokens, userPoolBalance, false) pair.getLiquidityValue(pair.token1, totalPoolTokens, userPoolBalance, false)
] ]
: [undefined, undefined] : [undefined, undefined]
if (minimal) { return (
return ( <HoverCard border={border}>
<> <AutoColumn gap="12px">
{userPoolBalance && ( <FixedHeightRow onClick={() => setShowMore(!showMore)} style={{ cursor: 'pointer' }}>
<GreyCard border={border}> <RowFixed>
<AutoColumn gap="12px"> <DoubleCurrencyLogo currency0={currency0} currency1={currency1} margin={true} size={20} />
<FixedHeightRow> <Text fontWeight={500} fontSize={20}>
{!currency0 || !currency1 ? <Dots>Loading</Dots> : `${currency0.symbol}/${currency1.symbol}`}
</Text>
</RowFixed>
<RowFixed>
{showMore ? (
<ChevronUp size="20" style={{ marginLeft: '10px' }} />
) : (
<ChevronDown size="20" style={{ marginLeft: '10px' }} />
)}
</RowFixed>
</FixedHeightRow>
{showMore && (
<AutoColumn gap="8px">
<FixedHeightRow>
<RowFixed>
<Text fontSize={16} fontWeight={500}>
Pooled {currency0.symbol}:
</Text>
</RowFixed>
{token0Deposited ? (
<RowFixed> <RowFixed>
<Text fontWeight={500} fontSize={16}> <Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
Your position {token0Deposited?.toSignificant(6)}
</Text> </Text>
<CurrencyLogo size="20px" style={{ marginLeft: '8px' }} currency={currency0} />
</RowFixed> </RowFixed>
</FixedHeightRow>
<FixedHeightRow onClick={() => setShowMore(!showMore)}>
<RowFixed>
<DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={20} />
<Text fontWeight={500} fontSize={20}>
{token0?.symbol}/{token1?.symbol}
</Text>
</RowFixed>
<RowFixed>
<Text fontWeight={500} fontSize={20}>
{userPoolBalance ? userPoolBalance.toSignificant(4) : '-'}
</Text>
</RowFixed>
</FixedHeightRow>
<AutoColumn gap="4px">
<FixedHeightRow>
<Text color="#888D9B" fontSize={16} fontWeight={500}>
{token0?.symbol}:
</Text>
{token0Deposited ? (
<RowFixed>
<Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}>
{token0Deposited?.toSignificant(6)}
</Text>
</RowFixed>
) : (
'-'
)}
</FixedHeightRow>
<FixedHeightRow>
<Text color="#888D9B" fontSize={16} fontWeight={500}>
{token1?.symbol}:
</Text>
{token1Deposited ? (
<RowFixed>
<Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}>
{token1Deposited?.toSignificant(6)}
</Text>
</RowFixed>
) : (
'-'
)}
</FixedHeightRow>
</AutoColumn>
</AutoColumn>
</GreyCard>
)}
</>
)
} else
return (
<HoverCard border={border}>
<AutoColumn gap="12px">
<FixedHeightRow onClick={() => setShowMore(!showMore)} style={{ cursor: 'pointer' }}>
<RowFixed>
<DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={20} />
<Text fontWeight={500} fontSize={20}>
{!token0 || !token1 ? <Dots>Loading</Dots> : `${token0.symbol}/${token1.symbol}`}
</Text>
</RowFixed>
<RowFixed>
{showMore ? (
<ChevronUp size="20" style={{ marginLeft: '10px' }} />
) : ( ) : (
<ChevronDown size="20" style={{ marginLeft: '10px' }} /> '-'
)} )}
</RowFixed> </FixedHeightRow>
</FixedHeightRow>
{showMore && (
<AutoColumn gap="8px">
<FixedHeightRow>
<RowFixed>
<Text fontSize={16} fontWeight={500}>
Pooled {token0?.symbol}:
</Text>
</RowFixed>
{token0Deposited ? (
<RowFixed>
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
{token0Deposited?.toSignificant(6)}
</Text>
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token0?.address} />
</RowFixed>
) : (
'-'
)}
</FixedHeightRow>
<FixedHeightRow> <FixedHeightRow>
<RowFixed>
<Text fontSize={16} fontWeight={500}>
Pooled {currency1.symbol}:
</Text>
</RowFixed>
{token1Deposited ? (
<RowFixed> <RowFixed>
<Text fontSize={16} fontWeight={500}> <Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
Pooled {token1?.symbol}: {token1Deposited?.toSignificant(6)}
</Text> </Text>
<CurrencyLogo size="20px" style={{ marginLeft: '8px' }} currency={currency1} />
</RowFixed> </RowFixed>
{token1Deposited ? ( ) : (
<RowFixed> '-'
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}> )}
{token1Deposited?.toSignificant(6)} </FixedHeightRow>
</Text> <FixedHeightRow>
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token1?.address} /> <Text fontSize={16} fontWeight={500}>
</RowFixed> Your pool tokens:
) : ( </Text>
'-' <Text fontSize={16} fontWeight={500}>
)} {userPoolBalance ? userPoolBalance.toSignificant(4) : '-'}
</FixedHeightRow> </Text>
<FixedHeightRow> </FixedHeightRow>
<Text fontSize={16} fontWeight={500}> <FixedHeightRow>
Your pool tokens: <Text fontSize={16} fontWeight={500}>
</Text> Your pool share:
<Text fontSize={16} fontWeight={500}> </Text>
{userPoolBalance ? userPoolBalance.toSignificant(4) : '-'} <Text fontSize={16} fontWeight={500}>
</Text> {poolTokenPercentage ? poolTokenPercentage.toFixed(2) + '%' : '-'}
</FixedHeightRow> </Text>
<FixedHeightRow> </FixedHeightRow>
<Text fontSize={16} fontWeight={500}>
Your pool share:
</Text>
<Text fontSize={16} fontWeight={500}>
{poolTokenPercentage ? poolTokenPercentage.toFixed(2) + '%' : '-'}
</Text>
</FixedHeightRow>
<AutoRow justify="center" marginTop={'10px'}> <AutoRow justify="center" marginTop={'10px'}>
<ExternalLink href={`https://uniswap.info/pair/${pair?.liquidityToken.address}`}> <ExternalLink href={`https://uniswap.info/pair/${pair.liquidityToken.address}`}>
View pool information View pool information
</ExternalLink> </ExternalLink>
</AutoRow> </AutoRow>
<RowBetween marginTop="10px"> <RowBetween marginTop="10px">
<ButtonSecondary <ButtonSecondary as={Link} to={`/add/${currencyId(currency0)}/${currencyId(currency1)}`} width="48%">
width="48%" Add
onClick={() => { </ButtonSecondary>
history.push('/add/' + token0?.address + '-' + token1?.address) <ButtonSecondary as={Link} width="48%" to={`/remove/${currencyId(currency0)}/${currencyId(currency1)}`}>
}} Remove
> </ButtonSecondary>
Add </RowBetween>
</ButtonSecondary> </AutoColumn>
<ButtonSecondary )}
width="48%" </AutoColumn>
onClick={() => { </HoverCard>
history.push('/remove/' + token0?.address + '-' + token1?.address) )
}}
>
Remove
</ButtonSecondary>
</RowBetween>
</AutoColumn>
)}
</AutoColumn>
</HoverCard>
)
} }
export default withRouter(PositionCard)

View File

@@ -5,14 +5,13 @@ const Row = styled(Box)<{ align?: string; padding?: string; border?: string; bor
width: 100%; width: 100%;
display: flex; display: flex;
padding: 0; padding: 0;
align-items: center; align-items: ${({ align }) => (align ? align : 'center')};
align-items: ${({ align }) => align && align};
padding: ${({ padding }) => padding}; padding: ${({ padding }) => padding};
border: ${({ border }) => border}; border: ${({ border }) => border};
border-radius: ${({ borderRadius }) => borderRadius}; border-radius: ${({ borderRadius }) => borderRadius};
` `
export const RowBetween = styled(Row)<{ align?: string; padding?: string; border?: string; borderRadius?: string }>` export const RowBetween = styled(Row)`
justify-content: space-between; justify-content: space-between;
` `

View File

@@ -1,41 +1,62 @@
import React from 'react' import React from 'react'
import { Text } from 'rebass' import { Text } from 'rebass'
import { Token } from '@uniswap/sdk' import { ChainId, Currency, currencyEquals, ETHER, Token } from '@uniswap/sdk'
import styled from 'styled-components'
import { SUGGESTED_BASES } from '../../constants' import { SUGGESTED_BASES } from '../../constants'
import { AutoColumn } from '../Column' import { AutoColumn } from '../Column'
import QuestionHelper from '../QuestionHelper' import QuestionHelper from '../QuestionHelper'
import { AutoRow } from '../Row' import { AutoRow } from '../Row'
import TokenLogo from '../TokenLogo' import CurrencyLogo from '../CurrencyLogo'
import { BaseWrapper } from './styleds'
const BaseWrapper = styled.div<{ disable?: boolean }>`
border: 1px solid ${({ theme, disable }) => (disable ? 'transparent' : theme.bg3)};
border-radius: 10px;
display: flex;
padding: 6px;
align-items: center;
:hover {
cursor: ${({ disable }) => !disable && 'pointer'};
background-color: ${({ theme, disable }) => !disable && theme.bg2};
}
background-color: ${({ theme, disable }) => disable && theme.bg3};
opacity: ${({ disable }) => disable && '0.4'};
`
export default function CommonBases({ export default function CommonBases({
chainId, chainId,
onSelect, onSelect,
selectedTokenAddress selectedCurrency
}: { }: {
chainId: number chainId?: ChainId
selectedTokenAddress: string selectedCurrency?: Currency
onSelect: (tokenAddress: string) => void onSelect: (currency: Currency) => void
}) { }) {
return ( return (
<AutoColumn gap="md"> <AutoColumn gap="md">
<AutoRow> <AutoRow>
<Text fontWeight={500} fontSize={16}> <Text fontWeight={500} fontSize={14}>
Common Bases Common bases
</Text> </Text>
<QuestionHelper text="These tokens are commonly used in pairs." /> <QuestionHelper text="These tokens are commonly paired with other tokens." />
</AutoRow> </AutoRow>
<AutoRow gap="10px"> <AutoRow gap="4px">
{(SUGGESTED_BASES[chainId] ?? []).map((token: Token) => { <BaseWrapper
onClick={() => !currencyEquals(selectedCurrency, ETHER) && onSelect(ETHER)}
disable={selectedCurrency === ETHER}
>
<CurrencyLogo currency={ETHER} style={{ marginRight: 8 }} />
<Text fontWeight={500} fontSize={16}>
ETH
</Text>
</BaseWrapper>
{(chainId ? SUGGESTED_BASES[chainId] : []).map((token: Token) => {
const selected = selectedCurrency instanceof Token && selectedCurrency.address === token.address
return ( return (
<BaseWrapper <BaseWrapper onClick={() => !selected && onSelect(token)} disable={selected} key={token.address}>
gap="6px" <CurrencyLogo currency={token} style={{ marginRight: 8 }} />
onClick={() => selectedTokenAddress !== token.address && onSelect(token.address)}
disable={selectedTokenAddress === token.address}
key={token.address}
>
<TokenLogo address={token.address} />
<Text fontWeight={500} fontSize={16}> <Text fontWeight={500} fontSize={16}>
{token.symbol} {token.symbol}
</Text> </Text>

View File

@@ -0,0 +1,155 @@
import { Currency, CurrencyAmount, currencyEquals, ETHER, JSBI, Token } from '@uniswap/sdk'
import React, { CSSProperties, memo, useContext, useMemo } from 'react'
import { FixedSizeList } from 'react-window'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import { useActiveWeb3React } from '../../hooks'
import { useAllTokens } from '../../hooks/Tokens'
import { useDefaultTokenList } from '../../state/lists/hooks'
import { useAddUserToken, useRemoveUserAddedToken } from '../../state/user/hooks'
import { useETHBalances } from '../../state/wallet/hooks'
import { LinkStyledButton, TYPE } from '../../theme'
import { ButtonSecondary } from '../Button'
import Column, { AutoColumn } from '../Column'
import { RowFixed } from '../Row'
import CurrencyLogo from '../CurrencyLogo'
import { FadedSpan, MenuItem } from './styleds'
import Loader from '../Loader'
import { isDefaultToken } from '../../utils'
function currencyKey(currency: Currency): string {
return currency instanceof Token ? currency.address : currency === ETHER ? 'ETHER' : ''
}
export default function CurrencyList({
currencies,
allBalances,
selectedCurrency,
onCurrencySelect,
otherCurrency,
showSendWithSwap
}: {
currencies: Currency[]
selectedCurrency: Currency
allBalances: { [tokenAddress: string]: CurrencyAmount }
onCurrencySelect: (currency: Currency) => void
otherCurrency: Currency
showSendWithSwap?: boolean
}) {
const { account, chainId } = useActiveWeb3React()
const theme = useContext(ThemeContext)
const allTokens = useAllTokens()
const defaultTokens = useDefaultTokenList()
const addToken = useAddUserToken()
const removeToken = useRemoveUserAddedToken()
const ETHBalance = useETHBalances([account])[account]
const CurrencyRow = useMemo(() => {
return memo(function CurrencyRow({ index, style }: { index: number; style: CSSProperties }) {
const currency = index === 0 ? Currency.ETHER : currencies[index - 1]
const key = currencyKey(currency)
const isDefault = isDefaultToken(defaultTokens, currency)
const customAdded = Boolean(!isDefault && currency instanceof Token && allTokens[currency.address])
const balance = currency === ETHER ? ETHBalance : allBalances[key]
const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw)
const isSelected = Boolean(selectedCurrency && currencyEquals(currency, selectedCurrency))
const otherSelected = Boolean(otherCurrency && currencyEquals(otherCurrency, currency))
return (
<MenuItem
style={style}
className={`token-item-${key}`}
onClick={() => (isSelected ? null : onCurrencySelect(currency))}
disabled={isSelected}
selected={otherSelected}
>
<RowFixed>
<CurrencyLogo currency={currency} size={'24px'} style={{ marginRight: '14px' }} />
<Column>
<Text fontWeight={500}>{currency.symbol}</Text>
<FadedSpan>
{customAdded ? (
<TYPE.main fontWeight={500}>
Added by user
<LinkStyledButton
onClick={event => {
event.stopPropagation()
if (currency instanceof Token) removeToken(chainId, currency.address)
}}
>
(Remove)
</LinkStyledButton>
</TYPE.main>
) : null}
{!isDefault && !customAdded ? (
<TYPE.main fontWeight={500}>
Found by address
<LinkStyledButton
onClick={event => {
event.stopPropagation()
if (currency instanceof Token) addToken(currency)
}}
>
(Add)
</LinkStyledButton>
</TYPE.main>
) : null}
</FadedSpan>
</Column>
</RowFixed>
<AutoColumn>
{balance ? (
<Text>
{zeroBalance && showSendWithSwap ? (
<ButtonSecondary padding={'4px 8px'}>
<Text textAlign="center" fontWeight={500} fontSize={14} color={theme.primary1}>
Send With Swap
</Text>
</ButtonSecondary>
) : balance ? (
balance.toSignificant(6)
) : (
'-'
)}
</Text>
) : account ? (
<Loader />
) : (
'-'
)}
</AutoColumn>
</MenuItem>
)
})
}, [
ETHBalance,
account,
addToken,
allBalances,
allTokens,
chainId,
currencies,
defaultTokens,
onCurrencySelect,
otherCurrency,
removeToken,
selectedCurrency,
showSendWithSwap,
theme.primary1
])
return (
<FixedSizeList
width="100%"
height={500}
itemCount={currencies.length + 1}
itemSize={56}
style={{ flex: '1' }}
itemKey={index => currencyKey(currencies[index])}
>
{CurrencyRow}
</FixedSizeList>
)
}

View File

@@ -1,5 +1,5 @@
import { Token } from '@uniswap/sdk' import { Currency, Token } from '@uniswap/sdk'
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' import React, { KeyboardEvent, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import { isMobile } from 'react-device-detect' import { isMobile } from 'react-device-detect'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Text } from 'rebass' import { Text } from 'rebass'
@@ -7,8 +7,9 @@ import { ThemeContext } from 'styled-components'
import Card from '../../components/Card' import Card from '../../components/Card'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { useAllTokens, useToken } from '../../hooks/Tokens' import { useAllTokens, useToken } from '../../hooks/Tokens'
import { useAllTokenBalancesTreatingWETHasETH, useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks' import useInterval from '../../hooks/useInterval'
import { CloseIcon, LinkStyledButton } from '../../theme/components' import { useAllTokenBalances, useTokenBalance } from '../../state/wallet/hooks'
import { CloseIcon, LinkStyledButton } from '../../theme'
import { isAddress } from '../../utils' import { isAddress } from '../../utils'
import Column from '../Column' import Column from '../Column'
import Modal from '../Modal' import Modal from '../Modal'
@@ -19,30 +20,28 @@ import CommonBases from './CommonBases'
import { filterTokens } from './filtering' import { filterTokens } from './filtering'
import { useTokenComparator } from './sorting' import { useTokenComparator } from './sorting'
import { PaddedColumn, SearchInput } from './styleds' import { PaddedColumn, SearchInput } from './styleds'
import TokenList from './TokenList' import CurrencyList from './CurrencyList'
import SortButton from './SortButton' import SortButton from './SortButton'
interface TokenSearchModalProps { interface CurrencySearchModalProps {
isOpen?: boolean isOpen?: boolean
onDismiss?: () => void onDismiss?: () => void
hiddenToken?: string hiddenCurrency?: Currency
showSendWithSwap?: boolean showSendWithSwap?: boolean
onTokenSelect?: (address: string) => void onCurrencySelect?: (currency: Currency) => void
otherSelectedTokenAddress?: string otherSelectedCurrency?: Currency
otherSelectedText?: string
showCommonBases?: boolean showCommonBases?: boolean
} }
export default function TokenSearchModal({ export default function CurrencySearchModal({
isOpen, isOpen,
onDismiss, onDismiss,
onTokenSelect, onCurrencySelect,
hiddenToken, hiddenCurrency,
showSendWithSwap, showSendWithSwap,
otherSelectedTokenAddress, otherSelectedCurrency,
otherSelectedText,
showCommonBases = false showCommonBases = false
}: TokenSearchModalProps) { }: CurrencySearchModalProps) {
const { t } = useTranslation() const { t } = useTranslation()
const { account, chainId } = useActiveWeb3React() const { account, chainId } = useActiveWeb3React()
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
@@ -54,8 +53,8 @@ export default function TokenSearchModal({
// if the current input is an address, and we don't have the token in context, try to fetch it and import // if the current input is an address, and we don't have the token in context, try to fetch it and import
const searchToken = useToken(searchQuery) const searchToken = useToken(searchQuery)
const searchTokenBalance = useTokenBalanceTreatingWETHasETH(account, searchToken) const searchTokenBalance = useTokenBalance(account, searchToken)
const allTokenBalances_ = useAllTokenBalancesTreatingWETHasETH() const allTokenBalances_ = useAllTokenBalances()
const allTokenBalances = searchToken const allTokenBalances = searchToken
? { ? {
[searchToken.address]: searchTokenBalance [searchToken.address]: searchTokenBalance
@@ -86,12 +85,12 @@ export default function TokenSearchModal({
] ]
}, [filteredTokens, searchQuery, searchToken, tokenComparator]) }, [filteredTokens, searchQuery, searchToken, tokenComparator])
const handleTokenSelect = useCallback( const handleCurrencySelect = useCallback(
(address: string) => { (currency: Currency) => {
onTokenSelect(address) onCurrencySelect(currency)
onDismiss() onDismiss()
}, },
[onDismiss, onTokenSelect] [onDismiss, onCurrencySelect]
) )
// clear the input on open // clear the input on open
@@ -110,10 +109,31 @@ export default function TokenSearchModal({
const openTooltip = useCallback(() => { const openTooltip = useCallback(() => {
setTooltipOpen(true) setTooltipOpen(true)
inputRef.current?.focus()
}, [setTooltipOpen]) }, [setTooltipOpen])
const closeTooltip = useCallback(() => setTooltipOpen(false), [setTooltipOpen]) const closeTooltip = useCallback(() => setTooltipOpen(false), [setTooltipOpen])
useInterval(
() => {
setTooltipOpen(false)
},
tooltipOpen ? 4000 : null,
false
)
const handleEnter = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && filteredSortedTokens.length > 0) {
if (
filteredSortedTokens[0].symbol.toLowerCase() === searchQuery.trim().toLowerCase() ||
filteredSortedTokens.length === 1
) {
handleCurrencySelect(filteredSortedTokens[0])
}
}
},
[filteredSortedTokens, handleCurrencySelect, searchQuery]
)
return ( return (
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
@@ -123,7 +143,7 @@ export default function TokenSearchModal({
minHeight={70} minHeight={70}
> >
<Column style={{ width: '100%' }}> <Column style={{ width: '100%' }}>
<PaddedColumn gap="20px"> <PaddedColumn gap="14px">
<RowBetween> <RowBetween>
<Text fontWeight={500} fontSize={16}> <Text fontWeight={500} fontSize={16}>
Select a token Select a token
@@ -146,11 +166,13 @@ export default function TokenSearchModal({
value={searchQuery} value={searchQuery}
ref={inputRef} ref={inputRef}
onChange={handleInput} onChange={handleInput}
onFocus={closeTooltip}
onBlur={closeTooltip} onBlur={closeTooltip}
onKeyDown={handleEnter}
/> />
</Tooltip> </Tooltip>
{showCommonBases && ( {showCommonBases && (
<CommonBases chainId={chainId} onSelect={handleTokenSelect} selectedTokenAddress={hiddenToken} /> <CommonBases chainId={chainId} onSelect={handleCurrencySelect} selectedCurrency={hiddenCurrency} />
)} )}
<RowBetween> <RowBetween>
<Text fontSize={14} fontWeight={500}> <Text fontSize={14} fontWeight={500}>
@@ -160,13 +182,12 @@ export default function TokenSearchModal({
</RowBetween> </RowBetween>
</PaddedColumn> </PaddedColumn>
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} /> <div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
<TokenList <CurrencyList
tokens={filteredSortedTokens} currencies={filteredSortedTokens}
allTokenBalances={allTokenBalances} allBalances={allTokenBalances}
onTokenSelect={handleTokenSelect} onCurrencySelect={handleCurrencySelect}
otherSelectedText={otherSelectedText} otherCurrency={otherSelectedCurrency}
otherToken={otherSelectedTokenAddress} selectedCurrency={hiddenCurrency}
selectedToken={hiddenToken}
showSendWithSwap={showSendWithSwap} showSendWithSwap={showSendWithSwap}
/> />
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} /> <div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />

View File

@@ -1,58 +0,0 @@
import { JSBI, Pair, TokenAmount } from '@uniswap/sdk'
import React from 'react'
import { FixedSizeList } from 'react-window'
import { Text } from 'rebass'
import { ButtonPrimary } from '../Button'
import DoubleTokenLogo from '../DoubleLogo'
import { RowFixed } from '../Row'
import { MenuItem, ModalInfo } from './styleds'
export default function PairList({
pairs,
focusTokenAddress,
pairBalances,
onSelectPair,
onAddLiquidity = onSelectPair
}: {
pairs: Pair[]
focusTokenAddress?: string
pairBalances: { [pairAddress: string]: TokenAmount }
onSelectPair: (pair: Pair) => void
onAddLiquidity: (pair: Pair) => void
}) {
if (pairs.length === 0) {
return <ModalInfo>No Pools Found</ModalInfo>
}
return (
<FixedSizeList itemSize={56} height={500} itemCount={pairs.length} width="100%" style={{ flex: '1' }}>
{({ index, style }) => {
const pair = pairs[index]
// the focused token is shown first
const tokenA = focusTokenAddress === pair.token1.address ? pair.token1 : pair.token0
const tokenB = tokenA === pair.token0 ? pair.token1 : pair.token0
const pairAddress = pair.liquidityToken.address
const balance = pairBalances[pairAddress]?.toSignificant(6)
const zeroBalance = pairBalances[pairAddress]?.raw && JSBI.equal(pairBalances[pairAddress].raw, JSBI.BigInt(0))
const selectPair = () => onSelectPair(pair)
const addLiquidity = () => onAddLiquidity(pair)
return (
<MenuItem style={style} onClick={selectPair}>
<RowFixed>
<DoubleTokenLogo a0={tokenA.address} a1={tokenB.address} size={24} margin={true} />
<Text fontWeight={500} fontSize={16}>{`${tokenA.symbol}/${tokenB.symbol}`}</Text>
</RowFixed>
<ButtonPrimary padding={'6px 8px'} width={'fit-content'} borderRadius={'12px'} onClick={addLiquidity}>
{balance ? (zeroBalance ? 'Join' : 'Add Liquidity') : 'Join'}
</ButtonPrimary>
</MenuItem>
)
}}
</FixedSizeList>
)
}

View File

@@ -1,135 +0,0 @@
import { Pair } from '@uniswap/sdk'
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import { isMobile } from 'react-device-detect'
import { useTranslation } from 'react-i18next'
import { RouteComponentProps, withRouter } from 'react-router-dom'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import Card from '../../components/Card'
import { useActiveWeb3React } from '../../hooks'
import { useAllTokens } from '../../hooks/Tokens'
import { useAllDummyPairs } from '../../state/user/hooks'
import { useTokenBalances } from '../../state/wallet/hooks'
import { CloseIcon, StyledInternalLink } from '../../theme/components'
import { isAddress } from '../../utils'
import Column from '../Column'
import Modal from '../Modal'
import QuestionHelper from '../QuestionHelper'
import { AutoRow, RowBetween } from '../Row'
import { filterPairs } from './filtering'
import PairList from './PairList'
import { pairComparator } from './sorting'
import { PaddedColumn, SearchInput } from './styleds'
interface PairSearchModalProps extends RouteComponentProps {
isOpen?: boolean
onDismiss?: () => void
}
function PairSearchModal({ history, isOpen, onDismiss }: PairSearchModalProps) {
const { t } = useTranslation()
const { account } = useActiveWeb3React()
const theme = useContext(ThemeContext)
const [searchQuery, setSearchQuery] = useState<string>('')
const allTokens = useAllTokens()
const allPairs = useAllDummyPairs()
const allPairBalances = useTokenBalances(
account,
allPairs.map(p => p.liquidityToken)
)
// clear the input on open
useEffect(() => {
if (isOpen) setSearchQuery('')
}, [isOpen, setSearchQuery])
// manage focus on modal show
const inputRef = useRef<HTMLInputElement>()
function onInput(event) {
const input = event.target.value
const checksummedInput = isAddress(input)
setSearchQuery(checksummedInput || input)
}
const sortedPairList = useMemo(() => {
return allPairs.sort((a, b): number => {
const balanceA = allPairBalances[a.liquidityToken.address]
const balanceB = allPairBalances[b.liquidityToken.address]
return pairComparator(a, b, balanceA, balanceB)
})
}, [allPairs, allPairBalances])
const filteredPairs = useMemo(() => {
return filterPairs(sortedPairList, searchQuery)
}, [searchQuery, sortedPairList])
const selectPair = useCallback(
(pair: Pair) => {
history.push(`/add/${pair.token0.address}-${pair.token1.address}`)
},
[history]
)
const focusedToken = Object.values(allTokens ?? {}).filter(token => {
return token.symbol.toLowerCase() === searchQuery || searchQuery === token.address
})[0]
return (
<Modal
isOpen={isOpen}
onDismiss={onDismiss}
maxHeight={70}
initialFocusRef={isMobile ? undefined : inputRef}
minHeight={70}
>
<Column style={{ width: '100%' }}>
<PaddedColumn gap="20px">
<RowBetween>
<Text fontWeight={500} fontSize={16}>
Select a pool
<QuestionHelper text="Find a pair by searching for its name below." />
</Text>
<CloseIcon onClick={onDismiss} />
</RowBetween>
<SearchInput
type="text"
id="token-search-input"
placeholder={t('tokenSearchPlaceholder')}
value={searchQuery}
ref={inputRef}
onChange={onInput}
/>
<RowBetween>
<Text fontSize={14} fontWeight={500}>
Pool Name
</Text>
</RowBetween>
</PaddedColumn>
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
<PairList
pairs={filteredPairs}
focusTokenAddress={focusedToken?.address}
onAddLiquidity={selectPair}
onSelectPair={selectPair}
pairBalances={allPairBalances}
/>
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
<Card>
<AutoRow justify={'center'}>
<div>
<Text fontWeight={500}>
{!isMobile && "Don't see a pool? "}
<StyledInternalLink to="/find">{!isMobile ? 'Import it.' : 'Import pool.'}</StyledInternalLink>
</Text>
</div>
</AutoRow>
</Card>
</Column>
</Modal>
)
}
export default withRouter(PairSearchModal)

View File

@@ -1,138 +0,0 @@
import { JSBI, Token, TokenAmount } from '@uniswap/sdk'
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { FixedSizeList } from 'react-window'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import { useActiveWeb3React } from '../../hooks'
import { useAllTokens } from '../../hooks/Tokens'
import { useAddUserToken, useRemoveUserAddedToken } from '../../state/user/hooks'
import { LinkStyledButton, TYPE } from '../../theme'
import { ButtonSecondary } from '../Button'
import Column, { AutoColumn } from '../Column'
import { RowFixed } from '../Row'
import TokenLogo from '../TokenLogo'
import { FadedSpan, GreySpan, MenuItem, ModalInfo } from './styleds'
import Loader from '../Loader'
import { isDefaultToken, isCustomAddedToken } from '../../utils'
export default function TokenList({
tokens,
allTokenBalances,
selectedToken,
onTokenSelect,
otherToken,
showSendWithSwap,
otherSelectedText
}: {
tokens: Token[]
selectedToken: string
allTokenBalances: { [tokenAddress: string]: TokenAmount }
onTokenSelect: (tokenAddress: string) => void
otherToken: string
showSendWithSwap?: boolean
otherSelectedText: string
}) {
const { t } = useTranslation()
const { account, chainId } = useActiveWeb3React()
const theme = useContext(ThemeContext)
const allTokens = useAllTokens()
const addToken = useAddUserToken()
const removeToken = useRemoveUserAddedToken()
if (tokens.length === 0) {
return <ModalInfo>{t('noToken')}</ModalInfo>
}
return (
<FixedSizeList
width="100%"
height={500}
itemCount={tokens.length}
itemSize={56}
style={{ flex: '1' }}
itemKey={index => tokens[index].address}
>
{({ index, style }) => {
const token = tokens[index]
const { address, symbol } = token
const isDefault = isDefaultToken(token)
const customAdded = isCustomAddedToken(allTokens, token)
const balance = allTokenBalances[address]
const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw)
return (
<MenuItem
style={style}
key={address}
className={`token-item-${address}`}
onClick={() => (selectedToken && selectedToken === address ? null : onTokenSelect(address))}
disabled={selectedToken && selectedToken === address}
selected={otherToken === address}
>
<RowFixed>
<TokenLogo address={address} size={'24px'} style={{ marginRight: '14px' }} />
<Column>
<Text fontWeight={500}>
{symbol}
{otherToken === address && <GreySpan> ({otherSelectedText})</GreySpan>}
</Text>
<FadedSpan>
{customAdded ? (
<TYPE.main fontWeight={500}>
Added by user
<LinkStyledButton
onClick={event => {
event.stopPropagation()
removeToken(chainId, address)
}}
>
(Remove)
</LinkStyledButton>
</TYPE.main>
) : null}
{!isDefault && !customAdded ? (
<TYPE.main fontWeight={500}>
Found by address
<LinkStyledButton
onClick={event => {
event.stopPropagation()
addToken(token)
}}
>
(Add)
</LinkStyledButton>
</TYPE.main>
) : null}
</FadedSpan>
</Column>
</RowFixed>
<AutoColumn>
{balance ? (
<Text>
{zeroBalance && showSendWithSwap ? (
<ButtonSecondary padding={'4px 8px'}>
<Text textAlign="center" fontWeight={500} fontSize={14} color={theme.primary1}>
Send With Swap
</Text>
</ButtonSecondary>
) : balance ? (
balance.toSignificant(6)
) : (
'-'
)}
</Text>
) : account ? (
<Loader />
) : (
'-'
)}
</AutoColumn>
</MenuItem>
)
}}
</FixedSizeList>
)
}

View File

@@ -1,5 +1,5 @@
import { isAddress } from '../../utils' import { isAddress } from '../../utils'
import { Pair, Token } from '@uniswap/sdk' import { Token } from '@uniswap/sdk'
export function filterTokens(tokens: Token[], search: string): Token[] { export function filterTokens(tokens: Token[], search: string): Token[] {
if (search.length === 0) return tokens if (search.length === 0) return tokens
@@ -34,27 +34,3 @@ export function filterTokens(tokens: Token[], search: string): Token[] {
return matchesSearch(symbol) || matchesSearch(name) return matchesSearch(symbol) || matchesSearch(name)
}) })
} }
export function filterPairs(pairs: Pair[], search: string): Pair[] {
if (search.trim().length === 0) return pairs
const addressSearch = isAddress(search)
if (addressSearch) {
return pairs.filter(p => {
return (
p.token0.address === addressSearch ||
p.token1.address === addressSearch ||
p.liquidityToken.address === addressSearch
)
})
}
const lowerSearch = search.toLowerCase()
return pairs.filter(pair => {
const pairExpressionA = `${pair.token0.symbol}/${pair.token1.symbol}`.toLowerCase()
if (pairExpressionA.startsWith(lowerSearch)) return true
const pairExpressionB = `${pair.token1.symbol}/${pair.token0.symbol}`.toLowerCase()
if (pairExpressionB.startsWith(lowerSearch)) return true
return filterTokens([pair.token0, pair.token1], search).length > 0
})
}

View File

@@ -1,8 +1,7 @@
import { Token, TokenAmount, WETH, Pair } from '@uniswap/sdk' import { Token, TokenAmount, WETH } from '@uniswap/sdk'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks' import { useAllTokenBalances } from '../../state/wallet/hooks'
import { DUMMY_PAIRS_TO_PIN } from '../../constants'
// compare two token amounts with highest one coming first // compare two token amounts with highest one coming first
function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount) { function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount) {
@@ -16,26 +15,6 @@ function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount) {
return 0 return 0
} }
// compare two pairs, favoring "pinned" pairs, and falling back to balances
export function pairComparator(pairA: Pair, pairB: Pair, balanceA?: TokenAmount, balanceB?: TokenAmount) {
const aShouldBePinned =
DUMMY_PAIRS_TO_PIN[pairA?.token0?.chainId]?.some(
dummyPairToPin => dummyPairToPin.liquidityToken.address === pairA?.liquidityToken?.address
) ?? false
const bShouldBePinned =
DUMMY_PAIRS_TO_PIN[pairB?.token0?.chainId]?.some(
dummyPairToPin => dummyPairToPin.liquidityToken.address === pairB?.liquidityToken?.address
) ?? false
if (aShouldBePinned && !bShouldBePinned) {
return -1
} else if (!aShouldBePinned && bShouldBePinned) {
return 1
} else {
return balanceComparator(balanceA, balanceB)
}
}
function getTokenComparator( function getTokenComparator(
weth: Token | undefined, weth: Token | undefined,
balances: { [tokenAddress: string]: TokenAmount } balances: { [tokenAddress: string]: TokenAmount }
@@ -65,7 +44,7 @@ function getTokenComparator(
export function useTokenComparator(inverted: boolean): (tokenA: Token, tokenB: Token) => number { export function useTokenComparator(inverted: boolean): (tokenA: Token, tokenB: Token) => number {
const { chainId } = useActiveWeb3React() const { chainId } = useActiveWeb3React()
const weth = WETH[chainId] const weth = WETH[chainId]
const balances = useAllTokenBalancesTreatingWETHasETH() const balances = useAllTokenBalances()
const comparator = useMemo(() => getTokenComparator(weth, balances ?? {}), [balances, weth]) const comparator = useMemo(() => getTokenComparator(weth, balances ?? {}), [balances, weth])
return useMemo(() => { return useMemo(() => {
if (inverted) { if (inverted) {

View File

@@ -1,6 +1,6 @@
import styled from 'styled-components' import styled from 'styled-components'
import { AutoColumn } from '../Column' import { AutoColumn } from '../Column'
import { AutoRow, RowBetween, RowFixed } from '../Row' import { RowBetween, RowFixed } from '../Row'
export const ModalInfo = styled.div` export const ModalInfo = styled.div`
${({ theme }) => theme.flexRowNoWrap} ${({ theme }) => theme.flexRowNoWrap}
@@ -61,21 +61,6 @@ export const MenuItem = styled(RowBetween)`
opacity: ${({ disabled, selected }) => (disabled || selected ? 0.5 : 1)}; opacity: ${({ disabled, selected }) => (disabled || selected ? 0.5 : 1)};
` `
export const BaseWrapper = styled(AutoRow)<{ disable?: boolean }>`
border: 1px solid ${({ theme, disable }) => (disable ? 'transparent' : theme.bg3)};
padding: 0 6px;
border-radius: 10px;
width: 120px;
:hover {
cursor: ${({ disable }) => !disable && 'pointer'};
background-color: ${({ theme, disable }) => !disable && theme.bg2};
}
background-color: ${({ theme, disable }) => disable && theme.bg3};
opacity: ${({ disable }) => disable && '0.4'};
`
export const SearchInput = styled(Input)` export const SearchInput = styled(Input)`
transition: border 100ms; transition: border 100ms;
:focus { :focus {

View File

@@ -88,6 +88,9 @@ const MenuFlyout = styled.span`
background-color: ${({ theme }) => theme.bg1}; background-color: ${({ theme }) => theme.bg1};
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04), box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
0px 24px 32px rgba(0, 0, 0, 0.01); 0px 24px 32px rgba(0, 0, 0, 0.01);
border: 1px solid ${({ theme }) => theme.bg3};
border-radius: 0.5rem; border-radius: 0.5rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -156,7 +159,7 @@ export default function SettingsTab() {
return ( return (
<StyledMenu ref={node}> <StyledMenu ref={node}>
<Modal isOpen={showConfirmation} onDismiss={() => setShowConfirmation(false)}> <Modal isOpen={showConfirmation} onDismiss={() => setShowConfirmation(false)} maxHeight={100}>
<ModalContentWrapper> <ModalContentWrapper>
<AutoColumn gap="lg"> <AutoColumn gap="lg">
<RowBetween style={{ padding: '0 2rem' }}> <RowBetween style={{ padding: '0 2rem' }}>
@@ -233,7 +236,10 @@ export default function SettingsTab() {
toggleExpertMode() toggleExpertMode()
setShowConfirmation(false) setShowConfirmation(false)
} }
: () => setShowConfirmation(true) : () => {
toggle()
setShowConfirmation(true)
}
} }
/> />
</RowBetween> </RowBetween>

View File

@@ -21,15 +21,15 @@ enum DeadlineError {
const FancyButton = styled.button` const FancyButton = styled.button`
color: ${({ theme }) => theme.text1}; color: ${({ theme }) => theme.text1};
align-items: center; align-items: center;
min-width: 55px;
height: 2rem; height: 2rem;
border-radius: 36px; border-radius: 36px;
font-size: 12px; font-size: 12px;
width: auto;
min-width: 3rem;
border: 1px solid ${({ theme }) => theme.bg3}; border: 1px solid ${({ theme }) => theme.bg3};
outline: none; outline: none;
background: ${({ theme }) => theme.bg1}; background: ${({ theme }) => theme.bg1};
:hover { :hover {
cursor: inherit;
border: 1px solid ${({ theme }) => theme.bg4}; border: 1px solid ${({ theme }) => theme.bg4};
} }
:focus { :focus {
@@ -48,9 +48,8 @@ const Option = styled(FancyButton)<{ active: boolean }>`
const Input = styled.input` const Input = styled.input`
background: ${({ theme }) => theme.bg1}; background: ${({ theme }) => theme.bg1};
flex-grow: 1; font-size: 16px;
font-size: 12px; width: auto;
min-width: 20px;
outline: none; outline: none;
&::-webkit-outer-spin-button, &::-webkit-outer-spin-button,
&::-webkit-inner-spin-button { &::-webkit-inner-spin-button {
@@ -64,6 +63,7 @@ const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean }
height: 2rem; height: 2rem;
position: relative; position: relative;
padding: 0 0.75rem; padding: 0 0.75rem;
flex: 1;
border: ${({ theme, active, warning }) => active && `1px solid ${warning ? theme.red1 : theme.primary1}`}; border: ${({ theme, active, warning }) => active && `1px solid ${warning ? theme.red1 : theme.primary1}`};
:hover { :hover {
border: ${({ theme, active, warning }) => border: ${({ theme, active, warning }) =>
@@ -78,6 +78,13 @@ const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean }
} }
` `
const SlippageEmojiContainer = styled.span`
color: #f3841e;
${({ theme }) => theme.mediaWidth.upToSmall`
display: none;
`}
`
export interface SlippageTabsProps { export interface SlippageTabsProps {
rawSlippage: number rawSlippage: number
setRawSlippage: (rawSlippage: number) => void setRawSlippage: (rawSlippage: number) => void
@@ -182,9 +189,11 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
<RowBetween> <RowBetween>
{!!slippageInput && {!!slippageInput &&
(slippageError === SlippageError.RiskyLow || slippageError === SlippageError.RiskyHigh) ? ( (slippageError === SlippageError.RiskyLow || slippageError === SlippageError.RiskyHigh) ? (
<span role="img" aria-label="warning" style={{ color: '#F3841E' }}> <SlippageEmojiContainer>
<span role="img" aria-label="warning">
</span>
</span>
</SlippageEmojiContainer>
) : null} ) : null}
<Input <Input
ref={inputRef} ref={inputRef}

View File

@@ -1,83 +0,0 @@
import React, { useState } from 'react'
import styled from 'styled-components'
import { isAddress } from '../../utils'
import { useActiveWeb3React } from '../../hooks'
import { WETH } from '@uniswap/sdk'
import EthereumLogo from '../../assets/images/ethereum-logo.png'
const TOKEN_ICON_API = address =>
`https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${address}/logo.png`
const BAD_IMAGES = {}
const Image = styled.img<{ size: string }>`
width: ${({ size }) => size};
height: ${({ size }) => size};
background-color: white;
border-radius: 1rem;
box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075);
`
const Emoji = styled.span<{ size?: string }>`
display: flex;
align-items: center;
justify-content: center;
font-size: ${({ size }) => size};
width: ${({ size }) => size};
height: ${({ size }) => size};
margin-bottom: -4px;
`
const StyledEthereumLogo = styled.img<{ size: string }>`
width: ${({ size }) => size};
height: ${({ size }) => size};
box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075);
border-radius: 24px;
`
export default function TokenLogo({
address,
size = '24px',
...rest
}: {
address?: string
size?: string
style?: React.CSSProperties
}) {
const [error, setError] = useState(false)
const { chainId } = useActiveWeb3React()
// mock rinkeby DAI
if (chainId === 4 && address === '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735') {
address = '0x6B175474E89094C44Da98b954EedeAC495271d0F'
}
let path = ''
// hard code to show ETH instead of WETH in UI
if (address === WETH[chainId].address) {
return <StyledEthereumLogo src={EthereumLogo} size={size} {...rest} />
} else if (!error && !BAD_IMAGES[address] && isAddress(address)) {
path = TOKEN_ICON_API(address)
} else {
return (
<Emoji {...rest} size={size}>
<span role="img" aria-label="Thinking">
🤔
</span>
</Emoji>
)
}
return (
<Image
{...rest}
// alt={address}
src={path}
size={size}
onError={() => {
BAD_IMAGES[address] = true
setError(true)
}}
/>
)
}

View File

@@ -1,17 +1,18 @@
import { Token } from '@uniswap/sdk' import { Currency, Token } from '@uniswap/sdk'
import { transparentize } from 'polished' import { transparentize } from 'polished'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { ReactComponent as Close } from '../../assets/images/x.svg' import { ReactComponent as Close } from '../../assets/images/x.svg'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { useAllTokens } from '../../hooks/Tokens' import { useAllTokens } from '../../hooks/Tokens'
import { useDefaultTokenList } from '../../state/lists/hooks'
import { Field } from '../../state/swap/actions' import { Field } from '../../state/swap/actions'
import { useTokenWarningDismissal } from '../../state/user/hooks' import { useTokenWarningDismissal } from '../../state/user/hooks'
import { ExternalLink, TYPE } from '../../theme' import { ExternalLink, TYPE } from '../../theme'
import { getEtherscanLink, isDefaultToken } from '../../utils' import { getEtherscanLink, isDefaultToken } from '../../utils'
import PropsOfExcluding from '../../utils/props-of-excluding' import PropsOfExcluding from '../../utils/props-of-excluding'
import QuestionHelper from '../QuestionHelper' import QuestionHelper from '../QuestionHelper'
import TokenLogo from '../TokenLogo' import CurrencyLogo from '../CurrencyLogo'
const Wrapper = styled.div<{ error: boolean }>` const Wrapper = styled.div<{ error: boolean }>`
background: ${({ theme, error }) => transparentize(0.9, error ? theme.red1 : theme.yellow1)}; background: ${({ theme, error }) => transparentize(0.9, error ? theme.red1 : theme.yellow1)};
@@ -67,8 +68,8 @@ interface TokenWarningCardProps extends PropsOfExcluding<typeof Wrapper, 'error'
export default function TokenWarningCard({ token, ...rest }: TokenWarningCardProps) { export default function TokenWarningCard({ token, ...rest }: TokenWarningCardProps) {
const { chainId } = useActiveWeb3React() const { chainId } = useActiveWeb3React()
const defaultTokens = useDefaultTokenList()
const isDefault = isDefaultToken(token) const isDefault = isDefaultToken(defaultTokens, token)
const tokenSymbol = token?.symbol?.toLowerCase() ?? '' const tokenSymbol = token?.symbol?.toLowerCase() ?? ''
const tokenName = token?.name?.toLowerCase() ?? '' const tokenName = token?.name?.toLowerCase() ?? ''
@@ -103,13 +104,13 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro
<QuestionHelper text={duplicateNameOrSymbol ? DUPLICATE_NAME_HELP_TEXT : HELP_TEXT} /> <QuestionHelper text={duplicateNameOrSymbol ? DUPLICATE_NAME_HELP_TEXT : HELP_TEXT} />
</Row> </Row>
<Row> <Row>
<TokenLogo address={token.address} /> <CurrencyLogo currency={token} />
<div style={{ fontWeight: 500 }}> <div style={{ fontWeight: 500 }}>
{token && token.name && token.symbol && token.name !== token.symbol {token && token.name && token.symbol && token.name !== token.symbol
? `${token.name} (${token.symbol})` ? `${token.name} (${token.symbol})`
: token.name || token.symbol} : token.name || token.symbol}
</div> </div>
<ExternalLink style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'address')}> <ExternalLink style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'token')}>
(View on Etherscan) (View on Etherscan)
</ExternalLink> </ExternalLink>
</Row> </Row>
@@ -127,11 +128,13 @@ const WarningContainer = styled.div`
padding-right: 1rem; padding-right: 1rem;
` `
export function TokenWarningCards({ tokens }: { tokens: { [field in Field]?: Token } }) { export function TokenWarningCards({ currencies }: { currencies: { [field in Field]?: Currency } }) {
return ( return (
<WarningContainer> <WarningContainer>
{Object.keys(tokens).map(field => {Object.keys(currencies).map(field =>
tokens[field] ? <TokenWarningCard style={{ marginBottom: 14 }} key={field} token={tokens[field]} /> : null currencies[field] instanceof Token ? (
<TokenWarningCard style={{ marginBottom: 14 }} key={field} token={currencies[field]} />
) : null
)} )}
</WarningContainer> </WarningContainer>
) )

View File

@@ -1,4 +1,4 @@
import React from 'react' import React, { useCallback, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import Popover, { PopoverProps } from '../Popover' import Popover, { PopoverProps } from '../Popover'
@@ -16,3 +16,16 @@ interface TooltipProps extends Omit<PopoverProps, 'content'> {
export default function Tooltip({ text, ...rest }: TooltipProps) { export default function Tooltip({ text, ...rest }: TooltipProps) {
return <Popover content={<TooltipContainer>{text}</TooltipContainer>} {...rest} /> return <Popover content={<TooltipContainer>{text}</TooltipContainer>} {...rest} />
} }
export function MouseoverTooltip({ 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}>
<div onMouseEnter={open} onMouseLeave={close}>
{children}
</div>
</Tooltip>
)
}

View File

@@ -1,73 +0,0 @@
import React, { useCallback, useContext, useState } from 'react'
import { AlertCircle, CheckCircle } from 'react-feather'
import styled, { ThemeContext } from 'styled-components'
import { useActiveWeb3React } from '../../hooks'
import useInterval from '../../hooks/useInterval'
import { useRemovePopup } from '../../state/application/hooks'
import { TYPE } from '../../theme'
import { ExternalLink } from '../../theme/components'
import { getEtherscanLink } from '../../utils'
import { AutoColumn } from '../Column'
import { AutoRow } from '../Row'
const Fader = styled.div<{ count: number }>`
position: absolute;
bottom: 0px;
left: 0px;
width: ${({ count }) => `calc(100% - (100% / ${150 / count}))`};
height: 2px;
background-color: ${({ theme }) => theme.bg3};
transition: width 100ms linear;
`
const delay = 100
export default function TxnPopup({
hash,
success,
summary,
popKey
}: {
hash: string
success?: boolean
summary?: string
popKey?: string
}) {
const { chainId } = useActiveWeb3React()
const [count, setCount] = useState(1)
const [isRunning, setIsRunning] = useState(true)
const removePopup = useRemovePopup()
const removeThisPopup = useCallback(() => removePopup(popKey), [popKey, removePopup])
useInterval(
() => {
count > 150 ? removeThisPopup() : setCount(count + 1)
},
isRunning ? delay : null
)
const handleMouseEnter = useCallback(() => setIsRunning(false), [])
const handleMouseLeave = useCallback(() => setIsRunning(true), [])
const theme = useContext(ThemeContext)
return (
<AutoRow onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<div style={{ paddingRight: 16 }}>
{success ? <CheckCircle color={theme.green1} size={24} /> : <AlertCircle color={theme.red1} size={24} />}
</div>
<AutoColumn gap="8px">
<TYPE.body fontWeight={500}>
{summary ? summary : 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}
</TYPE.body>
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>View on Etherscan</ExternalLink>
</AutoColumn>
<Fader count={count} />
</AutoRow>
)
}

View File

@@ -15,7 +15,7 @@ const InfoCard = styled.button<{ active?: boolean }>`
border-color: ${({ theme, active }) => (active ? 'transparent' : theme.bg3)}; border-color: ${({ theme, active }) => (active ? 'transparent' : theme.bg3)};
` `
const OptionCard = styled(InfoCard)` const OptionCard = styled(InfoCard as any)`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
@@ -30,7 +30,7 @@ const OptionCardLeft = styled.div`
height: 100%; height: 100%;
` `
const OptionCardClickable = styled(OptionCard)<{ clickable?: boolean }>` const OptionCardClickable = styled(OptionCard as any)<{ clickable?: boolean }>`
margin-top: 0; margin-top: 0;
&:hover { &:hover {
cursor: ${({ clickable }) => (clickable ? 'pointer' : '')}; cursor: ${({ clickable }) => (clickable ? 'pointer' : '')};
@@ -114,7 +114,6 @@ export default function Option({
<OptionCardClickable id={id} onClick={onClick} clickable={clickable && !active} active={active}> <OptionCardClickable id={id} onClick={onClick} clickable={clickable && !active} active={active}>
<OptionCardLeft> <OptionCardLeft>
<HeaderText color={color}> <HeaderText color={color}>
{' '}
{active ? ( {active ? (
<CircleWrapper> <CircleWrapper>
<GreenCircle> <GreenCircle>

View File

@@ -3,8 +3,7 @@ import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import Option from './Option' import Option from './Option'
import { SUPPORTED_WALLETS } from '../../constants' import { SUPPORTED_WALLETS } from '../../constants'
import WalletConnectData from './WalletConnectData' import { injected } from '../../connectors'
import { walletconnect, injected } from '../../connectors'
import { darken } from 'polished' import { darken } from 'polished'
import Loader from '../Loader' import Loader from '../Loader'
@@ -65,28 +64,22 @@ const LoadingWrapper = styled.div`
` `
export default function PendingView({ export default function PendingView({
uri = '',
size,
connector, connector,
error = false, error = false,
setPendingError, setPendingError,
tryActivation tryActivation
}: { }: {
uri?: string
size?: number
connector?: AbstractConnector connector?: AbstractConnector
error?: boolean error?: boolean
setPendingError: (error: boolean) => void setPendingError: (error: boolean) => void
tryActivation: (connector: AbstractConnector) => void tryActivation: (connector: AbstractConnector) => void
}) { }) {
const isMetamask = window.ethereum && window.ethereum.isMetaMask const isMetamask = window?.ethereum?.isMetaMask
return ( return (
<PendingSection> <PendingSection>
{!error && connector === walletconnect && <WalletConnectData size={size} uri={uri} />}
<LoadingMessage error={error}> <LoadingMessage error={error}>
<LoadingWrapper> <LoadingWrapper>
{!error && <StyledLoader />}
{error ? ( {error ? (
<ErrorGroup> <ErrorGroup>
<div>Error connecting.</div> <div>Error connecting.</div>
@@ -99,10 +92,11 @@ export default function PendingView({
Try Again Try Again
</ErrorButton> </ErrorButton>
</ErrorGroup> </ErrorGroup>
) : connector === walletconnect ? (
'Scan QR code with a compatible wallet...'
) : ( ) : (
'Initializing...' <>
<StyledLoader />
Initializing...
</>
)} )}
</LoadingWrapper> </LoadingWrapper>
</LoadingMessage> </LoadingMessage>

View File

@@ -1,24 +0,0 @@
import React from 'react'
import styled from 'styled-components'
import QRCode from 'qrcode.react'
const QRCodeWrapper = styled.div`
${({ theme }) => theme.flexColumnNoWrap};
align-items: center;
justify-content: center;
border-radius: 12px;
margin-bottom: 20px;
`
const StyledQRCode = styled(QRCode)`
border: 3px solid white;
`
interface WalletConnectDataProps {
uri?: string
size: number
}
export default function WalletConnectData({ uri = '', size }: WalletConnectDataProps) {
return <QRCodeWrapper>{uri && <StyledQRCode size={size} value={uri} />}</QRCodeWrapper>
}

View File

@@ -3,7 +3,6 @@ import ReactGA from 'react-ga'
import styled from 'styled-components' import styled from 'styled-components'
import { isMobile } from 'react-device-detect' import { isMobile } from 'react-device-detect'
import { UnsupportedChainIdError, useWeb3React } from '@web3-react/core' import { UnsupportedChainIdError, useWeb3React } from '@web3-react/core'
import { URI_AVAILABLE } from '@web3-react/walletconnect-connector'
import usePrevious from '../../hooks/usePrevious' import usePrevious from '../../hooks/usePrevious'
import { useWalletModalOpen, useWalletModalToggle } from '../../state/application/hooks' import { useWalletModalOpen, useWalletModalToggle } from '../../state/application/hooks'
@@ -15,8 +14,9 @@ import { SUPPORTED_WALLETS } from '../../constants'
import { ExternalLink } from '../../theme' import { ExternalLink } from '../../theme'
import MetamaskIcon from '../../assets/images/metamask.png' import MetamaskIcon from '../../assets/images/metamask.png'
import { ReactComponent as Close } from '../../assets/images/x.svg' import { ReactComponent as Close } from '../../assets/images/x.svg'
import { injected, walletconnect, fortmatic, portis } from '../../connectors' import { injected, fortmatic, portis } from '../../connectors'
import { OVERLAY_READY } from '../../connectors/Fortmatic' import { OVERLAY_READY } from '../../connectors/Fortmatic'
import { WalletConnectConnector } from '@web3-react/walletconnect-connector'
const CloseIcon = styled.div` const CloseIcon = styled.div`
position: absolute; position: absolute;
@@ -152,19 +152,6 @@ export default function WalletModal({
} }
}, [walletModalOpen]) }, [walletModalOpen])
// set up uri listener for walletconnect
const [uri, setUri] = useState()
useEffect(() => {
const activateWC = uri => {
setUri(uri)
// setWalletView(WALLET_VIEWS.PENDING)
}
walletconnect.on(URI_AVAILABLE, activateWC)
return () => {
walletconnect.off(URI_AVAILABLE, activateWC)
}
}, [])
// close modal when a connection is successful // close modal when a connection is successful
const activePrevious = usePrevious(active) const activePrevious = usePrevious(active)
const connectorPrevious = usePrevious(connector) const connectorPrevious = usePrevious(connector)
@@ -190,6 +177,12 @@ export default function WalletModal({
}) })
setPendingWallet(connector) // set wallet for pending view setPendingWallet(connector) // set wallet for pending view
setWalletView(WALLET_VIEWS.PENDING) setWalletView(WALLET_VIEWS.PENDING)
// if the connector is walletconnect and the user has already tried to connect, manually reset the connector
if (connector instanceof WalletConnectConnector && connector.walletConnectProvider?.wc?.uri) {
connector.walletConnectProvider = undefined
}
activate(connector, undefined, true).catch(error => { activate(connector, undefined, true).catch(error => {
if (error instanceof UnsupportedChainIdError) { if (error instanceof UnsupportedChainIdError) {
activate(connector) // a little janky...can't use setError because the connector isn't set activate(connector) // a little janky...can't use setError because the connector isn't set
@@ -345,8 +338,6 @@ export default function WalletModal({
<ContentWrapper> <ContentWrapper>
{walletView === WALLET_VIEWS.PENDING ? ( {walletView === WALLET_VIEWS.PENDING ? (
<PendingView <PendingView
uri={uri}
size={220}
connector={pendingWallet} connector={pendingWallet}
error={pendingError} error={pendingError}
setPendingError={setPendingError} setPendingError={setPendingError}

View File

@@ -5,6 +5,7 @@ import { useWeb3React, UnsupportedChainIdError } from '@web3-react/core'
import { darken, lighten } from 'polished' import { darken, lighten } from 'polished'
import { Activity } from 'react-feather' import { Activity } from 'react-feather'
import useENSName from '../../hooks/useENSName' import useENSName from '../../hooks/useENSName'
import { useHasSocks } from '../../hooks/useSocksBalance'
import { useWalletModalToggle } from '../../state/application/hooks' import { useWalletModalToggle } from '../../state/application/hooks'
import { TransactionDetails } from '../../state/transactions/reducer' import { TransactionDetails } from '../../state/transactions/reducer'
@@ -125,12 +126,18 @@ function recentTransactionsOnly(a: TransactionDetails) {
return new Date().getTime() - a.addedTime < 86_400_000 return new Date().getTime() - a.addedTime < 86_400_000
} }
const SOCK = (
<span role="img" aria-label="has socks emoji" style={{ marginTop: -4, marginBottom: -4 }}>
🧦
</span>
)
export default function Web3Status() { export default function Web3Status() {
const { t } = useTranslation() const { t } = useTranslation()
const { active, account, connector, error } = useWeb3React() const { active, account, connector, error } = useWeb3React()
const contextNetwork = useWeb3React(NetworkContextName) const contextNetwork = useWeb3React(NetworkContextName)
const ENSName = useENSName(account) const { ENSName } = useENSName(account)
const allTransactions = useAllTransactions() const allTransactions = useAllTransactions()
@@ -143,7 +150,7 @@ export default function Web3Status() {
const confirmed = sortedRecentTransactions.filter(tx => tx.receipt).map(tx => tx.hash) const confirmed = sortedRecentTransactions.filter(tx => tx.receipt).map(tx => tx.hash)
const hasPendingTransactions = !!pending.length const hasPendingTransactions = !!pending.length
const hasSocks = useHasSocks()
const toggleWalletModal = useWalletModalToggle() const toggleWalletModal = useWalletModalToggle()
// handle the logo we want to show with the account // handle the logo we want to show with the account
@@ -186,7 +193,10 @@ export default function Web3Status() {
<Text>{pending?.length} Pending</Text> <Loader stroke="white" /> <Text>{pending?.length} Pending</Text> <Loader stroke="white" />
</RowBetween> </RowBetween>
) : ( ) : (
<Text>{ENSName || shortenAddress(account)}</Text> <>
{hasSocks ? SOCK : null}
<Text>{ENSName || shortenAddress(account)}</Text>
</>
)} )}
{!hasPendingTransactions && getStatusIcon()} {!hasPendingTransactions && getStatusIcon()}
</Web3StatusConnected> </Web3StatusConnected>

View File

@@ -1,19 +1,16 @@
import { Trade, TradeType } from '@uniswap/sdk' import { Trade, TradeType } from '@uniswap/sdk'
import React, { useContext } from 'react' import React, { useContext } from 'react'
import { ChevronRight } from 'react-feather'
import { Flex } from 'rebass'
import { ThemeContext } from 'styled-components' import { ThemeContext } from 'styled-components'
import { Field } from '../../state/swap/actions' import { Field } from '../../state/swap/actions'
import { useUserSlippageTolerance } from '../../state/user/hooks'
import { TYPE } from '../../theme' import { TYPE } from '../../theme'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown } from '../../utils/prices' import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown } from '../../utils/prices'
import { AutoColumn } from '../Column' import { AutoColumn } from '../Column'
import { SectionBreak } from './styleds'
import QuestionHelper from '../QuestionHelper' import QuestionHelper from '../QuestionHelper'
import { RowBetween, RowFixed } from '../Row' import { RowBetween, RowFixed } from '../Row'
import FormattedPriceImpact from './FormattedPriceImpact' import FormattedPriceImpact from './FormattedPriceImpact'
import TokenLogo from '../TokenLogo' import { SectionBreak } from './styleds'
import flatMap from 'lodash.flatmap' import SwapRoute from './SwapRoute'
import { useUserSlippageTolerance } from '../../state/user/hooks'
function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippage: number }) { function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippage: number }) {
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
@@ -34,8 +31,10 @@ function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippag
<RowFixed> <RowFixed>
<TYPE.black color={theme.text1} fontSize={14}> <TYPE.black color={theme.text1} fontSize={14}>
{isExactIn {isExactIn
? `${slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(4)} ${trade.outputAmount.token.symbol}` ?? '-' ? `${slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(4)} ${trade.outputAmount.currency.symbol}` ??
: `${slippageAdjustedAmounts[Field.INPUT]?.toSignificant(4)} ${trade.inputAmount.token.symbol}` ?? '-'} '-'
: `${slippageAdjustedAmounts[Field.INPUT]?.toSignificant(4)} ${trade.inputAmount.currency.symbol}` ??
'-'}
</TYPE.black> </TYPE.black>
</RowFixed> </RowFixed>
</RowBetween> </RowBetween>
@@ -57,7 +56,7 @@ function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippag
<QuestionHelper text="A portion of each trade (0.30%) goes to liquidity providers as a protocol incentive." /> <QuestionHelper text="A portion of each trade (0.30%) goes to liquidity providers as a protocol incentive." />
</RowFixed> </RowFixed>
<TYPE.black fontSize={14} color={theme.text1}> <TYPE.black fontSize={14} color={theme.text1}>
{realizedLPFee ? `${realizedLPFee.toSignificant(4)} ${trade.inputAmount.token.symbol}` : '-'} {realizedLPFee ? `${realizedLPFee.toSignificant(4)} ${trade.inputAmount.currency.symbol}` : '-'}
</TYPE.black> </TYPE.black>
</RowBetween> </RowBetween>
</AutoColumn> </AutoColumn>
@@ -79,49 +78,19 @@ export function AdvancedSwapDetails({ trade }: AdvancedSwapDetailsProps) {
return ( return (
<AutoColumn gap="md"> <AutoColumn gap="md">
{trade && <TradeSummary trade={trade} allowedSlippage={allowedSlippage} />} {trade && <TradeSummary trade={trade} allowedSlippage={allowedSlippage} />}
{showRoute && <SectionBreak />}
{showRoute && ( {showRoute && (
<AutoColumn style={{ padding: '0 24px' }}> <>
<RowFixed> <SectionBreak />
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}> <AutoColumn style={{ padding: '0 24px' }}>
Route <RowFixed>
</TYPE.black> <TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
<QuestionHelper text="Routing through these tokens resulted in the best price for your trade." /> Route
</RowFixed> </TYPE.black>
<Flex <QuestionHelper text="Routing through these tokens resulted in the best price for your trade." />
px="1rem" </RowFixed>
py="0.5rem" <SwapRoute trade={trade} />
my="0.5rem" </AutoColumn>
style={{ border: `1px solid ${theme.bg3}`, borderRadius: '1rem' }} </>
flexWrap="wrap"
width="100%"
justifyContent="space-evenly"
alignItems="center"
>
{flatMap(
trade.route.path,
// add a null in-between each item
(token, i, array) => {
const lastItem = i === array.length - 1
return lastItem ? [token] : [token, null]
}
).map((token, i) => {
// use null as an indicator to insert chevrons
if (token === null) {
return <ChevronRight key={i} color={theme.text2} />
} else {
return (
<Flex my="0.5rem" alignItems="center" key={token.address} style={{ flexShrink: 0 }}>
<TokenLogo address={token.address} size="1.5rem" />
<TYPE.black fontSize={14} color={theme.text1} ml="0.5rem">
{token.symbol}
</TYPE.black>
</Flex>
)
}
})}
</Flex>
</AutoColumn>
)} )}
</AutoColumn> </AutoColumn>
) )

View File

@@ -24,7 +24,7 @@ export default function AdvancedSwapDetailsDropdown({ trade, ...rest }: Advanced
return ( return (
<AdvancedDetailsFooter show={Boolean(trade)}> <AdvancedDetailsFooter show={Boolean(trade)}>
<AdvancedSwapDetails {...rest} trade={lastTrade} /> <AdvancedSwapDetails {...rest} trade={trade ?? lastTrade} />
</AdvancedDetailsFooter> </AdvancedDetailsFooter>
) )
} }

View File

@@ -1,4 +1,4 @@
import { Percent, TokenAmount, Trade, TradeType } from '@uniswap/sdk' import { CurrencyAmount, Percent, Trade, TradeType } from '@uniswap/sdk'
import React, { useContext } from 'react' import React, { useContext } from 'react'
import { Repeat } from 'react-feather' import { Repeat } from 'react-feather'
import { Text } from 'rebass' import { Text } from 'rebass'
@@ -29,10 +29,10 @@ export default function SwapModalFooter({
showInverted: boolean showInverted: boolean
setShowInverted: (inverted: boolean) => void setShowInverted: (inverted: boolean) => void
severity: number severity: number
slippageAdjustedAmounts?: { [field in Field]?: TokenAmount } slippageAdjustedAmounts?: { [field in Field]?: CurrencyAmount }
onSwap: () => any onSwap: () => any
parsedAmounts?: { [field in Field]?: TokenAmount } parsedAmounts?: { [field in Field]?: CurrencyAmount }
realizedLPFee?: TokenAmount realizedLPFee?: CurrencyAmount
priceImpactWithoutFee?: Percent priceImpactWithoutFee?: Percent
confirmText: string confirmText: string
}) { }) {
@@ -71,7 +71,7 @@ export default function SwapModalFooter({
<RowBetween> <RowBetween>
<RowFixed> <RowFixed>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}> <TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
{trade?.tradeType === TradeType.EXACT_INPUT ? 'Minimum sent' : 'Maximum sold'} {trade?.tradeType === TradeType.EXACT_INPUT ? 'Minimum received' : 'Maximum sold'}
</TYPE.black> </TYPE.black>
<QuestionHelper text="Your transaction will revert if there is a large, unfavorable price movement before it is confirmed." /> <QuestionHelper text="Your transaction will revert if there is a large, unfavorable price movement before it is confirmed." />
</RowFixed> </RowFixed>
@@ -84,8 +84,8 @@ export default function SwapModalFooter({
{parsedAmounts[Field.OUTPUT] && parsedAmounts[Field.INPUT] && ( {parsedAmounts[Field.OUTPUT] && parsedAmounts[Field.INPUT] && (
<TYPE.black fontSize={14} marginLeft={'4px'}> <TYPE.black fontSize={14} marginLeft={'4px'}>
{trade?.tradeType === TradeType.EXACT_INPUT {trade?.tradeType === TradeType.EXACT_INPUT
? parsedAmounts[Field.OUTPUT]?.token?.symbol ? parsedAmounts[Field.OUTPUT]?.currency?.symbol
: parsedAmounts[Field.INPUT]?.token?.symbol} : parsedAmounts[Field.INPUT]?.currency?.symbol}
</TYPE.black> </TYPE.black>
)} )}
</RowFixed> </RowFixed>
@@ -107,7 +107,7 @@ export default function SwapModalFooter({
<QuestionHelper text="A portion of each trade (0.30%) goes to liquidity providers as a protocol incentive." /> <QuestionHelper text="A portion of each trade (0.30%) goes to liquidity providers as a protocol incentive." />
</RowFixed> </RowFixed>
<TYPE.black fontSize={14}> <TYPE.black fontSize={14}>
{realizedLPFee ? realizedLPFee?.toSignificant(6) + ' ' + trade?.inputAmount?.token?.symbol : '-'} {realizedLPFee ? realizedLPFee?.toSignificant(6) + ' ' + trade?.inputAmount?.currency?.symbol : '-'}
</TYPE.black> </TYPE.black>
</RowBetween> </RowBetween>
</AutoColumn> </AutoColumn>

View File

@@ -1,27 +1,30 @@
import { Token, TokenAmount } from '@uniswap/sdk' import { Currency, CurrencyAmount } from '@uniswap/sdk'
import React, { useContext } from 'react' import React, { useContext } from 'react'
import { ArrowDown } from 'react-feather' import { ArrowDown } from 'react-feather'
import { Text } from 'rebass' import { Text } from 'rebass'
import { ThemeContext } from 'styled-components' import { ThemeContext } from 'styled-components'
import { Field } from '../../state/swap/actions' import { Field } from '../../state/swap/actions'
import { TYPE } from '../../theme' import { TYPE } from '../../theme'
import { isAddress, shortenAddress } from '../../utils'
import { AutoColumn } from '../Column' import { AutoColumn } from '../Column'
import { RowBetween, RowFixed } from '../Row' import { RowBetween, RowFixed } from '../Row'
import TokenLogo from '../TokenLogo' import CurrencyLogo from '../CurrencyLogo'
import { TruncatedText } from './styleds' import { TruncatedText } from './styleds'
export default function SwapModalHeader({ export default function SwapModalHeader({
tokens, currencies,
formattedAmounts, formattedAmounts,
slippageAdjustedAmounts, slippageAdjustedAmounts,
priceImpactSeverity, priceImpactSeverity,
independentField independentField,
recipient
}: { }: {
tokens: { [field in Field]?: Token } currencies: { [field in Field]?: Currency }
formattedAmounts: { [field in Field]?: string } formattedAmounts: { [field in Field]?: string }
slippageAdjustedAmounts: { [field in Field]?: TokenAmount } slippageAdjustedAmounts: { [field in Field]?: CurrencyAmount }
priceImpactSeverity: number priceImpactSeverity: number
independentField: Field independentField: Field
recipient: string | null
}) { }) {
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
@@ -32,9 +35,9 @@ export default function SwapModalHeader({
{formattedAmounts[Field.INPUT]} {formattedAmounts[Field.INPUT]}
</TruncatedText> </TruncatedText>
<RowFixed gap="4px"> <RowFixed gap="4px">
<TokenLogo address={tokens[Field.INPUT]?.address} size={'24px'} /> <CurrencyLogo currency={currencies[Field.INPUT]} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}> <Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{tokens[Field.INPUT]?.symbol} {currencies[Field.INPUT]?.symbol}
</Text> </Text>
</RowFixed> </RowFixed>
</RowBetween> </RowBetween>
@@ -46,9 +49,9 @@ export default function SwapModalHeader({
{formattedAmounts[Field.OUTPUT]} {formattedAmounts[Field.OUTPUT]}
</TruncatedText> </TruncatedText>
<RowFixed gap="4px"> <RowFixed gap="4px">
<TokenLogo address={tokens[Field.OUTPUT]?.address} size={'24px'} /> <CurrencyLogo currency={currencies[Field.OUTPUT]} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}> <Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{tokens[Field.OUTPUT]?.symbol} {currencies[Field.OUTPUT]?.symbol}
</Text> </Text>
</RowFixed> </RowFixed>
</RowBetween> </RowBetween>
@@ -57,7 +60,7 @@ export default function SwapModalHeader({
<TYPE.italic textAlign="left" style={{ width: '100%' }}> <TYPE.italic textAlign="left" style={{ width: '100%' }}>
{`Output is estimated. You will receive at least `} {`Output is estimated. You will receive at least `}
<b> <b>
{slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} {tokens[Field.OUTPUT]?.symbol} {slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} {currencies[Field.OUTPUT]?.symbol}
</b> </b>
{' or the transaction will revert.'} {' or the transaction will revert.'}
</TYPE.italic> </TYPE.italic>
@@ -65,12 +68,20 @@ export default function SwapModalHeader({
<TYPE.italic textAlign="left" style={{ width: '100%' }}> <TYPE.italic textAlign="left" style={{ width: '100%' }}>
{`Input is estimated. You will sell at most `} {`Input is estimated. You will sell at most `}
<b> <b>
{slippageAdjustedAmounts[Field.INPUT]?.toSignificant(6)} {tokens[Field.INPUT]?.symbol} {slippageAdjustedAmounts[Field.INPUT]?.toSignificant(6)} {currencies[Field.INPUT]?.symbol}
</b> </b>
{' or the transaction will revert.'} {' or the transaction will revert.'}
</TYPE.italic> </TYPE.italic>
)} )}
</AutoColumn> </AutoColumn>
{recipient !== null ? (
<AutoColumn justify="flex-start" gap="sm" style={{ padding: '12px 0 0 0px' }}>
<TYPE.main>
Output will be sent to{' '}
<b title={recipient}>{isAddress(recipient) ? shortenAddress(recipient) : recipient}</b>
</TYPE.main>
</AutoColumn>
) : null}
</AutoColumn> </AutoColumn>
) )
} }

View File

@@ -0,0 +1,38 @@
import { Trade } from '@uniswap/sdk'
import React, { Fragment, memo, useContext } from 'react'
import { ChevronRight } from 'react-feather'
import { Flex } from 'rebass'
import { ThemeContext } from 'styled-components'
import { TYPE } from '../../theme'
import CurrencyLogo from '../CurrencyLogo'
export default memo(function SwapRoute({ trade }: { trade: Trade }) {
const theme = useContext(ThemeContext)
return (
<Flex
px="1rem"
py="0.5rem"
my="0.5rem"
style={{ border: `1px solid ${theme.bg3}`, borderRadius: '1rem' }}
flexWrap="wrap"
width="100%"
justifyContent="space-evenly"
alignItems="center"
>
{trade.route.path.map((token, i, path) => {
const isLastItem: boolean = i === path.length - 1
return (
<Fragment key={i}>
<Flex my="0.5rem" alignItems="center" style={{ flexShrink: 0 }}>
<CurrencyLogo currency={token} size="1.5rem" />
<TYPE.black fontSize={14} color={theme.text1} ml="0.5rem">
{token.symbol}
</TYPE.black>
</Flex>
{isLastItem ? null : <ChevronRight color={theme.text2} />}
</Fragment>
)
})}
</Flex>
)
})

View File

@@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import { Price, Token } from '@uniswap/sdk' import { Currency, Price } from '@uniswap/sdk'
import { useContext } from 'react' import { useContext } from 'react'
import { Repeat } from 'react-feather' import { Repeat } from 'react-feather'
import { Text } from 'rebass' import { Text } from 'rebass'
@@ -8,21 +8,27 @@ import { StyledBalanceMaxMini } from './styleds'
interface TradePriceProps { interface TradePriceProps {
price?: Price price?: Price
inputToken?: Token inputCurrency?: Currency
outputToken?: Token outputCurrency?: Currency
showInverted: boolean showInverted: boolean
setShowInverted: (showInverted: boolean) => void setShowInverted: (showInverted: boolean) => void
} }
export default function TradePrice({ price, inputToken, outputToken, showInverted, setShowInverted }: TradePriceProps) { export default function TradePrice({
price,
inputCurrency,
outputCurrency,
showInverted,
setShowInverted
}: TradePriceProps) {
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
const formattedPrice = showInverted ? price?.toSignificant(6) : price?.invert()?.toSignificant(6) const formattedPrice = showInverted ? price?.toSignificant(6) : price?.invert()?.toSignificant(6)
const show = Boolean(inputToken && outputToken) const show = Boolean(inputCurrency && outputCurrency)
const label = showInverted const label = showInverted
? `${outputToken?.symbol} per ${inputToken?.symbol}` ? `${outputCurrency?.symbol} per ${inputCurrency?.symbol}`
: `${inputToken?.symbol} per ${outputToken?.symbol}` : `${inputCurrency?.symbol} per ${outputCurrency?.symbol}`
return ( return (
<Text <Text

View File

@@ -1,55 +0,0 @@
import { TokenAmount } from '@uniswap/sdk'
import React from 'react'
import { Text } from 'rebass'
import { useActiveWeb3React } from '../../hooks'
import { ExternalLink, TYPE } from '../../theme'
import { getEtherscanLink } from '../../utils'
import Copy from '../AccountDetails/Copy'
import { AutoColumn } from '../Column'
import { AutoRow, RowBetween } from '../Row'
import TokenLogo from '../TokenLogo'
export function TransferModalHeader({
recipient,
ENSName,
amount
}: {
recipient: string
ENSName: string
amount: TokenAmount
}) {
const { chainId } = useActiveWeb3React()
return (
<AutoColumn gap="lg" style={{ marginTop: '40px' }}>
<RowBetween>
<Text fontSize={36} fontWeight={500}>
{amount?.toSignificant(6)} {amount?.token?.symbol}
</Text>
<TokenLogo address={amount?.token?.address} size={'30px'} />
</RowBetween>
<TYPE.darkGray fontSize={20}>To</TYPE.darkGray>
{ENSName ? (
<AutoColumn gap="lg">
<TYPE.blue fontSize={36}>{ENSName}</TYPE.blue>
<AutoRow gap="10px">
<ExternalLink href={getEtherscanLink(chainId, ENSName, 'address')}>
<TYPE.blue fontSize={18}>
{recipient?.slice(0, 8)}...{recipient?.slice(34, 42)}
</TYPE.blue>
</ExternalLink>
<Copy toCopy={recipient} />
</AutoRow>
</AutoColumn>
) : (
<AutoRow gap="10px">
<ExternalLink href={getEtherscanLink(chainId, recipient, 'address')}>
<TYPE.blue fontSize={36}>
{recipient?.slice(0, 6)}...{recipient?.slice(36, 42)}
</TYPE.blue>
</ExternalLink>
<Copy toCopy={recipient} />
</AutoRow>
)}
</AutoColumn>
)
}

View File

@@ -1,4 +1,4 @@
import styled from 'styled-components' import styled, { css } from 'styled-components'
import { AutoColumn } from '../Column' import { AutoColumn } from '../Column'
import { Text } from 'rebass' import { Text } from 'rebass'
@@ -8,17 +8,18 @@ export const Wrapper = styled.div`
position: relative; position: relative;
` `
export const ArrowWrapper = styled.div` export const ArrowWrapper = styled.div<{ clickable: boolean }>`
padding: 2px; padding: 2px;
border-radius: 12px;
display: flex;
justify-content: center;
align-items: center;
:hover { ${({ clickable }) =>
cursor: pointer; clickable
opacity: 0.8; ? css`
} :hover {
cursor: pointer;
opacity: 0.8;
}
`
: null}
` `
export const SectionBreak = styled.div` export const SectionBreak = styled.div`

View File

@@ -46,7 +46,16 @@ class MiniRpcProvider implements AsyncSendable {
.catch(error => callback(error, null)) .catch(error => callback(error, null))
} }
public readonly request = async (method: string, params?: unknown[] | object): Promise<unknown> => { public readonly request = async (
method: string | { method: string; params: unknown[] },
params?: unknown[] | object
): Promise<unknown> => {
if (typeof method !== 'string') {
return this.request(method.method, method.params)
}
if (method === 'eth_chainId') {
return `0x${this.chainId.toString(16)}`
}
const response = await fetch(this.url, { const response = await fetch(this.url, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({

View File

@@ -6,7 +6,6 @@ import { PortisConnector } from '@web3-react/portis-connector'
import { FortmaticConnector } from './Fortmatic' import { FortmaticConnector } from './Fortmatic'
import { NetworkConnector } from './NetworkConnector' import { NetworkConnector } from './NetworkConnector'
const POLLING_INTERVAL = 10000
const NETWORK_URL = process.env.REACT_APP_NETWORK_URL const NETWORK_URL = process.env.REACT_APP_NETWORK_URL
const FORMATIC_KEY = process.env.REACT_APP_FORTMATIC_KEY const FORMATIC_KEY = process.env.REACT_APP_FORTMATIC_KEY
const PORTIS_ID = process.env.REACT_APP_PORTIS_ID const PORTIS_ID = process.env.REACT_APP_PORTIS_ID
@@ -27,8 +26,8 @@ export const injected = new InjectedConnector({
export const walletconnect = new WalletConnectConnector({ export const walletconnect = new WalletConnectConnector({
rpc: { 1: NETWORK_URL }, rpc: { 1: NETWORK_URL },
bridge: 'https://bridge.walletconnect.org', bridge: 'https://bridge.walletconnect.org',
qrcode: false, qrcode: true,
pollingInterval: POLLING_INTERVAL pollingInterval: 15000
}) })
// mainnet only // mainnet only

View File

@@ -0,0 +1,471 @@
[
{
"name": "Transfer",
"inputs": [
{
"type": "address",
"name": "_from",
"indexed": true
},
{
"type": "address",
"name": "_to",
"indexed": true
},
{
"type": "uint256",
"name": "_tokenId",
"indexed": true
}
],
"anonymous": false,
"type": "event"
},
{
"name": "Approval",
"inputs": [
{
"type": "address",
"name": "_owner",
"indexed": true
},
{
"type": "address",
"name": "_approved",
"indexed": true
},
{
"type": "uint256",
"name": "_tokenId",
"indexed": true
}
],
"anonymous": false,
"type": "event"
},
{
"name": "ApprovalForAll",
"inputs": [
{
"type": "address",
"name": "_owner",
"indexed": true
},
{
"type": "address",
"name": "_operator",
"indexed": true
},
{
"type": "bool",
"name": "_approved",
"indexed": false
}
],
"anonymous": false,
"type": "event"
},
{
"outputs": [],
"inputs": [],
"constant": false,
"payable": false,
"type": "constructor"
},
{
"name": "tokenURI",
"outputs": [
{
"type": "string",
"name": "out"
}
],
"inputs": [
{
"type": "uint256",
"name": "_tokenId"
}
],
"constant": true,
"payable": false,
"type": "function",
"gas": 22405
},
{
"name": "tokenByIndex",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [
{
"type": "uint256",
"name": "_index"
}
],
"constant": true,
"payable": false,
"type": "function",
"gas": 631
},
{
"name": "tokenOfOwnerByIndex",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [
{
"type": "address",
"name": "_owner"
},
{
"type": "uint256",
"name": "_index"
}
],
"constant": true,
"payable": false,
"type": "function",
"gas": 1248
},
{
"name": "transferFrom",
"outputs": [],
"inputs": [
{
"type": "address",
"name": "_from"
},
{
"type": "address",
"name": "_to"
},
{
"type": "uint256",
"name": "_tokenId"
}
],
"constant": false,
"payable": false,
"type": "function",
"gas": 259486
},
{
"name": "safeTransferFrom",
"outputs": [],
"inputs": [
{
"type": "address",
"name": "_from"
},
{
"type": "address",
"name": "_to"
},
{
"type": "uint256",
"name": "_tokenId"
}
],
"constant": false,
"payable": false,
"type": "function"
},
{
"name": "safeTransferFrom",
"outputs": [],
"inputs": [
{
"type": "address",
"name": "_from"
},
{
"type": "address",
"name": "_to"
},
{
"type": "uint256",
"name": "_tokenId"
},
{
"type": "bytes",
"name": "_data"
}
],
"constant": false,
"payable": false,
"type": "function"
},
{
"name": "approve",
"outputs": [],
"inputs": [
{
"type": "address",
"name": "_approved"
},
{
"type": "uint256",
"name": "_tokenId"
}
],
"constant": false,
"payable": false,
"type": "function",
"gas": 38422
},
{
"name": "setApprovalForAll",
"outputs": [],
"inputs": [
{
"type": "address",
"name": "_operator"
},
{
"type": "bool",
"name": "_approved"
}
],
"constant": false,
"payable": false,
"type": "function",
"gas": 38016
},
{
"name": "mint",
"outputs": [
{
"type": "bool",
"name": "out"
}
],
"inputs": [
{
"type": "address",
"name": "_to"
}
],
"constant": false,
"payable": false,
"type": "function",
"gas": 182636
},
{
"name": "changeMinter",
"outputs": [],
"inputs": [
{
"type": "address",
"name": "_minter"
}
],
"constant": false,
"payable": false,
"type": "function",
"gas": 35897
},
{
"name": "changeURI",
"outputs": [],
"inputs": [
{
"type": "address",
"name": "_newURI"
}
],
"constant": false,
"payable": false,
"type": "function",
"gas": 35927
},
{
"name": "name",
"outputs": [
{
"type": "string",
"name": "out"
}
],
"inputs": [],
"constant": true,
"payable": false,
"type": "function",
"gas": 6612
},
{
"name": "symbol",
"outputs": [
{
"type": "string",
"name": "out"
}
],
"inputs": [],
"constant": true,
"payable": false,
"type": "function",
"gas": 6642
},
{
"name": "totalSupply",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [],
"constant": true,
"payable": false,
"type": "function",
"gas": 873
},
{
"name": "minter",
"outputs": [
{
"type": "address",
"name": "out"
}
],
"inputs": [],
"constant": true,
"payable": false,
"type": "function",
"gas": 903
},
{
"name": "socks",
"outputs": [
{
"type": "address",
"name": "out",
"unit": "Socks"
}
],
"inputs": [],
"constant": true,
"payable": false,
"type": "function",
"gas": 933
},
{
"name": "newURI",
"outputs": [
{
"type": "address",
"name": "out"
}
],
"inputs": [],
"constant": true,
"payable": false,
"type": "function",
"gas": 963
},
{
"name": "ownerOf",
"outputs": [
{
"type": "address",
"name": "out"
}
],
"inputs": [
{
"type": "uint256",
"name": "arg0"
}
],
"constant": true,
"payable": false,
"type": "function",
"gas": 1126
},
{
"name": "balanceOf",
"outputs": [
{
"type": "uint256",
"name": "out"
}
],
"inputs": [
{
"type": "address",
"name": "arg0"
}
],
"constant": true,
"payable": false,
"type": "function",
"gas": 1195
},
{
"name": "getApproved",
"outputs": [
{
"type": "address",
"name": "out"
}
],
"inputs": [
{
"type": "uint256",
"name": "arg0"
}
],
"constant": true,
"payable": false,
"type": "function",
"gas": 1186
},
{
"name": "isApprovedForAll",
"outputs": [
{
"type": "bool",
"name": "out"
}
],
"inputs": [
{
"type": "address",
"name": "arg0"
},
{
"type": "address",
"name": "arg1"
}
],
"constant": true,
"payable": false,
"type": "function",
"gas": 1415
},
{
"name": "supportsInterface",
"outputs": [
{
"type": "bool",
"name": "out"
}
],
"inputs": [
{
"type": "bytes32",
"name": "arg0"
}
],
"constant": true,
"payable": false,
"type": "function",
"gas": 1246
}
]

View File

@@ -0,0 +1,279 @@
[
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "guy",
"type": "address"
},
{
"name": "wad",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "src",
"type": "address"
},
{
"name": "dst",
"type": "address"
},
{
"name": "wad",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "wad",
"type": "uint256"
}
],
"name": "withdraw",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [
{
"name": "",
"type": "uint8"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "dst",
"type": "address"
},
{
"name": "wad",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "deposit",
"outputs": [],
"payable": true,
"stateMutability": "payable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "address"
},
{
"name": "",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"payable": true,
"stateMutability": "payable",
"type": "fallback"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "src",
"type": "address"
},
{
"indexed": true,
"name": "guy",
"type": "address"
},
{
"indexed": false,
"name": "wad",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "src",
"type": "address"
},
{
"indexed": true,
"name": "dst",
"type": "address"
},
{
"indexed": false,
"name": "wad",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "dst",
"type": "address"
},
{
"indexed": false,
"name": "wad",
"type": "uint256"
}
],
"name": "Deposit",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "src",
"type": "address"
},
{
"indexed": false,
"name": "wad",
"type": "uint256"
}
],
"name": "Withdrawal",
"type": "event"
}
]

View File

@@ -1,64 +1,58 @@
import { ChainId, JSBI, Percent, Token, WETH, Pair, TokenAmount } from '@uniswap/sdk' import { ChainId, JSBI, Percent, Token, WETH } from '@uniswap/sdk'
import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors' import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors'
export const ROUTER_ADDRESS = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D' export const ROUTER_ADDRESS = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'
// used to construct intermediary pairs for trading // a list of tokens by chain
export const BASES_TO_CHECK_TRADES_AGAINST: { readonly [chainId in ChainId]: Token[] } = { type ChainTokenList = {
[ChainId.MAINNET]: [ readonly [chainId in ChainId]: Token[]
WETH[ChainId.MAINNET], }
new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'),
new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C') export const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin')
], export const USDC = new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C')
export const USDT = new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'Tether USD')
export const COMP = new Token(ChainId.MAINNET, '0xc00e94Cb662C3520282E6f5717214004A7f26888', 18, 'COMP', 'Compound')
export const MKR = new Token(ChainId.MAINNET, '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', 18, 'MKR', 'Maker')
const WETH_ONLY: ChainTokenList = {
[ChainId.MAINNET]: [WETH[ChainId.MAINNET]],
[ChainId.ROPSTEN]: [WETH[ChainId.ROPSTEN]], [ChainId.ROPSTEN]: [WETH[ChainId.ROPSTEN]],
[ChainId.RINKEBY]: [WETH[ChainId.RINKEBY]], [ChainId.RINKEBY]: [WETH[ChainId.RINKEBY]],
[ChainId.GÖRLI]: [WETH[ChainId.GÖRLI]], [ChainId.GÖRLI]: [WETH[ChainId.GÖRLI]],
[ChainId.KOVAN]: [WETH[ChainId.KOVAN]] [ChainId.KOVAN]: [WETH[ChainId.KOVAN]]
} }
// used to construct intermediary pairs for trading
export const BASES_TO_CHECK_TRADES_AGAINST: ChainTokenList = {
...WETH_ONLY,
[ChainId.MAINNET]: [...WETH_ONLY[ChainId.MAINNET], DAI, USDC, USDT, COMP, MKR]
}
// used for display in the default list when adding liquidity // used for display in the default list when adding liquidity
export const SUGGESTED_BASES = BASES_TO_CHECK_TRADES_AGAINST export const SUGGESTED_BASES: ChainTokenList = {
...WETH_ONLY,
[ChainId.MAINNET]: [...WETH_ONLY[ChainId.MAINNET], DAI, USDC, USDT]
}
// used to construct the list of all pairs we consider by default in the frontend // used to construct the list of all pairs we consider by default in the frontend
export const BASES_TO_TRACK_LIQUIDITY_FOR = BASES_TO_CHECK_TRADES_AGAINST export const BASES_TO_TRACK_LIQUIDITY_FOR: ChainTokenList = {
...WETH_ONLY,
[ChainId.MAINNET]: [...WETH_ONLY[ChainId.MAINNET], DAI, USDC, USDT]
}
export const DUMMY_PAIRS_TO_PIN: { readonly [chainId in ChainId]?: Pair[] } = { export const PINNED_PAIRS: { readonly [chainId in ChainId]?: [Token, Token][] } = {
[ChainId.MAINNET]: [ [ChainId.MAINNET]: [
new Pair( [
new TokenAmount( new Token(ChainId.MAINNET, '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643', 8, 'cDAI', 'Compound Dai'),
new Token(ChainId.MAINNET, '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643', 8, 'cDAI', 'Compound Dai'), new Token(ChainId.MAINNET, '0x39AA39c021dfbaE8faC545936693aC917d5E7563', 8, 'cUSDC', 'Compound USD Coin')
'0' ],
), [USDC, USDT],
new TokenAmount( [DAI, USDT]
new Token(ChainId.MAINNET, '0x39AA39c021dfbaE8faC545936693aC917d5E7563', 8, 'cUSDC', 'Compound USD Coin'),
'0'
)
),
new Pair(
new TokenAmount(
new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C'),
'0'
),
new TokenAmount(
new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'Tether USD'),
'0'
)
),
new Pair(
new TokenAmount(
new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'),
'0'
),
new TokenAmount(
new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'Tether USD'),
'0'
)
)
] ]
} }
const MAINNET_WALLETS = { const TESTNET_CAPABLE_WALLETS = {
INJECTED: { INJECTED: {
connector: injected, connector: injected,
name: 'Injected', name: 'Injected',
@@ -80,9 +74,9 @@ const MAINNET_WALLETS = {
export const SUPPORTED_WALLETS = export const SUPPORTED_WALLETS =
process.env.REACT_APP_CHAIN_ID !== '1' process.env.REACT_APP_CHAIN_ID !== '1'
? MAINNET_WALLETS ? TESTNET_CAPABLE_WALLETS
: { : {
...MAINNET_WALLETS, ...TESTNET_CAPABLE_WALLETS,
...{ ...{
WALLET_CONNECT: { WALLET_CONNECT: {
connector: walletconnect, connector: walletconnect,
@@ -90,7 +84,8 @@ export const SUPPORTED_WALLETS =
iconName: 'walletConnectIcon.svg', iconName: 'walletConnectIcon.svg',
description: 'Connect to Trust Wallet, Rainbow Wallet and more...', description: 'Connect to Trust Wallet, Rainbow Wallet and more...',
href: null, href: null,
color: '#4196FC' color: '#4196FC',
mobile: true
}, },
WALLET_LINK: { WALLET_LINK: {
connector: walletlink, connector: walletlink,
@@ -109,15 +104,6 @@ export const SUPPORTED_WALLETS =
mobile: true, mobile: true,
mobileOnly: true mobileOnly: true
}, },
TRUST_WALLET_LINK: {
name: 'Open in Trust Wallet',
iconName: 'trustWallet.png',
description: 'iOS and Android app.',
href: 'https://link.trustwallet.com/open_url?coin_id=60&url=https://uniswap.exchange/swap',
color: '#1C74CC',
mobile: true,
mobileOnly: true
},
FORTMATIC: { FORTMATIC: {
connector: fortmatic, connector: fortmatic,
name: 'Fortmatic', name: 'Fortmatic',
@@ -161,3 +147,7 @@ export const BLOCKED_PRICE_IMPACT_NON_EXPERT: Percent = new Percent(JSBI.BigInt(
// used to ensure the user doesn't send so much ETH so they end up with <.01 // used to ensure the user doesn't send so much ETH so they end up with <.01
export const MIN_ETH: JSBI = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(16)) // .01 ETH export const MIN_ETH: JSBI = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(16)) // .01 ETH
export const BETTER_TRADE_LINK_THRESHOLD = new Percent(JSBI.BigInt(75), JSBI.BigInt(10000)) export const BETTER_TRADE_LINK_THRESHOLD = new Percent(JSBI.BigInt(75), JSBI.BigInt(10000))
// the Uniswap Default token list lives here
export const DEFAULT_TOKEN_LIST_URL =
'https://unpkg.com/@uniswap/default-token-list@latest/uniswap-default.tokenlist.json'

View File

@@ -1,44 +0,0 @@
import { ChainId, Token, WETH } from '@uniswap/sdk'
import KOVAN_TOKENS from './kovan'
import MAINNET_TOKENS from './mainnet'
import RINKEBY_TOKENS from './rinkeby'
import ROPSTEN_TOKENS from './ropsten'
type AllTokens = Readonly<{ [chainId in ChainId]: Readonly<{ [tokenAddress: string]: Token }> }>
export const ALL_TOKENS: AllTokens = [
// WETH on all chains
...Object.values(WETH),
// chain-specific tokens
...MAINNET_TOKENS,
...RINKEBY_TOKENS,
...KOVAN_TOKENS,
...ROPSTEN_TOKENS
]
// remap WETH to ETH
.map(token => {
if (token.equals(WETH[token.chainId])) {
;(token as any).symbol = 'ETH'
;(token as any).name = 'Ether'
}
return token
})
// put into an object
.reduce<AllTokens>(
(tokenMap, token) => {
if (tokenMap[token.chainId][token.address] !== undefined) throw Error('Duplicate tokens.')
return {
...tokenMap,
[token.chainId]: {
...tokenMap[token.chainId],
[token.address]: token
}
}
},
{
[ChainId.MAINNET]: {},
[ChainId.RINKEBY]: {},
[ChainId.GÖRLI]: {},
[ChainId.ROPSTEN]: {},
[ChainId.KOVAN]: {}
}
)

View File

@@ -1,6 +0,0 @@
import { Token, ChainId } from '@uniswap/sdk'
export default [
new Token(ChainId.KOVAN, '0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa', 18, 'DAI', 'Dai Stablecoin'),
new Token(ChainId.KOVAN, '0xAaF64BFCC32d0F15873a02163e7E500671a4ffcD', 18, 'MKR', 'Maker')
]

View File

@@ -1,125 +0,0 @@
import { Token, ChainId } from '@uniswap/sdk'
export default [
new Token(ChainId.MAINNET, '0xB6eD7644C69416d67B522e20bC294A9a9B405B31', 8, '0xBTC', '0xBitcoin Token'),
new Token(ChainId.MAINNET, '0xfC1E690f61EFd961294b3e1Ce3313fBD8aa4f85d', 18, 'aDAI', 'Aave Interest bearing DAI'),
new Token(ChainId.MAINNET, '0x737F98AC8cA59f2C68aD658E3C3d8C8963E40a4c', 18, 'AMN', 'Amon'),
new Token(ChainId.MAINNET, '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 9, 'AMPL', 'Ampleforth'),
new Token(ChainId.MAINNET, '0xcD62b1C403fa761BAadFC74C525ce2B51780b184', 18, 'ANJ', 'Aragon Network Juror'),
new Token(ChainId.MAINNET, '0x960b236A07cf122663c4303350609A66A7B288C0', 18, 'ANT', 'Aragon Network Token'),
new Token(ChainId.MAINNET, '0x27054b13b1B798B345b591a4d22e6562d47eA75a', 4, 'AST', 'AirSwap Token'),
new Token(ChainId.MAINNET, '0xBA11D00c5f74255f56a5E366F4F77f5A186d7f55', 18, 'BAND', 'BandToken'),
new Token(ChainId.MAINNET, '0x0D8775F648430679A709E98d2b0Cb6250d2887EF', 18, 'BAT', 'Basic Attention Token'),
new Token(ChainId.MAINNET, '0x107c4504cd79C5d2696Ea0030a8dD4e92601B82e', 18, 'BLT', 'Bloom Token'),
new Token(ChainId.MAINNET, '0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C', 18, 'BNT', 'Bancor Network Token'),
new Token(ChainId.MAINNET, '0x0327112423F3A68efdF1fcF402F6c5CB9f7C33fd', 18, 'BTC++', 'PieDAO BTC++'),
new Token(ChainId.MAINNET, '0x4F9254C83EB525f9FCf346490bbb3ed28a81C667', 18, 'CELR', 'CelerToken'),
new Token(ChainId.MAINNET, '0xF5DCe57282A584D2746FaF1593d3121Fcac444dC', 8, 'cSAI', 'Compound Dai'),
new Token(ChainId.MAINNET, '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643', 8, 'cDAI', 'Compound Dai'),
new Token(ChainId.MAINNET, '0x39AA39c021dfbaE8faC545936693aC917d5E7563', 8, 'cUSDC', 'Compound USD Coin'),
new Token(ChainId.MAINNET, '0xaaAEBE6Fe48E54f431b0C390CfaF0b017d09D42d', 4, 'CEL', 'Celsius'),
new Token(ChainId.MAINNET, '0x06AF07097C9Eeb7fD685c692751D5C66dB49c215', 18, 'CHAI', 'Chai'),
new Token(ChainId.MAINNET, '0xc00e94Cb662C3520282E6f5717214004A7f26888', 18, 'COMP', 'Compound'),
new Token(ChainId.MAINNET, '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359', 18, 'SAI', 'Dai Stablecoin v1.0 (SAI)'),
new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'),
new Token(ChainId.MAINNET, '0x0Cf0Ee63788A0849fE5297F3407f701E122cC023', 18, 'DATA', 'Streamr DATAcoin'),
new Token(ChainId.MAINNET, '0xE0B7927c4aF23765Cb51314A0E0521A9645F0E2A', 9, 'DGD', 'DigixDAO'),
new Token(ChainId.MAINNET, '0x4f3AfEC4E5a3F2A6a1A411DEF7D7dFe50eE057bF', 9, 'DGX', 'Digix Gold Token'),
new Token(
ChainId.MAINNET,
'0xc719d010B63E5bbF2C0551872CD5316ED26AcD83',
18,
'DIP',
'Decentralized Insurance Protocol'
),
new Token(ChainId.MAINNET, '0xC0F9bD5Fa5698B6505F643900FFA515Ea5dF54A9', 18, 'DONUT', 'Donut'),
new Token(ChainId.MAINNET, '0x86FADb80d8D2cff3C3680819E4da99C10232Ba0F', 18, 'EBASE', 'EURBASE Stablecoin'),
new Token(ChainId.MAINNET, '0xF629cBd94d3791C9250152BD8dfBDF380E2a3B9c', 18, 'ENJ', 'Enjin Coin'),
new Token(ChainId.MAINNET, '0x06f65b8CfCb13a9FE37d836fE9708dA38Ecb29B2', 18, 'FAME', 'SAINT FAME: Genesis Shirt'),
new Token(ChainId.MAINNET, '0x4946Fcea7C692606e8908002e55A582af44AC121', 18, 'FOAM', 'FOAM Token'),
new Token(ChainId.MAINNET, '0x419D0d8BdD9aF5e606Ae2232ed285Aff190E711b', 8, 'FUN', 'FunFair'),
new Token(ChainId.MAINNET, '0x4a57E687b9126435a9B19E4A802113e266AdeBde', 18, 'FXC', 'Flexacoin'),
new Token(ChainId.MAINNET, '0x543Ff227F64Aa17eA132Bf9886cAb5DB55DCAddf', 18, 'GEN', 'DAOstack'),
new Token(ChainId.MAINNET, '0x6810e776880C02933D47DB1b9fc05908e5386b96', 18, 'GNO', 'Gnosis Token'),
new Token(ChainId.MAINNET, '0x12B19D3e2ccc14Da04FAe33e63652ce469b3F2FD', 12, 'GRID', 'GRID Token'),
new Token(ChainId.MAINNET, '0x0000000000b3F879cb30FE243b4Dfee438691c04', 2, 'GST2', 'Gastoken.io'),
new Token(ChainId.MAINNET, '0xF1290473E210b2108A85237fbCd7b6eb42Cc654F', 18, 'HEDG', 'HedgeTrade'),
new Token(ChainId.MAINNET, '0x6c6EE5e31d828De241282B9606C8e98Ea48526E2', 18, 'HOT', 'HoloToken'),
new Token(ChainId.MAINNET, '0x493C57C4763932315A328269E1ADaD09653B9081', 18, 'iDAI', 'Fulcrum DAI iToken'),
new Token(ChainId.MAINNET, '0x14094949152EDDBFcd073717200DA82fEd8dC960', 18, 'iSAI', 'Fulcrum SAI iToken '),
new Token(ChainId.MAINNET, '0x6fB3e0A217407EFFf7Ca062D46c26E5d60a14d69', 18, 'IOTX', 'IoTeX Network'),
new Token(ChainId.MAINNET, '0x4Cd988AfBad37289BAAf53C13e98E2BD46aAEa8c', 18, 'KEY', 'KEY'),
new Token(ChainId.MAINNET, '0xdd974D5C2e2928deA5F71b9825b8b646686BD200', 18, 'KNC', 'Kyber Network Crystal'),
new Token(ChainId.MAINNET, '0x514910771AF9Ca656af840dff83E8264EcF986CA', 18, 'LINK', 'ChainLink Token'),
new Token(ChainId.MAINNET, '0xBBbbCA6A901c926F240b89EacB641d8Aec7AEafD', 18, 'LRC', 'LoopringCoin V2'),
new Token(ChainId.MAINNET, '0x80fB784B7eD66730e8b1DBd9820aFD29931aab03', 18, 'LEND', 'EthLend Token'),
new Token(ChainId.MAINNET, '0xA4e8C3Ec456107eA67d3075bF9e3DF3A75823DB0', 18, 'LOOM', 'LoomToken'),
new Token(ChainId.MAINNET, '0x58b6A8A3302369DAEc383334672404Ee733aB239', 18, 'LPT', 'Livepeer Token'),
new Token(ChainId.MAINNET, '0xD29F0b5b3F50b07Fe9a9511F7d86F4f4bAc3f8c4', 18, 'LQD', 'Liquidity.Network Token'),
new Token(ChainId.MAINNET, '0x0F5D2fB29fb7d3CFeE444a200298f468908cC942', 18, 'MANA', 'Decentraland MANA'),
new Token(ChainId.MAINNET, '0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0', 18, 'MATIC', 'Matic Token'),
new Token(ChainId.MAINNET, '0x8888889213DD4dA823EbDD1e235b09590633C150', 18, 'MBC', 'Marblecoin'),
new Token(ChainId.MAINNET, '0xd15eCDCF5Ea68e3995b2D0527A0aE0a3258302F8', 18, 'MCX', 'MachiX Token'),
new Token(ChainId.MAINNET, '0xa3d58c4E56fedCae3a7c43A725aeE9A71F0ece4e', 18, 'MET', 'Metronome'),
new Token(ChainId.MAINNET, '0x80f222a749a2e18Eb7f676D371F19ad7EFEEe3b7', 18, 'MGN', 'Magnolia Token'),
new Token(ChainId.MAINNET, '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', 18, 'MKR', 'Maker'),
new Token(ChainId.MAINNET, '0xec67005c4E498Ec7f55E092bd1d35cbC47C91892', 18, 'MLN', 'Melon Token'),
new Token(ChainId.MAINNET, '0x957c30aB0426e0C93CD8241E2c60392d08c6aC8e', 0, 'MOD', 'Modum Token'),
new Token(ChainId.MAINNET, '0xe2f2a5C287993345a840Db3B0845fbC70f5935a5', 18, 'mUSD', 'mStable USD'),
new Token(ChainId.MAINNET, '0xB62132e35a6c13ee1EE0f84dC5d40bad8d815206', 18, 'NEXO', 'Nexo'),
new Token(ChainId.MAINNET, '0x1776e1F26f98b1A5dF9cD347953a26dd3Cb46671', 18, 'NMR', 'Numeraire'),
new Token(ChainId.MAINNET, '0x985dd3D42De1e256d09e1c10F112bCCB8015AD41', 18, 'OCEAN', 'OceanToken'),
new Token(ChainId.MAINNET, '0x4575f41308EC1483f3d399aa9a2826d74Da13Deb', 18, 'OXT', 'Orchid'),
new Token(ChainId.MAINNET, '0xD56daC73A4d6766464b38ec6D91eB45Ce7457c44', 18, 'PAN', 'Panvala pan'),
new Token(ChainId.MAINNET, '0x8E870D67F660D95d5be530380D0eC0bd388289E1', 18, 'PAX', 'PAX'),
new Token(ChainId.MAINNET, '0x45804880De22913dAFE09f4980848ECE6EcbAf78', 18, 'PAXG', 'Paxos Gold'),
new Token(ChainId.MAINNET, '0x93ED3FBe21207Ec2E8f2d3c3de6e058Cb73Bc04d', 18, 'PNK', 'Pinakion'),
new Token(ChainId.MAINNET, '0x6758B7d441a9739b98552B373703d8d3d14f9e62', 18, 'POA20', 'POA ERC20 on Foundation'),
new Token(ChainId.MAINNET, '0x687BfC3E73f6af55F0CccA8450114D107E781a0e', 18, 'QCH', 'QChi'),
new Token(ChainId.MAINNET, '0x4a220E6096B25EADb88358cb44068A3248254675', 18, 'QNT', 'Quant'),
new Token(ChainId.MAINNET, '0x99ea4dB9EE77ACD40B119BD1dC4E33e1C070b80d', 18, 'QSP', 'Quantstamp Token'),
new Token(ChainId.MAINNET, '0xF970b8E36e23F7fC3FD752EeA86f8Be8D83375A6', 18, 'RCN', 'Ripio Credit Network Token'),
new Token(ChainId.MAINNET, '0x255Aa6DF07540Cb5d3d297f0D0D4D84cb52bc8e6', 18, 'RDN', 'Raiden Token'),
new Token(ChainId.MAINNET, '0x408e41876cCCDC0F92210600ef50372656052a38', 18, 'REN', 'Republic Token'),
new Token(ChainId.MAINNET, '0x459086F2376525BdCebA5bDDA135e4E9d3FeF5bf', 8, 'renBCH', 'renBCH'),
new Token(ChainId.MAINNET, '0xEB4C2781e4ebA804CE9a9803C67d0893436bB27D', 8, 'renBTC', 'renBTC'),
new Token(ChainId.MAINNET, '0x1C5db575E2Ff833E46a2E9864C22F4B22E0B37C2', 8, 'renZEC', 'renZEC'),
new Token(ChainId.MAINNET, '0x1985365e9f78359a9B6AD760e32412f4a445E862', 18, 'REP', 'Reputation'),
new Token(ChainId.MAINNET, '0x9469D013805bFfB7D3DEBe5E7839237e535ec483', 18, 'RING', 'Darwinia Network Native Token'),
new Token(ChainId.MAINNET, '0x607F4C5BB672230e8672085532f7e901544a7375', 9, 'RLC', 'iEx.ec Network Token'),
new Token(ChainId.MAINNET, '0xB4EFd85c19999D84251304bDA99E90B92300Bd93', 18, 'RPL', 'Rocket Pool'),
new Token(ChainId.MAINNET, '0x4156D3342D5c385a87D264F90653733592000581', 8, 'SALT', 'Salt'),
new Token(ChainId.MAINNET, '0x7C5A0CE9267ED19B22F8cae653F198e3E8daf098', 18, 'SAN', 'SANtiment network token'),
new Token(ChainId.MAINNET, '0x5e74C9036fb86BD7eCdcb084a0673EFc32eA31cb', 18, 'sETH', 'Synth sETH'),
new Token(ChainId.MAINNET, '0x3A9FfF453d50D4Ac52A6890647b823379ba36B9E', 18, 'SHUF', 'Shuffle.Monster V3'),
new Token(ChainId.MAINNET, '0x744d70FDBE2Ba4CF95131626614a1763DF805B9E', 18, 'SNT', 'Status Network Token'),
new Token(ChainId.MAINNET, '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F', 18, 'SNX', 'Synthetix Network Token'),
new Token(ChainId.MAINNET, '0x23B608675a2B2fB1890d3ABBd85c5775c51691d5', 18, 'SOCKS', 'Unisocks Edition 0'),
new Token(ChainId.MAINNET, '0x42d6622deCe394b54999Fbd73D108123806f6a18', 18, 'SPANK', 'SPANK'),
new Token(ChainId.MAINNET, '0x0Ae055097C6d159879521C384F1D2123D1f195e6', 18, 'STAKE', 'STAKE'),
new Token(ChainId.MAINNET, '0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC', 8, 'STORJ', 'StorjToken'),
new Token(ChainId.MAINNET, '0x57Ab1ec28D129707052df4dF418D58a2D46d5f51', 18, 'sUSD', 'Synth sUSD'),
new Token(ChainId.MAINNET, '0x261EfCdD24CeA98652B9700800a13DfBca4103fF', 18, 'sXAU', 'Synth sXAU'),
new Token(ChainId.MAINNET, '0x8CE9137d39326AD0cD6491fb5CC0CbA0e089b6A9', 18, 'SXP', 'Swipe'),
new Token(ChainId.MAINNET, '0x00006100F7090010005F1bd7aE6122c3C2CF0090', 18, 'TAUD', 'TrueAUD'),
new Token(ChainId.MAINNET, '0x00000100F2A2bd000715001920eB70D229700085', 18, 'TCAD', 'TrueCAD'),
new Token(ChainId.MAINNET, '0x00000000441378008EA67F4284A57932B1c000a5', 18, 'TGBP', 'TrueGBP'),
new Token(ChainId.MAINNET, '0x0000852600CEB001E08e00bC008be620d60031F2', 18, 'THKD', 'TrueHKD'),
new Token(ChainId.MAINNET, '0xaAAf91D9b90dF800Df4F55c205fd6989c977E73a', 8, 'TKN', 'Monolith TKN'),
new Token(ChainId.MAINNET, '0x0Ba45A8b5d5575935B8158a88C631E9F9C95a2e5', 18, 'TRB', 'Tellor Tributes'),
new Token(ChainId.MAINNET, '0xCb94be6f13A1182E4A4B6140cb7bf2025d28e41B', 6, 'TRST', 'Trustcoin'),
new Token(ChainId.MAINNET, '0x2C537E5624e4af88A7ae4060C022609376C8D0EB', 6, 'TRYB', 'BiLira'),
new Token(ChainId.MAINNET, '0x0000000000085d4780B73119b644AE5ecd22b376', 18, 'TUSD', 'TrueUSD'),
new Token(ChainId.MAINNET, '0x8400D94A5cb0fa0D041a3788e395285d61c9ee5e', 8, 'UBT', 'UniBright'),
new Token(ChainId.MAINNET, '0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828', 18, 'UMA', 'UMA Voting Token v1'),
new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C'),
new Token(ChainId.MAINNET, '0xA4Bdb11dc0a2bEC88d24A3aa1E6Bb17201112eBe', 6, 'USDS', 'StableUSD'),
new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'Tether USD'),
new Token(ChainId.MAINNET, '0xeb269732ab75A6fD61Ea60b06fE994cD32a83549', 18, 'USDx', 'dForce'),
new Token(ChainId.MAINNET, '0x8f3470A7388c05eE4e7AF3d01D8C722b0FF52374', 18, 'VERI', 'Veritaseum'),
new Token(ChainId.MAINNET, '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', 8, 'WBTC', 'Wrapped BTC'),
new Token(ChainId.MAINNET, '0x09fE5f0236F0Ea5D930197DCE254d77B04128075', 18, 'WCK', 'Wrapped CryptoKitties'),
new Token(ChainId.MAINNET, '0xB4272071eCAdd69d933AdcD19cA99fe80664fc08', 18, 'XCHF', 'CryptoFranc'),
new Token(ChainId.MAINNET, '0x0f7F961648aE6Db43C75663aC7E5414Eb79b5704', 18, 'XIO', 'XIO Network'),
new Token(ChainId.MAINNET, '0xE41d2489571d322189246DaFA5ebDe1F4699F498', 18, 'ZRX', '0x Protocol Token')
]

View File

@@ -1,6 +0,0 @@
import { Token, ChainId } from '@uniswap/sdk'
export default [
new Token(ChainId.RINKEBY, '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735', 18, 'DAI', 'Dai Stablecoin'),
new Token(ChainId.RINKEBY, '0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85', 18, 'MKR', 'Maker')
]

View File

@@ -1,3 +0,0 @@
import { Token, ChainId } from '@uniswap/sdk'
export default [new Token(ChainId.ROPSTEN, '0xaD6D458402F60fD3Bd25163575031ACDce07538D', 18, 'DAI', 'Dai Stablecoin')]

View File

@@ -1,18 +1,33 @@
import { Token, TokenAmount, Pair } from '@uniswap/sdk' import { TokenAmount, Pair, Currency } from '@uniswap/sdk'
import { useMemo } from 'react' import { useMemo } from 'react'
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json' import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { Interface } from '@ethersproject/abi' import { Interface } from '@ethersproject/abi'
import { useActiveWeb3React } from '../hooks'
import { useMultipleContractSingleData } from '../state/multicall/hooks' import { useMultipleContractSingleData } from '../state/multicall/hooks'
import { wrappedCurrency } from '../utils/wrappedCurrency'
const PAIR_INTERFACE = new Interface(IUniswapV2PairABI) const PAIR_INTERFACE = new Interface(IUniswapV2PairABI)
/* export enum PairState {
* if loading, return undefined LOADING,
* if no pair created yet, return null NOT_EXISTS,
* if pair already created (even if 0 reserves), return pair EXISTS,
*/ INVALID
export function usePairs(tokens: [Token | undefined, Token | undefined][]): (undefined | Pair | null)[] { }
export function usePairs(currencies: [Currency | undefined, Currency | undefined][]): [PairState, Pair | null][] {
const { chainId } = useActiveWeb3React()
const tokens = useMemo(
() =>
currencies.map(([currencyA, currencyB]) => [
wrappedCurrency(currencyA, chainId),
wrappedCurrency(currencyB, chainId)
]),
[chainId, currencies]
)
const pairAddresses = useMemo( const pairAddresses = useMemo(
() => () =>
tokens.map(([tokenA, tokenB]) => { tokens.map(([tokenA, tokenB]) => {
@@ -29,15 +44,19 @@ export function usePairs(tokens: [Token | undefined, Token | undefined][]): (und
const tokenA = tokens[i][0] const tokenA = tokens[i][0]
const tokenB = tokens[i][1] const tokenB = tokens[i][1]
if (loading || !tokenA || !tokenB) return undefined if (loading) return [PairState.LOADING, null]
if (!reserves) return null if (!tokenA || !tokenB || tokenA.equals(tokenB)) return [PairState.INVALID, null]
if (!reserves) return [PairState.NOT_EXISTS, null]
const { reserve0, reserve1 } = reserves const { reserve0, reserve1 } = reserves
const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA] const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]
return new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString())) return [
PairState.EXISTS,
new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString()))
]
}) })
}, [results, tokens]) }, [results, tokens])
} }
export function usePair(tokenA?: Token, tokenB?: Token): undefined | Pair | null { export function usePair(tokenA?: Currency, tokenB?: Currency): [PairState, Pair | null] {
return usePairs([[tokenA, tokenB]])[0] return usePairs([[tokenA, tokenB]])[0]
} }

View File

@@ -1,4 +1,20 @@
import { JSBI, Pair, Percent, Route, Token, TokenAmount, Trade, TradeType, WETH } from '@uniswap/sdk' import { AddressZero } from '@ethersproject/constants'
import {
BigintIsh,
Currency,
CurrencyAmount,
currencyEquals,
ETHER,
JSBI,
Pair,
Percent,
Route,
Token,
TokenAmount,
Trade,
TradeType,
WETH
} from '@uniswap/sdk'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useActiveWeb3React } from '../hooks' import { useActiveWeb3React } from '../hooks'
import { useAllTokens } from '../hooks/Tokens' import { useAllTokens } from '../hooks/Tokens'
@@ -6,7 +22,6 @@ import { useV1FactoryContract } from '../hooks/useContract'
import { Version } from '../hooks/useToggledVersion' import { Version } from '../hooks/useToggledVersion'
import { NEVER_RELOAD, useSingleCallResult, useSingleContractMultipleData } from '../state/multicall/hooks' import { NEVER_RELOAD, useSingleCallResult, useSingleContractMultipleData } from '../state/multicall/hooks'
import { useETHBalances, useTokenBalance, useTokenBalances } from '../state/wallet/hooks' import { useETHBalances, useTokenBalance, useTokenBalances } from '../state/wallet/hooks'
import { AddressZero } from '@ethersproject/constants'
export function useV1ExchangeAddress(tokenAddress?: string): string | undefined { export function useV1ExchangeAddress(tokenAddress?: string): string | undefined {
const contract = useV1FactoryContract() const contract = useV1FactoryContract()
@@ -15,18 +30,25 @@ export function useV1ExchangeAddress(tokenAddress?: string): string | undefined
return useSingleCallResult(contract, 'getExchange', inputs)?.result?.[0] return useSingleCallResult(contract, 'getExchange', inputs)?.result?.[0]
} }
class MockV1Pair extends Pair {} export class MockV1Pair extends Pair {
constructor(etherAmount: BigintIsh, tokenAmount: TokenAmount) {
super(tokenAmount, new TokenAmount(WETH[tokenAmount.token.chainId], etherAmount))
}
}
function useMockV1Pair(token?: Token): MockV1Pair | undefined { function useMockV1Pair(inputCurrency?: Currency): MockV1Pair | undefined {
const isWETH: boolean = token && WETH[token.chainId] ? token.equals(WETH[token.chainId]) : false const token = inputCurrency instanceof Token ? inputCurrency : undefined
const isWETH = Boolean(token && token.equals(WETH[token.chainId]))
const v1PairAddress = useV1ExchangeAddress(isWETH ? undefined : token?.address) const v1PairAddress = useV1ExchangeAddress(isWETH ? undefined : token?.address)
const tokenBalance = useTokenBalance(v1PairAddress, token) const tokenBalance = useTokenBalance(v1PairAddress, token)
const ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress ?? ''] const ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress ?? '']
return tokenBalance && ETHBalance && token return useMemo(
? new MockV1Pair(tokenBalance, new TokenAmount(WETH[token.chainId], ETHBalance.toString())) () =>
: undefined token && tokenBalance && ETHBalance && inputCurrency ? new MockV1Pair(ETHBalance.raw, tokenBalance) : undefined,
[ETHBalance, inputCurrency, token, tokenBalance]
)
} }
// returns all v1 exchange addresses in the user's token list // returns all v1 exchange addresses in the user's token list
@@ -41,8 +63,7 @@ export function useAllTokenV1Exchanges(): { [exchangeAddress: string]: Token } {
() => () =>
data?.reduce<{ [exchangeAddress: string]: Token }>((memo, { result }, ix) => { data?.reduce<{ [exchangeAddress: string]: Token }>((memo, { result }, ix) => {
if (result?.[0] && result[0] !== AddressZero) { if (result?.[0] && result[0] !== AddressZero) {
const token = allTokens[args[ix][0]] memo[result[0]] = allTokens[args[ix][0]]
memo[result[0]] = token
} }
return memo return memo
}, {}) ?? {}, }, {}) ?? {},
@@ -56,13 +77,13 @@ export function useUserHasLiquidityInAllTokens(): boolean | undefined {
const exchanges = useAllTokenV1Exchanges() const exchanges = useAllTokenV1Exchanges()
const fakeLiquidityTokens = useMemo( const v1ExchangeLiquidityTokens = useMemo(
() => () =>
chainId ? Object.keys(exchanges).map(address => new Token(chainId, address, 18, 'UNI-V1', 'Uniswap V1')) : [], chainId ? Object.keys(exchanges).map(address => new Token(chainId, address, 18, 'UNI-V1', 'Uniswap V1')) : [],
[chainId, exchanges] [chainId, exchanges]
) )
const balances = useTokenBalances(account ?? undefined, fakeLiquidityTokens) const balances = useTokenBalances(account ?? undefined, v1ExchangeLiquidityTokens)
return useMemo( return useMemo(
() => () =>
@@ -79,39 +100,39 @@ export function useUserHasLiquidityInAllTokens(): boolean | undefined {
*/ */
export function useV1Trade( export function useV1Trade(
isExactIn?: boolean, isExactIn?: boolean,
inputToken?: Token, inputCurrency?: Currency,
outputToken?: Token, outputCurrency?: Currency,
exactAmount?: TokenAmount exactAmount?: CurrencyAmount
): Trade | undefined { ): Trade | undefined {
const { chainId } = useActiveWeb3React()
// get the mock v1 pairs // get the mock v1 pairs
const inputPair = useMockV1Pair(inputToken) const inputPair = useMockV1Pair(inputCurrency)
const outputPair = useMockV1Pair(outputToken) const outputPair = useMockV1Pair(outputCurrency)
const inputIsWETH = (inputToken && chainId && WETH[chainId] && inputToken.equals(WETH[chainId])) ?? false const inputIsETH = inputCurrency === ETHER
const outputIsWETH = (outputToken && chainId && WETH[chainId] && outputToken.equals(WETH[chainId])) ?? false const outputIsETH = outputCurrency === ETHER
// construct a direct or through ETH v1 route // construct a direct or through ETH v1 route
let pairs: Pair[] = [] let pairs: Pair[] = []
if (inputIsWETH && outputPair) { if (inputIsETH && outputPair) {
pairs = [outputPair] pairs = [outputPair]
} else if (outputIsWETH && inputPair) { } else if (outputIsETH && inputPair) {
pairs = [inputPair] pairs = [inputPair]
} }
// if neither are WETH, it's token-to-token (if they both exist) // if neither are ETH, it's token-to-token (if they both exist)
else if (inputPair && outputPair) { else if (inputPair && outputPair) {
pairs = [inputPair, outputPair] pairs = [inputPair, outputPair]
} }
const route = inputToken && pairs && pairs.length > 0 && new Route(pairs, inputToken) const route = inputCurrency && pairs && pairs.length > 0 && new Route(pairs, inputCurrency, outputCurrency)
let v1Trade: Trade | undefined let v1Trade: Trade | undefined
try { try {
v1Trade = v1Trade =
route && exactAmount route && exactAmount
? new Trade(route, exactAmount, isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT) ? new Trade(route, exactAmount, isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT)
: undefined : undefined
} catch {} } catch (error) {
console.error('Failed to create V1 trade', error)
}
return v1Trade return v1Trade
} }
@@ -125,14 +146,13 @@ export function getTradeVersion(trade?: Trade): Version | undefined {
// returns the v1 exchange against which a trade should be executed // returns the v1 exchange against which a trade should be executed
export function useV1TradeExchangeAddress(trade: Trade | undefined): string | undefined { export function useV1TradeExchangeAddress(trade: Trade | undefined): string | undefined {
const tokenAddress: string | undefined = useMemo(() => { const tokenAddress: string | undefined = useMemo(() => {
const tradeVersion = getTradeVersion(trade) if (!trade) return undefined
const isV1 = tradeVersion === Version.v1 const isV1 = getTradeVersion(trade) === Version.v1
return isV1 if (!isV1) return undefined
? trade && return trade.inputAmount instanceof TokenAmount
WETH[trade.inputAmount.token.chainId] && ? trade.inputAmount.token.address
trade.inputAmount.token.equals(WETH[trade.inputAmount.token.chainId]) : trade.outputAmount instanceof TokenAmount
? trade.outputAmount.token.address ? trade.outputAmount.token.address
: trade?.inputAmount?.token?.address
: undefined : undefined
}, [trade]) }, [trade])
return useV1ExchangeAddress(tokenAddress) return useV1ExchangeAddress(tokenAddress)
@@ -140,7 +160,8 @@ export function useV1TradeExchangeAddress(trade: Trade | undefined): string | un
const ZERO_PERCENT = new Percent('0') const ZERO_PERCENT = new Percent('0')
const ONE_HUNDRED_PERCENT = new Percent('1') const ONE_HUNDRED_PERCENT = new Percent('1')
// returns whether tradeB is better than tradeA by at least a threshold
// returns whether tradeB is better than tradeA by at least a threshold percentage amount
export function isTradeBetter( export function isTradeBetter(
tradeA: Trade | undefined, tradeA: Trade | undefined,
tradeB: Trade | undefined, tradeB: Trade | undefined,
@@ -150,8 +171,8 @@ export function isTradeBetter(
if ( if (
tradeA.tradeType !== tradeB.tradeType || tradeA.tradeType !== tradeB.tradeType ||
!tradeA.inputAmount.token.equals(tradeB.inputAmount.token) || !currencyEquals(tradeA.inputAmount.currency, tradeB.inputAmount.currency) ||
!tradeB.outputAmount.token.equals(tradeB.outputAmount.token) !currencyEquals(tradeB.outputAmount.currency, tradeB.outputAmount.currency)
) { ) {
throw new Error('Trades are not comparable') throw new Error('Trades are not comparable')
} }

View File

@@ -1,7 +1,7 @@
import { parseBytes32String } from '@ethersproject/strings' import { parseBytes32String } from '@ethersproject/strings'
import { ChainId, Token, WETH } from '@uniswap/sdk' import { Currency, ETHER, Token } from '@uniswap/sdk'
import { useMemo } from 'react' import { useMemo } from 'react'
import { ALL_TOKENS } from '../constants/tokens' import { useDefaultTokenList } from '../state/lists/hooks'
import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks' import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'
import { useUserAddedTokens } from '../state/user/hooks' import { useUserAddedTokens } from '../state/user/hooks'
import { isAddress } from '../utils' import { isAddress } from '../utils'
@@ -12,30 +12,24 @@ import { useBytes32TokenContract, useTokenContract } from './useContract'
export function useAllTokens(): { [address: string]: Token } { export function useAllTokens(): { [address: string]: Token } {
const { chainId } = useActiveWeb3React() const { chainId } = useActiveWeb3React()
const userAddedTokens = useUserAddedTokens() const userAddedTokens = useUserAddedTokens()
const allTokens = useDefaultTokenList()
return useMemo(() => { return useMemo(() => {
if (!chainId) return {} if (!chainId) return {}
const tokens = userAddedTokens return (
// reduce into all ALL_TOKENS filtered by the current chain userAddedTokens
.reduce<{ [address: string]: Token }>( // reduce into all ALL_TOKENS filtered by the current chain
(tokenMap, token) => { .reduce<{ [address: string]: Token }>(
tokenMap[token.address] = token (tokenMap, token) => {
return tokenMap tokenMap[token.address] = token
}, return tokenMap
// must make a copy because reduce modifies the map, and we do not },
// want to make a copy in every iteration // must make a copy because reduce modifies the map, and we do not
{ ...ALL_TOKENS[chainId as ChainId] } // want to make a copy in every iteration
) { ...allTokens[chainId] }
)
const weth = WETH[chainId as ChainId] )
if (weth) { }, [chainId, userAddedTokens, allTokens])
// we have to replace it as a workaround because if it is automatically
// fetched by address it will cause an invariant when used in constructing
// pairs since we replace the name and symbol with 'ETH' and 'Ether'
tokens[weth.address] = weth
}
return tokens
}, [userAddedTokens, chainId])
} }
// parse a name or symbol from a token response // parse a name or symbol from a token response
@@ -100,3 +94,9 @@ export function useToken(tokenAddress?: string): Token | undefined | null {
tokenNameBytes32.result tokenNameBytes32.result
]) ])
} }
export function useCurrency(currencyId: string | undefined): Currency | null | undefined {
const isETH = currencyId?.toUpperCase() === 'ETH'
const token = useToken(isETH ? undefined : currencyId)
return isETH ? ETHER : token
}

View File

@@ -1,16 +1,21 @@
import { useMemo } from 'react' import { Currency, CurrencyAmount, Pair, Token, Trade } from '@uniswap/sdk'
import { Token, TokenAmount, Trade, ChainId, Pair } from '@uniswap/sdk'
import flatMap from 'lodash.flatmap' import flatMap from 'lodash.flatmap'
import { useMemo } from 'react'
import { useActiveWeb3React } from './index'
import { usePairs } from '../data/Reserves'
import { BASES_TO_CHECK_TRADES_AGAINST } from '../constants' import { BASES_TO_CHECK_TRADES_AGAINST } from '../constants'
import { PairState, usePairs } from '../data/Reserves'
import { wrappedCurrency } from '../utils/wrappedCurrency'
function useAllCommonPairs(tokenA?: Token, tokenB?: Token): Pair[] { import { useActiveWeb3React } from './index'
function useAllCommonPairs(currencyA?: Currency, currencyB?: Currency): Pair[] {
const { chainId } = useActiveWeb3React() const { chainId } = useActiveWeb3React()
const bases = useMemo(() => BASES_TO_CHECK_TRADES_AGAINST[chainId as ChainId] ?? [], [chainId]) const bases: Token[] = chainId ? BASES_TO_CHECK_TRADES_AGAINST[chainId] : []
const [tokenA, tokenB] = chainId
? [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)]
: [undefined, undefined]
const allPairCombinations: [Token | undefined, Token | undefined][] = useMemo( const allPairCombinations: [Token | undefined, Token | undefined][] = useMemo(
() => [ () => [
@@ -31,13 +36,16 @@ function useAllCommonPairs(tokenA?: Token, tokenB?: Token): Pair[] {
// only pass along valid pairs, non-duplicated pairs // only pass along valid pairs, non-duplicated pairs
return useMemo( return useMemo(
() => () =>
allPairs Object.values(
// filter out invalid pairs allPairs
.filter((p): p is Pair => !!p) // filter out invalid pairs
// filter out duplicated pairs .filter((result): result is [PairState.EXISTS, Pair] => Boolean(result[0] === PairState.EXISTS && result[1]))
.filter( // filter out duplicated pairs
(p, i, pairs) => i === pairs.findIndex(pair => pair?.liquidityToken.address === p.liquidityToken.address) .reduce<{ [pairAddress: string]: Pair }>((memo, [, curr]) => {
), memo[curr.liquidityToken.address] = memo[curr.liquidityToken.address] ?? curr
return memo
}, {})
),
[allPairs] [allPairs]
) )
} }
@@ -45,33 +53,32 @@ function useAllCommonPairs(tokenA?: Token, tokenB?: Token): Pair[] {
/** /**
* Returns the best trade for the exact amount of tokens in to the given token out * Returns the best trade for the exact amount of tokens in to the given token out
*/ */
export function useTradeExactIn(amountIn?: TokenAmount, tokenOut?: Token): Trade | null { export function useTradeExactIn(currencyAmountIn?: CurrencyAmount, currencyOut?: Currency): Trade | null {
const inputToken = amountIn?.token const allowedPairs = useAllCommonPairs(currencyAmountIn?.currency, currencyOut)
const outputToken = tokenOut
const allowedPairs = useAllCommonPairs(inputToken, outputToken)
return useMemo(() => { return useMemo(() => {
if (amountIn && tokenOut && allowedPairs.length > 0) { if (currencyAmountIn && currencyOut && allowedPairs.length > 0) {
return Trade.bestTradeExactIn(allowedPairs, amountIn, tokenOut)[0] ?? null return (
Trade.bestTradeExactIn(allowedPairs, currencyAmountIn, currencyOut, { maxHops: 3, maxNumResults: 1 })[0] ?? null
)
} }
return null return null
}, [allowedPairs, amountIn, tokenOut]) }, [allowedPairs, currencyAmountIn, currencyOut])
} }
/** /**
* Returns the best trade for the token in to the exact amount of token out * Returns the best trade for the token in to the exact amount of token out
*/ */
export function useTradeExactOut(tokenIn?: Token, amountOut?: TokenAmount): Trade | null { export function useTradeExactOut(currencyIn?: Currency, currencyAmountOut?: CurrencyAmount): Trade | null {
const inputToken = tokenIn const allowedPairs = useAllCommonPairs(currencyIn, currencyAmountOut?.currency)
const outputToken = amountOut?.token
const allowedPairs = useAllCommonPairs(inputToken, outputToken)
return useMemo(() => { return useMemo(() => {
if (tokenIn && amountOut && allowedPairs.length > 0) { if (currencyIn && currencyAmountOut && allowedPairs.length > 0) {
return Trade.bestTradeExactOut(allowedPairs, tokenIn, amountOut)[0] ?? null return (
Trade.bestTradeExactOut(allowedPairs, currencyIn, currencyAmountOut, { maxHops: 3, maxNumResults: 1 })[0] ??
null
)
} }
return null return null
}, [allowedPairs, tokenIn, amountOut]) }, [allowedPairs, currencyIn, currencyAmountOut])
} }

View File

@@ -72,21 +72,12 @@ export function useInactiveListener(suppress = false) {
} }
} }
const handleNetworkChanged = () => {
// eat errors
activate(injected, undefined, true).catch(error => {
console.error('Failed to activate after networks changed', error)
})
}
ethereum.on('chainChanged', handleChainChanged) ethereum.on('chainChanged', handleChainChanged)
ethereum.on('networkChanged', handleNetworkChanged)
ethereum.on('accountsChanged', handleAccountsChanged) ethereum.on('accountsChanged', handleAccountsChanged)
return () => { return () => {
if (ethereum.removeListener) { if (ethereum.removeListener) {
ethereum.removeListener('chainChanged', handleChainChanged) ethereum.removeListener('chainChanged', handleChainChanged)
ethereum.removeListener('networkChanged', handleNetworkChanged)
ethereum.removeListener('accountsChanged', handleAccountsChanged) ethereum.removeListener('accountsChanged', handleAccountsChanged)
} }
} }

View File

@@ -1,6 +1,6 @@
import { MaxUint256 } from '@ethersproject/constants' import { MaxUint256 } from '@ethersproject/constants'
import { TransactionResponse } from '@ethersproject/providers' import { TransactionResponse } from '@ethersproject/providers'
import { Trade, WETH, TokenAmount } from '@uniswap/sdk' import { Trade, TokenAmount, CurrencyAmount, ETHER } from '@uniswap/sdk'
import { useCallback, useMemo } from 'react' import { useCallback, useMemo } from 'react'
import { ROUTER_ADDRESS } from '../constants' import { ROUTER_ADDRESS } from '../constants'
import { useTokenAllowance } from '../data/Allowances' import { useTokenAllowance } from '../data/Allowances'
@@ -22,27 +22,30 @@ export enum ApprovalState {
// returns a variable indicating the state of the approval and a function which approves if necessary or early returns // returns a variable indicating the state of the approval and a function which approves if necessary or early returns
export function useApproveCallback( export function useApproveCallback(
amountToApprove?: TokenAmount, amountToApprove?: CurrencyAmount,
spender?: string spender?: string
): [ApprovalState, () => Promise<void>] { ): [ApprovalState, () => Promise<void>] {
const { account } = useActiveWeb3React() const { account } = useActiveWeb3React()
const token = amountToApprove instanceof TokenAmount ? amountToApprove.token : undefined
const currentAllowance = useTokenAllowance(amountToApprove?.token, account ?? undefined, spender) const currentAllowance = useTokenAllowance(token, account ?? undefined, spender)
const pendingApproval = useHasPendingApproval(amountToApprove?.token?.address, spender) const pendingApproval = useHasPendingApproval(token?.address, spender)
// check the current approval status // check the current approval status
const approvalState: ApprovalState = useMemo(() => { const approvalState: ApprovalState = useMemo(() => {
if (!amountToApprove || !spender) return ApprovalState.UNKNOWN if (!amountToApprove || !spender) return ApprovalState.UNKNOWN
// we treat WETH as ETH which requires no approvals if (amountToApprove.currency === ETHER) return ApprovalState.APPROVED
if (amountToApprove.token.equals(WETH[amountToApprove.token.chainId])) return ApprovalState.APPROVED
// we might not have enough data to know whether or not we need to approve // we might not have enough data to know whether or not we need to approve
if (!currentAllowance) return ApprovalState.UNKNOWN if (!currentAllowance) return ApprovalState.UNKNOWN
if (pendingApproval) return ApprovalState.PENDING
// amountToApprove will be defined if currentAllowance is // amountToApprove will be defined if currentAllowance is
return currentAllowance.lessThan(amountToApprove) ? ApprovalState.NOT_APPROVED : ApprovalState.APPROVED return currentAllowance.lessThan(amountToApprove)
? pendingApproval
? ApprovalState.PENDING
: ApprovalState.NOT_APPROVED
: ApprovalState.APPROVED
}, [amountToApprove, currentAllowance, pendingApproval, spender]) }, [amountToApprove, currentAllowance, pendingApproval, spender])
const tokenContract = useTokenContract(amountToApprove?.token?.address) const tokenContract = useTokenContract(token?.address)
const addTransaction = useTransactionAdder() const addTransaction = useTransactionAdder()
const approve = useCallback(async (): Promise<void> => { const approve = useCallback(async (): Promise<void> => {
@@ -50,6 +53,10 @@ export function useApproveCallback(
console.error('approve was called unnecessarily') console.error('approve was called unnecessarily')
return return
} }
if (!token) {
console.error('no token')
return
}
if (!tokenContract) { if (!tokenContract) {
console.error('tokenContract is null') console.error('tokenContract is null')
@@ -79,15 +86,15 @@ export function useApproveCallback(
}) })
.then((response: TransactionResponse) => { .then((response: TransactionResponse) => {
addTransaction(response, { addTransaction(response, {
summary: 'Approve ' + amountToApprove.token.symbol, summary: 'Approve ' + amountToApprove.currency.symbol,
approval: { tokenAddress: amountToApprove.token.address, spender: spender } approval: { tokenAddress: token.address, spender: spender }
}) })
}) })
.catch((error: Error) => { .catch((error: Error) => {
console.debug('Failed to approve token', error) console.debug('Failed to approve token', error)
throw error throw error
}) })
}, [approvalState, tokenContract, spender, amountToApprove, addTransaction]) }, [approvalState, token, tokenContract, amountToApprove, spender, addTransaction])
return [approvalState, approve] return [approvalState, approve]
} }

View File

@@ -1,25 +0,0 @@
import { useCallback, useEffect } from 'react'
// modified from https://usehooks.com/useKeyPress/
export default function useBodyKeyDown(targetKey: string, onKeyDown: () => void, suppressOnKeyDown = false) {
const downHandler = useCallback(
event => {
const {
target: { tagName },
key
} = event
if (key === targetKey && tagName === 'BODY' && !suppressOnKeyDown) {
event.preventDefault()
onKeyDown()
}
},
[targetKey, onKeyDown, suppressOnKeyDown]
)
useEffect(() => {
window.addEventListener('keydown', downHandler)
return () => {
window.removeEventListener('keydown', downHandler)
}
}, [downHandler])
}

View File

@@ -1,12 +1,14 @@
import { Contract } from '@ethersproject/contracts' import { Contract } from '@ethersproject/contracts'
import { ChainId } from '@uniswap/sdk' import { ChainId, WETH } from '@uniswap/sdk'
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json' import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { useMemo } from 'react' import { useMemo } from 'react'
import { ERC20_BYTES32_ABI } from '../constants/abis/erc20' import { ERC20_BYTES32_ABI } from '../constants/abis/erc20'
import UNISOCKS_ABI from '../constants/abis/unisocks.json'
import ERC20_ABI from '../constants/abis/erc20.json' import ERC20_ABI from '../constants/abis/erc20.json'
import WETH_ABI from '../constants/abis/weth.json'
import { MIGRATOR_ABI, MIGRATOR_ADDRESS } from '../constants/abis/migrator' import { MIGRATOR_ABI, MIGRATOR_ADDRESS } from '../constants/abis/migrator'
import { V1_EXCHANGE_ABI, V1_FACTORY_ABI, V1_FACTORY_ADDRESSES } from '../constants/v1'
import { MULTICALL_ABI, MULTICALL_NETWORKS } from '../constants/multicall' import { MULTICALL_ABI, MULTICALL_NETWORKS } from '../constants/multicall'
import { V1_EXCHANGE_ABI, V1_FACTORY_ABI, V1_FACTORY_ADDRESSES } from '../constants/v1'
import { getContract } from '../utils' import { getContract } from '../utils'
import { useActiveWeb3React } from './index' import { useActiveWeb3React } from './index'
@@ -27,7 +29,7 @@ function useContract(address?: string, ABI?: any, withSignerIfPossible = true):
export function useV1FactoryContract(): Contract | null { export function useV1FactoryContract(): Contract | null {
const { chainId } = useActiveWeb3React() const { chainId } = useActiveWeb3React()
return useContract(V1_FACTORY_ADDRESSES[chainId as ChainId], V1_FACTORY_ABI, false) return useContract(chainId && V1_FACTORY_ADDRESSES[chainId], V1_FACTORY_ABI, false)
} }
export function useV2MigratorContract(): Contract | null { export function useV2MigratorContract(): Contract | null {
@@ -42,6 +44,11 @@ export function useTokenContract(tokenAddress?: string, withSignerIfPossible?: b
return useContract(tokenAddress, ERC20_ABI, withSignerIfPossible) return useContract(tokenAddress, ERC20_ABI, withSignerIfPossible)
} }
export function useWETHContract(withSignerIfPossible?: boolean): Contract | null {
const { chainId } = useActiveWeb3React()
return useContract(chainId ? WETH[chainId].address : undefined, WETH_ABI, withSignerIfPossible)
}
export function useBytes32TokenContract(tokenAddress?: string, withSignerIfPossible?: boolean): Contract | null { export function useBytes32TokenContract(tokenAddress?: string, withSignerIfPossible?: boolean): Contract | null {
return useContract(tokenAddress, ERC20_BYTES32_ABI, withSignerIfPossible) return useContract(tokenAddress, ERC20_BYTES32_ABI, withSignerIfPossible)
} }
@@ -52,5 +59,14 @@ export function usePairContract(pairAddress?: string, withSignerIfPossible?: boo
export function useMulticallContract(): Contract | null { export function useMulticallContract(): Contract | null {
const { chainId } = useActiveWeb3React() const { chainId } = useActiveWeb3React()
return useContract(MULTICALL_NETWORKS[chainId as ChainId], MULTICALL_ABI, false) return useContract(chainId && MULTICALL_NETWORKS[chainId], MULTICALL_ABI, false)
}
export function useSocksController(): Contract | null {
const { chainId } = useActiveWeb3React()
return useContract(
chainId === ChainId.MAINNET ? '0x65770b5283117639760beA3F867b69b3697a91dd' : undefined,
UNISOCKS_ABI,
false
)
} }

21
src/hooks/useENS.ts Normal file
View File

@@ -0,0 +1,21 @@
import { isAddress } from '../utils'
import useENSAddress from './useENSAddress'
import useENSName from './useENSName'
/**
* Given a name or address, does a lookup to resolve to an address and name
* @param nameOrAddress ENS name or address
*/
export default function useENS(
nameOrAddress?: string | null
): { loading: boolean; address: string | null; name: string | null } {
const validated = isAddress(nameOrAddress)
const reverseLookup = useENSName(validated ? validated : undefined)
const lookup = useENSAddress(nameOrAddress)
return {
loading: reverseLookup.loading || lookup.loading,
address: validated ? validated : lookup.address,
name: reverseLookup.ENSName ? reverseLookup.ENSName : !validated && lookup.address ? nameOrAddress || null : null
}
}

View File

@@ -0,0 +1,46 @@
import { useEffect, useState } from 'react'
import { useActiveWeb3React } from './index'
/**
* Does a lookup for an ENS name to find its address.
*/
export default function useENSAddress(ensName?: string | null): { loading: boolean; address: string | null } {
const { library } = useActiveWeb3React()
const [address, setAddress] = useState<{ loading: boolean; address: string | null }>({
loading: false,
address: null
})
useEffect(() => {
if (!library || typeof ensName !== 'string') {
setAddress({ loading: false, address: null })
return
} else {
let stale = false
setAddress({ loading: true, address: null })
library
.resolveName(ensName)
.then(address => {
if (!stale) {
if (address) {
setAddress({ loading: false, address })
} else {
setAddress({ loading: false, address: null })
}
}
})
.catch(() => {
if (!stale) {
setAddress({ loading: false, address: null })
}
})
return () => {
stale = true
}
}
}, [library, ensName])
return address
}

View File

@@ -6,39 +6,43 @@ import { useActiveWeb3React } from './index'
* Does a reverse lookup for an address to find its ENS name. * Does a reverse lookup for an address to find its ENS name.
* Note this is not the same as looking up an ENS name to find an address. * Note this is not the same as looking up an ENS name to find an address.
*/ */
export default function useENSName(address?: string): string | null { export default function useENSName(address?: string): { ENSName: string | null; loading: boolean } {
const { library } = useActiveWeb3React() const { library } = useActiveWeb3React()
const [ENSName, setENSName] = useState<string | null>(null) const [ENSName, setENSName] = useState<{ ENSName: string | null; loading: boolean }>({
loading: false,
ENSName: null
})
useEffect(() => { useEffect(() => {
if (!library || !address) return
const validated = isAddress(address) const validated = isAddress(address)
if (validated) { if (!library || !validated) {
setENSName({ loading: false, ENSName: null })
return
} else {
let stale = false let stale = false
setENSName({ loading: true, ENSName: null })
library library
.lookupAddress(validated) .lookupAddress(validated)
.then(name => { .then(name => {
if (!stale) { if (!stale) {
if (name) { if (name) {
setENSName(name) setENSName({ loading: false, ENSName: name })
} else { } else {
setENSName(null) setENSName({ loading: false, ENSName: null })
} }
} }
}) })
.catch(() => { .catch(() => {
if (!stale) { if (!stale) {
setENSName(null) setENSName({ loading: false, ENSName: null })
} }
}) })
return () => { return () => {
stale = true stale = true
setENSName(null)
} }
} }
return
}, [library, address]) }, [library, address])
return ENSName return ENSName

View File

@@ -1,6 +1,6 @@
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
export default function useInterval(callback: () => void, delay: null | number) { export default function useInterval(callback: () => void, delay: null | number, leading = true) {
const savedCallback = useRef<() => void>() const savedCallback = useRef<() => void>()
// Remember the latest callback. // Remember the latest callback.
@@ -16,10 +16,10 @@ export default function useInterval(callback: () => void, delay: null | number)
} }
if (delay !== null) { if (delay !== null) {
tick() if (leading) tick()
const id = setInterval(tick, delay) const id = setInterval(tick, delay)
return () => clearInterval(id) return () => clearInterval(id)
} }
return return
}, [delay]) }, [delay, leading])
} }

View File

@@ -1,15 +1,23 @@
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
const VISIBILITY_STATE_SUPPORTED = 'visibilityState' in document
function isWindowVisible() {
return !VISIBILITY_STATE_SUPPORTED || document.visibilityState !== 'hidden'
}
/** /**
* Returns whether the window is currently visible to the user. * Returns whether the window is currently visible to the user.
*/ */
export default function useIsWindowVisible(): boolean { export default function useIsWindowVisible(): boolean {
const [focused, setFocused] = useState<boolean>(true) const [focused, setFocused] = useState<boolean>(isWindowVisible())
const listener = useCallback(() => { const listener = useCallback(() => {
setFocused(document.visibilityState !== 'hidden') setFocused(isWindowVisible())
}, [setFocused]) }, [setFocused])
useEffect(() => { useEffect(() => {
if (!VISIBILITY_STATE_SUPPORTED) return
document.addEventListener('visibilitychange', listener) document.addEventListener('visibilitychange', listener)
return () => { return () => {
document.removeEventListener('visibilitychange', listener) document.removeEventListener('visibilitychange', listener)

View File

@@ -1,70 +0,0 @@
import { BigNumber } from '@ethersproject/bignumber'
import { TransactionResponse } from '@ethersproject/providers'
import { WETH, TokenAmount, JSBI, ChainId } from '@uniswap/sdk'
import { useMemo } from 'react'
import { useTransactionAdder } from '../state/transactions/hooks'
import { useTokenBalanceTreatingWETHasETH } from '../state/wallet/hooks'
import { calculateGasMargin, getSigner, isAddress } from '../utils'
import { useTokenContract } from './useContract'
import { useActiveWeb3React } from './index'
import useENSName from './useENSName'
// returns a callback for sending a token amount, treating WETH as ETH
// returns null with invalid arguments
export function useSendCallback(amount?: TokenAmount, recipient?: string): null | (() => Promise<string>) {
const { library, account, chainId } = useActiveWeb3React()
const addTransaction = useTransactionAdder()
const ensName = useENSName(recipient)
const tokenContract = useTokenContract(amount?.token?.address)
const balance = useTokenBalanceTreatingWETHasETH(account ?? undefined, amount?.token)
return useMemo(() => {
if (!amount) return null
if (!amount.greaterThan(JSBI.BigInt(0))) return null
if (!isAddress(recipient)) return null
if (!balance) return null
if (balance.lessThan(amount)) return null
const token = amount?.token
return async function onSend(): Promise<string> {
if (!chainId || !library || !account || !tokenContract) {
throw new Error('missing dependencies in onSend callback')
}
if (token.equals(WETH[chainId as ChainId])) {
return getSigner(library, account)
.sendTransaction({ to: recipient, value: BigNumber.from(amount.raw.toString()) })
.then((response: TransactionResponse) => {
addTransaction(response, {
summary: 'Send ' + amount.toSignificant(3) + ' ' + token?.symbol + ' to ' + (ensName ?? recipient)
})
return response.hash
})
.catch((error: Error) => {
console.error('Failed to transfer ETH', error)
throw error
})
} else {
return tokenContract.estimateGas
.transfer(recipient, amount.raw.toString())
.then(estimatedGasLimit =>
tokenContract
.transfer(recipient, amount.raw.toString(), {
gasLimit: calculateGasMargin(estimatedGasLimit)
})
.then((response: TransactionResponse) => {
addTransaction(response, {
summary: 'Send ' + amount.toSignificant(3) + ' ' + token.symbol + ' to ' + (ensName ?? recipient)
})
return response.hash
})
)
.catch(error => {
console.error('Failed token transfer', error)
throw error
})
}
}
}, [addTransaction, library, account, chainId, amount, ensName, recipient, tokenContract, balance])
}

View File

@@ -0,0 +1,19 @@
import { JSBI } from '@uniswap/sdk'
import { useMemo } from 'react'
import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'
import { useActiveWeb3React } from './index'
import { useSocksController } from './useContract'
export default function useSocksBalance(): JSBI | undefined {
const { account } = useActiveWeb3React()
const socksContract = useSocksController()
const { result } = useSingleCallResult(socksContract, 'balanceOf', [account ?? undefined], NEVER_RELOAD)
const data = result?.[0]
return data ? JSBI.BigInt(data.toString()) : undefined
}
export function useHasSocks(): boolean | undefined {
const balance = useSocksBalance()
return useMemo(() => balance && JSBI.greaterThan(balance, JSBI.BigInt(0)), [balance])
}

View File

@@ -1,229 +1,86 @@
import { BigNumber } from '@ethersproject/bignumber' import { BigNumber } from '@ethersproject/bignumber'
import { MaxUint256 } from '@ethersproject/constants'
import { Contract } from '@ethersproject/contracts' import { Contract } from '@ethersproject/contracts'
import { ChainId, Trade, TradeType, WETH } from '@uniswap/sdk' import { JSBI, Percent, Router, Trade, TradeType } from '@uniswap/sdk'
import { useMemo } from 'react' import { useMemo } from 'react'
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, ROUTER_ADDRESS } from '../constants' import { BIPS_BASE, DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../constants'
import { useTokenAllowance } from '../data/Allowances'
import { getTradeVersion, useV1TradeExchangeAddress } from '../data/V1' import { getTradeVersion, useV1TradeExchangeAddress } from '../data/V1'
import { Field } from '../state/swap/actions'
import { useTransactionAdder } from '../state/transactions/hooks' import { useTransactionAdder } from '../state/transactions/hooks'
import { calculateGasMargin, getRouterContract, isAddress } from '../utils' import { calculateGasMargin, getRouterContract, isAddress, shortenAddress } from '../utils'
import { computeSlippageAdjustedAmounts } from '../utils/prices' import v1SwapArguments from '../utils/v1SwapArguments'
import { useActiveWeb3React } from './index' import { useActiveWeb3React } from './index'
import { useV1ExchangeContract } from './useContract' import { useV1ExchangeContract } from './useContract'
import useENSName from './useENSName' import useENS from './useENS'
import { Version } from './useToggledVersion' import { Version } from './useToggledVersion'
enum SwapType { function isZero(hexNumber: string) {
EXACT_TOKENS_FOR_TOKENS, return /^0x0*$/.test(hexNumber)
EXACT_TOKENS_FOR_ETH,
EXACT_ETH_FOR_TOKENS,
TOKENS_FOR_EXACT_TOKENS,
TOKENS_FOR_EXACT_ETH,
ETH_FOR_EXACT_TOKENS,
V1_EXACT_ETH_FOR_TOKENS,
V1_EXACT_TOKENS_FOR_ETH,
V1_EXACT_TOKENS_FOR_TOKENS,
V1_ETH_FOR_EXACT_TOKENS,
V1_TOKENS_FOR_EXACT_ETH,
V1_TOKENS_FOR_EXACT_TOKENS
}
function getSwapType(trade: Trade | undefined): SwapType | undefined {
if (!trade) return undefined
const chainId = trade.inputAmount.token.chainId
const inputWETH = trade.inputAmount.token.equals(WETH[chainId])
const outputWETH = trade.outputAmount.token.equals(WETH[chainId])
const isExactIn = trade.tradeType === TradeType.EXACT_INPUT
const isV1 = getTradeVersion(trade) === Version.v1
if (isExactIn) {
if (inputWETH) {
return isV1 ? SwapType.V1_EXACT_ETH_FOR_TOKENS : SwapType.EXACT_ETH_FOR_TOKENS
} else if (outputWETH) {
return isV1 ? SwapType.V1_EXACT_TOKENS_FOR_ETH : SwapType.EXACT_TOKENS_FOR_ETH
} else {
return isV1 ? SwapType.V1_EXACT_TOKENS_FOR_TOKENS : SwapType.EXACT_TOKENS_FOR_TOKENS
}
} else {
if (inputWETH) {
return isV1 ? SwapType.V1_ETH_FOR_EXACT_TOKENS : SwapType.ETH_FOR_EXACT_TOKENS
} else if (outputWETH) {
return isV1 ? SwapType.V1_TOKENS_FOR_EXACT_ETH : SwapType.TOKENS_FOR_EXACT_ETH
} else {
return isV1 ? SwapType.V1_TOKENS_FOR_EXACT_TOKENS : SwapType.TOKENS_FOR_EXACT_TOKENS
}
}
} }
// returns a function that will execute a swap, if the parameters are all valid // returns a function that will execute a swap, if the parameters are all valid
// and the user has approved the slippage adjusted input amount for the trade // and the user has approved the slippage adjusted input amount for the trade
export function useSwapCallback( export function useSwapCallback(
trade?: Trade, // trade to execute, required trade: Trade | undefined, // trade to execute, required
allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // in bips allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // in bips
deadline: number = DEFAULT_DEADLINE_FROM_NOW, // in seconds from now deadline: number = DEFAULT_DEADLINE_FROM_NOW, // in seconds from now
to?: string // recipient of output, optional recipientAddressOrName: string | null // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender
): null | (() => Promise<string>) { ): null | (() => Promise<string>) {
const { account, chainId, library } = useActiveWeb3React() const { account, chainId, library } = useActiveWeb3React()
const addTransaction = useTransactionAdder() const addTransaction = useTransactionAdder()
const recipient = to ? isAddress(to) : account
const ensName = useENSName(to) const { address: recipientAddress } = useENS(recipientAddressOrName)
const recipient = recipientAddressOrName === null ? account : recipientAddress
const tradeVersion = getTradeVersion(trade) const tradeVersion = getTradeVersion(trade)
const v1Exchange = useV1ExchangeContract(useV1TradeExchangeAddress(trade), true) const v1Exchange = useV1ExchangeContract(useV1TradeExchangeAddress(trade), true)
const inputAllowance = useTokenAllowance(
trade?.inputAmount?.token,
account ?? undefined,
tradeVersion === Version.v1 ? v1Exchange?.address : ROUTER_ADDRESS
)
return useMemo(() => { return useMemo(() => {
if (!trade || !recipient || !tradeVersion) return null if (!trade || !recipient || !library || !account || !tradeVersion || !chainId) return null
// will always be defined
const {
[Field.INPUT]: slippageAdjustedInput,
[Field.OUTPUT]: slippageAdjustedOutput
} = computeSlippageAdjustedAmounts(trade, allowedSlippage)
if (!slippageAdjustedInput || !slippageAdjustedOutput) return null
// no allowance
if (
!trade.inputAmount.token.equals(WETH[chainId as ChainId]) &&
(!inputAllowance || slippageAdjustedInput.greaterThan(inputAllowance))
) {
return null
}
return async function onSwap() { return async function onSwap() {
if (!chainId || !library || !account) {
throw new Error('missing dependencies in onSwap callback')
}
const contract: Contract | null = const contract: Contract | null =
tradeVersion === Version.v2 ? getRouterContract(chainId, library, account) : v1Exchange tradeVersion === Version.v2 ? getRouterContract(chainId, library, account) : v1Exchange
if (!contract) { if (!contract) {
throw new Error('Failed to get a swap contract') throw new Error('Failed to get a swap contract')
} }
const path = trade.route.path.map(t => t.address) const swapMethods = []
const deadlineFromNow: number = Math.ceil(Date.now() / 1000) + deadline switch (tradeVersion) {
case Version.v2:
swapMethods.push(
Router.swapCallParameters(trade, {
feeOnTransfer: false,
allowedSlippage: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE),
recipient,
ttl: deadline
})
)
const swapType = getSwapType(trade) if (trade.tradeType === TradeType.EXACT_INPUT) {
swapMethods.push(
// let estimate: Function, method: Function, Router.swapCallParameters(trade, {
let methodNames: string[], feeOnTransfer: true,
args: Array<string | string[] | number>, allowedSlippage: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE),
value: BigNumber | null = null recipient,
switch (swapType) { ttl: deadline
case SwapType.EXACT_TOKENS_FOR_TOKENS: })
methodNames = ['swapExactTokensForTokens', 'swapExactTokensForTokensSupportingFeeOnTransferTokens'] )
args = [ }
slippageAdjustedInput.raw.toString(),
slippageAdjustedOutput.raw.toString(),
path,
recipient,
deadlineFromNow
]
break break
case SwapType.TOKENS_FOR_EXACT_TOKENS: case Version.v1:
methodNames = ['swapTokensForExactTokens'] swapMethods.push(
args = [ v1SwapArguments(trade, {
slippageAdjustedOutput.raw.toString(), allowedSlippage: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE),
slippageAdjustedInput.raw.toString(), recipient,
path, ttl: deadline
recipient, })
deadlineFromNow )
]
break break
case SwapType.EXACT_ETH_FOR_TOKENS:
methodNames = ['swapExactETHForTokens', 'swapExactETHForTokensSupportingFeeOnTransferTokens']
args = [slippageAdjustedOutput.raw.toString(), path, recipient, deadlineFromNow]
value = BigNumber.from(slippageAdjustedInput.raw.toString())
break
case SwapType.TOKENS_FOR_EXACT_ETH:
methodNames = ['swapTokensForExactETH']
args = [
slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(),
path,
recipient,
deadlineFromNow
]
break
case SwapType.EXACT_TOKENS_FOR_ETH:
methodNames = ['swapExactTokensForETH', 'swapExactTokensForETHSupportingFeeOnTransferTokens']
args = [
slippageAdjustedInput.raw.toString(),
slippageAdjustedOutput.raw.toString(),
path,
recipient,
deadlineFromNow
]
break
case SwapType.ETH_FOR_EXACT_TOKENS:
methodNames = ['swapETHForExactTokens']
args = [slippageAdjustedOutput.raw.toString(), path, recipient, deadlineFromNow]
value = BigNumber.from(slippageAdjustedInput.raw.toString())
break
case SwapType.V1_EXACT_ETH_FOR_TOKENS:
methodNames = ['ethToTokenTransferInput']
args = [slippageAdjustedOutput.raw.toString(), deadlineFromNow, recipient]
value = BigNumber.from(slippageAdjustedInput.raw.toString())
break
case SwapType.V1_EXACT_TOKENS_FOR_TOKENS:
methodNames = ['tokenToTokenTransferInput']
args = [
slippageAdjustedInput.raw.toString(),
slippageAdjustedOutput.raw.toString(),
1,
deadlineFromNow,
recipient,
trade.outputAmount.token.address
]
break
case SwapType.V1_EXACT_TOKENS_FOR_ETH:
methodNames = ['tokenToEthTransferOutput']
args = [
slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(),
deadlineFromNow,
recipient
]
break
case SwapType.V1_ETH_FOR_EXACT_TOKENS:
methodNames = ['ethToTokenTransferOutput']
args = [slippageAdjustedOutput.raw.toString(), deadlineFromNow, recipient]
value = BigNumber.from(slippageAdjustedInput.raw.toString())
break
case SwapType.V1_TOKENS_FOR_EXACT_ETH:
methodNames = ['tokenToEthTransferOutput']
args = [
slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(),
deadlineFromNow,
recipient
]
break
case SwapType.V1_TOKENS_FOR_EXACT_TOKENS:
methodNames = ['tokenToTokenTransferOutput']
args = [
slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(),
MaxUint256.toString(),
deadlineFromNow,
recipient,
trade.outputAmount.token.address
]
break
default:
throw new Error(`Unhandled swap type: ${swapType}`)
} }
const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all( const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all(
methodNames.map(methodName => swapMethods.map(({ args, methodName, value }) =>
contract.estimateGas[methodName](...args, value ? { value } : {}) contract.estimateGas[methodName](...args, value && !isZero(value) ? { value } : {})
.then(calculateGasMargin) .then(calculateGasMargin)
.catch(error => { .catch(error => {
console.error(`estimateGas failed for ${methodName}`, error) console.error(`estimateGas failed for ${methodName}`, error)
@@ -253,7 +110,7 @@ export function useSwapCallback(
// if only 1 method exists, either: // if only 1 method exists, either:
// a) the token is doing something weird not related to FoT (e.g. enforcing a whitelist) // a) the token is doing something weird not related to FoT (e.g. enforcing a whitelist)
// b) the token is FoT and the user specified an exact output, which is not allowed // b) the token is FoT and the user specified an exact output, which is not allowed
if (methodNames.length === 1) { if (swapMethods.length === 1) {
throw Error( throw Error(
`An error occurred. If either of the tokens you're swapping take a fee on transfer, you must specify an exact input amount.` `An error occurred. If either of the tokens you're swapping take a fee on transfer, you must specify an exact input amount.`
) )
@@ -261,7 +118,7 @@ export function useSwapCallback(
// if 2 methods exists, either: // if 2 methods exists, either:
// a) the token is doing something weird not related to FoT (e.g. enforcing a whitelist) // a) the token is doing something weird not related to FoT (e.g. enforcing a whitelist)
// b) the token is FoT and is taking more than the specified slippage // b) the token is FoT and is taking more than the specified slippage
else if (methodNames.length === 2) { else if (swapMethods.length === 2) {
throw Error( throw Error(
`An error occurred. If either of the tokens you're swapping take a fee on transfer, you must specify a slippage tolerance higher than the fee.` `An error occurred. If either of the tokens you're swapping take a fee on transfer, you must specify a slippage tolerance higher than the fee.`
) )
@@ -269,24 +126,31 @@ export function useSwapCallback(
throw Error('This transaction would fail. Please contact support.') throw Error('This transaction would fail. Please contact support.')
} }
} else { } else {
const methodName = methodNames[indexOfSuccessfulEstimation] const { methodName, args, value } = swapMethods[indexOfSuccessfulEstimation]
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation] const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
return contract[methodName](...args, { return contract[methodName](...args, {
gasLimit: safeGasEstimate, gasLimit: safeGasEstimate,
...(value ? { value } : {}) ...(value && !isZero(value) ? { value } : {})
}) })
.then((response: any) => { .then((response: any) => {
const inputSymbol = trade.inputAmount.token.symbol const inputSymbol = trade.inputAmount.currency.symbol
const outputSymbol = trade.outputAmount.token.symbol const outputSymbol = trade.outputAmount.currency.symbol
const inputAmount = slippageAdjustedInput.toSignificant(3) const inputAmount = trade.inputAmount.toSignificant(3)
const outputAmount = slippageAdjustedOutput.toSignificant(3) const outputAmount = trade.outputAmount.toSignificant(3)
const base = `Swap ${inputAmount} ${inputSymbol} for ${outputAmount} ${outputSymbol}` const base = `Swap ${inputAmount} ${inputSymbol} for ${outputAmount} ${outputSymbol}`
const withRecipient = recipient === account ? base : `${base} to ${ensName ?? recipient}` const withRecipient =
recipient === account
? base
: `${base} to ${
recipientAddressOrName && isAddress(recipientAddressOrName)
? shortenAddress(recipientAddressOrName)
: recipientAddressOrName
}`
const withVersion = const withVersion =
tradeVersion === Version.v2 ? withRecipient : `${withRecipient} on ${tradeVersion.toUpperCase()}` tradeVersion === Version.v2 ? withRecipient : `${withRecipient} on ${(tradeVersion as any).toUpperCase()}`
addTransaction(response, { addTransaction(response, {
summary: withVersion summary: withVersion
@@ -310,15 +174,14 @@ export function useSwapCallback(
}, [ }, [
trade, trade,
recipient, recipient,
tradeVersion,
allowedSlippage,
chainId,
inputAllowance,
library, library,
account, account,
tradeVersion,
chainId,
allowedSlippage,
v1Exchange, v1Exchange,
deadline, deadline,
addTransaction, recipientAddressOrName,
ensName addTransaction
]) ])
} }

View File

@@ -0,0 +1,75 @@
import { Currency, currencyEquals, ETHER, WETH } from '@uniswap/sdk'
import { useMemo } from 'react'
import { tryParseAmount } from '../state/swap/hooks'
import { useTransactionAdder } from '../state/transactions/hooks'
import { useCurrencyBalance } from '../state/wallet/hooks'
import { useActiveWeb3React } from './index'
import { useWETHContract } from './useContract'
export enum WrapType {
NOT_APPLICABLE,
WRAP,
UNWRAP
}
const NOT_APPLICABLE = { wrapType: WrapType.NOT_APPLICABLE }
/**
* Given the selected input and output currency, return a wrap callback
* @param inputCurrency the selected input currency
* @param outputCurrency the selected output currency
* @param typedValue the user input value
*/
export default function useWrapCallback(
inputCurrency: Currency | undefined,
outputCurrency: Currency | undefined,
typedValue: string | undefined
): { wrapType: WrapType; execute?: undefined | (() => Promise<void>); error?: string } {
const { chainId, account } = useActiveWeb3React()
const wethContract = useWETHContract()
const balance = useCurrencyBalance(account ?? undefined, inputCurrency)
// we can always parse the amount typed as the input currency, since wrapping is 1:1
const inputAmount = useMemo(() => tryParseAmount(typedValue, inputCurrency), [inputCurrency, typedValue])
const addTransaction = useTransactionAdder()
return useMemo(() => {
if (!wethContract || !chainId || !inputCurrency || !outputCurrency) return NOT_APPLICABLE
const sufficientBalance = inputAmount && balance && !balance.lessThan(inputAmount)
if (inputCurrency === ETHER && currencyEquals(WETH[chainId], outputCurrency)) {
return {
wrapType: WrapType.WRAP,
execute:
sufficientBalance && inputAmount
? async () => {
try {
const txReceipt = await wethContract.deposit({ value: `0x${inputAmount.raw.toString(16)}` })
addTransaction(txReceipt, { summary: `Wrap ${inputAmount.toSignificant(6)} ETH to WETH` })
} catch (error) {
console.error('Could not deposit', error)
}
}
: undefined,
error: sufficientBalance ? undefined : 'Insufficient ETH balance'
}
} else if (currencyEquals(WETH[chainId], inputCurrency) && outputCurrency === ETHER) {
return {
wrapType: WrapType.UNWRAP,
execute:
sufficientBalance && inputAmount
? async () => {
try {
const txReceipt = await wethContract.withdraw(`0x${inputAmount.raw.toString(16)}`)
addTransaction(txReceipt, { summary: `Unwrap ${inputAmount.toSignificant(6)} WETH to ETH` })
} catch (error) {
console.error('Could not withdraw', error)
}
}
: undefined,
error: sufficientBalance ? undefined : 'Insufficient WETH balance'
}
} else {
return NOT_APPLICABLE
}
}, [wethContract, chainId, inputCurrency, outputCurrency, inputAmount, balance, addTransaction])
}

View File

@@ -3,15 +3,13 @@ import { initReactI18next } from 'react-i18next'
import XHR from 'i18next-xhr-backend' import XHR from 'i18next-xhr-backend'
import LanguageDetector from 'i18next-browser-languagedetector' import LanguageDetector from 'i18next-browser-languagedetector'
const LOAD_PATH: string = process.env.PUBLIC_URL === '.' ? `./locales/{{lng}}.json` : '/locales/{{lng}}.json'
i18next i18next
.use(XHR) .use(XHR)
.use(LanguageDetector) .use(LanguageDetector)
.use(initReactI18next) .use(initReactI18next)
.init({ .init({
backend: { backend: {
loadPath: LOAD_PATH loadPath: `./locales/{{lng}}.json`
}, },
react: { react: {
useSuspense: true useSuspense: true

View File

@@ -6,17 +6,23 @@ import ReactDOM from 'react-dom'
import ReactGA from 'react-ga' import ReactGA from 'react-ga'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { NetworkContextName } from './constants' import { NetworkContextName } from './constants'
import 'inter-ui'
import './i18n' import './i18n'
import App from './pages/App' import App from './pages/App'
import store from './state' import store from './state'
import ApplicationUpdater from './state/application/updater' import ApplicationUpdater from './state/application/updater'
import TransactionUpdater from './state/transactions/updater' import TransactionUpdater from './state/transactions/updater'
import ListsUpdater from './state/lists/updater'
import UserUpdater from './state/user/updater' import UserUpdater from './state/user/updater'
import MulticallUpdater from './state/multicall/updater' import MulticallUpdater from './state/multicall/updater'
import ThemeProvider, { FixedGlobalStyle, ThemedGlobalStyle } from './theme' import ThemeProvider, { FixedGlobalStyle, ThemedGlobalStyle } from './theme'
const Web3ProviderNetwork = createWeb3ReactRoot(NetworkContextName) const Web3ProviderNetwork = createWeb3ReactRoot(NetworkContextName)
if ('ethereum' in window) {
;(window.ethereum as any).autoRefreshOnNetworkChange = false
}
function getLibrary(provider: any): Web3Provider { function getLibrary(provider: any): Web3Provider {
const library = new Web3Provider(provider) const library = new Web3Provider(provider)
library.pollingInterval = 15000 library.pollingInterval = 15000
@@ -33,9 +39,17 @@ if (typeof GOOGLE_ANALYTICS_ID === 'string') {
ReactGA.initialize('test', { testMode: true, debug: true }) ReactGA.initialize('test', { testMode: true, debug: true })
} }
window.addEventListener('error', error => {
ReactGA.exception({
description: `${error.message} @ ${error.filename}:${error.lineno}:${error.colno}`,
fatal: true
})
})
function Updaters() { function Updaters() {
return ( return (
<> <>
<ListsUpdater />
<UserUpdater /> <UserUpdater />
<ApplicationUpdater /> <ApplicationUpdater />
<TransactionUpdater /> <TransactionUpdater />

View File

@@ -0,0 +1,67 @@
import { Currency, CurrencyAmount, Fraction, Percent } from '@uniswap/sdk'
import React from 'react'
import { Text } from 'rebass'
import { ButtonPrimary } from '../../components/Button'
import { RowBetween, RowFixed } from '../../components/Row'
import CurrencyLogo from '../../components/CurrencyLogo'
import { Field } from '../../state/mint/actions'
import { TYPE } from '../../theme'
export function ConfirmAddModalBottom({
noLiquidity,
price,
currencies,
parsedAmounts,
poolTokenPercentage,
onAdd
}: {
noLiquidity?: boolean
price?: Fraction
currencies: { [field in Field]?: Currency }
parsedAmounts: { [field in Field]?: CurrencyAmount }
poolTokenPercentage?: Percent
onAdd: () => void
}) {
return (
<>
<RowBetween>
<TYPE.body>{currencies[Field.CURRENCY_A]?.symbol} Deposited</TYPE.body>
<RowFixed>
<CurrencyLogo currency={currencies[Field.CURRENCY_A]} style={{ marginRight: '8px' }} />
<TYPE.body>{parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)}</TYPE.body>
</RowFixed>
</RowBetween>
<RowBetween>
<TYPE.body>{currencies[Field.CURRENCY_B]?.symbol} Deposited</TYPE.body>
<RowFixed>
<CurrencyLogo currency={currencies[Field.CURRENCY_B]} style={{ marginRight: '8px' }} />
<TYPE.body>{parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)}</TYPE.body>
</RowFixed>
</RowBetween>
<RowBetween>
<TYPE.body>Rates</TYPE.body>
<TYPE.body>
{`1 ${currencies[Field.CURRENCY_A]?.symbol} = ${price?.toSignificant(4)} ${
currencies[Field.CURRENCY_B]?.symbol
}`}
</TYPE.body>
</RowBetween>
<RowBetween style={{ justifyContent: 'flex-end' }}>
<TYPE.body>
{`1 ${currencies[Field.CURRENCY_B]?.symbol} = ${price?.invert().toSignificant(4)} ${
currencies[Field.CURRENCY_A]?.symbol
}`}
</TYPE.body>
</RowBetween>
<RowBetween>
<TYPE.body>Share of Pool:</TYPE.body>
<TYPE.body>{noLiquidity ? '100' : poolTokenPercentage?.toSignificant(4)}%</TYPE.body>
</RowBetween>
<ButtonPrimary style={{ margin: '20px 0 0 0' }} onClick={onAdd}>
<Text fontWeight={500} fontSize={20}>
{noLiquidity ? 'Create Pool & Supply' : 'Confirm Supply'}
</Text>
</ButtonPrimary>
</>
)
}

View File

@@ -0,0 +1,52 @@
import { Currency, Fraction, Percent } from '@uniswap/sdk'
import React, { useContext } from 'react'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import { AutoColumn } from '../../components/Column'
import { AutoRow } from '../../components/Row'
import { ONE_BIPS } from '../../constants'
import { Field } from '../../state/mint/actions'
import { TYPE } from '../../theme'
export const PoolPriceBar = ({
currencies,
noLiquidity,
poolTokenPercentage,
price
}: {
currencies: { [field in Field]?: Currency }
noLiquidity?: boolean
poolTokenPercentage?: Percent
price?: Fraction
}) => {
const theme = useContext(ThemeContext)
return (
<AutoColumn gap="md">
<AutoRow justify="space-around" gap="4px">
<AutoColumn justify="center">
<TYPE.black>{price?.toSignificant(6) ?? '0'}</TYPE.black>
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
{currencies[Field.CURRENCY_B]?.symbol} per {currencies[Field.CURRENCY_A]?.symbol}
</Text>
</AutoColumn>
<AutoColumn justify="center">
<TYPE.black>{price?.invert().toSignificant(6) ?? '0'}</TYPE.black>
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
{currencies[Field.CURRENCY_A]?.symbol} per {currencies[Field.CURRENCY_B]?.symbol}
</Text>
</AutoColumn>
<AutoColumn justify="center">
<TYPE.black>
{noLiquidity && price
? '100'
: (poolTokenPercentage?.lessThan(ONE_BIPS) ? '<0.01' : poolTokenPercentage?.toFixed(2)) ?? '0'}
%
</TYPE.black>
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
Share of Pool
</Text>
</AutoColumn>
</AutoRow>
</AutoColumn>
)
}

View File

@@ -1,49 +1,62 @@
import { BigNumber } from '@ethersproject/bignumber' import { BigNumber } from '@ethersproject/bignumber'
import { TokenAmount, WETH } from '@uniswap/sdk' import { TransactionResponse } from '@ethersproject/providers'
import React, { useContext, useState } from 'react' import { Currency, currencyEquals, ETHER, TokenAmount, WETH } from '@uniswap/sdk'
import React, { useCallback, useContext, useState } from 'react'
import { Plus } from 'react-feather' import { Plus } from 'react-feather'
import ReactGA from 'react-ga' import ReactGA from 'react-ga'
import { RouteComponentProps } from 'react-router-dom' import { RouteComponentProps } from 'react-router-dom'
import { Text } from 'rebass' import { Text } from 'rebass'
import { ThemeContext } from 'styled-components' import { ThemeContext } from 'styled-components'
import { ButtonLight, ButtonPrimary, ButtonError } from '../../components/Button' import { ButtonError, ButtonLight, ButtonPrimary } from '../../components/Button'
import { BlueCard, GreyCard, LightCard } from '../../components/Card' import { BlueCard, GreyCard, LightCard } from '../../components/Card'
import { AutoColumn, ColumnCenter } from '../../components/Column' import { AutoColumn, ColumnCenter } from '../../components/Column'
import ConfirmationModal from '../../components/ConfirmationModal' import ConfirmationModal from '../../components/ConfirmationModal'
import CurrencyInputPanel from '../../components/CurrencyInputPanel' import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import DoubleLogo from '../../components/DoubleLogo' import DoubleCurrencyLogo from '../../components/DoubleLogo'
import PositionCard from '../../components/PositionCard' import { AddRemoveTabs } from '../../components/NavigationTabs'
import Row, { AutoRow, RowBetween, RowFixed, RowFlat } from '../../components/Row' import { MinimalPositionCard } from '../../components/PositionCard'
import Row, { RowBetween, RowFlat } from '../../components/Row'
import TokenLogo from '../../components/TokenLogo' import { ROUTER_ADDRESS } from '../../constants'
import { PairState } from '../../data/Reserves'
import { ROUTER_ADDRESS, MIN_ETH, ONE_BIPS } from '../../constants'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { useCurrency } from '../../hooks/Tokens'
import { ApprovalState, useApproveCallback } from '../../hooks/useApproveCallback'
import { useWalletModalToggle } from '../../state/application/hooks'
import { Field } from '../../state/mint/actions'
import { useDerivedMintInfo, useMintActionHandlers, useMintState } from '../../state/mint/hooks'
import { useTransactionAdder } from '../../state/transactions/hooks' import { useTransactionAdder } from '../../state/transactions/hooks'
import { useIsExpertMode, useUserDeadline, useUserSlippageTolerance } from '../../state/user/hooks'
import { TYPE } from '../../theme' import { TYPE } from '../../theme'
import { calculateGasMargin, calculateSlippageAmount, getRouterContract } from '../../utils' import { calculateGasMargin, calculateSlippageAmount, getRouterContract } from '../../utils'
import { maxAmountSpend } from '../../utils/maxAmountSpend'
import { wrappedCurrency } from '../../utils/wrappedCurrency'
import AppBody from '../AppBody' import AppBody from '../AppBody'
import { Dots, Wrapper } from '../Pool/styleds' import { Dots, Wrapper } from '../Pool/styleds'
import { import { ConfirmAddModalBottom } from './ConfirmAddModalBottom'
useDefaultsFromURLMatchParams, import { currencyId } from '../../utils/currencyId'
useMintState, import { PoolPriceBar } from './PoolPriceBar'
useDerivedMintInfo,
useMintActionHandlers
} from '../../state/mint/hooks'
import { Field } from '../../state/mint/actions'
import { useApproveCallback, ApprovalState } from '../../hooks/useApproveCallback'
import { useWalletModalToggle } from '../../state/application/hooks'
import { useUserSlippageTolerance, useUserDeadline, useIsExpertMode } from '../../state/user/hooks'
export default function AddLiquidity({ match: { params } }: RouteComponentProps<{ tokens: string }>) {
useDefaultsFromURLMatchParams(params)
export default function AddLiquidity({
match: {
params: { currencyIdA, currencyIdB }
},
history
}: RouteComponentProps<{ currencyIdA?: string; currencyIdB?: string }>) {
const { account, chainId, library } = useActiveWeb3React() const { account, chainId, library } = useActiveWeb3React()
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
// toggle wallet when disconnected const currencyA = useCurrency(currencyIdA)
const toggleWalletModal = useWalletModalToggle() const currencyB = useCurrency(currencyIdB)
const oneCurrencyIsWETH = Boolean(
chainId &&
((currencyA && currencyEquals(currencyA, WETH[chainId])) ||
(currencyB && currencyEquals(currencyB, WETH[chainId])))
)
const toggleWalletModal = useWalletModalToggle() // toggle wallet when disconnected
const expertMode = useIsExpertMode() const expertMode = useIsExpertMode()
@@ -51,17 +64,18 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
const { independentField, typedValue, otherTypedValue } = useMintState() const { independentField, typedValue, otherTypedValue } = useMintState()
const { const {
dependentField, dependentField,
tokens, currencies,
pair, pair,
tokenBalances, pairState,
currencyBalances,
parsedAmounts, parsedAmounts,
price, price,
noLiquidity, noLiquidity,
liquidityMinted, liquidityMinted,
poolTokenPercentage, poolTokenPercentage,
error error
} = useDerivedMintInfo() } = useDerivedMintInfo(currencyA ?? undefined, currencyB ?? undefined)
const { onUserInput } = useMintActionHandlers() const { onFieldAInput, onFieldBInput } = useMintActionHandlers(noLiquidity)
const isValid = !error const isValid = !error
@@ -81,72 +95,75 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
} }
// get the max amounts user can add // get the max amounts user can add
const maxAmounts: { [field in Field]?: TokenAmount } = [Field.TOKEN_A, Field.TOKEN_B].reduce((accumulator, field) => { const maxAmounts: { [field in Field]?: TokenAmount } = [Field.CURRENCY_A, Field.CURRENCY_B].reduce(
return {
...accumulator,
[field]:
!!tokenBalances[field] &&
!!tokens[field] &&
!!WETH[chainId] &&
tokenBalances[field].greaterThan(
new TokenAmount(tokens[field], tokens[field].equals(WETH[chainId]) ? MIN_ETH : '0')
)
? tokens[field].equals(WETH[chainId])
? tokenBalances[field].subtract(new TokenAmount(WETH[chainId], MIN_ETH))
: tokenBalances[field]
: undefined
}
}, {})
const atMaxAmounts: { [field in Field]?: TokenAmount } = [Field.TOKEN_A, Field.TOKEN_B].reduce(
(accumulator, field) => { (accumulator, field) => {
return { return {
...accumulator, ...accumulator,
[field]: maxAmounts[field] && parsedAmounts[field] ? maxAmounts[field].equalTo(parsedAmounts[field]) : undefined [field]: maxAmountSpend(currencyBalances[field])
}
},
{}
)
const atMaxAmounts: { [field in Field]?: TokenAmount } = [Field.CURRENCY_A, Field.CURRENCY_B].reduce(
(accumulator, field) => {
return {
...accumulator,
[field]: maxAmounts[field]?.equalTo(parsedAmounts[field] ?? '0')
} }
}, },
{} {}
) )
// check whether the user has approved the router on the tokens // check whether the user has approved the router on the tokens
const [approvalA, approveACallback] = useApproveCallback(parsedAmounts[Field.TOKEN_A], ROUTER_ADDRESS) const [approvalA, approveACallback] = useApproveCallback(parsedAmounts[Field.CURRENCY_A], ROUTER_ADDRESS)
const [approvalB, approveBCallback] = useApproveCallback(parsedAmounts[Field.TOKEN_B], ROUTER_ADDRESS) const [approvalB, approveBCallback] = useApproveCallback(parsedAmounts[Field.CURRENCY_B], ROUTER_ADDRESS)
const addTransaction = useTransactionAdder() const addTransaction = useTransactionAdder()
async function onAdd() { async function onAdd() {
if (!chainId || !library || !account) return
const router = getRouterContract(chainId, library, account) const router = getRouterContract(chainId, library, account)
const { [Field.CURRENCY_A]: parsedAmountA, [Field.CURRENCY_B]: parsedAmountB } = parsedAmounts
if (!parsedAmountA || !parsedAmountB || !currencyA || !currencyB) {
return
}
const amountsMin = { const amountsMin = {
[Field.TOKEN_A]: calculateSlippageAmount(parsedAmounts[Field.TOKEN_A], noLiquidity ? 0 : allowedSlippage)[0], [Field.CURRENCY_A]: calculateSlippageAmount(parsedAmountA, noLiquidity ? 0 : allowedSlippage)[0],
[Field.TOKEN_B]: calculateSlippageAmount(parsedAmounts[Field.TOKEN_B], noLiquidity ? 0 : allowedSlippage)[0] [Field.CURRENCY_B]: calculateSlippageAmount(parsedAmountB, noLiquidity ? 0 : allowedSlippage)[0]
} }
const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline
let estimate, method: Function, args: Array<string | string[] | number>, value: BigNumber | null let estimate,
if (tokens[Field.TOKEN_A].equals(WETH[chainId]) || tokens[Field.TOKEN_B].equals(WETH[chainId])) { method: (...args: any) => Promise<TransactionResponse>,
const tokenBIsETH = tokens[Field.TOKEN_B].equals(WETH[chainId]) args: Array<string | string[] | number>,
value: BigNumber | null
if (currencyA === ETHER || currencyB === ETHER) {
const tokenBIsETH = currencyB === ETHER
estimate = router.estimateGas.addLiquidityETH estimate = router.estimateGas.addLiquidityETH
method = router.addLiquidityETH method = router.addLiquidityETH
args = [ args = [
tokens[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].address, // token wrappedCurrency(tokenBIsETH ? currencyA : currencyB, chainId)?.address ?? '', // token
parsedAmounts[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].raw.toString(), // token desired (tokenBIsETH ? parsedAmountA : parsedAmountB).raw.toString(), // token desired
amountsMin[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].toString(), // token min amountsMin[tokenBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(), // token min
amountsMin[tokenBIsETH ? Field.TOKEN_B : Field.TOKEN_A].toString(), // eth min amountsMin[tokenBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(), // eth min
account, account,
deadlineFromNow deadlineFromNow
] ]
value = BigNumber.from(parsedAmounts[tokenBIsETH ? Field.TOKEN_B : Field.TOKEN_A].raw.toString()) value = BigNumber.from((tokenBIsETH ? parsedAmountB : parsedAmountA).raw.toString())
} else { } else {
estimate = router.estimateGas.addLiquidity estimate = router.estimateGas.addLiquidity
method = router.addLiquidity method = router.addLiquidity
args = [ args = [
tokens[Field.TOKEN_A].address, wrappedCurrency(currencyA, chainId)?.address ?? '',
tokens[Field.TOKEN_B].address, wrappedCurrency(currencyB, chainId)?.address ?? '',
parsedAmounts[Field.TOKEN_A].raw.toString(), parsedAmountA.raw.toString(),
parsedAmounts[Field.TOKEN_B].raw.toString(), parsedAmountB.raw.toString(),
amountsMin[Field.TOKEN_A].toString(), amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.TOKEN_B].toString(), amountsMin[Field.CURRENCY_B].toString(),
account, account,
deadlineFromNow deadlineFromNow
] ]
@@ -165,13 +182,13 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
addTransaction(response, { addTransaction(response, {
summary: summary:
'Add ' + 'Add ' +
parsedAmounts[Field.TOKEN_A]?.toSignificant(3) + parsedAmounts[Field.CURRENCY_A]?.toSignificant(3) +
' ' + ' ' +
tokens[Field.TOKEN_A]?.symbol + currencies[Field.CURRENCY_A]?.symbol +
' and ' + ' and ' +
parsedAmounts[Field.TOKEN_B]?.toSignificant(3) + parsedAmounts[Field.CURRENCY_B]?.toSignificant(3) +
' ' + ' ' +
tokens[Field.TOKEN_B]?.symbol currencies[Field.CURRENCY_B]?.symbol
}) })
setTxHash(response.hash) setTxHash(response.hash)
@@ -179,7 +196,7 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
ReactGA.event({ ReactGA.event({
category: 'Liquidity', category: 'Liquidity',
action: 'Add', action: 'Add',
label: [tokens[Field.TOKEN_A]?.symbol, tokens[Field.TOKEN_B]?.symbol].join('/') label: [currencies[Field.CURRENCY_A]?.symbol, currencies[Field.CURRENCY_B]?.symbol].join('/')
}) })
}) })
) )
@@ -198,9 +215,13 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
<LightCard mt="20px" borderRadius="20px"> <LightCard mt="20px" borderRadius="20px">
<RowFlat> <RowFlat>
<Text fontSize="48px" fontWeight={500} lineHeight="42px" marginRight={10}> <Text fontSize="48px" fontWeight={500} lineHeight="42px" marginRight={10}>
{tokens[Field.TOKEN_A]?.symbol + '/' + tokens[Field.TOKEN_B]?.symbol} {currencies[Field.CURRENCY_A]?.symbol + '/' + currencies[Field.CURRENCY_B]?.symbol}
</Text> </Text>
<DoubleLogo a0={tokens[Field.TOKEN_A]?.address} a1={tokens[Field.TOKEN_B]?.address} size={30} /> <DoubleCurrencyLogo
currency0={currencies[Field.CURRENCY_A]}
currency1={currencies[Field.CURRENCY_B]}
size={30}
/>
</RowFlat> </RowFlat>
</LightCard> </LightCard>
</AutoColumn> </AutoColumn>
@@ -210,11 +231,15 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
<Text fontSize="48px" fontWeight={500} lineHeight="42px" marginRight={10}> <Text fontSize="48px" fontWeight={500} lineHeight="42px" marginRight={10}>
{liquidityMinted?.toSignificant(6)} {liquidityMinted?.toSignificant(6)}
</Text> </Text>
<DoubleLogo a0={tokens[Field.TOKEN_A]?.address} a1={tokens[Field.TOKEN_B]?.address} size={30} /> <DoubleCurrencyLogo
currency0={currencies[Field.CURRENCY_A]}
currency1={currencies[Field.CURRENCY_B]}
size={30}
/>
</RowFlat> </RowFlat>
<Row> <Row>
<Text fontSize="24px"> <Text fontSize="24px">
{tokens[Field.TOKEN_A]?.symbol + '/' + tokens[Field.TOKEN_B]?.symbol + ' Pool Tokens'} {currencies[Field.CURRENCY_A]?.symbol + '/' + currencies[Field.CURRENCY_B]?.symbol + ' Pool Tokens'}
</Text> </Text>
</Row> </Row>
<TYPE.italic fontSize={12} textAlign="left" padding={'8px 0 0 0 '}> <TYPE.italic fontSize={12} textAlign="left" padding={'8px 0 0 0 '}>
@@ -227,86 +252,52 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
const modalBottom = () => { const modalBottom = () => {
return ( return (
<> <ConfirmAddModalBottom
<RowBetween> price={price}
<TYPE.body>{tokens[Field.TOKEN_A]?.symbol} Deposited</TYPE.body> currencies={currencies}
<RowFixed> parsedAmounts={parsedAmounts}
<TokenLogo address={tokens[Field.TOKEN_A]?.address} style={{ marginRight: '8px' }} /> noLiquidity={noLiquidity}
<TYPE.body>{parsedAmounts[Field.TOKEN_A]?.toSignificant(6)}</TYPE.body> onAdd={onAdd}
</RowFixed> poolTokenPercentage={poolTokenPercentage}
</RowBetween> />
<RowBetween>
<TYPE.body>{tokens[Field.TOKEN_B]?.symbol} Deposited</TYPE.body>
<RowFixed>
<TokenLogo address={tokens[Field.TOKEN_B]?.address} style={{ marginRight: '8px' }} />
<TYPE.body>{parsedAmounts[Field.TOKEN_B]?.toSignificant(6)}</TYPE.body>
</RowFixed>
</RowBetween>
<RowBetween>
<TYPE.body>Rates</TYPE.body>
<TYPE.body>
{`1 ${tokens[Field.TOKEN_A]?.symbol} = ${price?.toSignificant(4)} ${tokens[Field.TOKEN_B]?.symbol}`}
</TYPE.body>
</RowBetween>
<RowBetween style={{ justifyContent: 'flex-end' }}>
<TYPE.body>
{`1 ${tokens[Field.TOKEN_B]?.symbol} = ${price?.invert().toSignificant(4)} ${
tokens[Field.TOKEN_A]?.symbol
}`}
</TYPE.body>
</RowBetween>
<RowBetween>
<TYPE.body>Share of Pool:</TYPE.body>
<TYPE.body>{noLiquidity ? '100' : poolTokenPercentage?.toSignificant(4)}%</TYPE.body>
</RowBetween>
<ButtonPrimary style={{ margin: '20px 0 0 0' }} onClick={onAdd}>
<Text fontWeight={500} fontSize={20}>
{noLiquidity ? 'Create Pool & Supply' : 'Confirm Supply'}
</Text>
</ButtonPrimary>
</>
) )
} }
const PriceBar = () => { const pendingText = `Supplying ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)} ${
return ( currencies[Field.CURRENCY_A]?.symbol
<AutoColumn gap="md"> } and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)} ${currencies[Field.CURRENCY_B]?.symbol}`
<AutoRow justify="space-around" gap="4px">
<AutoColumn justify="center">
<TYPE.black>{price?.toSignificant(6) ?? '0'}</TYPE.black>
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
{tokens[Field.TOKEN_B]?.symbol} per {tokens[Field.TOKEN_A]?.symbol}
</Text>
</AutoColumn>
<AutoColumn justify="center">
<TYPE.black>{price?.invert().toSignificant(6) ?? '0'}</TYPE.black>
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
{tokens[Field.TOKEN_A]?.symbol} per {tokens[Field.TOKEN_B]?.symbol}
</Text>
</AutoColumn>
<AutoColumn justify="center">
<TYPE.black>
{noLiquidity && price
? '100'
: (poolTokenPercentage?.lessThan(ONE_BIPS) ? '<0.01' : poolTokenPercentage?.toFixed(2)) ?? '0'}
%
</TYPE.black>
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
Share of Pool
</Text>
</AutoColumn>
</AutoRow>
</AutoColumn>
)
}
const pendingText = `Supplying ${parsedAmounts[Field.TOKEN_A]?.toSignificant(6)} ${ const handleCurrencyASelect = useCallback(
tokens[Field.TOKEN_A]?.symbol (currencyA: Currency) => {
} and ${parsedAmounts[Field.TOKEN_B]?.toSignificant(6)} ${tokens[Field.TOKEN_B]?.symbol}` const newCurrencyIdA = currencyId(currencyA)
if (newCurrencyIdA === currencyIdB) {
history.push(`/add/${currencyIdB}/${currencyIdA}`)
} else {
history.push(`/add/${newCurrencyIdA}/${currencyIdB}`)
}
},
[currencyIdB, history, currencyIdA]
)
const handleCurrencyBSelect = useCallback(
(currencyB: Currency) => {
const newCurrencyIdB = currencyId(currencyB)
if (currencyIdA === newCurrencyIdB) {
if (currencyIdB) {
history.push(`/add/${currencyIdB}/${newCurrencyIdB}`)
} else {
history.push(`/add/${newCurrencyIdB}`)
}
} else {
history.push(`/add/${currencyIdA ? currencyIdA : 'ETH'}/${newCurrencyIdB}`)
}
},
[currencyIdA, history, currencyIdB]
)
return ( return (
<> <>
<AppBody> <AppBody>
<AddRemoveTabs adding={true} />
<Wrapper> <Wrapper>
<ConfirmationModal <ConfirmationModal
isOpen={showConfirm} isOpen={showConfirm}
@@ -314,7 +305,7 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
setShowConfirm(false) setShowConfirm(false)
// if there was a tx hash, we want to clear the input // if there was a tx hash, we want to clear the input
if (txHash) { if (txHash) {
onUserInput(Field.TOKEN_A, '') onFieldAInput('')
} }
setTxHash('') setTxHash('')
}} }}
@@ -344,36 +335,33 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
</ColumnCenter> </ColumnCenter>
)} )}
<CurrencyInputPanel <CurrencyInputPanel
disableTokenSelect={true} value={formattedAmounts[Field.CURRENCY_A]}
field={Field.TOKEN_A} onUserInput={onFieldAInput}
value={formattedAmounts[Field.TOKEN_A]}
onUserInput={onUserInput}
onMax={() => { onMax={() => {
maxAmounts[Field.TOKEN_A] && onUserInput(Field.TOKEN_A, maxAmounts[Field.TOKEN_A].toExact()) onFieldAInput(maxAmounts[Field.CURRENCY_A]?.toExact() ?? '')
}} }}
showMaxButton={!atMaxAmounts[Field.TOKEN_A]} onCurrencySelect={handleCurrencyASelect}
token={tokens[Field.TOKEN_A]} showMaxButton={!atMaxAmounts[Field.CURRENCY_A]}
pair={pair} currency={currencies[Field.CURRENCY_A]}
label="Input"
id="add-liquidity-input-tokena" id="add-liquidity-input-tokena"
showCommonBases
/> />
<ColumnCenter> <ColumnCenter>
<Plus size="16" color={theme.text2} /> <Plus size="16" color={theme.text2} />
</ColumnCenter> </ColumnCenter>
<CurrencyInputPanel <CurrencyInputPanel
disableTokenSelect={true} value={formattedAmounts[Field.CURRENCY_B]}
field={Field.TOKEN_B} onUserInput={onFieldBInput}
value={formattedAmounts[Field.TOKEN_B]} onCurrencySelect={handleCurrencyBSelect}
onUserInput={onUserInput}
onMax={() => { onMax={() => {
maxAmounts[Field.TOKEN_B] && onUserInput(Field.TOKEN_B, maxAmounts[Field.TOKEN_B].toExact()) onFieldBInput(maxAmounts[Field.CURRENCY_B]?.toExact() ?? '')
}} }}
showMaxButton={!atMaxAmounts[Field.TOKEN_B]} showMaxButton={!atMaxAmounts[Field.CURRENCY_B]}
token={tokens[Field.TOKEN_B]} currency={currencies[Field.CURRENCY_B]}
pair={pair}
id="add-liquidity-input-tokenb" id="add-liquidity-input-tokenb"
showCommonBases
/> />
{tokens[Field.TOKEN_A] && tokens[Field.TOKEN_B] && ( {currencies[Field.CURRENCY_A] && currencies[Field.CURRENCY_B] && pairState !== PairState.INVALID && (
<> <>
<GreyCard padding="0px" borderRadius={'20px'}> <GreyCard padding="0px" borderRadius={'20px'}>
<RowBetween padding="1rem"> <RowBetween padding="1rem">
@@ -382,7 +370,12 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
</TYPE.subHeader> </TYPE.subHeader>
</RowBetween>{' '} </RowBetween>{' '}
<LightCard padding="1rem" borderRadius={'20px'}> <LightCard padding="1rem" borderRadius={'20px'}>
<PriceBar /> <PoolPriceBar
currencies={currencies}
poolTokenPercentage={poolTokenPercentage}
noLiquidity={noLiquidity}
price={price}
/>
</LightCard> </LightCard>
</GreyCard> </GreyCard>
</> </>
@@ -405,9 +398,9 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
width={approvalB !== ApprovalState.APPROVED ? '48%' : '100%'} width={approvalB !== ApprovalState.APPROVED ? '48%' : '100%'}
> >
{approvalA === ApprovalState.PENDING ? ( {approvalA === ApprovalState.PENDING ? (
<Dots>Approving {tokens[Field.TOKEN_A]?.symbol}</Dots> <Dots>Approving {currencies[Field.CURRENCY_A]?.symbol}</Dots>
) : ( ) : (
'Approve ' + tokens[Field.TOKEN_A]?.symbol 'Approve ' + currencies[Field.CURRENCY_A]?.symbol
)} )}
</ButtonPrimary> </ButtonPrimary>
)} )}
@@ -418,9 +411,9 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
width={approvalA !== ApprovalState.APPROVED ? '48%' : '100%'} width={approvalA !== ApprovalState.APPROVED ? '48%' : '100%'}
> >
{approvalB === ApprovalState.PENDING ? ( {approvalB === ApprovalState.PENDING ? (
<Dots>Approving {tokens[Field.TOKEN_B]?.symbol}</Dots> <Dots>Approving {currencies[Field.CURRENCY_B]?.symbol}</Dots>
) : ( ) : (
'Approve ' + tokens[Field.TOKEN_B]?.symbol 'Approve ' + currencies[Field.CURRENCY_B]?.symbol
)} )}
</ButtonPrimary> </ButtonPrimary>
)} )}
@@ -431,7 +424,7 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
expertMode ? onAdd() : setShowConfirm(true) expertMode ? onAdd() : setShowConfirm(true)
}} }}
disabled={!isValid || approvalA !== ApprovalState.APPROVED || approvalB !== ApprovalState.APPROVED} disabled={!isValid || approvalA !== ApprovalState.APPROVED || approvalB !== ApprovalState.APPROVED}
error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]} error={!isValid && !!parsedAmounts[Field.CURRENCY_A] && !!parsedAmounts[Field.CURRENCY_B]}
> >
<Text fontSize={20} fontWeight={500}> <Text fontSize={20} fontWeight={500}>
{error ?? 'Supply'} {error ?? 'Supply'}
@@ -443,9 +436,9 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
</Wrapper> </Wrapper>
</AppBody> </AppBody>
{pair && !noLiquidity ? ( {pair && !noLiquidity && pairState !== PairState.INVALID ? (
<AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}> <AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
<PositionCard pair={pair} minimal={true} /> <MinimalPositionCard showUnwrapped={oneCurrencyIsWETH} pair={pair} />
</AutoColumn> </AutoColumn>
) : null} ) : null}
</> </>

View File

@@ -0,0 +1,34 @@
import React from 'react'
import { Redirect, RouteComponentProps } from 'react-router-dom'
import AddLiquidity from './index'
export function RedirectToAddLiquidity() {
return <Redirect to="/add/" />
}
const OLD_PATH_STRUCTURE = /^(0x[a-fA-F0-9]{40})-(0x[a-fA-F0-9]{40})$/
export function RedirectOldAddLiquidityPathStructure(props: RouteComponentProps<{ currencyIdA: string }>) {
const {
match: {
params: { currencyIdA }
}
} = props
const match = currencyIdA.match(OLD_PATH_STRUCTURE)
if (match?.length) {
return <Redirect to={`/add/${match[1]}/${match[2]}`} />
}
return <AddLiquidity {...props} />
}
export function RedirectDuplicateTokenIds(props: RouteComponentProps<{ currencyIdA: string; currencyIdB: string }>) {
const {
match: {
params: { currencyIdA, currencyIdB }
}
} = props
if (currencyIdA.toLowerCase() === currencyIdB.toLowerCase()) {
return <Redirect to={`/add/${currencyIdA}`} />
}
return <AddLiquidity {...props} />
}

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