Compare commits

...

79 Commits

Author SHA1 Message Date
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
125 changed files with 4592 additions and 2482 deletions

View File

@@ -2,14 +2,15 @@ name: Release
on:
# every morning
schedule:
- cron: '0 12 * * *'
- cron: '0 12 * * 1-4'
# releases are triggered on changes to this file
push:
branches:
- v2
- master
paths:
- '.github/workflows/release.yaml'
- '.env.production'
jobs:
bump_version:
@@ -46,7 +47,7 @@ jobs:
run: yarn install --ignore-scripts --frozen-lockfile
- name: Build the IPFS bundle
run: yarn ipfs-build
run: yarn build
- name: Pin to IPFS
id: upload
@@ -64,14 +65,14 @@ jobs:
cidv0: ${{ steps.upload.outputs.hash }}
- name: Update DNS with new IPFS hash
uses: uniswap/replace-vercel-dns-records@v1.0.0
env:
CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_TOKEN }}
RECORD_DOMAIN: 'uniswap.org'
RECORD_NAME: '_dnslink.app'
CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}
uses: textileio/cloudflare-update-dnslink@0fe7b7a1ffc865db3a4da9773f0f987447ad5848
with:
domain: 'uniswap.org'
subdomain: '_dnslink.app'
record-type: 'TXT'
value: dnslink=/ipfs/${{ steps.upload.outputs.hash }}
token: ${{ secrets.VERCEL_TOKEN }}
team-name: 'uniswap'
cid: ${{ steps.upload.outputs.hash }}
- name: Create GitHub Release
id: create_release
@@ -82,15 +83,17 @@ jobs:
tag_name: ${{ needs.bump_version.outputs.new_tag }}
release_name: Release ${{ needs.bump_version.outputs.new_tag }}
body: |
Release built from commit [`${{ github.sha }}`](https://github.com/Uniswap/uniswap-frontend/tree/${{ github.sha }})
The IPFS hash of the bundle is:
IPFS hash of the deployment:
- CIDv0: `${{ steps.upload.outputs.hash }}`
- CIDv1: `${{ steps.convert_cidv0.outputs.cidv1 }}`
Uniswap 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 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.
The latest release is always accessible via our alias to the Cloudflare IPFS gateway at [app.uniswap.org](https://app.uniswap.org).
You can also access the Uniswap Interface directly from an IPFS gateway.
The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to store your settings.
**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:
- https://${{ steps.convert_cidv0.outputs.cidv1 }}.ipfs.dweb.link/

View File

@@ -2,10 +2,10 @@ name: Tests
on:
push:
branches:
- v2
- master
pull_request:
branches:
- v2
- master
jobs:
integration-tests:
name: Integration tests

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/)
An open source interface for Uniswap -- a protocol for decentralized exchange of Ethereum tokens.
- Website: [uniswap.org](https://uniswap.org/)
- Interface: [app.uniswap.org](https://app.uniswap.org)
- Docs: [uniswap.org/docs/](https://uniswap.org/docs/)
- Twitter: [@UniswapProtocol](https://twitter.com/UniswapProtocol)
- 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)
- 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
[latest release](https://github.com/Uniswap/uniswap-frontend/releases/latest)
or visit [uniswap.exchange](https://uniswap.exchange).
To access the Uniswap Interface, use an IPFS gateway link from the
[latest release](https://github.com/Uniswap/uniswap-interface/releases/latest),
or visit [app.uniswap.org](https://app.uniswap.org).
## Development
@@ -27,26 +28,32 @@ or visit [uniswap.exchange](https://uniswap.exchange).
yarn
```
### Configure Environment (optional)
Copy `.env` to `.env.local` and change the appropriate variables.
### Run
```bash
yarn start
```
To have the frontend default to a different network, make a copy of `.env` named `.env.local`,
change `REACT_APP_NETWORK_ID` to `"{yourNetworkId}"`, and change `REACT_APP_NETWORK_URL` to e.g.
`"https://{yourNetwork}.infura.io/v3/{yourKey}"`.
### Configuring the environment (optional)
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
[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
**Please open all pull requests against the `v2` branch.**
CI checks will run against all PRs.
**Please open all pull requests against the `master` branch.**
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

@@ -1,4 +1,4 @@
import { TEST_ADDRESS_NEVER_USE } from '../support/commands'
import { TEST_ADDRESS_NEVER_USE_SHORTENED } from '../support/commands'
describe('Landing Page', () => {
beforeEach(() => cy.visit('/'))
@@ -22,6 +22,6 @@ describe('Landing Page', () => {
it('is connected', () => {
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

@@ -2,7 +2,7 @@ describe('Pool', () => {
beforeEach(() => cy.visit('/pool'))
it('can search for a pool', () => {
cy.get('#join-pool-button').click()
cy.get('#token-search-input').type('DAI')
cy.get('#token-search-input').type('DAI', { delay: 200 })
})
it('can import a pool', () => {

View File

@@ -3,7 +3,7 @@ describe('Send', () => {
it('can enter an amount into input', () => {
cy.get('#sending-no-swap-input')
.type('0.001')
.type('0.001', { delay: 200 })
.should('have.value', '0.001')
})
})

View File

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

View File

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

View File

@@ -14,6 +14,8 @@ const PRIVATE_KEY_TEST_NEVER_USE = '0xad20c82497421e9784f18460ad2fe84f73569068e9
// address of the above key
export const TEST_ADDRESS_NEVER_USE = '0x0fF2D1eFd7A57B7562b2bf27F3f37899dB27F4a5'
export const TEST_ADDRESS_NEVER_USE_SHORTENED = '0x0fF2...F4a5'
class CustomizedBridge extends _Eip1193Bridge {
async sendAsync(...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
Cypress.Commands.overwrite('visit', (original, url, options) => {
return original(url, {
return original(url.startsWith('/') && url.length > 2 && !url.startsWith('/#') ? `/#${url}` : url, {
...options,
onBeforeLoad(win) {
options && options.onBeforeLoad && options.onBeforeLoad(win)
const provider = new JsonRpcProvider('https://rinkeby.infura.io/v3/acb7e55995d04c49bfb52b7141599467', 4)
const signer = new Wallet(PRIVATE_KEY_TEST_NEVER_USE, 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",
"description": "Uniswap Interface",
"homepage": "https://uniswap.exchange",
"homepage": ".",
"private": true,
"devDependencies": {
"@ethersproject/address": "^5.0.0-beta.134",
@@ -33,12 +33,12 @@
"@typescript-eslint/parser": "^2.31.0",
"@uniswap/sdk": "^2.0.5",
"@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/fortmatic-connector": "^6.0.9",
"@web3-react/injected-connector": "^6.0.7",
"@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",
"copy-to-clipboard": "^3.2.0",
"cross-env": "^7.0.2",
@@ -51,6 +51,7 @@
"i18next": "^15.0.9",
"i18next-browser-languagedetector": "^3.0.1",
"i18next-xhr-backend": "^2.0.1",
"inter-ui": "^3.13.1",
"jazzicon": "^1.5.0",
"lodash.flatmap": "^4.5.0",
"polished": "^3.3.2",
@@ -80,8 +81,7 @@
},
"scripts": {
"start": "react-scripts start",
"build": "cross-env REACT_APP_GIT_COMMIT_HASH=$(git show -s --format=%H) react-scripts build",
"ipfs-build": "cross-env PUBLIC_URL=\".\" react-scripts build",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",

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

View File

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

View File

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

View File

@@ -6,7 +6,12 @@ import { RowBetween } from '../Row'
import { ChevronDown } from 'react-feather'
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')};
width: ${({ width }) => (width ? width : '100%')};
font-weight: 500;
@@ -45,10 +50,12 @@ export const ButtonPrimary = styled(Base)`
background-color: ${({ theme }) => darken(0.1, theme.primary1)};
}
&:disabled {
background-color: ${({ theme }) => theme.bg3};
color: ${({ theme }) => theme.text3}
background-color: ${({ theme, altDisbaledStyle }) => (altDisbaledStyle ? theme.primary1 : theme.bg3)};
color: ${({ theme, altDisbaledStyle }) => (altDisbaledStyle ? 'white' : theme.text3)};
cursor: auto;
box-shadow: none;
border: 1px solid transparent;
outline: none;
}
`
@@ -68,6 +75,16 @@ export const ButtonLight = styled(Base)`
box-shadow: 0 0 0 1pt ${({ 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)`
@@ -180,7 +197,6 @@ export const ButtonEmpty = styled(Base)`
export const ButtonWhite = styled(Base)`
border: 1px solid #edeef2;
background-color: ${({ theme }) => theme.bg1};
};
color: black;
&:focus {
@@ -228,6 +244,9 @@ const ButtonErrorStyle = styled(Base)`
&:disabled {
opacity: 50%;
cursor: auto;
box-shadow: none;
background-color: ${({ theme }) => theme.red1};
border: 1px solid ${({ theme }) => theme.red1};
}
`
@@ -247,7 +266,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 (
<ButtonPrimary {...rest} disabled={disabled}>
<RowBetween>
@@ -258,7 +277,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 (
<ButtonOutlined {...rest} disabled={disabled}>
<RowBetween>

View File

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

View File

@@ -1,12 +1,12 @@
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 { darken } from 'polished'
import { Field } from '../../state/swap/actions'
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import TokenSearchModal from '../SearchModal/TokenSearchModal'
import TokenLogo from '../TokenLogo'
import DoubleLogo from '../DoubleLogo'
import SearchModal from '../SearchModal'
import { RowBetween } from '../Row'
import { TYPE, CursorPointer } from '../../theme'
import { Input as NumericalInput } from '../NumericalInput'
@@ -49,7 +49,6 @@ const LabelRow = styled.div`
font-size: 0.75rem;
line-height: 1rem;
padding: 0.75rem 1rem 0 1rem;
height: 20px;
span:hover {
cursor: pointer;
color: ${({ theme }) => darken(0.2, theme.text2)};
@@ -160,6 +159,10 @@ export default function CurrencyInputPanel({
const userTokenBalance = useTokenBalanceTreatingWETHasETH(account, token)
const theme = useContext(ThemeContext)
const handleDismissSearch = useCallback(() => {
setModalOpen(false)
}, [setModalOpen])
return (
<InputPanel id={id}>
<Container hideInput={hideInput}>
@@ -236,12 +239,9 @@ export default function CurrencyInputPanel({
</InputRow>
</Container>
{!disableTokenSelect && (
<SearchModal
<TokenSearchModal
isOpen={modalOpen}
onDismiss={() => {
setModalOpen(false)
}}
filterType="tokens"
onDismiss={handleDismissSearch}
onTokenSelect={onTokenSelection}
showSendWithSwap={showSendWithSwap}
hiddenToken={token?.address}

View File

@@ -13,7 +13,7 @@ interface DoubleTokenLogoProps {
margin?: boolean
size?: number
a0: string
a1: string
a1?: string
}
const HigherLogo = styled(TokenLogo)`
@@ -28,7 +28,7 @@ export default function DoubleTokenLogo({ a0, a1, size = 16, margin = false }: D
return (
<TokenWrapper sizeraw={size} margin={margin}>
<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>
)
}

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,28 @@
import { ChainId, WETH } from '@uniswap/sdk'
import React from 'react'
import { isMobile } from 'react-device-detect'
import { Link as HistoryLink } from 'react-router-dom'
import { Text } from 'rebass'
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 Wordmark from '../../assets/svg/wordmark.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 { useActiveWeb3React } from '../../hooks'
import { useDarkModeManager } from '../../state/user/hooks'
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import { ExternalLink, StyledInternalLink } from '../../theme'
import { YellowCard } from '../Card'
import { AutoColumn } from '../Column'
import { RowBetween } from '../Row'
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`
display: flex;
@@ -31,15 +32,12 @@ const HeaderFrame = styled.div`
width: 100%;
top: 0;
position: absolute;
pointer-events: none;
z-index: 2;
${({ theme }) => theme.mediaWidth.upToExtraSmall`
padding: 12px 0 0 0;
width: calc(100%);
position: relative;
`};
z-index: 2;
`
const HeaderElement = styled.div`
@@ -47,6 +45,15 @@ const HeaderElement = styled.div`
align-items: center;
`
const HeaderElementWrap = styled.div`
display: flex;
align-items: center;
${({ theme }) => theme.mediaWidth.upToExtraSmall`
margin-top: 0.5rem;
`};
`
const Title = styled.div`
display: flex;
align-items: center;
@@ -72,6 +79,7 @@ const AccountElement = styled.div<{ active: boolean }>`
background-color: ${({ theme, active }) => (!active ? theme.bg1 : theme.bg3)};
border-radius: 12px;
white-space: nowrap;
width: 100%;
:focus {
border: 1px solid blue;
@@ -82,10 +90,7 @@ const TestnetWrapper = styled.div`
white-space: nowrap;
width: fit-content;
margin-left: 10px;
${({ theme }) => theme.mediaWidth.upToSmall`
display: none;
`};
pointer-events: auto;
`
const NetworkCard = styled(YellowCard)`
@@ -122,32 +127,23 @@ const MigrateBanner = styled(AutoColumn)`
`};
`
const VersionLabel = styled.span<{ isV2?: boolean }>`
padding: ${({ isV2 }) => (isV2 ? '0.15rem 0.5rem 0.16rem 0.45rem' : '0.15rem 0.5rem 0.16rem 0.35rem')};
border-radius: 14px;
background: ${({ theme, isV2 }) => (isV2 ? theme.primary1 : 'none')};
color: ${({ theme, isV2 }) => (isV2 ? theme.white : theme.primary1)};
font-size: 0.825rem;
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 HeaderControls = styled.div`
display: flex;
flex-direction: row;
align-items: flex-end;
${({ theme }) => theme.mediaWidth.upToSmall`
flex-direction: column;
`};
`
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;
}
`
const NETWORK_LABELS: { [chainId in ChainId]: string | null } = {
[ChainId.MAINNET]: null,
[ChainId.RINKEBY]: 'Rinkeby',
[ChainId.ROPSTEN]: 'Ropsten',
[ChainId.GÖRLI]: 'Görli',
[ChainId.KOVAN]: 'Kovan'
}
export default function Header() {
const { account, chainId } = useActiveWeb3React()
@@ -163,12 +159,12 @@ export default function Header() {
<b>blog post </b>
</ExternalLink>
&nbsp;or&nbsp;
<ExternalLink href="https://migrate.uniswap.exchange/">
<StyledInternalLink to="/migrate/v1">
<b>migrate your liquidity </b>
</ExternalLink>
</StyledInternalLink>
.
</MigrateBanner>
<RowBetween padding="1rem">
<RowBetween style={{ alignItems: 'flex-start' }} padding="1rem 1rem 0 1rem">
<HeaderElement>
<Title>
<UniIcon id="link" to="/">
@@ -186,34 +182,27 @@ export default function Header() {
</TitleText>
)}
</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>
<TestnetWrapper>
{!isMobile && chainId === ChainId.ROPSTEN && <NetworkCard>Ropsten</NetworkCard>}
{!isMobile && chainId === ChainId.RINKEBY && <NetworkCard>Rinkeby</NetworkCard>}
{!isMobile && chainId === ChainId.GÖRLI && <NetworkCard>Görli</NetworkCard>}
{!isMobile && chainId === ChainId.KOVAN && <NetworkCard>Kovan</NetworkCard>}
</TestnetWrapper>
<AccountElement active={!!account} style={{ pointerEvents: 'auto' }}>
{account && userEthBalance ? (
<Text style={{ flexShrink: 0 }} px="0.5rem" fontWeight={500}>
{userEthBalance?.toSignificant(4)} ETH
</Text>
) : null}
<Web3Status />
</AccountElement>
<div style={{ pointerEvents: 'auto' }}>
<HeaderControls>
<HeaderElement>
<TestnetWrapper>
{!isMobile && NETWORK_LABELS[chainId] && <NetworkCard>{NETWORK_LABELS[chainId]}</NetworkCard>}
</TestnetWrapper>
<AccountElement active={!!account} style={{ pointerEvents: 'auto' }}>
{account && userEthBalance ? (
<Text style={{ flexShrink: 0 }} pl="0.75rem" pr="0.5rem" fontWeight={500}>
{userEthBalance?.toSignificant(4)} ETH
</Text>
) : null}
<Web3Status />
</AccountElement>
</HeaderElement>
<HeaderElementWrap>
<VersionSwitch />
<Settings />
<Menu />
</div>
</HeaderElement>
</HeaderElementWrap>
</HeaderControls>
</RowBetween>
</HeaderFrame>
)

View File

@@ -1,15 +1,38 @@
import React from 'react'
import styled from 'styled-components'
import styled, { keyframes } from 'styled-components'
import { Spinner } from '../../theme'
import Circle from '../../assets/images/blue-loader.svg'
const SpinnerWrapper = styled(Spinner)<{ size: string }>`
height: ${({ size }) => size};
width: ${({ size }) => size};
const rotate = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`
export default function Loader({ size }: { size: string }) {
return <SpinnerWrapper src={Circle} alt="loader" size={size} />
const StyledSVG = styled.svg<{ size: string; stroke?: string }>`
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
? `https://github.com/Uniswap/uniswap-frontend/tree/${process.env.REACT_APP_GIT_COMMIT_HASH}`
: 'https://github.com/Uniswap/uniswap-frontend'
const CODE_LINK = 'https://github.com/Uniswap/uniswap-interface'
export default function Menu() {
const node = useRef<HTMLDivElement>()

View File

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

View File

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

View File

@@ -0,0 +1,75 @@
import React, { useContext } from 'react'
import { 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, history }: 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%"
onClick={() => {
history.push(`/migrate/v1/${V1LiquidityBalance.token.address}`)
}}
>
Migrate
</ButtonSecondary>
<ButtonSecondary
style={{ backgroundColor: 'transparent' }}
width="28%"
onClick={() => {
history.push(`/remove/v1/${V1LiquidityBalance.token.address}`)
}}
>
Remove
</ButtonSecondary>
</RowBetween>
</AutoColumn>
</AutoColumn>
</HoverCard>
)
}
export default withRouter(V1PositionCard)

View File

@@ -17,12 +17,13 @@ import { AutoColumn } from '../Column'
import { ChevronDown, ChevronUp } from 'react-feather'
import { ButtonSecondary } from '../Button'
import { RowBetween, RowFixed, AutoRow } from '../Row'
import { Dots } from '../swap/styleds'
const FixedHeightRow = styled(RowBetween)`
export const FixedHeightRow = styled(RowBetween)`
height: 24px;
`
const HoverCard = styled(Card)`
export const HoverCard = styled(Card)`
border: 1px solid ${({ theme }) => theme.bg2};
:hover {
border: 1px solid ${({ theme }) => darken(0.06, theme.bg2)};
@@ -72,7 +73,7 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
<FixedHeightRow>
<RowFixed>
<Text fontWeight={500} fontSize={16}>
Your current position
Your position
</Text>
</RowFixed>
</FixedHeightRow>
@@ -96,7 +97,6 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
</Text>
{token0Deposited ? (
<RowFixed>
{!minimal && <TokenLogo address={token0?.address} />}
<Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}>
{token0Deposited?.toSignificant(6)}
</Text>
@@ -111,7 +111,6 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
</Text>
{token1Deposited ? (
<RowFixed>
{!minimal && <TokenLogo address={token1?.address} />}
<Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}>
{token1Deposited?.toSignificant(6)}
</Text>
@@ -134,7 +133,7 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
<RowFixed>
<DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={20} />
<Text fontWeight={500} fontSize={20}>
{token0?.symbol}/{token1?.symbol}
{!token0 || !token1 ? <Dots>Loading</Dots> : `${token0.symbol}/${token1.symbol}`}
</Text>
</RowFixed>
<RowFixed>
@@ -158,7 +157,7 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
{token0Deposited?.toSignificant(6)}
</Text>
{!minimal && <TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token0?.address} />}
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token0?.address} />
</RowFixed>
) : (
'-'
@@ -176,32 +175,28 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
{token1Deposited?.toSignificant(6)}
</Text>
{!minimal && <TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token1?.address} />}
<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>
<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'}>
<ExternalLink href={`https://uniswap.info/pair/${pair?.liquidityToken.address}`}>

View File

@@ -1,6 +1,8 @@
import React from 'react'
import { Text } from 'rebass'
import { COMMON_BASES } from '../../constants'
import { Token } from '@uniswap/sdk'
import { SUGGESTED_BASES } from '../../constants'
import { AutoColumn } from '../Column'
import QuestionHelper from '../QuestionHelper'
import { AutoRow } from '../Row'
@@ -25,7 +27,7 @@ export default function CommonBases({
<QuestionHelper text="These tokens are commonly used in pairs." />
</AutoRow>
<AutoRow gap="10px">
{COMMON_BASES[chainId]?.map(token => {
{(SUGGESTED_BASES[chainId] ?? []).map((token: Token) => {
return (
<BaseWrapper
gap="6px"

View File

@@ -25,13 +25,7 @@ export default function PairList({
}
return (
<FixedSizeList
itemSize={54}
height={500}
itemCount={pairs.length}
width="100%"
style={{ flex: '1', minHeight: 200 }}
>
<FixedSizeList itemSize={56} height={500} itemCount={pairs.length} width="100%" style={{ flex: '1' }}>
{({ index, style }) => {
const pair = pairs[index]

View File

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

View File

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

View File

@@ -0,0 +1,185 @@
import { 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 { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import Card from '../../components/Card'
import { useActiveWeb3React } from '../../hooks'
import { useAllTokens, useToken } from '../../hooks/Tokens'
import { useAllTokenBalancesTreatingWETHasETH, useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import { CloseIcon, LinkStyledButton } 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 { 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)
inputRef.current?.focus()
}, [setTooltipOpen])
const closeTooltip = useCallback(() => setTooltipOpen(false), [setTooltipOpen])
return (
<Modal
isOpen={isOpen}
onDismiss={onDismiss}
maxHeight={70}
initialFocusRef={isMobile ? undefined : inputRef}
minHeight={70}
>
<Column style={{ width: '100%' }}>
<PaddedColumn gap="20px">
<RowBetween>
<Text fontWeight={500} fontSize={16}>
Select a 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}
onBlur={closeTooltip}
/>
</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)
}
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 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 => {

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 { useActiveWeb3React } from '../../hooks'
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
import { DUMMY_PAIRS_TO_PIN } from '../../constants'
// 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) {
return balanceA.greaterThan(balanceB) ? -1 : balanceA.equalTo(balanceB) ? 0 : 1
} else if (balanceA && balanceA.greaterThan('0')) {
@@ -15,6 +16,26 @@ export function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount
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(
weth: Token | undefined,
balances: { [tokenAddress: string]: TokenAmount }

View File

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

View File

@@ -0,0 +1,253 @@
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)}>
<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)
}
: () => 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

@@ -78,10 +78,6 @@ const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean }
}
`
const SlippageSelector = styled.div`
padding: 0 20px;
`
export interface SlippageTabsProps {
rawSlippage: number
setRawSlippage: (rawSlippage: number) => void
@@ -146,15 +142,14 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
}
return (
<>
<RowFixed padding={'0 20px'}>
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
Set slippage tolerance
</TYPE.black>
<QuestionHelper text="Your transaction will revert if the price changes unfavorably by more than this percentage." />
</RowFixed>
<SlippageSelector>
<AutoColumn gap="md">
<AutoColumn gap="sm">
<RowFixed>
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
Slippage tolerance
</TYPE.black>
<QuestionHelper text="Your transaction will revert if the price changes unfavorably by more than this percentage." />
</RowFixed>
<RowBetween>
<Option
onClick={() => {
@@ -220,16 +215,16 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
: 'Your transaction may be frontrun'}
</RowBetween>
)}
</SlippageSelector>
</AutoColumn>
<AutoColumn gap="sm">
<RowFixed padding={'0 20px'}>
<TYPE.black fontSize={14} color={theme.text2}>
Deadline
<RowFixed>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
Transaction deadline
</TYPE.black>
<QuestionHelper text="Your transaction will revert if it is pending for more than this long." />
</RowFixed>
<RowFixed padding={'0 20px'}>
<RowFixed>
<OptionCustom style={{ width: '80px' }} tabIndex={-1}>
<Input
color={!!deadlineError ? 'red' : undefined}
@@ -246,6 +241,6 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
</TYPE.body>
</RowFixed>
</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'
const TOKEN_ICON_API = address =>
const getTokenLogoURL = address =>
`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 }>`
width: ${({ size }) => size};
@@ -44,20 +44,16 @@ export default function TokenLogo({
size?: string
style?: React.CSSProperties
}) {
const [error, setError] = useState(false)
const [, refresh] = useState<number>(0)
const { chainId } = useActiveWeb3React()
// mock rinkeby DAI
if (chainId === 4 && address === '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735') {
address = '0x6B175474E89094C44Da98b954EedeAC495271d0F'
}
let path = ''
const validated = isAddress(address)
// 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} />
} else if (!error && !BAD_IMAGES[address] && isAddress(address)) {
path = TOKEN_ICON_API(address)
} else if (!NO_LOGO_ADDRESSES[address] && validated) {
path = getTokenLogoURL(validated)
} else {
return (
<Emoji {...rest} size={size}>
@@ -75,8 +71,8 @@ export default function TokenLogo({
src={path}
size={size}
onError={() => {
BAD_IMAGES[address] = true
setError(true)
NO_LOGO_ADDRESSES[address] = true
refresh(i => i + 1)
}}
/>
)

View File

@@ -3,13 +3,12 @@ import { transparentize } from 'polished'
import React, { useMemo } from 'react'
import styled from 'styled-components'
import { ReactComponent as Close } from '../../assets/images/x.svg'
import { ALL_TOKENS } from '../../constants/tokens'
import { useActiveWeb3React } from '../../hooks'
import { useAllTokens } from '../../hooks/Tokens'
import { Field } from '../../state/swap/actions'
import { useTokenWarningDismissal } from '../../state/user/hooks'
import { ExternalLink, TYPE } from '../../theme'
import { getEtherscanLink } from '../../utils'
import { getEtherscanLink, isDefaultToken } from '../../utils'
import PropsOfExcluding from '../../utils/props-of-excluding'
import QuestionHelper from '../QuestionHelper'
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)};
position: relative;
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;
margin-bottom: 20px;
display: grid;
grid-template-rows: auto auto auto;
grid-template-rows: 14px auto auto;
grid-row-gap: 14px;
`
@@ -42,15 +41,15 @@ const CloseColor = styled(Close)`
const CloseIcon = styled.div`
position: absolute;
right: 1rem;
top: 14px;
top: 12px;
&:hover {
cursor: pointer;
opacity: 0.6;
}
& > * {
height: 14px;
width: 14px;
height: 16px;
width: 16px;
}
`
@@ -68,9 +67,8 @@ interface TokenWarningCardProps extends PropsOfExcluding<typeof Wrapper, 'error'
export default function TokenWarningCard({ token, ...rest }: TokenWarningCardProps) {
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 tokenName = token?.name?.toLowerCase() ?? ''
@@ -80,7 +78,7 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro
const allTokens = useAllTokens()
const duplicateNameOrSymbol = useMemo(() => {
if (isDefaultToken || !token || !chainId) return false
if (isDefault || !token || !chainId) return false
return Object.keys(allTokens).some(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
})
}, [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 (
<Wrapper error={duplicateNameOrSymbol} {...rest}>
@@ -111,7 +109,7 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro
? `${token.name} (${token.symbol})`
: token.name || token.symbol}
</div>
<ExternalLink style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'address')}>
<ExternalLink style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'token')}>
(View on Etherscan)
</ExternalLink>
</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 styled from 'styled-components'
import styled, { ThemeContext } from 'styled-components'
import { useActiveWeb3React } from '../../hooks'
import useInterval from '../../hooks/useInterval'
@@ -51,13 +51,16 @@ export default function TxnPopup({
isRunning ? delay : null
)
const handleMouseEnter = useCallback(() => setIsRunning(false), [])
const handleMouseLeave = useCallback(() => setIsRunning(true), [])
const theme = useContext(ThemeContext)
return (
<AutoRow onMouseEnter={() => setIsRunning(false)} onMouseLeave={() => setIsRunning(true)}>
{success ? (
<CheckCircle color={'#27AE60'} size={24} style={{ paddingRight: '24px' }} />
) : (
<AlertCircle color={'#FF6871'} size={24} style={{ paddingRight: '24px' }} />
)}
<AutoRow onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<div style={{ paddingRight: 16 }}>
{success ? <CheckCircle color={theme.green1} size={24} /> : <AlertCircle color={theme.red1} size={24} />}
</div>
<AutoColumn gap="8px">
<TYPE.body fontWeight={500}>
{summary ? summary : 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}

View File

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

View File

@@ -3,11 +3,9 @@ import React from 'react'
import styled from 'styled-components'
import Option from './Option'
import { SUPPORTED_WALLETS } from '../../constants'
import WalletConnectData from './WalletConnectData'
import { walletconnect, injected } from '../../connectors'
import { Spinner } from '../../theme'
import Circle from '../../assets/images/circle.svg'
import { injected } from '../../connectors'
import { darken } from 'polished'
import Loader from '../Loader'
const PendingSection = styled.div`
${({ theme }) => theme.flexColumnNoWrap};
@@ -19,14 +17,8 @@ const PendingSection = styled.div`
}
`
const SpinnerWrapper = styled(Spinner)`
font-size: 4rem;
const StyledLoader = styled(Loader)`
margin-right: 1rem;
svg {
path {
color: ${({ theme }) => theme.bg4};
}
}
`
const LoadingMessage = styled.div<{ error?: boolean }>`
@@ -72,28 +64,22 @@ const LoadingWrapper = styled.div`
`
export default function PendingView({
uri = '',
size,
connector,
error = false,
setPendingError,
tryActivation
}: {
uri?: string
size?: number
connector?: AbstractConnector
error?: boolean
setPendingError: (error: boolean) => void
tryActivation: (connector: AbstractConnector) => void
}) {
const isMetamask = window.ethereum && window.ethereum.isMetaMask
const isMetamask = window?.ethereum?.isMetaMask
return (
<PendingSection>
{!error && connector === walletconnect && <WalletConnectData size={size} uri={uri} />}
<LoadingMessage error={error}>
<LoadingWrapper>
{!error && <SpinnerWrapper src={Circle} />}
{error ? (
<ErrorGroup>
<div>Error connecting.</div>
@@ -106,10 +92,11 @@ export default function PendingView({
Try Again
</ErrorButton>
</ErrorGroup>
) : connector === walletconnect ? (
'Scan QR code with a compatible wallet...'
) : (
'Initializing...'
<>
<StyledLoader />
Initializing...
</>
)}
</LoadingWrapper>
</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 { isMobile } from 'react-device-detect'
import { UnsupportedChainIdError, useWeb3React } from '@web3-react/core'
import { URI_AVAILABLE } from '@web3-react/walletconnect-connector'
import usePrevious from '../../hooks/usePrevious'
import { useWalletModalOpen, useWalletModalToggle } from '../../state/application/hooks'
@@ -15,8 +14,9 @@ import { SUPPORTED_WALLETS } from '../../constants'
import { ExternalLink } from '../../theme'
import MetamaskIcon from '../../assets/images/metamask.png'
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 { WalletConnectConnector } from '@web3-react/walletconnect-connector'
const CloseIcon = styled.div`
position: absolute;
@@ -152,19 +152,6 @@ export default function WalletModal({
}
}, [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
const activePrevious = usePrevious(active)
const connectorPrevious = usePrevious(connector)
@@ -190,6 +177,12 @@ export default function WalletModal({
})
setPendingWallet(connector) // set wallet for pending view
setWalletView(WALLET_VIEWS.PENDING)
// if the connector is walletconnect and the user has already tried to connect, manually reset the connector
if (connector instanceof WalletConnectConnector && connector.walletConnectProvider?.wc?.uri) {
connector.walletConnectProvider = undefined
}
activate(connector, undefined, true).catch(error => {
if (error instanceof UnsupportedChainIdError) {
activate(connector) // a little janky...can't use setError because the connector isn't set
@@ -345,8 +338,6 @@ export default function WalletModal({
<ContentWrapper>
{walletView === WALLET_VIEWS.PENDING ? (
<PendingView
uri={uri}
size={220}
connector={pendingWallet}
error={pendingError}
setPendingError={setPendingError}

View File

@@ -5,9 +5,8 @@ import { useTranslation } from 'react-i18next'
import { network } from '../../connectors'
import { useEagerConnect, useInactiveListener } from '../../hooks'
import { Spinner } from '../../theme'
import Circle from '../../assets/images/circle.svg'
import { NetworkContextName } from '../../constants'
import Loader from '../Loader'
const MessageWrapper = styled.div`
display: flex;
@@ -20,16 +19,6 @@ const Message = styled.h2`
color: ${({ theme }) => theme.secondary1};
`
const SpinnerWrapper = styled(Spinner)`
font-size: 4rem;
svg {
path {
color: ${({ theme }) => theme.secondary1};
}
}
`
export default function Web3ReactManager({ children }) {
const { t } = useTranslation()
const { active } = useWeb3React()
@@ -78,7 +67,7 @@ export default function Web3ReactManager({ children }) {
if (!active && !networkActive) {
return showLoader ? (
<MessageWrapper>
<SpinnerWrapper src={Circle} />
<Loader />
</MessageWrapper>
) : null
}

View File

@@ -16,18 +16,12 @@ import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
import { Spinner } from '../../theme'
import LightCircle from '../../assets/svg/lightcircle.svg'
import { RowBetween } from '../Row'
import { shortenAddress } from '../../utils'
import { useAllTransactions } from '../../state/transactions/hooks'
import { NetworkContextName } from '../../constants'
import { injected, walletconnect, walletlink, fortmatic, portis } from '../../connectors'
const SpinnerWrapper = styled(Spinner)`
margin: 0 0.25rem 0 0.25rem;
`
import Loader from '../Loader'
const IconWrapper = styled.div<{ size?: number }>`
${({ theme }) => theme.flexColumnNoWrap};
@@ -189,7 +183,7 @@ export default function Web3Status() {
<Web3StatusConnected id="web3-status-connected" onClick={toggleWalletModal} pending={hasPendingTransactions}>
{hasPendingTransactions ? (
<RowBetween>
<Text>{pending?.length} Pending</Text> <SpinnerWrapper src={LightCircle} alt="loader" />
<Text>{pending?.length} Pending</Text> <Loader stroke="white" />
</RowBetween>
) : (
<Text>{ENSName || shortenAddress(account)}</Text>

View File

@@ -1,19 +1,16 @@
import { Trade, TradeType } from '@uniswap/sdk'
import React, { useContext } from 'react'
import { ChevronUp, ChevronRight } from 'react-feather'
import { Text, Flex } from 'rebass'
import { ThemeContext } from 'styled-components'
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 { AutoColumn } from '../Column'
import { SectionBreak } from './styleds'
import QuestionHelper from '../QuestionHelper'
import { RowBetween, RowFixed } from '../Row'
import SlippageTabs, { SlippageTabsProps } from '../SlippageTabs'
import FormattedPriceImpact from './FormattedPriceImpact'
import TokenLogo from '../TokenLogo'
import flatMap from 'lodash.flatmap'
import { SectionBreak } from './styleds'
import SwapRoute from './SwapRoute'
function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippage: number }) {
const theme = useContext(ThemeContext)
@@ -61,79 +58,37 @@ function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippag
</TYPE.black>
</RowBetween>
</AutoColumn>
<SectionBreak />
</>
)
}
export interface AdvancedSwapDetailsProps extends SlippageTabsProps {
export interface AdvancedSwapDetailsProps {
trade?: Trade
onDismiss: () => void
}
export function AdvancedSwapDetails({ trade, onDismiss, ...slippageTabProps }: AdvancedSwapDetailsProps) {
export function AdvancedSwapDetails({ trade }: AdvancedSwapDetailsProps) {
const theme = useContext(ThemeContext)
const [allowedSlippage] = useUserSlippageTolerance()
const showRoute = trade?.route?.path?.length > 2
return (
<AutoColumn gap="md">
<CursorPointer>
<RowBetween onClick={onDismiss} padding={'8px 20px'}>
<Text fontSize={16} color={theme.text2} fontWeight={500} style={{ userSelect: 'none' }}>
Hide Advanced
</Text>
<ChevronUp color={theme.text2} />
</RowBetween>
</CursorPointer>
<SectionBreak />
{trade && <TradeSummary trade={trade} allowedSlippage={slippageTabProps.rawSlippage} />}
<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>
{trade && <TradeSummary trade={trade} allowedSlippage={allowedSlippage} />}
{showRoute && (
<>
<SectionBreak />
<AutoColumn style={{ padding: '0 24px' }}>
<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>
<SwapRoute trade={trade} />
</AutoColumn>
</>
)}
</AutoColumn>
)

View File

@@ -1,35 +1,30 @@
import React, { useContext } from 'react'
import { ChevronDown } from 'react-feather'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import { CursorPointer } from '../../theme'
import { RowBetween } from '../Row'
import React from 'react'
import styled from 'styled-components'
import useLast from '../../hooks/useLast'
import { AdvancedSwapDetails, AdvancedSwapDetailsProps } from './AdvancedSwapDetails'
import { AdvancedDropdown } from './styleds'
export default function AdvancedSwapDetailsDropdown({
showAdvanced,
setShowAdvanced,
...rest
}: Omit<AdvancedSwapDetailsProps, 'onDismiss'> & {
showAdvanced: boolean
setShowAdvanced: (showAdvanced: boolean) => void
}) {
const theme = useContext(ThemeContext)
const AdvancedDetailsFooter = styled.div<{ show: boolean }>`
padding-top: calc(16px + 2rem);
padding-bottom: 20px;
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;
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 (
<AdvancedDropdown>
{showAdvanced ? (
<AdvancedSwapDetails {...rest} onDismiss={() => setShowAdvanced(false)} />
) : (
<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>
<AdvancedDetailsFooter show={Boolean(trade)}>
<AdvancedSwapDetails {...rest} trade={lastTrade} />
</AdvancedDetailsFooter>
)
}

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
}) {
const theme = useContext(ThemeContext)
if (!trade) {
return null
}
return (
<>
<AutoColumn gap="0px">

View File

@@ -11,29 +11,30 @@ import TokenLogo from '../TokenLogo'
import { TruncatedText } from './styleds'
export default function SwapModalHeader({
formattedAmounts,
tokens,
formattedAmounts,
slippageAdjustedAmounts,
priceImpactSeverity,
independentField
}: {
formattedAmounts?: { [field in Field]?: string }
tokens?: { [field in Field]?: Token }
slippageAdjustedAmounts?: { [field in Field]?: TokenAmount }
tokens: { [field in Field]?: Token }
formattedAmounts: { [field in Field]?: string }
slippageAdjustedAmounts: { [field in Field]?: TokenAmount }
priceImpactSeverity: number
independentField: Field
}) {
const theme = useContext(ThemeContext)
return (
<AutoColumn gap={'md'} style={{ marginTop: '20px' }}>
<RowBetween align="flex-end">
<TruncatedText fontSize={24} fontWeight={500}>
{!!formattedAmounts[Field.INPUT] && formattedAmounts[Field.INPUT]}
{formattedAmounts[Field.INPUT]}
</TruncatedText>
<RowFixed gap="4px">
<TokenLogo address={tokens[Field.INPUT]?.address} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{tokens[Field.INPUT]?.symbol || ''}
{tokens[Field.INPUT]?.symbol}
</Text>
</RowFixed>
</RowBetween>
@@ -42,12 +43,12 @@ export default function SwapModalHeader({
</RowFixed>
<RowBetween align="flex-end">
<TruncatedText fontSize={24} fontWeight={500} color={priceImpactSeverity > 2 ? theme.red1 : ''}>
{!!formattedAmounts[Field.OUTPUT] && formattedAmounts[Field.OUTPUT]}
{formattedAmounts[Field.OUTPUT]}
</TruncatedText>
<RowFixed gap="4px">
<TokenLogo address={tokens[Field.OUTPUT]?.address} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{tokens[Field.OUTPUT]?.symbol || ''}
{tokens[Field.OUTPUT]?.symbol}
</Text>
</RowFixed>
</RowBetween>
@@ -56,7 +57,7 @@ export default function SwapModalHeader({
<TYPE.italic textAlign="left" style={{ width: '100%' }}>
{`Output is estimated. You will receive at least `}
<b>
{slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} {tokens[Field.OUTPUT]?.symbol}{' '}
{slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} {tokens[Field.OUTPUT]?.symbol}
</b>
{' or the transaction will revert.'}
</TYPE.italic>

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 { Trade } from '@uniswap/sdk'
import { Price, Token } from '@uniswap/sdk'
import { useContext } from 'react'
import { Repeat } from 'react-feather'
import { Text } from 'rebass'
@@ -7,20 +7,19 @@ import { ThemeContext } from 'styled-components'
import { StyledBalanceMaxMini } from './styleds'
interface TradePriceProps {
trade?: Trade
price?: Price
inputToken?: Token
outputToken?: Token
showInverted: boolean
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 inputToken = trade?.inputAmount?.token
const outputToken = trade?.outputAmount?.token
const price = showInverted
? trade?.executionPrice?.toSignificant(6)
: trade?.executionPrice?.invert()?.toSignificant(6)
const formattedPrice = showInverted ? price?.toSignificant(6) : price?.invert()?.toSignificant(6)
const show = Boolean(inputToken && outputToken)
const label = showInverted
? `${outputToken?.symbol} per ${inputToken?.symbol}`
: `${inputToken?.symbol} per ${outputToken?.symbol}`
@@ -32,10 +31,16 @@ export default function TradePrice({ trade, showInverted, setShowInverted }: Tra
color={theme.text2}
style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }}
>
{price && `${price} ${label}`}
<StyledBalanceMaxMini onClick={() => setShowInverted(!showInverted)}>
<Repeat size={14} />
</StyledBalanceMaxMini>
{show ? (
<>
{formattedPrice ?? '-'} {label}
<StyledBalanceMaxMini onClick={() => setShowInverted(!showInverted)}>
<Repeat size={14} />
</StyledBalanceMaxMini>
</>
) : (
'-'
)}
</Text>
)
}

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

@@ -21,19 +21,6 @@ export const ArrowWrapper = styled.div`
}
`
export const AdvancedDropdown = styled.div`
padding-top: calc(10px + 2rem);
padding-bottom: 10px;
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`
height: 1px;
width: 100%;
@@ -45,9 +32,15 @@ export const BottomGrouping = styled.div`
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 }) =>
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)`
@@ -65,7 +58,7 @@ export const StyledNumerical = styled(NumericalInput)`
color: ${({ theme }) => theme.text4};
}
`
export const StyledBalanceMaxMini = styled.button<{ active?: boolean }>`
export const StyledBalanceMaxMini = styled.button`
height: 22px;
width: 22px;
background-color: ${({ theme }) => theme.bg2};

View File

@@ -27,7 +27,7 @@ export const injected = new InjectedConnector({
export const walletconnect = new WalletConnectConnector({
rpc: { 1: NETWORK_URL },
bridge: 'https://bridge.walletconnect.org',
qrcode: false,
qrcode: true,
pollingInterval: POLLING_INTERVAL
})

View File

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

View File

@@ -3,56 +3,12 @@
"constant": true,
"inputs": [],
"name": "name",
"outputs": [{ "name": "", "type": "bytes32" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"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" }
"outputs": [
{
"name": "",
"type": "bytes32"
}
],
"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,
"stateMutability": "view",
"type": "function"
@@ -61,48 +17,14 @@
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [{ "name": "", "type": "bytes32" }],
"outputs": [
{
"name": "",
"type": "bytes32"
}
],
"payable": false,
"stateMutability": "view",
"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

@@ -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 { 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
export const COMMON_BASES = {
[ChainId.MAINNET]: [
WETH[ChainId.MAINNET],
new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'),
new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C')
],
// a list of tokens by chain
type ChainTokenList = {
readonly [chainId in ChainId]: Token[]
}
const WETH_ONLY: ChainTokenList = {
[ChainId.MAINNET]: [WETH[ChainId.MAINNET]],
[ChainId.ROPSTEN]: [WETH[ChainId.ROPSTEN]],
[ChainId.RINKEBY]: [
WETH[ChainId.RINKEBY],
new Token(ChainId.RINKEBY, '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735', 18, 'DAI', 'Dai Stablecoin')
],
[ChainId.RINKEBY]: [WETH[ChainId.RINKEBY]],
[ChainId.GÖRLI]: [WETH[ChainId.GÖRLI]],
[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: {
connector: injected,
name: 'Injected',
@@ -41,9 +93,9 @@ const MAINNET_WALLETS = {
export const SUPPORTED_WALLETS =
process.env.REACT_APP_CHAIN_ID !== '1'
? MAINNET_WALLETS
? TESTNET_CAPABLE_WALLETS
: {
...MAINNET_WALLETS,
...TESTNET_CAPABLE_WALLETS,
...{
WALLET_CONNECT: {
connector: walletconnect,
@@ -51,7 +103,8 @@ export const SUPPORTED_WALLETS =
iconName: 'walletConnectIcon.svg',
description: 'Connect to Trust Wallet, Rainbow Wallet and more...',
href: null,
color: '#4196FC'
color: '#4196FC',
mobile: true
},
WALLET_LINK: {
connector: walletlink,
@@ -70,15 +123,6 @@ export const SUPPORTED_WALLETS =
mobile: 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: {
connector: 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)
// used for warning states
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_HIGH: Percent = new Percent(JSBI.BigInt(1000), BIPS_BASE) // 10%
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(500), BIPS_BASE) // 5%
// 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
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'
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 [
new Token(ChainId.MAINNET, '0xB6eD7644C69416d67B522e20bC294A9a9B405B31', 8, '0xBTC', '0xBitcoin Token'),
new Token(ChainId.MAINNET, '0xfC1E690f61EFd961294b3e1Ce3313fBD8aa4f85d', 18, 'aDAI', 'Aave Interest bearing DAI'),
@@ -7,8 +13,10 @@ export default [
new Token(ChainId.MAINNET, '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 9, 'AMPL', 'Ampleforth'),
new Token(ChainId.MAINNET, '0xcD62b1C403fa761BAadFC74C525ce2B51780b184', 18, 'ANJ', 'Aragon Network Juror'),
new Token(ChainId.MAINNET, '0x960b236A07cf122663c4303350609A66A7B288C0', 18, 'ANT', 'Aragon Network Token'),
new Token(ChainId.MAINNET, '0x27054b13b1B798B345b591a4d22e6562d47eA75a', 4, 'AST', 'AirSwap Token'),
new Token(ChainId.MAINNET, '0xBA11D00c5f74255f56a5E366F4F77f5A186d7f55', 18, 'BAND', 'BandToken'),
new Token(ChainId.MAINNET, '0x0D8775F648430679A709E98d2b0Cb6250d2887EF', 18, 'BAT', 'Basic Attention Token'),
new Token(ChainId.MAINNET, '0xba100000625a3754423978a60c9317c58a424e3D', 18, 'BAL', 'Balancer'),
new Token(ChainId.MAINNET, '0x107c4504cd79C5d2696Ea0030a8dD4e92601B82e', 18, 'BLT', 'Bloom Token'),
new Token(ChainId.MAINNET, '0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C', 18, 'BNT', 'Bancor Network Token'),
new Token(ChainId.MAINNET, '0x0327112423F3A68efdF1fcF402F6c5CB9f7C33fd', 18, 'BTC++', 'PieDAO BTC++'),
@@ -18,8 +26,9 @@ export default [
new Token(ChainId.MAINNET, '0x39AA39c021dfbaE8faC545936693aC917d5E7563', 8, 'cUSDC', 'Compound USD Coin'),
new Token(ChainId.MAINNET, '0xaaAEBE6Fe48E54f431b0C390CfaF0b017d09D42d', 4, 'CEL', 'Celsius'),
new Token(ChainId.MAINNET, '0x06AF07097C9Eeb7fD685c692751D5C66dB49c215', 18, 'CHAI', 'Chai'),
COMP,
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, '0xE0B7927c4aF23765Cb51314A0E0521A9645F0E2A', 9, 'DGD', 'DigixDAO'),
new Token(ChainId.MAINNET, '0x4f3AfEC4E5a3F2A6a1A411DEF7D7dFe50eE057bF', 9, 'DGX', 'Digix Gold Token'),
@@ -31,6 +40,7 @@ export default [
'Decentralized Insurance Protocol'
),
new Token(ChainId.MAINNET, '0xC0F9bD5Fa5698B6505F643900FFA515Ea5dF54A9', 18, 'DONUT', 'Donut'),
new Token(ChainId.MAINNET, '0x86FADb80d8D2cff3C3680819E4da99C10232Ba0F', 18, 'EBASE', 'EURBASE Stablecoin'),
new Token(ChainId.MAINNET, '0xF629cBd94d3791C9250152BD8dfBDF380E2a3B9c', 18, 'ENJ', 'Enjin Coin'),
new Token(ChainId.MAINNET, '0x06f65b8CfCb13a9FE37d836fE9708dA38Ecb29B2', 18, 'FAME', 'SAINT FAME: Genesis Shirt'),
new Token(ChainId.MAINNET, '0x4946Fcea7C692606e8908002e55A582af44AC121', 18, 'FOAM', 'FOAM Token'),
@@ -59,9 +69,10 @@ export default [
new Token(ChainId.MAINNET, '0xd15eCDCF5Ea68e3995b2D0527A0aE0a3258302F8', 18, 'MCX', 'MachiX Token'),
new Token(ChainId.MAINNET, '0xa3d58c4E56fedCae3a7c43A725aeE9A71F0ece4e', 18, 'MET', 'Metronome'),
new Token(ChainId.MAINNET, '0x80f222a749a2e18Eb7f676D371F19ad7EFEEe3b7', 18, 'MGN', 'Magnolia Token'),
new Token(ChainId.MAINNET, '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', 18, 'MKR', 'Maker'),
MKR,
new Token(ChainId.MAINNET, '0xec67005c4E498Ec7f55E092bd1d35cbC47C91892', 18, 'MLN', 'Melon Token'),
new Token(ChainId.MAINNET, '0x957c30aB0426e0C93CD8241E2c60392d08c6aC8e', 0, 'MOD', 'Modum Token'),
new Token(ChainId.MAINNET, '0xe2f2a5C287993345a840Db3B0845fbC70f5935a5', 18, 'mUSD', 'mStable USD'),
new Token(ChainId.MAINNET, '0xB62132e35a6c13ee1EE0f84dC5d40bad8d815206', 18, 'NEXO', 'Nexo'),
new Token(ChainId.MAINNET, '0x1776e1F26f98b1A5dF9cD347953a26dd3Cb46671', 18, 'NMR', 'Numeraire'),
new Token(ChainId.MAINNET, '0x985dd3D42De1e256d09e1c10F112bCCB8015AD41', 18, 'OCEAN', 'OceanToken'),
@@ -77,7 +88,10 @@ export default [
new Token(ChainId.MAINNET, '0xF970b8E36e23F7fC3FD752EeA86f8Be8D83375A6', 18, 'RCN', 'Ripio Credit Network Token'),
new Token(ChainId.MAINNET, '0x255Aa6DF07540Cb5d3d297f0D0D4D84cb52bc8e6', 18, 'RDN', 'Raiden Token'),
new Token(ChainId.MAINNET, '0x408e41876cCCDC0F92210600ef50372656052a38', 18, 'REN', 'Republic Token'),
new Token(ChainId.MAINNET, '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, '0x607F4C5BB672230e8672085532f7e901544a7375', 9, 'RLC', 'iEx.ec Network Token'),
new Token(ChainId.MAINNET, '0xB4EFd85c19999D84251304bDA99E90B92300Bd93', 18, 'RPL', 'Rocket Pool'),
@@ -89,6 +103,7 @@ export default [
new Token(ChainId.MAINNET, '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F', 18, 'SNX', 'Synthetix Network Token'),
new Token(ChainId.MAINNET, '0x23B608675a2B2fB1890d3ABBd85c5775c51691d5', 18, 'SOCKS', 'Unisocks Edition 0'),
new Token(ChainId.MAINNET, '0x42d6622deCe394b54999Fbd73D108123806f6a18', 18, 'SPANK', 'SPANK'),
new Token(ChainId.MAINNET, '0x0Ae055097C6d159879521C384F1D2123D1f195e6', 18, 'STAKE', 'STAKE'),
new Token(ChainId.MAINNET, '0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC', 8, 'STORJ', 'StorjToken'),
new Token(ChainId.MAINNET, '0x57Ab1ec28D129707052df4dF418D58a2D46d5f51', 18, 'sUSD', 'Synth sUSD'),
new Token(ChainId.MAINNET, '0x261EfCdD24CeA98652B9700800a13DfBca4103fF', 18, 'sXAU', 'Synth sXAU'),
@@ -104,10 +119,11 @@ export default [
new Token(ChainId.MAINNET, '0x0000000000085d4780B73119b644AE5ecd22b376', 18, 'TUSD', 'TrueUSD'),
new Token(ChainId.MAINNET, '0x8400D94A5cb0fa0D041a3788e395285d61c9ee5e', 8, 'UBT', 'UniBright'),
new Token(ChainId.MAINNET, '0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828', 18, 'UMA', 'UMA Voting Token v1'),
new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C'),
USDC,
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, '0x9A48BD0EC040ea4f1D3147C025cd4076A2e71e3e', 18, 'USD++', 'PieDAO USD++'),
new Token(ChainId.MAINNET, '0x8f3470A7388c05eE4e7AF3d01D8C722b0FF52374', 18, 'VERI', 'Veritaseum'),
new Token(ChainId.MAINNET, '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', 8, 'WBTC', 'Wrapped BTC'),
new Token(ChainId.MAINNET, '0x09fE5f0236F0Ea5D930197DCE254d77B04128075', 18, 'WCK', 'Wrapped CryptoKitties'),

View File

@@ -1,9 +1,17 @@
import { Interface } from '@ethersproject/abi'
import { ChainId } from '@uniswap/sdk'
import V1_EXCHANGE_ABI from './v1_exchange.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_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 { 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 { useSingleCallResult } from '../state/multicall/hooks'
import { useMultipleContractSingleData } from '../state/multicall/hooks'
const PAIR_INTERFACE = new Interface(IUniswapV2PairABI)
/*
* if loading, return undefined
* if no pair created yet, return null
* if pair already created (even if 0 reserves), return pair
*/
export function usePair(tokenA?: Token, tokenB?: Token): undefined | Pair | null {
const pairAddress = tokenA && tokenB && !tokenA.equals(tokenB) ? Pair.getAddress(tokenA, tokenB) : undefined
const contract = usePairContract(pairAddress, false)
const { result: reserves, loading } = useSingleCallResult(contract, 'getReserves')
export function usePairs(tokens: [Token | undefined, Token | undefined][]): (undefined | Pair | null)[] {
const pairAddresses = useMemo(
() =>
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(() => {
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()))
}, [loading, reserves, tokenA, tokenB])
return results.map((result, i) => {
const { result: reserves, loading } = result
const tokenA = tokens[i][0]
const tokenB = tokens[i][1]
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 { useActiveWeb3React } from '../hooks'
import { useAllTokens } from '../hooks/Tokens'
import { useV1FactoryContract } from '../hooks/useContract'
import { Version } from '../hooks/useToggledVersion'
import { NEVER_RELOAD, useSingleCallResult, useSingleContractMultipleData } from '../state/multicall/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 inputs = useMemo(() => [tokenAddress], [tokenAddress])
return useSingleCallResult(contract, 'getExchange', inputs)?.result?.[0]
}
class MockV1Pair extends Pair {
readonly isV1: true = true
}
class MockV1Pair extends Pair {}
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 = useV1PairAddress(isWETH ? undefined : token?.address)
const v1PairAddress = useV1ExchangeAddress(isWETH ? undefined : token?.address)
const tokenBalance = useTokenBalance(v1PairAddress, token)
const ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress ?? '']
@@ -30,42 +29,40 @@ function useMockV1Pair(token?: Token): MockV1Pair | 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
export function useAllTokenV1ExchangeAddresses(): string[] {
export function useAllTokenV1Exchanges(): { [exchangeAddress: string]: Token } {
const allTokens = useAllTokens()
const factory = useV1FactoryContract()
const args = useMemo(() => Object.keys(allTokens).map(tokenAddress => [tokenAddress]), [allTokens])
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
export function useUserProbablyHasV1Liquidity(): boolean | undefined {
const exchangeAddresses = useAllTokenV1ExchangeAddresses()
export function useUserHasLiquidityInAllTokens(): boolean | undefined {
const { account, chainId } = useActiveWeb3React()
const fakeTokens = useMemo(
() => (chainId ? exchangeAddresses.map(address => new Token(chainId, address, 18, 'UNI-V1')) : []),
[chainId, exchangeAddresses]
const exchanges = useAllTokenV1Exchanges()
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(
() =>
@@ -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,
input?: Token,
output?: Token,
exactAmount?: TokenAmount,
v2Trade?: Trade,
minimumDelta: Percent = new Percent('0')
): string | undefined {
inputToken?: Token,
outputToken?: Token,
exactAmount?: TokenAmount
): Trade | undefined {
const { chainId } = useActiveWeb3React()
const isMainnet: boolean = chainId === ChainId.MAINNET
// get the mock v1 pairs
const inputPair = useMockV1Pair(input)
const outputPair = useMockV1Pair(output)
const inputPair = useMockV1Pair(inputToken)
const outputPair = useMockV1Pair(outputToken)
const inputIsWETH = isMainnet && input?.equals(WETH[ChainId.MAINNET])
const outputIsWETH = isMainnet && output?.equals(WETH[ChainId.MAINNET])
const inputIsWETH = (inputToken && chainId && WETH[chainId] && inputToken.equals(WETH[chainId])) ?? false
const outputIsWETH = (outputToken && chainId && WETH[chainId] && outputToken.equals(WETH[chainId])) ?? false
// construct a direct or through ETH v1 route
let pairs: Pair[] = []
@@ -108,7 +104,7 @@ export function useV1TradeLinkIfBetter(
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
try {
v1Trade =
@@ -116,25 +112,53 @@ export function useV1TradeLinkIfBetter(
? new Trade(route, exactAmount, isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT)
: undefined
} catch {}
return v1Trade
}
let v1HasBetterTrade = false
if (v1Trade) {
if (isExactIn) {
// discount the v1 output amount by minimumDelta
const discountedV1Output = v1Trade?.outputAmount.multiply(new Percent('1').subtract(minimumDelta))
// 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 {
// inflate the v1 amount by minimumDelta
const inflatedV1Input = v1Trade?.inputAmount.multiply(new Percent('1').add(minimumDelta))
// check if the inflated v1 amount is still less than v2, short-circuiting if no v2 trade exists
v1HasBetterTrade = !v2Trade || inflatedV1Input.lessThan(v2Trade.inputAmount)
}
export function getTradeVersion(trade?: Trade): Version | undefined {
const isV1 = trade?.route?.pairs?.some(pair => pair instanceof MockV1Pair)
if (isV1) return Version.v1
if (isV1 === false) return Version.v2
return undefined
}
// returns the v1 exchange against which a trade should be executed
export function useV1TradeExchangeAddress(trade: Trade | undefined): string | undefined {
const tokenAddress: string | undefined = useMemo(() => {
const tradeVersion = getTradeVersion(trade)
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
? `https://v1.uniswap.exchange/swap?inputCurrency=${inputIsWETH ? 'ETH' : input.address}&outputCurrency=${
outputIsWETH ? 'ETH' : output.address
}`
: undefined
if (minimumDelta.equalTo(ZERO_PERCENT)) {
return tradeA.executionPrice.lessThan(tradeB.executionPrice)
} else {
return tradeA.executionPrice.raw.multiply(minimumDelta.add(ONE_HUNDRED_PERCENT)).lessThan(tradeB.executionPrice)
}
}

View File

@@ -1,10 +1,13 @@
import { parseBytes32String } from '@ethersproject/strings'
import { ChainId, Token, WETH } from '@uniswap/sdk'
import { useEffect, useMemo } from 'react'
import { useMemo } from 'react'
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 { useActiveWeb3React } from './index'
import { useBytes32TokenContract, useTokenContract } from './useContract'
export function useAllTokens(): { [address: string]: Token } {
const { chainId } = useActiveWeb3React()
@@ -35,36 +38,65 @@ export function useAllTokens(): { [address: string]: Token } {
}, [userAddedTokens, chainId])
}
export function useToken(tokenAddress?: string): Token | undefined {
const tokens = useAllTokens()
return useMemo(() => {
const validatedAddress = isAddress(tokenAddress)
if (!validatedAddress) return
return tokens[validatedAddress]
}, [tokens, tokenAddress])
// parse a name or symbol from a token response
const BYTES32_REGEX = /^0x[a-fA-F0-9]{64}$/
function parseStringOrBytes32(str: string | undefined, bytes32: string | undefined, defaultValue: string): string {
return str && str.length > 0
? str
: bytes32 && BYTES32_REGEX.test(bytes32)
? parseBytes32String(bytes32)
: defaultValue
}
// gets token information by address (typically user input) and
// automatically adds it for the user if the token address is valid
export function useTokenByAddressAndAutomaticallyAdd(tokenAddress?: string): Token | undefined {
const fetchTokenByAddress = useFetchTokenByAddress()
const addToken = useAddUserToken()
const token = useToken(tokenAddress)
// undefined if invalid or does not exist
// null if loading
// otherwise returns the token
export function useToken(tokenAddress?: string): Token | undefined | null {
const { chainId } = useActiveWeb3React()
const tokens = useAllTokens()
useEffect(() => {
if (!chainId || !isAddress(tokenAddress)) return
const weth = WETH[chainId as ChainId]
if (weth && weth.address === isAddress(tokenAddress)) return
const address = isAddress(tokenAddress)
if (tokenAddress && !token) {
fetchTokenByAddress(tokenAddress).then(token => {
if (token !== null) {
addToken(token)
}
})
const tokenContract = useTokenContract(address ? address : undefined, false)
const tokenContractBytes32 = useBytes32TokenContract(address ? address : undefined, false)
const token: Token | undefined = address ? tokens[address] : undefined
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 token
return undefined
}, [
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 { 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')
const USDC = new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C')
import { BASES_TO_CHECK_TRADES_AGAINST } from '../constants'
import { usePairs } from '../data/Reserves'
import { useActiveWeb3React } from './index'
function useAllCommonPairs(tokenA?: Token, tokenB?: Token): Pair[] {
const { chainId } = useActiveWeb3React()
// check for direct pair between tokens
const pairBetween = usePair(tokenA, tokenB)
const bases: Token[] = chainId ? BASES_TO_CHECK_TRADES_AGAINST[chainId] : []
// get token<->WETH pairs
const aToETH = usePair(tokenA, WETH[chainId as ChainId])
const bToETH = usePair(tokenB, WETH[chainId as ChainId])
// get token<->DAI pairs
const aToDAI = usePair(tokenA, chainId === ChainId.MAINNET ? DAI : undefined)
const bToDAI = usePair(tokenB, chainId === ChainId.MAINNET ? DAI : undefined)
// get token<->USDC pairs
const aToUSDC = usePair(tokenA, chainId === ChainId.MAINNET ? USDC : undefined)
const bToUSDC = usePair(tokenB, chainId === ChainId.MAINNET ? USDC : undefined)
// 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 allPairCombinations: [Token | undefined, Token | undefined][] = useMemo(
() => [
// the direct pair
[tokenA, tokenB],
// token A against all bases
...bases.map((base): [Token | undefined, Token | undefined] => [tokenA, base]),
// token B against all bases
...bases.map((base): [Token | undefined, Token | undefined] => [tokenB, base]),
// each base against all bases
...flatMap(bases, (base): [Token, Token][] => bases.map(otherBase => [base, otherBase]))
],
[tokenA, tokenB, bases]
)
const allPairs = usePairs(allPairCombinations)
// only pass along valid pairs, non-duplicated pairs
return useMemo(
() =>
[pairBetween, aToETH, bToETH, aToDAI, bToDAI, aToUSDC, bToUSDC, DAIToETH, USDCToETH, DAIToUSDC]
// filter out invalid pairs
.filter((p): p is Pair => !!p)
// filter out duplicated pairs
.filter(
(p, i, pairs) => i === pairs.findIndex(pair => pair?.liquidityToken.address === p.liquidityToken.address)
),
[pairBetween, aToETH, bToETH, aToDAI, bToDAI, aToUSDC, bToUSDC, DAIToETH, USDCToETH, DAIToUSDC]
Object.values(
allPairs
// filter out invalid pairs
.filter((p): p is Pair => !!p)
// filter out duplicated pairs
.reduce<{ [pairAddress: string]: Pair }>((memo, curr) => {
memo[curr.liquidityToken.address] = memo[curr.liquidityToken.address] ?? curr
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
*/
export function useTradeExactIn(amountIn?: TokenAmount, tokenOut?: Token): Trade | null {
const inputToken = amountIn?.token
const outputToken = tokenOut
const allowedPairs = useAllCommonPairs(inputToken, outputToken)
const allowedPairs = useAllCommonPairs(amountIn?.token, tokenOut)
return useMemo(() => {
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
}, [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
*/
export function useTradeExactOut(tokenIn?: Token, amountOut?: TokenAmount): Trade | null {
const inputToken = tokenIn
const outputToken = amountOut?.token
const allowedPairs = useAllCommonPairs(inputToken, outputToken)
const allowedPairs = useAllCommonPairs(tokenIn, amountOut?.token)
return useMemo(() => {
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
}, [allowedPairs, tokenIn, amountOut])

View File

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

View File

@@ -4,12 +4,14 @@ import { Trade, WETH, TokenAmount } from '@uniswap/sdk'
import { useCallback, useMemo } from 'react'
import { ROUTER_ADDRESS } from '../constants'
import { useTokenAllowance } from '../data/Allowances'
import { getTradeVersion, useV1TradeExchangeAddress } from '../data/V1'
import { Field } from '../state/swap/actions'
import { useTransactionAdder, useHasPendingApproval } from '../state/transactions/hooks'
import { computeSlippageAdjustedAmounts } from '../utils/prices'
import { calculateGasMargin } from '../utils'
import { useTokenContract } from './useContract'
import { useActiveWeb3React } from './index'
import { Version } from './useToggledVersion'
export enum ApprovalState {
UNKNOWN,
@@ -21,16 +23,16 @@ export enum ApprovalState {
// returns a variable indicating the state of the approval and a function which approves if necessary or early returns
export function useApproveCallback(
amountToApprove?: TokenAmount,
addressToApprove?: string
spender?: string
): [ApprovalState, () => Promise<void>] {
const { account } = useActiveWeb3React()
const currentAllowance = useTokenAllowance(amountToApprove?.token, account ?? undefined, addressToApprove)
const pendingApproval = useHasPendingApproval(amountToApprove?.token?.address)
const currentAllowance = useTokenAllowance(amountToApprove?.token, account ?? undefined, spender)
const pendingApproval = useHasPendingApproval(amountToApprove?.token?.address, spender)
// check the current approval status
const approval = useMemo(() => {
if (!amountToApprove) return ApprovalState.UNKNOWN
const approvalState: ApprovalState = useMemo(() => {
if (!amountToApprove || !spender) return ApprovalState.UNKNOWN
// we treat WETH as ETH which requires no approvals
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
@@ -38,13 +40,13 @@ export function useApproveCallback(
if (pendingApproval) return ApprovalState.PENDING
// amountToApprove will be defined if currentAllowance is
return currentAllowance.lessThan(amountToApprove) ? ApprovalState.NOT_APPROVED : ApprovalState.APPROVED
}, [amountToApprove, currentAllowance, pendingApproval])
}, [amountToApprove, currentAllowance, pendingApproval, spender])
const tokenContract = useTokenContract(amountToApprove?.token?.address)
const addTransaction = useTransactionAdder()
const approve = useCallback(async (): Promise<void> => {
if (approval !== ApprovalState.NOT_APPROVED) {
if (approvalState !== ApprovalState.NOT_APPROVED) {
console.error('approve was called unnecessarily')
return
}
@@ -59,30 +61,35 @@ export function useApproveCallback(
return
}
if (!spender) {
console.error('no spender')
return
}
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
useExact = true
return tokenContract.estimateGas.approve(addressToApprove, amountToApprove.raw.toString())
return tokenContract.estimateGas.approve(spender, amountToApprove.raw.toString())
})
return tokenContract
.approve(addressToApprove, useExact ? amountToApprove.raw.toString() : MaxUint256, {
.approve(spender, useExact ? amountToApprove.raw.toString() : MaxUint256, {
gasLimit: calculateGasMargin(estimatedGas)
})
.then((response: TransactionResponse) => {
addTransaction(response, {
summary: 'Approve ' + amountToApprove?.token?.symbol,
approvalOfToken: amountToApprove?.token?.address
summary: 'Approve ' + amountToApprove.token.symbol,
approval: { tokenAddress: amountToApprove.token.address, spender: spender }
})
})
.catch((error: Error) => {
console.debug('Failed to approve token', 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
@@ -91,5 +98,7 @@ export function useApproveCallbackFromTrade(trade?: Trade, allowedSlippage = 0)
() => (trade ? computeSlippageAdjustedAmounts(trade, allowedSlippage)[Field.INPUT] : undefined),
[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,8 +2,10 @@ import { Contract } from '@ethersproject/contracts'
import { ChainId } from '@uniswap/sdk'
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { useMemo } from 'react'
import { ERC20_BYTES32_ABI } from '../constants/abis/erc20'
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 { V1_EXCHANGE_ABI, V1_FACTORY_ABI, V1_FACTORY_ADDRESSES } from '../constants/v1'
import { MULTICALL_ABI, MULTICALL_NETWORKS } from '../constants/multicall'
import { getContract } from '../utils'
import { useActiveWeb3React } from './index'
@@ -25,18 +27,26 @@ function useContract(address?: string, ABI?: any, withSignerIfPossible = true):
export function useV1FactoryContract(): Contract | null {
const { chainId } = useActiveWeb3React()
return useContract(chainId === 1 ? V1_FACTORY_ADDRESS : undefined, V1_FACTORY_ABI, false)
return useContract(V1_FACTORY_ADDRESSES[chainId as ChainId], V1_FACTORY_ABI, false)
}
export function useV1ExchangeContract(address: string): Contract | null {
return useContract(address, V1_EXCHANGE_ABI, false)
export function useV2MigratorContract(): Contract | null {
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)
}
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)
}

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,15 +1,19 @@
import { BigNumber } from '@ethersproject/bignumber'
import { MaxUint256 } from '@ethersproject/constants'
import { Contract } from '@ethersproject/contracts'
import { ChainId, Token, Trade, TradeType, WETH } from '@uniswap/sdk'
import { ChainId, Trade, TradeType, WETH } from '@uniswap/sdk'
import { useMemo } from 'react'
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, ROUTER_ADDRESS } from '../constants'
import { useTokenAllowance } from '../data/Allowances'
import { getTradeVersion, useV1TradeExchangeAddress } from '../data/V1'
import { Field } from '../state/swap/actions'
import { useTransactionAdder } from '../state/transactions/hooks'
import { computeSlippageAdjustedAmounts } from '../utils/prices'
import { calculateGasMargin, getRouterContract, isAddress } from '../utils'
import { computeSlippageAdjustedAmounts } from '../utils/prices'
import { useActiveWeb3React } from './index'
import { useV1ExchangeContract } from './useContract'
import useENSName from './useENSName'
import { Version } from './useToggledVersion'
enum SwapType {
EXACT_TOKENS_FOR_TOKENS,
@@ -17,25 +21,37 @@ enum SwapType {
EXACT_ETH_FOR_TOKENS,
TOKENS_FOR_EXACT_TOKENS,
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 (tokens[Field.INPUT]?.equals(WETH[chainId as ChainId])) {
return SwapType.EXACT_ETH_FOR_TOKENS
} else if (tokens[Field.OUTPUT]?.equals(WETH[chainId as ChainId])) {
return SwapType.EXACT_TOKENS_FOR_ETH
if (inputWETH) {
return isV1 ? SwapType.V1_EXACT_ETH_FOR_TOKENS : SwapType.EXACT_ETH_FOR_TOKENS
} else if (outputWETH) {
return isV1 ? SwapType.V1_EXACT_TOKENS_FOR_ETH : SwapType.EXACT_TOKENS_FOR_ETH
} else {
return SwapType.EXACT_TOKENS_FOR_TOKENS
return isV1 ? SwapType.V1_EXACT_TOKENS_FOR_TOKENS : SwapType.EXACT_TOKENS_FOR_TOKENS
}
} else {
if (tokens[Field.INPUT]?.equals(WETH[chainId as ChainId])) {
return SwapType.ETH_FOR_EXACT_TOKENS
} else if (tokens[Field.OUTPUT]?.equals(WETH[chainId as ChainId])) {
return SwapType.TOKENS_FOR_EXACT_ETH
if (inputWETH) {
return isV1 ? SwapType.V1_ETH_FOR_EXACT_TOKENS : SwapType.ETH_FOR_EXACT_TOKENS
} else if (outputWETH) {
return isV1 ? SwapType.V1_TOKENS_FOR_EXACT_ETH : SwapType.TOKENS_FOR_EXACT_ETH
} else {
return SwapType.TOKENS_FOR_EXACT_TOKENS
return isV1 ? SwapType.V1_TOKENS_FOR_EXACT_TOKENS : SwapType.TOKENS_FOR_EXACT_TOKENS
}
}
}
@@ -44,18 +60,24 @@ function getSwapType(tokens: { [field in Field]?: Token }, isExactIn: boolean, c
// and the user has approved the slippage adjusted input amount for the trade
export function useSwapCallback(
trade?: Trade, // trade to execute, required
allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // in bips, optional
deadline: number = DEFAULT_DEADLINE_FROM_NOW, // in seconds from now, optional
allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // in bips
deadline: number = DEFAULT_DEADLINE_FROM_NOW, // in seconds from now
to?: string // recipient of output, optional
): null | (() => Promise<string>) {
const { account, chainId, library } = useActiveWeb3React()
const inputAllowance = useTokenAllowance(trade?.inputAmount?.token, account ?? undefined, ROUTER_ADDRESS)
const addTransaction = useTransactionAdder()
const recipient = to ? isAddress(to) : account
const ensName = useENSName(to)
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(() => {
if (!trade || !recipient) return null
if (!trade || !recipient || !tradeVersion) return null
// will always be defined
const {
@@ -78,23 +100,25 @@ export function useSwapCallback(
throw new Error('missing dependencies in onSwap callback')
}
const routerContract: Contract = getRouterContract(chainId, library, account)
const contract: Contract | null =
tradeVersion === Version.v2 ? getRouterContract(chainId, library, account) : v1Exchange
if (!contract) {
throw new Error('Failed to get a swap contract')
}
const path = trade.route.path.map(t => t.address)
const deadlineFromNow: number = Math.ceil(Date.now() / 1000) + deadline
const swapType = getSwapType(
{ [Field.INPUT]: trade.inputAmount.token, [Field.OUTPUT]: trade.outputAmount.token },
trade.tradeType === TradeType.EXACT_INPUT,
chainId as ChainId
)
const swapType = getSwapType(trade)
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) {
case SwapType.EXACT_TOKENS_FOR_TOKENS:
estimate = routerContract.estimateGas.swapExactTokensForTokens
method = routerContract.swapExactTokensForTokens
methodNames = ['swapExactTokensForTokens', 'swapExactTokensForTokensSupportingFeeOnTransferTokens']
args = [
slippageAdjustedInput.raw.toString(),
slippageAdjustedOutput.raw.toString(),
@@ -102,11 +126,9 @@ export function useSwapCallback(
recipient,
deadlineFromNow
]
value = null
break
case SwapType.TOKENS_FOR_EXACT_TOKENS:
estimate = routerContract.estimateGas.swapTokensForExactTokens
method = routerContract.swapTokensForExactTokens
methodNames = ['swapTokensForExactTokens']
args = [
slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(),
@@ -114,17 +136,14 @@ export function useSwapCallback(
recipient,
deadlineFromNow
]
value = null
break
case SwapType.EXACT_ETH_FOR_TOKENS:
estimate = routerContract.estimateGas.swapExactETHForTokens
method = routerContract.swapExactETHForTokens
methodNames = ['swapExactETHForTokens', 'swapExactETHForTokensSupportingFeeOnTransferTokens']
args = [slippageAdjustedOutput.raw.toString(), path, recipient, deadlineFromNow]
value = BigNumber.from(slippageAdjustedInput.raw.toString())
break
case SwapType.TOKENS_FOR_EXACT_ETH:
estimate = routerContract.estimateGas.swapTokensForExactETH
method = routerContract.swapTokensForExactETH
methodNames = ['swapTokensForExactETH']
args = [
slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(),
@@ -132,11 +151,9 @@ export function useSwapCallback(
recipient,
deadlineFromNow
]
value = null
break
case SwapType.EXACT_TOKENS_FOR_ETH:
estimate = routerContract.estimateGas.swapExactTokensForETH
method = routerContract.swapExactTokensForETH
methodNames = ['swapExactTokensForETH', 'swapExactTokensForETHSupportingFeeOnTransferTokens']
args = [
slippageAdjustedInput.raw.toString(),
slippageAdjustedOutput.raw.toString(),
@@ -144,58 +161,164 @@ export function useSwapCallback(
recipient,
deadlineFromNow
]
value = null
break
case SwapType.ETH_FOR_EXACT_TOKENS:
estimate = routerContract.estimateGas.swapETHForExactTokens
method = routerContract.swapETHForExactTokens
methodNames = ['swapETHForExactTokens']
args = [slippageAdjustedOutput.raw.toString(), path, recipient, deadlineFromNow]
value = BigNumber.from(slippageAdjustedInput.raw.toString())
break
case SwapType.V1_EXACT_ETH_FOR_TOKENS:
methodNames = ['ethToTokenTransferInput']
args = [slippageAdjustedOutput.raw.toString(), deadlineFromNow, recipient]
value = BigNumber.from(slippageAdjustedInput.raw.toString())
break
case SwapType.V1_EXACT_TOKENS_FOR_TOKENS:
methodNames = ['tokenToTokenTransferInput']
args = [
slippageAdjustedInput.raw.toString(),
slippageAdjustedOutput.raw.toString(),
1,
deadlineFromNow,
recipient,
trade.outputAmount.token.address
]
break
case SwapType.V1_EXACT_TOKENS_FOR_ETH:
methodNames = ['tokenToEthTransferOutput']
args = [
slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(),
deadlineFromNow,
recipient
]
break
case SwapType.V1_ETH_FOR_EXACT_TOKENS:
methodNames = ['ethToTokenTransferOutput']
args = [slippageAdjustedOutput.raw.toString(), deadlineFromNow, recipient]
value = BigNumber.from(slippageAdjustedInput.raw.toString())
break
case SwapType.V1_TOKENS_FOR_EXACT_ETH:
methodNames = ['tokenToEthTransferOutput']
args = [
slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(),
deadlineFromNow,
recipient
]
break
case SwapType.V1_TOKENS_FOR_EXACT_TOKENS:
methodNames = ['tokenToTokenTransferOutput']
args = [
slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(),
MaxUint256.toString(),
deadlineFromNow,
recipient,
trade.outputAmount.token.address
]
break
default:
throw new Error(`Unhandled swap type: ${swapType}`)
}
return estimate(...args, value ? { value } : {})
.then(estimatedGasLimit =>
method(...args, {
...(value ? { value } : {}),
gasLimit: calculateGasMargin(estimatedGasLimit)
})
const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all(
methodNames.map(methodName =>
contract.estimateGas[methodName](...args, value ? { value } : {})
.then(calculateGasMargin)
.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
})
.catch(error => {
console.error(`Swap or gas estimate failed`, error)
throw error
// we expect failures from left to right, so throw if we see failures
// from right to left
for (let i = 0; i < safeGasEstimates.length - 1; i++) {
// if the FoT method fails, but the regular method does not, we should not
// 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 ${ensName ?? recipient}`
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,
tradeVersion,
allowedSlippage,
chainId,
inputAllowance,
library,
account,
v1Exchange,
deadline,
addTransaction,
ensName
])
}

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 LanguageDetector from 'i18next-browser-languagedetector'
const LOAD_PATH: string = process.env.PUBLIC_URL === '.' ? `./locales/{{lng}}.json` : '/locales/{{lng}}.json'
i18next
.use(XHR)
.use(LanguageDetector)
.use(initReactI18next)
.init({
backend: {
loadPath: LOAD_PATH
loadPath: `./locales/{{lng}}.json`
},
react: {
useSuspense: true

View File

@@ -6,6 +6,7 @@ import ReactDOM from 'react-dom'
import ReactGA from 'react-ga'
import { Provider } from 'react-redux'
import { NetworkContextName } from './constants'
import 'inter-ui'
import './i18n'
import App from './pages/App'
import store from './state'
@@ -18,7 +19,9 @@ import ThemeProvider, { FixedGlobalStyle, ThemedGlobalStyle } from './theme'
const Web3ProviderNetwork = createWeb3ReactRoot(NetworkContextName)
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

View File

@@ -17,7 +17,7 @@ import Row, { AutoRow, RowBetween, RowFixed, RowFlat } from '../../components/Ro
import TokenLogo from '../../components/TokenLogo'
import { ROUTER_ADDRESS, MIN_ETH, ONE_BIPS, DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
import { ROUTER_ADDRESS, MIN_ETH, ONE_BIPS } from '../../constants'
import { useActiveWeb3React } from '../../hooks'
import { useTransactionAdder } from '../../state/transactions/hooks'
@@ -34,9 +34,9 @@ import {
import { Field } from '../../state/mint/actions'
import { useApproveCallback, ApprovalState } from '../../hooks/useApproveCallback'
import { useWalletModalToggle } from '../../state/application/hooks'
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
import { useUserSlippageTolerance, useUserDeadline, useIsExpertMode } from '../../state/user/hooks'
export default function AddLiquidity({ match: { params }, history }: RouteComponentProps<{ tokens: string }>) {
export default function AddLiquidity({ match: { params } }: RouteComponentProps<{ tokens: string }>) {
useDefaultsFromURLMatchParams(params)
const { account, chainId, library } = useActiveWeb3React()
@@ -45,6 +45,8 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
// toggle wallet when disconnected
const toggleWalletModal = useWalletModalToggle()
const expertMode = useIsExpertMode()
// mint state
const { independentField, typedValue, otherTypedValue } = useMintState()
const {
@@ -65,14 +67,12 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showAdvanced, setShowAdvanced] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirm
const [pendingConfirmation, setPendingConfirmation] = useState<boolean>(true) // waiting for user confirmation
// txn values
const [deadline] = useUserDeadline() // custom from users settings
const [allowedSlippage] = useUserSlippageTolerance() // custom from users
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
const formattedAmounts = {
@@ -114,8 +114,6 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
const addTransaction = useTransactionAdder()
async function onAdd() {
setAttemptingTxn(true)
const router = getRouterContract(chainId, library, account)
const amountsMin = {
@@ -155,12 +153,15 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
value = null
}
setAttemptingTxn(true)
await estimate(...args, value ? { value } : {})
.then(estimatedGasLimit =>
method(...args, {
...(value ? { value } : {}),
gasLimit: calculateGasMargin(estimatedGasLimit)
}).then(response => {
setAttemptingTxn(false)
addTransaction(response, {
summary:
'Add ' +
@@ -174,7 +175,6 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
})
setTxHash(response.hash)
setPendingConfirmation(false)
ReactGA.event({
category: 'Liquidity',
@@ -183,12 +183,12 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
})
})
)
.catch((e: Error) => {
console.error(e)
setPendingConfirmation(true)
.catch(error => {
setAttemptingTxn(false)
setShowConfirm(false)
setShowAdvanced(false)
// we only care if the error is something _other_ than the user rejected the tx
if (error?.code !== 4001) {
console.error(error)
}
})
}
@@ -270,8 +270,8 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
const PriceBar = () => {
return (
<AutoColumn gap="md" justify="space-between">
<AutoRow justify="space-between">
<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}>
@@ -311,17 +311,15 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
<ConfirmationModal
isOpen={showConfirm}
onDismiss={() => {
if (attemptingTxn) {
history.push('/pool')
return
}
setPendingConfirmation(true)
setAttemptingTxn(false)
setShowConfirm(false)
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.TOKEN_A, '')
}
setTxHash('')
}}
attemptingTxn={attemptingTxn}
pendingConfirmation={pendingConfirmation}
hash={txHash ? txHash : ''}
hash={txHash}
topContent={() => modalHeader()}
bottomContent={modalBottom}
pendingText={pendingText}
@@ -392,50 +390,59 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
{!account ? (
<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
onClick={() => {
setShowConfirm(true)
}}
disabled={!isValid}
error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]}
>
<Text fontSize={20} fontWeight={500}>
{error ?? 'Supply'}
</Text>
</ButtonError>
<AutoColumn gap={'md'}>
{(approvalA === ApprovalState.NOT_APPROVED ||
approvalA === ApprovalState.PENDING ||
approvalB === ApprovalState.NOT_APPROVED ||
approvalB === ApprovalState.PENDING) &&
isValid && (
<RowBetween>
{approvalA !== ApprovalState.APPROVED && (
<ButtonPrimary
onClick={approveACallback}
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>
</Wrapper>
</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 ? (
<AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
<PositionCard pair={pair} minimal={true} />

View File

@@ -1,14 +1,16 @@
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 GoogleAnalyticsReporter from '../components/analytics/GoogleAnalyticsReporter'
import Footer from '../components/Footer'
import Header from '../components/Header'
import Popups from '../components/Popups'
import Web3ReactManager from '../components/Web3ReactManager'
import DarkModeQueryParamReader from '../theme/DarkModeQueryParamReader'
import AddLiquidity from './AddLiquidity'
import CreatePool from './CreatePool'
import MigrateV1 from './MigrateV1'
import MigrateV1Exchange from './MigrateV1/MigrateV1Exchange'
import RemoveV1Exchange from './MigrateV1/RemoveV1Exchange'
import Pool from './Pool'
import PoolFinder from './PoolFinder'
import RemoveLiquidity from './RemoveLiquidity'
@@ -47,40 +49,14 @@ const BodyWrapper = styled.div`
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`
margin-top: 5rem;
`
let Router: React.ComponentType
if (process.env.PUBLIC_URL === '.') {
Router = HashRouter
} else {
Router = BrowserRouter
}
export default function App() {
return (
<Suspense fallback={null}>
<Router>
<HashRouter>
<Route component={GoogleAnalyticsReporter} />
<Route component={DarkModeQueryParamReader} />
<AppWrapper>
@@ -99,15 +75,16 @@ export default function App() {
<Route exact strict path="/create" component={CreatePool} />
<Route exact strict path="/add/:tokens" component={AddLiquidity} />
<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} />
</Switch>
</Web3ReactManager>
<Marginer />
<Footer />
</BodyWrapper>
<BackgroundGradient />
</AppWrapper>
</Router>
</HashRouter>
</Suspense>
)
}

View File

@@ -2,7 +2,7 @@ import React from 'react'
import styled from 'styled-components'
import NavigationTabs from '../components/NavigationTabs'
const Body = styled.div`
export const BodyWrapper = styled.div`
position: relative;
max-width: 420px;
width: 100%;
@@ -18,9 +18,9 @@ const Body = styled.div`
*/
export default function AppBody({ children }: { children: React.ReactNode }) {
return (
<Body>
<BodyWrapper>
<NavigationTabs />
<>{children}</>
</Body>
</BodyWrapper>
)
}

View File

@@ -5,12 +5,12 @@ import AppBody from '../AppBody'
import Row, { AutoRow } from '../../components/Row'
import TokenLogo from '../../components/TokenLogo'
import SearchModal from '../../components/SearchModal'
import TokenSearchModal from '../../components/SearchModal/TokenSearchModal'
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 { ButtonPrimary, ButtonDropdown, ButtonDropdownLight } from '../../components/Button'
import { useToken } from '../../hooks/Tokens'
import { useActiveWeb3React } from '../../hooks'
@@ -59,16 +59,16 @@ export default function CreatePool({ location }: RouteComponentProps) {
<AutoColumn gap="20px">
<AutoColumn gap="24px">
{!token0Address ? (
<ButtonDropwdown
<ButtonDropdown
onClick={() => {
setShowSearch(true)
setActiveField(Fields.TOKEN0)
}}
>
<Text fontSize={20}>Select first token</Text>
</ButtonDropwdown>
</ButtonDropdown>
) : (
<ButtonDropwdownLight
<ButtonDropdownLight
onClick={() => {
setShowSearch(true)
setActiveField(Fields.TOKEN0)
@@ -83,13 +83,13 @@ export default function CreatePool({ location }: RouteComponentProps) {
{token0?.address === WETH[chainId]?.address && '(default)'}
</TYPE.darkGray>
</Row>
</ButtonDropwdownLight>
</ButtonDropdownLight>
)}
<ColumnCenter>
<Plus size="16" color="#888D9B" />
</ColumnCenter>
{!token1Address ? (
<ButtonDropwdown
<ButtonDropdown
onClick={() => {
setShowSearch(true)
setActiveField(Fields.TOKEN1)
@@ -97,9 +97,9 @@ export default function CreatePool({ location }: RouteComponentProps) {
disabled={step !== STEP.SELECT_TOKENS}
>
<Text fontSize={20}>Select second token</Text>
</ButtonDropwdown>
</ButtonDropdown>
) : (
<ButtonDropwdownLight
<ButtonDropdownLight
onClick={() => {
setShowSearch(true)
setActiveField(Fields.TOKEN1)
@@ -111,7 +111,7 @@ export default function CreatePool({ location }: RouteComponentProps) {
{token1?.symbol}
</Text>
</Row>
</ButtonDropwdownLight>
</ButtonDropdownLight>
)}
{pair ? ( // pair already exists - prompt to add liquidity to existing pool
<AutoRow padding="10px" justify="center">
@@ -128,9 +128,8 @@ export default function CreatePool({ location }: RouteComponentProps) {
</ButtonPrimary>
)}
</AutoColumn>
<SearchModal
<TokenSearchModal
isOpen={showSearch}
filterType="tokens"
onTokenSelect={address => {
activeField === Fields.TOKEN0 ? setToken0Address(address) : setToken1Address(address)
}}

View File

@@ -0,0 +1,11 @@
import React from 'react'
import { AutoColumn } from '../../components/Column'
import { TYPE } from '../../theme'
export function EmptyState({ message }: { message: string }) {
return (
<AutoColumn style={{ minHeight: 200, justifyContent: 'center', alignItems: 'center' }}>
<TYPE.body>{message}</TYPE.body>
</AutoColumn>
)
}

View File

@@ -0,0 +1,395 @@
import { TransactionResponse } from '@ethersproject/abstract-provider'
import { ChainId, Fraction, JSBI, Percent, Token, TokenAmount, WETH } from '@uniswap/sdk'
import React, { useCallback, useMemo, useState } from 'react'
import { ArrowLeft } from 'react-feather'
import ReactGA from 'react-ga'
import { Redirect, RouteComponentProps } from 'react-router'
import { ButtonConfirmed } from '../../components/Button'
import { PinkCard, YellowCard, LightCard } from '../../components/Card'
import { AutoColumn } from '../../components/Column'
import QuestionHelper from '../../components/QuestionHelper'
import { AutoRow, RowBetween, RowFixed } from '../../components/Row'
import { Dots } from '../../components/swap/styleds'
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
import { MIGRATOR_ADDRESS } from '../../constants/abis/migrator'
import { usePair } from '../../data/Reserves'
import { useTotalSupply } from '../../data/TotalSupply'
import { useActiveWeb3React } from '../../hooks'
import { useToken } from '../../hooks/Tokens'
import { ApprovalState, useApproveCallback } from '../../hooks/useApproveCallback'
import { useV1ExchangeContract, useV2MigratorContract } from '../../hooks/useContract'
import { NEVER_RELOAD, useSingleCallResult } from '../../state/multicall/hooks'
import { useIsTransactionPending, useTransactionAdder } from '../../state/transactions/hooks'
import { useETHBalances, useTokenBalance } from '../../state/wallet/hooks'
import { TYPE, ExternalLink } from '../../theme'
import { isAddress, getEtherscanLink } from '../../utils'
import { BodyWrapper } from '../AppBody'
import { EmptyState } from './EmptyState'
import TokenLogo from '../../components/TokenLogo'
import { AddressZero } from '@ethersproject/constants'
import { Text } from 'rebass'
const POOL_TOKEN_AMOUNT_MIN = new Fraction(JSBI.BigInt(1), JSBI.BigInt(1000000))
const WEI_DENOM = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(18))
const ZERO = JSBI.BigInt(0)
const ONE = JSBI.BigInt(1)
const ZERO_FRACTION = new Fraction(ZERO, ONE)
const ALLOWED_OUTPUT_MIN_PERCENT = new Percent(JSBI.BigInt(10000 - INITIAL_ALLOWED_SLIPPAGE), JSBI.BigInt(10000))
function FormattedPoolTokenAmount({ tokenAmount }: { tokenAmount: TokenAmount }) {
return (
<>
{tokenAmount.equalTo(JSBI.BigInt(0))
? '0'
: tokenAmount.greaterThan(POOL_TOKEN_AMOUNT_MIN)
? tokenAmount.toSignificant(4)
: `<${POOL_TOKEN_AMOUNT_MIN.toSignificant(1)}`}
</>
)
}
export function V1LiquidityInfo({
token,
liquidityTokenAmount,
tokenWorth,
ethWorth
}: {
token: Token
liquidityTokenAmount: TokenAmount
tokenWorth: TokenAmount
ethWorth: Fraction
}) {
const { chainId } = useActiveWeb3React()
return (
<>
<AutoRow style={{ justifyContent: 'flex-start', width: 'fit-content' }}>
<TokenLogo size="24px" address={token.address} />
<div style={{ marginLeft: '.75rem' }}>
<TYPE.mediumHeader>
{<FormattedPoolTokenAmount tokenAmount={liquidityTokenAmount} />}{' '}
{token.equals(WETH[chainId]) ? 'WETH' : token.symbol}/ETH
</TYPE.mediumHeader>
</div>
</AutoRow>
<RowBetween my="1rem">
<Text fontSize={16} fontWeight={500}>
Pooled {token.equals(WETH[chainId]) ? 'WETH' : token.symbol}:
</Text>
<RowFixed>
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
{tokenWorth.toSignificant(4)}
</Text>
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token.address} />
</RowFixed>
</RowBetween>
<RowBetween mb="1rem">
<Text fontSize={16} fontWeight={500}>
Pooled ETH:
</Text>
<RowFixed>
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
{ethWorth.toSignificant(4)}
</Text>
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={WETH[chainId].address} />
</RowFixed>
</RowBetween>
</>
)
}
function V1PairMigration({ liquidityTokenAmount, token }: { liquidityTokenAmount: TokenAmount; token: Token }) {
const { account, chainId } = useActiveWeb3React()
const totalSupply = useTotalSupply(liquidityTokenAmount.token)
const exchangeETHBalance = useETHBalances([liquidityTokenAmount.token.address])?.[liquidityTokenAmount.token.address]
const exchangeTokenBalance = useTokenBalance(liquidityTokenAmount.token.address, token)
const v2Pair = usePair(WETH[chainId as ChainId], token)
const isFirstLiquidityProvider: boolean = v2Pair === null
const v2SpotPrice = v2Pair?.reserveOf(token)?.divide(v2Pair?.reserveOf(WETH[chainId as ChainId]))
const [confirmingMigration, setConfirmingMigration] = useState<boolean>(false)
const [pendingMigrationHash, setPendingMigrationHash] = useState<string | null>(null)
const shareFraction: Fraction = totalSupply ? new Percent(liquidityTokenAmount.raw, totalSupply.raw) : ZERO_FRACTION
const ethWorth: Fraction = exchangeETHBalance
? new Fraction(shareFraction.multiply(exchangeETHBalance).quotient, WEI_DENOM)
: ZERO_FRACTION
const tokenWorth: TokenAmount = exchangeTokenBalance
? new TokenAmount(token, shareFraction.multiply(exchangeTokenBalance.raw).quotient)
: new TokenAmount(token, ZERO)
const [approval, approve] = useApproveCallback(liquidityTokenAmount, MIGRATOR_ADDRESS)
const v1SpotPrice =
exchangeTokenBalance && exchangeETHBalance
? exchangeTokenBalance.divide(new Fraction(exchangeETHBalance, WEI_DENOM))
: null
const priceDifferenceFraction: Fraction | undefined =
v1SpotPrice && v2SpotPrice
? v1SpotPrice
.divide(v2SpotPrice)
.multiply('100')
.subtract('100')
: undefined
const priceDifferenceAbs: Fraction | undefined = priceDifferenceFraction?.lessThan(ZERO)
? priceDifferenceFraction?.multiply('-1')
: priceDifferenceFraction
const minAmountETH: JSBI | undefined =
v2SpotPrice && tokenWorth
? tokenWorth
.divide(v2SpotPrice)
.multiply(WEI_DENOM)
.multiply(ALLOWED_OUTPUT_MIN_PERCENT).quotient
: ethWorth?.numerator
const minAmountToken: JSBI | undefined =
v2SpotPrice && ethWorth
? ethWorth
.multiply(v2SpotPrice)
.multiply(JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(token.decimals)))
.multiply(ALLOWED_OUTPUT_MIN_PERCENT).quotient
: tokenWorth?.numerator
const addTransaction = useTransactionAdder()
const isMigrationPending = useIsTransactionPending(pendingMigrationHash)
const migrator = useV2MigratorContract()
const migrate = useCallback(() => {
if (!minAmountToken || !minAmountETH) return
setConfirmingMigration(true)
migrator
.migrate(
token.address,
minAmountToken.toString(),
minAmountETH.toString(),
account,
Math.floor(new Date().getTime() / 1000) + DEFAULT_DEADLINE_FROM_NOW
)
.then((response: TransactionResponse) => {
ReactGA.event({
category: 'Migrate',
action: 'V1->V2',
label: token?.symbol
})
addTransaction(response, {
summary: `Migrate ${token.symbol} liquidity to V2`
})
setPendingMigrationHash(response.hash)
})
.catch(() => {
setConfirmingMigration(false)
})
}, [minAmountToken, minAmountETH, migrator, token, account, addTransaction])
const noLiquidityTokens = !!liquidityTokenAmount && liquidityTokenAmount.equalTo(ZERO)
const largePriceDifference = !!priceDifferenceAbs && !priceDifferenceAbs.lessThan(JSBI.BigInt(5))
const isSuccessfullyMigrated = !!pendingMigrationHash && !!noLiquidityTokens
return (
<AutoColumn gap="20px">
<TYPE.body my={9} style={{ fontWeight: 400 }}>
This tool will safely migrate your V1 liquidity to V2 with minimal price risk. The process is completely
trustless thanks to the{' '}
<ExternalLink href={getEtherscanLink(chainId, MIGRATOR_ADDRESS, 'address')}>
<TYPE.blue display="inline">Uniswap migration contract</TYPE.blue>
</ExternalLink>
.
</TYPE.body>
{!isFirstLiquidityProvider && largePriceDifference ? (
<YellowCard>
<TYPE.body style={{ marginBottom: 8, fontWeight: 400 }}>
It{"'"}s best to deposit liquidity into Uniswap V2 at a price you believe is correct. If the V2 price seems
incorrect, you can either make a swap to move the price or wait for someone else to do so.
</TYPE.body>
<AutoColumn gap="8px">
<RowBetween>
<TYPE.body>V1 Price:</TYPE.body>
<TYPE.black>
{v1SpotPrice?.toSignificant(6)} {token.symbol}/ETH
</TYPE.black>
</RowBetween>
<RowBetween>
<div />
<TYPE.black>
{v1SpotPrice?.invert()?.toSignificant(6)} ETH/{token.symbol}
</TYPE.black>
</RowBetween>
<RowBetween>
<TYPE.body>V2 Price:</TYPE.body>
<TYPE.black>
{v2SpotPrice?.toSignificant(6)} {token.symbol}/ETH
</TYPE.black>
</RowBetween>
<RowBetween>
<div />
<TYPE.black>
{v2SpotPrice?.invert()?.toSignificant(6)} ETH/{token.symbol}
</TYPE.black>
</RowBetween>
<RowBetween>
<TYPE.body color="inherit">Price Difference:</TYPE.body>
<TYPE.black color="inherit">{priceDifferenceAbs.toSignificant(4)}%</TYPE.black>
</RowBetween>
</AutoColumn>
</YellowCard>
) : null}
{isFirstLiquidityProvider && (
<PinkCard>
<TYPE.body style={{ marginBottom: 8, fontWeight: 400 }}>
You are the first liquidity provider for this pair on Uniswap V2. Your liquidity will be migrated at the
current V1 price. Your transaction cost also includes the gas to create the pool.
</TYPE.body>
<AutoColumn gap="8px">
<RowBetween>
<TYPE.body>V1 Price:</TYPE.body>
<TYPE.black>
{v1SpotPrice?.toSignificant(6)} {token.symbol}/ETH
</TYPE.black>
</RowBetween>
<RowBetween>
<div />
<TYPE.black>
{v1SpotPrice?.invert()?.toSignificant(6)} ETH/{token.symbol}
</TYPE.black>
</RowBetween>
</AutoColumn>
</PinkCard>
)}
<LightCard>
<V1LiquidityInfo
token={token}
liquidityTokenAmount={liquidityTokenAmount}
tokenWorth={tokenWorth}
ethWorth={ethWorth}
/>
<div style={{ display: 'flex', marginTop: '1rem' }}>
<AutoColumn gap="12px" style={{ flex: '1', marginRight: 12 }}>
<ButtonConfirmed
confirmed={approval === ApprovalState.APPROVED}
disabled={approval !== ApprovalState.NOT_APPROVED}
onClick={approve}
>
{approval === ApprovalState.PENDING ? (
<Dots>Approving</Dots>
) : approval === ApprovalState.APPROVED ? (
'Approved'
) : (
'Approve'
)}
</ButtonConfirmed>
</AutoColumn>
<AutoColumn gap="12px" style={{ flex: '1' }}>
<ButtonConfirmed
confirmed={isSuccessfullyMigrated}
disabled={
isSuccessfullyMigrated ||
noLiquidityTokens ||
isMigrationPending ||
approval !== ApprovalState.APPROVED ||
confirmingMigration
}
onClick={migrate}
>
{isSuccessfullyMigrated ? 'Success' : isMigrationPending ? <Dots>Migrating</Dots> : 'Migrate'}
</ButtonConfirmed>
</AutoColumn>
</div>
</LightCard>
<TYPE.darkGray style={{ textAlign: 'center' }}>
{`Your Uniswap V1 ${token.symbol}/ETH liquidity will become Uniswap V2 ${token.symbol}/ETH liquidity.`}
</TYPE.darkGray>
</AutoColumn>
)
}
export default function MigrateV1Exchange({
history,
match: {
params: { address }
}
}: RouteComponentProps<{ address: string }>) {
const validatedAddress = isAddress(address)
const { chainId, account } = useActiveWeb3React()
const exchangeContract = useV1ExchangeContract(validatedAddress ? validatedAddress : undefined)
const tokenAddress = useSingleCallResult(exchangeContract, 'tokenAddress', undefined, NEVER_RELOAD)?.result?.[0]
const token = useToken(tokenAddress)
const liquidityToken: Token | undefined = useMemo(
() =>
validatedAddress && token
? new Token(chainId, validatedAddress, 18, `UNI-V1-${token.symbol}`, 'Uniswap V1')
: undefined,
[chainId, validatedAddress, token]
)
const userLiquidityBalance = useTokenBalance(account, liquidityToken)
const handleBack = useCallback(() => {
history.push('/migrate/v1')
}, [history])
// redirect for invalid url params
if (!validatedAddress || tokenAddress === AddressZero) {
console.error('Invalid address in path', address)
return <Redirect to="/migrate/v1" />
}
return (
<BodyWrapper style={{ padding: 24 }}>
<AutoColumn gap="16px">
<AutoRow style={{ alignItems: 'center', justifyContent: 'space-between' }} gap="8px">
<div style={{ cursor: 'pointer' }}>
<ArrowLeft onClick={handleBack} />
</div>
<TYPE.mediumHeader>Migrate V1 Liquidity</TYPE.mediumHeader>
<div>
<QuestionHelper text="Migrate your liquidity tokens from Uniswap V1 to Uniswap V2." />
</div>
</AutoRow>
{!account ? (
<TYPE.largeHeader>You must connect an account.</TYPE.largeHeader>
) : validatedAddress && token?.equals(WETH[chainId]) ? (
<>
<TYPE.body my={9} style={{ fontWeight: 400 }}>
Because Uniswap V2 uses WETH under the hood, your Uniswap V1 WETH/ETH liquidity cannot be migrated. You
may want to remove your liquidity instead.
</TYPE.body>
<ButtonConfirmed
onClick={() => {
history.push(`/remove/v1/${validatedAddress}`)
}}
>
Remove
</ButtonConfirmed>
</>
) : userLiquidityBalance && token ? (
<V1PairMigration liquidityTokenAmount={userLiquidityBalance} token={token} />
) : (
<EmptyState message="Loading..." />
)}
</AutoColumn>
</BodyWrapper>
)
}

View File

@@ -0,0 +1,189 @@
import { TransactionResponse } from '@ethersproject/abstract-provider'
import { JSBI, Token, TokenAmount, WETH, Fraction, Percent } from '@uniswap/sdk'
import React, { useCallback, useMemo, useState } from 'react'
import { ArrowLeft } from 'react-feather'
import ReactGA from 'react-ga'
import { Redirect, RouteComponentProps } from 'react-router'
import { ButtonConfirmed } from '../../components/Button'
import { LightCard } from '../../components/Card'
import { AutoColumn } from '../../components/Column'
import QuestionHelper from '../../components/QuestionHelper'
import { AutoRow } from '../../components/Row'
import { DEFAULT_DEADLINE_FROM_NOW } from '../../constants'
import { useActiveWeb3React } from '../../hooks'
import { useToken } from '../../hooks/Tokens'
import { useV1ExchangeContract } from '../../hooks/useContract'
import { NEVER_RELOAD, useSingleCallResult } from '../../state/multicall/hooks'
import { useIsTransactionPending, useTransactionAdder } from '../../state/transactions/hooks'
import { useTokenBalance, useETHBalances } from '../../state/wallet/hooks'
import { TYPE } from '../../theme'
import { isAddress } from '../../utils'
import { BodyWrapper } from '../AppBody'
import { EmptyState } from './EmptyState'
import { V1LiquidityInfo } from './MigrateV1Exchange'
import { AddressZero } from '@ethersproject/constants'
import { Dots } from '../../components/swap/styleds'
import { Contract } from '@ethersproject/contracts'
import { useTotalSupply } from '../../data/TotalSupply'
const WEI_DENOM = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(18))
const ZERO = JSBI.BigInt(0)
const ONE = JSBI.BigInt(1)
const ZERO_FRACTION = new Fraction(ZERO, ONE)
function V1PairRemoval({
exchangeContract,
liquidityTokenAmount,
token
}: {
exchangeContract: Contract
liquidityTokenAmount: TokenAmount
token: Token
}) {
const { chainId } = useActiveWeb3React()
const totalSupply = useTotalSupply(liquidityTokenAmount.token)
const exchangeETHBalance = useETHBalances([liquidityTokenAmount.token.address])?.[liquidityTokenAmount.token.address]
const exchangeTokenBalance = useTokenBalance(liquidityTokenAmount.token.address, token)
const [confirmingRemoval, setConfirmingRemoval] = useState<boolean>(false)
const [pendingRemovalHash, setPendingRemovalHash] = useState<string | null>(null)
const shareFraction: Fraction = totalSupply ? new Percent(liquidityTokenAmount.raw, totalSupply.raw) : ZERO_FRACTION
const ethWorth: Fraction = exchangeETHBalance
? new Fraction(shareFraction.multiply(exchangeETHBalance).quotient, WEI_DENOM)
: ZERO_FRACTION
const tokenWorth: TokenAmount = exchangeTokenBalance
? new TokenAmount(token, shareFraction.multiply(exchangeTokenBalance.raw).quotient)
: new TokenAmount(token, ZERO)
const addTransaction = useTransactionAdder()
const isRemovalPending = useIsTransactionPending(pendingRemovalHash)
const remove = useCallback(() => {
if (!liquidityTokenAmount) return
setConfirmingRemoval(true)
exchangeContract
.removeLiquidity(
liquidityTokenAmount.raw.toString(),
1, // min_eth, this is safe because we're removing liquidity
1, // min_tokens, this is safe because we're removing liquidity
Math.floor(new Date().getTime() / 1000) + DEFAULT_DEADLINE_FROM_NOW
)
.then((response: TransactionResponse) => {
ReactGA.event({
category: 'Remove',
action: 'V1',
label: token?.symbol
})
addTransaction(response, {
summary: `Remove ${token.equals(WETH[chainId]) ? 'WETH' : token.symbol}/ETH V1 liquidity`
})
setPendingRemovalHash(response.hash)
})
.catch(error => {
console.error(error)
setConfirmingRemoval(false)
})
}, [exchangeContract, liquidityTokenAmount, token, chainId, addTransaction])
const noLiquidityTokens = !!liquidityTokenAmount && liquidityTokenAmount.equalTo(ZERO)
const isSuccessfullyRemoved = !!pendingRemovalHash && !!noLiquidityTokens
return (
<AutoColumn gap="20px">
<TYPE.body my={9} style={{ fontWeight: 400 }}>
This tool will remove your V1 liquidity and send the underlying assets to your wallet.
</TYPE.body>
<LightCard>
<V1LiquidityInfo
token={token}
liquidityTokenAmount={liquidityTokenAmount}
tokenWorth={tokenWorth}
ethWorth={ethWorth}
/>
<div style={{ display: 'flex', marginTop: '1rem' }}>
<ButtonConfirmed
confirmed={isSuccessfullyRemoved}
disabled={isSuccessfullyRemoved || noLiquidityTokens || isRemovalPending || confirmingRemoval}
onClick={remove}
>
{isSuccessfullyRemoved ? 'Success' : isRemovalPending ? <Dots>Removing</Dots> : 'Remove'}
</ButtonConfirmed>
</div>
</LightCard>
<TYPE.darkGray style={{ textAlign: 'center' }}>
{`Your Uniswap V1 ${
token.equals(WETH[chainId]) ? 'WETH' : token.symbol
}/ETH liquidity will be redeemed for underlying assets.`}
</TYPE.darkGray>
</AutoColumn>
)
}
export default function RemoveV1Exchange({
history,
match: {
params: { address }
}
}: RouteComponentProps<{ address: string }>) {
const validatedAddress = isAddress(address)
const { chainId, account } = useActiveWeb3React()
const exchangeContract = useV1ExchangeContract(validatedAddress ? validatedAddress : undefined, true)
const tokenAddress = useSingleCallResult(exchangeContract, 'tokenAddress', undefined, NEVER_RELOAD)?.result?.[0]
const token = useToken(tokenAddress)
const liquidityToken: Token | undefined = useMemo(
() =>
validatedAddress && token
? new Token(chainId, validatedAddress, 18, `UNI-V1-${token.symbol}`, 'Uniswap V1')
: undefined,
[chainId, validatedAddress, token]
)
const userLiquidityBalance = useTokenBalance(account, liquidityToken)
const handleBack = useCallback(() => {
history.push('/migrate/v1')
}, [history])
// redirect for invalid url params
if (!validatedAddress || tokenAddress === AddressZero) {
console.error('Invalid address in path', address)
return <Redirect to="/migrate/v1" />
}
return (
<BodyWrapper style={{ padding: 24 }}>
<AutoColumn gap="16px">
<AutoRow style={{ alignItems: 'center', justifyContent: 'space-between' }} gap="8px">
<div style={{ cursor: 'pointer' }}>
<ArrowLeft onClick={handleBack} />
</div>
<TYPE.mediumHeader>Remove V1 Liquidity</TYPE.mediumHeader>
<div>
<QuestionHelper text="Remove your Uniswap V1 liquidity tokens." />
</div>
</AutoRow>
{!account ? (
<TYPE.largeHeader>You must connect an account.</TYPE.largeHeader>
) : userLiquidityBalance && token ? (
<V1PairRemoval
exchangeContract={exchangeContract}
liquidityTokenAmount={userLiquidityBalance}
token={token}
/>
) : (
<EmptyState message="Loading..." />
)}
</AutoColumn>
</BodyWrapper>
)
}

View File

@@ -1,40 +1,124 @@
import { JSBI, Token } from '@uniswap/sdk'
import React, { useMemo } from 'react'
import React, { useCallback, useContext, useMemo, useState, useEffect } from 'react'
import { ArrowLeft } from 'react-feather'
import { RouteComponentProps } from 'react-router'
import { useAllV1ExchangeAddresses } from '../../data/V1'
import { ThemeContext } from 'styled-components'
import { AutoColumn } from '../../components/Column'
import { AutoRow } from '../../components/Row'
import { SearchInput } from '../../components/SearchModal/styleds'
import { useAllTokenV1Exchanges } from '../../data/V1'
import { useActiveWeb3React } from '../../hooks'
import { useTokenBalances } from '../../state/wallet/hooks'
import { useToken, useAllTokens } from '../../hooks/Tokens'
import { useTokenBalancesWithLoadingIndicator } from '../../state/wallet/hooks'
import { TYPE } from '../../theme'
import { LightCard } from '../../components/Card'
import { BodyWrapper } from '../AppBody'
import { EmptyState } from './EmptyState'
import V1PositionCard from '../../components/PositionCard/V1'
import QuestionHelper from '../../components/QuestionHelper'
import { Dots } from '../../components/swap/styleds'
import { useAddUserToken } from '../../state/user/hooks'
import { isDefaultToken, isCustomAddedToken } from '../../utils'
const PLACEHOLDER_ACCOUNT = (
<div>
<h1>You must connect a wallet to use this tool.</h1>
</div>
)
/**
* Page component for migrating liquidity from V1
*/
export default function MigrateV1({}: RouteComponentProps) {
export default function MigrateV1({ history }: RouteComponentProps) {
const theme = useContext(ThemeContext)
const { account, chainId } = useActiveWeb3React()
const v1ExchangeAddresses = useAllV1ExchangeAddresses()
const v1ExchangeTokens: Token[] = useMemo(() => {
return v1ExchangeAddresses.map(exchangeAddress => new Token(chainId, exchangeAddress, 18))
}, [chainId, v1ExchangeAddresses])
const [tokenSearch, setTokenSearch] = useState<string>('')
const handleTokenSearchChange = useCallback(e => setTokenSearch(e.target.value), [setTokenSearch])
const tokenBalances = useTokenBalances(account, v1ExchangeTokens)
// automatically add the search token
const token = useToken(tokenSearch)
const isDefault = isDefaultToken(token)
const allTokens = useAllTokens()
const isCustomAdded = isCustomAddedToken(allTokens, token)
const addToken = useAddUserToken()
useEffect(() => {
if (token && !isDefault && !isCustomAdded) {
addToken(token)
}
}, [token, isDefault, isCustomAdded, addToken])
const unmigratedExchangeAddresses = useMemo(
() =>
Object.keys(tokenBalances).filter(tokenAddress =>
tokenBalances[tokenAddress] ? JSBI.greaterThan(tokenBalances[tokenAddress]?.raw, JSBI.BigInt(0)) : false
),
[tokenBalances]
// get V1 LP balances
const V1Exchanges = useAllTokenV1Exchanges()
const V1LiquidityTokens: Token[] = useMemo(() => {
return Object.keys(V1Exchanges).map(
exchangeAddress => new Token(chainId, exchangeAddress, 18, 'UNI-V1', 'Uniswap V1')
)
}, [chainId, V1Exchanges])
const [V1LiquidityBalances, V1LiquidityBalancesLoading] = useTokenBalancesWithLoadingIndicator(
account,
V1LiquidityTokens
)
const allV1PairsWithLiquidity = V1LiquidityTokens.filter(V1LiquidityToken => {
return (
V1LiquidityBalances?.[V1LiquidityToken.address] &&
JSBI.greaterThan(V1LiquidityBalances[V1LiquidityToken.address].raw, JSBI.BigInt(0))
)
}).map(V1LiquidityToken => {
return (
<V1PositionCard
key={V1LiquidityToken.address}
token={V1Exchanges[V1LiquidityToken.address]}
V1LiquidityBalance={V1LiquidityBalances[V1LiquidityToken.address]}
/>
)
})
if (!account) {
return PLACEHOLDER_ACCOUNT
}
// should never always be false, because a V1 exhchange exists for WETH on all testnets
const isLoading = Object.keys(V1Exchanges)?.length === 0 || V1LiquidityBalancesLoading
return <div>{unmigratedExchangeAddresses?.join('\n')}</div>
const handleBackClick = useCallback(() => {
history.push('/pool')
}, [history])
return (
<BodyWrapper style={{ padding: 24 }}>
<AutoColumn gap="16px">
<AutoRow style={{ alignItems: 'center', justifyContent: 'space-between' }} gap="8px">
<div style={{ cursor: 'pointer' }}>
<ArrowLeft onClick={handleBackClick} />
</div>
<TYPE.mediumHeader>Migrate V1 Liquidity</TYPE.mediumHeader>
<div>
<QuestionHelper text="Migrate your liquidity tokens from Uniswap V1 to Uniswap V2." />
</div>
</AutoRow>
<TYPE.body style={{ marginBottom: 8, fontWeight: 400 }}>
For each pool shown below, click migrate to remove your liquidity from Uniswap V1 and deposit it into Uniswap
V2.
</TYPE.body>
{!account ? (
<LightCard padding="40px">
<TYPE.body color={theme.text3} textAlign="center">
Connect to a wallet to view your V1 liquidity.
</TYPE.body>
</LightCard>
) : isLoading ? (
<LightCard padding="40px">
<TYPE.body color={theme.text3} textAlign="center">
<Dots>Loading</Dots>
</TYPE.body>
</LightCard>
) : (
<>
<AutoRow>
<SearchInput
value={tokenSearch}
onChange={handleTokenSearchChange}
placeholder="Enter a token address to find liquidity"
/>
</AutoRow>
{allV1PairsWithLiquidity?.length > 0 ? (
<>{allV1PairsWithLiquidity}</>
) : (
<EmptyState message="No V1 Liquidity found." />
)}
</>
)}
</AutoColumn>
</BodyWrapper>
)
}

View File

@@ -1,14 +1,14 @@
import React, { useState, useContext } from 'react'
import React, { useState, useContext, useCallback } from 'react'
import styled, { ThemeContext } from 'styled-components'
import { JSBI, Pair } from '@uniswap/sdk'
import { JSBI } from '@uniswap/sdk'
import { RouteComponentProps } from 'react-router-dom'
import Question from '../../components/QuestionHelper'
import SearchModal from '../../components/SearchModal'
import PairSearchModal from '../../components/SearchModal/PairSearchModal'
import PositionCard from '../../components/PositionCard'
import { useUserProbablyHasV1Liquidity } from '../../data/V1'
import { useTokenBalances } from '../../state/wallet/hooks'
import { LinkStyledButton, StyledInternalLink, TYPE } from '../../theme'
import { useUserHasLiquidityInAllTokens } from '../../data/V1'
import { useTokenBalancesWithLoadingIndicator } from '../../state/wallet/hooks'
import { StyledInternalLink, TYPE } from '../../theme'
import { Text } from 'rebass'
import { LightCard } from '../../components/Card'
import { RowBetween } from '../../components/Row'
@@ -16,9 +16,10 @@ import { ButtonPrimary, ButtonSecondary } from '../../components/Button'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import { useActiveWeb3React } from '../../hooks'
import { usePair } from '../../data/Reserves'
import { usePairs } from '../../data/Reserves'
import { useAllDummyPairs } from '../../state/user/hooks'
import AppBody from '../AppBody'
import { Dots } from '../../components/swap/styleds'
const Positions = styled.div`
position: relative;
@@ -31,35 +32,41 @@ const FixedBottom = styled.div`
width: 100%;
`
function PositionCardWrapper({ dummyPair }: { dummyPair: Pair }) {
const pair = usePair(dummyPair.token0, dummyPair.token1)
return <PositionCard pair={pair} />
}
export default function Pool({ history }: RouteComponentProps) {
const theme = useContext(ThemeContext)
const { account } = useActiveWeb3React()
const [showPoolSearch, setShowPoolSearch] = useState(false)
// initiate listener for LP balances
const pairs = useAllDummyPairs()
const pairBalances = useTokenBalances(
// fetch the user's balances of all tracked V2 LP tokens
const V2DummyPairs = useAllDummyPairs()
const [V2PairsBalances, fetchingV2PairBalances] = useTokenBalancesWithLoadingIndicator(
account,
pairs?.map(p => p.liquidityToken)
V2DummyPairs?.map(p => p.liquidityToken)
)
// fetch the reserves for all V2 pools in which the user has a balance
const V2DummyPairsWithABalance = V2DummyPairs.filter(
V2DummyPair =>
V2PairsBalances[V2DummyPair.liquidityToken.address] &&
JSBI.greaterThan(V2PairsBalances[V2DummyPair.liquidityToken.address].raw, JSBI.BigInt(0))
)
const V2Pairs = usePairs(
V2DummyPairsWithABalance.map(V2DummyPairWithABalance => [
V2DummyPairWithABalance.token0,
V2DummyPairWithABalance.token1
])
)
const V2IsLoading =
fetchingV2PairBalances || V2Pairs?.length < V2DummyPairsWithABalance.length || V2Pairs?.some(V2Pair => !!!V2Pair)
const filteredExchangeList = pairs
.filter(pair => {
return (
pairBalances?.[pair.liquidityToken.address] &&
JSBI.greaterThan(pairBalances[pair.liquidityToken.address].raw, JSBI.BigInt(0))
)
})
.map((pair, i) => {
return <PositionCardWrapper key={i} dummyPair={pair} />
})
const allV2PairsWithLiquidity = V2Pairs.filter(V2Pair => !!V2Pair).map(V2Pair => (
<PositionCard key={V2Pair.liquidityToken.address} pair={V2Pair} />
))
const hasV1Liquidity = useUserProbablyHasV1Liquidity()
const hasV1Liquidity = useUserHasLiquidityInAllTokens()
const handleSearchDismiss = useCallback(() => {
setShowPoolSearch(false)
}, [setShowPoolSearch])
return (
<AppBody>
@@ -72,40 +79,49 @@ export default function Pool({ history }: RouteComponentProps) {
}}
>
<Text fontWeight={500} fontSize={20}>
Join {filteredExchangeList?.length > 0 ? 'another' : 'a'} pool
Join {allV2PairsWithLiquidity?.length > 0 ? 'another' : 'a'} pool
</Text>
</ButtonPrimary>
<Positions>
<AutoColumn gap="12px">
<RowBetween padding={'0 8px'}>
<Text color={theme.text1} fontWeight={500}>
Your Pooled Liquidity
Your Liquidity
</Text>
<Question text="When you add liquidity, you are given pool tokens that represent your share. If you dont see a pool you joined in this list, try importing a pool below." />
</RowBetween>
{filteredExchangeList?.length === 0 && (
<LightCard
padding="40px
"
>
{!account ? (
<LightCard padding="40px">
<TYPE.body color={theme.text3} textAlign="center">
Connect to a wallet to view your liquidity.
</TYPE.body>
</LightCard>
) : V2IsLoading ? (
<LightCard padding="40px">
<TYPE.body color={theme.text3} textAlign="center">
<Dots>Loading</Dots>
</TYPE.body>
</LightCard>
) : allV2PairsWithLiquidity?.length > 0 ? (
<>{allV2PairsWithLiquidity}</>
) : (
<LightCard padding="40px">
<TYPE.body color={theme.text3} textAlign="center">
No liquidity found.
</TYPE.body>
</LightCard>
)}
{filteredExchangeList}
<Text textAlign="center" fontSize={14} style={{ padding: '.5rem 0 .5rem 0' }}>
{!hasV1Liquidity ? (
<>
{filteredExchangeList?.length !== 0 ? `Don't see a pool you joined? ` : 'Already joined a pool? '}{' '}
<StyledInternalLink id="import-pool-link" to="/find">
Import it.
</StyledInternalLink>
</>
) : (
<LinkStyledButton id="migrate-v1-liquidity-link">Migrate your V1 liquidity.</LinkStyledButton>
)}
</Text>
<div>
<Text textAlign="center" fontSize={14} style={{ padding: '.5rem 0 .5rem 0' }}>
{hasV1Liquidity ? 'Uniswap V1 liquidity found!' : "Don't see a pool you joined?"}{' '}
<StyledInternalLink id="import-pool-link" to={hasV1Liquidity ? '/migrate/v1' : '/find'}>
{hasV1Liquidity ? 'Migrate now.' : 'Import it.'}
</StyledInternalLink>
</Text>
</div>
</AutoColumn>
<FixedBottom>
<ColumnCenter>
@@ -115,7 +131,7 @@ export default function Pool({ history }: RouteComponentProps) {
</ColumnCenter>
</FixedBottom>
</Positions>
<SearchModal isOpen={showPoolSearch} onDismiss={() => setShowPoolSearch(false)} />
<PairSearchModal isOpen={showPoolSearch} onDismiss={handleSearchDismiss} />
</AutoColumn>
</AppBody>
)

View File

@@ -17,9 +17,13 @@ export const MaxButton = styled.button<{ width: string }>`
border: 1px solid ${({ theme }) => theme.primary5};
border-radius: 0.5rem;
font-size: 1rem;
${({ theme }) => theme.mediaWidth.upToSmall`
padding: 0.25rem 0.5rem;
`};
font-weight: 500;
cursor: pointer;
margin-right: 0.5rem;
margin: 0.25rem;
overflow: hidden;
color: ${({ theme }) => theme.primary1};
:hover {
border: 1px solid ${({ theme }) => theme.primary1};

View File

@@ -1,14 +1,13 @@
import { JSBI, Pair, Token, TokenAmount } from '@uniswap/sdk'
import React, { useEffect, useState } from 'react'
import { JSBI, Pair, Token, TokenAmount, WETH } from '@uniswap/sdk'
import React, { useCallback, useEffect, useState } from 'react'
import { Plus } from 'react-feather'
import { RouteComponentProps } from 'react-router-dom'
import { Text } from 'rebass'
import { ButtonDropwdown, ButtonDropwdownLight, ButtonPrimary } from '../../components/Button'
import { ButtonDropdownLight } from '../../components/Button'
import { LightCard } from '../../components/Card'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import PositionCard from '../../components/PositionCard'
import Row from '../../components/Row'
import SearchModal from '../../components/SearchModal'
import TokenSearchModal from '../../components/SearchModal/TokenSearchModal'
import TokenLogo from '../../components/TokenLogo'
import { usePair } from '../../data/Reserves'
import { useActiveWeb3React } from '../../hooks'
@@ -23,138 +22,133 @@ enum Fields {
TOKEN1 = 1
}
export default function PoolFinder({ history }: RouteComponentProps) {
const { account } = useActiveWeb3React()
const [showSearch, setShowSearch] = useState<boolean>(false)
const [activeField, setActiveField] = useState<number>(Fields.TOKEN0)
export default function PoolFinder() {
const { account, chainId } = useActiveWeb3React()
const [token0Address, setToken0Address] = useState<string>()
const [showSearch, setShowSearch] = useState<boolean>(false)
const [activeField, setActiveField] = useState<number>(Fields.TOKEN1)
const [token0Address, setToken0Address] = useState<string>(WETH[chainId].address)
const [token1Address, setToken1Address] = useState<string>()
const token0: Token = useToken(token0Address)
const token1: Token = useToken(token1Address)
const pair: Pair = usePair(token0, token1)
const addPair = usePairAdder()
useEffect(() => {
if (pair) {
addPair(pair)
}
}, [pair, addPair])
const position: TokenAmount = useTokenBalanceTreatingWETHasETH(account, pair?.liquidityToken)
const newPair: boolean =
pair === null ||
(!!pair && JSBI.equal(pair.reserve0.raw, JSBI.BigInt(0)) && JSBI.equal(pair.reserve1.raw, JSBI.BigInt(0)))
const allowImport: boolean = position && JSBI.greaterThan(position.raw, JSBI.BigInt(0))
const position: TokenAmount = useTokenBalanceTreatingWETHasETH(account, pair?.liquidityToken)
const poolImported: boolean = !!position && JSBI.greaterThan(position.raw, JSBI.BigInt(0))
const handleTokenSelect = useCallback(
(address: string) => {
activeField === Fields.TOKEN0 ? setToken0Address(address) : setToken1Address(address)
},
[activeField]
)
const handleSearchDismiss = useCallback(() => {
setShowSearch(false)
}, [setShowSearch])
return (
<AppBody>
<AutoColumn gap="md">
{!token0Address ? (
<ButtonDropwdown
onClick={() => {
setShowSearch(true)
setActiveField(Fields.TOKEN0)
}}
>
<Text fontSize={20}>Select first token</Text>
</ButtonDropwdown>
) : (
<ButtonDropwdownLight
onClick={() => {
setShowSearch(true)
setActiveField(Fields.TOKEN0)
}}
>
<ButtonDropdownLight
onClick={() => {
setShowSearch(true)
setActiveField(Fields.TOKEN0)
}}
>
{token0 ? (
<Row>
<TokenLogo address={token0Address} />
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
{token0?.symbol}
{token0.symbol}
</Text>
</Row>
</ButtonDropwdownLight>
)}
) : (
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
Select a Token
</Text>
)}
</ButtonDropdownLight>
<ColumnCenter>
<Plus size="16" color="#888D9B" />
</ColumnCenter>
{!token1Address ? (
<ButtonDropwdown
onClick={() => {
setShowSearch(true)
setActiveField(Fields.TOKEN1)
}}
>
<Text fontSize={20}>Select second token</Text>
</ButtonDropwdown>
) : (
<ButtonDropwdownLight
onClick={() => {
setShowSearch(true)
setActiveField(Fields.TOKEN1)
}}
>
<ButtonDropdownLight
onClick={() => {
setShowSearch(true)
setActiveField(Fields.TOKEN1)
}}
>
{token1 ? (
<Row>
<TokenLogo address={token1Address} />
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
{token1?.symbol}
{token1.symbol}
</Text>
</Row>
</ButtonDropwdownLight>
)}
{allowImport && (
) : (
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
Select a Token
</Text>
)}
</ButtonDropdownLight>
{poolImported && (
<ColumnCenter
style={{ justifyItems: 'center', backgroundColor: '', padding: '12px 0px', borderRadius: '12px' }}
>
<Text textAlign="center" fontWeight={500} color="">
Pool Imported!
Pool Found!
</Text>
</ColumnCenter>
)}
{position ? (
!JSBI.equal(position.raw, JSBI.BigInt(0)) ? (
poolImported ? (
<PositionCard pair={pair} minimal={true} border="1px solid #CED0D9" />
) : (
<LightCard padding="45px 10px">
<AutoColumn gap="sm" justify="center">
<Text textAlign="center">Pool found, you dont have liquidity on this pair yet.</Text>
<StyledInternalLink to={`/add/${token0Address}-${token1Address}`}>
<Text textAlign="center">Add liquidity to this pair instead.</Text>
<Text textAlign="center">You dont have liquidity in this pool yet.</Text>
<StyledInternalLink to={`/add/${token0.address}-${token1.address}`}>
<Text textAlign="center">Add liquidity?</Text>
</StyledInternalLink>
</AutoColumn>
</LightCard>
)
) : newPair ? (
<LightCard padding="45px">
<LightCard padding="45px 10px">
<AutoColumn gap="sm" justify="center">
<Text color="">No pool found.</Text>
<Text textAlign="center">No pool found.</Text>
<StyledInternalLink to={`/add/${token0Address}-${token1Address}`}>Create pool?</StyledInternalLink>
</AutoColumn>
</LightCard>
) : (
<LightCard padding={'45px'}>
<Text color="#C3C5CB" textAlign="center">
Select a token pair to find your liquidity.
<LightCard padding="45px 10px">
<Text textAlign="center">
{!account ? 'Connect to a wallet to find pools' : 'Select a token to find your liquidity.'}
</Text>
</LightCard>
)}
<ButtonPrimary disabled={!allowImport} onClick={() => history.goBack()}>
<Text fontWeight={500} fontSize={20}>
Close
</Text>
</ButtonPrimary>
</AutoColumn>
<SearchModal
<TokenSearchModal
isOpen={showSearch}
filterType="tokens"
onTokenSelect={address => {
activeField === Fields.TOKEN0 ? setToken0Address(address) : setToken1Address(address)
}}
onDismiss={() => {
setShowSearch(false)
}}
onTokenSelect={handleTokenSelect}
onDismiss={handleSearchDismiss}
hiddenToken={activeField === Fields.TOKEN0 ? token1Address : token0Address}
/>
</AppBody>

View File

@@ -7,7 +7,7 @@ import ReactGA from 'react-ga'
import { RouteComponentProps } from 'react-router'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import { ButtonConfirmed, ButtonPrimary, ButtonLight, ButtonError } from '../../components/Button'
import { ButtonPrimary, ButtonLight, ButtonError, ButtonConfirmed } from '../../components/Button'
import { LightCard } from '../../components/Card'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import ConfirmationModal from '../../components/ConfirmationModal'
@@ -18,7 +18,7 @@ import Row, { RowBetween, RowFixed } from '../../components/Row'
import Slider from '../../components/Slider'
import TokenLogo from '../../components/TokenLogo'
import { ROUTER_ADDRESS, DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
import { ROUTER_ADDRESS } from '../../constants'
import { useActiveWeb3React } from '../../hooks'
import { usePairContract } from '../../hooks/useContract'
@@ -31,11 +31,12 @@ import { useApproveCallback, ApprovalState } from '../../hooks/useApproveCallbac
import { Dots } from '../../components/swap/styleds'
import { useDefaultsFromURLMatchParams, useBurnActionHandlers } from '../../state/burn/hooks'
import { useDerivedBurnInfo, useBurnState } from '../../state/burn/hooks'
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
import { Field } from '../../state/burn/actions'
import { useWalletModalToggle } from '../../state/application/hooks'
import { useUserDeadline, useUserSlippageTolerance } from '../../state/user/hooks'
import { BigNumber } from '@ethersproject/bignumber'
export default function RemoveLiquidity({ match: { params }, history }: RouteComponentProps<{ tokens: string }>) {
export default function RemoveLiquidity({ match: { params } }: RouteComponentProps<{ tokens: string }>) {
useDefaultsFromURLMatchParams(params)
const { account, chainId, library } = useActiveWeb3React()
@@ -47,20 +48,18 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
// burn state
const { independentField, typedValue } = useBurnState()
const { tokens, pair, route, parsedAmounts, error } = useDerivedBurnInfo()
const { onUserInput } = useBurnActionHandlers()
const { onUserInput: _onUserInput } = useBurnActionHandlers()
const isValid = !error
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showAdvanced, setShowAdvanced] = useState<boolean>(false)
const [showDetailed, setShowDetailed] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirm
const [pendingConfirmation, setPendingConfirmation] = useState(true) // waiting for
// txn values
const [txHash, setTxHash] = useState<string>('')
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
const [deadline] = useUserDeadline()
const [allowedSlippage] = useUserSlippageTolerance()
const formattedAmounts = {
[Field.LIQUIDITY_PERCENT]: parsedAmounts[Field.LIQUIDITY_PERCENT].equalTo('0')
@@ -144,17 +143,18 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
})
}
function resetModalState() {
setSignatureData(null)
setAttemptingTxn(false)
setPendingConfirmation(true)
}
// wrapped onUserInput to clear signatures
const onUserInput = useCallback(
(field: Field, typedValue: string) => {
setSignatureData(null)
return _onUserInput(field, typedValue)
},
[_onUserInput]
)
// tx sending
const addTransaction = useTransactionAdder()
async function onRemove() {
setAttemptingTxn(true)
const router = getRouterContract(chainId, library, account)
const amountsMin = {
@@ -167,13 +167,12 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline
let estimate, method: Function, args: Array<string | string[] | number | boolean>
let methodNames: string[], args: Array<string | string[] | number | boolean>
// we have approval, use normal remove liquidity
if (approval === ApprovalState.APPROVED) {
// removeLiquidityETH
if (oneTokenIsETH) {
estimate = router.estimateGas.removeLiquidityETH
method = router.removeLiquidityETH
methodNames = ['removeLiquidityETH', 'removeLiquidityETHSupportingFeeOnTransferTokens']
args = [
tokens[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].address,
parsedAmounts[Field.LIQUIDITY].raw.toString(),
@@ -185,8 +184,7 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
}
// removeLiquidity
else {
estimate = router.estimateGas.removeLiquidity
method = router.removeLiquidity
methodNames = ['removeLiquidity']
args = [
tokens[Field.TOKEN_A].address,
tokens[Field.TOKEN_B].address,
@@ -202,8 +200,7 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
else if (signatureData !== null) {
// removeLiquidityETHWithPermit
if (oneTokenIsETH) {
estimate = router.estimateGas.removeLiquidityETHWithPermit
method = router.removeLiquidityETHWithPermit
methodNames = ['removeLiquidityETHWithPermit', 'removeLiquidityETHWithPermitSupportingFeeOnTransferTokens']
args = [
tokens[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].address,
parsedAmounts[Field.LIQUIDITY].raw.toString(),
@@ -219,8 +216,7 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
}
// removeLiquidityETHWithPermit
else {
estimate = router.estimateGas.removeLiquidityWithPermit
method = router.removeLiquidityWithPermit
methodNames = ['removeLiquidityWithPermit']
args = [
tokens[Field.TOKEN_A].address,
tokens[Field.TOKEN_B].address,
@@ -236,14 +232,37 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
]
}
} else {
console.error('Attempting to confirm without approval or a signature.')
console.error('Attempting to confirm without approval or a signature. Please contact support.')
}
await estimate(...args)
.then(estimatedGasLimit =>
method(...args, {
gasLimit: calculateGasMargin(estimatedGasLimit)
}).then(response => {
const safeGasEstimates = await Promise.all(
methodNames.map(methodName =>
router.estimateGas[methodName](...args)
.then(calculateGasMargin)
.catch(error => {
console.error(`estimateGas failed for ${methodName}`, error)
})
)
)
const indexOfSuccessfulEstimation = safeGasEstimates.findIndex(safeGasEstimate =>
BigNumber.isBigNumber(safeGasEstimate)
)
// all estimations failed...
if (indexOfSuccessfulEstimation === -1) {
console.error('This transaction would fail. Please contact support.')
} else {
const methodName = methodNames[indexOfSuccessfulEstimation]
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
setAttemptingTxn(true)
await router[methodName](...args, {
gasLimit: safeGasEstimate
})
.then(response => {
setAttemptingTxn(false)
addTransaction(response, {
summary:
'Remove ' +
@@ -257,7 +276,6 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
})
setTxHash(response.hash)
setPendingConfirmation(false)
ReactGA.event({
category: 'Liquidity',
@@ -265,13 +283,14 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
label: [tokens[Field.TOKEN_A]?.symbol, tokens[Field.TOKEN_B]?.symbol].join('/')
})
})
)
.catch(e => {
console.error(e)
resetModalState()
setShowConfirm(false)
setShowAdvanced(false)
})
.catch(error => {
setAttemptingTxn(false)
// we only care if the error is something _other_ than the user rejected the tx
if (error?.code !== 4001) {
console.error(error)
}
})
}
}
function modalHeader() {
@@ -348,34 +367,11 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
</RowBetween>
</>
)}
<RowBetween mt="1rem">
<ButtonConfirmed
onClick={onAttemptToApprove}
confirmed={approval === ApprovalState.APPROVED || signatureData !== null}
disabled={approval !== ApprovalState.NOT_APPROVED || signatureData !== null}
mr="0.5rem"
fontWeight={500}
fontSize={20}
>
{approval === ApprovalState.PENDING ? (
<Dots>Approving</Dots>
) : approval === ApprovalState.APPROVED || signatureData !== null ? (
'Approved'
) : (
'Approve'
)}
</ButtonConfirmed>
<ButtonPrimary
disabled={!(approval === ApprovalState.APPROVED || signatureData !== null)}
onClick={onRemove}
ml="0.5rem"
>
<Text fontWeight={500} fontSize={20}>
Confirm
</Text>
</ButtonPrimary>
</RowBetween>
<ButtonPrimary disabled={!(approval === ApprovalState.APPROVED || signatureData !== null)} onClick={onRemove}>
<Text fontWeight={500} fontSize={20}>
Confirm
</Text>
</ButtonPrimary>
</>
)
}
@@ -398,16 +394,15 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
<ConfirmationModal
isOpen={showConfirm}
onDismiss={() => {
if (attemptingTxn) {
history.push('/pool')
return
}
resetModalState()
setShowConfirm(false)
setShowAdvanced(false)
setSignatureData(null) // important that we clear signature data to avoid bad sigs
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.LIQUIDITY_PERCENT, '0')
}
setTxHash('')
}}
attemptingTxn={attemptingTxn}
pendingConfirmation={pendingConfirmation}
hash={txHash ? txHash : ''}
topContent={modalHeader}
bottomContent={modalBottom}
@@ -561,34 +556,41 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
{!account ? (
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
) : (
<ButtonError
onClick={() => {
setShowConfirm(true)
}}
disabled={!isValid}
error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]}
>
<Text fontSize={20} fontWeight={500}>
{error || 'Remove'}
</Text>
</ButtonError>
<RowBetween>
<ButtonConfirmed
onClick={onAttemptToApprove}
confirmed={approval === ApprovalState.APPROVED || signatureData !== null}
disabled={approval !== ApprovalState.NOT_APPROVED || signatureData !== null}
mr="0.5rem"
fontWeight={500}
fontSize={16}
>
{approval === ApprovalState.PENDING ? (
<Dots>Approving</Dots>
) : approval === ApprovalState.APPROVED || signatureData !== null ? (
'Approved'
) : (
'Approve'
)}
</ButtonConfirmed>
<ButtonError
onClick={() => {
setShowConfirm(true)
}}
disabled={!isValid || (signatureData === null && approval !== ApprovalState.APPROVED)}
error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]}
>
<Text fontSize={16} fontWeight={500}>
{error || 'Remove'}
</Text>
</ButtonError>
</RowBetween>
)}
</div>
</AutoColumn>
</Wrapper>
</AppBody>
{isValid ? (
<AdvancedSwapDetailsDropdown
rawSlippage={allowedSlippage}
deadline={deadline}
showAdvanced={showAdvanced}
setShowAdvanced={setShowAdvanced}
setDeadline={setDeadline}
setRawSlippage={setAllowedSlippage}
/>
) : null}
{pair ? (
<AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
<PositionCard pair={pair} minimal={true} />

View File

@@ -2,7 +2,6 @@ import { JSBI, TokenAmount, WETH } from '@uniswap/sdk'
import React, { useContext, useEffect, useState } from 'react'
import { ArrowDown } from 'react-feather'
import ReactGA from 'react-ga'
import { RouteComponentProps } from 'react-router-dom'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import AddressInputPanel from '../../components/AddressInputPanel'
@@ -11,24 +10,24 @@ import Card, { BlueCard, GreyCard } from '../../components/Card'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import ConfirmationModal from '../../components/ConfirmationModal'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import QuestionHelper from '../../components/QuestionHelper'
import { AutoRow, RowBetween, RowFixed } from '../../components/Row'
import { AutoRow, RowBetween } from '../../components/Row'
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
import FormattedPriceImpact from '../../components/swap/FormattedPriceImpact'
import SwapModalFooter from '../../components/swap/SwapModalFooter'
import { ArrowWrapper, BottomGrouping, Dots, InputGroup, StyledNumerical, Wrapper } from '../../components/swap/styleds'
import TradePrice from '../../components/swap/TradePrice'
import { TransferModalHeader } from '../../components/swap/TransferModalHeader'
import V1TradeLink from '../../components/swap/V1TradeLink'
import BetterTradeLink from '../../components/swap/BetterTradeLink'
import TokenLogo from '../../components/TokenLogo'
import { TokenWarningCards } from '../../components/TokenWarningCard'
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, MIN_ETH } from '../../constants'
import { INITIAL_ALLOWED_SLIPPAGE, MIN_ETH, BETTER_TRADE_LINK_THRESHOLD } from '../../constants'
import { getTradeVersion, isTradeBetter } from '../../data/V1'
import { useActiveWeb3React } from '../../hooks'
import { useApproveCallbackFromTrade, ApprovalState } from '../../hooks/useApproveCallback'
import { useSendCallback } from '../../hooks/useSendCallback'
import { useSwapCallback } from '../../hooks/useSwapCallback'
import { useWalletModalToggle } from '../../state/application/hooks'
import { useWalletModalToggle, useToggleSettingsMenu } from '../../state/application/hooks'
import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
import { Field } from '../../state/swap/actions'
import {
useDefaultsFromURLSearch,
@@ -40,10 +39,12 @@ import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
import { CursorPointer, TYPE } from '../../theme'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
import AppBody from '../AppBody'
import { PriceSlippageWarningCard } from '../../components/swap/PriceSlippageWarningCard'
import { useUserSlippageTolerance, useUserDeadline, useExpertModeManager } from '../../state/user/hooks'
import { ClickableText } from '../Pool/styleds'
export default function Send({ location: { search } }: RouteComponentProps) {
useDefaultsFromURLSearch(search)
export default function Send() {
// override auto ETH populate to allow for single inputs or swap and send
useDefaultsFromURLSearch(true)
// text translation
// const { t } = useTranslation()
@@ -53,6 +54,10 @@ export default function Send({ location: { search } }: RouteComponentProps) {
// toggle wallet when disconnected
const toggleWalletModal = useWalletModalToggle()
// for expert mode
const toggleSettings = useToggleSettingsMenu()
const [expertMode] = useExpertModeManager()
// sending state
const [sendingWithSwap, setSendingWithSwap] = useState<boolean>(false)
const [recipient, setRecipient] = useState<string>('')
@@ -60,29 +65,55 @@ export default function Send({ location: { search } }: RouteComponentProps) {
const [recipientError, setRecipientError] = useState<string | null>('Enter a Recipient')
// trade details, check query params for initial state
const { independentField, typedValue } = useSwapState()
const {
parsedAmounts,
bestTrade,
independentField,
typedValue,
[Field.OUTPUT]: { address: output }
} = useSwapState()
// if output is valid set to sending view (will reset to undefined on remove swap)
useEffect(() => {
if (output) {
setSendingWithSwap(true)
}
}, [output])
const {
parsedAmount,
bestTrade: bestTradeV2,
tokenBalances,
tokens,
error: swapError,
v1TradeLinkIfBetter
v1Trade
} = useDerivedSwapInfo()
const isSwapValid = !swapError && !recipientError && bestTrade
const toggledVersion = useToggledVersion()
const bestTrade = {
[Version.v1]: v1Trade,
[Version.v2]: bestTradeV2
}[toggledVersion]
const betterTradeLinkVersion: Version | undefined =
toggledVersion === Version.v2 && isTradeBetter(bestTradeV2, v1Trade, BETTER_TRADE_LINK_THRESHOLD)
? Version.v1
: toggledVersion === Version.v1 && isTradeBetter(v1Trade, bestTradeV2)
? Version.v2
: undefined
const parsedAmounts = {
[Field.INPUT]: independentField === Field.INPUT ? parsedAmount : bestTrade?.inputAmount,
[Field.OUTPUT]: independentField === Field.OUTPUT ? parsedAmount : bestTrade?.outputAmount
}
const isSwapValid = !swapError && !recipientError && bestTrade
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showAdvanced, setShowAdvanced] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirmed
const [pendingConfirmation, setPendingConfirmation] = useState<boolean>(true) // waiting for user confirmation
// txn values
const [showConfirm, setShowConfirm] = useState<boolean>(false) // show confirmation modal
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // waiting for user confirmaion/rejection
const [txHash, setTxHash] = useState<string>('')
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
const [deadline] = useUserDeadline() // custom from user settings
const [allowedSlippage] = useUserSlippageTolerance() // custom from user settings
const route = bestTrade?.route
const userHasSpecifiedInputOutput =
@@ -95,6 +126,16 @@ export default function Send({ location: { search } }: RouteComponentProps) {
// check whether the user has approved the router on the input token
const [approval, approveCallback] = useApproveCallbackFromTrade(bestTrade, allowedSlippage)
// check if user has gone through approval process, used to show two step buttons, reset on token change
const [approvalSubmitted, setApprovalSubmitted] = useState<boolean>(false)
// mark when a user has submitted an approval, reset onTokenSelection for input field
useEffect(() => {
if (approval === ApprovalState.PENDING) {
setApprovalSubmitted(true)
}
}, [approval, approvalSubmitted])
const formattedAmounts = {
[independentField]: typedValue,
[dependentField]: parsedAmounts[dependentField] ? parsedAmounts[dependentField].toSignificant(6) : ''
@@ -106,13 +147,6 @@ export default function Send({ location: { search } }: RouteComponentProps) {
const { onSwitchTokens, onTokenSelection, onUserInput } = useSwapActionHandlers()
// reset field if sending with with swap is cancled
useEffect(() => {
if (!sendingWithSwap) {
onTokenSelection(Field.OUTPUT, null)
}
}, [onTokenSelection, sendingWithSwap])
const maxAmountInput: TokenAmount =
!!tokenBalances[Field.INPUT] &&
!!tokens[Field.INPUT] &&
@@ -127,17 +161,6 @@ export default function Send({ location: { search } }: RouteComponentProps) {
const atMaxAmountInput: boolean =
!!maxAmountInput && !!parsedAmounts[Field.INPUT] ? maxAmountInput.equalTo(parsedAmounts[Field.INPUT]) : undefined
// reset modal state when closed
function resetModal() {
// clear input if txn submitted
if (!pendingConfirmation) {
onUserInput(Field.INPUT, '')
}
setPendingConfirmation(true)
setAttemptingTxn(false)
setShowAdvanced(false)
}
const swapCallback = useSwapCallback(bestTrade, allowedSlippage, deadline, recipient)
function onSwap() {
@@ -146,16 +169,28 @@ export default function Send({ location: { search } }: RouteComponentProps) {
}
setAttemptingTxn(true)
swapCallback().then(hash => {
setTxHash(hash)
setPendingConfirmation(false)
swapCallback()
.then(hash => {
setAttemptingTxn(false)
setTxHash(hash)
ReactGA.event({
category: 'Send',
action: recipient === account ? 'Swap w/o Send' : 'Swap w/ Send',
label: [bestTrade.inputAmount.token.symbol, bestTrade.outputAmount.token.symbol].join(';')
ReactGA.event({
category: 'Send',
action: recipient === account ? 'Swap w/o Send' : 'Swap w/ Send',
label: [
bestTrade.inputAmount.token.symbol,
bestTrade.outputAmount.token.symbol,
getTradeVersion(bestTrade)
].join('/')
})
})
.catch(error => {
setAttemptingTxn(false)
// we only care if the error is something _other_ than the user rejected the tx
if (error?.code !== 4001) {
console.error(error)
}
})
})
}
const sendCallback = useSendCallback(parsedAmounts?.[Field.INPUT], recipient)
@@ -163,16 +198,19 @@ export default function Send({ location: { search } }: RouteComponentProps) {
async function onSend() {
setAttemptingTxn(true)
sendCallback()
.then(hash => {
setAttemptingTxn(false)
setTxHash(hash)
ReactGA.event({ category: 'Send', action: 'Send', label: tokens[Field.INPUT]?.symbol })
setPendingConfirmation(false)
})
.catch(() => {
resetModal()
setShowConfirm(false)
.catch(error => {
setAttemptingTxn(false)
// we only care if the error is something _other_ than the user rejected the tx
if (error?.code !== 4001) {
console.error(error)
}
})
}
@@ -181,6 +219,14 @@ export default function Send({ location: { search } }: RouteComponentProps) {
// warnings on slippage
const severity = !sendingWithSwap ? 0 : warningSeverity(priceImpactWithoutFee)
// show approval buttons when: no errors on input, not approved or pending, or has been approved in this session
const showApproveFlow =
((sendingWithSwap && isSwapValid) || (!sendingWithSwap && isSendValid)) &&
(approval === ApprovalState.NOT_APPROVED ||
approval === ApprovalState.PENDING ||
(approvalSubmitted && approval === ApprovalState.APPROVED)) &&
!(severity > 3 && !expertMode)
function modalHeader() {
if (!sendingWithSwap) {
return <TransferModalHeader amount={parsedAmounts?.[Field.INPUT]} ENSName={ENS} recipient={recipient} />
@@ -289,11 +335,13 @@ export default function Send({ location: { search } }: RouteComponentProps) {
isOpen={showConfirm}
title={sendingWithSwap ? 'Confirm swap and send' : 'Confirm Send'}
onDismiss={() => {
resetModal()
setShowConfirm(false)
if (txHash) {
onUserInput(Field.INPUT, '')
}
setTxHash('')
}}
attemptingTxn={attemptingTxn}
pendingConfirmation={pendingConfirmation}
hash={txHash}
topContent={modalHeader}
bottomContent={modalBottom}
@@ -363,11 +411,13 @@ export default function Send({ location: { search } }: RouteComponentProps) {
onMax={() => {
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
}}
onTokenSelection={address => onTokenSelection(Field.INPUT, address)}
onTokenSelection={address => {
setApprovalSubmitted(false)
onTokenSelection(Field.INPUT, address)
}}
otherSelectedTokenAddress={tokens[Field.OUTPUT]?.address}
id="swap-currency-input"
/>
{sendingWithSwap ? (
<ColumnCenter>
<RowBetween padding="0 1rem 0 12px">
@@ -375,7 +425,10 @@ export default function Send({ location: { search } }: RouteComponentProps) {
<ArrowDown size="16" color={theme.text2} onClick={onSwitchTokens} />
</ArrowWrapper>
<ButtonSecondary
onClick={() => setSendingWithSwap(false)}
onClick={() => {
setSendingWithSwap(false)
onTokenSelection(Field.OUTPUT, null)
}}
style={{ marginRight: '0px', width: 'auto', fontSize: '14px' }}
padding={'4px 6px'}
>
@@ -400,7 +453,6 @@ export default function Send({ location: { search } }: RouteComponentProps) {
field={Field.OUTPUT}
value={formattedAmounts[Field.OUTPUT]}
onUserInput={onUserInput}
// eslint-disable-next-line @typescript-eslint/no-empty-function
label={independentField === Field.INPUT && parsedAmounts[Field.OUTPUT] ? 'To (estimated)' : 'To'}
showMaxButton={false}
token={tokens[Field.OUTPUT]}
@@ -430,28 +482,34 @@ export default function Send({ location: { search } }: RouteComponentProps) {
}}
/>
</AutoColumn>
{!noRoute && tokens[Field.OUTPUT] && tokens[Field.INPUT] && (
<Card padding={'.25rem 1.25rem 0 .75rem'} borderRadius={'20px'}>
{sendingWithSwap && (
<Card padding={'.25rem .75rem 0 .75rem'} borderRadius={'20px'}>
<AutoColumn gap="4px">
<RowBetween align="center">
<Text fontWeight={500} fontSize={14} color={theme.text2}>
Price
</Text>
<TradePrice showInverted={showInverted} setShowInverted={setShowInverted} trade={bestTrade} />
<TradePrice
inputToken={tokens[Field.INPUT]}
outputToken={tokens[Field.OUTPUT]}
price={bestTrade?.executionPrice}
showInverted={showInverted}
setShowInverted={setShowInverted}
/>
</RowBetween>
{bestTrade && severity > 1 && (
<RowBetween>
<TYPE.main
style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }}
fontSize={14}
>
Price Impact
</TYPE.main>
<RowFixed>
<FormattedPriceImpact priceImpact={priceImpactWithoutFee} />
<QuestionHelper text="The difference between the market price and estimated price due to trade size." />
</RowFixed>
{allowedSlippage !== INITIAL_ALLOWED_SLIPPAGE && (
<RowBetween align="center">
<ClickableText>
<Text fontWeight={500} fontSize={14} color={theme.text2} onClick={toggleSettings}>
Slippage Tolerance
</Text>
</ClickableText>
<ClickableText>
<Text fontWeight={500} fontSize={14} color={theme.text2} onClick={toggleSettings}>
{allowedSlippage ? allowedSlippage / 100 : '-'}%
</Text>
</ClickableText>
</RowBetween>
)}
</AutoColumn>
@@ -471,53 +529,64 @@ export default function Send({ location: { search } }: RouteComponentProps) {
<GreyCard style={{ textAlign: 'center' }}>
<TYPE.main mb="4px">Insufficient liquidity for this trade.</TYPE.main>
</GreyCard>
) : approval === ApprovalState.NOT_APPROVED || approval === ApprovalState.PENDING ? (
<ButtonLight onClick={approveCallback} disabled={approval === ApprovalState.PENDING}>
{approval === ApprovalState.PENDING ? (
<Dots>Approving {tokens[Field.INPUT]?.symbol}</Dots>
) : (
'Approve ' + tokens[Field.INPUT]?.symbol
)}
</ButtonLight>
) : showApproveFlow ? (
<RowBetween>
<ButtonPrimary
onClick={approveCallback}
disabled={approval !== ApprovalState.NOT_APPROVED || approvalSubmitted}
width="48%"
altDisbaledStyle={approval === ApprovalState.PENDING} // show solid button while waiting
>
{approval === ApprovalState.PENDING ? (
<Dots>Approving</Dots>
) : approvalSubmitted && approval === ApprovalState.APPROVED ? (
'Approved'
) : (
'Approve ' + tokens[Field.INPUT]?.symbol
)}
</ButtonPrimary>
<ButtonError
onClick={() => {
expertMode ? (sendingWithSwap ? onSwap() : onSend()) : setShowConfirm(true)
}}
width="48%"
id="send-button"
disabled={approval !== ApprovalState.APPROVED}
error={sendingWithSwap && isSwapValid && severity > 2}
>
<Text fontSize={16} fontWeight={500}>
{severity > 3 && !expertMode ? `Price Impact High` : `Send${severity > 2 ? ' Anyway' : ''}`}
</Text>
</ButtonError>
</RowBetween>
) : (
<ButtonError
onClick={() => {
setShowConfirm(true)
expertMode ? (sendingWithSwap ? onSwap() : onSend()) : setShowConfirm(true)
}}
id="send-button"
disabled={(sendingWithSwap && !isSwapValid) || (!sendingWithSwap && !isSendValid)}
disabled={
(sendingWithSwap && !isSwapValid) ||
(!sendingWithSwap && !isSendValid) ||
(severity > 3 && !expertMode && sendingWithSwap)
}
error={sendingWithSwap && isSwapValid && severity > 2}
>
<Text fontSize={20} fontWeight={500}>
{(sendingWithSwap ? swapError : null) ||
sendAmountError ||
recipientError ||
(severity > 3 && !expertMode && `Price Impact Too High`) ||
`Send${severity > 2 ? ' Anyway' : ''}`}
</Text>
</ButtonError>
)}
<V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} />
{betterTradeLinkVersion && <BetterTradeLink version={betterTradeLinkVersion} />}
</BottomGrouping>
</Wrapper>
</AppBody>
{bestTrade && (
<AdvancedSwapDetailsDropdown
trade={bestTrade}
rawSlippage={allowedSlippage}
deadline={deadline}
showAdvanced={showAdvanced}
setShowAdvanced={setShowAdvanced}
setDeadline={setDeadline}
setRawSlippage={setAllowedSlippage}
/>
)}
{priceImpactWithoutFee && severity > 2 && (
<AutoColumn gap="lg" style={{ marginTop: '1rem' }}>
<PriceSlippageWarningCard priceSlippage={priceImpactWithoutFee} />
</AutoColumn>
)}
<AdvancedSwapDetailsDropdown trade={bestTrade} />
</>
)
}

View File

@@ -1,31 +1,32 @@
import { JSBI, TokenAmount, WETH } from '@uniswap/sdk'
import React, { useContext, useState } from 'react'
import React, { useContext, useState, useEffect } from 'react'
import { ArrowDown } from 'react-feather'
import ReactGA from 'react-ga'
import { RouteComponentProps } from 'react-router-dom'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import { ButtonError, ButtonLight } from '../../components/Button'
import { ButtonError, ButtonLight, ButtonPrimary } from '../../components/Button'
import Card, { GreyCard } from '../../components/Card'
import { AutoColumn } from '../../components/Column'
import ConfirmationModal from '../../components/ConfirmationModal'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import QuestionHelper from '../../components/QuestionHelper'
import { RowBetween, RowFixed } from '../../components/Row'
import { RowBetween } from '../../components/Row'
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
import FormattedPriceImpact from '../../components/swap/FormattedPriceImpact'
import { ArrowWrapper, BottomGrouping, Dots, Wrapper } from '../../components/swap/styleds'
import SwapModalFooter from '../../components/swap/SwapModalFooter'
import SwapModalHeader from '../../components/swap/SwapModalHeader'
import TradePrice from '../../components/swap/TradePrice'
import V1TradeLink from '../../components/swap/V1TradeLink'
import BetterTradeLink from '../../components/swap/BetterTradeLink'
import { TokenWarningCards } from '../../components/TokenWarningCard'
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, MIN_ETH } from '../../constants'
import { useActiveWeb3React } from '../../hooks'
import { useApproveCallbackFromTrade, ApprovalState } from '../../hooks/useApproveCallback'
import { useSwapCallback } from '../../hooks/useSwapCallback'
import { useWalletModalToggle } from '../../state/application/hooks'
import { useWalletModalToggle, useToggleSettingsMenu } from '../../state/application/hooks'
import { useExpertModeManager, useUserSlippageTolerance, useUserDeadline } from '../../state/user/hooks'
import { INITIAL_ALLOWED_SLIPPAGE, MIN_ETH, BETTER_TRADE_LINK_THRESHOLD } from '../../constants'
import { getTradeVersion, isTradeBetter } from '../../data/V1'
import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
import { Field } from '../../state/swap/actions'
import {
useDefaultsFromURLSearch,
@@ -36,10 +37,10 @@ import {
import { CursorPointer, TYPE } from '../../theme'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
import AppBody from '../AppBody'
import { PriceSlippageWarningCard } from '../../components/swap/PriceSlippageWarningCard'
import { ClickableText } from '../Pool/styleds'
export default function Swap({ location: { search } }: RouteComponentProps) {
useDefaultsFromURLSearch(search)
export default function Swap() {
useDefaultsFromURLSearch()
const { chainId, account } = useActiveWeb3React()
const theme = useContext(ThemeContext)
@@ -47,23 +48,43 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
// toggle wallet when disconnected
const toggleWalletModal = useWalletModalToggle()
// for expert mode
const toggleSettings = useToggleSettingsMenu()
const [expertMode] = useExpertModeManager()
// get custom setting values for user
const [deadline] = useUserDeadline()
const [allowedSlippage] = useUserSlippageTolerance()
// swap state
const { independentField, typedValue } = useSwapState()
const { bestTrade, tokenBalances, parsedAmounts, tokens, error, v1TradeLinkIfBetter } = useDerivedSwapInfo()
const { bestTrade: bestTradeV2, tokenBalances, parsedAmount, tokens, error, v1Trade } = useDerivedSwapInfo()
const toggledVersion = useToggledVersion()
const bestTrade = {
[Version.v1]: v1Trade,
[Version.v2]: bestTradeV2
}[toggledVersion]
const betterTradeLinkVersion: Version | undefined =
toggledVersion === Version.v2 && isTradeBetter(bestTradeV2, v1Trade, BETTER_TRADE_LINK_THRESHOLD)
? Version.v1
: toggledVersion === Version.v1 && isTradeBetter(v1Trade, bestTradeV2)
? Version.v2
: undefined
const parsedAmounts = {
[Field.INPUT]: independentField === Field.INPUT ? parsedAmount : bestTrade?.inputAmount,
[Field.OUTPUT]: independentField === Field.OUTPUT ? parsedAmount : bestTrade?.outputAmount
}
const { onSwitchTokens, onTokenSelection, onUserInput } = useSwapActionHandlers()
const isValid = !error
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showAdvanced, setShowAdvanced] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirmed
const [pendingConfirmation, setPendingConfirmation] = useState<boolean>(true) // waiting for user confirmation
// txn values
const [showConfirm, setShowConfirm] = useState<boolean>(false) // show confirmation modal
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // waiting for user confirmaion/rejection
const [txHash, setTxHash] = useState<string>('')
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
const formattedAmounts = {
[independentField]: typedValue,
@@ -81,6 +102,16 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
// check whether the user has approved the router on the input token
const [approval, approveCallback] = useApproveCallbackFromTrade(bestTrade, allowedSlippage)
// check if user has gone through approval process, used to show two step buttons, reset on token change
const [approvalSubmitted, setApprovalSubmitted] = useState<boolean>(false)
// mark when a user has submitted an approval, reset onTokenSelection for input field
useEffect(() => {
if (approval === ApprovalState.PENDING) {
setApprovalSubmitted(true)
}
}, [approval, approvalSubmitted])
const maxAmountInput: TokenAmount =
!!tokenBalances[Field.INPUT] &&
!!tokens[Field.INPUT] &&
@@ -97,17 +128,6 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(bestTrade, allowedSlippage)
// reset modal state when closed
function resetModal() {
// clear input if txn submitted
if (!pendingConfirmation) {
onUserInput(Field.INPUT, '')
}
setPendingConfirmation(true)
setAttemptingTxn(false)
setShowAdvanced(false)
}
// the callback to execute the swap
const swapCallback = useSwapCallback(bestTrade, allowedSlippage, deadline)
@@ -117,18 +137,29 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
if (priceImpactWithoutFee && !confirmPriceImpactWithoutFee(priceImpactWithoutFee)) {
return
}
setAttemptingTxn(true)
swapCallback().then(hash => {
setTxHash(hash)
setPendingConfirmation(false)
swapCallback()
.then(hash => {
setAttemptingTxn(false)
setTxHash(hash)
ReactGA.event({
category: 'Swap',
action: 'Swap w/o Send',
label: [bestTrade.inputAmount.token.symbol, bestTrade.outputAmount.token.symbol].join('/')
ReactGA.event({
category: 'Swap',
action: 'Swap w/o Send',
label: [
bestTrade.inputAmount.token.symbol,
bestTrade.outputAmount.token.symbol,
getTradeVersion(bestTrade)
].join('/')
})
})
.catch(error => {
setAttemptingTxn(false)
// we only care if the error is something _other_ than the user rejected the tx
if (error?.code !== 4001) {
console.error(error)
}
})
})
}
// errors
@@ -137,14 +168,23 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
// warnings on slippage
const priceImpactSeverity = warningSeverity(priceImpactWithoutFee)
// show approve flow when: no error on inputs, not approved or pending, or approved in current session
// never show if price impact is above threshold in non expert mode
const showApproveFlow =
!error &&
(approval === ApprovalState.NOT_APPROVED ||
approval === ApprovalState.PENDING ||
(approvalSubmitted && approval === ApprovalState.APPROVED)) &&
!(priceImpactSeverity > 3 && !expertMode)
function modalHeader() {
return (
<SwapModalHeader
independentField={independentField}
priceImpactSeverity={priceImpactSeverity}
tokens={tokens}
formattedAmounts={formattedAmounts}
slippageAdjustedAmounts={slippageAdjustedAmounts}
priceImpactSeverity={priceImpactSeverity}
independentField={independentField}
/>
)
}
@@ -180,11 +220,14 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
isOpen={showConfirm}
title="Confirm Swap"
onDismiss={() => {
resetModal()
setShowConfirm(false)
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.INPUT, '')
}
setTxHash('')
}}
attemptingTxn={attemptingTxn}
pendingConfirmation={pendingConfirmation}
hash={txHash}
topContent={modalHeader}
bottomContent={modalBottom}
@@ -203,7 +246,10 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
onMax={() => {
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
}}
onTokenSelection={address => onTokenSelection(Field.INPUT, address)}
onTokenSelection={address => {
setApprovalSubmitted(false) // reset 2 step UI for approvals
onTokenSelection(Field.INPUT, address)
}}
otherSelectedTokenAddress={tokens[Field.OUTPUT]?.address}
id="swap-currency-input"
/>
@@ -213,18 +259,19 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
<ArrowWrapper>
<ArrowDown
size="16"
onClick={onSwitchTokens}
onClick={() => {
setApprovalSubmitted(false) // reset 2 step UI for approvals
onSwitchTokens()
}}
color={tokens[Field.INPUT] && tokens[Field.OUTPUT] ? theme.primary1 : theme.text2}
/>
</ArrowWrapper>
</AutoColumn>
</CursorPointer>
<CurrencyInputPanel
field={Field.OUTPUT}
value={formattedAmounts[Field.OUTPUT]}
onUserInput={onUserInput}
// eslint-disable-next-line @typescript-eslint/no-empty-function
label={independentField === Field.INPUT ? 'To (estimated)' : 'To'}
showMaxButton={false}
token={tokens[Field.OUTPUT]}
@@ -234,33 +281,33 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
/>
</>
{!noRoute && tokens[Field.OUTPUT] && tokens[Field.INPUT] && (
<Card padding={'.25rem 1.25rem 0 .75rem'} borderRadius={'20px'}>
<AutoColumn gap="4px">
<RowBetween align="center">
<Text fontWeight={500} fontSize={14} color={theme.text2}>
Price
</Text>
<TradePrice trade={bestTrade} showInverted={showInverted} setShowInverted={setShowInverted} />
</RowBetween>
<Card padding={'.25rem .75rem 0 .75rem'} borderRadius={'20px'}>
<AutoColumn gap="4px">
<RowBetween align="center">
<Text fontWeight={500} fontSize={14} color={theme.text2}>
Price
</Text>
<TradePrice
inputToken={tokens[Field.INPUT]}
outputToken={tokens[Field.OUTPUT]}
price={bestTrade?.executionPrice}
showInverted={showInverted}
setShowInverted={setShowInverted}
/>
</RowBetween>
{bestTrade && priceImpactSeverity > 1 && (
<RowBetween>
<TYPE.main
style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }}
fontSize={14}
>
Price Impact
</TYPE.main>
<RowFixed>
<FormattedPriceImpact priceImpact={priceImpactWithoutFee} />
<QuestionHelper text="The difference between the market price and estimated price due to trade size." />
</RowFixed>
</RowBetween>
)}
</AutoColumn>
</Card>
)}
{allowedSlippage !== INITIAL_ALLOWED_SLIPPAGE && (
<RowBetween align="center">
<ClickableText fontWeight={500} fontSize={14} color={theme.text2} onClick={toggleSettings}>
Slippage Tolerance
</ClickableText>
<ClickableText fontWeight={500} fontSize={14} color={theme.text2} onClick={toggleSettings}>
{allowedSlippage ? allowedSlippage / 100 : '-'}%
</ClickableText>
</RowBetween>
)}
</AutoColumn>
</Card>
</AutoColumn>
<BottomGrouping>
{!account ? (
@@ -269,50 +316,62 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
<GreyCard style={{ textAlign: 'center' }}>
<TYPE.main mb="4px">Insufficient liquidity for this trade.</TYPE.main>
</GreyCard>
) : approval === ApprovalState.NOT_APPROVED || approval === ApprovalState.PENDING ? (
<ButtonLight onClick={approveCallback} disabled={approval === ApprovalState.PENDING}>
{approval === ApprovalState.PENDING ? (
<Dots>Approving {tokens[Field.INPUT]?.symbol}</Dots>
) : (
'Approve ' + tokens[Field.INPUT]?.symbol
)}
</ButtonLight>
) : showApproveFlow ? (
<RowBetween>
<ButtonPrimary
onClick={approveCallback}
disabled={approval !== ApprovalState.NOT_APPROVED || approvalSubmitted}
width="48%"
altDisbaledStyle={approval === ApprovalState.PENDING} // show solid button while waiting
>
{approval === ApprovalState.PENDING ? (
<Dots>Approving</Dots>
) : approvalSubmitted && approval === ApprovalState.APPROVED ? (
'Approved'
) : (
'Approve ' + tokens[Field.INPUT]?.symbol
)}
</ButtonPrimary>
<ButtonError
onClick={() => {
expertMode ? onSwap() : setShowConfirm(true)
}}
width="48%"
id="swap-button"
disabled={!isValid || approval !== ApprovalState.APPROVED || (priceImpactSeverity > 3 && !expertMode)}
error={isValid && priceImpactSeverity > 2}
>
<Text fontSize={16} fontWeight={500}>
{priceImpactSeverity > 3 && !expertMode
? `Price Impact High`
: `Swap${priceImpactSeverity > 2 ? ' Anyway' : ''}`}
</Text>
</ButtonError>
</RowBetween>
) : (
<ButtonError
onClick={() => {
setShowConfirm(true)
expertMode ? onSwap() : setShowConfirm(true)
}}
id="swap-button"
disabled={!isValid}
disabled={!isValid || (priceImpactSeverity > 3 && !expertMode)}
error={isValid && priceImpactSeverity > 2}
>
<Text fontSize={20} fontWeight={500}>
{error ?? `Swap${priceImpactSeverity > 2 ? ' Anyway' : ''}`}
{error
? error
: priceImpactSeverity > 3 && !expertMode
? `Price Impact Too High`
: `Swap${priceImpactSeverity > 2 ? ' Anyway' : ''}`}
</Text>
</ButtonError>
)}
<V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} />
{betterTradeLinkVersion && <BetterTradeLink version={betterTradeLinkVersion} />}
</BottomGrouping>
</Wrapper>
</AppBody>
{bestTrade && (
<AdvancedSwapDetailsDropdown
trade={bestTrade}
rawSlippage={allowedSlippage}
deadline={deadline}
showAdvanced={showAdvanced}
setShowAdvanced={setShowAdvanced}
setDeadline={setDeadline}
setRawSlippage={setAllowedSlippage}
/>
)}
{priceImpactWithoutFee && priceImpactSeverity > 2 && (
<AutoColumn gap="lg" style={{ marginTop: '1rem' }}>
<PriceSlippageWarningCard priceSlippage={priceImpactWithoutFee} />
</AutoColumn>
)}
<AdvancedSwapDetailsDropdown trade={bestTrade} />
</>
)
}

View File

@@ -23,5 +23,6 @@ export type PopupContent =
export const updateBlockNumber = createAction<{ chainId: number; blockNumber: number }>('updateBlockNumber')
export const toggleWalletModal = createAction<void>('toggleWalletModal')
export const addPopup = createAction<{ content: PopupContent }>('addPopup')
export const toggleSettingsMenu = createAction<void>('toggleSettingsMenu')
export const addPopup = createAction<{ key?: string; content: PopupContent }>('addPopup')
export const removePopup = createAction<{ key: string }>('removePopup')

View File

@@ -1,6 +1,6 @@
import { useCallback, useMemo } from 'react'
import { useActiveWeb3React } from '../../hooks'
import { addPopup, PopupContent, removePopup, toggleWalletModal } from './actions'
import { addPopup, PopupContent, removePopup, toggleWalletModal, toggleSettingsMenu } from './actions'
import { useSelector, useDispatch } from 'react-redux'
import { AppState } from '../index'
@@ -19,13 +19,22 @@ export function useWalletModalToggle(): () => void {
return useCallback(() => dispatch(toggleWalletModal()), [dispatch])
}
export function useSettingsMenuOpen(): boolean {
return useSelector((state: AppState) => state.application.settingsMenuOpen)
}
export function useToggleSettingsMenu(): () => void {
const dispatch = useDispatch()
return useCallback(() => dispatch(toggleSettingsMenu()), [dispatch])
}
// returns a function that allows adding a popup
export function useAddPopup(): (content: PopupContent) => void {
export function useAddPopup(): (content: PopupContent, key?: string) => void {
const dispatch = useDispatch()
return useCallback(
(content: PopupContent) => {
dispatch(addPopup({ content }))
(content: PopupContent, key?: string) => {
dispatch(addPopup({ content, key }))
},
[dispatch]
)

View File

@@ -1,5 +1,12 @@
import { createReducer, nanoid } from '@reduxjs/toolkit'
import { addPopup, PopupContent, removePopup, toggleWalletModal, updateBlockNumber } from './actions'
import {
addPopup,
PopupContent,
removePopup,
toggleWalletModal,
toggleSettingsMenu,
updateBlockNumber
} from './actions'
type PopupList = Array<{ key: string; show: boolean; content: PopupContent }>
@@ -7,12 +14,14 @@ interface ApplicationState {
blockNumber: { [chainId: number]: number }
popupList: PopupList
walletModalOpen: boolean
settingsMenuOpen: boolean
}
const initialState: ApplicationState = {
blockNumber: {},
popupList: [],
walletModalOpen: false
walletModalOpen: false,
settingsMenuOpen: false
}
export default createReducer(initialState, builder =>
@@ -28,9 +37,13 @@ export default createReducer(initialState, builder =>
.addCase(toggleWalletModal, state => {
state.walletModalOpen = !state.walletModalOpen
})
.addCase(addPopup, (state, { payload: { content } }) => {
.addCase(toggleSettingsMenu, state => {
state.settingsMenuOpen = !state.settingsMenuOpen
})
.addCase(addPopup, (state, { payload: { content, key } }) => {
if (key && state.popupList.some(popup => popup.key === key)) return
state.popupList.push({
key: nanoid(),
key: key || nanoid(),
show: true,
content
})

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useActiveWeb3React } from '../../hooks'
import useDebounce from '../../hooks/useDebounce'
import useIsWindowVisible from '../../hooks/useIsWindowVisible'
@@ -10,41 +10,48 @@ export default function Updater() {
const dispatch = useDispatch()
const windowVisible = useIsWindowVisible()
const [maxBlockNumber, setMaxBlockNumber] = useState<number | null>(null)
// because blocks arrive in bunches with longer polling periods, we just want
// to process the latest one.
const debouncedMaxBlockNumber = useDebounce<number | null>(maxBlockNumber, 100)
// update block number
useEffect(() => {
if (!library || !chainId) return
const [state, setState] = useState<{ chainId: number | undefined; blockNumber: number | null }>({
chainId,
blockNumber: null
})
const blockListener = (blockNumber: number) => {
setMaxBlockNumber(maxBlockNumber => {
if (typeof maxBlockNumber !== 'number') return blockNumber
return Math.max(maxBlockNumber, blockNumber)
const blockNumberCallback = useCallback(
(blockNumber: number) => {
setState(state => {
if (chainId === state.chainId) {
if (typeof state.blockNumber !== 'number') return { chainId, blockNumber }
return { chainId, blockNumber: Math.max(blockNumber, state.blockNumber) }
}
return state
})
}
},
[chainId, setState]
)
setMaxBlockNumber(null)
// attach/detach listeners
useEffect(() => {
if (!library || !chainId || !windowVisible) return
setState({ chainId, blockNumber: null })
library
.getBlockNumber()
.then(blockNumber => dispatch(updateBlockNumber({ chainId, blockNumber })))
.catch(error => console.error(`Failed to get block number for chainId ${chainId}`, error))
.then(blockNumberCallback)
.catch(error => console.error(`Failed to get block number for chainId: ${chainId}`, error))
library.on('block', blockListener)
library.on('block', blockNumberCallback)
return () => {
library.removeListener('block', blockListener)
library.removeListener('block', blockNumberCallback)
}
}, [dispatch, chainId, library])
}, [dispatch, chainId, library, blockNumberCallback, windowVisible])
const debouncedState = useDebounce(state, 100)
useEffect(() => {
if (!chainId || !debouncedMaxBlockNumber) return
if (windowVisible) {
dispatch(updateBlockNumber({ chainId, blockNumber: debouncedMaxBlockNumber }))
}
}, [chainId, debouncedMaxBlockNumber, windowVisible, dispatch])
if (!debouncedState.chainId || !debouncedState.blockNumber || !windowVisible) return
dispatch(updateBlockNumber({ chainId: debouncedState.chainId, blockNumber: debouncedState.blockNumber }))
}, [windowVisible, dispatch, debouncedState.blockNumber, debouncedState.chainId])
return null
}

View File

@@ -5,7 +5,7 @@ import { useActiveWeb3React } from '../../hooks'
import { AppDispatch, AppState } from '../index'
import { Field, typeInput } from './actions'
import { setDefaultsFromURLMatchParams } from '../mint/actions'
import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
import { useToken } from '../../hooks/Tokens'
import { Token, Pair, TokenAmount, Percent, JSBI, Route } from '@uniswap/sdk'
import { usePair } from '../../data/Reserves'
import { useTokenBalances } from '../wallet/hooks'
@@ -40,12 +40,12 @@ export function useDerivedBurnInfo(): {
} = useBurnState()
// tokens
const tokenA = useTokenByAddressAndAutomaticallyAdd(tokenAAddress)
const tokenB = useTokenByAddressAndAutomaticallyAdd(tokenBAddress)
const tokenA = useToken(tokenAAddress)
const tokenB = useToken(tokenBAddress)
const tokens: { [field in Extract<Field, Field.TOKEN_A | Field.TOKEN_B>]?: Token } = useMemo(
() => ({
[Field.TOKEN_A]: tokenA,
[Field.TOKEN_B]: tokenB
[Field.TOKEN_A]: tokenA ?? undefined,
[Field.TOKEN_B]: tokenB ?? undefined
}),
[tokenA, tokenB]
)

View File

@@ -5,7 +5,7 @@ import { Token, TokenAmount, Route, JSBI, Price, Percent, Pair } from '@uniswap/
import { useActiveWeb3React } from '../../hooks'
import { AppDispatch, AppState } from '../index'
import { setDefaultsFromURLMatchParams, Field, typeInput } from './actions'
import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
import { useToken } from '../../hooks/Tokens'
import { useTokenBalancesTreatWETHAsETH } from '../wallet/hooks'
import { usePair } from '../../data/Reserves'
import { useTotalSupply } from '../../data/TotalSupply'
@@ -42,8 +42,8 @@ export function useDerivedMintInfo(): {
const dependentField = independentField === Field.TOKEN_A ? Field.TOKEN_B : Field.TOKEN_A
// tokens
const tokenA = useTokenByAddressAndAutomaticallyAdd(tokenAAddress)
const tokenB = useTokenByAddressAndAutomaticallyAdd(tokenBAddress)
const tokenA = useToken(tokenAAddress)
const tokenB = useToken(tokenBAddress)
const tokens: { [field in Field]?: Token } = useMemo(
() => ({
[Field.TOKEN_A]: tokenA,

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