Compare commits

...

106 Commits

Author SHA1 Message Date
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
Moody Salem
4d5a43351f fix(trustwallet confirm modal): fix the confirmation modal to not be obscured by the trustwallet bar 2020-06-15 23:09:33 -04:00
Moody Salem
ac1bc3b3a6 cleanup(modal): clean up modal code a bit 2020-06-15 16:46:38 -04:00
Noah Zinsmeister
d1063d50ed add COMP, mUSD, STAKE 2020-06-15 16:24:39 -04:00
Moody Salem
46fc74e90f chore(deploy): trigger a deploy and also trigger deploys on .env.production changes 2020-06-15 10:19:46 -04:00
Moody Salem
2c4f4092d8 improvement(advanced): always show advanced (#890)
* always show advanced

* fix test

* always show the price, less jitter

* show tokens in price field

* fix the dropdown sticking around when switching between swap/send

* lint

* fix ios scrolling the modal body into the viewport bug

* don't use react-spring for simple slide animation

* safer price impact constants
2020-06-15 10:13:12 -04:00
Moody Salem
aac7268dc8 just drop the git commit hash (more trouble than it's worth) 2020-06-15 09:36:39 -04:00
Callil Capuozzo
fd162a72ff feat(expert mode): Add expert mode (#828)
* Add expert mode scaffolding

* move advanced settings to settings tab, add expert mode

* update settings modal

* update font weight

* fix text

* update with modal
;

* add null checks

* update with input checking

* merge and add fixes

* update language and bg

Co-authored-by: ianlapham <ianlapham@gmail.com>
2020-06-12 15:26:28 -04:00
Noah Zinsmeister
e20936709c improvement(migration): improve v1 migration flow (#885)
* first stab at improving v1 migration flow

* lint errors

* improve UI

* fix loading indicator

* switch back to dedicated migration UI

* address comments

* make migrate consistent with new token behavior

* hooks -> utils
2020-06-12 14:04:42 -04:00
Moody Salem
2fda2c8c15 fix(background image): cuts off at the bottom when scrolling 2020-06-12 13:42:35 -04:00
Moody Salem
1f09757c49 fix(lp fee): correct the computation of the realized LP fee 2020-06-12 12:07:28 -04:00
Moody Salem
7e49babff7 improvement(token search): No automatic add (#888)
* no automatic add, major refactors

* fix nit with version link

* add/remove links in the token search modal

* close tooltip when user types

* remove skip
2020-06-12 11:15:18 -04:00
Noah Zinsmeister
b35653ade1 fix approval bugs (#887)
clear signature on input in remove
2020-06-11 19:34:53 -04:00
Ian Lapham
57b53013d1 improvement(add/remove liquidity): update approve flow on add + remove (#879)
* update approve flow on add + remove

* add confirm to remove page
2020-06-11 17:01:39 -04:00
Moody Salem
bafd3f3c05 feat(v1-support): Enable executing swaps on v1 or v2 (#883)
* swap-v1

* toggle the version switch based on the search query parameter

* rework some of the query parameter stuff in send/swap

* hide the url when they click it

* allow switching back to v2 via the toggle

* represent the v1 trade in the UI if they toggle it on

* show trade link in both directions (5% threshold for v1 link)

* input amounts should reflect v1/v2

* perform the approve on v1 exchange for v1 trades

* get swap on v1 working

* move some code around to reduce duplication

* fix ts error

* correct input allowance

* fix exact token to token on v1

* fix pending approvals to be specific to the spender

* google analytics for swap version

* disable the version switch on pages other than swap and send
2020-06-11 15:56:28 -04:00
Moody Salem
29db0a50b3 fix(multicall): reduce spam in error case 2020-06-10 16:14:49 -04:00
Moody Salem
9566fb888e - do not deploy on weekends
- update text in release body
2020-06-10 13:55:35 -04:00
Ian Lapham
13c8903e8f add border to QR scanner (#877) 2020-06-08 11:53:40 -04:00
Moody Salem
1d06b47e8d workaround so tests are less flaky 2020-06-08 10:51:14 -04:00
CryptoCatVC
0089c2ee43 Create iw.json (#868) 2020-06-08 10:06:44 -04:00
Moody Salem
9869a9fcb7 fix(swap): if swap fails in FoT router but not in regular router, throw an error (#875)
this is an alternative solution to hard coding forced FoT router tokens
2020-06-08 10:06:37 -04:00
dependabot[bot]
631f29d66d chore(deps): bump websocket-extensions from 0.1.3 to 0.1.4 (#873)
Bumps [websocket-extensions](https://github.com/faye/websocket-extensions-node) from 0.1.3 to 0.1.4.
- [Release notes](https://github.com/faye/websocket-extensions-node/releases)
- [Changelog](https://github.com/faye/websocket-extensions-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/faye/websocket-extensions-node/compare/0.1.3...0.1.4)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-06-08 10:04:57 -04:00
Moody Salem
97deebad37 temporary workaround for tokens that have transfer methods that succeed but do not transfer tokens 2020-06-06 08:41:31 -04:00
Noah Zinsmeister
e667615449 support proposed new FoT-capable methods (#866)
* support new FoT-capable methods

short-circuit modal footer rendering if no trade

improve clarity of tx modal flow

* update router address + ABI
2020-06-05 18:40:02 -04:00
CryptoCatVC
4ab61faeae Create he.json (#864) 2020-06-05 18:30:45 -04:00
Ian Lapham
0004db3d4a Style updates, Approve UI updates (#841)
improvements(approvals): match approve flow to add/remove, update UI styles
2020-06-05 13:20:47 -04:00
Moody Salem
c133c472be fix(search): fix searching tokens to be more tolerant, and sort the exact matched symbols first 2020-06-05 11:20:04 -04:00
Noah Zinsmeister
0019ccdf51 add ren* tokens 2020-06-04 20:58:14 -04:00
Moody Salem
5a1a469f35 perf(multicall): remove the validation that caused app to feel sluggish 2020-06-03 23:19:19 -04:00
Moody Salem
4c28f34803 remove vercel.json to put it in another repo 2020-06-03 22:35:56 -04:00
Moody Salem
104be830fc perf(multicall): use single call to get token information (#855)
* use single call to get token information

* delete the bytes32 overload

* console log statement

* add a bunch of tests to actions.ts for multicall

* fix to work with bytes32 symbols/names

* only include name/symbol

* enforce lowercase calldata
2020-06-03 22:15:26 -04:00
Moody Salem
24c70791cd lint error 2020-06-03 20:23:01 -04:00
Moody Salem
216fdea290 fix(instrumentation): event for migration v1->v2 2020-06-03 20:22:28 -04:00
Moody Salem
40e4ce2ed3 fix(style): add missing padding 2020-06-03 17:23:21 -04:00
Moody Salem
b2508fc6f2 Add missing capture group 2020-06-03 14:52:38 -04:00
Moody Salem
f73b37287f Remove the invalid redirect 2020-06-03 14:48:25 -04:00
Moody Salem
c09eb738c3 Add vercel.json for redirects in vercel deployment 2020-06-03 14:46:02 -04:00
Moody Salem
6de3a6ec28 Avoid duplicate popups for mined transactions 2020-06-03 14:30:07 -04:00
Moody Salem
c1d35cc8b3 feat(migrate): adds the migrate flow to the uniswap exchange site
* links and page

* print all the details of the liquidity

* show working approve/migrate buttons

* testnet v1 factory addresses

* split code up into two pages

* getting closer to styled

* compute min amount out of eth and token

* compute min amount out of eth and token

* add a back button to the list page

* Improve empty states

* Improve the state management

* change the display of the migrate page to be more similar to original

* style fix, pending transaction hook fix

* add forwarding to netlify.toml

* handle case where v2 spot price does not exist because pair does not exist

* make ternaries more accurate

* handle first liquidity provider situation

* Style tweaks for migrate

* merge

* Address feedback
- show pool token amount
- show success when migration complete
- show price warning only if price difference is large

Co-authored-by: Callil Capuozzo <callil.capuozzo@gmail.com>
2020-06-03 14:07:01 -04:00
Moody Salem
f279b2bea2 fix(misc): migrate link and gradient cut off 2020-06-03 13:36:31 -04:00
Noah Zinsmeister
6ffbf756f8 memoize common bases (#853)
* memoize common bases

* improve memoization
2020-06-02 15:43:10 -04:00
Moody Salem
10837d7ba1 fix(scrolling): too much scrolling 2020-06-02 12:28:35 -04:00
Noah Zinsmeister
2d6eddf9d4 introduce batched usePairs (#851)
centralize all lists of bases

pin some pairs
2020-06-01 16:55:31 -04:00
Moody Salem
aadf43efc3 chore(v1): add ipfs links for v1 to the v1 release 2020-06-01 11:20:05 -04:00
Noah Zinsmeister
227f729ecd add AST and EBASE 2020-06-01 10:53:40 -04:00
Moody Salem
a5b15e37f6 fix(application state): fix block number updates after changing networks 2020-05-31 15:33:31 -04:00
155 changed files with 6141 additions and 4130 deletions

View File

@@ -2,14 +2,10 @@ name: Release
on: on:
# every morning # every morning
schedule: schedule:
- cron: '0 12 * * *' - 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'
jobs: jobs:
bump_version: bump_version:
@@ -43,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'
@@ -63,15 +59,15 @@ jobs:
with: with:
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:
with: # CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_TOKEN }}
domain: 'uniswap.org' # RECORD_DOMAIN: 'uniswap.org'
subdomain: '_dnslink.app' # RECORD_NAME: '_dnslink.app'
record-type: 'TXT' # CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}
value: dnslink=/ipfs/${{ steps.upload.outputs.hash }} # uses: textileio/cloudflare-update-dnslink@0fe7b7a1ffc865db3a4da9773f0f987447ad5848
token: ${{ secrets.VERCEL_TOKEN }} # with:
team-name: 'uniswap' # cid: ${{ steps.upload.outputs.hash }}
- name: Create GitHub Release - name: Create GitHub Release
id: create_release id: create_release
@@ -82,19 +78,21 @@ jobs:
tag_name: ${{ needs.bump_version.outputs.new_tag }} tag_name: ${{ needs.bump_version.outputs.new_tag }}
release_name: Release ${{ needs.bump_version.outputs.new_tag }} release_name: Release ${{ needs.bump_version.outputs.new_tag }}
body: | body: |
Release built from commit [`${{ github.sha }}`](https://github.com/Uniswap/uniswap-frontend/tree/${{ github.sha }}) IPFS hash of the deployment:
The IPFS hash of the bundle is:
- CIDv0: `${{ steps.upload.outputs.hash }}` - CIDv0: `${{ steps.upload.outputs.hash }}`
- CIDv1: `${{ steps.convert_cidv0.outputs.cidv1 }}` - CIDv1: `${{ steps.convert_cidv0.outputs.cidv1 }}`
Uniswap uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to store your settings. The latest release is always accessible via our alias to the Cloudflare IPFS gateway at [app.uniswap.org](https://app.uniswap.org).
**Beware** that other sites you access via the _same_ IPFS gateway can read and modify your settings on Uniswap 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 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.
**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, 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,11 @@ 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).
## Development ## Development
@@ -27,26 +28,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 Interface V1
The Uniswap Interface supports swapping against, and migrating or removing liquidity from Uniswap V1. However,
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,29 @@ 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/ETH/token', () => {
cy.visit('/add/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
cy.url().should('contain', '/add/ETH/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
})
it('redirects /add/token-WETH to /add/token/ETH', () => {
cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85-0xc778417E063141139Fce010982780140Aa0cD5Ab')
cy.url().should('contain', '/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85/ETH')
})
}) })

View File

@@ -1,4 +1,4 @@
import { TEST_ADDRESS_NEVER_USE } from '../support/commands' import { TEST_ADDRESS_NEVER_USE_SHORTENED } from '../support/commands'
describe('Landing Page', () => { describe('Landing Page', () => {
beforeEach(() => cy.visit('/')) beforeEach(() => cy.visit('/'))
@@ -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')
@@ -22,6 +17,6 @@ describe('Landing Page', () => {
it('is connected', () => { it('is connected', () => {
cy.get('#web3-status-connected').click() cy.get('#web3-status-connected').click()
cy.get('#web3-account-identifier-row').contains(TEST_ADDRESS_NEVER_USE) cy.get('#web3-account-identifier-row').contains(TEST_ADDRESS_NEVER_USE_SHORTENED)
}) })
}) })

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') 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,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') cy.url().should('contain', '/swap?outputCurrency=ETH&recipient=bob.argent.xyz')
.should('have.value', '0.001')
}) })
}) })

View File

@@ -2,31 +2,31 @@ describe('Swap', () => {
beforeEach(() => cy.visit('/swap')) beforeEach(() => cy.visit('/swap'))
it('can enter an amount into input', () => { it('can enter an amount into input', () => {
cy.get('#swap-currency-input .token-amount-input') cy.get('#swap-currency-input .token-amount-input')
.type('0.001') .type('0.001', { delay: 200 })
.should('have.value', '0.001') .should('have.value', '0.001')
}) })
it('zero swap amount', () => { it('zero swap amount', () => {
cy.get('#swap-currency-input .token-amount-input') cy.get('#swap-currency-input .token-amount-input')
.type('0.0') .type('0.0', { delay: 200 })
.should('have.value', '0.0') .should('have.value', '0.0')
}) })
it('invalid swap amount', () => { it('invalid swap amount', () => {
cy.get('#swap-currency-input .token-amount-input') cy.get('#swap-currency-input .token-amount-input')
.type('\\') .type('\\', { delay: 200 })
.should('have.value', '') .should('have.value', '')
}) })
it('can enter an amount into output', () => { it('can enter an amount into output', () => {
cy.get('#swap-currency-output .token-amount-input') cy.get('#swap-currency-output .token-amount-input')
.type('0.001') .type('0.001', { delay: 200 })
.should('have.value', '0.001') .should('have.value', '0.001')
}) })
it('zero output amount', () => { it('zero output amount', () => {
cy.get('#swap-currency-output .token-amount-input') cy.get('#swap-currency-output .token-amount-input')
.type('0.0') .type('0.0', { delay: 200 })
.should('have.value', '0.0') .should('have.value', '0.0')
}) })
@@ -35,10 +35,20 @@ describe('Swap', () => {
cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').should('be.visible') cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').should('be.visible')
cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').click({ force: true }) cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').click({ force: true })
cy.get('#swap-currency-input .token-amount-input').should('be.visible') cy.get('#swap-currency-input .token-amount-input').should('be.visible')
cy.get('#swap-currency-input .token-amount-input').type('0.001', { force: true }) cy.get('#swap-currency-input .token-amount-input').type('0.001', { force: true, delay: 200 })
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '') cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
cy.get('#show-advanced').click()
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

@@ -1,5 +1,7 @@
export const TEST_ADDRESS_NEVER_USE: string export const TEST_ADDRESS_NEVER_USE: string
export const TEST_ADDRESS_NEVER_USE_SHORTENED: string
// declare namespace Cypress { // declare namespace Cypress {
// // eslint-disable-next-line @typescript-eslint/class-name-casing // // eslint-disable-next-line @typescript-eslint/class-name-casing
// interface cy { // interface cy {

View File

@@ -14,6 +14,8 @@ const PRIVATE_KEY_TEST_NEVER_USE = '0xad20c82497421e9784f18460ad2fe84f73569068e9
// address of the above key // address of the above key
export const TEST_ADDRESS_NEVER_USE = '0x0fF2D1eFd7A57B7562b2bf27F3f37899dB27F4a5' export const TEST_ADDRESS_NEVER_USE = '0x0fF2D1eFd7A57B7562b2bf27F3f37899dB27F4a5'
export const TEST_ADDRESS_NEVER_USE_SHORTENED = '0x0fF2...F4a5'
class CustomizedBridge extends _Eip1193Bridge { class CustomizedBridge extends _Eip1193Bridge {
async sendAsync(...args) { async sendAsync(...args) {
console.debug('sendAsync called', ...args) console.debug('sendAsync called', ...args)
@@ -67,13 +69,13 @@ 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/acb7e55995d04c49bfb52b7141599467', 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,20 +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 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,7 +1,7 @@
{ {
"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",
@@ -13,7 +13,7 @@
"@ethersproject/strings": "^5.0.0-beta.136", "@ethersproject/strings": "^5.0.0-beta.136",
"@ethersproject/units": "^5.0.0-beta.132", "@ethersproject/units": "^5.0.0-beta.132",
"@ethersproject/wallet": "^5.0.0-beta.141", "@ethersproject/wallet": "^5.0.0-beta.141",
"@popperjs/core": "^2.4.0", "@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",
@@ -33,12 +33,12 @@
"@typescript-eslint/parser": "^2.31.0", "@typescript-eslint/parser": "^2.31.0",
"@uniswap/sdk": "^2.0.5", "@uniswap/sdk": "^2.0.5",
"@uniswap/v2-core": "1.0.0", "@uniswap/v2-core": "1.0.0",
"@uniswap/v2-periphery": "1.0.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",
"copy-to-clipboard": "^3.2.0", "copy-to-clipboard": "^3.2.0",
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
@@ -51,6 +51,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",
@@ -80,15 +81,11 @@
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
"build": "cross-env REACT_APP_GIT_COMMIT_HASH=$(git show -s --format=%H) 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"

75
public/locales/iw.json Normal file
View File

@@ -0,0 +1,75 @@
{
"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

@@ -6,21 +6,21 @@ import { LinkStyledButton } from '../../theme'
import { CheckCircle, Copy } from 'react-feather' import { CheckCircle, Copy } from 'react-feather'
const CopyIcon = styled(LinkStyledButton)` const CopyIcon = styled(LinkStyledButton)`
color: ${({ theme }) => theme.text4}; color: ${({ theme }) => theme.text3};
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
margin-right: 1rem;
margin-left: 0.5rem;
text-decoration: none; text-decoration: none;
font-size: 0.825rem;
:hover, :hover,
:active, :active,
:focus { :focus {
text-decoration: none; text-decoration: none;
color: ${({ theme }) => theme.text3}; color: ${({ theme }) => theme.text2};
} }
` `
const TransactionStatusText = styled.span` const TransactionStatusText = styled.span`
margin-left: 0.25rem; margin-left: 0.25rem;
font-size: 0.825rem;
${({ theme }) => theme.flexRowNoWrap}; ${({ theme }) => theme.flexRowNoWrap};
align-items: center; align-items: center;
` `
@@ -30,7 +30,6 @@ export default function CopyHelper(props: { toCopy: string; children?: React.Rea
return ( return (
<CopyIcon onClick={() => setCopied(props.toCopy)}> <CopyIcon onClick={() => setCopied(props.toCopy)}>
{props.children}
{isCopied ? ( {isCopied ? (
<TransactionStatusText> <TransactionStatusText>
<CheckCircle size={'16'} /> <CheckCircle size={'16'} />
@@ -41,6 +40,7 @@ export default function CopyHelper(props: { toCopy: string; children?: React.Rea
<Copy size={'16'} /> <Copy size={'16'} />
</TransactionStatusText> </TransactionStatusText>
)} )}
{isCopied ? '' : props.children}
</CopyIcon> </CopyIcon>
) )
} }

View File

@@ -1,55 +1,39 @@
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { Check, Triangle } 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'
import { ExternalLink, Spinner } from '../../theme' import { ExternalLink } from '../../theme'
import Circle from '../../assets/images/circle.svg'
import { transparentize } from 'polished'
import { useAllTransactions } from '../../state/transactions/hooks' import { useAllTransactions } from '../../state/transactions/hooks'
import { RowFixed } from '../Row'
import Loader from '../Loader'
const TransactionWrapper = styled.div` const TransactionWrapper = styled.div``
margin-top: 0.75rem;
`
const TransactionStatusText = styled.div` const TransactionStatusText = styled.div`
margin-right: 0.5rem; margin-right: 0.5rem;
display: flex;
align-items: center;
:hover {
text-decoration: underline;
}
` `
const TransactionState = styled(ExternalLink)<{ pending: boolean; success?: boolean }>` const TransactionState = styled(ExternalLink)<{ pending: boolean; success?: boolean }>`
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center;
text-decoration: none !important; text-decoration: none !important;
border-radius: 0.5rem; border-radius: 0.5rem;
padding: 0.5rem 0.75rem; padding: 0.25rem 0rem;
font-weight: 500; font-weight: 500;
font-size: 0.75rem; font-size: 0.825rem;
border: 1px solid; color: ${({ theme }) => theme.primary1};
color: ${({ pending, success, theme }) => (pending ? theme.primary1 : success ? theme.green1 : theme.red1)};
border-color: ${({ pending, success, theme }) =>
pending
? transparentize(0.75, theme.primary1)
: success
? transparentize(0.75, theme.green1)
: transparentize(0.75, theme.red1)};
:hover {
border-color: ${({ pending, success, theme }) =>
pending
? transparentize(0, theme.primary1)
: success
? transparentize(0, theme.green1)
: transparentize(0, theme.red1)};
}
` `
const IconWrapper = styled.div` const IconWrapper = styled.div<{ pending: boolean; success?: boolean }>`
flex-shrink: 0; color: ${({ pending, success, theme }) => (pending ? theme.primary1 : success ? theme.green1 : theme.red1)};
` `
export default function Transaction({ hash }: { hash: string }) { export default function Transaction({ hash }: { hash: string }) {
@@ -65,9 +49,11 @@ export default function Transaction({ hash }: { hash: string }) {
return ( return (
<TransactionWrapper> <TransactionWrapper>
<TransactionState href={getEtherscanLink(chainId, hash, 'transaction')} pending={pending} success={success}> <TransactionState href={getEtherscanLink(chainId, hash, 'transaction')} pending={pending} success={success}>
<TransactionStatusText>{summary ? summary : hash}</TransactionStatusText> <RowFixed>
<IconWrapper> <TransactionStatusText>{summary ?? hash} </TransactionStatusText>
{pending ? <Spinner src={Circle} /> : success ? <Check size="16" /> : <Triangle size="16" />} </RowFixed>
<IconWrapper pending={pending} success={success}>
{pending ? <Loader /> : success ? <CheckCircle size="16" /> : <Triangle size="16" />}
</IconWrapper> </IconWrapper>
</TransactionState> </TransactionState>
</TransactionWrapper> </TransactionWrapper>

View File

@@ -2,9 +2,9 @@ import React, { useCallback, useContext } from 'react'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import styled, { ThemeContext } from 'styled-components' import styled, { ThemeContext } from 'styled-components'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { isMobile } from 'react-device-detect'
import { AppDispatch } from '../../state' import { AppDispatch } from '../../state'
import { clearAllTransactions } from '../../state/transactions/actions' import { clearAllTransactions } from '../../state/transactions/actions'
import { shortenAddress } from '../../utils'
import { AutoRow } from '../Row' import { AutoRow } from '../Row'
import Copy from './Copy' import Copy from './Copy'
import Transaction from './Transaction' import Transaction from './Transaction'
@@ -18,9 +18,8 @@ import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
import FortmaticIcon from '../../assets/images/fortmaticIcon.png' import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
import PortisIcon from '../../assets/images/portisIcon.png' import PortisIcon from '../../assets/images/portisIcon.png'
import Identicon from '../Identicon' import Identicon from '../Identicon'
import { ButtonSecondary } from '../Button'
import { ButtonEmpty } from '../Button' import { ExternalLink as LinkIcon } from 'react-feather'
import { ExternalLink, LinkStyledButton, TYPE } from '../../theme' import { ExternalLink, LinkStyledButton, TYPE } from '../../theme'
const HeaderRow = styled.div` const HeaderRow = styled.div`
@@ -55,31 +54,31 @@ const UpperSection = styled.div`
const InfoCard = styled.div` const InfoCard = styled.div`
padding: 1rem; padding: 1rem;
background-color: ${({ theme }) => theme.bg2}; border: 1px solid ${({ theme }) => theme.bg3};
border-radius: 20px; border-radius: 20px;
position: relative;
display: grid;
grid-row-gap: 12px;
margin-bottom: 20px;
` `
const AccountGroupingRow = styled.div` const AccountGroupingRow = styled.div`
${({ theme }) => theme.flexRowNoWrap}; ${({ theme }) => theme.flexRowNoWrap};
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
font-weight: 500; font-weight: 400;
color: ${({ theme }) => theme.text1}; color: ${({ theme }) => theme.text1};
div { div {
${({ theme }) => theme.flexRowNoWrap} ${({ theme }) => theme.flexRowNoWrap}
align-items: center; align-items: center;
} }
&:first-of-type {
margin-bottom: 8px;
}
` `
const AccountSection = styled.div` const AccountSection = styled.div`
background-color: ${({ theme }) => theme.bg1}; background-color: ${({ theme }) => theme.bg1};
padding: 0rem 1rem; padding: 0rem 1rem;
${({ theme }) => theme.mediaWidth.upToMedium`padding: 0rem 1rem 1rem 1rem;`}; ${({ theme }) => theme.mediaWidth.upToMedium`padding: 0rem 1rem 1.5rem 1rem;`};
` `
const YourAccount = styled.div` const YourAccount = styled.div`
@@ -94,28 +93,6 @@ const YourAccount = styled.div`
} }
` `
const GreenCircle = styled.div`
${({ theme }) => theme.flexRowNoWrap}
justify-content: center;
align-items: center;
&:first-child {
height: 8px;
width: 8px;
margin-left: 12px;
margin-right: 2px;
background-color: ${({ theme }) => theme.green1};
border-radius: 50%;
}
`
const CircleWrapper = styled.div`
color: ${({ theme }) => theme.green1};
display: flex;
justify-content: center;
align-items: center;
`
const LowerSection = styled.div` const LowerSection = styled.div`
${({ theme }) => theme.flexColumnNoWrap} ${({ theme }) => theme.flexColumnNoWrap}
padding: 1.5rem; padding: 1.5rem;
@@ -132,13 +109,14 @@ const LowerSection = styled.div`
} }
` `
const AccountControl = styled.div<{ hasENS: boolean; isENS: boolean }>` const AccountControl = styled.div`
${({ theme }) => theme.flexRowNoWrap}; display: flex;
align-items: center; justify-content: space-between;
min-width: 0; min-width: 0;
width: 100%;
font-weight: ${({ hasENS, isENS }) => (hasENS ? (isENS ? 500 : 400) : 500)}; font-weight: 500;
font-size: ${({ hasENS, isENS }) => (hasENS ? (isENS ? '1rem' : '0.8rem') : '1rem')}; font-size: 1.25rem;
a:hover { a:hover {
text-decoration: underline; text-decoration: underline;
@@ -146,22 +124,22 @@ const AccountControl = styled.div<{ hasENS: boolean; isENS: boolean }>`
p { p {
min-width: 0; min-width: 0;
margin: 0.5rem 0; margin: 0;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
` `
const ConnectButtonRow = styled.div`
${({ theme }) => theme.flexRowNoWrap}
align-items: center;
justify-content: center;
margin: 10px 0;
`
const AddressLink = styled(ExternalLink)<{ hasENS: boolean; isENS: boolean }>` const AddressLink = styled(ExternalLink)<{ hasENS: boolean; isENS: boolean }>`
color: ${({ hasENS, isENS, theme }) => (hasENS ? (isENS ? theme.primary1 : theme.text3) : theme.primary1)}; font-size: 0.825rem;
color: ${({ theme }) => theme.text3};
margin-left: 1rem;
font-size: 0.825rem;
display: flex;
:hover {
color: ${({ theme }) => theme.text2};
}
` `
const CloseIcon = styled.div` const CloseIcon = styled.div`
@@ -181,14 +159,17 @@ const CloseColor = styled(Close)`
` `
const WalletName = styled.div` const WalletName = styled.div`
padding-left: 0.5rem;
width: initial; width: initial;
font-size: 0.825rem;
font-weight: 500;
color: ${({ theme }) => theme.text3};
` `
const IconWrapper = styled.div<{ size?: number }>` const IconWrapper = styled.div<{ size?: number }>`
${({ theme }) => theme.flexColumnNoWrap}; ${({ theme }) => theme.flexColumnNoWrap};
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-right: 8px;
& > img, & > img,
span { span {
height: ${({ size }) => (size ? size + 'px' : '32px')}; height: ${({ size }) => (size ? size + 'px' : '32px')};
@@ -203,10 +184,12 @@ const TransactionListWrapper = styled.div`
${({ theme }) => theme.flexColumnNoWrap}; ${({ theme }) => theme.flexColumnNoWrap};
` `
const WalletAction = styled.div` const WalletAction = styled(ButtonSecondary)`
color: ${({ theme }) => theme.text4}; width: fit-content;
margin-left: 16px;
font-weight: 400; font-weight: 400;
margin-left: 8px;
font-size: 0.825rem;
padding: 4px 6px;
:hover { :hover {
cursor: pointer; cursor: pointer;
text-decoration: underline; text-decoration: underline;
@@ -255,39 +238,39 @@ export default function AccountDetails({
SUPPORTED_WALLETS[k].connector === connector && (connector !== injected || isMetaMask === (k === 'METAMASK')) SUPPORTED_WALLETS[k].connector === connector && (connector !== injected || isMetaMask === (k === 'METAMASK'))
) )
.map(k => SUPPORTED_WALLETS[k].name)[0] .map(k => SUPPORTED_WALLETS[k].name)[0]
return <WalletName>{name}</WalletName> return <WalletName>Connected with {name}</WalletName>
} }
function getStatusIcon() { function getStatusIcon() {
if (connector === injected) { if (connector === injected) {
return ( return (
<IconWrapper size={16}> <IconWrapper size={16}>
<Identicon /> {formatConnectorName()} <Identicon />
</IconWrapper> </IconWrapper>
) )
} else if (connector === walletconnect) { } else if (connector === walletconnect) {
return ( return (
<IconWrapper size={16}> <IconWrapper size={16}>
<img src={WalletConnectIcon} alt={''} /> {formatConnectorName()} <img src={WalletConnectIcon} alt={''} />
</IconWrapper> </IconWrapper>
) )
} else if (connector === walletlink) { } else if (connector === walletlink) {
return ( return (
<IconWrapper size={16}> <IconWrapper size={16}>
<img src={CoinbaseWalletIcon} alt={''} /> {formatConnectorName()} <img src={CoinbaseWalletIcon} alt={''} />
</IconWrapper> </IconWrapper>
) )
} else if (connector === fortmatic) { } else if (connector === fortmatic) {
return ( return (
<IconWrapper size={16}> <IconWrapper size={16}>
<img src={FortmaticIcon} alt={''} /> {formatConnectorName()} <img src={FortmaticIcon} alt={''} />
</IconWrapper> </IconWrapper>
) )
} else if (connector === portis) { } else if (connector === portis) {
return ( return (
<> <>
<IconWrapper size={16}> <IconWrapper size={16}>
<img src={PortisIcon} alt={''} /> {formatConnectorName()} <img src={PortisIcon} alt={''} />
<MainWalletAction <MainWalletAction
onClick={() => { onClick={() => {
portis.portis.showPortis() portis.portis.showPortis()
@@ -320,10 +303,11 @@ export default function AccountDetails({
<YourAccount> <YourAccount>
<InfoCard> <InfoCard>
<AccountGroupingRow> <AccountGroupingRow>
{getStatusIcon()} {formatConnectorName()}
<div> <div>
{connector !== injected && connector !== walletlink && ( {connector !== injected && connector !== walletlink && (
<WalletAction <WalletAction
style={{ fontSize: '.825rem', fontWeight: 400, marginRight: '8px' }}
onClick={() => { onClick={() => {
;(connector as any).close() ;(connector as any).close()
}} }}
@@ -331,73 +315,82 @@ export default function AccountDetails({
Disconnect Disconnect
</WalletAction> </WalletAction>
)} )}
<CircleWrapper> <WalletAction
<GreenCircle> style={{ fontSize: '.825rem', fontWeight: 400 }}
<div /> onClick={() => {
</GreenCircle> openOptions()
</CircleWrapper> }}
>
Change
</WalletAction>
</div> </div>
</AccountGroupingRow> </AccountGroupingRow>
<AccountGroupingRow id="web3-account-identifier-row"> <AccountGroupingRow id="web3-account-identifier-row">
{ENSName ? ( <AccountControl>
<> {ENSName ? (
<AccountControl hasENS={!!ENSName} isENS={true}> <>
<p>{ENSName}</p> <Copy toCopy={account} /> <div>
</AccountControl> {getStatusIcon()}
</> <p> {ENSName}</p>
) : ( </div>
<> </>
<AccountControl hasENS={!!ENSName} isENS={false}> ) : (
<p>{account}</p> <Copy toCopy={account} /> <>
</AccountControl> <div>
</> {getStatusIcon()}
)} <p> {shortenAddress(account)}</p>
</div>
</>
)}
</AccountControl>
</AccountGroupingRow> </AccountGroupingRow>
<AccountGroupingRow> <AccountGroupingRow>
{ENSName ? ( {ENSName ? (
<> <>
<AccountControl hasENS={!!ENSName} isENS={false}> <AccountControl>
<AddressLink hasENS={!!ENSName} isENS={true} href={getEtherscanLink(chainId, ENSName, 'address')}> <div>
View on Etherscan <Copy toCopy={account}>
</AddressLink> <span style={{ marginLeft: '4px' }}>Copy Address</span>
</Copy>
<AddressLink
hasENS={!!ENSName}
isENS={true}
href={getEtherscanLink(chainId, ENSName, 'address')}
>
<LinkIcon size={16} />
<span style={{ marginLeft: '4px' }}>View on Etherscan</span>
</AddressLink>
</div>
</AccountControl> </AccountControl>
</> </>
) : ( ) : (
<> <>
<AccountControl hasENS={!!ENSName} isENS={false}> <AccountControl>
<AddressLink <div>
hasENS={!!ENSName} <Copy toCopy={account}>
isENS={false} <span style={{ marginLeft: '4px' }}>Copy Address</span>
href={getEtherscanLink(chainId, account, 'address')} </Copy>
> <AddressLink
View on Etherscan hasENS={!!ENSName}
</AddressLink> isENS={false}
href={getEtherscanLink(chainId, account, 'address')}
>
<LinkIcon size={16} />
<span style={{ marginLeft: '4px' }}>View on Etherscan</span>
</AddressLink>
</div>
</AccountControl> </AccountControl>
</> </>
)} )}
{/* {formatConnectorName()} */}
</AccountGroupingRow> </AccountGroupingRow>
</InfoCard> </InfoCard>
</YourAccount> </YourAccount>
{!(isMobile && (window.web3 || window.ethereum)) && (
<ConnectButtonRow>
<ButtonEmpty
style={{ fontWeight: 400 }}
padding={'12px'}
width={'260px'}
onClick={() => {
openOptions()
}}
>
Connect to a different wallet
</ButtonEmpty>
</ConnectButtonRow>
)}
</AccountSection> </AccountSection>
</UpperSection> </UpperSection>
{!!pendingTransactions.length || !!confirmedTransactions.length ? ( {!!pendingTransactions.length || !!confirmedTransactions.length ? (
<LowerSection> <LowerSection>
<AutoRow style={{ justifyContent: 'space-between' }}> <AutoRow mb={'1rem'} style={{ justifyContent: 'space-between' }}>
<TYPE.body>Recent Transactions</TYPE.body> <TYPE.body>Recent Transactions</TYPE.body>
<LinkStyledButton onClick={clearAllTransactionsCallback}>(clear all)</LinkStyledButton> <LinkStyledButton onClick={clearAllTransactionsCallback}>(clear all)</LinkStyledButton>
</AutoRow> </AutoRow>

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

@@ -6,7 +6,12 @@ import { RowBetween } from '../Row'
import { ChevronDown } from 'react-feather' import { ChevronDown } from 'react-feather'
import { Button as RebassButton, ButtonProps } from 'rebass/styled-components' import { Button as RebassButton, ButtonProps } from 'rebass/styled-components'
const Base = styled(RebassButton)<{ padding?: string; width?: string; borderRadius?: string }>` const Base = styled(RebassButton)<{
padding?: string
width?: string
borderRadius?: string
altDisbaledStyle?: boolean
}>`
padding: ${({ padding }) => (padding ? padding : '18px')}; padding: ${({ padding }) => (padding ? padding : '18px')};
width: ${({ width }) => (width ? width : '100%')}; width: ${({ width }) => (width ? width : '100%')};
font-weight: 500; font-weight: 500;
@@ -16,6 +21,7 @@ const Base = styled(RebassButton)<{ padding?: string; width?: string; borderRadi
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;
@@ -45,10 +51,12 @@ export const ButtonPrimary = styled(Base)`
background-color: ${({ theme }) => darken(0.1, theme.primary1)}; background-color: ${({ theme }) => darken(0.1, theme.primary1)};
} }
&:disabled { &:disabled {
background-color: ${({ theme }) => theme.bg3}; background-color: ${({ theme, altDisbaledStyle }) => (altDisbaledStyle ? theme.primary1 : theme.bg3)};
color: ${({ theme }) => theme.text3} color: ${({ theme, altDisbaledStyle }) => (altDisbaledStyle ? 'white' : theme.text3)};
cursor: auto; cursor: auto;
box-shadow: none; box-shadow: none;
border: 1px solid transparent;
outline: none;
} }
` `
@@ -68,6 +76,16 @@ export const ButtonLight = styled(Base)`
box-shadow: 0 0 0 1pt ${({ theme, disabled }) => !disabled && darken(0.05, theme.primary5)}; box-shadow: 0 0 0 1pt ${({ theme, disabled }) => !disabled && darken(0.05, theme.primary5)};
background-color: ${({ theme, disabled }) => !disabled && darken(0.05, theme.primary5)}; background-color: ${({ theme, disabled }) => !disabled && darken(0.05, theme.primary5)};
} }
:disabled {
opacity: 0.4;
:hover {
cursor: auto;
background-color: ${({ theme }) => theme.primary5};
box-shadow: none;
border: 1px solid transparent;
outline: none;
}
}
` `
export const ButtonGray = styled(Base)` export const ButtonGray = styled(Base)`
@@ -180,7 +198,6 @@ export const ButtonEmpty = styled(Base)`
export const ButtonWhite = styled(Base)` export const ButtonWhite = styled(Base)`
border: 1px solid #edeef2; border: 1px solid #edeef2;
background-color: ${({ theme }) => theme.bg1}; background-color: ${({ theme }) => theme.bg1};
};
color: black; color: black;
&:focus { &:focus {
@@ -228,6 +245,9 @@ const ButtonErrorStyle = styled(Base)`
&:disabled { &:disabled {
opacity: 50%; opacity: 50%;
cursor: auto; cursor: auto;
box-shadow: none;
background-color: ${({ theme }) => theme.red1};
border: 1px solid ${({ theme }) => theme.red1};
} }
` `
@@ -247,7 +267,7 @@ export function ButtonError({ error, ...rest }: { error?: boolean } & ButtonProp
} }
} }
export function ButtonDropwdown({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) { export function ButtonDropdown({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
return ( return (
<ButtonPrimary {...rest} disabled={disabled}> <ButtonPrimary {...rest} disabled={disabled}>
<RowBetween> <RowBetween>
@@ -258,7 +278,7 @@ export function ButtonDropwdown({ disabled = false, children, ...rest }: { disab
) )
} }
export function ButtonDropwdownLight({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) { export function ButtonDropdownLight({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
return ( return (
<ButtonOutlined {...rest} disabled={disabled}> <ButtonOutlined {...rest} disabled={disabled}>
<RowBetween> <RowBetween>

View File

@@ -1,17 +1,17 @@
import React, { useContext } from 'react' import React, { useContext } from 'react'
import styled, { ThemeContext } from 'styled-components' import styled, { ThemeContext } from 'styled-components'
import Modal from '../Modal' import Modal from '../Modal'
import Loader from '../Loader'
import { ExternalLink } from '../../theme' import { ExternalLink } from '../../theme'
import { Text } from 'rebass' import { Text } from 'rebass'
import { CloseIcon } from '../../theme/components' import { CloseIcon, Spinner } from '../../theme/components'
import { RowBetween } from '../Row' import { RowBetween } from '../Row'
import { ArrowUpCircle } from 'react-feather' import { ArrowUpCircle } from 'react-feather'
import { ButtonPrimary } from '../Button' import { ButtonPrimary } from '../Button'
import { AutoColumn, ColumnCenter } from '../Column' import { AutoColumn, ColumnCenter } from '../Column'
import Circle from '../../assets/images/blue-loader.svg'
import { useActiveWeb3React } from '../../hooks'
import { getEtherscanLink } from '../../utils' import { getEtherscanLink } from '../../utils'
import { useActiveWeb3React } from '../../hooks'
const Wrapper = styled.div` const Wrapper = styled.div`
width: 100%; width: 100%;
@@ -30,6 +30,11 @@ const ConfirmedIcon = styled(ColumnCenter)`
padding: 60px 0; padding: 60px 0;
` `
const CustomLightSpinner = styled(Spinner)<{ size: string }>`
height: ${({ size }) => size};
width: ${({ size }) => size};
`
interface ConfirmationModalProps { interface ConfirmationModalProps {
isOpen: boolean isOpen: boolean
onDismiss: () => void onDismiss: () => void
@@ -37,7 +42,6 @@ interface ConfirmationModalProps {
topContent: () => React.ReactChild topContent: () => React.ReactChild
bottomContent: () => React.ReactChild bottomContent: () => React.ReactChild
attemptingTxn: boolean attemptingTxn: boolean
pendingConfirmation: boolean
pendingText: string pendingText: string
title?: string title?: string
} }
@@ -45,33 +49,22 @@ interface ConfirmationModalProps {
export default function ConfirmationModal({ export default function ConfirmationModal({
isOpen, isOpen,
onDismiss, onDismiss,
hash,
topContent, topContent,
bottomContent, bottomContent,
attemptingTxn, attemptingTxn,
pendingConfirmation, hash,
pendingText, pendingText,
title = '' title = ''
}: ConfirmationModalProps) { }: ConfirmationModalProps) {
const { chainId } = useActiveWeb3React() const { chainId } = useActiveWeb3React()
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
return ( const transactionBroadcast = !!hash
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90}>
{!attemptingTxn ? ( // waiting for user to confirm/reject tx _or_ showing info on a tx that has been broadcast
<Wrapper> if (attemptingTxn || transactionBroadcast) {
<Section> return (
<RowBetween> <Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90}>
<Text fontWeight={500} fontSize={20}>
{title}
</Text>
<CloseIcon onClick={onDismiss} />
</RowBetween>
{topContent()}
</Section>
<BottomSection gap="12px">{bottomContent()}</BottomSection>
</Wrapper>
) : (
<Wrapper> <Wrapper>
<Section> <Section>
<RowBetween> <RowBetween>
@@ -79,22 +72,23 @@ export default function ConfirmationModal({
<CloseIcon onClick={onDismiss} /> <CloseIcon onClick={onDismiss} />
</RowBetween> </RowBetween>
<ConfirmedIcon> <ConfirmedIcon>
{pendingConfirmation ? ( {transactionBroadcast ? (
<Loader size="90px" />
) : (
<ArrowUpCircle strokeWidth={0.5} size={90} color={theme.primary1} /> <ArrowUpCircle strokeWidth={0.5} size={90} color={theme.primary1} />
) : (
<CustomLightSpinner src={Circle} alt="loader" size={'90px'} />
)} )}
</ConfirmedIcon> </ConfirmedIcon>
<AutoColumn gap="12px" justify={'center'}> <AutoColumn gap="12px" justify={'center'}>
<Text fontWeight={500} fontSize={20}> <Text fontWeight={500} fontSize={20}>
{!pendingConfirmation ? 'Transaction Submitted' : 'Waiting For Confirmation'} {transactionBroadcast ? 'Transaction Submitted' : 'Waiting For Confirmation'}
</Text> </Text>
<AutoColumn gap="12px" justify={'center'}> <AutoColumn gap="12px" justify={'center'}>
<Text fontWeight={600} fontSize={14} color="" textAlign="center"> <Text fontWeight={600} fontSize={14} color="" textAlign="center">
{pendingText} {pendingText}
</Text> </Text>
</AutoColumn> </AutoColumn>
{!pendingConfirmation && (
{transactionBroadcast ? (
<> <>
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}> <ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>
<Text fontWeight={500} fontSize={14} color={theme.primary1}> <Text fontWeight={500} fontSize={14} color={theme.primary1}>
@@ -107,9 +101,7 @@ export default function ConfirmationModal({
</Text> </Text>
</ButtonPrimary> </ButtonPrimary>
</> </>
)} ) : (
{pendingConfirmation && (
<Text fontSize={12} color="#565A69" textAlign="center"> <Text fontSize={12} color="#565A69" textAlign="center">
Confirm this transaction in your wallet Confirm this transaction in your wallet
</Text> </Text>
@@ -117,7 +109,25 @@ export default function ConfirmationModal({
</AutoColumn> </AutoColumn>
</Section> </Section>
</Wrapper> </Wrapper>
)} </Modal>
)
}
// confirmation screen
return (
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90}>
<Wrapper>
<Section>
<RowBetween>
<Text fontWeight={500} fontSize={20}>
{title}
</Text>
<CloseIcon onClick={onDismiss} />
</RowBetween>
{topContent()}
</Section>
<BottomSection gap="12px">{bottomContent()}</BottomSection>
</Wrapper>
</Modal> </Modal>
) )
} }

View File

@@ -1,12 +1,12 @@
import { Pair, Token } from '@uniswap/sdk' import { Pair, Token } from '@uniswap/sdk'
import React, { useState, useContext } 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 { Field } from '../../state/swap/actions'
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks' import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import TokenSearchModal from '../SearchModal/TokenSearchModal'
import TokenLogo from '../TokenLogo' import TokenLogo from '../TokenLogo'
import DoubleLogo from '../DoubleLogo' import DoubleLogo from '../DoubleLogo'
import SearchModal from '../SearchModal'
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'
@@ -49,7 +49,6 @@ const LabelRow = styled.div`
font-size: 0.75rem; font-size: 0.75rem;
line-height: 1rem; line-height: 1rem;
padding: 0.75rem 1rem 0 1rem; padding: 0.75rem 1rem 0 1rem;
height: 20px;
span:hover { span:hover {
cursor: pointer; cursor: pointer;
color: ${({ theme }) => darken(0.2, theme.text2)}; color: ${({ theme }) => darken(0.2, theme.text2)};
@@ -133,6 +132,7 @@ interface CurrencyInputPanelProps {
showSendWithSwap?: boolean showSendWithSwap?: boolean
otherSelectedTokenAddress?: string | null otherSelectedTokenAddress?: string | null
id: string id: string
showCommonBases?: boolean
} }
export default function CurrencyInputPanel({ export default function CurrencyInputPanel({
@@ -151,7 +151,8 @@ export default function CurrencyInputPanel({
hideInput = false, hideInput = false,
showSendWithSwap = false, showSendWithSwap = false,
otherSelectedTokenAddress = null, otherSelectedTokenAddress = null,
id id,
showCommonBases
}: CurrencyInputPanelProps) { }: CurrencyInputPanelProps) {
const { t } = useTranslation() const { t } = useTranslation()
@@ -160,6 +161,10 @@ export default function CurrencyInputPanel({
const userTokenBalance = useTokenBalanceTreatingWETHasETH(account, token) const userTokenBalance = useTokenBalanceTreatingWETHasETH(account, token)
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
const handleDismissSearch = useCallback(() => {
setModalOpen(false)
}, [setModalOpen])
return ( return (
<InputPanel id={id}> <InputPanel id={id}>
<Container hideInput={hideInput}> <Container hideInput={hideInput}>
@@ -236,17 +241,15 @@ export default function CurrencyInputPanel({
</InputRow> </InputRow>
</Container> </Container>
{!disableTokenSelect && ( {!disableTokenSelect && (
<SearchModal <TokenSearchModal
isOpen={modalOpen} isOpen={modalOpen}
onDismiss={() => { onDismiss={handleDismissSearch}
setModalOpen(false)
}}
filterType="tokens"
onTokenSelect={onTokenSelection} onTokenSelect={onTokenSelection}
showSendWithSwap={showSendWithSwap} showSendWithSwap={showSendWithSwap}
hiddenToken={token?.address} hiddenToken={token?.address}
otherSelectedTokenAddress={otherSelectedTokenAddress} otherSelectedTokenAddress={otherSelectedTokenAddress}
otherSelectedText={field === Field.INPUT ? 'Selected as output' : 'Selected as input'} otherSelectedText={field === Field.INPUT ? 'Selected as output' : 'Selected as input'}
showCommonBases={showCommonBases}
/> />
)} )}
</InputPanel> </InputPanel>

View File

@@ -12,8 +12,8 @@ const TokenWrapper = styled.div<{ margin: boolean; sizeraw: number }>`
interface DoubleTokenLogoProps { interface DoubleTokenLogoProps {
margin?: boolean margin?: boolean
size?: number size?: number
a0: string a0?: string
a1: string a1?: string
} }
const HigherLogo = styled(TokenLogo)` const HigherLogo = styled(TokenLogo)`
@@ -27,8 +27,8 @@ const CoveredLogo = styled(TokenLogo)<{ sizeraw: number }>`
export default function DoubleTokenLogo({ a0, a1, size = 16, margin = false }: DoubleTokenLogoProps) { export default function DoubleTokenLogo({ a0, a1, size = 16, margin = false }: DoubleTokenLogoProps) {
return ( return (
<TokenWrapper sizeraw={size} margin={margin}> <TokenWrapper sizeraw={size} margin={margin}>
<HigherLogo address={a0} size={size.toString() + 'px'} /> {a0 && <HigherLogo address={a0} size={size.toString() + 'px'} />}
<CoveredLogo address={a1} size={size.toString() + 'px'} sizeraw={size} /> {a1 && <CoveredLogo address={a1} size={size.toString() + 'px'} sizeraw={size} />}
</TokenWrapper> </TokenWrapper>
) )
} }

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

@@ -0,0 +1,70 @@
import { stringify } from 'qs'
import React, { useCallback, useMemo } from 'react'
import { Link, useLocation } from 'react-router-dom'
import styled from 'styled-components'
import useParsedQueryString from '../../hooks/useParsedQueryString'
import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
const VersionLabel = styled.span<{ enabled: boolean }>`
padding: 0.35rem 0.6rem;
border-radius: 12px;
background: ${({ theme, enabled }) => (enabled ? theme.primary1 : 'none')};
color: ${({ theme, enabled }) => (enabled ? theme.white : theme.text1)};
font-size: 1rem;
font-weight: ${({ enabled }) => (enabled ? '500' : '400')};
:hover {
user-select: ${({ enabled }) => (enabled ? 'none' : 'initial')};
background: ${({ theme, enabled }) => (enabled ? theme.primary1 : 'none')};
color: ${({ theme, enabled }) => (enabled ? theme.white : theme.text1)};
}
`
interface VersionToggleProps extends React.ComponentProps<typeof Link> {
enabled: boolean
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const VersionToggle = styled(({ enabled, ...rest }: VersionToggleProps) => <Link {...rest} />)<VersionToggleProps>`
border-radius: 12px;
opacity: ${({ enabled }) => (enabled ? 1 : 0.5)};
cursor: ${({ enabled }) => (enabled ? 'pointer' : 'default')};
background: ${({ theme }) => theme.bg3};
color: ${({ theme }) => theme.primary1};
display: flex;
width: fit-content;
margin-left: 0.5rem;
text-decoration: none;
:hover {
text-decoration: none;
}
`
export default function VersionSwitch() {
const version = useToggledVersion()
const location = useLocation()
const query = useParsedQueryString()
const versionSwitchAvailable = location.pathname === '/swap' || location.pathname === '/send'
const toggleDest = useMemo(() => {
return versionSwitchAvailable
? {
...location,
search: `?${stringify({ ...query, use: version === Version.v1 ? undefined : Version.v1 })}`
}
: location
}, [location, query, version, versionSwitchAvailable])
const handleClick = useCallback(
e => {
if (!versionSwitchAvailable) e.preventDefault()
},
[versionSwitchAvailable]
)
return (
<VersionToggle enabled={versionSwitchAvailable} to={toggleDest} onClick={handleClick}>
<VersionLabel enabled={version === Version.v2 || !versionSwitchAvailable}>V2</VersionLabel>
<VersionLabel enabled={version === Version.v1 && versionSwitchAvailable}>V1</VersionLabel>
</VersionToggle>
)
}

View File

@@ -1,27 +1,25 @@
import { ChainId, WETH } from '@uniswap/sdk'
import React from 'react' import React from 'react'
import { Link as HistoryLink } from 'react-router-dom' import { isMobile } from 'react-device-detect'
import { Text } from 'rebass'
import styled from 'styled-components' import styled from 'styled-components'
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import Row from '../Row'
import Menu from '../Menu'
import Web3Status from '../Web3Status'
import { ExternalLink } from '../../theme'
import { Text } from 'rebass'
import { WETH, ChainId } from '@uniswap/sdk'
import { isMobile } from 'react-device-detect'
import { YellowCard } from '../Card'
import { useActiveWeb3React } from '../../hooks'
import { useDarkModeManager } from '../../state/user/hooks'
import Logo from '../../assets/svg/logo.svg' import Logo from '../../assets/svg/logo.svg'
import Wordmark from '../../assets/svg/wordmark.svg'
import LogoDark from '../../assets/svg/logo_white.svg' import LogoDark from '../../assets/svg/logo_white.svg'
import Wordmark from '../../assets/svg/wordmark.svg'
import WordmarkDark from '../../assets/svg/wordmark_white.svg' import WordmarkDark from '../../assets/svg/wordmark_white.svg'
import { AutoColumn } from '../Column' import { useActiveWeb3React } from '../../hooks'
import { RowBetween } from '../Row' import { useDarkModeManager } from '../../state/user/hooks'
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import { YellowCard } from '../Card'
import Settings from '../Settings'
import Menu from '../Menu'
import Row, { RowBetween } from '../Row'
import Web3Status from '../Web3Status'
import VersionSwitch from './VersionSwitch'
const HeaderFrame = styled.div` const HeaderFrame = styled.div`
display: flex; display: flex;
@@ -31,15 +29,12 @@ const HeaderFrame = styled.div`
width: 100%; width: 100%;
top: 0; top: 0;
position: absolute; position: absolute;
z-index: 2;
pointer-events: none;
${({ theme }) => theme.mediaWidth.upToExtraSmall` ${({ theme }) => theme.mediaWidth.upToExtraSmall`
padding: 12px 0 0 0; padding: 12px 0 0 0;
width: calc(100%); width: calc(100%);
position: relative; position: relative;
`}; `};
z-index: 2;
` `
const HeaderElement = styled.div` const HeaderElement = styled.div`
@@ -47,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;
@@ -72,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;
@@ -82,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)`
@@ -95,59 +97,42 @@ 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;
`}; `};
` `
const VersionLabel = styled.span<{ isV2?: boolean }>` const NETWORK_LABELS: { [chainId in ChainId]: string | null } = {
padding: ${({ isV2 }) => (isV2 ? '0.15rem 0.5rem 0.16rem 0.45rem' : '0.15rem 0.5rem 0.16rem 0.35rem')}; [ChainId.MAINNET]: null,
border-radius: 14px; [ChainId.RINKEBY]: 'Rinkeby',
background: ${({ theme, isV2 }) => (isV2 ? theme.primary1 : 'none')}; [ChainId.ROPSTEN]: 'Ropsten',
color: ${({ theme, isV2 }) => (isV2 ? theme.white : theme.primary1)}; [ChainId.GÖRLI]: 'Görli',
font-size: 0.825rem; [ChainId.KOVAN]: 'Kovan'
font-weight: 400; }
:hover {
user-select: ${({ isV2 }) => (isV2 ? 'none' : 'initial')};
background: ${({ theme, isV2 }) => (isV2 ? theme.primary1 : 'none')};
color: ${({ theme, isV2 }) => (isV2 ? theme.white : theme.primary3)};
}
`
const VersionToggle = styled.a`
border-radius: 16px;
border: 1px solid ${({ theme }) => theme.primary1};
color: ${({ theme }) => theme.primary1};
display: flex;
width: fit-content;
cursor: pointer;
text-decoration: none;
:hover {
text-decoration: none;
}
`
export default function Header() { export default function Header() {
const { account, chainId } = useActiveWeb3React() const { account, chainId } = useActiveWeb3React()
@@ -157,63 +142,37 @@ export default function Header() {
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;
<ExternalLink href="https://migrate.uniswap.exchange/">
<b>migrate your liquidity </b>
</ExternalLink>
.
</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 && (
<VersionToggle target="_self" href="https://v1.uniswap.exchange">
<VersionLabel isV2={true}>V2</VersionLabel>
<VersionLabel isV2={false}>V1</VersionLabel>
</VersionToggle>
)}
</TestnetWrapper>
</HeaderElement> </HeaderElement>
<HeaderElement> <HeaderControls>
<TestnetWrapper> <HeaderElement>
{!isMobile && chainId === ChainId.ROPSTEN && <NetworkCard>Ropsten</NetworkCard>} <TestnetWrapper>
{!isMobile && chainId === ChainId.RINKEBY && <NetworkCard>Rinkeby</NetworkCard>} {!isMobile && NETWORK_LABELS[chainId] && <NetworkCard>{NETWORK_LABELS[chainId]}</NetworkCard>}
{!isMobile && chainId === ChainId.GÖRLI && <NetworkCard>Görli</NetworkCard>} </TestnetWrapper>
{!isMobile && chainId === ChainId.KOVAN && <NetworkCard>Kovan</NetworkCard>} <AccountElement active={!!account} style={{ pointerEvents: 'auto' }}>
</TestnetWrapper> {account && userEthBalance ? (
<AccountElement active={!!account} style={{ pointerEvents: 'auto' }}> <BalanceText style={{ flexShrink: 0 }} pl="0.75rem" pr="0.5rem" fontWeight={500}>
{account && userEthBalance ? ( {userEthBalance?.toSignificant(4)} ETH
<Text style={{ flexShrink: 0 }} px="0.5rem" fontWeight={500}> </BalanceText>
{userEthBalance?.toSignificant(4)} ETH ) : null}
</Text> <Web3Status />
) : null} </AccountElement>
<Web3Status /> </HeaderElement>
</AccountElement> <HeaderElementWrap>
<div style={{ pointerEvents: 'auto' }}> <VersionSwitch />
<Settings />
<Menu /> <Menu />
</div> </HeaderElementWrap>
</HeaderElement> </HeaderControls>
</RowBetween> </RowBetween>
</HeaderFrame> </HeaderFrame>
) )

View File

@@ -1,15 +1,38 @@
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled, { keyframes } from 'styled-components'
import { Spinner } from '../../theme' const rotate = keyframes`
import Circle from '../../assets/images/blue-loader.svg' from {
transform: rotate(0deg);
const SpinnerWrapper = styled(Spinner)<{ size: string }>` }
height: ${({ size }) => size}; to {
width: ${({ size }) => size}; transform: rotate(360deg);
}
` `
export default function Loader({ size }: { size: string }) { const StyledSVG = styled.svg<{ size: string; stroke?: string }>`
return <SpinnerWrapper src={Circle} alt="loader" size={size} /> animation: 2s ${rotate} linear infinite;
height: ${({ size }) => size};
width: ${({ size }) => size};
path {
stroke: ${({ stroke, theme }) => stroke ?? theme.primary1};
}
`
/**
* Takes in custom size and stroke for circle color, default to primary color as fill,
* need ...rest for layered styles on top
*/
export default function Loader({ size = '16px', stroke = null, ...rest }: { size?: string; stroke?: string }) {
return (
<StyledSVG viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" size={size} stroke={stroke} {...rest}>
<path
d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 9.27455 20.9097 6.80375 19.1414 5"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</StyledSVG>
)
} }

View File

@@ -77,9 +77,7 @@ const MenuItem = styled(ExternalLink)`
} }
` `
const CODE_LINK = !!process.env.REACT_APP_GIT_COMMIT_HASH const CODE_LINK = 'https://github.com/Uniswap/uniswap-interface'
? `https://github.com/Uniswap/uniswap-frontend/tree/${process.env.REACT_APP_GIT_COMMIT_HASH}`
: 'https://github.com/Uniswap/uniswap-frontend'
export default function Menu() { export default function Menu() {
const node = useRef<HTMLDivElement>() const node = useRef<HTMLDivElement>()
@@ -123,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

@@ -18,6 +18,7 @@ const StyledDialogOverlay = styled(({ mobile, ...rest }) => <AnimatedDialogOverl
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-color: transparent; background-color: transparent;
overflow: hidden;
${({ mobile }) => ${({ mobile }) =>
mobile && mobile &&
@@ -42,8 +43,10 @@ const StyledDialogOverlay = styled(({ mobile, ...rest }) => <AnimatedDialogOverl
// destructure to not pass custom props to Dialog DOM element // destructure to not pass custom props to Dialog DOM element
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...rest }) => ( const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...rest }) => (
<DialogContent aria-label="content" {...rest} /> <DialogContent {...rest} />
))` )).attrs({
'aria-label': 'dialog'
})`
&[data-reach-dialog-content] { &[data-reach-dialog-content] {
margin: 0 0 2rem 0; margin: 0 0 2rem 0;
border: 1px solid ${({ theme }) => theme.bg1}; border: 1px solid ${({ theme }) => theme.bg1};
@@ -67,12 +70,10 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...r
border-radius: 20px; border-radius: 20px;
${({ theme }) => theme.mediaWidth.upToMedium` ${({ theme }) => theme.mediaWidth.upToMedium`
width: 65vw; width: 65vw;
max-height: 65vh;
margin: 0; margin: 0;
`} `}
${({ theme, mobile }) => theme.mediaWidth.upToSmall` ${({ theme, mobile }) => theme.mediaWidth.upToSmall`
width: 85vw; width: 85vw;
max-height: 66vh;
${mobile && ${mobile &&
css` css`
width: 100vw; width: 100vw;
@@ -84,14 +85,6 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...r
} }
` `
const HiddenCloseButton = styled.button`
margin: 0;
padding: 0;
width: 0;
height: 0;
border: none;
`
interface ModalProps { interface ModalProps {
isOpen: boolean isOpen: boolean
onDismiss: () => void onDismiss: () => void
@@ -116,21 +109,13 @@ export default function Modal({
leave: { opacity: 0 } leave: { opacity: 0 }
}) })
const [{ xy }, set] = useSpring(() => ({ xy: [0, 0] })) 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({
xy: state.down ? state.movement : [0, 0], y: state.down ? state.movement[1] : 0
config: { mass: 1, tension: 210, friction: 20 }
}) })
if (velocity > 3 && state.direction[1] > 0) { if (state.velocity > 3 && state.direction[1] > 0) {
onDismiss() onDismiss()
} }
} }
@@ -149,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)'
@@ -161,18 +148,17 @@ export default function Modal({
<animated.div <animated.div
{...bind()} {...bind()}
style={{ style={{
transform: (xy as any).interpolate((x, y) => `translate3d(${0}px,${y > 0 ? y : 0}px,0)`) transform: y.interpolate(y => `translateY(${y > 0 ? y : 0}px)`)
}} }}
> >
<StyledDialogContent <StyledDialogContent
ariaLabel="test" aria-label="dialog content"
style={props} style={props}
hidden={true} hidden={true}
minHeight={minHeight} minHeight={minHeight}
maxHeight={maxHeight} maxHeight={maxHeight}
mobile={isMobile} mobile={isMobile}
> >
<HiddenCloseButton onClick={onDismiss} />
{children} {children}
</StyledDialogContent> </StyledDialogContent>
</animated.div> </animated.div>
@@ -189,15 +175,14 @@ export default function Modal({
{transitions.map( {transitions.map(
({ item, key, props }) => ({ item, key, props }) =>
item && ( item && (
<StyledDialogOverlay <StyledDialogOverlay key={key} style={props} onDismiss={onDismiss} initialFocusRef={initialFocusRef}>
key={key} <StyledDialogContent
style={props} aria-label="dialog content"
onDismiss={onDismiss} hidden={true}
initialFocusRef={initialFocusRef} minHeight={minHeight}
mobile={false} maxHeight={maxHeight}
> isOpen={isOpen}
<StyledDialogContent hidden={true} minHeight={minHeight} maxHeight={maxHeight} isOpen={isOpen}> >
<HiddenCloseButton onClick={onDismiss} />
{children} {children}
</StyledDialogContent> </StyledDialogContent>
</StyledDialogOverlay> </StyledDialogOverlay>

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,68 @@ 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 CreatePoolTabs() {
return (
<Tabs>
<RowBetween style={{ padding: '1rem' }}>
<HistoryLink to="/pool">
<StyledArrowLeft />
</HistoryLink>
<ActiveText>Create Pool</ActiveText>
<QuestionHelper text={'Use this interface to create a new pool.'} />
</RowBetween>
</Tabs>
)
}
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,69 @@
import React, { useContext } from 'react'
import { Link, RouteComponentProps, withRouter } from 'react-router-dom'
import { Token, TokenAmount, WETH } from '@uniswap/sdk'
import { Text } from 'rebass'
import { AutoColumn } from '../Column'
import { ButtonSecondary } from '../Button'
import { RowBetween, RowFixed } from '../Row'
import { FixedHeightRow, HoverCard } from './index'
import DoubleTokenLogo from '../DoubleLogo'
import { useActiveWeb3React } from '../../hooks'
import { ThemeContext } from 'styled-components'
interface PositionCardProps extends RouteComponentProps<{}> {
token: Token
V1LiquidityBalance: TokenAmount
}
function V1PositionCard({ token, V1LiquidityBalance }: PositionCardProps) {
const theme = useContext(ThemeContext)
const { chainId } = useActiveWeb3React()
return (
<HoverCard>
<AutoColumn gap="12px">
<FixedHeightRow>
<RowFixed>
<DoubleTokenLogo a0={token.address} margin={true} size={20} />
<Text fontWeight={500} fontSize={20} style={{ marginLeft: '' }}>
{`${token.equals(WETH[chainId]) ? 'WETH' : token.symbol}/ETH`}
</Text>
<Text
fontSize={12}
fontWeight={500}
ml="0.5rem"
px="0.75rem"
py="0.25rem"
style={{ borderRadius: '1rem' }}
backgroundColor={theme.yellow1}
color={'black'}
>
V1
</Text>
</RowFixed>
</FixedHeightRow>
<AutoColumn gap="8px">
<RowBetween marginTop="10px">
<ButtonSecondary width="68%" as={Link} to={`/migrate/v1/${V1LiquidityBalance.token.address}`}>
Migrate
</ButtonSecondary>
<ButtonSecondary
style={{ backgroundColor: 'transparent' }}
width="28%"
as={Link}
to={`/remove/v1/${V1LiquidityBalance.token.address}`}
>
Remove
</ButtonSecondary>
</RowBetween>
</AutoColumn>
</AutoColumn>
</HoverCard>
)
}
export default withRouter(V1PositionCard)

View File

@@ -1,41 +1,127 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { darken } from 'polished' import { darken } from 'polished'
import { RouteComponentProps, withRouter } from 'react-router-dom' import { Link } from 'react-router-dom'
import { Percent, Pair, JSBI } from '@uniswap/sdk' import { Percent, Pair, JSBI } from '@uniswap/sdk'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { useTotalSupply } from '../../data/TotalSupply' import { useTotalSupply } from '../../data/TotalSupply'
import { currencyId } from '../../pages/AddLiquidity/currencyId'
import { useTokenBalance } from '../../state/wallet/hooks' import { useTokenBalance } from '../../state/wallet/hooks'
import Card, { GreyCard } from '../Card' import Card, { GreyCard } from '../Card'
import TokenLogo from '../TokenLogo' import TokenLogo from '../TokenLogo'
import DoubleLogo from '../DoubleLogo' import DoubleLogo from '../DoubleLogo'
import { Text } from 'rebass' import { Text } from 'rebass'
import { ExternalLink } from '../../theme/components' import { ExternalLink } from '../../theme'
import { AutoColumn } from '../Column' import { AutoColumn } from '../Column'
import { ChevronDown, ChevronUp } from 'react-feather' import { ChevronDown, ChevronUp } from 'react-feather'
import { ButtonSecondary } from '../Button' import { ButtonSecondary } from '../Button'
import { RowBetween, RowFixed, AutoRow } from '../Row' import { RowBetween, RowFixed, AutoRow } from '../Row'
import { Dots } from '../swap/styleds'
const FixedHeightRow = styled(RowBetween)` export const FixedHeightRow = styled(RowBetween)`
height: 24px; height: 24px;
` `
const HoverCard = styled(Card)` export const HoverCard = styled(Card)`
border: 1px solid ${({ theme }) => theme.bg2}; border: 1px solid ${({ theme }) => theme.bg2};
:hover { :hover {
border: 1px solid ${({ theme }) => darken(0.06, theme.bg2)}; border: 1px solid ${({ theme }) => darken(0.06, theme.bg2)};
} }
` `
interface PositionCardProps extends RouteComponentProps<{}> { interface PositionCardProps {
pair: Pair pair: Pair | undefined | null
minimal?: boolean
border?: string border?: string
} }
function PositionCard({ pair, history, border, minimal = false }: PositionCardProps) { export function MinimalPositionCard({ pair, border }: PositionCardProps) {
const { account } = useActiveWeb3React()
const token0 = pair?.token0
const token1 = pair?.token1
const [showMore, setShowMore] = useState(false)
const userPoolBalance = useTokenBalance(account, 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(token0, totalPoolTokens, userPoolBalance, false),
pair.getLiquidityValue(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>
<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>
)}
</>
)
}
export default function FullPositionCard({ pair, border }: PositionCardProps) {
const { account } = useActiveWeb3React() const { account } = useActiveWeb3React()
const token0 = pair?.token0 const token0 = pair?.token0
@@ -63,174 +149,94 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
] ]
: [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"> <DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={20} />
<FixedHeightRow> <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>
{showMore && (
<AutoColumn gap="8px">
<FixedHeightRow>
<RowFixed>
<Text fontSize={16} fontWeight={500}>
Pooled {token0?.symbol}:
</Text>
</RowFixed>
{token0Deposited ? (
<RowFixed> <RowFixed>
<Text fontWeight={500} fontSize={16}> <Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
Your current position {token0Deposited?.toSignificant(6)}
</Text> </Text>
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token0?.address} />
</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>
{!minimal && <TokenLogo address={token0?.address} />}
<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>
{!minimal && <TokenLogo address={token1?.address} />}
<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?.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>
{!minimal && <TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token0?.address} />}
</RowFixed>
) : (
'-'
)}
</FixedHeightRow>
<FixedHeightRow> <FixedHeightRow>
<RowFixed>
<Text fontSize={16} fontWeight={500}>
Pooled {token1?.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>
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token1?.address} />
</RowFixed> </RowFixed>
{token1Deposited ? ( ) : (
<RowFixed> '-'
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
{token1Deposited?.toSignificant(6)}
</Text>
{!minimal && <TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token1?.address} />}
</RowFixed>
) : (
'-'
)}
</FixedHeightRow>
{!minimal && (
<FixedHeightRow>
<Text fontSize={16} fontWeight={500}>
Your pool tokens:
</Text>
<Text fontSize={16} fontWeight={500}>
{userPoolBalance ? userPoolBalance.toSignificant(4) : '-'}
</Text>
</FixedHeightRow>
)}
{!minimal && (
<FixedHeightRow>
<Text fontSize={16} fontWeight={500}>
Your pool share
</Text>
<Text fontSize={16} fontWeight={500}>
{poolTokenPercentage ? poolTokenPercentage.toFixed(2) + '%' : '-'}
</Text>
</FixedHeightRow>
)} )}
</FixedHeightRow>
<FixedHeightRow>
<Text fontSize={16} fontWeight={500}>
Your pool tokens:
</Text>
<Text fontSize={16} fontWeight={500}>
{userPoolBalance ? userPoolBalance.toSignificant(4) : '-'}
</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(token0)}/${currencyId(token1)}`} width="48%">
width="48%" Add
onClick={() => { </ButtonSecondary>
history.push('/add/' + token0?.address + '-' + token1?.address) <ButtonSecondary as={Link} width="48%" to={`/remove/${token0?.address}-${token1?.address}`}>
}} 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

@@ -1,39 +1,56 @@
import React from 'react' import React from 'react'
import { Text } from 'rebass' import { Text } from 'rebass'
import { COMMON_BASES } from '../../constants' import { ChainId, Token } from '@uniswap/sdk'
import styled from 'styled-components'
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 TokenLogo from '../TokenLogo'
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 selectedTokenAddress
}: { }: {
chainId: number chainId: ChainId
selectedTokenAddress: string selectedTokenAddress: string
onSelect: (tokenAddress: string) => void onSelect: (tokenAddress: string) => 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">
{COMMON_BASES[chainId]?.map(token => { {(SUGGESTED_BASES[chainId as ChainId] ?? []).map((token: Token) => {
return ( return (
<BaseWrapper <BaseWrapper
gap="6px"
onClick={() => selectedTokenAddress !== token.address && onSelect(token.address)} onClick={() => selectedTokenAddress !== token.address && onSelect(token.address)}
disable={selectedTokenAddress === token.address} disable={selectedTokenAddress === token.address}
key={token.address} key={token.address}
> >
<TokenLogo address={token.address} /> <TokenLogo address={token.address} style={{ marginRight: 8 }} />
<Text fontWeight={500} fontSize={16}> <Text fontWeight={500} fontSize={16}>
{token.symbol} {token.symbol}
</Text> </Text>

View File

@@ -1,64 +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={54}
height={500}
itemCount={pairs.length}
width="100%"
style={{ flex: '1', minHeight: 200 }}
>
{({ 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,24 +1,20 @@
import { ChainId, JSBI, Token, TokenAmount } from '@uniswap/sdk' import { JSBI, Token, TokenAmount } from '@uniswap/sdk'
import React, { useContext } from 'react' import React, { CSSProperties, memo, useContext, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FixedSizeList } from 'react-window' import { FixedSizeList } from 'react-window'
import { Text } from 'rebass' import { Text } from 'rebass'
import { ThemeContext } from 'styled-components' import { ThemeContext } from 'styled-components'
import Circle from '../../assets/images/circle.svg'
import { ALL_TOKENS } from '../../constants/tokens'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { useAllTokens } from '../../hooks/Tokens'
import { useAddUserToken, useRemoveUserAddedToken } from '../../state/user/hooks'
import { LinkStyledButton, TYPE } from '../../theme' import { LinkStyledButton, TYPE } from '../../theme'
import { isAddress } from '../../utils'
import { ButtonSecondary } from '../Button' import { ButtonSecondary } from '../Button'
import Column, { AutoColumn } from '../Column' import Column, { AutoColumn } from '../Column'
import { RowFixed } from '../Row' import { RowFixed } from '../Row'
import TokenLogo from '../TokenLogo' import TokenLogo from '../TokenLogo'
import { FadedSpan, GreySpan, MenuItem, SpinnerWrapper, ModalInfo } from './styleds' import { FadedSpan, GreySpan, MenuItem, ModalInfo } from './styleds'
import Loader from '../Loader'
function isDefaultToken(tokenAddress: string, chainId?: number): boolean { import { isDefaultToken, isCustomAddedToken } from '../../utils'
const address = isAddress(tokenAddress)
return Boolean(chainId && address && ALL_TOKENS[chainId as ChainId]?.[tokenAddress])
}
export default function TokenList({ export default function TokenList({
tokens, tokens,
@@ -27,14 +23,12 @@ export default function TokenList({
onTokenSelect, onTokenSelect,
otherToken, otherToken,
showSendWithSwap, showSendWithSwap,
onRemoveAddedToken,
otherSelectedText otherSelectedText
}: { }: {
tokens: Token[] tokens: Token[]
selectedToken: string selectedToken: string
allTokenBalances: { [tokenAddress: string]: TokenAmount } allTokenBalances: { [tokenAddress: string]: TokenAmount }
onTokenSelect: (tokenAddress: string) => void onTokenSelect: (tokenAddress: string) => void
onRemoveAddedToken: (chainId: number, tokenAddress: string) => void
otherToken: string otherToken: string
showSendWithSwap?: boolean showSendWithSwap?: boolean
otherSelectedText: string otherSelectedText: string
@@ -42,81 +36,121 @@ export default function TokenList({
const { t } = useTranslation() const { t } = useTranslation()
const { account, chainId } = useActiveWeb3React() const { account, chainId } = useActiveWeb3React()
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
const allTokens = useAllTokens()
const addToken = useAddUserToken()
const removeToken = useRemoveUserAddedToken()
const TokenRow = useMemo(() => {
return memo(function TokenRow({ index, style }: { index: number; style: CSSProperties }) {
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>
)
})
}, [
account,
addToken,
allTokenBalances,
allTokens,
chainId,
onTokenSelect,
otherSelectedText,
otherToken,
removeToken,
selectedToken,
showSendWithSwap,
theme.primary1,
tokens
])
if (tokens.length === 0) { if (tokens.length === 0) {
return <ModalInfo>{t('noToken')}</ModalInfo> return <ModalInfo>{t('noToken')}</ModalInfo>
} }
return ( return (
<FixedSizeList <FixedSizeList
width="100%" width="100%"
height={500} height={500}
itemCount={tokens.length} itemCount={tokens.length}
itemSize={50} itemSize={56}
style={{ flex: '1', minHeight: 200 }} style={{ flex: '1' }}
itemKey={index => tokens[index].address}
> >
{({ index, style }) => { {TokenRow}
const { address, symbol } = tokens[index]
const customAdded = !isDefaultToken(address, chainId)
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>
<TYPE.main fontWeight={500}>{customAdded && 'Added by user'}</TYPE.main>
{customAdded && (
<div
onClick={event => {
event.stopPropagation()
onRemoveAddedToken(chainId, address)
}}
>
<LinkStyledButton style={{ marginLeft: '4px', fontWeight: 400 }}>(Remove)</LinkStyledButton>
</div>
)}
</FadedSpan>
</Column>
</RowFixed>
<AutoColumn gap="4px" justify="end">
{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 ? (
<SpinnerWrapper src={Circle} alt="loader" />
) : (
'-'
)}
</AutoColumn>
</MenuItem>
)
}}
</FixedSizeList> </FixedSizeList>
) )
} }

View File

@@ -0,0 +1,209 @@
import { Token } from '@uniswap/sdk'
import React, { KeyboardEvent, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import { isMobile } from 'react-device-detect'
import { useTranslation } from 'react-i18next'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import Card from '../../components/Card'
import { useActiveWeb3React } from '../../hooks'
import { useAllTokens, useToken } from '../../hooks/Tokens'
import useInterval from '../../hooks/useInterval'
import { useAllTokenBalancesTreatingWETHasETH, useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import { CloseIcon, LinkStyledButton } from '../../theme'
import { isAddress } from '../../utils'
import Column from '../Column'
import Modal from '../Modal'
import QuestionHelper from '../QuestionHelper'
import { AutoRow, RowBetween } from '../Row'
import Tooltip from '../Tooltip'
import CommonBases from './CommonBases'
import { filterTokens } from './filtering'
import { useTokenComparator } from './sorting'
import { PaddedColumn, SearchInput } from './styleds'
import TokenList from './TokenList'
import SortButton from './SortButton'
interface TokenSearchModalProps {
isOpen?: boolean
onDismiss?: () => void
hiddenToken?: string
showSendWithSwap?: boolean
onTokenSelect?: (address: string) => void
otherSelectedTokenAddress?: string
otherSelectedText?: string
showCommonBases?: boolean
}
export default function TokenSearchModal({
isOpen,
onDismiss,
onTokenSelect,
hiddenToken,
showSendWithSwap,
otherSelectedTokenAddress,
otherSelectedText,
showCommonBases = false
}: TokenSearchModalProps) {
const { t } = useTranslation()
const { account, chainId } = useActiveWeb3React()
const theme = useContext(ThemeContext)
const [searchQuery, setSearchQuery] = useState<string>('')
const [tooltipOpen, setTooltipOpen] = useState<boolean>(false)
const [invertSearchOrder, setInvertSearchOrder] = useState<boolean>(false)
const allTokens = useAllTokens()
// 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 searchTokenBalance = useTokenBalanceTreatingWETHasETH(account, searchToken)
const allTokenBalances_ = useAllTokenBalancesTreatingWETHasETH()
const allTokenBalances = searchToken
? {
[searchToken.address]: searchTokenBalance
}
: allTokenBalances_ ?? {}
const tokenComparator = useTokenComparator(invertSearchOrder)
const filteredTokens: Token[] = useMemo(() => {
if (searchToken) return [searchToken]
return filterTokens(Object.values(allTokens), searchQuery)
}, [searchToken, allTokens, searchQuery])
const filteredSortedTokens: Token[] = useMemo(() => {
if (searchToken) return [searchToken]
const sorted = filteredTokens.sort(tokenComparator)
const symbolMatch = searchQuery
.toLowerCase()
.split(/\s+/)
.filter(s => s.length > 0)
if (symbolMatch.length > 1) return sorted
return [
...(searchToken ? [searchToken] : []),
// sort any exact symbol matches first
...sorted.filter(token => token.symbol.toLowerCase() === symbolMatch[0]),
...sorted.filter(token => token.symbol.toLowerCase() !== symbolMatch[0])
]
}, [filteredTokens, searchQuery, searchToken, tokenComparator])
const handleTokenSelect = useCallback(
(address: string) => {
onTokenSelect(address)
onDismiss()
},
[onDismiss, onTokenSelect]
)
// clear the input on open
useEffect(() => {
if (isOpen) setSearchQuery('')
}, [isOpen, setSearchQuery])
// manage focus on modal show
const inputRef = useRef<HTMLInputElement>()
const handleInput = useCallback(event => {
const input = event.target.value
const checksummedInput = isAddress(input)
setSearchQuery(checksummedInput || input)
setTooltipOpen(false)
}, [])
const openTooltip = useCallback(() => {
setTooltipOpen(true)
}, [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
) {
handleTokenSelect(filteredSortedTokens[0].address)
}
}
},
[filteredSortedTokens, handleTokenSelect, searchQuery]
)
return (
<Modal
isOpen={isOpen}
onDismiss={onDismiss}
maxHeight={70}
initialFocusRef={isMobile ? undefined : inputRef}
minHeight={70}
>
<Column style={{ width: '100%' }}>
<PaddedColumn gap="14px">
<RowBetween>
<Text fontWeight={500} fontSize={16}>
Select a token
<QuestionHelper
disabled={tooltipOpen}
text="Find a token by searching for its name or symbol or by pasting its address below."
/>
</Text>
<CloseIcon onClick={onDismiss} />
</RowBetween>
<Tooltip
text="Import any token into your list by pasting the token address into the search field."
show={tooltipOpen}
placement="bottom"
>
<SearchInput
type="text"
id="token-search-input"
placeholder={t('tokenSearchPlaceholder')}
value={searchQuery}
ref={inputRef}
onChange={handleInput}
onFocus={closeTooltip}
onBlur={closeTooltip}
onKeyDown={handleEnter}
/>
</Tooltip>
{showCommonBases && (
<CommonBases chainId={chainId} onSelect={handleTokenSelect} selectedTokenAddress={hiddenToken} />
)}
<RowBetween>
<Text fontSize={14} fontWeight={500}>
Token Name
</Text>
<SortButton ascending={invertSearchOrder} toggleSortOrder={() => setInvertSearchOrder(iso => !iso)} />
</RowBetween>
</PaddedColumn>
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
<TokenList
tokens={filteredSortedTokens}
allTokenBalances={allTokenBalances}
onTokenSelect={handleTokenSelect}
otherSelectedText={otherSelectedText}
otherToken={otherSelectedTokenAddress}
selectedToken={hiddenToken}
showSendWithSwap={showSendWithSwap}
/>
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
<Card>
<AutoRow justify={'center'}>
<div>
<LinkStyledButton style={{ fontWeight: 500, color: theme.text2, fontSize: 16 }} onClick={openTooltip}>
Having trouble finding a token?
</LinkStyledButton>
</div>
</AutoRow>
</Card>
</Column>
</Modal>
)
}

View File

@@ -10,12 +10,22 @@ export function filterTokens(tokens: Token[], search: string): Token[] {
return tokens.filter(token => token.address === searchingAddress) return tokens.filter(token => token.address === searchingAddress)
} }
const lowerSearchParts = searchingAddress ? [] : search.toLowerCase().split(/\s+/) const lowerSearchParts = search
.toLowerCase()
.split(/\s+/)
.filter(s => s.length > 0)
if (lowerSearchParts.length === 0) {
return tokens
}
const matchesSearch = (s: string): boolean => { const matchesSearch = (s: string): boolean => {
const sParts = s.toLowerCase().split(/\s+/) const sParts = s
.toLowerCase()
.split(/\s+/)
.filter(s => s.length > 0)
return lowerSearchParts.every(p => p.length === 0 || sParts.some(sp => sp.startsWith(p))) return lowerSearchParts.every(p => p.length === 0 || sParts.some(sp => sp.startsWith(p) || sp.endsWith(p)))
} }
return tokens.filter(token => { return tokens.filter(token => {

View File

@@ -1,226 +0,0 @@
import { Pair, Token } 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, useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
import { useAllDummyPairs, useRemoveUserAddedToken } from '../../state/user/hooks'
import { useAllTokenBalancesTreatingWETHasETH, useTokenBalances } from '../../state/wallet/hooks'
import { CloseIcon, LinkStyledButton, 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 Tooltip from '../Tooltip'
import CommonBases from './CommonBases'
import { filterPairs, filterTokens } from './filtering'
import PairList from './PairList'
import { balanceComparator, useTokenComparator } from './sorting'
import { PaddedColumn, SearchInput } from './styleds'
import TokenList from './TokenList'
import SortButton from './SortButton'
interface SearchModalProps extends RouteComponentProps {
isOpen?: boolean
onDismiss?: () => void
filterType?: 'tokens'
hiddenToken?: string
showSendWithSwap?: boolean
onTokenSelect?: (address: string) => void
otherSelectedTokenAddress?: string
otherSelectedText?: string
showCommonBases?: boolean
}
function SearchModal({
history,
isOpen,
onDismiss,
onTokenSelect,
filterType,
hiddenToken,
showSendWithSwap,
otherSelectedTokenAddress,
otherSelectedText,
showCommonBases = false
}: SearchModalProps) {
const { t } = useTranslation()
const { account, chainId } = useActiveWeb3React()
const theme = useContext(ThemeContext)
const isTokenView = filterType === 'tokens'
const allTokens = useAllTokens()
const allPairs = useAllDummyPairs()
const allTokenBalances = useAllTokenBalancesTreatingWETHasETH() ?? {}
const allPairBalances = useTokenBalances(
account,
allPairs.map(p => p.liquidityToken)
)
const [searchQuery, setSearchQuery] = useState<string>('')
const [tooltipOpen, setTooltipOpen] = useState<boolean>(false)
const [invertSearchOrder, setInvertSearchOrder] = useState<boolean>(false)
const removeTokenByAddress = useRemoveUserAddedToken()
// if the current input is an address, and we don't have the token in context, try to fetch it and import
useTokenByAddressAndAutomaticallyAdd(searchQuery)
const tokenComparator = useTokenComparator(invertSearchOrder)
const sortedTokens: Token[] = useMemo(() => {
if (!isTokenView) return []
return Object.values(allTokens).sort(tokenComparator)
}, [allTokens, isTokenView, tokenComparator])
const filteredTokens: Token[] = useMemo(() => {
if (!isTokenView) return []
return filterTokens(sortedTokens, searchQuery)
}, [isTokenView, sortedTokens, searchQuery])
function _onTokenSelect(address: string) {
onTokenSelect(address)
onDismiss()
}
// 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(() => {
if (isTokenView) return []
return allPairs.sort((a, b): number => {
// sort by balance
const balanceA = allPairBalances[a.liquidityToken.address]
const balanceB = allPairBalances[b.liquidityToken.address]
return balanceComparator(balanceA, balanceB)
})
}, [isTokenView, allPairs, allPairBalances])
const filteredPairs = useMemo(() => {
if (isTokenView) return []
return filterPairs(sortedPairList, searchQuery)
}, [isTokenView, 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]
const openTooltip = useCallback(() => {
setTooltipOpen(true)
inputRef.current?.focus()
}, [setTooltipOpen])
const closeTooltip = useCallback(() => setTooltipOpen(false), [setTooltipOpen])
return (
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={70} initialFocusRef={isMobile ? undefined : inputRef}>
<Column style={{ width: '100%' }}>
<PaddedColumn gap="20px">
<RowBetween>
<Text fontWeight={500} fontSize={16}>
{isTokenView ? 'Select a token' : 'Select a pool'}
<QuestionHelper
disabled={tooltipOpen}
text={
isTokenView
? 'Find a token by searching for its name or symbol or by pasting its address below.'
: 'Find a pair by searching for its name below.'
}
/>
</Text>
<CloseIcon onClick={onDismiss} />
</RowBetween>
<Tooltip
text="Import any token into your list by pasting the token address into the search field."
show={tooltipOpen}
placement="bottom"
>
<SearchInput
type={'text'}
id="token-search-input"
placeholder={t('tokenSearchPlaceholder')}
value={searchQuery}
ref={inputRef}
onChange={onInput}
onBlur={closeTooltip}
/>
</Tooltip>
{showCommonBases && (
<CommonBases chainId={chainId} onSelect={_onTokenSelect} selectedTokenAddress={hiddenToken} />
)}
<RowBetween>
<Text fontSize={14} fontWeight={500}>
{isTokenView ? 'Token Name' : 'Pool Name'}
</Text>
{isTokenView && (
<SortButton ascending={invertSearchOrder} toggleSortOrder={() => setInvertSearchOrder(iso => !iso)} />
)}
</RowBetween>
</PaddedColumn>
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
{isTokenView ? (
<TokenList
tokens={filteredTokens}
allTokenBalances={allTokenBalances}
onRemoveAddedToken={removeTokenByAddress}
onTokenSelect={_onTokenSelect}
otherSelectedText={otherSelectedText}
otherToken={otherSelectedTokenAddress}
selectedToken={hiddenToken}
showSendWithSwap={showSendWithSwap}
/>
) : (
<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>
{isTokenView ? (
<LinkStyledButton style={{ fontWeight: 500, color: theme.text2, fontSize: 16 }} onClick={openTooltip}>
Having trouble finding a token?
</LinkStyledButton>
) : (
<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(SearchModal)

View File

@@ -1,10 +1,11 @@
import { Token, TokenAmount, WETH } from '@uniswap/sdk' import { Token, TokenAmount, WETH, Pair } 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 { useAllTokenBalancesTreatingWETHasETH } 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
export function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount) { function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount) {
if (balanceA && balanceB) { if (balanceA && balanceB) {
return balanceA.greaterThan(balanceB) ? -1 : balanceA.equalTo(balanceB) ? 0 : 1 return balanceA.greaterThan(balanceB) ? -1 : balanceA.equalTo(balanceB) ? 0 : 1
} else if (balanceA && balanceA.greaterThan('0')) { } else if (balanceA && balanceA.greaterThan('0')) {
@@ -15,6 +16,26 @@ export 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 }

View File

@@ -1,7 +1,6 @@
import styled from 'styled-components' import styled from 'styled-components'
import { Spinner } from '../../theme'
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}
@@ -9,8 +8,8 @@ export const ModalInfo = styled.div`
padding: 1rem 1rem; padding: 1rem 1rem;
margin: 0.25rem 0.5rem; margin: 0.25rem 0.5rem;
justify-content: center; justify-content: center;
flex: 1;
user-select: none; user-select: none;
min-height: 200px;
` `
export const FadedSpan = styled(RowFixed)` export const FadedSpan = styled(RowFixed)`
@@ -23,12 +22,6 @@ export const GreySpan = styled.span`
font-weight: 400; font-weight: 400;
` `
export const SpinnerWrapper = styled(Spinner)`
margin: 0 0.25rem 0 0.25rem;
color: ${({ theme }) => theme.text4};
opacity: 0.6;
`
export const Input = styled.input` export const Input = styled.input`
position: relative; position: relative;
display: flex; display: flex;
@@ -57,12 +50,9 @@ export const PaddedColumn = styled(AutoColumn)`
padding-bottom: 12px; padding-bottom: 12px;
` `
const PaddedItem = styled(RowBetween)` export const MenuItem = styled(RowBetween)`
padding: 4px 20px; padding: 4px 20px;
height: 56px; height: 56px;
`
export const MenuItem = styled(PaddedItem)`
cursor: ${({ disabled }) => !disabled && 'pointer'}; cursor: ${({ disabled }) => !disabled && 'pointer'};
pointer-events: ${({ disabled }) => disabled && 'none'}; pointer-events: ${({ disabled }) => disabled && 'none'};
:hover { :hover {
@@ -71,21 +61,6 @@ export const MenuItem = styled(PaddedItem)`
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

@@ -0,0 +1,256 @@
import React, { useRef, useEffect, useContext, useState } from 'react'
import { Settings, X } from 'react-feather'
import styled from 'styled-components'
import {
useUserSlippageTolerance,
useExpertModeManager,
useUserDeadline,
useDarkModeManager
} from '../../state/user/hooks'
import SlippageTabs from '../SlippageTabs'
import { RowFixed, RowBetween } from '../Row'
import { TYPE } from '../../theme'
import QuestionHelper from '../QuestionHelper'
import Toggle from '../Toggle'
import { ThemeContext } from 'styled-components'
import { AutoColumn } from '../Column'
import { ButtonError } from '../Button'
import { useSettingsMenuOpen, useToggleSettingsMenu } from '../../state/application/hooks'
import { Text } from 'rebass'
import Modal from '../Modal'
const StyledMenuIcon = styled(Settings)`
height: 20px;
width: 20px;
> * {
stroke: ${({ theme }) => theme.text1};
}
`
const StyledCloseIcon = styled(X)`
height: 20px;
width: 20px;
:hover {
cursor: pointer;
}
> * {
stroke: ${({ theme }) => theme.text1};
}
`
const StyledMenuButton = styled.button`
position: relative;
width: 100%;
height: 100%;
border: none;
background-color: transparent;
margin: 0;
padding: 0;
height: 35px;
background-color: ${({ theme }) => theme.bg3};
padding: 0.15rem 0.5rem;
border-radius: 0.5rem;
:hover,
:focus {
cursor: pointer;
outline: none;
background-color: ${({ theme }) => theme.bg4};
}
svg {
margin-top: 2px;
}
`
const EmojiWrapper = styled.div`
position: absolute;
bottom: -6px;
right: 0px;
font-size: 14px;
`
const StyledMenu = styled.div`
margin-left: 0.5rem;
display: flex;
justify-content: center;
align-items: center;
position: relative;
border: none;
text-align: left;
`
const MenuFlyout = styled.span`
min-width: 20.125rem;
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),
0px 24px 32px rgba(0, 0, 0, 0.01);
border-radius: 0.5rem;
display: flex;
flex-direction: column;
font-size: 1rem;
position: absolute;
top: 3rem;
right: 0rem;
z-index: 100;
${({ theme }) => theme.mediaWidth.upToExtraSmall`
min-width: 18.125rem;
right: -46px;
`};
`
const Break = styled.div`
width: 100%;
height: 1px;
background-color: ${({ theme }) => theme.bg3};
`
const ModalContentWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
padding: 2rem 0;
background-color: ${({ theme }) => theme.bg2};
border-radius: 20px;
`
export default function SettingsTab() {
const node = useRef<HTMLDivElement>()
const open = useSettingsMenuOpen()
const toggle = useToggleSettingsMenu()
const theme = useContext(ThemeContext)
const [userSlippageTolerance, setUserslippageTolerance] = useUserSlippageTolerance()
const [deadline, setDeadline] = useUserDeadline()
const [expertMode, toggleExpertMode] = useExpertModeManager()
const [darkMode, toggleDarkMode] = useDarkModeManager()
// show confirmation view before turning on
const [showConfirmation, setShowConfirmation] = useState(false)
useEffect(() => {
const handleClickOutside = e => {
if (node.current?.contains(e.target) ?? false) {
return
}
toggle()
}
if (open) {
document.addEventListener('mousedown', handleClickOutside)
} else {
document.removeEventListener('mousedown', handleClickOutside)
}
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [open, toggle])
return (
<StyledMenu ref={node}>
<Modal isOpen={showConfirmation} onDismiss={() => setShowConfirmation(false)} maxHeight={100}>
<ModalContentWrapper>
<AutoColumn gap="lg">
<RowBetween style={{ padding: '0 2rem' }}>
<div />
<Text fontWeight={500} fontSize={20}>
Are you sure?
</Text>
<StyledCloseIcon onClick={() => setShowConfirmation(false)} />
</RowBetween>
<Break />
<AutoColumn gap="lg" style={{ padding: '0 2rem' }}>
<Text fontWeight={500} fontSize={20}>
Expert mode turns off the confirm transaction prompt and allows high slippage trades that often result
in bad rates and lost funds.
</Text>
<Text fontWeight={600} fontSize={20}>
ONLY USE THIS MODE IF YOU KNOW WHAT YOU ARE DOING.
</Text>
<ButtonError
error={true}
padding={'12px'}
onClick={() => {
if (window.prompt(`Please type the word "confirm" to enable expert mode.`) === 'confirm') {
toggleExpertMode()
setShowConfirmation(false)
}
}}
>
<Text fontSize={20} fontWeight={500}>
Turn On Expert Mode
</Text>
</ButtonError>
</AutoColumn>
</AutoColumn>
</ModalContentWrapper>
</Modal>
<StyledMenuButton onClick={toggle}>
<StyledMenuIcon />
{expertMode && (
<EmojiWrapper>
<span role="img" aria-label="wizard-icon">
🧙
</span>
</EmojiWrapper>
)}
</StyledMenuButton>
{open && (
<MenuFlyout>
<AutoColumn gap="md" style={{ padding: '1rem' }}>
<Text fontWeight={600} fontSize={14}>
Transaction Settings
</Text>
<SlippageTabs
rawSlippage={userSlippageTolerance}
setRawSlippage={setUserslippageTolerance}
deadline={deadline}
setDeadline={setDeadline}
/>
<Text fontWeight={600} fontSize={14}>
Interface Settings
</Text>
<RowBetween>
<RowFixed>
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
Toggle Expert Mode
</TYPE.black>
<QuestionHelper text="Bypasses confirmation modals and allows high slippage trades. Use at your own risk." />
</RowFixed>
<Toggle
isActive={expertMode}
toggle={
expertMode
? () => {
toggleExpertMode()
setShowConfirmation(false)
}
: () => {
toggle()
setShowConfirmation(true)
}
}
/>
</RowBetween>
<RowBetween>
<RowFixed>
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
Toggle Dark Mode
</TYPE.black>
</RowFixed>
<Toggle isActive={darkMode} toggle={toggleDarkMode} />
</RowBetween>
</AutoColumn>
</MenuFlyout>
)}
</StyledMenu>
)
}

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,8 +78,11 @@ const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean }
} }
` `
const SlippageSelector = styled.div` const SlippageEmojiContainer = styled.span`
padding: 0 20px; color: #f3841e;
${({ theme }) => theme.mediaWidth.upToSmall`
display: none;
`}
` `
export interface SlippageTabsProps { export interface SlippageTabsProps {
@@ -146,15 +149,14 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
} }
return ( return (
<> <AutoColumn gap="md">
<RowFixed padding={'0 20px'}> <AutoColumn gap="sm">
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}> <RowFixed>
Set slippage tolerance <TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
</TYPE.black> Slippage tolerance
<QuestionHelper text="Your transaction will revert if the price changes unfavorably by more than this percentage." /> </TYPE.black>
</RowFixed> <QuestionHelper text="Your transaction will revert if the price changes unfavorably by more than this percentage." />
</RowFixed>
<SlippageSelector>
<RowBetween> <RowBetween>
<Option <Option
onClick={() => { onClick={() => {
@@ -187,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}
@@ -220,16 +224,16 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
: 'Your transaction may be frontrun'} : 'Your transaction may be frontrun'}
</RowBetween> </RowBetween>
)} )}
</SlippageSelector> </AutoColumn>
<AutoColumn gap="sm"> <AutoColumn gap="sm">
<RowFixed padding={'0 20px'}> <RowFixed>
<TYPE.black fontSize={14} color={theme.text2}> <TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
Deadline Transaction deadline
</TYPE.black> </TYPE.black>
<QuestionHelper text="Your transaction will revert if it is pending for more than this long." /> <QuestionHelper text="Your transaction will revert if it is pending for more than this long." />
</RowFixed> </RowFixed>
<RowFixed padding={'0 20px'}> <RowFixed>
<OptionCustom style={{ width: '80px' }} tabIndex={-1}> <OptionCustom style={{ width: '80px' }} tabIndex={-1}>
<Input <Input
color={!!deadlineError ? 'red' : undefined} color={!!deadlineError ? 'red' : undefined}
@@ -246,6 +250,6 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
</TYPE.body> </TYPE.body>
</RowFixed> </RowFixed>
</AutoColumn> </AutoColumn>
</> </AutoColumn>
) )
} }

View File

@@ -0,0 +1,41 @@
import React from 'react'
import styled from 'styled-components'
const ToggleElement = styled.span<{ isActive?: boolean; isOnSwitch?: boolean }>`
padding: 0.25rem 0.5rem;
border-radius: 14px;
background: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.primary1 : theme.text4) : 'none')};
color: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.white : theme.text2) : theme.text3)};
font-size: 0.825rem;
font-weight: 400;
`
const StyledToggle = styled.a<{ isActive?: boolean; activeElement?: boolean }>`
border-radius: 16px;
border: 1px solid ${({ theme, isActive }) => (isActive ? theme.primary5 : theme.text4)};
display: flex;
width: fit-content;
cursor: pointer;
text-decoration: none;
:hover {
text-decoration: none;
}
`
export interface ToggleProps {
isActive: boolean
toggle: () => void
}
export default function Toggle({ isActive, toggle }: ToggleProps) {
return (
<StyledToggle isActive={isActive} target="_self" onClick={toggle}>
<ToggleElement isActive={isActive} isOnSwitch={true}>
On
</ToggleElement>
<ToggleElement isActive={!isActive} isOnSwitch={false}>
Off
</ToggleElement>
</StyledToggle>
)
}

View File

@@ -6,9 +6,9 @@ import { WETH } from '@uniswap/sdk'
import EthereumLogo from '../../assets/images/ethereum-logo.png' import EthereumLogo from '../../assets/images/ethereum-logo.png'
const TOKEN_ICON_API = address => const getTokenLogoURL = address =>
`https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${address}/logo.png` `https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${address}/logo.png`
const BAD_IMAGES = {} const NO_LOGO_ADDRESSES: { [tokenAddress: string]: true } = {}
const Image = styled.img<{ size: string }>` const Image = styled.img<{ size: string }>`
width: ${({ size }) => size}; width: ${({ size }) => size};
@@ -44,20 +44,16 @@ export default function TokenLogo({
size?: string size?: string
style?: React.CSSProperties style?: React.CSSProperties
}) { }) {
const [error, setError] = useState(false) const [, refresh] = useState<number>(0)
const { chainId } = useActiveWeb3React() const { chainId } = useActiveWeb3React()
// mock rinkeby DAI
if (chainId === 4 && address === '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735') {
address = '0x6B175474E89094C44Da98b954EedeAC495271d0F'
}
let path = '' let path = ''
const validated = isAddress(address)
// hard code to show ETH instead of WETH in UI // hard code to show ETH instead of WETH in UI
if (address === WETH[chainId].address) { if (validated === WETH[chainId].address) {
return <StyledEthereumLogo src={EthereumLogo} size={size} {...rest} /> return <StyledEthereumLogo src={EthereumLogo} size={size} {...rest} />
} else if (!error && !BAD_IMAGES[address] && isAddress(address)) { } else if (!NO_LOGO_ADDRESSES[address] && validated) {
path = TOKEN_ICON_API(address) path = getTokenLogoURL(validated)
} else { } else {
return ( return (
<Emoji {...rest} size={size}> <Emoji {...rest} size={size}>
@@ -75,8 +71,8 @@ export default function TokenLogo({
src={path} src={path}
size={size} size={size}
onError={() => { onError={() => {
BAD_IMAGES[address] = true NO_LOGO_ADDRESSES[address] = true
setError(true) refresh(i => i + 1)
}} }}
/> />
) )

View File

@@ -3,13 +3,12 @@ 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 { ALL_TOKENS } from '../../constants/tokens'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { useAllTokens } from '../../hooks/Tokens' import { useAllTokens } from '../../hooks/Tokens'
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 } 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 TokenLogo from '../TokenLogo'
@@ -18,11 +17,11 @@ 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)};
position: relative; position: relative;
padding: 1rem; padding: 1rem;
border: 0.5px solid ${({ theme, error }) => transparentize(0.4, error ? theme.red1 : theme.yellow1)}; /* border: 0.5px solid ${({ theme, error }) => transparentize(0.4, error ? theme.red1 : theme.yellow1)}; */
border-radius: 10px; border-radius: 10px;
margin-bottom: 20px; margin-bottom: 20px;
display: grid; display: grid;
grid-template-rows: auto auto auto; grid-template-rows: 14px auto auto;
grid-row-gap: 14px; grid-row-gap: 14px;
` `
@@ -42,15 +41,15 @@ const CloseColor = styled(Close)`
const CloseIcon = styled.div` const CloseIcon = styled.div`
position: absolute; position: absolute;
right: 1rem; right: 1rem;
top: 14px; top: 12px;
&:hover { &:hover {
cursor: pointer; cursor: pointer;
opacity: 0.6; opacity: 0.6;
} }
& > * { & > * {
height: 14px; height: 16px;
width: 14px; width: 16px;
} }
` `
@@ -68,9 +67,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 isDefaultToken = Boolean(
token && token.address && chainId && ALL_TOKENS[chainId] && ALL_TOKENS[chainId][token.address] const isDefault = isDefaultToken(token)
)
const tokenSymbol = token?.symbol?.toLowerCase() ?? '' const tokenSymbol = token?.symbol?.toLowerCase() ?? ''
const tokenName = token?.name?.toLowerCase() ?? '' const tokenName = token?.name?.toLowerCase() ?? ''
@@ -80,7 +78,7 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro
const allTokens = useAllTokens() const allTokens = useAllTokens()
const duplicateNameOrSymbol = useMemo(() => { const duplicateNameOrSymbol = useMemo(() => {
if (isDefaultToken || !token || !chainId) return false if (isDefault || !token || !chainId) return false
return Object.keys(allTokens).some(tokenAddress => { return Object.keys(allTokens).some(tokenAddress => {
const userToken = allTokens[tokenAddress] const userToken = allTokens[tokenAddress]
@@ -89,9 +87,9 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro
} }
return userToken.symbol.toLowerCase() === tokenSymbol || userToken.name.toLowerCase() === tokenName return userToken.symbol.toLowerCase() === tokenSymbol || userToken.name.toLowerCase() === tokenName
}) })
}, [isDefaultToken, token, chainId, allTokens, tokenSymbol, tokenName]) }, [isDefault, token, chainId, allTokens, tokenSymbol, tokenName])
if (isDefaultToken || !token || dismissed) return null if (isDefault || !token || dismissed) return null
return ( return (
<Wrapper error={duplicateNameOrSymbol} {...rest}> <Wrapper error={duplicateNameOrSymbol} {...rest}>
@@ -111,7 +109,7 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro
? `${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>

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useState } from 'react' import React, { useCallback, useContext, useState } from 'react'
import { AlertCircle, CheckCircle } from 'react-feather' import { AlertCircle, CheckCircle } from 'react-feather'
import styled from 'styled-components' import styled, { ThemeContext } from 'styled-components'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import useInterval from '../../hooks/useInterval' import useInterval from '../../hooks/useInterval'
@@ -51,17 +51,18 @@ export default function TxnPopup({
isRunning ? delay : null isRunning ? delay : null
) )
const handleMouseEnter = useCallback(() => setIsRunning(false), [])
const handleMouseLeave = useCallback(() => setIsRunning(true), [])
const theme = useContext(ThemeContext)
return ( return (
<AutoRow onMouseEnter={() => setIsRunning(false)} onMouseLeave={() => setIsRunning(true)}> <AutoRow onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
{success ? ( <div style={{ paddingRight: 16 }}>
<CheckCircle color={'#27AE60'} size={24} style={{ paddingRight: '24px' }} /> {success ? <CheckCircle color={theme.green1} size={24} /> : <AlertCircle color={theme.red1} size={24} />}
) : ( </div>
<AlertCircle color={'#FF6871'} size={24} style={{ paddingRight: '24px' }} />
)}
<AutoColumn gap="8px"> <AutoColumn gap="8px">
<TYPE.body fontWeight={500}> <TYPE.body fontWeight={500}>{summary ?? 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}</TYPE.body>
{summary ? summary : 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}
</TYPE.body>
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>View on Etherscan</ExternalLink> <ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>View on Etherscan</ExternalLink>
</AutoColumn> </AutoColumn>
<Fader count={count} /> <Fader count={count} />

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,11 +3,9 @@ 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 { Spinner } from '../../theme'
import Circle from '../../assets/images/circle.svg'
import { darken } from 'polished' import { darken } from 'polished'
import Loader from '../Loader'
const PendingSection = styled.div` const PendingSection = styled.div`
${({ theme }) => theme.flexColumnNoWrap}; ${({ theme }) => theme.flexColumnNoWrap};
@@ -19,14 +17,8 @@ const PendingSection = styled.div`
} }
` `
const SpinnerWrapper = styled(Spinner)` const StyledLoader = styled(Loader)`
font-size: 4rem;
margin-right: 1rem; margin-right: 1rem;
svg {
path {
color: ${({ theme }) => theme.bg4};
}
}
` `
const LoadingMessage = styled.div<{ error?: boolean }>` const LoadingMessage = styled.div<{ error?: boolean }>`
@@ -72,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 && <SpinnerWrapper src={Circle} />}
{error ? ( {error ? (
<ErrorGroup> <ErrorGroup>
<div>Error connecting.</div> <div>Error connecting.</div>
@@ -106,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,20 +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;
`
interface WalletConnectDataProps {
uri?: string
size: number
}
export default function WalletConnectData({ uri = '', size }: WalletConnectDataProps) {
return <QRCodeWrapper>{uri && <QRCode 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,9 +5,8 @@ import { useTranslation } from 'react-i18next'
import { network } from '../../connectors' import { network } from '../../connectors'
import { useEagerConnect, useInactiveListener } from '../../hooks' import { useEagerConnect, useInactiveListener } from '../../hooks'
import { Spinner } from '../../theme'
import Circle from '../../assets/images/circle.svg'
import { NetworkContextName } from '../../constants' import { NetworkContextName } from '../../constants'
import Loader from '../Loader'
const MessageWrapper = styled.div` const MessageWrapper = styled.div`
display: flex; display: flex;
@@ -20,16 +19,6 @@ const Message = styled.h2`
color: ${({ theme }) => theme.secondary1}; color: ${({ theme }) => theme.secondary1};
` `
const SpinnerWrapper = styled(Spinner)`
font-size: 4rem;
svg {
path {
color: ${({ theme }) => theme.secondary1};
}
}
`
export default function Web3ReactManager({ children }) { export default function Web3ReactManager({ children }) {
const { t } = useTranslation() const { t } = useTranslation()
const { active } = useWeb3React() const { active } = useWeb3React()
@@ -78,7 +67,7 @@ export default function Web3ReactManager({ children }) {
if (!active && !networkActive) { if (!active && !networkActive) {
return showLoader ? ( return showLoader ? (
<MessageWrapper> <MessageWrapper>
<SpinnerWrapper src={Circle} /> <Loader />
</MessageWrapper> </MessageWrapper>
) : null ) : null
} }

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'
@@ -16,18 +17,12 @@ import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg' import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg' import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
import { Spinner } from '../../theme'
import LightCircle from '../../assets/svg/lightcircle.svg'
import { RowBetween } from '../Row' import { RowBetween } from '../Row'
import { shortenAddress } from '../../utils' import { shortenAddress } from '../../utils'
import { useAllTransactions } from '../../state/transactions/hooks' import { useAllTransactions } from '../../state/transactions/hooks'
import { NetworkContextName } from '../../constants' import { NetworkContextName } from '../../constants'
import { injected, walletconnect, walletlink, fortmatic, portis } from '../../connectors' import { injected, walletconnect, walletlink, fortmatic, portis } from '../../connectors'
import Loader from '../Loader'
const SpinnerWrapper = styled(Spinner)`
margin: 0 0.25rem 0 0.25rem;
`
const IconWrapper = styled.div<{ size?: number }>` const IconWrapper = styled.div<{ size?: number }>`
${({ theme }) => theme.flexColumnNoWrap}; ${({ theme }) => theme.flexColumnNoWrap};
@@ -136,7 +131,7 @@ export default function Web3Status() {
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()
@@ -149,7 +144,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
@@ -189,10 +184,12 @@ export default function Web3Status() {
<Web3StatusConnected id="web3-status-connected" onClick={toggleWalletModal} pending={hasPendingTransactions}> <Web3StatusConnected id="web3-status-connected" onClick={toggleWalletModal} pending={hasPendingTransactions}>
{hasPendingTransactions ? ( {hasPendingTransactions ? (
<RowBetween> <RowBetween>
<Text>{pending?.length} Pending</Text> <SpinnerWrapper src={LightCircle} alt="loader" /> <Text>{pending?.length} Pending</Text> <Loader stroke="white" />
</RowBetween> </RowBetween>
) : ( ) : (
<Text>{ENSName || shortenAddress(account)}</Text> <Text>
{hasSocks ? '🧦' : ''} {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 { ChevronUp, ChevronRight } from 'react-feather'
import { Text, 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 { CursorPointer, TYPE } from '../../theme' import { useUserSlippageTolerance } from '../../state/user/hooks'
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 SlippageTabs, { SlippageTabsProps } from '../SlippageTabs'
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'
function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippage: number }) { function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippage: number }) {
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
@@ -61,79 +58,37 @@ function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippag
</TYPE.black> </TYPE.black>
</RowBetween> </RowBetween>
</AutoColumn> </AutoColumn>
<SectionBreak />
</> </>
) )
} }
export interface AdvancedSwapDetailsProps extends SlippageTabsProps { export interface AdvancedSwapDetailsProps {
trade?: Trade trade?: Trade
onDismiss: () => void
} }
export function AdvancedSwapDetails({ trade, onDismiss, ...slippageTabProps }: AdvancedSwapDetailsProps) { export function AdvancedSwapDetails({ trade }: AdvancedSwapDetailsProps) {
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
const [allowedSlippage] = useUserSlippageTolerance()
const showRoute = trade?.route?.path?.length > 2
return ( return (
<AutoColumn gap="md"> <AutoColumn gap="md">
<CursorPointer> {trade && <TradeSummary trade={trade} allowedSlippage={allowedSlippage} />}
<RowBetween onClick={onDismiss} padding={'8px 20px'}> {showRoute && (
<Text fontSize={16} color={theme.text2} fontWeight={500} style={{ userSelect: 'none' }}> <>
Hide Advanced <SectionBreak />
</Text> <AutoColumn style={{ padding: '0 24px' }}>
<ChevronUp color={theme.text2} /> <RowFixed>
</RowBetween> <TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
</CursorPointer> Route
</TYPE.black>
<SectionBreak /> <QuestionHelper text="Routing through these tokens resulted in the best price for your trade." />
</RowFixed>
{trade && <TradeSummary trade={trade} allowedSlippage={slippageTabProps.rawSlippage} />} <SwapRoute trade={trade} />
</AutoColumn>
<SlippageTabs {...slippageTabProps} /> </>
{trade?.route?.path?.length > 2 && (
<AutoColumn style={{ padding: '0 20px' }}>
<RowFixed>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
Route
</TYPE.black>
<QuestionHelper text="Routing through these tokens resulted in the best price for your trade." />
</RowFixed>
<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"
>
{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

@@ -1,35 +1,30 @@
import React, { useContext } from 'react' import React from 'react'
import { ChevronDown } from 'react-feather' import styled from 'styled-components'
import { Text } from 'rebass' import useLast from '../../hooks/useLast'
import { ThemeContext } from 'styled-components'
import { CursorPointer } from '../../theme'
import { RowBetween } from '../Row'
import { AdvancedSwapDetails, AdvancedSwapDetailsProps } from './AdvancedSwapDetails' import { AdvancedSwapDetails, AdvancedSwapDetailsProps } from './AdvancedSwapDetails'
import { AdvancedDropdown } from './styleds'
export default function AdvancedSwapDetailsDropdown({ const AdvancedDetailsFooter = styled.div<{ show: boolean }>`
showAdvanced, padding-top: calc(16px + 2rem);
setShowAdvanced, padding-bottom: 20px;
...rest margin-top: -2rem;
}: Omit<AdvancedSwapDetailsProps, 'onDismiss'> & { width: 100%;
showAdvanced: boolean max-width: 400px;
setShowAdvanced: (showAdvanced: boolean) => void border-bottom-left-radius: 20px;
}) { border-bottom-right-radius: 20px;
const theme = useContext(ThemeContext) color: ${({ theme }) => theme.text2};
background-color: ${({ theme }) => theme.advancedBG};
z-index: -1;
transform: ${({ show }) => (show ? 'translateY(0%)' : 'translateY(-100%)')};
transition: transform 300ms ease-in-out;
`
export default function AdvancedSwapDetailsDropdown({ trade, ...rest }: AdvancedSwapDetailsProps) {
const lastTrade = useLast(trade)
return ( return (
<AdvancedDropdown> <AdvancedDetailsFooter show={Boolean(trade)}>
{showAdvanced ? ( <AdvancedSwapDetails {...rest} trade={trade ?? lastTrade} />
<AdvancedSwapDetails {...rest} onDismiss={() => setShowAdvanced(false)} /> </AdvancedDetailsFooter>
) : (
<CursorPointer>
<RowBetween onClick={() => setShowAdvanced(true)} padding={'8px 20px'} id="show-advanced">
<Text fontSize={16} fontWeight={500} style={{ userSelect: 'none' }}>
Show Advanced
</Text>
<ChevronDown color={theme.text2} />
</RowBetween>
</CursorPointer>
)}
</AdvancedDropdown>
) )
} }

View File

@@ -0,0 +1,40 @@
import { stringify } from 'qs'
import React, { useContext, useMemo } from 'react'
import { useLocation } from 'react-router'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import useParsedQueryString from '../../hooks/useParsedQueryString'
import { DEFAULT_VERSION, Version } from '../../hooks/useToggledVersion'
import { StyledInternalLink } from '../../theme'
import { YellowCard } from '../Card'
import { AutoColumn } from '../Column'
export default function BetterTradeLink({ version }: { version: Version }) {
const theme = useContext(ThemeContext)
const location = useLocation()
const search = useParsedQueryString()
const linkDestination = useMemo(() => {
return {
...location,
search: `?${stringify({
...search,
use: version !== DEFAULT_VERSION ? version : undefined
})}`
}
}, [location, search, version])
return (
<YellowCard style={{ marginTop: '12px', padding: '8px 4px' }}>
<AutoColumn gap="sm" justify="center" style={{ alignItems: 'center', textAlign: 'center' }}>
<Text lineHeight="145.23%;" fontSize={14} fontWeight={400} color={theme.text1}>
There is a better price for this trade on{' '}
<StyledInternalLink to={linkDestination}>
<b>Uniswap {version.toUpperCase()} </b>
</StyledInternalLink>
</Text>
</AutoColumn>
</YellowCard>
)
}

View File

@@ -1,30 +0,0 @@
import { Percent } from '@uniswap/sdk'
import React, { useContext } from 'react'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import { YellowCard } from '../Card'
import { AutoColumn } from '../Column'
import { RowBetween, RowFixed } from '../Row'
export function PriceSlippageWarningCard({ priceSlippage }: { priceSlippage: Percent }) {
const theme = useContext(ThemeContext)
return (
<YellowCard style={{ padding: '20px', paddingTop: '10px' }}>
<AutoColumn gap="md">
<RowBetween>
<RowFixed style={{ paddingTop: '8px' }}>
<span role="img" aria-label="warning">
</span>{' '}
<Text fontWeight={500} marginLeft="4px" color={theme.text1}>
Price Warning
</Text>
</RowFixed>
</RowBetween>
<Text lineHeight="145.23%;" fontSize={16} fontWeight={400} color={theme.text1}>
This trade will move the price by ~{priceSlippage.toFixed(2)}%.
</Text>
</AutoColumn>
</YellowCard>
)
}

View File

@@ -37,6 +37,11 @@ export default function SwapModalFooter({
confirmText: string confirmText: string
}) { }) {
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
if (!trade) {
return null
}
return ( return (
<> <>
<AutoColumn gap="0px"> <AutoColumn gap="0px">

View File

@@ -5,35 +5,39 @@ 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 TokenLogo from '../TokenLogo'
import { TruncatedText } from './styleds' import { TruncatedText } from './styleds'
export default function SwapModalHeader({ export default function SwapModalHeader({
formattedAmounts,
tokens, tokens,
formattedAmounts,
slippageAdjustedAmounts, slippageAdjustedAmounts,
priceImpactSeverity, priceImpactSeverity,
independentField independentField,
recipient
}: { }: {
formattedAmounts?: { [field in Field]?: string } tokens: { [field in Field]?: Token }
tokens?: { [field in Field]?: Token } formattedAmounts: { [field in Field]?: string }
slippageAdjustedAmounts?: { [field in Field]?: TokenAmount } slippageAdjustedAmounts: { [field in Field]?: TokenAmount }
priceImpactSeverity: number priceImpactSeverity: number
independentField: Field independentField: Field
recipient: string | null
}) { }) {
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
return ( return (
<AutoColumn gap={'md'} style={{ marginTop: '20px' }}> <AutoColumn gap={'md'} style={{ marginTop: '20px' }}>
<RowBetween align="flex-end"> <RowBetween align="flex-end">
<TruncatedText fontSize={24} fontWeight={500}> <TruncatedText fontSize={24} fontWeight={500}>
{!!formattedAmounts[Field.INPUT] && formattedAmounts[Field.INPUT]} {formattedAmounts[Field.INPUT]}
</TruncatedText> </TruncatedText>
<RowFixed gap="4px"> <RowFixed gap="4px">
<TokenLogo address={tokens[Field.INPUT]?.address} size={'24px'} /> <TokenLogo address={tokens[Field.INPUT]?.address} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}> <Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{tokens[Field.INPUT]?.symbol || ''} {tokens[Field.INPUT]?.symbol}
</Text> </Text>
</RowFixed> </RowFixed>
</RowBetween> </RowBetween>
@@ -42,12 +46,12 @@ export default function SwapModalHeader({
</RowFixed> </RowFixed>
<RowBetween align="flex-end"> <RowBetween align="flex-end">
<TruncatedText fontSize={24} fontWeight={500} color={priceImpactSeverity > 2 ? theme.red1 : ''}> <TruncatedText fontSize={24} fontWeight={500} color={priceImpactSeverity > 2 ? theme.red1 : ''}>
{!!formattedAmounts[Field.OUTPUT] && formattedAmounts[Field.OUTPUT]} {formattedAmounts[Field.OUTPUT]}
</TruncatedText> </TruncatedText>
<RowFixed gap="4px"> <RowFixed gap="4px">
<TokenLogo address={tokens[Field.OUTPUT]?.address} size={'24px'} /> <TokenLogo address={tokens[Field.OUTPUT]?.address} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}> <Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{tokens[Field.OUTPUT]?.symbol || ''} {tokens[Field.OUTPUT]?.symbol}
</Text> </Text>
</RowFixed> </RowFixed>
</RowBetween> </RowBetween>
@@ -56,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)} {tokens[Field.OUTPUT]?.symbol}
</b> </b>
{' or the transaction will revert.'} {' or the transaction will revert.'}
</TYPE.italic> </TYPE.italic>
@@ -70,6 +74,14 @@ export default function SwapModalHeader({
</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 TokenLogo from '../TokenLogo'
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 }}>
<TokenLogo address={token.address} 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 { Trade } from '@uniswap/sdk' import { Price, Token } 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'
@@ -7,20 +7,19 @@ import { ThemeContext } from 'styled-components'
import { StyledBalanceMaxMini } from './styleds' import { StyledBalanceMaxMini } from './styleds'
interface TradePriceProps { interface TradePriceProps {
trade?: Trade price?: Price
inputToken?: Token
outputToken?: Token
showInverted: boolean showInverted: boolean
setShowInverted: (showInverted: boolean) => void setShowInverted: (showInverted: boolean) => void
} }
export default function TradePrice({ trade, showInverted, setShowInverted }: TradePriceProps) { export default function TradePrice({ price, inputToken, outputToken, showInverted, setShowInverted }: TradePriceProps) {
const theme = useContext(ThemeContext) const theme = useContext(ThemeContext)
const inputToken = trade?.inputAmount?.token
const outputToken = trade?.outputAmount?.token
const price = showInverted const formattedPrice = showInverted ? price?.toSignificant(6) : price?.invert()?.toSignificant(6)
? trade?.executionPrice?.toSignificant(6)
: trade?.executionPrice?.invert()?.toSignificant(6)
const show = Boolean(inputToken && outputToken)
const label = showInverted const label = showInverted
? `${outputToken?.symbol} per ${inputToken?.symbol}` ? `${outputToken?.symbol} per ${inputToken?.symbol}`
: `${inputToken?.symbol} per ${outputToken?.symbol}` : `${inputToken?.symbol} per ${outputToken?.symbol}`
@@ -32,10 +31,16 @@ export default function TradePrice({ trade, showInverted, setShowInverted }: Tra
color={theme.text2} color={theme.text2}
style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }} style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }}
> >
{price && `${price} ${label}`} {show ? (
<StyledBalanceMaxMini onClick={() => setShowInverted(!showInverted)}> <>
<Repeat size={14} /> {formattedPrice ?? '-'} {label}
</StyledBalanceMaxMini> <StyledBalanceMaxMini onClick={() => setShowInverted(!showInverted)}>
<Repeat size={14} />
</StyledBalanceMaxMini>
</>
) : (
'-'
)}
</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,23 +0,0 @@
import React, { useContext } from 'react'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import { ExternalLink } from '../../theme'
import { YellowCard } from '../Card'
import { AutoColumn } from '../Column'
export default function V1TradeLink({ v1TradeLinkIfBetter }: { v1TradeLinkIfBetter: string }) {
const theme = useContext(ThemeContext)
return v1TradeLinkIfBetter ? (
<YellowCard style={{ marginTop: '12px', padding: '8px 4px' }}>
<AutoColumn gap="sm" justify="center" style={{ alignItems: 'center', textAlign: 'center' }}>
<Text lineHeight="145.23%;" fontSize={14} fontWeight={400} color={theme.text1}>
There is a better price for this trade on{' '}
<ExternalLink href={v1TradeLinkIfBetter}>
<b>Uniswap V1 </b>
</ExternalLink>
</Text>
</AutoColumn>
</YellowCard>
) : null
}

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,30 +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;
export const AdvancedDropdown = styled.div` }
padding-top: calc(10px + 2rem); `
padding-bottom: 10px; : null}
margin-top: -2rem;
width: 100%;
max-width: 400px;
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
color: ${({ theme }) => theme.text2};
background-color: ${({ theme }) => theme.advancedBG};
z-index: -1;
` `
export const SectionBreak = styled.div` export const SectionBreak = styled.div`
@@ -45,9 +33,15 @@ export const BottomGrouping = styled.div`
position: relative; position: relative;
` `
export const ErrorText = styled(Text)<{ severity?: 0 | 1 | 2 | 3 }>` export const ErrorText = styled(Text)<{ severity?: 0 | 1 | 2 | 3 | 4 }>`
color: ${({ theme, severity }) => color: ${({ theme, severity }) =>
severity === 3 ? theme.red1 : severity === 2 ? theme.yellow2 : severity === 1 ? theme.text1 : theme.green1}; severity === 3 || severity === 4
? theme.red1
: severity === 2
? theme.yellow2
: severity === 1
? theme.text1
: theme.green1};
` `
export const InputGroup = styled(AutoColumn)` export const InputGroup = styled(AutoColumn)`
@@ -65,7 +59,7 @@ export const StyledNumerical = styled(NumericalInput)`
color: ${({ theme }) => theme.text4}; color: ${({ theme }) => theme.text4};
} }
` `
export const StyledBalanceMaxMini = styled.button<{ active?: boolean }>` export const StyledBalanceMaxMini = styled.button`
height: 22px; height: 22px;
width: 22px; width: 22px;
background-color: ${({ theme }) => theme.bg2}; background-color: ${({ theme }) => theme.bg2};

View File

@@ -27,7 +27,7 @@ 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: POLLING_INTERVAL
}) })

View File

@@ -1,6 +1,10 @@
import { Interface } from '@ethersproject/abi' import { Interface } from '@ethersproject/abi'
import ERC20_ABI from './erc20.json' import ERC20_ABI from './erc20.json'
import ERC20_BYTES32_ABI from './erc20_bytes32.json'
const ERC20_INTERFACE = new Interface(ERC20_ABI) const ERC20_INTERFACE = new Interface(ERC20_ABI)
const ERC20_BYTES32_INTERFACE = new Interface(ERC20_BYTES32_ABI)
export default ERC20_INTERFACE export default ERC20_INTERFACE
export { ERC20_ABI, ERC20_BYTES32_INTERFACE, ERC20_BYTES32_ABI }

View File

@@ -3,56 +3,12 @@
"constant": true, "constant": true,
"inputs": [], "inputs": [],
"name": "name", "name": "name",
"outputs": [{ "name": "", "type": "bytes32" }], "outputs": [
"payable": false, {
"stateMutability": "view", "name": "",
"type": "function" "type": "bytes32"
}, }
{
"constant": false,
"inputs": [{ "name": "_spender", "type": "address" }, { "name": "_value", "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": "_from", "type": "address" },
{ "name": "_to", "type": "address" },
{ "name": "_value", "type": "uint256" }
], ],
"name": "transferFrom",
"outputs": [{ "name": "", "type": "bool" }],
"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": "_owner", "type": "address" }],
"name": "balanceOf",
"outputs": [{ "name": "balance", "type": "uint256" }],
"payable": false, "payable": false,
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
@@ -61,48 +17,14 @@
"constant": true, "constant": true,
"inputs": [], "inputs": [],
"name": "symbol", "name": "symbol",
"outputs": [{ "name": "", "type": "bytes32" }], "outputs": [
{
"name": "",
"type": "bytes32"
}
],
"payable": false, "payable": false,
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
},
{
"constant": false,
"inputs": [{ "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" }],
"name": "transfer",
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [{ "name": "_owner", "type": "address" }, { "name": "_spender", "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": "owner", "type": "address" },
{ "indexed": true, "name": "spender", "type": "address" },
{ "indexed": false, "name": "value", "type": "uint256" }
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": true, "name": "from", "type": "address" },
{ "indexed": true, "name": "to", "type": "address" },
{ "indexed": false, "name": "value", "type": "uint256" }
],
"name": "Transfer",
"type": "event"
} }
] ]

View File

@@ -0,0 +1,55 @@
[
{
"inputs": [
{
"internalType": "address",
"name": "_factoryV1",
"type": "address"
},
{
"internalType": "address",
"name": "_router",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "amountTokenMin",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountETHMin",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "migrate",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]

View File

@@ -0,0 +1,5 @@
import MIGRATOR_ABI from './migrator.json'
const MIGRATOR_ADDRESS = '0x16D4F26C15f3658ec65B1126ff27DD3dF2a2996b'
export { MIGRATOR_ADDRESS, MIGRATOR_ABI }

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

@@ -1,25 +1,77 @@
import { ChainId, JSBI, Percent, Token, WETH } from '@uniswap/sdk' import { ChainId, JSBI, Percent, Token, WETH, Pair, TokenAmount } from '@uniswap/sdk'
import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors' import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors'
import { COMP, DAI, MKR, USDC, USDT } from './tokens/mainnet'
export const ROUTER_ADDRESS = '0xf164fC0Ec4E93095b804a4795bBe1e041497b92a' export const ROUTER_ADDRESS = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'
// used for display in the default list when adding liquidity // a list of tokens by chain
export const COMMON_BASES = { 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') const WETH_ONLY: ChainTokenList = {
], [ChainId.MAINNET]: [WETH[ChainId.MAINNET]],
[ChainId.ROPSTEN]: [WETH[ChainId.ROPSTEN]], [ChainId.ROPSTEN]: [WETH[ChainId.ROPSTEN]],
[ChainId.RINKEBY]: [ [ChainId.RINKEBY]: [WETH[ChainId.RINKEBY]],
WETH[ChainId.RINKEBY],
new Token(ChainId.RINKEBY, '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735', 18, 'DAI', 'Dai Stablecoin')
],
[ChainId.GÖRLI]: [WETH[ChainId.GÖRLI]], [ChainId.GÖRLI]: [WETH[ChainId.GÖRLI]],
[ChainId.KOVAN]: [WETH[ChainId.KOVAN]] [ChainId.KOVAN]: [WETH[ChainId.KOVAN]]
} }
const MAINNET_WALLETS = { // 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
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
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[] } = {
[ChainId.MAINNET]: [
new Pair(
new TokenAmount(
new Token(ChainId.MAINNET, '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643', 8, 'cDAI', 'Compound Dai'),
'0'
),
new TokenAmount(
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 TESTNET_CAPABLE_WALLETS = {
INJECTED: { INJECTED: {
connector: injected, connector: injected,
name: 'Injected', name: 'Injected',
@@ -41,9 +93,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,
@@ -51,7 +103,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,
@@ -70,15 +123,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',
@@ -112,12 +156,13 @@ export const ONE_BIPS = new Percent(JSBI.BigInt(1), JSBI.BigInt(10000))
export const BIPS_BASE = JSBI.BigInt(10000) export const BIPS_BASE = JSBI.BigInt(10000)
// used for warning states // used for warning states
export const ALLOWED_PRICE_IMPACT_LOW: Percent = new Percent(JSBI.BigInt(100), BIPS_BASE) // 1% export const ALLOWED_PRICE_IMPACT_LOW: Percent = new Percent(JSBI.BigInt(100), BIPS_BASE) // 1%
export const ALLOWED_PRICE_IMPACT_MEDIUM: Percent = new Percent(JSBI.BigInt(500), BIPS_BASE) // 5% export const ALLOWED_PRICE_IMPACT_MEDIUM: Percent = new Percent(JSBI.BigInt(300), BIPS_BASE) // 3%
export const ALLOWED_PRICE_IMPACT_HIGH: Percent = new Percent(JSBI.BigInt(1000), BIPS_BASE) // 10% export const ALLOWED_PRICE_IMPACT_HIGH: Percent = new Percent(JSBI.BigInt(500), BIPS_BASE) // 5%
// if the price slippage exceeds this number, force the user to type 'confirm' to execute // if the price slippage exceeds this number, force the user to type 'confirm' to execute
export const PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN: Percent = new Percent(JSBI.BigInt(2500), BIPS_BASE) // 25% export const PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN: Percent = new Percent(JSBI.BigInt(1000), BIPS_BASE) // 10%
// for non expert mode disable swaps above this
export const BLOCKED_PRICE_IMPACT_NON_EXPERT: Percent = new Percent(JSBI.BigInt(1500), BIPS_BASE) // 15%
// 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 V1_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))

View File

@@ -1,5 +1,11 @@
import { Token, ChainId } from '@uniswap/sdk' import { Token, ChainId } from '@uniswap/sdk'
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')
export default [ export default [
new Token(ChainId.MAINNET, '0xB6eD7644C69416d67B522e20bC294A9a9B405B31', 8, '0xBTC', '0xBitcoin Token'), new Token(ChainId.MAINNET, '0xB6eD7644C69416d67B522e20bC294A9a9B405B31', 8, '0xBTC', '0xBitcoin Token'),
new Token(ChainId.MAINNET, '0xfC1E690f61EFd961294b3e1Ce3313fBD8aa4f85d', 18, 'aDAI', 'Aave Interest bearing DAI'), new Token(ChainId.MAINNET, '0xfC1E690f61EFd961294b3e1Ce3313fBD8aa4f85d', 18, 'aDAI', 'Aave Interest bearing DAI'),
@@ -7,19 +13,23 @@ export default [
new Token(ChainId.MAINNET, '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 9, 'AMPL', 'Ampleforth'), new Token(ChainId.MAINNET, '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 9, 'AMPL', 'Ampleforth'),
new Token(ChainId.MAINNET, '0xcD62b1C403fa761BAadFC74C525ce2B51780b184', 18, 'ANJ', 'Aragon Network Juror'), new Token(ChainId.MAINNET, '0xcD62b1C403fa761BAadFC74C525ce2B51780b184', 18, 'ANJ', 'Aragon Network Juror'),
new Token(ChainId.MAINNET, '0x960b236A07cf122663c4303350609A66A7B288C0', 18, 'ANT', 'Aragon Network Token'), 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, '0xBA11D00c5f74255f56a5E366F4F77f5A186d7f55', 18, 'BAND', 'BandToken'),
new Token(ChainId.MAINNET, '0x0D8775F648430679A709E98d2b0Cb6250d2887EF', 18, 'BAT', 'Basic Attention Token'), new Token(ChainId.MAINNET, '0x0D8775F648430679A709E98d2b0Cb6250d2887EF', 18, 'BAT', 'Basic Attention Token'),
new Token(ChainId.MAINNET, '0xba100000625a3754423978a60c9317c58a424e3D', 18, 'BAL', 'Balancer'),
new Token(ChainId.MAINNET, '0x107c4504cd79C5d2696Ea0030a8dD4e92601B82e', 18, 'BLT', 'Bloom Token'), new Token(ChainId.MAINNET, '0x107c4504cd79C5d2696Ea0030a8dD4e92601B82e', 18, 'BLT', 'Bloom Token'),
new Token(ChainId.MAINNET, '0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C', 18, 'BNT', 'Bancor Network Token'), new Token(ChainId.MAINNET, '0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C', 18, 'BNT', 'Bancor Network Token'),
new Token(ChainId.MAINNET, '0x0327112423F3A68efdF1fcF402F6c5CB9f7C33fd', 18, 'BTC++', 'PieDAO BTC++'), new Token(ChainId.MAINNET, '0x0327112423F3A68efdF1fcF402F6c5CB9f7C33fd', 18, 'BTC++', 'PieDAO BTC++'),
new Token(ChainId.MAINNET, '0x56d811088235F11C8920698a204A5010a788f4b3', 18, 'BZRX', 'bZx Protocol Token'),
new Token(ChainId.MAINNET, '0x4F9254C83EB525f9FCf346490bbb3ed28a81C667', 18, 'CELR', 'CelerToken'), new Token(ChainId.MAINNET, '0x4F9254C83EB525f9FCf346490bbb3ed28a81C667', 18, 'CELR', 'CelerToken'),
new Token(ChainId.MAINNET, '0xF5DCe57282A584D2746FaF1593d3121Fcac444dC', 8, 'cSAI', 'Compound Dai'), new Token(ChainId.MAINNET, '0xF5DCe57282A584D2746FaF1593d3121Fcac444dC', 8, 'cSAI', 'Compound Dai'),
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'), new Token(ChainId.MAINNET, '0x39AA39c021dfbaE8faC545936693aC917d5E7563', 8, 'cUSDC', 'Compound USD Coin'),
new Token(ChainId.MAINNET, '0xaaAEBE6Fe48E54f431b0C390CfaF0b017d09D42d', 4, 'CEL', 'Celsius'), new Token(ChainId.MAINNET, '0xaaAEBE6Fe48E54f431b0C390CfaF0b017d09D42d', 4, 'CEL', 'Celsius'),
new Token(ChainId.MAINNET, '0x06AF07097C9Eeb7fD685c692751D5C66dB49c215', 18, 'CHAI', 'Chai'), new Token(ChainId.MAINNET, '0x06AF07097C9Eeb7fD685c692751D5C66dB49c215', 18, 'CHAI', 'Chai'),
COMP,
new Token(ChainId.MAINNET, '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359', 18, 'SAI', 'Dai Stablecoin v1.0 (SAI)'), new Token(ChainId.MAINNET, '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359', 18, 'SAI', 'Dai Stablecoin v1.0 (SAI)'),
new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'), DAI,
new Token(ChainId.MAINNET, '0x0Cf0Ee63788A0849fE5297F3407f701E122cC023', 18, 'DATA', 'Streamr DATAcoin'), new Token(ChainId.MAINNET, '0x0Cf0Ee63788A0849fE5297F3407f701E122cC023', 18, 'DATA', 'Streamr DATAcoin'),
new Token(ChainId.MAINNET, '0xE0B7927c4aF23765Cb51314A0E0521A9645F0E2A', 9, 'DGD', 'DigixDAO'), new Token(ChainId.MAINNET, '0xE0B7927c4aF23765Cb51314A0E0521A9645F0E2A', 9, 'DGD', 'DigixDAO'),
new Token(ChainId.MAINNET, '0x4f3AfEC4E5a3F2A6a1A411DEF7D7dFe50eE057bF', 9, 'DGX', 'Digix Gold Token'), new Token(ChainId.MAINNET, '0x4f3AfEC4E5a3F2A6a1A411DEF7D7dFe50eE057bF', 9, 'DGX', 'Digix Gold Token'),
@@ -31,6 +41,7 @@ export default [
'Decentralized Insurance Protocol' 'Decentralized Insurance Protocol'
), ),
new Token(ChainId.MAINNET, '0xC0F9bD5Fa5698B6505F643900FFA515Ea5dF54A9', 18, 'DONUT', 'Donut'), 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, '0xF629cBd94d3791C9250152BD8dfBDF380E2a3B9c', 18, 'ENJ', 'Enjin Coin'),
new Token(ChainId.MAINNET, '0x06f65b8CfCb13a9FE37d836fE9708dA38Ecb29B2', 18, 'FAME', 'SAINT FAME: Genesis Shirt'), new Token(ChainId.MAINNET, '0x06f65b8CfCb13a9FE37d836fE9708dA38Ecb29B2', 18, 'FAME', 'SAINT FAME: Genesis Shirt'),
new Token(ChainId.MAINNET, '0x4946Fcea7C692606e8908002e55A582af44AC121', 18, 'FOAM', 'FOAM Token'), new Token(ChainId.MAINNET, '0x4946Fcea7C692606e8908002e55A582af44AC121', 18, 'FOAM', 'FOAM Token'),
@@ -59,9 +70,10 @@ export default [
new Token(ChainId.MAINNET, '0xd15eCDCF5Ea68e3995b2D0527A0aE0a3258302F8', 18, 'MCX', 'MachiX Token'), new Token(ChainId.MAINNET, '0xd15eCDCF5Ea68e3995b2D0527A0aE0a3258302F8', 18, 'MCX', 'MachiX Token'),
new Token(ChainId.MAINNET, '0xa3d58c4E56fedCae3a7c43A725aeE9A71F0ece4e', 18, 'MET', 'Metronome'), new Token(ChainId.MAINNET, '0xa3d58c4E56fedCae3a7c43A725aeE9A71F0ece4e', 18, 'MET', 'Metronome'),
new Token(ChainId.MAINNET, '0x80f222a749a2e18Eb7f676D371F19ad7EFEEe3b7', 18, 'MGN', 'Magnolia Token'), new Token(ChainId.MAINNET, '0x80f222a749a2e18Eb7f676D371F19ad7EFEEe3b7', 18, 'MGN', 'Magnolia Token'),
new Token(ChainId.MAINNET, '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', 18, 'MKR', 'Maker'), MKR,
new Token(ChainId.MAINNET, '0xec67005c4E498Ec7f55E092bd1d35cbC47C91892', 18, 'MLN', 'Melon Token'), new Token(ChainId.MAINNET, '0xec67005c4E498Ec7f55E092bd1d35cbC47C91892', 18, 'MLN', 'Melon Token'),
new Token(ChainId.MAINNET, '0x957c30aB0426e0C93CD8241E2c60392d08c6aC8e', 0, 'MOD', 'Modum 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, '0xB62132e35a6c13ee1EE0f84dC5d40bad8d815206', 18, 'NEXO', 'Nexo'),
new Token(ChainId.MAINNET, '0x1776e1F26f98b1A5dF9cD347953a26dd3Cb46671', 18, 'NMR', 'Numeraire'), new Token(ChainId.MAINNET, '0x1776e1F26f98b1A5dF9cD347953a26dd3Cb46671', 18, 'NMR', 'Numeraire'),
new Token(ChainId.MAINNET, '0x985dd3D42De1e256d09e1c10F112bCCB8015AD41', 18, 'OCEAN', 'OceanToken'), new Token(ChainId.MAINNET, '0x985dd3D42De1e256d09e1c10F112bCCB8015AD41', 18, 'OCEAN', 'OceanToken'),
@@ -77,7 +89,10 @@ export default [
new Token(ChainId.MAINNET, '0xF970b8E36e23F7fC3FD752EeA86f8Be8D83375A6', 18, 'RCN', 'Ripio Credit Network 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, '0x255Aa6DF07540Cb5d3d297f0D0D4D84cb52bc8e6', 18, 'RDN', 'Raiden Token'),
new Token(ChainId.MAINNET, '0x408e41876cCCDC0F92210600ef50372656052a38', 18, 'REN', 'Republic Token'), new Token(ChainId.MAINNET, '0x408e41876cCCDC0F92210600ef50372656052a38', 18, 'REN', 'Republic Token'),
new Token(ChainId.MAINNET, '0x1985365e9f78359a9B6AD760e32412f4a445E862', 18, 'REP', 'Reputation'), 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, 'REPv1', 'Augur v1 Reputation'),
new Token(ChainId.MAINNET, '0x9469D013805bFfB7D3DEBe5E7839237e535ec483', 18, 'RING', 'Darwinia Network Native Token'), 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, '0x607F4C5BB672230e8672085532f7e901544a7375', 9, 'RLC', 'iEx.ec Network Token'),
new Token(ChainId.MAINNET, '0xB4EFd85c19999D84251304bDA99E90B92300Bd93', 18, 'RPL', 'Rocket Pool'), new Token(ChainId.MAINNET, '0xB4EFd85c19999D84251304bDA99E90B92300Bd93', 18, 'RPL', 'Rocket Pool'),
@@ -89,6 +104,7 @@ export default [
new Token(ChainId.MAINNET, '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F', 18, 'SNX', 'Synthetix 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, '0x23B608675a2B2fB1890d3ABBd85c5775c51691d5', 18, 'SOCKS', 'Unisocks Edition 0'),
new Token(ChainId.MAINNET, '0x42d6622deCe394b54999Fbd73D108123806f6a18', 18, 'SPANK', 'SPANK'), 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, '0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC', 8, 'STORJ', 'StorjToken'),
new Token(ChainId.MAINNET, '0x57Ab1ec28D129707052df4dF418D58a2D46d5f51', 18, 'sUSD', 'Synth sUSD'), new Token(ChainId.MAINNET, '0x57Ab1ec28D129707052df4dF418D58a2D46d5f51', 18, 'sUSD', 'Synth sUSD'),
new Token(ChainId.MAINNET, '0x261EfCdD24CeA98652B9700800a13DfBca4103fF', 18, 'sXAU', 'Synth sXAU'), new Token(ChainId.MAINNET, '0x261EfCdD24CeA98652B9700800a13DfBca4103fF', 18, 'sXAU', 'Synth sXAU'),
@@ -104,10 +120,11 @@ export default [
new Token(ChainId.MAINNET, '0x0000000000085d4780B73119b644AE5ecd22b376', 18, 'TUSD', 'TrueUSD'), new Token(ChainId.MAINNET, '0x0000000000085d4780B73119b644AE5ecd22b376', 18, 'TUSD', 'TrueUSD'),
new Token(ChainId.MAINNET, '0x8400D94A5cb0fa0D041a3788e395285d61c9ee5e', 8, 'UBT', 'UniBright'), new Token(ChainId.MAINNET, '0x8400D94A5cb0fa0D041a3788e395285d61c9ee5e', 8, 'UBT', 'UniBright'),
new Token(ChainId.MAINNET, '0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828', 18, 'UMA', 'UMA Voting Token v1'), new Token(ChainId.MAINNET, '0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828', 18, 'UMA', 'UMA Voting Token v1'),
new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C'), USDC,
new Token(ChainId.MAINNET, '0xA4Bdb11dc0a2bEC88d24A3aa1E6Bb17201112eBe', 6, 'USDS', 'StableUSD'), new Token(ChainId.MAINNET, '0xA4Bdb11dc0a2bEC88d24A3aa1E6Bb17201112eBe', 6, 'USDS', 'StableUSD'),
new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'Tether USD'), USDT,
new Token(ChainId.MAINNET, '0xeb269732ab75A6fD61Ea60b06fE994cD32a83549', 18, 'USDx', 'dForce'), new Token(ChainId.MAINNET, '0xeb269732ab75A6fD61Ea60b06fE994cD32a83549', 18, 'USDx', 'dForce'),
new Token(ChainId.MAINNET, '0x9A48BD0EC040ea4f1D3147C025cd4076A2e71e3e', 18, 'USD++', 'PieDAO USD++'),
new Token(ChainId.MAINNET, '0x8f3470A7388c05eE4e7AF3d01D8C722b0FF52374', 18, 'VERI', 'Veritaseum'), new Token(ChainId.MAINNET, '0x8f3470A7388c05eE4e7AF3d01D8C722b0FF52374', 18, 'VERI', 'Veritaseum'),
new Token(ChainId.MAINNET, '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', 8, 'WBTC', 'Wrapped BTC'), new Token(ChainId.MAINNET, '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', 8, 'WBTC', 'Wrapped BTC'),
new Token(ChainId.MAINNET, '0x09fE5f0236F0Ea5D930197DCE254d77B04128075', 18, 'WCK', 'Wrapped CryptoKitties'), new Token(ChainId.MAINNET, '0x09fE5f0236F0Ea5D930197DCE254d77B04128075', 18, 'WCK', 'Wrapped CryptoKitties'),

View File

@@ -1,9 +1,17 @@
import { Interface } from '@ethersproject/abi' import { Interface } from '@ethersproject/abi'
import { ChainId } from '@uniswap/sdk'
import V1_EXCHANGE_ABI from './v1_exchange.json' import V1_EXCHANGE_ABI from './v1_exchange.json'
import V1_FACTORY_ABI from './v1_factory.json' import V1_FACTORY_ABI from './v1_factory.json'
const V1_FACTORY_ADDRESS = '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95' const V1_FACTORY_ADDRESSES: { [chainId in ChainId]: string } = {
[ChainId.MAINNET]: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95',
[ChainId.ROPSTEN]: '0x9c83dCE8CA20E9aAF9D3efc003b2ea62aBC08351',
[ChainId.RINKEBY]: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36',
[ChainId.GÖRLI]: '0x6Ce570d02D73d4c384b46135E87f8C592A8c86dA',
[ChainId.KOVAN]: '0xD3E51Ef092B2845f10401a0159B2B96e8B6c3D30'
}
const V1_FACTORY_INTERFACE = new Interface(V1_FACTORY_ABI) const V1_FACTORY_INTERFACE = new Interface(V1_FACTORY_ABI)
const V1_EXCHANGE_INTERFACE = new Interface(V1_EXCHANGE_ABI) const V1_EXCHANGE_INTERFACE = new Interface(V1_EXCHANGE_ABI)
export { V1_FACTORY_ADDRESS, V1_FACTORY_INTERFACE, V1_FACTORY_ABI, V1_EXCHANGE_INTERFACE, V1_EXCHANGE_ABI } export { V1_FACTORY_ADDRESSES, V1_FACTORY_INTERFACE, V1_FACTORY_ABI, V1_EXCHANGE_INTERFACE, V1_EXCHANGE_ABI }

View File

@@ -1,24 +1,43 @@
import { Token, TokenAmount, Pair } from '@uniswap/sdk' import { Token, TokenAmount, Pair } from '@uniswap/sdk'
import { useMemo } from 'react' import { useMemo } from 'react'
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { Interface } from '@ethersproject/abi'
import { usePairContract } from '../hooks/useContract' import { useMultipleContractSingleData } from '../state/multicall/hooks'
import { useSingleCallResult } from '../state/multicall/hooks'
const PAIR_INTERFACE = new Interface(IUniswapV2PairABI)
/* /*
* if loading, return undefined * if loading, return undefined
* if no pair created yet, return null * if no pair created yet, return null
* if pair already created (even if 0 reserves), return pair * if pair already created (even if 0 reserves), return pair
*/ */
export function usePair(tokenA?: Token, tokenB?: Token): undefined | Pair | null { export function usePairs(tokens: [Token | undefined, Token | undefined][]): (undefined | Pair | null)[] {
const pairAddress = tokenA && tokenB && !tokenA.equals(tokenB) ? Pair.getAddress(tokenA, tokenB) : undefined const pairAddresses = useMemo(
const contract = usePairContract(pairAddress, false) () =>
const { result: reserves, loading } = useSingleCallResult(contract, 'getReserves') tokens.map(([tokenA, tokenB]) => {
return tokenA && tokenB && !tokenA.equals(tokenB) ? Pair.getAddress(tokenA, tokenB) : undefined
}),
[tokens]
)
const results = useMultipleContractSingleData(pairAddresses, PAIR_INTERFACE, 'getReserves')
return useMemo(() => { return useMemo(() => {
if (loading || !tokenA || !tokenB) return undefined return results.map((result, i) => {
if (!reserves) return null const { result: reserves, loading } = result
const { reserve0, reserve1 } = reserves const tokenA = tokens[i][0]
const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA] const tokenB = tokens[i][1]
return new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString()))
}, [loading, reserves, tokenA, tokenB]) if (loading || !tokenA || !tokenB) return undefined
if (!reserves) return null
const { reserve0, reserve1 } = reserves
const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]
return new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString()))
})
}, [results, tokens])
}
export function usePair(tokenA?: Token, tokenB?: Token): undefined | Pair | null {
return usePairs([[tokenA, tokenB]])[0]
} }

View File

@@ -1,27 +1,26 @@
import { ChainId, JSBI, Pair, Percent, Route, Token, TokenAmount, Trade, TradeType, WETH } from '@uniswap/sdk' import { 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'
import { useV1FactoryContract } from '../hooks/useContract' import { useV1FactoryContract } from '../hooks/useContract'
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'
function useV1PairAddress(tokenAddress?: string): string | undefined { export function useV1ExchangeAddress(tokenAddress?: string): string | undefined {
const contract = useV1FactoryContract() const contract = useV1FactoryContract()
const inputs = useMemo(() => [tokenAddress], [tokenAddress]) const inputs = useMemo(() => [tokenAddress], [tokenAddress])
return useSingleCallResult(contract, 'getExchange', inputs)?.result?.[0] return useSingleCallResult(contract, 'getExchange', inputs)?.result?.[0]
} }
class MockV1Pair extends Pair { class MockV1Pair extends Pair {}
readonly isV1: true = true
}
function useMockV1Pair(token?: Token): MockV1Pair | undefined { function useMockV1Pair(token?: Token): MockV1Pair | undefined {
const isWETH = token?.equals(WETH[token?.chainId]) const isWETH: boolean = token && WETH[token.chainId] ? token.equals(WETH[token.chainId]) : false
// will only return an address on mainnet, and not for WETH const v1PairAddress = useV1ExchangeAddress(isWETH ? undefined : token?.address)
const v1PairAddress = useV1PairAddress(isWETH ? undefined : token?.address)
const tokenBalance = useTokenBalance(v1PairAddress, token) const tokenBalance = useTokenBalance(v1PairAddress, token)
const ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress ?? ''] const ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress ?? '']
@@ -30,42 +29,40 @@ function useMockV1Pair(token?: Token): MockV1Pair | undefined {
: undefined : undefined
} }
// returns ALL v1 exchange addresses
export function useAllV1ExchangeAddresses(): string[] {
const factory = useV1FactoryContract()
const exchangeCount = useSingleCallResult(factory, 'tokenCount')?.result
const parsedCount = parseInt(exchangeCount?.toString() ?? '0')
const indices = useMemo(() => [...Array(parsedCount).keys()].map(ix => [ix]), [parsedCount])
const data = useSingleContractMultipleData(factory, 'getTokenWithId', indices, NEVER_RELOAD)
return useMemo(() => data?.map(({ result }) => result?.[0])?.filter(x => x) ?? [], [data])
}
// returns all v1 exchange addresses in the user's token list // returns all v1 exchange addresses in the user's token list
export function useAllTokenV1ExchangeAddresses(): string[] { export function useAllTokenV1Exchanges(): { [exchangeAddress: string]: Token } {
const allTokens = useAllTokens() const allTokens = useAllTokens()
const factory = useV1FactoryContract() const factory = useV1FactoryContract()
const args = useMemo(() => Object.keys(allTokens).map(tokenAddress => [tokenAddress]), [allTokens]) const args = useMemo(() => Object.keys(allTokens).map(tokenAddress => [tokenAddress]), [allTokens])
const data = useSingleContractMultipleData(factory, 'getExchange', args, NEVER_RELOAD) const data = useSingleContractMultipleData(factory, 'getExchange', args, NEVER_RELOAD)
return useMemo(() => data?.map(({ result }) => result?.[0])?.filter(x => x) ?? [], [data]) return useMemo(
() =>
data?.reduce<{ [exchangeAddress: string]: Token }>((memo, { result }, ix) => {
if (result?.[0] && result[0] !== AddressZero) {
const token = allTokens[args[ix][0]]
memo[result[0]] = token
}
return memo
}, {}) ?? {},
[allTokens, args, data]
)
} }
// returns whether any of the tokens in the user's token list have liquidity on v1 // returns whether any of the tokens in the user's token list have liquidity on v1
export function useUserProbablyHasV1Liquidity(): boolean | undefined { export function useUserHasLiquidityInAllTokens(): boolean | undefined {
const exchangeAddresses = useAllTokenV1ExchangeAddresses()
const { account, chainId } = useActiveWeb3React() const { account, chainId } = useActiveWeb3React()
const fakeTokens = useMemo( const exchanges = useAllTokenV1Exchanges()
() => (chainId ? exchangeAddresses.map(address => new Token(chainId, address, 18, 'UNI-V1')) : []),
[chainId, exchangeAddresses] const fakeLiquidityTokens = useMemo(
() =>
chainId ? Object.keys(exchanges).map(address => new Token(chainId, address, 18, 'UNI-V1', 'Uniswap V1')) : [],
[chainId, exchanges]
) )
const balances = useTokenBalances(account ?? undefined, fakeTokens) const balances = useTokenBalances(account ?? undefined, fakeLiquidityTokens)
return useMemo( return useMemo(
() => () =>
@@ -77,24 +74,23 @@ export function useUserProbablyHasV1Liquidity(): boolean | undefined {
) )
} }
export function useV1TradeLinkIfBetter( /**
* Returns the trade to execute on V1 to go between input and output token
*/
export function useV1Trade(
isExactIn?: boolean, isExactIn?: boolean,
input?: Token, inputToken?: Token,
output?: Token, outputToken?: Token,
exactAmount?: TokenAmount, exactAmount?: TokenAmount
v2Trade?: Trade, ): Trade | undefined {
minimumDelta: Percent = new Percent('0')
): string | undefined {
const { chainId } = useActiveWeb3React() const { chainId } = useActiveWeb3React()
const isMainnet: boolean = chainId === ChainId.MAINNET
// get the mock v1 pairs // get the mock v1 pairs
const inputPair = useMockV1Pair(input) const inputPair = useMockV1Pair(inputToken)
const outputPair = useMockV1Pair(output) const outputPair = useMockV1Pair(outputToken)
const inputIsWETH = isMainnet && input?.equals(WETH[ChainId.MAINNET]) const inputIsWETH = (inputToken && chainId && WETH[chainId] && inputToken.equals(WETH[chainId])) ?? false
const outputIsWETH = isMainnet && output?.equals(WETH[ChainId.MAINNET]) const outputIsWETH = (outputToken && chainId && WETH[chainId] && outputToken.equals(WETH[chainId])) ?? false
// construct a direct or through ETH v1 route // construct a direct or through ETH v1 route
let pairs: Pair[] = [] let pairs: Pair[] = []
@@ -108,7 +104,7 @@ export function useV1TradeLinkIfBetter(
pairs = [inputPair, outputPair] pairs = [inputPair, outputPair]
} }
const route = input && pairs && pairs.length > 0 && new Route(pairs, input) const route = inputToken && pairs && pairs.length > 0 && new Route(pairs, inputToken)
let v1Trade: Trade | undefined let v1Trade: Trade | undefined
try { try {
v1Trade = v1Trade =
@@ -116,25 +112,53 @@ export function useV1TradeLinkIfBetter(
? 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 {}
return v1Trade
}
let v1HasBetterTrade = false export function getTradeVersion(trade?: Trade): Version | undefined {
if (v1Trade) { const isV1 = trade?.route?.pairs?.some(pair => pair instanceof MockV1Pair)
if (isExactIn) { if (isV1) return Version.v1
// discount the v1 output amount by minimumDelta if (isV1 === false) return Version.v2
const discountedV1Output = v1Trade?.outputAmount.multiply(new Percent('1').subtract(minimumDelta)) return undefined
// check if the discounted v1 amount is still greater than v2, short-circuiting if no v2 trade exists }
v1HasBetterTrade = !v2Trade || discountedV1Output.greaterThan(v2Trade.outputAmount)
} else { // returns the v1 exchange against which a trade should be executed
// inflate the v1 amount by minimumDelta export function useV1TradeExchangeAddress(trade: Trade | undefined): string | undefined {
const inflatedV1Input = v1Trade?.inputAmount.multiply(new Percent('1').add(minimumDelta)) const tokenAddress: string | undefined = useMemo(() => {
// check if the inflated v1 amount is still less than v2, short-circuiting if no v2 trade exists const tradeVersion = getTradeVersion(trade)
v1HasBetterTrade = !v2Trade || inflatedV1Input.lessThan(v2Trade.inputAmount) const isV1 = tradeVersion === Version.v1
} return isV1
? trade &&
WETH[trade.inputAmount.token.chainId] &&
trade.inputAmount.token.equals(WETH[trade.inputAmount.token.chainId])
? trade.outputAmount.token.address
: trade?.inputAmount?.token?.address
: undefined
}, [trade])
return useV1ExchangeAddress(tokenAddress)
}
const ZERO_PERCENT = new Percent('0')
const ONE_HUNDRED_PERCENT = new Percent('1')
// returns whether tradeB is better than tradeA by at least a threshold
export function isTradeBetter(
tradeA: Trade | undefined,
tradeB: Trade | undefined,
minimumDelta: Percent = ZERO_PERCENT
): boolean | undefined {
if (!tradeA || !tradeB) return undefined
if (
tradeA.tradeType !== tradeB.tradeType ||
!tradeA.inputAmount.token.equals(tradeB.inputAmount.token) ||
!tradeB.outputAmount.token.equals(tradeB.outputAmount.token)
) {
throw new Error('Trades are not comparable')
} }
return v1HasBetterTrade && input && output if (minimumDelta.equalTo(ZERO_PERCENT)) {
? `https://v1.uniswap.exchange/swap?inputCurrency=${inputIsWETH ? 'ETH' : input.address}&outputCurrency=${ return tradeA.executionPrice.lessThan(tradeB.executionPrice)
outputIsWETH ? 'ETH' : output.address } else {
}` return tradeA.executionPrice.raw.multiply(minimumDelta.add(ONE_HUNDRED_PERCENT)).lessThan(tradeB.executionPrice)
: undefined }
} }

View File

@@ -1,10 +1,13 @@
import { parseBytes32String } from '@ethersproject/strings'
import { ChainId, Token, WETH } from '@uniswap/sdk' import { ChainId, Token, WETH } from '@uniswap/sdk'
import { useEffect, useMemo } from 'react' import { useMemo } from 'react'
import { ALL_TOKENS } from '../constants/tokens' import { ALL_TOKENS } from '../constants/tokens'
import { useAddUserToken, useFetchTokenByAddress, useUserAddedTokens } from '../state/user/hooks' import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'
import { useUserAddedTokens } from '../state/user/hooks'
import { isAddress } from '../utils' import { isAddress } from '../utils'
import { useActiveWeb3React } from './index' import { useActiveWeb3React } from './index'
import { useBytes32TokenContract, useTokenContract } from './useContract'
export function useAllTokens(): { [address: string]: Token } { export function useAllTokens(): { [address: string]: Token } {
const { chainId } = useActiveWeb3React() const { chainId } = useActiveWeb3React()
@@ -35,36 +38,65 @@ export function useAllTokens(): { [address: string]: Token } {
}, [userAddedTokens, chainId]) }, [userAddedTokens, chainId])
} }
export function useToken(tokenAddress?: string): Token | undefined { // parse a name or symbol from a token response
const tokens = useAllTokens() const BYTES32_REGEX = /^0x[a-fA-F0-9]{64}$/
return useMemo(() => { function parseStringOrBytes32(str: string | undefined, bytes32: string | undefined, defaultValue: string): string {
const validatedAddress = isAddress(tokenAddress) return str && str.length > 0
if (!validatedAddress) return ? str
return tokens[validatedAddress] : bytes32 && BYTES32_REGEX.test(bytes32)
}, [tokens, tokenAddress]) ? parseBytes32String(bytes32)
: defaultValue
} }
// gets token information by address (typically user input) and // undefined if invalid or does not exist
// automatically adds it for the user if the token address is valid // null if loading
export function useTokenByAddressAndAutomaticallyAdd(tokenAddress?: string): Token | undefined { // otherwise returns the token
const fetchTokenByAddress = useFetchTokenByAddress() export function useToken(tokenAddress?: string): Token | undefined | null {
const addToken = useAddUserToken()
const token = useToken(tokenAddress)
const { chainId } = useActiveWeb3React() const { chainId } = useActiveWeb3React()
const tokens = useAllTokens()
useEffect(() => { const address = isAddress(tokenAddress)
if (!chainId || !isAddress(tokenAddress)) return
const weth = WETH[chainId as ChainId]
if (weth && weth.address === isAddress(tokenAddress)) return
if (tokenAddress && !token) { const tokenContract = useTokenContract(address ? address : undefined, false)
fetchTokenByAddress(tokenAddress).then(token => { const tokenContractBytes32 = useBytes32TokenContract(address ? address : undefined, false)
if (token !== null) { const token: Token | undefined = address ? tokens[address] : undefined
addToken(token)
} const tokenName = useSingleCallResult(token ? undefined : tokenContract, 'name', undefined, NEVER_RELOAD)
}) const tokenNameBytes32 = useSingleCallResult(
token ? undefined : tokenContractBytes32,
'name',
undefined,
NEVER_RELOAD
)
const symbol = useSingleCallResult(token ? undefined : tokenContract, 'symbol', undefined, NEVER_RELOAD)
const symbolBytes32 = useSingleCallResult(token ? undefined : tokenContractBytes32, 'symbol', undefined, NEVER_RELOAD)
const decimals = useSingleCallResult(token ? undefined : tokenContract, 'decimals', undefined, NEVER_RELOAD)
return useMemo(() => {
if (token) return token
if (!chainId || !address) return undefined
if (decimals.loading || symbol.loading || tokenName.loading) return null
if (decimals.result) {
return new Token(
chainId,
address,
decimals.result[0],
parseStringOrBytes32(symbol.result?.[0], symbolBytes32.result?.[0], 'UNKNOWN'),
parseStringOrBytes32(tokenName.result?.[0], tokenNameBytes32.result?.[0], 'Unknown Token')
)
} }
}, [tokenAddress, token, fetchTokenByAddress, addToken, chainId]) return undefined
}, [
return token address,
chainId,
decimals.loading,
decimals.result,
symbol.loading,
symbol.result,
symbolBytes32.result,
token,
tokenName.loading,
tokenName.result,
tokenNameBytes32.result
])
} }

View File

@@ -1,47 +1,47 @@
import { Pair, Token, TokenAmount, Trade } from '@uniswap/sdk'
import flatMap from 'lodash.flatmap'
import { useMemo } from 'react' import { useMemo } from 'react'
import { WETH, Token, TokenAmount, Trade, ChainId, Pair } from '@uniswap/sdk'
import { useActiveWeb3React } from './index'
import { usePair } from '../data/Reserves'
const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin') import { BASES_TO_CHECK_TRADES_AGAINST } from '../constants'
const USDC = new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C') import { usePairs } from '../data/Reserves'
import { useActiveWeb3React } from './index'
function useAllCommonPairs(tokenA?: Token, tokenB?: Token): Pair[] { function useAllCommonPairs(tokenA?: Token, tokenB?: Token): Pair[] {
const { chainId } = useActiveWeb3React() const { chainId } = useActiveWeb3React()
// check for direct pair between tokens const bases: Token[] = chainId ? BASES_TO_CHECK_TRADES_AGAINST[chainId] : []
const pairBetween = usePair(tokenA, tokenB)
// get token<->WETH pairs const allPairCombinations: [Token | undefined, Token | undefined][] = useMemo(
const aToETH = usePair(tokenA, WETH[chainId as ChainId]) () => [
const bToETH = usePair(tokenB, WETH[chainId as ChainId]) // the direct pair
[tokenA, tokenB],
// get token<->DAI pairs // token A against all bases
const aToDAI = usePair(tokenA, chainId === ChainId.MAINNET ? DAI : undefined) ...bases.map((base): [Token | undefined, Token | undefined] => [tokenA, base]),
const bToDAI = usePair(tokenB, chainId === ChainId.MAINNET ? DAI : undefined) // token B against all bases
...bases.map((base): [Token | undefined, Token | undefined] => [tokenB, base]),
// get token<->USDC pairs // each base against all bases
const aToUSDC = usePair(tokenA, chainId === ChainId.MAINNET ? USDC : undefined) ...flatMap(bases, (base): [Token, Token][] => bases.map(otherBase => [base, otherBase]))
const bToUSDC = usePair(tokenB, chainId === ChainId.MAINNET ? USDC : undefined) ],
[tokenA, tokenB, bases]
// get connecting pairs
const DAIToETH = usePair(chainId === ChainId.MAINNET ? DAI : undefined, WETH[chainId as ChainId])
const USDCToETH = usePair(chainId === ChainId.MAINNET ? USDC : undefined, WETH[chainId as ChainId])
const DAIToUSDC = usePair(
chainId === ChainId.MAINNET ? DAI : undefined,
chainId === ChainId.MAINNET ? USDC : undefined
) )
const allPairs = usePairs(allPairCombinations)
// only pass along valid pairs, non-duplicated pairs // only pass along valid pairs, non-duplicated pairs
return useMemo( return useMemo(
() => () =>
[pairBetween, aToETH, bToETH, aToDAI, bToDAI, aToUSDC, bToUSDC, DAIToETH, USDCToETH, DAIToUSDC] Object.values(
// filter out invalid pairs allPairs
.filter((p): p is Pair => !!p) // filter out invalid pairs
// filter out duplicated pairs .filter((p): p is Pair => !!p)
.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
[pairBetween, aToETH, bToETH, aToDAI, bToDAI, aToUSDC, bToUSDC, DAIToETH, USDCToETH, DAIToUSDC] return memo
}, {})
),
[allPairs]
) )
} }
@@ -49,14 +49,11 @@ 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(amountIn?: TokenAmount, tokenOut?: Token): Trade | null {
const inputToken = amountIn?.token const allowedPairs = useAllCommonPairs(amountIn?.token, tokenOut)
const outputToken = tokenOut
const allowedPairs = useAllCommonPairs(inputToken, outputToken)
return useMemo(() => { return useMemo(() => {
if (amountIn && tokenOut && allowedPairs.length > 0) { if (amountIn && tokenOut && allowedPairs.length > 0) {
return Trade.bestTradeExactIn(allowedPairs, amountIn, tokenOut)[0] ?? null return Trade.bestTradeExactIn(allowedPairs, amountIn, tokenOut, { maxHops: 3, maxNumResults: 1 })[0] ?? null
} }
return null return null
}, [allowedPairs, amountIn, tokenOut]) }, [allowedPairs, amountIn, tokenOut])
@@ -66,14 +63,11 @@ export function useTradeExactIn(amountIn?: TokenAmount, tokenOut?: Token): Trade
* 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(tokenIn?: Token, amountOut?: TokenAmount): Trade | null {
const inputToken = tokenIn const allowedPairs = useAllCommonPairs(tokenIn, amountOut?.token)
const outputToken = amountOut?.token
const allowedPairs = useAllCommonPairs(inputToken, outputToken)
return useMemo(() => { return useMemo(() => {
if (tokenIn && amountOut && allowedPairs.length > 0) { if (tokenIn && amountOut && allowedPairs.length > 0) {
return Trade.bestTradeExactOut(allowedPairs, tokenIn, amountOut)[0] ?? null return Trade.bestTradeExactOut(allowedPairs, tokenIn, amountOut, { maxHops: 3, maxNumResults: 1 })[0] ?? null
} }
return null return null
}, [allowedPairs, tokenIn, amountOut]) }, [allowedPairs, tokenIn, amountOut])

View File

@@ -1,11 +1,13 @@
import { Web3Provider } from '@ethersproject/providers' import { Web3Provider } from '@ethersproject/providers'
import { ChainId } from '@uniswap/sdk'
import { useWeb3React as useWeb3ReactCore } from '@web3-react/core' import { useWeb3React as useWeb3ReactCore } from '@web3-react/core'
import { Web3ReactContextInterface } from '@web3-react/core/dist/types'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { isMobile } from 'react-device-detect' import { isMobile } from 'react-device-detect'
import { injected } from '../connectors' import { injected } from '../connectors'
import { NetworkContextName } from '../constants' import { NetworkContextName } from '../constants'
export function useActiveWeb3React() { export function useActiveWeb3React(): Web3ReactContextInterface<Web3Provider> & { chainId?: ChainId } {
const context = useWeb3ReactCore<Web3Provider>() const context = useWeb3ReactCore<Web3Provider>()
const contextNetwork = useWeb3ReactCore<Web3Provider>(NetworkContextName) const contextNetwork = useWeb3ReactCore<Web3Provider>(NetworkContextName)
return context.active ? context : contextNetwork return context.active ? context : contextNetwork

View File

@@ -4,12 +4,14 @@ import { Trade, WETH, TokenAmount } 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'
import { getTradeVersion, useV1TradeExchangeAddress } from '../data/V1'
import { Field } from '../state/swap/actions' import { Field } from '../state/swap/actions'
import { useTransactionAdder, useHasPendingApproval } from '../state/transactions/hooks' import { useTransactionAdder, useHasPendingApproval } from '../state/transactions/hooks'
import { computeSlippageAdjustedAmounts } from '../utils/prices' import { computeSlippageAdjustedAmounts } from '../utils/prices'
import { calculateGasMargin } from '../utils' import { calculateGasMargin } from '../utils'
import { useTokenContract } from './useContract' import { useTokenContract } from './useContract'
import { useActiveWeb3React } from './index' import { useActiveWeb3React } from './index'
import { Version } from './useToggledVersion'
export enum ApprovalState { export enum ApprovalState {
UNKNOWN, UNKNOWN,
@@ -21,30 +23,34 @@ 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?: TokenAmount,
addressToApprove?: string spender?: string
): [ApprovalState, () => Promise<void>] { ): [ApprovalState, () => Promise<void>] {
const { account } = useActiveWeb3React() const { account } = useActiveWeb3React()
const currentAllowance = useTokenAllowance(amountToApprove?.token, account ?? undefined, addressToApprove) const currentAllowance = useTokenAllowance(amountToApprove?.token, account ?? undefined, spender)
const pendingApproval = useHasPendingApproval(amountToApprove?.token?.address) const pendingApproval = useHasPendingApproval(amountToApprove?.token?.address, spender)
// check the current approval status // check the current approval status
const approval = useMemo(() => { const approvalState: ApprovalState = useMemo(() => {
if (!amountToApprove) return ApprovalState.UNKNOWN if (!amountToApprove || !spender) return ApprovalState.UNKNOWN
// we treat WETH as ETH which requires no approvals // we treat WETH as ETH which requires no approvals
if (amountToApprove.token.equals(WETH[amountToApprove.token.chainId])) 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)
}, [amountToApprove, currentAllowance, pendingApproval]) ? pendingApproval
? ApprovalState.PENDING
: ApprovalState.NOT_APPROVED
: ApprovalState.APPROVED
}, [amountToApprove, currentAllowance, pendingApproval, spender])
const tokenContract = useTokenContract(amountToApprove?.token?.address) const tokenContract = useTokenContract(amountToApprove?.token?.address)
const addTransaction = useTransactionAdder() const addTransaction = useTransactionAdder()
const approve = useCallback(async (): Promise<void> => { const approve = useCallback(async (): Promise<void> => {
if (approval !== ApprovalState.NOT_APPROVED) { if (approvalState !== ApprovalState.NOT_APPROVED) {
console.error('approve was called unnecessarily') console.error('approve was called unnecessarily')
return return
} }
@@ -59,30 +65,35 @@ export function useApproveCallback(
return return
} }
if (!spender) {
console.error('no spender')
return
}
let useExact = false let useExact = false
const estimatedGas = await tokenContract.estimateGas.approve(addressToApprove, MaxUint256).catch(() => { const estimatedGas = await tokenContract.estimateGas.approve(spender, MaxUint256).catch(() => {
// general fallback for tokens who restrict approval amounts // general fallback for tokens who restrict approval amounts
useExact = true useExact = true
return tokenContract.estimateGas.approve(addressToApprove, amountToApprove.raw.toString()) return tokenContract.estimateGas.approve(spender, amountToApprove.raw.toString())
}) })
return tokenContract return tokenContract
.approve(addressToApprove, useExact ? amountToApprove.raw.toString() : MaxUint256, { .approve(spender, useExact ? amountToApprove.raw.toString() : MaxUint256, {
gasLimit: calculateGasMargin(estimatedGas) gasLimit: calculateGasMargin(estimatedGas)
}) })
.then((response: TransactionResponse) => { .then((response: TransactionResponse) => {
addTransaction(response, { addTransaction(response, {
summary: 'Approve ' + amountToApprove?.token?.symbol, summary: 'Approve ' + amountToApprove.token.symbol,
approvalOfToken: amountToApprove?.token?.address approval: { tokenAddress: amountToApprove.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
}) })
}, [approval, tokenContract, addressToApprove, amountToApprove, addTransaction]) }, [approvalState, tokenContract, spender, amountToApprove, addTransaction])
return [approval, approve] return [approvalState, approve]
} }
// wraps useApproveCallback in the context of a swap // wraps useApproveCallback in the context of a swap
@@ -91,5 +102,7 @@ export function useApproveCallbackFromTrade(trade?: Trade, allowedSlippage = 0)
() => (trade ? computeSlippageAdjustedAmounts(trade, allowedSlippage)[Field.INPUT] : undefined), () => (trade ? computeSlippageAdjustedAmounts(trade, allowedSlippage)[Field.INPUT] : undefined),
[trade, allowedSlippage] [trade, allowedSlippage]
) )
return useApproveCallback(amountToApprove, ROUTER_ADDRESS) const tradeIsV1 = getTradeVersion(trade) === Version.v1
const v1ExchangeAddress = useV1TradeExchangeAddress(trade)
return useApproveCallback(amountToApprove, tradeIsV1 ? v1ExchangeAddress : ROUTER_ADDRESS)
} }

View File

@@ -2,9 +2,12 @@ import { Contract } from '@ethersproject/contracts'
import { ChainId } from '@uniswap/sdk' import { ChainId } 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 UNISOCKS_ABI from '../constants/abis/unisocks.json'
import ERC20_ABI from '../constants/abis/erc20.json' import ERC20_ABI from '../constants/abis/erc20.json'
import { V1_EXCHANGE_ABI, V1_FACTORY_ABI, V1_FACTORY_ADDRESS } from '../constants/v1' import { MIGRATOR_ABI, MIGRATOR_ADDRESS } from '../constants/abis/migrator'
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'
@@ -25,22 +28,39 @@ 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(chainId === 1 ? V1_FACTORY_ADDRESS : undefined, V1_FACTORY_ABI, false) return useContract(chainId && V1_FACTORY_ADDRESSES[chainId], V1_FACTORY_ABI, false)
} }
export function useV1ExchangeContract(address: string): Contract | null { export function useV2MigratorContract(): Contract | null {
return useContract(address, V1_EXCHANGE_ABI, false) return useContract(MIGRATOR_ADDRESS, MIGRATOR_ABI, true)
} }
export function useTokenContract(tokenAddress?: string, withSignerIfPossible = true): Contract | null { export function useV1ExchangeContract(address?: string, withSignerIfPossible?: boolean): Contract | null {
return useContract(address, V1_EXCHANGE_ABI, withSignerIfPossible)
}
export function useTokenContract(tokenAddress?: string, withSignerIfPossible?: boolean): Contract | null {
return useContract(tokenAddress, ERC20_ABI, withSignerIfPossible) return useContract(tokenAddress, ERC20_ABI, withSignerIfPossible)
} }
export function usePairContract(pairAddress?: string, withSignerIfPossible = true): Contract | null { export function useBytes32TokenContract(tokenAddress?: string, withSignerIfPossible?: boolean): Contract | null {
return useContract(tokenAddress, ERC20_BYTES32_ABI, withSignerIfPossible)
}
export function usePairContract(pairAddress?: string, withSignerIfPossible?: boolean): Contract | null {
return useContract(pairAddress, IUniswapV2PairABI, withSignerIfPossible) return useContract(pairAddress, IUniswapV2PairABI, withSignerIfPossible)
} }
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])
} }

13
src/hooks/useLast.ts Normal file
View File

@@ -0,0 +1,13 @@
import { useEffect, useState } from 'react'
/**
* Returns the last truthy value of type T
* @param value changing value
*/
export default function useLast<T>(value: T | undefined | null): T | null | undefined {
const [last, setLast] = useState<T | null | undefined>(value)
useEffect(() => {
setLast(last => value ?? last)
}, [value])
return last
}

View File

@@ -0,0 +1,11 @@
import { parse, ParsedQs } from 'qs'
import { useMemo } from 'react'
import { useLocation } from 'react-router-dom'
export default function useParsedQueryString(): ParsedQs {
const { search } = useLocation()
return useMemo(
() => (search && search.length > 1 ? parse(search, { parseArrays: false, ignoreQueryPrefix: true }) : {}),
[search]
)
}

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 { 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], { blocksPerFetch: 100 })
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,15 +1,19 @@
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, Token, Trade, TradeType, WETH } from '@uniswap/sdk' import { Trade, TradeType, WETH } from '@uniswap/sdk'
import { useMemo } from 'react' import { useMemo } from 'react'
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, ROUTER_ADDRESS } from '../constants' import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, ROUTER_ADDRESS } from '../constants'
import { useTokenAllowance } from '../data/Allowances' import { useTokenAllowance } from '../data/Allowances'
import { getTradeVersion, useV1TradeExchangeAddress } from '../data/V1'
import { Field } from '../state/swap/actions' import { Field } from '../state/swap/actions'
import { useTransactionAdder } from '../state/transactions/hooks' import { useTransactionAdder } from '../state/transactions/hooks'
import { calculateGasMargin, getRouterContract, shortenAddress, isAddress } from '../utils'
import { computeSlippageAdjustedAmounts } from '../utils/prices' import { computeSlippageAdjustedAmounts } from '../utils/prices'
import { calculateGasMargin, getRouterContract, isAddress } from '../utils'
import { useActiveWeb3React } from './index' import { useActiveWeb3React } from './index'
import useENSName from './useENSName' import { useV1ExchangeContract } from './useContract'
import useENS from './useENS'
import { Version } from './useToggledVersion'
enum SwapType { enum SwapType {
EXACT_TOKENS_FOR_TOKENS, EXACT_TOKENS_FOR_TOKENS,
@@ -17,25 +21,37 @@ enum SwapType {
EXACT_ETH_FOR_TOKENS, EXACT_ETH_FOR_TOKENS,
TOKENS_FOR_EXACT_TOKENS, TOKENS_FOR_EXACT_TOKENS,
TOKENS_FOR_EXACT_ETH, TOKENS_FOR_EXACT_ETH,
ETH_FOR_EXACT_TOKENS 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(tokens: { [field in Field]?: Token }, isExactIn: boolean, chainId: number): SwapType { 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 (isExactIn) {
if (tokens[Field.INPUT]?.equals(WETH[chainId as ChainId])) { if (inputWETH) {
return SwapType.EXACT_ETH_FOR_TOKENS return isV1 ? SwapType.V1_EXACT_ETH_FOR_TOKENS : SwapType.EXACT_ETH_FOR_TOKENS
} else if (tokens[Field.OUTPUT]?.equals(WETH[chainId as ChainId])) { } else if (outputWETH) {
return SwapType.EXACT_TOKENS_FOR_ETH return isV1 ? SwapType.V1_EXACT_TOKENS_FOR_ETH : SwapType.EXACT_TOKENS_FOR_ETH
} else { } else {
return SwapType.EXACT_TOKENS_FOR_TOKENS return isV1 ? SwapType.V1_EXACT_TOKENS_FOR_TOKENS : SwapType.EXACT_TOKENS_FOR_TOKENS
} }
} else { } else {
if (tokens[Field.INPUT]?.equals(WETH[chainId as ChainId])) { if (inputWETH) {
return SwapType.ETH_FOR_EXACT_TOKENS return isV1 ? SwapType.V1_ETH_FOR_EXACT_TOKENS : SwapType.ETH_FOR_EXACT_TOKENS
} else if (tokens[Field.OUTPUT]?.equals(WETH[chainId as ChainId])) { } else if (outputWETH) {
return SwapType.TOKENS_FOR_EXACT_ETH return isV1 ? SwapType.V1_TOKENS_FOR_EXACT_ETH : SwapType.TOKENS_FOR_EXACT_ETH
} else { } else {
return SwapType.TOKENS_FOR_EXACT_TOKENS return isV1 ? SwapType.V1_TOKENS_FOR_EXACT_TOKENS : SwapType.TOKENS_FOR_EXACT_TOKENS
} }
} }
} }
@@ -43,19 +59,27 @@ function getSwapType(tokens: { [field in Field]?: Token }, isExactIn: boolean, c
// 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, optional allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // in bips
deadline: number = DEFAULT_DEADLINE_FROM_NOW, // in seconds from now, optional 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 inputAllowance = useTokenAllowance(trade?.inputAmount?.token, account ?? undefined, ROUTER_ADDRESS)
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 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) return null if (!trade || !recipient || !library || !account || !tradeVersion || !chainId) return null
// will always be defined // will always be defined
const { const {
@@ -67,34 +91,32 @@ export function useSwapCallback(
// no allowance // no allowance
if ( if (
!trade.inputAmount.token.equals(WETH[chainId as ChainId]) && !trade.inputAmount.token.equals(WETH[chainId]) &&
(!inputAllowance || slippageAdjustedInput.greaterThan(inputAllowance)) (!inputAllowance || slippageAdjustedInput.greaterThan(inputAllowance))
) { ) {
return null return null
} }
return async function onSwap() { return async function onSwap() {
if (!chainId || !library || !account) { const contract: Contract | null =
throw new Error('missing dependencies in onSwap callback') tradeVersion === Version.v2 ? getRouterContract(chainId, library, account) : v1Exchange
if (!contract) {
throw new Error('Failed to get a swap contract')
} }
const routerContract: Contract = getRouterContract(chainId, library, account)
const path = trade.route.path.map(t => t.address) const path = trade.route.path.map(t => t.address)
const deadlineFromNow: number = Math.ceil(Date.now() / 1000) + deadline const deadlineFromNow: number = Math.ceil(Date.now() / 1000) + deadline
const swapType = getSwapType( const swapType = getSwapType(trade)
{ [Field.INPUT]: trade.inputAmount.token, [Field.OUTPUT]: trade.outputAmount.token },
trade.tradeType === TradeType.EXACT_INPUT,
chainId as ChainId
)
let estimate, method: Function, args: Array<string | string[] | number>, value: BigNumber | null // let estimate: Function, method: Function,
let methodNames: string[],
args: Array<string | string[] | number>,
value: BigNumber | null = null
switch (swapType) { switch (swapType) {
case SwapType.EXACT_TOKENS_FOR_TOKENS: case SwapType.EXACT_TOKENS_FOR_TOKENS:
estimate = routerContract.estimateGas.swapExactTokensForTokens methodNames = ['swapExactTokensForTokens', 'swapExactTokensForTokensSupportingFeeOnTransferTokens']
method = routerContract.swapExactTokensForTokens
args = [ args = [
slippageAdjustedInput.raw.toString(), slippageAdjustedInput.raw.toString(),
slippageAdjustedOutput.raw.toString(), slippageAdjustedOutput.raw.toString(),
@@ -102,11 +124,9 @@ export function useSwapCallback(
recipient, recipient,
deadlineFromNow deadlineFromNow
] ]
value = null
break break
case SwapType.TOKENS_FOR_EXACT_TOKENS: case SwapType.TOKENS_FOR_EXACT_TOKENS:
estimate = routerContract.estimateGas.swapTokensForExactTokens methodNames = ['swapTokensForExactTokens']
method = routerContract.swapTokensForExactTokens
args = [ args = [
slippageAdjustedOutput.raw.toString(), slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(), slippageAdjustedInput.raw.toString(),
@@ -114,17 +134,14 @@ export function useSwapCallback(
recipient, recipient,
deadlineFromNow deadlineFromNow
] ]
value = null
break break
case SwapType.EXACT_ETH_FOR_TOKENS: case SwapType.EXACT_ETH_FOR_TOKENS:
estimate = routerContract.estimateGas.swapExactETHForTokens methodNames = ['swapExactETHForTokens', 'swapExactETHForTokensSupportingFeeOnTransferTokens']
method = routerContract.swapExactETHForTokens
args = [slippageAdjustedOutput.raw.toString(), path, recipient, deadlineFromNow] args = [slippageAdjustedOutput.raw.toString(), path, recipient, deadlineFromNow]
value = BigNumber.from(slippageAdjustedInput.raw.toString()) value = BigNumber.from(slippageAdjustedInput.raw.toString())
break break
case SwapType.TOKENS_FOR_EXACT_ETH: case SwapType.TOKENS_FOR_EXACT_ETH:
estimate = routerContract.estimateGas.swapTokensForExactETH methodNames = ['swapTokensForExactETH']
method = routerContract.swapTokensForExactETH
args = [ args = [
slippageAdjustedOutput.raw.toString(), slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(), slippageAdjustedInput.raw.toString(),
@@ -132,11 +149,9 @@ export function useSwapCallback(
recipient, recipient,
deadlineFromNow deadlineFromNow
] ]
value = null
break break
case SwapType.EXACT_TOKENS_FOR_ETH: case SwapType.EXACT_TOKENS_FOR_ETH:
estimate = routerContract.estimateGas.swapExactTokensForETH methodNames = ['swapExactTokensForETH', 'swapExactTokensForETHSupportingFeeOnTransferTokens']
method = routerContract.swapExactTokensForETH
args = [ args = [
slippageAdjustedInput.raw.toString(), slippageAdjustedInput.raw.toString(),
slippageAdjustedOutput.raw.toString(), slippageAdjustedOutput.raw.toString(),
@@ -144,58 +159,171 @@ export function useSwapCallback(
recipient, recipient,
deadlineFromNow deadlineFromNow
] ]
value = null
break break
case SwapType.ETH_FOR_EXACT_TOKENS: case SwapType.ETH_FOR_EXACT_TOKENS:
estimate = routerContract.estimateGas.swapETHForExactTokens methodNames = ['swapETHForExactTokens']
method = routerContract.swapETHForExactTokens
args = [slippageAdjustedOutput.raw.toString(), path, recipient, deadlineFromNow] args = [slippageAdjustedOutput.raw.toString(), path, recipient, deadlineFromNow]
value = BigNumber.from(slippageAdjustedInput.raw.toString()) value = BigNumber.from(slippageAdjustedInput.raw.toString())
break 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}`)
} }
return estimate(...args, value ? { value } : {}) const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all(
.then(estimatedGasLimit => methodNames.map(methodName =>
method(...args, { contract.estimateGas[methodName](...args, value ? { value } : {})
...(value ? { value } : {}), .then(calculateGasMargin)
gasLimit: calculateGasMargin(estimatedGasLimit) .catch(error => {
}) console.error(`estimateGas failed for ${methodName}`, error)
return undefined
})
) )
.then(response => { )
if (recipient === account) {
addTransaction(response, {
summary:
'Swap ' +
slippageAdjustedInput.toSignificant(3) +
' ' +
trade.inputAmount.token.symbol +
' for ' +
slippageAdjustedOutput.toSignificant(3) +
' ' +
trade.outputAmount.token.symbol
})
} else {
addTransaction(response, {
summary:
'Swap ' +
slippageAdjustedInput.toSignificant(3) +
' ' +
trade.inputAmount.token.symbol +
' for ' +
slippageAdjustedOutput.toSignificant(3) +
' ' +
trade.outputAmount.token.symbol +
' to ' +
(ensName ?? recipient)
})
}
return response.hash // we expect failures from left to right, so throw if we see failures
}) // from right to left
.catch(error => { for (let i = 0; i < safeGasEstimates.length - 1; i++) {
console.error(`Swap or gas estimate failed`, error) // if the FoT method fails, but the regular method does not, we should not
throw error // use the regular method. this probably means something is wrong with the fot token.
if (BigNumber.isBigNumber(safeGasEstimates[i]) && !BigNumber.isBigNumber(safeGasEstimates[i + 1])) {
throw new Error(
'An error occurred. Please try raising your slippage. If that does not work, contact support.'
)
}
}
const indexOfSuccessfulEstimation = safeGasEstimates.findIndex(safeGasEstimate =>
BigNumber.isBigNumber(safeGasEstimate)
)
// all estimations failed...
if (indexOfSuccessfulEstimation === -1) {
// if only 1 method exists, either:
// 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
if (methodNames.length === 1) {
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.`
)
}
// if 2 methods exists, either:
// 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
else if (methodNames.length === 2) {
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.`
)
} else {
throw Error('This transaction would fail. Please contact support.')
}
} else {
const methodName = methodNames[indexOfSuccessfulEstimation]
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
return contract[methodName](...args, {
gasLimit: safeGasEstimate,
...(value ? { value } : {})
}) })
.then((response: any) => {
const inputSymbol = trade.inputAmount.token.symbol
const outputSymbol = trade.outputAmount.token.symbol
const inputAmount = slippageAdjustedInput.toSignificant(3)
const outputAmount = slippageAdjustedOutput.toSignificant(3)
const base = `Swap ${inputAmount} ${inputSymbol} for ${outputAmount} ${outputSymbol}`
const withRecipient =
recipient === account
? base
: `${base} to ${
recipientAddressOrName && isAddress(recipientAddressOrName)
? shortenAddress(recipientAddressOrName)
: recipientAddressOrName
}`
const withVersion =
tradeVersion === Version.v2 ? withRecipient : `${withRecipient} on ${tradeVersion.toUpperCase()}`
addTransaction(response, {
summary: withVersion
})
return response.hash
})
.catch((error: any) => {
// if the user rejected the tx, pass this along
if (error?.code === 4001) {
throw error
}
// otherwise, the error was unexpected and we need to convey that
else {
console.error(`Swap failed`, error, methodName, args, value)
throw Error('An error occurred while swapping. Please contact support.')
}
})
}
} }
}, [account, allowedSlippage, addTransaction, chainId, deadline, inputAllowance, library, trade, ensName, recipient]) }, [
trade,
recipient,
library,
account,
tradeVersion,
chainId,
allowedSlippage,
inputAllowance,
v1Exchange,
deadline,
recipientAddressOrName,
addTransaction
])
} }

View File

@@ -0,0 +1,15 @@
import useParsedQueryString from './useParsedQueryString'
export enum Version {
v1 = 'v1',
v2 = 'v2'
}
export const DEFAULT_VERSION: Version = Version.v2
export default function useToggledVersion(): Version {
const { use } = useParsedQueryString()
if (!use || typeof use !== 'string') return Version.v2
if (use.toLowerCase() === 'v1') return Version.v1
return DEFAULT_VERSION
}

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,6 +6,7 @@ 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'
@@ -18,7 +19,9 @@ import ThemeProvider, { FixedGlobalStyle, ThemedGlobalStyle } from './theme'
const Web3ProviderNetwork = createWeb3ReactRoot(NetworkContextName) const Web3ProviderNetwork = createWeb3ReactRoot(NetworkContextName)
function getLibrary(provider: any): Web3Provider { function getLibrary(provider: any): Web3Provider {
return new Web3Provider(provider) const library = new Web3Provider(provider)
library.pollingInterval = 15000
return library
} }
const GOOGLE_ANALYTICS_ID: string | undefined = process.env.REACT_APP_GOOGLE_ANALYTICS_ID const GOOGLE_ANALYTICS_ID: string | undefined = process.env.REACT_APP_GOOGLE_ANALYTICS_ID
@@ -31,6 +34,13 @@ 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 (
<> <>

View File

@@ -0,0 +1,63 @@
import { Fraction, Percent, Token, TokenAmount } from '@uniswap/sdk'
import React from 'react'
import { Text } from 'rebass'
import { ButtonPrimary } from '../../components/Button'
import { RowBetween, RowFixed } from '../../components/Row'
import TokenLogo from '../../components/TokenLogo'
import { Field } from '../../state/mint/actions'
import { TYPE } from '../../theme'
export function ConfirmAddModalBottom({
noLiquidity,
price,
tokens,
parsedAmounts,
poolTokenPercentage,
onAdd
}: {
noLiquidity?: boolean
price?: Fraction
tokens: { [field in Field]?: Token }
parsedAmounts: { [field in Field]?: TokenAmount }
poolTokenPercentage?: Percent
onAdd: () => void
}) {
return (
<>
<RowBetween>
<TYPE.body>{tokens[Field.TOKEN_A]?.symbol} Deposited</TYPE.body>
<RowFixed>
<TokenLogo address={tokens[Field.TOKEN_A]?.address} style={{ marginRight: '8px' }} />
<TYPE.body>{parsedAmounts[Field.TOKEN_A]?.toSignificant(6)}</TYPE.body>
</RowFixed>
</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>
</>
)
}

View File

@@ -0,0 +1,52 @@
import { Fraction, Percent, Token } 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 = ({
tokens,
noLiquidity,
poolTokenPercentage,
price
}: {
tokens: { [field in Field]?: Token }
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}>
{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>
)
}

View File

@@ -0,0 +1,13 @@
import { Token, ChainId, WETH } from '@uniswap/sdk'
export function currencyId(...args: [ChainId | undefined, string] | [Token]): string {
if (args.length === 2) {
const [chainId, tokenAddress] = args
return chainId && tokenAddress === WETH[chainId].address ? 'ETH' : tokenAddress
} else if (args.length === 1) {
const [token] = args
return currencyId(token.chainId, token.address)
} else {
throw new Error('unexpected call signature')
}
}

View File

@@ -1,50 +1,64 @@
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 { ChainId, Token, 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 DoubleLogo 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 { ROUTER_ADDRESS, MIN_ETH, ONE_BIPS, DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import { useToken } 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 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 './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 AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
export default function AddLiquidity({ match: { params }, history }: RouteComponentProps<{ tokens: string }>) { function useTokenByCurrencyId(chainId: ChainId | undefined, currencyId: string | undefined): Token | undefined {
useDefaultsFromURLMatchParams(params) const isETH = currencyId?.toUpperCase() === 'ETH'
const token = useToken(isETH ? undefined : currencyId)
return isETH && chainId ? WETH[chainId] : token ?? undefined
}
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)
const tokenA = useTokenByCurrencyId(chainId, currencyIdA)
const tokenB = useTokenByCurrencyId(chainId, currencyIdB)
// toggle wallet when disconnected // toggle wallet when disconnected
const toggleWalletModal = useWalletModalToggle() const toggleWalletModal = useWalletModalToggle()
const expertMode = useIsExpertMode()
// mint state // mint state
const { independentField, typedValue, otherTypedValue } = useMintState() const { independentField, typedValue, otherTypedValue } = useMintState()
const { const {
@@ -58,21 +72,32 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
liquidityMinted, liquidityMinted,
poolTokenPercentage, poolTokenPercentage,
error error
} = useDerivedMintInfo() } = useDerivedMintInfo(tokenA ?? undefined, tokenB ?? undefined)
const { onUserInput } = useMintActionHandlers() const { onUserInput } = useMintActionHandlers(noLiquidity)
const handleTokenAInput = useCallback(
(field: string, value: string) => {
return onUserInput(Field.TOKEN_A, value)
},
[onUserInput]
)
const handleTokenBInput = useCallback(
(field: string, value: string) => {
return onUserInput(Field.TOKEN_B, value)
},
[onUserInput]
)
const isValid = !error const isValid = !error
// modal and loading // modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false) const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showAdvanced, setShowAdvanced] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirm const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirm
const [pendingConfirmation, setPendingConfirmation] = useState<boolean>(true) // waiting for user confirmation
// txn values // txn values
const [deadline] = useUserDeadline() // custom from users settings
const [allowedSlippage] = useUserSlippageTolerance() // custom from users
const [txHash, setTxHash] = useState<string>('') const [txHash, setTxHash] = useState<string>('')
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
// get formatted amounts // get formatted amounts
const formattedAmounts = { const formattedAmounts = {
@@ -84,17 +109,7 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
const maxAmounts: { [field in Field]?: TokenAmount } = [Field.TOKEN_A, Field.TOKEN_B].reduce((accumulator, field) => { const maxAmounts: { [field in Field]?: TokenAmount } = [Field.TOKEN_A, Field.TOKEN_B].reduce((accumulator, field) => {
return { return {
...accumulator, ...accumulator,
[field]: [field]: maxAmountSpend(tokenBalances[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
} }
}, {}) }, {})
@@ -102,7 +117,7 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
(accumulator, field) => { (accumulator, field) => {
return { return {
...accumulator, ...accumulator,
[field]: maxAmounts[field] && parsedAmounts[field] ? maxAmounts[field].equalTo(parsedAmounts[field]) : undefined [field]: maxAmounts[field]?.equalTo(parsedAmounts[field] ?? '0')
} }
}, },
{} {}
@@ -113,40 +128,48 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
const [approvalB, approveBCallback] = useApproveCallback(parsedAmounts[Field.TOKEN_B], ROUTER_ADDRESS) const [approvalB, approveBCallback] = useApproveCallback(parsedAmounts[Field.TOKEN_B], ROUTER_ADDRESS)
const addTransaction = useTransactionAdder() const addTransaction = useTransactionAdder()
async function onAdd() {
setAttemptingTxn(true)
async function onAdd() {
if (!chainId || !library || !account) return
const router = getRouterContract(chainId, library, account) const router = getRouterContract(chainId, library, account)
const { [Field.TOKEN_A]: parsedAmountA, [Field.TOKEN_B]: parsedAmountB } = parsedAmounts
if (!parsedAmountA || !parsedAmountB || !tokenA || !tokenB) {
return
}
const amountsMin = { const amountsMin = {
[Field.TOKEN_A]: calculateSlippageAmount(parsedAmounts[Field.TOKEN_A], noLiquidity ? 0 : allowedSlippage)[0], [Field.TOKEN_A]: calculateSlippageAmount(parsedAmountA, noLiquidity ? 0 : allowedSlippage)[0],
[Field.TOKEN_B]: calculateSlippageAmount(parsedAmounts[Field.TOKEN_B], noLiquidity ? 0 : allowedSlippage)[0] [Field.TOKEN_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 (tokenA.equals(WETH[chainId]) || tokenB.equals(WETH[chainId])) {
const tokenBIsETH = tokenB.equals(WETH[chainId])
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 (tokenBIsETH ? tokenA : tokenB).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.TOKEN_A : Field.TOKEN_B].toString(), // token min
amountsMin[tokenBIsETH ? Field.TOKEN_B : Field.TOKEN_A].toString(), // eth min amountsMin[tokenBIsETH ? Field.TOKEN_B : Field.TOKEN_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, tokenA.address,
tokens[Field.TOKEN_B].address, tokenB.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.TOKEN_A].toString(),
amountsMin[Field.TOKEN_B].toString(), amountsMin[Field.TOKEN_B].toString(),
account, account,
@@ -155,12 +178,15 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
value = null value = null
} }
setAttemptingTxn(true)
await estimate(...args, value ? { value } : {}) await estimate(...args, value ? { value } : {})
.then(estimatedGasLimit => .then(estimatedGasLimit =>
method(...args, { method(...args, {
...(value ? { value } : {}), ...(value ? { value } : {}),
gasLimit: calculateGasMargin(estimatedGasLimit) gasLimit: calculateGasMargin(estimatedGasLimit)
}).then(response => { }).then(response => {
setAttemptingTxn(false)
addTransaction(response, { addTransaction(response, {
summary: summary:
'Add ' + 'Add ' +
@@ -174,7 +200,6 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
}) })
setTxHash(response.hash) setTxHash(response.hash)
setPendingConfirmation(false)
ReactGA.event({ ReactGA.event({
category: 'Liquidity', category: 'Liquidity',
@@ -183,12 +208,12 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
}) })
}) })
) )
.catch((e: Error) => { .catch(error => {
console.error(e)
setPendingConfirmation(true)
setAttemptingTxn(false) setAttemptingTxn(false)
setShowConfirm(false) // we only care if the error is something _other_ than the user rejected the tx
setShowAdvanced(false) if (error?.code !== 4001) {
console.error(error)
}
}) })
} }
@@ -227,76 +252,14 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
const modalBottom = () => { const modalBottom = () => {
return ( return (
<> <ConfirmAddModalBottom
<RowBetween> price={price}
<TYPE.body>{tokens[Field.TOKEN_A]?.symbol} Deposited</TYPE.body> tokens={tokens}
<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 = () => {
return (
<AutoColumn gap="md" justify="space-between">
<AutoRow justify="space-between">
<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>
) )
} }
@@ -304,24 +267,52 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
tokens[Field.TOKEN_A]?.symbol tokens[Field.TOKEN_A]?.symbol
} and ${parsedAmounts[Field.TOKEN_B]?.toSignificant(6)} ${tokens[Field.TOKEN_B]?.symbol}` } and ${parsedAmounts[Field.TOKEN_B]?.toSignificant(6)} ${tokens[Field.TOKEN_B]?.symbol}`
const handleTokenASelect = useCallback(
(tokenAddress: string) => {
const [tokenAId, tokenBId] = [
currencyId(chainId, tokenAddress),
tokenB ? currencyId(chainId, tokenB.address) : undefined
]
if (tokenAId === tokenBId) {
history.push(`/add/${tokenAId}/${tokenA ? currencyId(chainId, tokenA.address) : ''}`)
} else {
history.push(`/add/${tokenAId}/${tokenBId}`)
}
},
[chainId, tokenB, history, tokenA]
)
const handleTokenBSelect = useCallback(
(tokenAddress: string) => {
const [tokenAId, tokenBId] = [
tokenA ? currencyId(chainId, tokenA.address) : undefined,
currencyId(chainId, tokenAddress)
]
if (tokenAId === tokenBId) {
history.push(`/add/${tokenB ? currencyId(chainId, tokenB.address) : ''}/${tokenAId}`)
} else {
history.push(`/add/${currencyIdA ? currencyIdA : 'ETH'}/${currencyId(chainId, tokenAddress)}`)
}
},
[tokenA, chainId, history, tokenB, currencyIdA]
)
return ( return (
<> <>
<AppBody> <AppBody>
<AddRemoveTabs adding={true} />
<Wrapper> <Wrapper>
<ConfirmationModal <ConfirmationModal
isOpen={showConfirm} isOpen={showConfirm}
onDismiss={() => { onDismiss={() => {
if (attemptingTxn) {
history.push('/pool')
return
}
setPendingConfirmation(true)
setAttemptingTxn(false)
setShowConfirm(false) setShowConfirm(false)
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.TOKEN_A, '')
}
setTxHash('')
}} }}
attemptingTxn={attemptingTxn} attemptingTxn={attemptingTxn}
pendingConfirmation={pendingConfirmation} hash={txHash}
hash={txHash ? txHash : ''}
topContent={() => modalHeader()} topContent={() => modalHeader()}
bottomContent={modalBottom} bottomContent={modalBottom}
pendingText={pendingText} pendingText={pendingText}
@@ -346,34 +337,35 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
</ColumnCenter> </ColumnCenter>
)} )}
<CurrencyInputPanel <CurrencyInputPanel
disableTokenSelect={true}
field={Field.TOKEN_A} field={Field.TOKEN_A}
value={formattedAmounts[Field.TOKEN_A]} value={formattedAmounts[Field.TOKEN_A]}
onUserInput={onUserInput} onUserInput={handleTokenAInput}
onMax={() => { onMax={() => {
maxAmounts[Field.TOKEN_A] && onUserInput(Field.TOKEN_A, maxAmounts[Field.TOKEN_A].toExact()) onUserInput(Field.TOKEN_A, maxAmounts[Field.TOKEN_A]?.toExact() ?? '')
}} }}
onTokenSelection={handleTokenASelect}
showMaxButton={!atMaxAmounts[Field.TOKEN_A]} showMaxButton={!atMaxAmounts[Field.TOKEN_A]}
token={tokens[Field.TOKEN_A]} token={tokens[Field.TOKEN_A]}
pair={pair} pair={pair}
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}
field={Field.TOKEN_B} field={Field.TOKEN_B}
value={formattedAmounts[Field.TOKEN_B]} value={formattedAmounts[Field.TOKEN_B]}
onUserInput={onUserInput} onUserInput={handleTokenBInput}
onTokenSelection={handleTokenBSelect}
onMax={() => { onMax={() => {
maxAmounts[Field.TOKEN_B] && onUserInput(Field.TOKEN_B, maxAmounts[Field.TOKEN_B].toExact()) onUserInput(Field.TOKEN_B, maxAmounts[Field.TOKEN_B]?.toExact() ?? '')
}} }}
showMaxButton={!atMaxAmounts[Field.TOKEN_B]} showMaxButton={!atMaxAmounts[Field.TOKEN_B]}
token={tokens[Field.TOKEN_B]} token={tokens[Field.TOKEN_B]}
pair={pair} pair={pair}
id="add-liquidity-input-tokenb" id="add-liquidity-input-tokenb"
showCommonBases
/> />
{tokens[Field.TOKEN_A] && tokens[Field.TOKEN_B] && ( {tokens[Field.TOKEN_A] && tokens[Field.TOKEN_B] && (
<> <>
@@ -384,7 +376,12 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
</TYPE.subHeader> </TYPE.subHeader>
</RowBetween>{' '} </RowBetween>{' '}
<LightCard padding="1rem" borderRadius={'20px'}> <LightCard padding="1rem" borderRadius={'20px'}>
<PriceBar /> <PoolPriceBar
tokens={tokens}
poolTokenPercentage={poolTokenPercentage}
noLiquidity={noLiquidity}
price={price}
/>
</LightCard> </LightCard>
</GreyCard> </GreyCard>
</> </>
@@ -392,53 +389,62 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
{!account ? ( {!account ? (
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight> <ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
) : approvalA === ApprovalState.NOT_APPROVED || approvalA === ApprovalState.PENDING ? (
<ButtonLight onClick={approveACallback} disabled={approvalA === ApprovalState.PENDING}>
{approvalA === ApprovalState.PENDING ? (
<Dots>Approving {tokens[Field.TOKEN_A]?.symbol}</Dots>
) : (
'Approve ' + tokens[Field.TOKEN_A]?.symbol
)}
</ButtonLight>
) : approvalB === ApprovalState.NOT_APPROVED || approvalB === ApprovalState.PENDING ? (
<ButtonLight onClick={approveBCallback} disabled={approvalB === ApprovalState.PENDING}>
{approvalB === ApprovalState.PENDING ? (
<Dots>Approving {tokens[Field.TOKEN_B]?.symbol}</Dots>
) : (
'Approve ' + tokens[Field.TOKEN_B]?.symbol
)}
</ButtonLight>
) : ( ) : (
<ButtonError <AutoColumn gap={'md'}>
onClick={() => { {(approvalA === ApprovalState.NOT_APPROVED ||
setShowConfirm(true) approvalA === ApprovalState.PENDING ||
}} approvalB === ApprovalState.NOT_APPROVED ||
disabled={!isValid} approvalB === ApprovalState.PENDING) &&
error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]} isValid && (
> <RowBetween>
<Text fontSize={20} fontWeight={500}> {approvalA !== ApprovalState.APPROVED && (
{error ?? 'Supply'} <ButtonPrimary
</Text> onClick={approveACallback}
</ButtonError> disabled={approvalA === ApprovalState.PENDING}
width={approvalB !== ApprovalState.APPROVED ? '48%' : '100%'}
>
{approvalA === ApprovalState.PENDING ? (
<Dots>Approving {tokens[Field.TOKEN_A]?.symbol}</Dots>
) : (
'Approve ' + tokens[Field.TOKEN_A]?.symbol
)}
</ButtonPrimary>
)}
{approvalB !== ApprovalState.APPROVED && (
<ButtonPrimary
onClick={approveBCallback}
disabled={approvalB === ApprovalState.PENDING}
width={approvalA !== ApprovalState.APPROVED ? '48%' : '100%'}
>
{approvalB === ApprovalState.PENDING ? (
<Dots>Approving {tokens[Field.TOKEN_B]?.symbol}</Dots>
) : (
'Approve ' + tokens[Field.TOKEN_B]?.symbol
)}
</ButtonPrimary>
)}
</RowBetween>
)}
<ButtonError
onClick={() => {
expertMode ? onAdd() : setShowConfirm(true)
}}
disabled={!isValid || approvalA !== ApprovalState.APPROVED || approvalB !== ApprovalState.APPROVED}
error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]}
>
<Text fontSize={20} fontWeight={500}>
{error ?? 'Supply'}
</Text>
</ButtonError>
</AutoColumn>
)} )}
</AutoColumn> </AutoColumn>
</Wrapper> </Wrapper>
</AppBody> </AppBody>
{isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B] ? (
<AdvancedSwapDetailsDropdown
rawSlippage={allowedSlippage}
deadline={deadline}
showAdvanced={showAdvanced}
setShowAdvanced={setShowAdvanced}
setDeadline={setDeadline}
setRawSlippage={setAllowedSlippage}
/>
) : null}
{pair && !noLiquidity ? ( {pair && !noLiquidity ? (
<AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}> <AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
<PositionCard pair={pair} minimal={true} /> <MinimalPositionCard pair={pair} />
</AutoColumn> </AutoColumn>
) : null} ) : null}
</> </>

View File

@@ -0,0 +1,42 @@
import { WETH } from '@uniswap/sdk'
import React from 'react'
import { Redirect, RouteComponentProps } from 'react-router-dom'
import AddLiquidity from './index'
export function RedirectToAddLiquidity() {
return <Redirect to="/add/" />
}
function convertToCurrencyIds(address: string): string {
if (Object.values(WETH).some(weth => weth.address === address)) {
return 'ETH'
}
return address
}
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/${convertToCurrencyIds(match[1])}/${convertToCurrencyIds(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} />
}

View File

@@ -0,0 +1,7 @@
{
"extends": "../../../tsconfig.strict.json",
"include": [
"**/*",
"../../../node_modules/eslint-plugin-react/lib/types.d.ts"
]
}

View File

@@ -1,18 +1,23 @@
import React, { Suspense } from 'react' import React, { Suspense } from 'react'
import { BrowserRouter, HashRouter, Route, Switch } from 'react-router-dom' import { HashRouter, Route, Switch } from 'react-router-dom'
import styled from 'styled-components' import styled from 'styled-components'
import GoogleAnalyticsReporter from '../components/analytics/GoogleAnalyticsReporter' import GoogleAnalyticsReporter from '../components/analytics/GoogleAnalyticsReporter'
import Footer from '../components/Footer'
import Header from '../components/Header' import Header from '../components/Header'
import Popups from '../components/Popups' import Popups from '../components/Popups'
import Web3ReactManager from '../components/Web3ReactManager' import Web3ReactManager from '../components/Web3ReactManager'
import DarkModeQueryParamReader from '../theme/DarkModeQueryParamReader' import DarkModeQueryParamReader from '../theme/DarkModeQueryParamReader'
import AddLiquidity from './AddLiquidity' import AddLiquidity from './AddLiquidity'
import CreatePool from './CreatePool' import {
RedirectDuplicateTokenIds,
RedirectOldAddLiquidityPathStructure,
RedirectToAddLiquidity
} from './AddLiquidity/redirects'
import MigrateV1 from './MigrateV1'
import MigrateV1Exchange from './MigrateV1/MigrateV1Exchange'
import RemoveV1Exchange from './MigrateV1/RemoveV1Exchange'
import Pool from './Pool' import Pool from './Pool'
import PoolFinder from './PoolFinder' import PoolFinder from './PoolFinder'
import RemoveLiquidity from './RemoveLiquidity' import RemoveLiquidity from './RemoveLiquidity'
import Send from './Send'
import Swap from './Swap' import Swap from './Swap'
import { RedirectPathToSwapOnly, RedirectToSwap } from './Swap/redirects' import { RedirectPathToSwapOnly, RedirectToSwap } from './Swap/redirects'
@@ -47,40 +52,14 @@ const BodyWrapper = styled.div`
z-index: 1; z-index: 1;
` `
const BackgroundGradient = styled.div`
width: 100%;
height: 200vh;
background: ${({ theme }) => `radial-gradient(50% 50% at 50% 50%, ${theme.primary1} 0%, ${theme.bg1} 100%)`};
position: absolute;
top: 0px;
left: 0px;
opacity: 0.1;
z-index: -1;
transform: translateY(-70vh);
@media (max-width: 960px) {
height: 300px;
width: 100%;
transform: translateY(-150px);
}
`
const Marginer = styled.div` const Marginer = styled.div`
margin-top: 5rem; margin-top: 5rem;
` `
let Router: React.ComponentType
if (process.env.PUBLIC_URL === '.') {
Router = HashRouter
} else {
Router = BrowserRouter
}
export default function App() { export default function App() {
return ( return (
<Suspense fallback={null}> <Suspense fallback={null}>
<Router> <HashRouter>
<Route component={GoogleAnalyticsReporter} /> <Route component={GoogleAnalyticsReporter} />
<Route component={DarkModeQueryParamReader} /> <Route component={DarkModeQueryParamReader} />
<AppWrapper> <AppWrapper>
@@ -93,21 +72,24 @@ export default function App() {
<Switch> <Switch>
<Route exact strict path="/swap" component={Swap} /> <Route exact strict path="/swap" component={Swap} />
<Route exact strict path="/swap/:outputCurrency" component={RedirectToSwap} /> <Route exact strict path="/swap/:outputCurrency" component={RedirectToSwap} />
<Route exact strict path="/send" component={Send} /> <Route exact strict path="/send" component={RedirectPathToSwapOnly} />
<Route exact strict path="/find" component={PoolFinder} /> <Route exact strict path="/find" component={PoolFinder} />
<Route exact strict path="/pool" component={Pool} /> <Route exact strict path="/pool" component={Pool} />
<Route exact strict path="/create" component={CreatePool} /> <Route exact strict path="/create" component={RedirectToAddLiquidity} />
<Route exact strict path="/add/:tokens" component={AddLiquidity} /> <Route exact path="/add" component={AddLiquidity} />
<Route exact path="/add/:currencyIdA" component={RedirectOldAddLiquidityPathStructure} />
<Route exact path="/add/:currencyIdA/:currencyIdB" component={RedirectDuplicateTokenIds} />
<Route exact strict path="/remove/:tokens" component={RemoveLiquidity} /> <Route exact strict path="/remove/:tokens" component={RemoveLiquidity} />
<Route exact strict path="/migrate/v1" component={MigrateV1} />
<Route exact strict path="/migrate/v1/:address" component={MigrateV1Exchange} />
<Route exact strict path="/remove/v1/:address" component={RemoveV1Exchange} />
<Route component={RedirectPathToSwapOnly} /> <Route component={RedirectPathToSwapOnly} />
</Switch> </Switch>
</Web3ReactManager> </Web3ReactManager>
<Marginer /> <Marginer />
<Footer />
</BodyWrapper> </BodyWrapper>
<BackgroundGradient />
</AppWrapper> </AppWrapper>
</Router> </HashRouter>
</Suspense> </Suspense>
) )
} }

View File

@@ -1,8 +1,7 @@
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import NavigationTabs from '../components/NavigationTabs'
const Body = styled.div` export const BodyWrapper = styled.div`
position: relative; position: relative;
max-width: 420px; max-width: 420px;
width: 100%; width: 100%;
@@ -17,10 +16,5 @@ const Body = styled.div`
* The styled container element that wraps the content of most pages and the tabs. * The styled container element that wraps the content of most pages and the tabs.
*/ */
export default function AppBody({ children }: { children: React.ReactNode }) { export default function AppBody({ children }: { children: React.ReactNode }) {
return ( return <BodyWrapper>{children}</BodyWrapper>
<Body>
<NavigationTabs />
<>{children}</>
</Body>
)
} }

View File

@@ -1,146 +0,0 @@
import React, { useState, useEffect } from 'react'
import { RouteComponentProps, Redirect } from 'react-router-dom'
import { Token, WETH } from '@uniswap/sdk'
import AppBody from '../AppBody'
import Row, { AutoRow } from '../../components/Row'
import TokenLogo from '../../components/TokenLogo'
import SearchModal from '../../components/SearchModal'
import { Text } from 'rebass'
import { Plus } from 'react-feather'
import { TYPE, StyledInternalLink } from '../../theme'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import { ButtonPrimary, ButtonDropwdown, ButtonDropwdownLight } from '../../components/Button'
import { useToken } from '../../hooks/Tokens'
import { useActiveWeb3React } from '../../hooks'
import { usePair } from '../../data/Reserves'
enum Fields {
TOKEN0 = 0,
TOKEN1 = 1
}
enum STEP {
SELECT_TOKENS = 'SELECT_TOKENS', // choose input and output tokens
READY_TO_CREATE = 'READY_TO_CREATE', // enable 'create' button
SHOW_CREATE_PAGE = 'SHOW_CREATE_PAGE' // show create page
}
export default function CreatePool({ location }: RouteComponentProps) {
const { chainId } = useActiveWeb3React()
const [showSearch, setShowSearch] = useState<boolean>(false)
const [activeField, setActiveField] = useState<number>(Fields.TOKEN0)
const [token0Address, setToken0Address] = useState<string>(WETH[chainId].address)
const [token1Address, setToken1Address] = useState<string>()
const token0: Token = useToken(token0Address)
const token1: Token = useToken(token1Address)
const [step, setStep] = useState<string>(STEP.SELECT_TOKENS)
const pair = usePair(token0, token1)
// if both tokens selected but pair doesnt exist, enable button to create pair
useEffect(() => {
if (token0Address && token1Address && pair === null) {
setStep(STEP.READY_TO_CREATE)
}
}, [pair, token0Address, token1Address])
// if theyve clicked create, show add liquidity page
if (step === STEP.SHOW_CREATE_PAGE) {
return <Redirect to={{ ...location, pathname: `/add/${token0Address}-${token1Address}` }} push={true} />
}
return (
<AppBody>
<AutoColumn gap="20px">
<AutoColumn gap="24px">
{!token0Address ? (
<ButtonDropwdown
onClick={() => {
setShowSearch(true)
setActiveField(Fields.TOKEN0)
}}
>
<Text fontSize={20}>Select first token</Text>
</ButtonDropwdown>
) : (
<ButtonDropwdownLight
onClick={() => {
setShowSearch(true)
setActiveField(Fields.TOKEN0)
}}
>
<Row align="flex-end">
<TokenLogo address={token0Address} />
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
{token0?.symbol}{' '}
</Text>
<TYPE.darkGray fontWeight={500} fontSize={16} marginLeft={'8px'}>
{token0?.address === WETH[chainId]?.address && '(default)'}
</TYPE.darkGray>
</Row>
</ButtonDropwdownLight>
)}
<ColumnCenter>
<Plus size="16" color="#888D9B" />
</ColumnCenter>
{!token1Address ? (
<ButtonDropwdown
onClick={() => {
setShowSearch(true)
setActiveField(Fields.TOKEN1)
}}
disabled={step !== STEP.SELECT_TOKENS}
>
<Text fontSize={20}>Select second token</Text>
</ButtonDropwdown>
) : (
<ButtonDropwdownLight
onClick={() => {
setShowSearch(true)
setActiveField(Fields.TOKEN1)
}}
>
<Row>
<TokenLogo address={token1Address} />
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
{token1?.symbol}
</Text>
</Row>
</ButtonDropwdownLight>
)}
{pair ? ( // pair already exists - prompt to add liquidity to existing pool
<AutoRow padding="10px" justify="center">
<TYPE.body textAlign="center">
Pool already exists!{' '}
<StyledInternalLink to={`/add/${token0Address}-${token1Address}`}>Join the pool.</StyledInternalLink>
</TYPE.body>
</AutoRow>
) : (
<ButtonPrimary disabled={step !== STEP.READY_TO_CREATE} onClick={() => setStep(STEP.SHOW_CREATE_PAGE)}>
<Text fontWeight={500} fontSize={20}>
Create Pool
</Text>
</ButtonPrimary>
)}
</AutoColumn>
<SearchModal
isOpen={showSearch}
filterType="tokens"
onTokenSelect={address => {
activeField === Fields.TOKEN0 ? setToken0Address(address) : setToken1Address(address)
}}
onDismiss={() => {
setShowSearch(false)
}}
hiddenToken={activeField === Fields.TOKEN0 ? token1Address : token0Address}
showCommonBases={activeField === Fields.TOKEN0}
/>
</AutoColumn>
</AppBody>
)
}

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