Compare commits

..

44 Commits

Author SHA1 Message Date
Moody Salem
19b1e9e399 feat(weth): support WETH across the site and use sdk 3.0 (#947)
* first pass of sdk 3.0

* second pass using weth

* kill unused pool popup

* get it compiling again

* first pass of sdk 3.0

* switch to currencies

* get it compiling after the big move merge

* restore margin

* clean up add liquidity more

* fix a bunch of bugs

* todo trade on v1

* show eth in currency list

* allow selecting eth in the swap page

* fix unit tests for swap page

* test lint errors

* fix failing integration tests

* fix another couple of failing unit tests

* handle selecting currency b when no currency a

* improve the import pool page

* clean up add liquidity for invalid pairs

* bold

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

* fix some bugs in add liquidity, burn hook

* fix last of ts errors in remove liquidity

* support wrapping/unwrapping weth

* kill a bunch of code including the dummy pairs

* required pair prop in the position card

* tests for the v1 swap arguments

* do not say estimated on the wrap ui

* show ETH instead of WETH in the pool summaries

* small size socks

* fix lint error

* in burn, use currencies from the URL

* fix some integration tests

* both contain weth

* receive eth/weth link

* fix empty row

* show wrapped only if one currency is weth

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

* strict, some refactoring

* denser common bases

* more add liquidity refactoring

* add liquidity paths working

* show common bases in the token selects

* fix the ability to select duplicate tokens

* useless rename

* try to handle alllll the duplicate token edge cases

* think i got them all lol

* remove common bases header

* Revert "remove common bases header"

This reverts commit 6ac4565d

* fix and add integration tests

* make gap between rows smaller

* get integration tests actually running again

* try another format of the command, upgrade serve

* frozen lockfile on install

* try the cypress github action

* install cypress in ci

* remove redundant ignore-scripts command

* use a specific github commit for the pinata action

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

* improve the enter key on the token search modal

* stop using history.push

* fix linting errors

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

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

* accidental import

* error for recipient field

* query parameter working

* undo id change

* tweaks to match mocks better

* tweaks to match mocks better

* some extra integration tests

* clean up nav tabs a bit

* clean up nav tabs a bit

* space swap/pool better

* stop selecting button text when double clicking

* remove unused transfer modal header

* add info to swap confirm modal

* shorten address

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

* fix lint errors

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

* revert the unfinished portis logic

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

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

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

View File

@@ -4,13 +4,8 @@ on:
schedule:
- cron: '0 12 * * 1-4'
# releases are triggered on changes to this file
push:
branches:
- v2
paths:
- '.github/workflows/release.yaml'
- '.env.production'
# manual trigger
workflow_dispatch:
jobs:
bump_version:
@@ -44,14 +39,14 @@ jobs:
node-version: '12'
- name: Install dependencies
run: yarn install --ignore-scripts --frozen-lockfile
run: yarn install --frozen-lockfile
- name: Build the IPFS bundle
run: yarn ipfs-build
run: yarn build
- name: Pin to IPFS
id: upload
uses: anantaramdas/ipfs-pinata-deploy-action@v1.5.2
uses: anantaramdas/ipfs-pinata-deploy-action@39bbda1ce1fe24c69c6f57861b8038278d53688d
with:
pin-name: Uniswap ${{ needs.bump_version.outputs.new_tag }}
path: './build'
@@ -64,15 +59,15 @@ jobs:
with:
cidv0: ${{ steps.upload.outputs.hash }}
- name: Update DNS with new IPFS hash
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:
cid: ${{ steps.upload.outputs.hash }}
# - name: Update DNS with new IPFS hash
# 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:
# cid: ${{ steps.upload.outputs.hash }}
- name: Create GitHub Release
id: create_release
@@ -97,7 +92,7 @@ jobs:
Preferred URLs:
- https://${{ steps.convert_cidv0.outputs.cidv1 }}.ipfs.dweb.link/
- https://${{ steps.convert_cidv0.outputs.cidv1 }}.cf-ipfs.com/
- https://${{ steps.convert_cidv0.outputs.cidv1 }}.ipfs.cf-ipfs.com/
- [ipfs://${{ steps.upload.outputs.hash }}/](ipfs://${{ steps.upload.outputs.hash }}/)
Other IPFS gateways:

View File

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

1
.yarnrc Normal file
View File

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

View File

@@ -1,11 +1,12 @@
# Uniswap Frontend
# Uniswap Interface
[![Tests](https://github.com/Uniswap/uniswap-frontend/workflows/Tests/badge.svg)](https://github.com/Uniswap/uniswap-frontend/actions?query=workflow%3ATests)
[![Tests](https://github.com/Uniswap/uniswap-interface/workflows/Tests/badge.svg)](https://github.com/Uniswap/uniswap-interface/actions?query=workflow%3ATests)
[![Styled With Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://prettier.io/)
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,31 +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 V1 interface
## Accessing Uniswap Interface 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-frontend/releases/tag/v1.0.0).
The Uniswap Interface supports swapping against, and migrating or removing liquidity from Uniswap V1. However,
if you would like to use Uniswap V1, the Uniswap V1 interface for mainnet and testnets is accessible via IPFS gateways
linked from the [v1.0.0 release](https://github.com/Uniswap/uniswap-interface/releases/tag/v1.0.0).

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,19 +1,19 @@
{
"name": "@uniswap/interface",
"description": "Uniswap Interface",
"homepage": "https://uniswap.exchange",
"homepage": ".",
"private": true,
"devDependencies": {
"@ethersproject/address": "^5.0.0-beta.134",
"@ethersproject/bignumber": "^5.0.0-beta.138",
"@ethersproject/constants": "^5.0.0-beta.133",
"@ethersproject/contracts": "^5.0.0-beta.151",
"@ethersproject/experimental": "^5.0.0-beta.141",
"@ethersproject/providers": "5.0.0-beta.162",
"@ethersproject/strings": "^5.0.0-beta.136",
"@ethersproject/units": "^5.0.0-beta.132",
"@ethersproject/wallet": "^5.0.0-beta.141",
"@popperjs/core": "^2.4.0",
"@ethersproject/address": "^5.0.1",
"@ethersproject/bignumber": "^5.0.3",
"@ethersproject/constants": "^5.0.1",
"@ethersproject/contracts": "^5.0.1",
"@ethersproject/experimental": "^5.0.0",
"@ethersproject/providers": "^5.0.4",
"@ethersproject/strings": "^5.0.1",
"@ethersproject/units": "^5.0.1",
"@ethersproject/wallet": "^5.0.1",
"@popperjs/core": "^2.4.4",
"@reach/dialog": "^0.10.3",
"@reach/portal": "^0.10.3",
"@reduxjs/toolkit": "^1.3.5",
@@ -27,11 +27,11 @@
"@types/react-router-dom": "^5.0.0",
"@types/react-window": "^1.8.2",
"@types/rebass": "^4.0.5",
"@types/styled-components": "^4.2.0",
"@types/styled-components": "^5.1.0",
"@types/testing-library__cypress": "^5.0.5",
"@typescript-eslint/eslint-plugin": "^2.31.0",
"@typescript-eslint/parser": "^2.31.0",
"@uniswap/sdk": "^2.0.5",
"@uniswap/sdk": "3.0.1",
"@uniswap/v2-core": "1.0.0",
"@uniswap/v2-periphery": "^1.1.0-beta.0",
"@web3-react/core": "^6.0.9",
@@ -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",
@@ -81,14 +82,10 @@
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"ipfs-build": "cross-env PUBLIC_URL=\".\" react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
"lint:fix": "eslint 'src/**/*.{js,jsx,ts,tsx}' --fix",
"cy:run": "cypress run",
"serve:build": "serve -s build -l 3000",
"integration-test": "yarn build && start-server-and-test 'yarn run serve:build' http://localhost:3000 cy:run"
"integration-test": "start-server-and-test 'serve build -l 3000' http://localhost:3000 'cypress run'"
},
"eslintConfig": {
"extends": "react-app"

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,6 @@
import React, { useState } from 'react'
import styled from 'styled-components'
import { isAddress } from '../../utils'
import { useActiveWeb3React } from '../../hooks'
import { WETH } from '@uniswap/sdk'
import { Currency, Token } from '@uniswap/sdk'
import EthereumLogo from '../../assets/images/ethereum-logo.png'
@@ -35,45 +33,46 @@ const StyledEthereumLogo = styled.img<{ size: string }>`
border-radius: 24px;
`
export default function TokenLogo({
address,
export default function CurrencyLogo({
currency,
size = '24px',
...rest
}: {
address?: string
currency?: Currency
size?: string
style?: React.CSSProperties
}) {
const [, refresh] = useState<number>(0)
const { chainId } = useActiveWeb3React()
let path = ''
const validated = isAddress(address)
// hard code to show ETH instead of WETH in UI
if (validated === WETH[chainId].address) {
return <StyledEthereumLogo src={EthereumLogo} size={size} {...rest} />
} else if (!NO_LOGO_ADDRESSES[address] && validated) {
path = getTokenLogoURL(validated)
} else {
if (currency instanceof Token) {
let path = ''
if (!NO_LOGO_ADDRESSES[currency.address]) {
path = getTokenLogoURL(currency.address)
} else {
return (
<Emoji {...rest} size={size}>
<span role="img" aria-label="Thinking">
🤔
</span>
</Emoji>
)
}
return (
<Emoji {...rest} size={size}>
<span role="img" aria-label="Thinking">
🤔
</span>
</Emoji>
<Image
{...rest}
alt={`${currency.name} Logo`}
src={path}
size={size}
onError={() => {
if (currency instanceof Token) {
NO_LOGO_ADDRESSES[currency.address] = true
}
refresh(i => i + 1)
}}
/>
)
} else {
return <StyledEthereumLogo src={EthereumLogo} size={size} {...rest} />
}
return (
<Image
{...rest}
// alt={address}
src={path}
size={size}
onError={() => {
NO_LOGO_ADDRESSES[address] = true
refresh(i => i + 1)
}}
/>
)
}

View File

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

View File

@@ -4,6 +4,7 @@ import { Link, useLocation } from 'react-router-dom'
import styled from 'styled-components'
import useParsedQueryString from '../../hooks/useParsedQueryString'
import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
import { MouseoverTooltip } from '../Tooltip'
const VersionLabel = styled.span<{ enabled: boolean }>`
padding: 0.35rem 0.6rem;
@@ -11,7 +12,7 @@ const VersionLabel = styled.span<{ enabled: boolean }>`
background: ${({ theme, enabled }) => (enabled ? theme.primary1 : 'none')};
color: ${({ theme, enabled }) => (enabled ? theme.white : theme.text1)};
font-size: 1rem;
font-weight: ${({ theme, enabled }) => (enabled ? '500' : '400')};
font-weight: ${({ enabled }) => (enabled ? '500' : '400')};
:hover {
user-select: ${({ enabled }) => (enabled ? 'none' : 'initial')};
background: ${({ theme, enabled }) => (enabled ? theme.primary1 : 'none')};
@@ -61,10 +62,15 @@ export default function VersionSwitch() {
[versionSwitchAvailable]
)
return (
const toggle = (
<VersionToggle enabled={versionSwitchAvailable} to={toggleDest} onClick={handleClick}>
<VersionLabel enabled={version === Version.v2 || !versionSwitchAvailable}>V2</VersionLabel>
<VersionLabel enabled={version === Version.v1 && versionSwitchAvailable}>V1</VersionLabel>
</VersionToggle>
)
return versionSwitchAvailable ? (
toggle
) : (
<MouseoverTooltip text="This page is only compatible with Uniswap V2.">{toggle}</MouseoverTooltip>
)
}

View File

@@ -1,7 +1,6 @@
import { ChainId, WETH } from '@uniswap/sdk'
import { ChainId } from '@uniswap/sdk'
import React from 'react'
import { isMobile } from 'react-device-detect'
import { Link as HistoryLink } from 'react-router-dom'
import { Text } from 'rebass'
import styled from 'styled-components'
@@ -12,11 +11,9 @@ 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 { useETHBalances } from '../../state/wallet/hooks'
import { ExternalLink, StyledInternalLink } from '../../theme'
import { YellowCard } from '../Card'
import { AutoColumn } from '../Column'
import Settings from '../Settings'
import Menu from '../Menu'
@@ -49,12 +46,12 @@ const HeaderElementWrap = styled.div`
display: flex;
align-items: center;
${({ theme }) => theme.mediaWidth.upToExtraSmall`
margin-top: 0.5rem;
${({ theme }) => theme.mediaWidth.upToSmall`
margin-top: 0.5rem;
`};
`
const Title = styled.div`
const Title = styled.a`
display: flex;
align-items: center;
pointer-events: auto;
@@ -100,41 +97,33 @@ const NetworkCard = styled(YellowCard)`
padding: 8px 12px;
`
const UniIcon = styled(HistoryLink)<{ to: string }>`
const UniIcon = styled.div`
transition: transform 0.3s ease;
:hover {
transform: rotate(-5deg);
}
`
const MigrateBanner = styled(AutoColumn)`
width: 100%;
padding: 12px 0;
display: flex;
justify-content: center;
background-color: ${({ theme }) => theme.primary5};
color: ${({ theme }) => theme.primaryText1};
font-weight: 400;
text-align: center;
pointer-events: auto;
a {
color: ${({ theme }) => theme.primaryText1};
}
${({ theme }) => theme.mediaWidth.upToSmall`
padding: 0;
display: none;
img {
width: 4.5rem;
}
`};
`
const HeaderControls = styled.div`
display: flex;
flex-direction: row;
align-items: flex-end;
align-items: center;
${({ theme }) => theme.mediaWidth.upToSmall`
flex-direction: column;
`};
align-items: flex-end;
`};
`
const BalanceText = styled(Text)`
${({ theme }) => theme.mediaWidth.upToExtraSmall`
display: none;
`};
`
const NETWORK_LABELS: { [chainId in ChainId]: string | null } = {
@@ -148,39 +137,20 @@ const NETWORK_LABELS: { [chainId in ChainId]: string | null } = {
export default function Header() {
const { account, chainId } = useActiveWeb3React()
const userEthBalance = useTokenBalanceTreatingWETHasETH(account, WETH[chainId])
const userEthBalance = useETHBalances([account])[account]
const [isDark] = useDarkModeManager()
return (
<HeaderFrame>
<MigrateBanner>
Uniswap V2 is live! Read the&nbsp;
<ExternalLink href="https://uniswap.org/blog/launch-uniswap-v2/">
<b>blog post </b>
</ExternalLink>
&nbsp;or&nbsp;
<StyledInternalLink to="/migrate/v1">
<b>migrate your liquidity </b>
</StyledInternalLink>
.
</MigrateBanner>
<RowBetween style={{ alignItems: 'flex-start' }} padding="1rem 1rem 0 1rem">
<HeaderElement>
<Title>
<UniIcon id="link" to="/">
<Title href=".">
<UniIcon>
<img src={isDark ? LogoDark : Logo} alt="logo" />
</UniIcon>
{!isMobile && (
<TitleText>
<HistoryLink id="link" to="/">
<img
style={{ marginLeft: '4px', marginTop: '4px' }}
src={isDark ? WordmarkDark : Wordmark}
alt="logo"
/>
</HistoryLink>
</TitleText>
)}
<TitleText>
<img style={{ marginLeft: '4px', marginTop: '4px' }} src={isDark ? WordmarkDark : Wordmark} alt="logo" />
</TitleText>
</Title>
</HeaderElement>
<HeaderControls>
@@ -190,9 +160,9 @@ export default function Header() {
</TestnetWrapper>
<AccountElement active={!!account} style={{ pointerEvents: 'auto' }}>
{account && userEthBalance ? (
<Text style={{ flexShrink: 0 }} pl="0.75rem" pr="0.5rem" fontWeight={500}>
<BalanceText style={{ flexShrink: 0 }} pl="0.75rem" pr="0.5rem" fontWeight={500}>
{userEthBalance?.toSignificant(4)} ETH
</Text>
</BalanceText>
) : null}
<Web3Status />
</AccountElement>

View File

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

View File

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

View File

@@ -1,17 +1,12 @@
import { ChainId, Pair, Token } from '@uniswap/sdk'
import React, { useContext, useMemo } from 'react'
import React, { useContext } from 'react'
import styled, { ThemeContext } from 'styled-components'
import { useMediaLayout } from 'use-media'
import { X } from 'react-feather'
import { PopupContent } from '../../state/application/actions'
import { useActivePopups, useRemovePopup } from '../../state/application/hooks'
import { ExternalLink } from '../../theme'
import { AutoColumn } from '../Column'
import DoubleTokenLogo from '../DoubleLogo'
import Row from '../Row'
import TxnPopup from '../TxnPopup'
import { Text } from 'rebass'
const StyledClose = styled(X)`
position: absolute;
@@ -72,52 +67,12 @@ const Popup = styled.div`
`}
`
function PoolPopup({
token0,
token1
}: {
token0: { address?: string; symbol?: string }
token1: { address?: string; symbol?: string }
}) {
const pairAddress: string | null = useMemo(() => {
if (!token0 || !token1) return null
// just mock it out
return Pair.getAddress(
new Token(ChainId.MAINNET, token0.address, 18),
new Token(ChainId.MAINNET, token1.address, 18)
)
}, [token0, token1])
return (
<AutoColumn gap={'10px'}>
<Text fontSize={20} fontWeight={500}>
Pool Imported
</Text>
<Row>
<DoubleTokenLogo a0={token0?.address ?? ''} a1={token1?.address ?? ''} margin={true} />
<Text fontSize={16} fontWeight={500}>
UNI {token0?.symbol} / {token1?.symbol}
</Text>
</Row>
{pairAddress ? (
<ExternalLink href={`https://uniswap.info/pair/${pairAddress}`}>View on Uniswap Info.</ExternalLink>
) : null}
</AutoColumn>
)
}
function PopupItem({ content, popKey }: { content: PopupContent; popKey: string }) {
if ('txn' in content) {
const {
txn: { hash, success, summary }
} = content
return <TxnPopup popKey={popKey} hash={hash} success={success} summary={summary} />
} else if ('poolAdded' in content) {
const {
poolAdded: { token0, token1 }
} = content
return <PoolPopup token0={token0} token1={token1} />
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,141 +0,0 @@
import { Pair } from '@uniswap/sdk'
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import { isMobile } from 'react-device-detect'
import { useTranslation } from 'react-i18next'
import { RouteComponentProps, withRouter } from 'react-router-dom'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import Card from '../../components/Card'
import { useActiveWeb3React } from '../../hooks'
import { useAllTokens } from '../../hooks/Tokens'
import { useAllDummyPairs } from '../../state/user/hooks'
import { useTokenBalances } from '../../state/wallet/hooks'
import { CloseIcon, StyledInternalLink } from '../../theme/components'
import { isAddress } from '../../utils'
import Column from '../Column'
import Modal from '../Modal'
import QuestionHelper from '../QuestionHelper'
import { AutoRow, RowBetween } from '../Row'
import { filterPairs } from './filtering'
import PairList from './PairList'
import { pairComparator } from './sorting'
import { PaddedColumn, SearchInput } from './styleds'
interface PairSearchModalProps extends RouteComponentProps {
isOpen?: boolean
onDismiss?: () => void
}
function PairSearchModal({ history, isOpen, onDismiss }: PairSearchModalProps) {
const { t } = useTranslation()
const { account } = useActiveWeb3React()
const theme = useContext(ThemeContext)
const [searchQuery, setSearchQuery] = useState<string>('')
const allTokens = useAllTokens()
const allPairs = useAllDummyPairs()
const allPairBalances = useTokenBalances(
account,
allPairs.map(p => p.liquidityToken)
)
// clear the input on open
useEffect(() => {
if (isOpen) setSearchQuery('')
}, [isOpen, setSearchQuery])
// manage focus on modal show
const inputRef = useRef<HTMLInputElement>()
function onInput(event) {
const input = event.target.value
const checksummedInput = isAddress(input)
setSearchQuery(checksummedInput || input)
}
const 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,138 +0,0 @@
import { JSBI, Token, TokenAmount } from '@uniswap/sdk'
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { FixedSizeList } from 'react-window'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import { useActiveWeb3React } from '../../hooks'
import { useAllTokens } from '../../hooks/Tokens'
import { useAddUserToken, useRemoveUserAddedToken } from '../../state/user/hooks'
import { LinkStyledButton, TYPE } from '../../theme'
import { ButtonSecondary } from '../Button'
import Column, { AutoColumn } from '../Column'
import { RowFixed } from '../Row'
import TokenLogo from '../TokenLogo'
import { FadedSpan, GreySpan, MenuItem, ModalInfo } from './styleds'
import Loader from '../Loader'
import { isDefaultToken, isCustomAddedToken } from '../../utils'
export default function TokenList({
tokens,
allTokenBalances,
selectedToken,
onTokenSelect,
otherToken,
showSendWithSwap,
otherSelectedText
}: {
tokens: Token[]
selectedToken: string
allTokenBalances: { [tokenAddress: string]: TokenAmount }
onTokenSelect: (tokenAddress: string) => void
otherToken: string
showSendWithSwap?: boolean
otherSelectedText: string
}) {
const { t } = useTranslation()
const { account, chainId } = useActiveWeb3React()
const theme = useContext(ThemeContext)
const allTokens = useAllTokens()
const addToken = useAddUserToken()
const removeToken = useRemoveUserAddedToken()
if (tokens.length === 0) {
return <ModalInfo>{t('noToken')}</ModalInfo>
}
return (
<FixedSizeList
width="100%"
height={500}
itemCount={tokens.length}
itemSize={56}
style={{ flex: '1' }}
itemKey={index => tokens[index].address}
>
{({ index, style }) => {
const token = tokens[index]
const { address, symbol } = token
const isDefault = isDefaultToken(token)
const customAdded = isCustomAddedToken(allTokens, token)
const balance = allTokenBalances[address]
const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw)
return (
<MenuItem
style={style}
key={address}
className={`token-item-${address}`}
onClick={() => (selectedToken && selectedToken === address ? null : onTokenSelect(address))}
disabled={selectedToken && selectedToken === address}
selected={otherToken === address}
>
<RowFixed>
<TokenLogo address={address} size={'24px'} style={{ marginRight: '14px' }} />
<Column>
<Text fontWeight={500}>
{symbol}
{otherToken === address && <GreySpan> ({otherSelectedText})</GreySpan>}
</Text>
<FadedSpan>
{customAdded ? (
<TYPE.main fontWeight={500}>
Added by user
<LinkStyledButton
onClick={event => {
event.stopPropagation()
removeToken(chainId, address)
}}
>
(Remove)
</LinkStyledButton>
</TYPE.main>
) : null}
{!isDefault && !customAdded ? (
<TYPE.main fontWeight={500}>
Found by address
<LinkStyledButton
onClick={event => {
event.stopPropagation()
addToken(token)
}}
>
(Add)
</LinkStyledButton>
</TYPE.main>
) : null}
</FadedSpan>
</Column>
</RowFixed>
<AutoColumn>
{balance ? (
<Text>
{zeroBalance && showSendWithSwap ? (
<ButtonSecondary padding={'4px 8px'}>
<Text textAlign="center" fontWeight={500} fontSize={14} color={theme.primary1}>
Send With Swap
</Text>
</ButtonSecondary>
) : balance ? (
balance.toSignificant(6)
) : (
'-'
)}
</Text>
) : account ? (
<Loader />
) : (
'-'
)}
</AutoColumn>
</MenuItem>
)
}}
</FixedSizeList>
)
}

View File

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

View File

@@ -1,8 +1,7 @@
import { Token, TokenAmount, WETH, Pair } from '@uniswap/sdk'
import { Token, TokenAmount, WETH } from '@uniswap/sdk'
import { useMemo } from 'react'
import { useActiveWeb3React } from '../../hooks'
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
import { DUMMY_PAIRS_TO_PIN } from '../../constants'
import { useAllTokenBalances } from '../../state/wallet/hooks'
// compare two token amounts with highest one coming first
function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount) {
@@ -16,26 +15,6 @@ 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 }
@@ -65,7 +44,7 @@ function getTokenComparator(
export function useTokenComparator(inverted: boolean): (tokenA: Token, tokenB: Token) => number {
const { chainId } = useActiveWeb3React()
const weth = WETH[chainId]
const balances = useAllTokenBalancesTreatingWETHasETH()
const balances = useAllTokenBalances()
const comparator = useMemo(() => getTokenComparator(weth, balances ?? {}), [balances, weth])
return useMemo(() => {
if (inverted) {

View File

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

View File

@@ -156,7 +156,7 @@ export default function SettingsTab() {
return (
<StyledMenu ref={node}>
<Modal isOpen={showConfirmation} onDismiss={() => setShowConfirmation(false)}>
<Modal isOpen={showConfirmation} onDismiss={() => setShowConfirmation(false)} maxHeight={100}>
<ModalContentWrapper>
<AutoColumn gap="lg">
<RowBetween style={{ padding: '0 2rem' }}>
@@ -233,7 +233,10 @@ export default function SettingsTab() {
toggleExpertMode()
setShowConfirmation(false)
}
: () => setShowConfirmation(true)
: () => {
toggle()
setShowConfirmation(true)
}
}
/>
</RowBetween>

View File

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

View File

@@ -1,4 +1,4 @@
import { Token } from '@uniswap/sdk'
import { Currency, Token } from '@uniswap/sdk'
import { transparentize } from 'polished'
import React, { useMemo } from 'react'
import styled from 'styled-components'
@@ -11,7 +11,7 @@ import { ExternalLink, TYPE } from '../../theme'
import { getEtherscanLink, isDefaultToken } from '../../utils'
import PropsOfExcluding from '../../utils/props-of-excluding'
import QuestionHelper from '../QuestionHelper'
import TokenLogo from '../TokenLogo'
import CurrencyLogo from '../CurrencyLogo'
const Wrapper = styled.div<{ error: boolean }>`
background: ${({ theme, error }) => transparentize(0.9, error ? theme.red1 : theme.yellow1)};
@@ -103,13 +103,13 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro
<QuestionHelper text={duplicateNameOrSymbol ? DUPLICATE_NAME_HELP_TEXT : HELP_TEXT} />
</Row>
<Row>
<TokenLogo address={token.address} />
<CurrencyLogo currency={token} />
<div style={{ fontWeight: 500 }}>
{token && token.name && token.symbol && token.name !== token.symbol
? `${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>
@@ -127,11 +127,13 @@ const WarningContainer = styled.div`
padding-right: 1rem;
`
export function TokenWarningCards({ tokens }: { tokens: { [field in Field]?: Token } }) {
export function TokenWarningCards({ currencies }: { currencies: { [field in Field]?: Currency } }) {
return (
<WarningContainer>
{Object.keys(tokens).map(field =>
tokens[field] ? <TokenWarningCard style={{ marginBottom: 14 }} key={field} token={tokens[field]} /> : null
{Object.keys(currencies).map(field =>
currencies[field] instanceof Token ? (
<TokenWarningCard style={{ marginBottom: 14 }} key={field} token={currencies[field]} />
) : null
)}
</WarningContainer>
)

View File

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

View File

@@ -62,9 +62,7 @@ export default function TxnPopup({
{success ? <CheckCircle color={theme.green1} size={24} /> : <AlertCircle color={theme.red1} size={24} />}
</div>
<AutoColumn gap="8px">
<TYPE.body fontWeight={500}>
{summary ? summary : 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}
</TYPE.body>
<TYPE.body fontWeight={500}>{summary ?? 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}</TYPE.body>
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>View on Etherscan</ExternalLink>
</AutoColumn>
<Fader count={count} />

View File

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

View File

@@ -31,8 +31,10 @@ function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippag
<RowFixed>
<TYPE.black color={theme.text1} fontSize={14}>
{isExactIn
? `${slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(4)} ${trade.outputAmount.token.symbol}` ?? '-'
: `${slippageAdjustedAmounts[Field.INPUT]?.toSignificant(4)} ${trade.inputAmount.token.symbol}` ?? '-'}
? `${slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(4)} ${trade.outputAmount.currency.symbol}` ??
'-'
: `${slippageAdjustedAmounts[Field.INPUT]?.toSignificant(4)} ${trade.inputAmount.currency.symbol}` ??
'-'}
</TYPE.black>
</RowFixed>
</RowBetween>
@@ -54,7 +56,7 @@ function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippag
<QuestionHelper text="A portion of each trade (0.30%) goes to liquidity providers as a protocol incentive." />
</RowFixed>
<TYPE.black fontSize={14} color={theme.text1}>
{realizedLPFee ? `${realizedLPFee.toSignificant(4)} ${trade.inputAmount.token.symbol}` : '-'}
{realizedLPFee ? `${realizedLPFee.toSignificant(4)} ${trade.inputAmount.currency.symbol}` : '-'}
</TYPE.black>
</RowBetween>
</AutoColumn>

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ import { ChevronRight } from 'react-feather'
import { Flex } from 'rebass'
import { ThemeContext } from 'styled-components'
import { TYPE } from '../../theme'
import TokenLogo from '../TokenLogo'
import CurrencyLogo from '../CurrencyLogo'
export default memo(function SwapRoute({ trade }: { trade: Trade }) {
const theme = useContext(ThemeContext)
@@ -24,7 +24,7 @@ export default memo(function SwapRoute({ trade }: { trade: Trade }) {
return (
<Fragment key={i}>
<Flex my="0.5rem" alignItems="center" style={{ flexShrink: 0 }}>
<TokenLogo address={token.address} size="1.5rem" />
<CurrencyLogo currency={token} size="1.5rem" />
<TYPE.black fontSize={14} color={theme.text1} ml="0.5rem">
{token.symbol}
</TYPE.black>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { ChainId, JSBI, Percent, Token, WETH, Pair, TokenAmount } from '@uniswap/sdk'
import { ChainId, JSBI, Percent, Token, WETH } from '@uniswap/sdk'
import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors'
import { COMP, DAI, MKR, USDC, USDT } from './tokens/mainnet'
@@ -36,38 +36,14 @@ export const BASES_TO_TRACK_LIQUIDITY_FOR: ChainTokenList = {
[ChainId.MAINNET]: [...WETH_ONLY[ChainId.MAINNET], DAI, USDC, USDT]
}
export const DUMMY_PAIRS_TO_PIN: { readonly [chainId in ChainId]?: Pair[] } = {
export const PINNED_PAIRS: { readonly [chainId in ChainId]?: [Token, Token][] } = {
[ChainId.MAINNET]: [
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'
)
)
[
new Token(ChainId.MAINNET, '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643', 8, 'cDAI', 'Compound Dai'),
new Token(ChainId.MAINNET, '0x39AA39c021dfbaE8faC545936693aC917d5E7563', 8, 'cUSDC', 'Compound USD Coin')
],
[USDC, USDT],
[DAI, USDT]
]
}

View File

@@ -14,14 +14,6 @@ export const ALL_TOKENS: AllTokens = [
...KOVAN_TOKENS,
...ROPSTEN_TOKENS
]
// remap WETH to ETH
.map(token => {
if (token.equals(WETH[token.chainId])) {
;(token as any).symbol = 'ETH'
;(token as any).name = 'Ether'
}
return token
})
// put into an object
.reduce<AllTokens>(
(tokenMap, token) => {

View File

@@ -20,6 +20,7 @@ export default [
new Token(ChainId.MAINNET, '0x107c4504cd79C5d2696Ea0030a8dD4e92601B82e', 18, 'BLT', 'Bloom Token'),
new Token(ChainId.MAINNET, '0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C', 18, 'BNT', 'Bancor Network Token'),
new Token(ChainId.MAINNET, '0x0327112423F3A68efdF1fcF402F6c5CB9f7C33fd', 18, 'BTC++', 'PieDAO BTC++'),
new Token(ChainId.MAINNET, '0x56d811088235F11C8920698a204A5010a788f4b3', 18, 'BZRX', 'bZx Protocol Token'),
new Token(ChainId.MAINNET, '0x4F9254C83EB525f9FCf346490bbb3ed28a81C667', 18, 'CELR', 'CelerToken'),
new Token(ChainId.MAINNET, '0xF5DCe57282A584D2746FaF1593d3121Fcac444dC', 8, 'cSAI', 'Compound Dai'),
new Token(ChainId.MAINNET, '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643', 8, 'cDAI', 'Compound Dai'),
@@ -91,7 +92,7 @@ export default [
new Token(ChainId.MAINNET, '0x459086F2376525BdCebA5bDDA135e4E9d3FeF5bf', 8, 'renBCH', 'renBCH'),
new Token(ChainId.MAINNET, '0xEB4C2781e4ebA804CE9a9803C67d0893436bB27D', 8, 'renBTC', 'renBTC'),
new Token(ChainId.MAINNET, '0x1C5db575E2Ff833E46a2E9864C22F4B22E0B37C2', 8, 'renZEC', 'renZEC'),
new Token(ChainId.MAINNET, '0x1985365e9f78359a9B6AD760e32412f4a445E862', 18, 'REP', 'Reputation'),
new Token(ChainId.MAINNET, '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'),
@@ -123,6 +124,7 @@ export default [
new Token(ChainId.MAINNET, '0xA4Bdb11dc0a2bEC88d24A3aa1E6Bb17201112eBe', 6, 'USDS', 'StableUSD'),
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,18 +1,33 @@
import { Token, TokenAmount, Pair } from '@uniswap/sdk'
import { TokenAmount, Pair, Currency } from '@uniswap/sdk'
import { useMemo } from 'react'
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { Interface } from '@ethersproject/abi'
import { useActiveWeb3React } from '../hooks'
import { useMultipleContractSingleData } from '../state/multicall/hooks'
import { wrappedCurrency } from '../utils/wrappedCurrency'
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 usePairs(tokens: [Token | undefined, Token | undefined][]): (undefined | Pair | null)[] {
export enum PairState {
LOADING,
NOT_EXISTS,
EXISTS,
INVALID
}
export function usePairs(currencies: [Currency | undefined, Currency | undefined][]): [PairState, Pair | null][] {
const { chainId } = useActiveWeb3React()
const tokens = useMemo(
() =>
currencies.map(([currencyA, currencyB]) => [
wrappedCurrency(currencyA, chainId),
wrappedCurrency(currencyB, chainId)
]),
[chainId, currencies]
)
const pairAddresses = useMemo(
() =>
tokens.map(([tokenA, tokenB]) => {
@@ -29,15 +44,19 @@ export function usePairs(tokens: [Token | undefined, Token | undefined][]): (und
const tokenA = tokens[i][0]
const tokenB = tokens[i][1]
if (loading || !tokenA || !tokenB) return undefined
if (!reserves) return null
if (loading) return [PairState.LOADING, null]
if (!tokenA || !tokenB || tokenA.equals(tokenB)) return [PairState.INVALID, null]
if (!reserves) return [PairState.NOT_EXISTS, 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()))
return [
PairState.EXISTS,
new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString()))
]
})
}, [results, tokens])
}
export function usePair(tokenA?: Token, tokenB?: Token): undefined | Pair | null {
export function usePair(tokenA?: Currency, tokenB?: Currency): [PairState, Pair | null] {
return usePairs([[tokenA, tokenB]])[0]
}

View File

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

View File

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

View File

@@ -1,17 +1,22 @@
import { Pair, Token, TokenAmount, Trade } from '@uniswap/sdk'
import { Currency, CurrencyAmount, Pair, Token, Trade } from '@uniswap/sdk'
import flatMap from 'lodash.flatmap'
import { useMemo } from 'react'
import { BASES_TO_CHECK_TRADES_AGAINST } from '../constants'
import { usePairs } from '../data/Reserves'
import { PairState, usePairs } from '../data/Reserves'
import { wrappedCurrency } from '../utils/wrappedCurrency'
import { useActiveWeb3React } from './index'
function useAllCommonPairs(tokenA?: Token, tokenB?: Token): Pair[] {
function useAllCommonPairs(currencyA?: Currency, currencyB?: Currency): Pair[] {
const { chainId } = useActiveWeb3React()
const bases: Token[] = chainId ? BASES_TO_CHECK_TRADES_AGAINST[chainId] : []
const [tokenA, tokenB] = chainId
? [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)]
: [undefined, undefined]
const allPairCombinations: [Token | undefined, Token | undefined][] = useMemo(
() => [
// the direct pair
@@ -34,9 +39,9 @@ function useAllCommonPairs(tokenA?: Token, tokenB?: Token): Pair[] {
Object.values(
allPairs
// filter out invalid pairs
.filter((p): p is Pair => !!p)
.filter((result): result is [PairState.EXISTS, Pair] => Boolean(result[0] === PairState.EXISTS && result[1]))
// filter out duplicated pairs
.reduce<{ [pairAddress: string]: Pair }>((memo, curr) => {
.reduce<{ [pairAddress: string]: Pair }>((memo, [, curr]) => {
memo[curr.liquidityToken.address] = memo[curr.liquidityToken.address] ?? curr
return memo
}, {})
@@ -48,27 +53,32 @@ function useAllCommonPairs(tokenA?: Token, tokenB?: Token): Pair[] {
/**
* Returns the best trade for the exact amount of tokens in to the given token out
*/
export function useTradeExactIn(amountIn?: TokenAmount, tokenOut?: Token): Trade | null {
const allowedPairs = useAllCommonPairs(amountIn?.token, tokenOut)
export function useTradeExactIn(currencyAmountIn?: CurrencyAmount, currencyOut?: Currency): Trade | null {
const allowedPairs = useAllCommonPairs(currencyAmountIn?.currency, currencyOut)
return useMemo(() => {
if (amountIn && tokenOut && allowedPairs.length > 0) {
return Trade.bestTradeExactIn(allowedPairs, amountIn, tokenOut, { maxHops: 3, maxNumResults: 1 })[0] ?? null
if (currencyAmountIn && currencyOut && allowedPairs.length > 0) {
return (
Trade.bestTradeExactIn(allowedPairs, currencyAmountIn, currencyOut, { maxHops: 3, maxNumResults: 1 })[0] ?? null
)
}
return null
}, [allowedPairs, amountIn, tokenOut])
}, [allowedPairs, currencyAmountIn, currencyOut])
}
/**
* 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 allowedPairs = useAllCommonPairs(tokenIn, amountOut?.token)
export function useTradeExactOut(currencyIn?: Currency, currencyAmountOut?: CurrencyAmount): Trade | null {
const allowedPairs = useAllCommonPairs(currencyIn, currencyAmountOut?.currency)
return useMemo(() => {
if (tokenIn && amountOut && allowedPairs.length > 0) {
return Trade.bestTradeExactOut(allowedPairs, tokenIn, amountOut, { maxHops: 3, maxNumResults: 1 })[0] ?? null
if (currencyIn && currencyAmountOut && allowedPairs.length > 0) {
return (
Trade.bestTradeExactOut(allowedPairs, currencyIn, currencyAmountOut, { maxHops: 3, maxNumResults: 1 })[0] ??
null
)
}
return null
}, [allowedPairs, tokenIn, amountOut])
}, [allowedPairs, currencyIn, currencyAmountOut])
}

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,228 +1,81 @@
import { BigNumber } from '@ethersproject/bignumber'
import { MaxUint256 } from '@ethersproject/constants'
import { Contract } from '@ethersproject/contracts'
import { ChainId, Trade, TradeType, WETH } from '@uniswap/sdk'
import { JSBI, Percent, Router, Trade, TradeType } from '@uniswap/sdk'
import { useMemo } from 'react'
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, ROUTER_ADDRESS } from '../constants'
import { useTokenAllowance } from '../data/Allowances'
import { BIPS_BASE, DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../constants'
import { getTradeVersion, useV1TradeExchangeAddress } from '../data/V1'
import { Field } from '../state/swap/actions'
import { useTransactionAdder } from '../state/transactions/hooks'
import { calculateGasMargin, getRouterContract, isAddress } from '../utils'
import { computeSlippageAdjustedAmounts } from '../utils/prices'
import { calculateGasMargin, getRouterContract, isAddress, shortenAddress } from '../utils'
import v1SwapArguments from '../utils/v1SwapArguments'
import { useActiveWeb3React } from './index'
import { useV1ExchangeContract } from './useContract'
import useENSName from './useENSName'
import useENS from './useENS'
import { Version } from './useToggledVersion'
enum SwapType {
EXACT_TOKENS_FOR_TOKENS,
EXACT_TOKENS_FOR_ETH,
EXACT_ETH_FOR_TOKENS,
TOKENS_FOR_EXACT_TOKENS,
TOKENS_FOR_EXACT_ETH,
ETH_FOR_EXACT_TOKENS,
V1_EXACT_ETH_FOR_TOKENS,
V1_EXACT_TOKENS_FOR_ETH,
V1_EXACT_TOKENS_FOR_TOKENS,
V1_ETH_FOR_EXACT_TOKENS,
V1_TOKENS_FOR_EXACT_ETH,
V1_TOKENS_FOR_EXACT_TOKENS
}
function getSwapType(trade: Trade | undefined): SwapType | undefined {
if (!trade) return undefined
const chainId = trade.inputAmount.token.chainId
const inputWETH = trade.inputAmount.token.equals(WETH[chainId])
const outputWETH = trade.outputAmount.token.equals(WETH[chainId])
const isExactIn = trade.tradeType === TradeType.EXACT_INPUT
const isV1 = getTradeVersion(trade) === Version.v1
if (isExactIn) {
if (inputWETH) {
return isV1 ? SwapType.V1_EXACT_ETH_FOR_TOKENS : SwapType.EXACT_ETH_FOR_TOKENS
} else if (outputWETH) {
return isV1 ? SwapType.V1_EXACT_TOKENS_FOR_ETH : SwapType.EXACT_TOKENS_FOR_ETH
} else {
return isV1 ? SwapType.V1_EXACT_TOKENS_FOR_TOKENS : SwapType.EXACT_TOKENS_FOR_TOKENS
}
} else {
if (inputWETH) {
return isV1 ? SwapType.V1_ETH_FOR_EXACT_TOKENS : SwapType.ETH_FOR_EXACT_TOKENS
} else if (outputWETH) {
return isV1 ? SwapType.V1_TOKENS_FOR_EXACT_ETH : SwapType.TOKENS_FOR_EXACT_ETH
} else {
return isV1 ? SwapType.V1_TOKENS_FOR_EXACT_TOKENS : SwapType.TOKENS_FOR_EXACT_TOKENS
}
}
}
// returns a function that will execute a swap, if the parameters are all valid
// and the user has approved the slippage adjusted input amount for the trade
export function useSwapCallback(
trade?: Trade, // trade to execute, required
trade: Trade | undefined, // trade to execute, required
allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // in bips
deadline: number = DEFAULT_DEADLINE_FROM_NOW, // in seconds from now
to?: string // recipient of output, optional
recipientAddressOrName: string | null // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender
): null | (() => Promise<string>) {
const { account, chainId, library } = useActiveWeb3React()
const addTransaction = useTransactionAdder()
const recipient = to ? isAddress(to) : account
const ensName = useENSName(to)
const { address: recipientAddress } = useENS(recipientAddressOrName)
const recipient = recipientAddressOrName === null ? account : recipientAddress
const tradeVersion = getTradeVersion(trade)
const v1Exchange = useV1ExchangeContract(useV1TradeExchangeAddress(trade), true)
const inputAllowance = useTokenAllowance(
trade?.inputAmount?.token,
account ?? undefined,
tradeVersion === Version.v1 ? v1Exchange?.address : ROUTER_ADDRESS
)
return useMemo(() => {
if (!trade || !recipient || !tradeVersion) return null
// will always be defined
const {
[Field.INPUT]: slippageAdjustedInput,
[Field.OUTPUT]: slippageAdjustedOutput
} = computeSlippageAdjustedAmounts(trade, allowedSlippage)
if (!slippageAdjustedInput || !slippageAdjustedOutput) return null
// no allowance
if (
!trade.inputAmount.token.equals(WETH[chainId as ChainId]) &&
(!inputAllowance || slippageAdjustedInput.greaterThan(inputAllowance))
) {
return null
}
if (!trade || !recipient || !library || !account || !tradeVersion || !chainId) return null
return async function onSwap() {
if (!chainId || !library || !account) {
throw new Error('missing dependencies in onSwap callback')
}
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 swapMethods = []
const deadlineFromNow: number = Math.ceil(Date.now() / 1000) + deadline
switch (tradeVersion) {
case Version.v2:
swapMethods.push(
Router.swapCallParameters(trade, {
feeOnTransfer: false,
allowedSlippage: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE),
recipient,
ttl: deadline
})
)
const swapType = getSwapType(trade)
// 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:
methodNames = ['swapExactTokensForTokens', 'swapExactTokensForTokensSupportingFeeOnTransferTokens']
args = [
slippageAdjustedInput.raw.toString(),
slippageAdjustedOutput.raw.toString(),
path,
recipient,
deadlineFromNow
]
if (trade.tradeType === TradeType.EXACT_INPUT) {
swapMethods.push(
Router.swapCallParameters(trade, {
feeOnTransfer: true,
allowedSlippage: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE),
recipient,
ttl: deadline
})
)
}
break
case SwapType.TOKENS_FOR_EXACT_TOKENS:
methodNames = ['swapTokensForExactTokens']
args = [
slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(),
path,
recipient,
deadlineFromNow
]
case Version.v1:
swapMethods.push(
v1SwapArguments(trade, {
allowedSlippage: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE),
recipient,
ttl: deadline
})
)
break
case SwapType.EXACT_ETH_FOR_TOKENS:
methodNames = ['swapExactETHForTokens', 'swapExactETHForTokensSupportingFeeOnTransferTokens']
args = [slippageAdjustedOutput.raw.toString(), path, recipient, deadlineFromNow]
value = BigNumber.from(slippageAdjustedInput.raw.toString())
break
case SwapType.TOKENS_FOR_EXACT_ETH:
methodNames = ['swapTokensForExactETH']
args = [
slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(),
path,
recipient,
deadlineFromNow
]
break
case SwapType.EXACT_TOKENS_FOR_ETH:
methodNames = ['swapExactTokensForETH', 'swapExactTokensForETHSupportingFeeOnTransferTokens']
args = [
slippageAdjustedInput.raw.toString(),
slippageAdjustedOutput.raw.toString(),
path,
recipient,
deadlineFromNow
]
break
case SwapType.ETH_FOR_EXACT_TOKENS:
methodNames = ['swapETHForExactTokens']
args = [slippageAdjustedOutput.raw.toString(), path, recipient, deadlineFromNow]
value = BigNumber.from(slippageAdjustedInput.raw.toString())
break
case SwapType.V1_EXACT_ETH_FOR_TOKENS:
methodNames = ['ethToTokenTransferInput']
args = [slippageAdjustedOutput.raw.toString(), deadlineFromNow, recipient]
value = BigNumber.from(slippageAdjustedInput.raw.toString())
break
case SwapType.V1_EXACT_TOKENS_FOR_TOKENS:
methodNames = ['tokenToTokenTransferInput']
args = [
slippageAdjustedInput.raw.toString(),
slippageAdjustedOutput.raw.toString(),
1,
deadlineFromNow,
recipient,
trade.outputAmount.token.address
]
break
case SwapType.V1_EXACT_TOKENS_FOR_ETH:
methodNames = ['tokenToEthTransferOutput']
args = [
slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(),
deadlineFromNow,
recipient
]
break
case SwapType.V1_ETH_FOR_EXACT_TOKENS:
methodNames = ['ethToTokenTransferOutput']
args = [slippageAdjustedOutput.raw.toString(), deadlineFromNow, recipient]
value = BigNumber.from(slippageAdjustedInput.raw.toString())
break
case SwapType.V1_TOKENS_FOR_EXACT_ETH:
methodNames = ['tokenToEthTransferOutput']
args = [
slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(),
deadlineFromNow,
recipient
]
break
case SwapType.V1_TOKENS_FOR_EXACT_TOKENS:
methodNames = ['tokenToTokenTransferOutput']
args = [
slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(),
MaxUint256.toString(),
deadlineFromNow,
recipient,
trade.outputAmount.token.address
]
break
default:
throw new Error(`Unhandled swap type: ${swapType}`)
}
const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all(
methodNames.map(methodName =>
swapMethods.map(({ args, methodName, value }) =>
contract.estimateGas[methodName](...args, value ? { value } : {})
.then(calculateGasMargin)
.catch(error => {
@@ -253,7 +106,7 @@ export function useSwapCallback(
// 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) {
if (swapMethods.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.`
)
@@ -261,7 +114,7 @@ export function useSwapCallback(
// 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) {
else if (swapMethods.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.`
)
@@ -269,7 +122,7 @@ export function useSwapCallback(
throw Error('This transaction would fail. Please contact support.')
}
} else {
const methodName = methodNames[indexOfSuccessfulEstimation]
const { methodName, args, value } = swapMethods[indexOfSuccessfulEstimation]
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
return contract[methodName](...args, {
@@ -277,16 +130,23 @@ export function useSwapCallback(
...(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 inputSymbol = trade.inputAmount.currency.symbol
const outputSymbol = trade.outputAmount.currency.symbol
const inputAmount = trade.inputAmount.toSignificant(3)
const outputAmount = trade.outputAmount.toSignificant(3)
const base = `Swap ${inputAmount} ${inputSymbol} for ${outputAmount} ${outputSymbol}`
const withRecipient = recipient === account ? base : `${base} to ${ensName ?? recipient}`
const withRecipient =
recipient === account
? base
: `${base} to ${
recipientAddressOrName && isAddress(recipientAddressOrName)
? shortenAddress(recipientAddressOrName)
: recipientAddressOrName
}`
const withVersion =
tradeVersion === Version.v2 ? withRecipient : `${withRecipient} on ${tradeVersion.toUpperCase()}`
tradeVersion === Version.v2 ? withRecipient : `${withRecipient} on ${(tradeVersion as any).toUpperCase()}`
addTransaction(response, {
summary: withVersion
@@ -310,15 +170,14 @@ export function useSwapCallback(
}, [
trade,
recipient,
tradeVersion,
allowedSlippage,
chainId,
inputAllowance,
library,
account,
tradeVersion,
chainId,
allowedSlippage,
v1Exchange,
deadline,
addTransaction,
ensName
recipientAddressOrName,
addTransaction
])
}

View File

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

View File

@@ -3,15 +3,13 @@ import { initReactI18next } from 'react-i18next'
import XHR from 'i18next-xhr-backend'
import 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'
@@ -33,6 +34,13 @@ if (typeof GOOGLE_ANALYTICS_ID === 'string') {
ReactGA.initialize('test', { testMode: true, debug: true })
}
window.addEventListener('error', error => {
ReactGA.exception({
description: `${error.message} @ ${error.filename}:${error.lineno}:${error.colno}`,
fatal: true
})
})
function Updaters() {
return (
<>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
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 Header from '../components/Header'
@@ -7,14 +7,18 @@ import Popups from '../components/Popups'
import Web3ReactManager from '../components/Web3ReactManager'
import DarkModeQueryParamReader from '../theme/DarkModeQueryParamReader'
import AddLiquidity from './AddLiquidity'
import CreatePool from './CreatePool'
import {
RedirectDuplicateTokenIds,
RedirectOldAddLiquidityPathStructure,
RedirectToAddLiquidity
} from './AddLiquidity/redirects'
import MigrateV1 from './MigrateV1'
import MigrateV1Exchange from './MigrateV1/MigrateV1Exchange'
import RemoveV1Exchange from './MigrateV1/RemoveV1Exchange'
import Pool from './Pool'
import PoolFinder from './PoolFinder'
import RemoveLiquidity from './RemoveLiquidity'
import Send from './Send'
import { RedirectOldRemoveLiquidityPathStructure } from './RemoveLiquidity/redirects'
import Swap from './Swap'
import { RedirectPathToSwapOnly, RedirectToSwap } from './Swap/redirects'
@@ -53,17 +57,10 @@ 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>
@@ -76,12 +73,15 @@ export default function App() {
<Switch>
<Route exact strict path="/swap" component={Swap} />
<Route exact strict path="/swap/:outputCurrency" component={RedirectToSwap} />
<Route exact strict path="/send" component={Send} />
<Route exact strict path="/send" component={RedirectPathToSwapOnly} />
<Route exact strict path="/find" component={PoolFinder} />
<Route exact strict path="/pool" component={Pool} />
<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="/create" component={RedirectToAddLiquidity} />
<Route exact path="/add" component={AddLiquidity} />
<Route exact path="/add/:currencyIdA" component={RedirectOldAddLiquidityPathStructure} />
<Route exact path="/add/:currencyIdA/:currencyIdB" component={RedirectDuplicateTokenIds} />
<Route exact strict path="/remove/:tokens" component={RedirectOldRemoveLiquidityPathStructure} />
<Route exact strict path="/remove/:currencyIdA/:currencyIdB" 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} />
@@ -91,7 +91,7 @@ export default function App() {
<Marginer />
</BodyWrapper>
</AppWrapper>
</Router>
</HashRouter>
</Suspense>
)
}

View File

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

View File

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

View File

@@ -1,18 +1,20 @@
import { TransactionResponse } from '@ethersproject/abstract-provider'
import { ChainId, Fraction, JSBI, Percent, Token, TokenAmount, WETH } from '@uniswap/sdk'
import { AddressZero } from '@ethersproject/constants'
import { Currency, 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 { Text } from 'rebass'
import { ButtonConfirmed } from '../../components/Button'
import { PinkCard, YellowCard, LightCard } from '../../components/Card'
import { LightCard, PinkCard, YellowCard } from '../../components/Card'
import { AutoColumn } from '../../components/Column'
import CurrencyLogo from '../../components/CurrencyLogo'
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 { PairState, usePair } from '../../data/Reserves'
import { useTotalSupply } from '../../data/TotalSupply'
import { useActiveWeb3React } from '../../hooks'
import { useToken } from '../../hooks/Tokens'
@@ -21,13 +23,10 @@ import { useV1ExchangeContract, useV2MigratorContract } from '../../hooks/useCon
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 { BackArrow, ExternalLink, TYPE } from '../../theme'
import { getEtherscanLink, isAddress } 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))
@@ -64,7 +63,7 @@ export function V1LiquidityInfo({
return (
<>
<AutoRow style={{ justifyContent: 'flex-start', width: 'fit-content' }}>
<TokenLogo size="24px" address={token.address} />
<CurrencyLogo size="24px" currency={token} />
<div style={{ marginLeft: '.75rem' }}>
<TYPE.mediumHeader>
{<FormattedPoolTokenAmount tokenAmount={liquidityTokenAmount} />}{' '}
@@ -81,7 +80,7 @@ export function V1LiquidityInfo({
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
{tokenWorth.toSignificant(4)}
</Text>
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token.address} />
<CurrencyLogo size="20px" style={{ marginLeft: '8px' }} currency={token} />
</RowFixed>
</RowBetween>
<RowBetween mb="1rem">
@@ -92,7 +91,7 @@ export function V1LiquidityInfo({
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
{ethWorth.toSignificant(4)}
</Text>
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={WETH[chainId].address} />
<CurrencyLogo size="20px" style={{ marginLeft: '8px' }} currency={Currency.ETHER} />
</RowFixed>
</RowBetween>
</>
@@ -105,10 +104,10 @@ function V1PairMigration({ liquidityTokenAmount, token }: { liquidityTokenAmount
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 [v2PairState, v2Pair] = usePair(chainId ? WETH[chainId] : undefined, token)
const isFirstLiquidityProvider: boolean = v2PairState === PairState.NOT_EXISTS
const v2SpotPrice = v2Pair?.reserveOf(token)?.divide(v2Pair?.reserveOf(WETH[chainId as ChainId]))
const v2SpotPrice = v2Pair?.reserveOf(token)?.divide(v2Pair?.reserveOf(WETH[chainId]))
const [confirmingMigration, setConfirmingMigration] = useState<boolean>(false)
const [pendingMigrationHash, setPendingMigrationHash] = useState<string | null>(null)
@@ -127,7 +126,7 @@ function V1PairMigration({ liquidityTokenAmount, token }: { liquidityTokenAmount
const v1SpotPrice =
exchangeTokenBalance && exchangeETHBalance
? exchangeTokenBalance.divide(new Fraction(exchangeETHBalance, WEI_DENOM))
? exchangeTokenBalance.divide(new Fraction(exchangeETHBalance.raw, WEI_DENOM))
: null
const priceDifferenceFraction: Fraction | undefined =
@@ -344,10 +343,6 @@ export default function MigrateV1Exchange({
)
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)
@@ -358,9 +353,7 @@ export default function MigrateV1Exchange({
<BodyWrapper style={{ padding: 24 }}>
<AutoColumn gap="16px">
<AutoRow style={{ alignItems: 'center', justifyContent: 'space-between' }} gap="8px">
<div style={{ cursor: 'pointer' }}>
<ArrowLeft onClick={handleBack} />
</div>
<BackArrow to="/migrate/v1" />
<TYPE.mediumHeader>Migrate V1 Liquidity</TYPE.mediumHeader>
<div>
<QuestionHelper text="Migrate your liquidity tokens from Uniswap V1 to Uniswap V2." />

View File

@@ -1,7 +1,6 @@
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'
@@ -16,7 +15,7 @@ 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 { BackArrow, TYPE } from '../../theme'
import { isAddress } from '../../utils'
import { BodyWrapper } from '../AppBody'
import { EmptyState } from './EmptyState'
@@ -128,7 +127,6 @@ function V1PairRemoval({
}
export default function RemoveV1Exchange({
history,
match: {
params: { address }
}
@@ -149,10 +147,6 @@ export default function RemoveV1Exchange({
)
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)
@@ -163,9 +157,7 @@ export default function RemoveV1Exchange({
<BodyWrapper style={{ padding: 24 }}>
<AutoColumn gap="16px">
<AutoRow style={{ alignItems: 'center', justifyContent: 'space-between' }} gap="8px">
<div style={{ cursor: 'pointer' }}>
<ArrowLeft onClick={handleBack} />
</div>
<BackArrow to="/migrate/v1" />
<TYPE.mediumHeader>Remove V1 Liquidity</TYPE.mediumHeader>
<div>
<QuestionHelper text="Remove your Uniswap V1 liquidity tokens." />

View File

@@ -1,7 +1,5 @@
import { JSBI, Token } from '@uniswap/sdk'
import React, { useCallback, useContext, useMemo, useState, useEffect } from 'react'
import { ArrowLeft } from 'react-feather'
import { RouteComponentProps } from 'react-router'
import { ThemeContext } from 'styled-components'
import { AutoColumn } from '../../components/Column'
import { AutoRow } from '../../components/Row'
@@ -10,7 +8,7 @@ import { useAllTokenV1Exchanges } from '../../data/V1'
import { useActiveWeb3React } from '../../hooks'
import { useToken, useAllTokens } from '../../hooks/Tokens'
import { useTokenBalancesWithLoadingIndicator } from '../../state/wallet/hooks'
import { TYPE } from '../../theme'
import { BackArrow, TYPE } from '../../theme'
import { LightCard } from '../../components/Card'
import { BodyWrapper } from '../AppBody'
import { EmptyState } from './EmptyState'
@@ -20,7 +18,7 @@ import { Dots } from '../../components/swap/styleds'
import { useAddUserToken } from '../../state/user/hooks'
import { isDefaultToken, isCustomAddedToken } from '../../utils'
export default function MigrateV1({ history }: RouteComponentProps) {
export default function MigrateV1() {
const theme = useContext(ThemeContext)
const { account, chainId } = useActiveWeb3React()
@@ -68,17 +66,11 @@ export default function MigrateV1({ history }: RouteComponentProps) {
// should never always be false, because a V1 exhchange exists for WETH on all testnets
const isLoading = Object.keys(V1Exchanges)?.length === 0 || V1LiquidityBalancesLoading
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>
<BackArrow to="/pool" />
<TYPE.mediumHeader>Migrate V1 Liquidity</TYPE.mediumHeader>
<div>
<QuestionHelper text="Migrate your liquidity tokens from Uniswap V1 to Uniswap V2." />

View File

@@ -1,11 +1,11 @@
import React, { useState, useContext, useCallback } from 'react'
import styled, { ThemeContext } from 'styled-components'
import { JSBI } from '@uniswap/sdk'
import { RouteComponentProps } from 'react-router-dom'
import React, { useContext, useMemo } from 'react'
import { ThemeContext } from 'styled-components'
import { Pair } from '@uniswap/sdk'
import { Link } from 'react-router-dom'
import { SwapPoolTabs } from '../../components/NavigationTabs'
import Question from '../../components/QuestionHelper'
import PairSearchModal from '../../components/SearchModal/PairSearchModal'
import PositionCard from '../../components/PositionCard'
import FullPositionCard from '../../components/PositionCard'
import { useUserHasLiquidityInAllTokens } from '../../data/V1'
import { useTokenBalancesWithLoadingIndicator } from '../../state/wallet/hooks'
import { StyledInternalLink, TYPE } from '../../theme'
@@ -13,78 +13,61 @@ import { Text } from 'rebass'
import { LightCard } from '../../components/Card'
import { RowBetween } from '../../components/Row'
import { ButtonPrimary, ButtonSecondary } from '../../components/Button'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import { AutoColumn } from '../../components/Column'
import { useActiveWeb3React } from '../../hooks'
import { usePairs } from '../../data/Reserves'
import { useAllDummyPairs } from '../../state/user/hooks'
import { toV2LiquidityToken, useTrackedTokenPairs } from '../../state/user/hooks'
import AppBody from '../AppBody'
import { Dots } from '../../components/swap/styleds'
const Positions = styled.div`
position: relative;
width: 100%;
`
const FixedBottom = styled.div`
position: absolute;
bottom: -80px;
width: 100%;
`
export default function Pool({ history }: RouteComponentProps) {
export default function Pool() {
const theme = useContext(ThemeContext)
const { account } = useActiveWeb3React()
const [showPoolSearch, setShowPoolSearch] = useState(false)
// fetch the user's balances of all tracked V2 LP tokens
const V2DummyPairs = useAllDummyPairs()
const [V2PairsBalances, fetchingV2PairBalances] = useTokenBalancesWithLoadingIndicator(
account,
V2DummyPairs?.map(p => p.liquidityToken)
const trackedTokenPairs = useTrackedTokenPairs()
const tokenPairsWithLiquidityTokens = useMemo(
() => trackedTokenPairs.map(tokens => ({ liquidityToken: toV2LiquidityToken(tokens), tokens })),
[trackedTokenPairs]
)
// 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 liquidityTokens = useMemo(() => tokenPairsWithLiquidityTokens.map(tpwlt => tpwlt.liquidityToken), [
tokenPairsWithLiquidityTokens
])
const [v2PairsBalances, fetchingV2PairBalances] = useTokenBalancesWithLoadingIndicator(
account ?? undefined,
liquidityTokens
)
const V2Pairs = usePairs(
V2DummyPairsWithABalance.map(V2DummyPairWithABalance => [
V2DummyPairWithABalance.token0,
V2DummyPairWithABalance.token1
])
)
const V2IsLoading =
fetchingV2PairBalances || V2Pairs?.length < V2DummyPairsWithABalance.length || V2Pairs?.some(V2Pair => !!!V2Pair)
const allV2PairsWithLiquidity = V2Pairs.filter(V2Pair => !!V2Pair).map(V2Pair => (
<PositionCard key={V2Pair.liquidityToken.address} pair={V2Pair} />
))
// fetch the reserves for all V2 pools in which the user has a balance
const liquidityTokensWithBalances = useMemo(
() =>
tokenPairsWithLiquidityTokens.filter(({ liquidityToken }) =>
v2PairsBalances[liquidityToken.address]?.greaterThan('0')
),
[tokenPairsWithLiquidityTokens, v2PairsBalances]
)
const v2Pairs = usePairs(liquidityTokensWithBalances.map(({ tokens }) => tokens))
const v2IsLoading =
fetchingV2PairBalances || v2Pairs?.length < liquidityTokensWithBalances.length || v2Pairs?.some(V2Pair => !V2Pair)
const allV2PairsWithLiquidity = v2Pairs.map(([, pair]) => pair).filter((v2Pair): v2Pair is Pair => Boolean(v2Pair))
const hasV1Liquidity = useUserHasLiquidityInAllTokens()
const handleSearchDismiss = useCallback(() => {
setShowPoolSearch(false)
}, [setShowPoolSearch])
return (
<AppBody>
<AutoColumn gap="lg" justify="center">
<ButtonPrimary
id="join-pool-button"
padding="16px"
onClick={() => {
setShowPoolSearch(true)
}}
>
<Text fontWeight={500} fontSize={20}>
Join {allV2PairsWithLiquidity?.length > 0 ? 'another' : 'a'} pool
</Text>
</ButtonPrimary>
<>
<AppBody>
<SwapPoolTabs active={'pool'} />
<AutoColumn gap="lg" justify="center">
<ButtonPrimary id="join-pool-button" as={Link} style={{ padding: 16 }} to="/add/ETH">
<Text fontWeight={500} fontSize={20}>
Add Liquidity
</Text>
</ButtonPrimary>
<Positions>
<AutoColumn gap="12px">
<AutoColumn gap="12px" style={{ width: '100%' }}>
<RowBetween padding={'0 8px'}>
<Text color={theme.text1} fontWeight={500}>
Your Liquidity
@@ -98,14 +81,18 @@ export default function Pool({ history }: RouteComponentProps) {
Connect to a wallet to view your liquidity.
</TYPE.body>
</LightCard>
) : V2IsLoading ? (
) : v2IsLoading ? (
<LightCard padding="40px">
<TYPE.body color={theme.text3} textAlign="center">
<Dots>Loading</Dots>
</TYPE.body>
</LightCard>
) : allV2PairsWithLiquidity?.length > 0 ? (
<>{allV2PairsWithLiquidity}</>
<>
{allV2PairsWithLiquidity.map(v2Pair => (
<FullPositionCard key={v2Pair.liquidityToken.address} pair={v2Pair} />
))}
</>
) : (
<LightCard padding="40px">
<TYPE.body color={theme.text3} textAlign="center">
@@ -123,16 +110,14 @@ export default function Pool({ history }: RouteComponentProps) {
</Text>
</div>
</AutoColumn>
<FixedBottom>
<ColumnCenter>
<ButtonSecondary width="136px" padding="8px" borderRadius="10px" onClick={() => history.push('/create')}>
+ Create Pool
</ButtonSecondary>
</ColumnCenter>
</FixedBottom>
</Positions>
<PairSearchModal isOpen={showPoolSearch} onDismiss={handleSearchDismiss} />
</AutoColumn>
</AppBody>
</AutoColumn>
</AppBody>
<div style={{ display: 'flex', alignItems: 'center', marginTop: '1.5rem' }}>
<ButtonSecondary as={Link} style={{ width: 'initial' }} to="/migrate/v1">
Migrate V1 Liquidity
</ButtonSecondary>
</div>
</>
)
}

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../tsconfig.strict.json",
"include": ["**/*"]
}

View File

@@ -1,21 +1,23 @@
import { JSBI, Pair, Token, TokenAmount, WETH } from '@uniswap/sdk'
import { Currency, ETHER, JSBI, TokenAmount } from '@uniswap/sdk'
import React, { useCallback, useEffect, useState } from 'react'
import { Plus } from 'react-feather'
import { Text } from 'rebass'
import { ButtonDropdownLight } from '../../components/Button'
import { LightCard } from '../../components/Card'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import PositionCard from '../../components/PositionCard'
import CurrencyLogo from '../../components/CurrencyLogo'
import { FindPoolTabs } from '../../components/NavigationTabs'
import { MinimalPositionCard } from '../../components/PositionCard'
import Row from '../../components/Row'
import TokenSearchModal from '../../components/SearchModal/TokenSearchModal'
import TokenLogo from '../../components/TokenLogo'
import { usePair } from '../../data/Reserves'
import CurrencySearchModal from '../../components/SearchModal/CurrencySearchModal'
import { PairState, usePair } from '../../data/Reserves'
import { useActiveWeb3React } from '../../hooks'
import { useToken } from '../../hooks/Tokens'
import { usePairAdder } from '../../state/user/hooks'
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import { useTokenBalance } from '../../state/wallet/hooks'
import { StyledInternalLink } from '../../theme'
import { currencyId } from '../../utils/currencyId'
import AppBody from '../AppBody'
import { Dots } from '../Pool/styleds'
enum Fields {
TOKEN0 = 0,
@@ -23,17 +25,15 @@ enum Fields {
}
export default function PoolFinder() {
const { account, chainId } = useActiveWeb3React()
const { account } = useActiveWeb3React()
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 [currency0, setCurrency0] = useState<Currency | null>(ETHER)
const [currency1, setCurrency1] = useState<Currency | null>(null)
const pair: Pair = usePair(token0, token1)
const [pairState, pair] = usePair(currency0 ?? undefined, currency1 ?? undefined)
const addPair = usePairAdder()
useEffect(() => {
if (pair) {
@@ -41,16 +41,25 @@ export default function PoolFinder() {
}
}, [pair, addPair])
const newPair: boolean =
pair === null ||
(!!pair && JSBI.equal(pair.reserve0.raw, JSBI.BigInt(0)) && JSBI.equal(pair.reserve1.raw, JSBI.BigInt(0)))
const validPairNoLiquidity: boolean =
pairState === PairState.NOT_EXISTS ||
Boolean(
pairState === PairState.EXISTS &&
pair &&
JSBI.equal(pair.reserve0.raw, JSBI.BigInt(0)) &&
JSBI.equal(pair.reserve1.raw, JSBI.BigInt(0))
)
const position: TokenAmount = useTokenBalanceTreatingWETHasETH(account, pair?.liquidityToken)
const poolImported: boolean = !!position && JSBI.greaterThan(position.raw, JSBI.BigInt(0))
const position: TokenAmount | undefined = useTokenBalance(account ?? undefined, pair?.liquidityToken)
const hasPosition = Boolean(position && JSBI.greaterThan(position.raw, JSBI.BigInt(0)))
const handleTokenSelect = useCallback(
(address: string) => {
activeField === Fields.TOKEN0 ? setToken0Address(address) : setToken1Address(address)
const handleCurrencySelect = useCallback(
(currency: Currency) => {
if (activeField === Fields.TOKEN0) {
setCurrency0(currency)
} else {
setCurrency1(currency)
}
},
[activeField]
)
@@ -59,8 +68,17 @@ export default function PoolFinder() {
setShowSearch(false)
}, [setShowSearch])
const prerequisiteMessage = (
<LightCard padding="45px 10px">
<Text textAlign="center">
{!account ? 'Connect to a wallet to find pools' : 'Select a token to find your liquidity.'}
</Text>
</LightCard>
)
return (
<AppBody>
<FindPoolTabs />
<AutoColumn gap="md">
<ButtonDropdownLight
onClick={() => {
@@ -68,11 +86,11 @@ export default function PoolFinder() {
setActiveField(Fields.TOKEN0)
}}
>
{token0 ? (
{currency0 ? (
<Row>
<TokenLogo address={token0Address} />
<CurrencyLogo currency={currency0} />
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
{token0.symbol}
{currency0.symbol}
</Text>
</Row>
) : (
@@ -92,11 +110,11 @@ export default function PoolFinder() {
setActiveField(Fields.TOKEN1)
}}
>
{token1 ? (
{currency1 ? (
<Row>
<TokenLogo address={token1Address} />
<CurrencyLogo currency={currency1} />
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
{token1.symbol}
{currency1.symbol}
</Text>
</Row>
) : (
@@ -106,50 +124,68 @@ export default function PoolFinder() {
)}
</ButtonDropdownLight>
{poolImported && (
{hasPosition && (
<ColumnCenter
style={{ justifyItems: 'center', backgroundColor: '', padding: '12px 0px', borderRadius: '12px' }}
>
<Text textAlign="center" fontWeight={500} color="">
<Text textAlign="center" fontWeight={500}>
Pool Found!
</Text>
</ColumnCenter>
)}
{position ? (
poolImported ? (
<PositionCard pair={pair} minimal={true} border="1px solid #CED0D9" />
) : (
{currency0 && currency1 ? (
pairState === PairState.EXISTS ? (
hasPosition && pair ? (
<MinimalPositionCard pair={pair} border="1px solid #CED0D9" />
) : (
<LightCard padding="45px 10px">
<AutoColumn gap="sm" justify="center">
<Text textAlign="center">You dont have liquidity in this pool yet.</Text>
<StyledInternalLink to={`/add/${currencyId(currency0)}/${currencyId(currency1)}`}>
<Text textAlign="center">Add liquidity.</Text>
</StyledInternalLink>
</AutoColumn>
</LightCard>
)
) : validPairNoLiquidity ? (
<LightCard padding="45px 10px">
<AutoColumn gap="sm" justify="center">
<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>
<Text textAlign="center">No pool found.</Text>
<StyledInternalLink to={`/add/${currencyId(currency0)}/${currencyId(currency1)}`}>
Create pool.
</StyledInternalLink>
</AutoColumn>
</LightCard>
)
) : newPair ? (
<LightCard padding="45px 10px">
<AutoColumn gap="sm" justify="center">
<Text textAlign="center">No pool found.</Text>
<StyledInternalLink to={`/add/${token0Address}-${token1Address}`}>Create pool?</StyledInternalLink>
</AutoColumn>
</LightCard>
) : pairState === PairState.INVALID ? (
<LightCard padding="45px 10px">
<AutoColumn gap="sm" justify="center">
<Text textAlign="center" fontWeight={500}>
Invalid pair.
</Text>
</AutoColumn>
</LightCard>
) : pairState === PairState.LOADING ? (
<LightCard padding="45px 10px">
<AutoColumn gap="sm" justify="center">
<Text textAlign="center">
Loading
<Dots />
</Text>
</AutoColumn>
</LightCard>
) : null
) : (
<LightCard padding="45px 10px">
<Text textAlign="center">
{!account ? 'Connect to a wallet to find pools' : 'Select a token to find your liquidity.'}
</Text>
</LightCard>
prerequisiteMessage
)}
</AutoColumn>
<TokenSearchModal
<CurrencySearchModal
isOpen={showSearch}
onTokenSelect={handleTokenSelect}
onCurrencySelect={handleCurrencySelect}
onDismiss={handleSearchDismiss}
hiddenToken={activeField === Fields.TOKEN0 ? token1Address : token0Address}
showCommonBases
hiddenCurrency={(activeField === Fields.TOKEN0 ? currency1 : currency0) ?? undefined}
/>
</AppBody>
)

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../tsconfig.strict.json",
"include": ["**/*"]
}

View File

@@ -1,7 +1,8 @@
import { splitSignature } from '@ethersproject/bytes'
import { Contract } from '@ethersproject/contracts'
import { Percent, WETH } from '@uniswap/sdk'
import React, { useCallback, useContext, useState } from 'react'
import { TransactionResponse } from '@ethersproject/providers'
import { Currency, currencyEquals, ETHER, Percent, WETH } from '@uniswap/sdk'
import React, { useCallback, useContext, useMemo, useState } from 'react'
import { ArrowDown, Plus } from 'react-feather'
import ReactGA from 'react-ga'
import { RouteComponentProps } from 'react-router'
@@ -12,34 +13,48 @@ import { LightCard } from '../../components/Card'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import ConfirmationModal from '../../components/ConfirmationModal'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import DoubleLogo from '../../components/DoubleLogo'
import PositionCard from '../../components/PositionCard'
import DoubleCurrencyLogo from '../../components/DoubleLogo'
import { AddRemoveTabs } from '../../components/NavigationTabs'
import { MinimalPositionCard } from '../../components/PositionCard'
import Row, { RowBetween, RowFixed } from '../../components/Row'
import Slider from '../../components/Slider'
import TokenLogo from '../../components/TokenLogo'
import CurrencyLogo from '../../components/CurrencyLogo'
import { ROUTER_ADDRESS } from '../../constants'
import { useActiveWeb3React } from '../../hooks'
import { useCurrency } from '../../hooks/Tokens'
import { usePairContract } from '../../hooks/useContract'
import { useTransactionAdder } from '../../state/transactions/hooks'
import { TYPE } from '../../theme'
import { StyledInternalLink, TYPE } from '../../theme'
import { calculateGasMargin, calculateSlippageAmount, getRouterContract } from '../../utils'
import { currencyId } from '../../utils/currencyId'
import { wrappedCurrency } from '../../utils/wrappedCurrency'
import AppBody from '../AppBody'
import { ClickableText, MaxButton, Wrapper } from '../Pool/styleds'
import { useApproveCallback, ApprovalState } from '../../hooks/useApproveCallback'
import { Dots } from '../../components/swap/styleds'
import { useDefaultsFromURLMatchParams, useBurnActionHandlers } from '../../state/burn/hooks'
import { useBurnActionHandlers } from '../../state/burn/hooks'
import { useDerivedBurnInfo, useBurnState } from '../../state/burn/hooks'
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 } }: RouteComponentProps<{ tokens: string }>) {
useDefaultsFromURLMatchParams(params)
export default function RemoveLiquidity({
history,
match: {
params: { currencyIdA, currencyIdB }
}
}: RouteComponentProps<{ currencyIdA: string; currencyIdB: string }>) {
const [currencyA, currencyB] = [useCurrency(currencyIdA) ?? undefined, useCurrency(currencyIdB) ?? undefined]
const { account, chainId, library } = useActiveWeb3React()
const [tokenA, tokenB] = useMemo(() => [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)], [
currencyA,
currencyB,
chainId
])
const theme = useContext(ThemeContext)
// toggle wallet when disconnected
@@ -47,7 +62,7 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
// burn state
const { independentField, typedValue } = useBurnState()
const { tokens, pair, route, parsedAmounts, error } = useDerivedBurnInfo()
const { pair, parsedAmounts, error } = useDerivedBurnInfo(currencyA ?? undefined, currencyB ?? undefined)
const { onUserInput: _onUserInput } = useBurnActionHandlers()
const isValid = !error
@@ -69,23 +84,27 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
: parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0),
[Field.LIQUIDITY]:
independentField === Field.LIQUIDITY ? typedValue : parsedAmounts[Field.LIQUIDITY]?.toSignificant(6) ?? '',
[Field.TOKEN_A]:
independentField === Field.TOKEN_A ? typedValue : parsedAmounts[Field.TOKEN_A]?.toSignificant(6) ?? '',
[Field.TOKEN_B]:
independentField === Field.TOKEN_B ? typedValue : parsedAmounts[Field.TOKEN_B]?.toSignificant(6) ?? ''
[Field.CURRENCY_A]:
independentField === Field.CURRENCY_A ? typedValue : parsedAmounts[Field.CURRENCY_A]?.toSignificant(6) ?? '',
[Field.CURRENCY_B]:
independentField === Field.CURRENCY_B ? typedValue : parsedAmounts[Field.CURRENCY_B]?.toSignificant(6) ?? ''
}
const atMaxAmount = parsedAmounts[Field.LIQUIDITY_PERCENT]?.equalTo(new Percent('1'))
// pair contract
const pairContract: Contract = usePairContract(pair?.liquidityToken?.address)
const pairContract: Contract | null = usePairContract(pair?.liquidityToken?.address)
// allowance handling
const [signatureData, setSignatureData] = useState<{ v: number; r: string; s: string; deadline: number }>(null)
const [signatureData, setSignatureData] = useState<{ v: number; r: string; s: string; deadline: number } | null>(null)
const [approval, approveCallback] = useApproveCallback(parsedAmounts[Field.LIQUIDITY], ROUTER_ADDRESS)
async function onAttemptToApprove() {
if (!pairContract || !pair || !library) throw new Error('missing dependencies')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
// try to gather a signature for permission
const nonce = await pairContract.nonces(account)
const deadlineForSignature: number = Math.ceil(Date.now() / 1000) + deadline
const EIP712Domain = [
@@ -110,7 +129,7 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
const message = {
owner: account,
spender: ROUTER_ADDRESS,
value: parsedAmounts[Field.LIQUIDITY].raw.toString(),
value: liquidityAmount.raw.toString(),
nonce: nonce.toHexString(),
deadline: deadlineForSignature
}
@@ -152,32 +171,52 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
[_onUserInput]
)
const onLiquidityInput = useCallback((typedValue: string): void => onUserInput(Field.LIQUIDITY, typedValue), [
onUserInput
])
const onCurrencyAInput = useCallback((typedValue: string): void => onUserInput(Field.CURRENCY_A, typedValue), [
onUserInput
])
const onCurrencyBInput = useCallback((typedValue: string): void => onUserInput(Field.CURRENCY_B, typedValue), [
onUserInput
])
// tx sending
const addTransaction = useTransactionAdder()
async function onRemove() {
if (!chainId || !library || !account) throw new Error('missing dependencies')
const { [Field.CURRENCY_A]: currencyAmountA, [Field.CURRENCY_B]: currencyAmountB } = parsedAmounts
if (!currencyAmountA || !currencyAmountB) {
throw new Error('missing currency amounts')
}
const router = getRouterContract(chainId, library, account)
const amountsMin = {
[Field.TOKEN_A]: calculateSlippageAmount(parsedAmounts[Field.TOKEN_A], allowedSlippage)[0],
[Field.TOKEN_B]: calculateSlippageAmount(parsedAmounts[Field.TOKEN_B], allowedSlippage)[0]
[Field.CURRENCY_A]: calculateSlippageAmount(currencyAmountA, allowedSlippage)[0],
[Field.CURRENCY_B]: calculateSlippageAmount(currencyAmountB, allowedSlippage)[0]
}
const tokenBIsETH = tokens[Field.TOKEN_B].equals(WETH[chainId])
const oneTokenIsETH = tokens[Field.TOKEN_A].equals(WETH[chainId]) || tokenBIsETH
if (!currencyA || !currencyB) throw new Error('missing tokens')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
const currencyBIsETH = currencyB === ETHER
const oneCurrencyIsETH = currencyA === ETHER || currencyBIsETH
const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline
if (!tokenA || !tokenB) throw new Error('could not wrap')
let methodNames: string[], args: Array<string | string[] | number | boolean>
// we have approval, use normal remove liquidity
if (approval === ApprovalState.APPROVED) {
// removeLiquidityETH
if (oneTokenIsETH) {
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETH', 'removeLiquidityETHSupportingFeeOnTransferTokens']
args = [
tokens[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].address,
parsedAmounts[Field.LIQUIDITY].raw.toString(),
amountsMin[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].toString(),
amountsMin[tokenBIsETH ? Field.TOKEN_B : Field.TOKEN_A].toString(),
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
deadlineFromNow
]
@@ -186,11 +225,11 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
else {
methodNames = ['removeLiquidity']
args = [
tokens[Field.TOKEN_A].address,
tokens[Field.TOKEN_B].address,
parsedAmounts[Field.LIQUIDITY].raw.toString(),
amountsMin[Field.TOKEN_A].toString(),
amountsMin[Field.TOKEN_B].toString(),
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
deadlineFromNow
]
@@ -199,13 +238,13 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
// we have a signataure, use permit versions of remove liquidity
else if (signatureData !== null) {
// removeLiquidityETHWithPermit
if (oneTokenIsETH) {
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETHWithPermit', 'removeLiquidityETHWithPermitSupportingFeeOnTransferTokens']
args = [
tokens[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].address,
parsedAmounts[Field.LIQUIDITY].raw.toString(),
amountsMin[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].toString(),
amountsMin[tokenBIsETH ? Field.TOKEN_B : Field.TOKEN_A].toString(),
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
signatureData.deadline,
false,
@@ -218,11 +257,11 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
else {
methodNames = ['removeLiquidityWithPermit']
args = [
tokens[Field.TOKEN_A].address,
tokens[Field.TOKEN_B].address,
parsedAmounts[Field.LIQUIDITY].raw.toString(),
amountsMin[Field.TOKEN_A].toString(),
amountsMin[Field.TOKEN_B].toString(),
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
signatureData.deadline,
false,
@@ -232,7 +271,7 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
]
}
} else {
console.error('Attempting to confirm without approval or a signature. Please contact support.')
throw new Error('Attempting to confirm without approval or a signature. Please contact support.')
}
const safeGasEstimates = await Promise.all(
@@ -260,19 +299,19 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
await router[methodName](...args, {
gasLimit: safeGasEstimate
})
.then(response => {
.then((response: TransactionResponse) => {
setAttemptingTxn(false)
addTransaction(response, {
summary:
'Remove ' +
parsedAmounts[Field.TOKEN_A]?.toSignificant(3) +
parsedAmounts[Field.CURRENCY_A]?.toSignificant(3) +
' ' +
tokens[Field.TOKEN_A]?.symbol +
currencyA?.symbol +
' and ' +
parsedAmounts[Field.TOKEN_B]?.toSignificant(3) +
parsedAmounts[Field.CURRENCY_B]?.toSignificant(3) +
' ' +
tokens[Field.TOKEN_B]?.symbol
currencyB?.symbol
})
setTxHash(response.hash)
@@ -280,15 +319,13 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
ReactGA.event({
category: 'Liquidity',
action: 'Remove',
label: [tokens[Field.TOKEN_A]?.symbol, tokens[Field.TOKEN_B]?.symbol].join('/')
label: [currencyA?.symbol, currencyB?.symbol].join('/')
})
})
.catch(error => {
.catch((error: 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)
}
console.error(error)
})
}
}
@@ -298,12 +335,12 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
<AutoColumn gap={'md'} style={{ marginTop: '20px' }}>
<RowBetween align="flex-end">
<Text fontSize={24} fontWeight={500}>
{parsedAmounts[Field.TOKEN_A]?.toSignificant(6)}
{parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)}
</Text>
<RowFixed gap="4px">
<TokenLogo address={tokens[Field.TOKEN_A]?.address} size={'24px'} />
<CurrencyLogo currency={currencyA} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{tokens[Field.TOKEN_A]?.symbol}
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
@@ -312,12 +349,12 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
</RowFixed>
<RowBetween align="flex-end">
<Text fontSize={24} fontWeight={500}>
{parsedAmounts[Field.TOKEN_B]?.toSignificant(6)}
{parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)}
</Text>
<RowFixed gap="4px">
<TokenLogo address={tokens[Field.TOKEN_B]?.address} size={'24px'} />
<CurrencyLogo currency={currencyB} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{tokens[Field.TOKEN_B]?.symbol}
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
@@ -335,34 +372,29 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
<>
<RowBetween>
<Text color={theme.text2} fontWeight={500} fontSize={16}>
{'UNI ' + tokens[Field.TOKEN_A]?.symbol + '/' + tokens[Field.TOKEN_B]?.symbol} Burned
{'UNI ' + currencyA?.symbol + '/' + currencyB?.symbol} Burned
</Text>
<RowFixed>
<DoubleLogo
a0={tokens[Field.TOKEN_A]?.address || ''}
a1={tokens[Field.TOKEN_B]?.address || ''}
margin={true}
/>
<DoubleCurrencyLogo currency0={currencyA} currency1={currencyB} margin={true} />
<Text fontWeight={500} fontSize={16}>
{parsedAmounts[Field.LIQUIDITY]?.toSignificant(6)}
</Text>
</RowFixed>
</RowBetween>
{route && (
{pair && (
<>
<RowBetween>
<Text color={theme.text2} fontWeight={500} fontSize={16}>
Price
</Text>
<Text fontWeight={500} fontSize={16} color={theme.text1}>
1 {tokens[Field.TOKEN_A]?.symbol} = {route.midPrice.toSignificant(6)} {tokens[Field.TOKEN_B]?.symbol}
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</Text>
</RowBetween>
<RowBetween>
<div />
<Text fontWeight={500} fontSize={16} color={theme.text1}>
1 {tokens[Field.TOKEN_B]?.symbol} = {route.midPrice.invert().toSignificant(6)}{' '}
{tokens[Field.TOKEN_A]?.symbol}
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</Text>
</RowBetween>
</>
@@ -376,9 +408,9 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
)
}
const pendingText = `Removing ${parsedAmounts[Field.TOKEN_A]?.toSignificant(6)} ${
tokens[Field.TOKEN_A]?.symbol
} and ${parsedAmounts[Field.TOKEN_B]?.toSignificant(6)} ${tokens[Field.TOKEN_B]?.symbol}`
const pendingText = `Removing ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)} ${
currencyA?.symbol
} and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)} ${currencyB?.symbol}`
const liquidityPercentChangeCallback = useCallback(
(value: number) => {
@@ -387,9 +419,38 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
[onUserInput]
)
const oneCurrencyIsETH = currencyA === ETHER || currencyB === ETHER
const oneCurrencyIsWETH = Boolean(
chainId &&
((currencyA && currencyEquals(WETH[chainId], currencyA)) ||
(currencyB && currencyEquals(WETH[chainId], currencyB)))
)
const handleSelectCurrencyA = useCallback(
(currency: Currency) => {
if (currencyIdB && currencyId(currency) === currencyIdB) {
history.push(`/remove/${currencyId(currency)}/${currencyIdA}`)
} else {
history.push(`/remove/${currencyId(currency)}/${currencyIdB}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleSelectCurrencyB = useCallback(
(currency: Currency) => {
if (currencyIdA && currencyId(currency) === currencyIdA) {
history.push(`/remove/${currencyIdB}/${currencyId(currency)}`)
} else {
history.push(`/remove/${currencyIdA}/${currencyId(currency)}`)
}
},
[currencyIdA, currencyIdB, history]
)
return (
<>
<AppBody>
<AddRemoveTabs adding={false} />
<Wrapper>
<ConfirmationModal
isOpen={showConfirm}
@@ -461,26 +522,47 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
<AutoColumn gap="10px">
<RowBetween>
<Text fontSize={24} fontWeight={500}>
{formattedAmounts[Field.TOKEN_A] || '-'}
{formattedAmounts[Field.CURRENCY_A] || '-'}
</Text>
<RowFixed>
<TokenLogo address={tokens[Field.TOKEN_A]?.address} style={{ marginRight: '12px' }} />
<CurrencyLogo currency={currencyA} style={{ marginRight: '12px' }} />
<Text fontSize={24} fontWeight={500} id="remove-liquidity-tokena-symbol">
{tokens[Field.TOKEN_A]?.symbol}
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowBetween>
<Text fontSize={24} fontWeight={500}>
{formattedAmounts[Field.TOKEN_B] || '-'}
{formattedAmounts[Field.CURRENCY_B] || '-'}
</Text>
<RowFixed>
<TokenLogo address={tokens[Field.TOKEN_B]?.address} style={{ marginRight: '12px' }} />
<CurrencyLogo currency={currencyB} style={{ marginRight: '12px' }} />
<Text fontSize={24} fontWeight={500} id="remove-liquidity-tokenb-symbol">
{tokens[Field.TOKEN_B]?.symbol}
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
{chainId && (oneCurrencyIsWETH || oneCurrencyIsETH) ? (
<RowBetween style={{ justifyContent: 'flex-end' }}>
{oneCurrencyIsETH ? (
<StyledInternalLink
to={`/remove/${currencyA === ETHER ? WETH[chainId].address : currencyIdA}/${
currencyB === ETHER ? WETH[chainId].address : currencyIdB
}`}
>
Receive WETH
</StyledInternalLink>
) : oneCurrencyIsWETH ? (
<StyledInternalLink
to={`/remove/${
currencyA && currencyEquals(currencyA, WETH[chainId]) ? 'ETH' : currencyIdA
}/${currencyB && currencyEquals(currencyB, WETH[chainId]) ? 'ETH' : currencyIdB}`}
>
Receive ETH
</StyledInternalLink>
) : null}
</RowBetween>
) : null}
</AutoColumn>
</LightCard>
</>
@@ -489,16 +571,14 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
{showDetailed && (
<>
<CurrencyInputPanel
field={Field.LIQUIDITY}
value={formattedAmounts[Field.LIQUIDITY]}
onUserInput={onUserInput}
onUserInput={onLiquidityInput}
onMax={() => {
onUserInput(Field.LIQUIDITY_PERCENT, '100')
}}
showMaxButton={!atMaxAmount}
disableTokenSelect
token={pair?.liquidityToken}
isExchange={true}
disableCurrencySelect
currency={pair?.liquidityToken}
pair={pair}
id="liquidity-amount"
/>
@@ -507,14 +587,13 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
</ColumnCenter>
<CurrencyInputPanel
hideBalance={true}
field={Field.TOKEN_A}
value={formattedAmounts[Field.TOKEN_A]}
onUserInput={onUserInput}
value={formattedAmounts[Field.CURRENCY_A]}
onUserInput={onCurrencyAInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
token={tokens[Field.TOKEN_A]}
currency={currencyA}
label={'Output'}
disableTokenSelect
onCurrencySelect={handleSelectCurrencyA}
id="remove-liquidity-tokena"
/>
<ColumnCenter>
@@ -522,32 +601,36 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
</ColumnCenter>
<CurrencyInputPanel
hideBalance={true}
field={Field.TOKEN_B}
value={formattedAmounts[Field.TOKEN_B]}
onUserInput={onUserInput}
value={formattedAmounts[Field.CURRENCY_B]}
onUserInput={onCurrencyBInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
token={tokens[Field.TOKEN_B]}
currency={currencyB}
label={'Output'}
disableTokenSelect
onCurrencySelect={handleSelectCurrencyB}
id="remove-liquidity-tokenb"
/>
</>
)}
{route && (
{pair && (
<div style={{ padding: '10px 20px' }}>
<RowBetween>
Price:
<div>
1 {tokens[Field.TOKEN_A]?.symbol} = {route.midPrice.toSignificant(6)}{' '}
{tokens[Field.TOKEN_B]?.symbol}
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</div>
</RowBetween>
<RowBetween>
<div />
<div>
1 {tokens[Field.TOKEN_B]?.symbol} = {route.midPrice.invert().toSignificant(6)}{' '}
{tokens[Field.TOKEN_A]?.symbol}
1 {currencyB?.symbol} ={' '}
{tokenB
? pair
.priceOf(tokenB)
.invert()
.toSignificant(6)
: '-'}{' '}
{currencyA?.symbol}
</div>
</RowBetween>
</div>
@@ -578,7 +661,7 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
setShowConfirm(true)
}}
disabled={!isValid || (signatureData === null && approval !== ApprovalState.APPROVED)}
error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]}
error={!isValid && !!parsedAmounts[Field.CURRENCY_A] && !!parsedAmounts[Field.CURRENCY_B]}
>
<Text fontSize={16} fontWeight={500}>
{error || 'Remove'}
@@ -593,7 +676,7 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
{pair ? (
<AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
<PositionCard pair={pair} minimal={true} />
<MinimalPositionCard showUnwrapped={oneCurrencyIsWETH} pair={pair} />
</AutoColumn>
) : null}
</>

View File

@@ -0,0 +1,17 @@
import React from 'react'
import { RouteComponentProps, Redirect } from 'react-router-dom'
const OLD_PATH_STRUCTURE = /^(0x[a-fA-F0-9]{40})-(0x[a-fA-F0-9]{40})$/
export function RedirectOldRemoveLiquidityPathStructure({
match: {
params: { tokens }
}
}: RouteComponentProps<{ tokens: string }>) {
if (!OLD_PATH_STRUCTURE.test(tokens)) {
return <Redirect to="/pool" />
}
const [currency0, currency1] = tokens.split('-')
return <Redirect to={`/remove/${currency0}/${currency1}`} />
}

View File

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

View File

@@ -1,583 +0,0 @@
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 { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import AddressInputPanel from '../../components/AddressInputPanel'
import { ButtonError, ButtonLight, ButtonPrimary, ButtonSecondary } from '../../components/Button'
import Card, { BlueCard, GreyCard } from '../../components/Card'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import ConfirmationModal from '../../components/ConfirmationModal'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import { AutoRow, RowBetween } from '../../components/Row'
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
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 BetterTradeLink from '../../components/swap/BetterTradeLink'
import TokenLogo from '../../components/TokenLogo'
import { TokenWarningCards } from '../../components/TokenWarningCard'
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, useToggleSettingsMenu } from '../../state/application/hooks'
import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
import { Field } from '../../state/swap/actions'
import {
useDefaultsFromURLSearch,
useDerivedSwapInfo,
useSwapActionHandlers,
useSwapState
} from '../../state/swap/hooks'
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
import { CursorPointer, TYPE } from '../../theme'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
import AppBody from '../AppBody'
import { useUserSlippageTolerance, useUserDeadline, useExpertModeManager } from '../../state/user/hooks'
import { ClickableText } from '../Pool/styleds'
export default function Send() {
useDefaultsFromURLSearch()
// text translation
// const { t } = useTranslation()
const { chainId, account } = useActiveWeb3React()
const theme = useContext(ThemeContext)
// 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>('')
const [ENS, setENS] = useState<string>('')
const [recipientError, setRecipientError] = useState<string | null>('Enter a Recipient')
// trade details, check query params for initial state
const { independentField, typedValue } = useSwapState()
const {
parsedAmount,
bestTrade: bestTradeV2,
tokenBalances,
tokens,
error: swapError,
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 isSwapValid = !swapError && !recipientError && bestTrade
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
// modal and loading
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] = useUserDeadline() // custom from user settings
const [allowedSlippage] = useUserSlippageTolerance() // custom from user settings
const route = bestTrade?.route
const userHasSpecifiedInputOutput =
!!tokens[Field.INPUT] &&
!!tokens[Field.OUTPUT] &&
!!parsedAmounts[independentField] &&
parsedAmounts[independentField].greaterThan(JSBI.BigInt(0))
const noRoute = !route
// 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) : ''
}
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(bestTrade, allowedSlippage)
const { priceImpactWithoutFee, realizedLPFee } = computeTradePriceBreakdown(bestTrade)
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] &&
!!WETH[chainId] &&
tokenBalances[Field.INPUT].greaterThan(
new TokenAmount(tokens[Field.INPUT], tokens[Field.INPUT].equals(WETH[chainId]) ? MIN_ETH : '0')
)
? tokens[Field.INPUT].equals(WETH[chainId])
? tokenBalances[Field.INPUT].subtract(new TokenAmount(WETH[chainId], MIN_ETH))
: tokenBalances[Field.INPUT]
: undefined
const atMaxAmountInput: boolean =
!!maxAmountInput && !!parsedAmounts[Field.INPUT] ? maxAmountInput.equalTo(parsedAmounts[Field.INPUT]) : undefined
const swapCallback = useSwapCallback(bestTrade, allowedSlippage, deadline, recipient)
function onSwap() {
if (priceImpactWithoutFee && !confirmPriceImpactWithoutFee(priceImpactWithoutFee)) {
return
}
setAttemptingTxn(true)
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,
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)
const isSendValid = sendCallback !== null && (sendingWithSwap === false || approval === ApprovalState.APPROVED)
async function onSend() {
setAttemptingTxn(true)
sendCallback()
.then(hash => {
setAttemptingTxn(false)
setTxHash(hash)
ReactGA.event({ category: 'Send', action: 'Send', label: tokens[Field.INPUT]?.symbol })
})
.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 [showInverted, setShowInverted] = useState<boolean>(false)
// 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} />
}
if (sendingWithSwap) {
return (
<AutoColumn gap="lg" style={{ marginTop: '40px' }}>
<AutoColumn gap="sm">
<AutoRow gap="10px">
<TokenLogo address={tokens[Field.OUTPUT]?.address} size={'30px'} />
<Text fontSize={36} fontWeight={500}>
{slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(4)} {tokens[Field.OUTPUT]?.symbol}
</Text>
</AutoRow>
<BlueCard>
Via {parsedAmounts[Field.INPUT]?.toSignificant(4)} {tokens[Field.INPUT]?.symbol} swap
</BlueCard>
</AutoColumn>
<AutoColumn gap="sm">
<TYPE.darkGray fontSize={20}>To</TYPE.darkGray>
<TYPE.blue fontSize={36}>
{recipient?.slice(0, 6)}...{recipient?.slice(36, 42)}
</TYPE.blue>
</AutoColumn>
</AutoColumn>
)
}
}
function modalBottom() {
if (!sendingWithSwap) {
return (
<AutoColumn>
<ButtonPrimary onClick={onSend} id="confirm-send">
<Text color="white" fontSize={20}>
Confirm send
</Text>
</ButtonPrimary>
</AutoColumn>
)
}
if (sendingWithSwap) {
return (
<SwapModalFooter
trade={bestTrade}
onSwap={onSwap}
setShowInverted={setShowInverted}
severity={severity}
showInverted={showInverted}
slippageAdjustedAmounts={slippageAdjustedAmounts}
priceImpactWithoutFee={priceImpactWithoutFee}
parsedAmounts={parsedAmounts}
realizedLPFee={realizedLPFee}
confirmText={severity > 2 ? 'Send Anyway' : 'Confirm Send'}
/>
)
}
}
// text to show while loading
const pendingText: string = sendingWithSwap
? `Sending ${parsedAmounts[Field.OUTPUT]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol} to ${recipient}`
: `Sending ${parsedAmounts[Field.INPUT]?.toSignificant(6)} ${tokens[Field.INPUT]?.symbol} to ${recipient}`
const allBalances = useAllTokenBalancesTreatingWETHasETH() // only for 0 balance token selection behavior
const swapState = useSwapState()
function _onTokenSelect(address: string) {
// if no user balance - switch view to a send with swap
const hasBalance = allBalances?.[address]?.greaterThan('0') ?? false
if (!hasBalance) {
onTokenSelection(
Field.INPUT,
swapState[Field.INPUT]?.address === address ? null : swapState[Field.INPUT]?.address
)
onTokenSelection(Field.OUTPUT, address)
setSendingWithSwap(true)
} else {
onTokenSelection(Field.INPUT, address)
}
}
function _onRecipient(result) {
if (result.address) {
setRecipient(result.address)
} else {
setRecipient('')
}
if (result.name) {
setENS(result.name)
}
}
const sendAmountError =
!sendingWithSwap && JSBI.equal(parsedAmounts?.[Field.INPUT]?.raw ?? JSBI.BigInt(0), JSBI.BigInt(0))
? 'Enter an amount'
: null
return (
<>
{sendingWithSwap ? <TokenWarningCards tokens={tokens} /> : null}
<AppBody>
<Wrapper id="send-page">
<ConfirmationModal
isOpen={showConfirm}
title={sendingWithSwap ? 'Confirm swap and send' : 'Confirm Send'}
onDismiss={() => {
setShowConfirm(false)
if (txHash) {
onUserInput(Field.INPUT, '')
}
setTxHash('')
}}
attemptingTxn={attemptingTxn}
hash={txHash}
topContent={modalHeader}
bottomContent={modalBottom}
pendingText={pendingText}
/>
{!sendingWithSwap && (
<AutoColumn justify="center" style={{ marginBottom: '1rem' }}>
<InputGroup gap="lg" justify="center">
<StyledNumerical
id="sending-no-swap-input"
value={formattedAmounts[Field.INPUT]}
onUserInput={val => onUserInput(Field.INPUT, val)}
/>
<CurrencyInputPanel
field={Field.INPUT}
value={formattedAmounts[Field.INPUT]}
onUserInput={(field, val) => onUserInput(Field.INPUT, val)}
onMax={() => {
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
}}
showMaxButton={!atMaxAmountInput}
token={tokens[Field.INPUT]}
onTokenSelection={address => _onTokenSelect(address)}
hideBalance={true}
hideInput={true}
showSendWithSwap={true}
label={''}
id="swap-currency-input"
otherSelectedTokenAddress={tokens[Field.OUTPUT]?.address}
/>
</InputGroup>
<RowBetween style={{ width: 'fit-content' }}>
<ButtonSecondary
width="fit-content"
style={{ fontSize: '14px' }}
padding={'4px 8px'}
onClick={() => setSendingWithSwap(true)}
>
+ Add a swap
</ButtonSecondary>
{account && (
<ButtonSecondary
style={{ fontSize: '14px', marginLeft: '8px' }}
padding={'4px 8px'}
width="fit-content"
disabled={atMaxAmountInput}
onClick={() => {
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
}}
>
Input Max
</ButtonSecondary>
)}
</RowBetween>
</AutoColumn>
)}
<AutoColumn gap={'md'}>
{sendingWithSwap && (
<>
<CurrencyInputPanel
field={Field.INPUT}
label={independentField === Field.OUTPUT && parsedAmounts[Field.INPUT] ? 'From (estimated)' : 'From'}
value={formattedAmounts[Field.INPUT]}
showMaxButton={!atMaxAmountInput}
token={tokens[Field.INPUT]}
onUserInput={onUserInput}
onMax={() => {
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
}}
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">
<ArrowWrapper onClick={onSwitchTokens}>
<ArrowDown size="16" color={theme.text2} onClick={onSwitchTokens} />
</ArrowWrapper>
<ButtonSecondary
onClick={() => setSendingWithSwap(false)}
style={{ marginRight: '0px', width: 'auto', fontSize: '14px' }}
padding={'4px 6px'}
>
Remove Swap
</ButtonSecondary>
</RowBetween>
</ColumnCenter>
) : (
<CursorPointer>
<AutoColumn style={{ padding: '0 1rem' }}>
<ArrowWrapper>
<ArrowDown
size="16"
onClick={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}
label={independentField === Field.INPUT && parsedAmounts[Field.OUTPUT] ? 'To (estimated)' : 'To'}
showMaxButton={false}
token={tokens[Field.OUTPUT]}
onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)}
otherSelectedTokenAddress={tokens[Field.INPUT]?.address}
id="swap-currency-output"
/>
{sendingWithSwap && (
<RowBetween padding="0 1rem 0 12px">
<ArrowDown size="16" color={theme.text2} />
</RowBetween>
)}
</>
)}
<AutoColumn gap="lg" justify="center">
<AddressInputPanel
onChange={_onRecipient}
onError={(error: boolean, input) => {
if (error && input !== '') {
setRecipientError('Invalid Recipient')
} else if (error && input === '') {
setRecipientError('Enter a Recipient')
} else {
setRecipientError(null)
}
}}
/>
</AutoColumn>
{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
inputToken={tokens[Field.INPUT]}
outputToken={tokens[Field.OUTPUT]}
price={bestTrade?.executionPrice}
showInverted={showInverted}
setShowInverted={setShowInverted}
/>
</RowBetween>
{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>
</Card>
)}
</AutoColumn>
<BottomGrouping>
{!account ? (
<ButtonLight
onClick={() => {
toggleWalletModal()
}}
>
Connect Wallet
</ButtonLight>
) : noRoute && userHasSpecifiedInputOutput ? (
<GreyCard style={{ textAlign: 'center' }}>
<TYPE.main mb="4px">Insufficient liquidity for this trade.</TYPE.main>
</GreyCard>
) : 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={() => {
expertMode ? (sendingWithSwap ? onSwap() : onSend()) : setShowConfirm(true)
}}
id="send-button"
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>
)}
{betterTradeLinkVersion && <BetterTradeLink version={betterTradeLinkVersion} />}
</BottomGrouping>
</Wrapper>
</AppBody>
<AdvancedSwapDetailsDropdown trade={bestTrade} />
</>
)
}

View File

@@ -1,32 +1,35 @@
import { JSBI, TokenAmount, WETH } from '@uniswap/sdk'
import React, { useContext, useState, useEffect } from 'react'
import { CurrencyAmount, JSBI } from '@uniswap/sdk'
import React, { useCallback, useContext, useEffect, useState } from 'react'
import { ArrowDown } from 'react-feather'
import ReactGA from 'react-ga'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import AddressInputPanel from '../../components/AddressInputPanel'
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 { RowBetween } from '../../components/Row'
import { SwapPoolTabs } from '../../components/NavigationTabs'
import { AutoRow, RowBetween } from '../../components/Row'
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
import BetterTradeLink from '../../components/swap/BetterTradeLink'
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
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 BetterTradeLink from '../../components/swap/BetterTradeLink'
import { TokenWarningCards } from '../../components/TokenWarningCard'
import { useActiveWeb3React } from '../../hooks'
import { useApproveCallbackFromTrade, ApprovalState } from '../../hooks/useApproveCallback'
import { useSwapCallback } from '../../hooks/useSwapCallback'
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 { BETTER_TRADE_LINK_THRESHOLD, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
import { getTradeVersion, isTradeBetter } from '../../data/V1'
import { useActiveWeb3React } from '../../hooks'
import { ApprovalState, useApproveCallbackFromTrade } from '../../hooks/useApproveCallback'
import useENSAddress from '../../hooks/useENSAddress'
import { useSwapCallback } from '../../hooks/useSwapCallback'
import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
import useWrapCallback, { WrapType } from '../../hooks/useWrapCallback'
import { useToggleSettingsMenu, useWalletModalToggle } from '../../state/application/hooks'
import { Field } from '../../state/swap/actions'
import {
useDefaultsFromURLSearch,
@@ -34,7 +37,9 @@ import {
useSwapActionHandlers,
useSwapState
} from '../../state/swap/hooks'
import { CursorPointer, TYPE } from '../../theme'
import { useExpertModeManager, useUserDeadline, useUserSlippageTolerance } from '../../state/user/hooks'
import { CursorPointer, LinkStyledButton, TYPE } from '../../theme'
import { maxAmountSpend } from '../../utils/maxAmountSpend'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
import AppBody from '../AppBody'
import { ClickableText } from '../Pool/styleds'
@@ -42,7 +47,7 @@ import { ClickableText } from '../Pool/styleds'
export default function Swap() {
useDefaultsFromURLSearch()
const { chainId, account } = useActiveWeb3React()
const { account } = useActiveWeb3React()
const theme = useContext(ThemeContext)
// toggle wallet when disconnected
@@ -57,30 +62,57 @@ export default function Swap() {
const [allowedSlippage] = useUserSlippageTolerance()
// swap state
const { independentField, typedValue } = useSwapState()
const { bestTrade: bestTradeV2, tokenBalances, parsedAmount, tokens, error, v1Trade } = useDerivedSwapInfo()
const { independentField, typedValue, recipient } = useSwapState()
const { v1Trade, v2Trade, currencyBalances, parsedAmount, currencies, error } = useDerivedSwapInfo()
const { wrapType, execute: onWrap, error: wrapError } = useWrapCallback(
currencies[Field.INPUT],
currencies[Field.OUTPUT],
typedValue
)
const showWrap: boolean = wrapType !== WrapType.NOT_APPLICABLE
const { address: recipientAddress } = useENSAddress(recipient)
const toggledVersion = useToggledVersion()
const bestTrade = {
[Version.v1]: v1Trade,
[Version.v2]: bestTradeV2
}[toggledVersion]
const trade = showWrap
? undefined
: {
[Version.v1]: v1Trade,
[Version.v2]: v2Trade
}[toggledVersion]
const betterTradeLinkVersion: Version | undefined =
toggledVersion === Version.v2 && isTradeBetter(bestTradeV2, v1Trade, BETTER_TRADE_LINK_THRESHOLD)
toggledVersion === Version.v2 && isTradeBetter(v2Trade, v1Trade, BETTER_TRADE_LINK_THRESHOLD)
? Version.v1
: toggledVersion === Version.v1 && isTradeBetter(v1Trade, bestTradeV2)
: toggledVersion === Version.v1 && isTradeBetter(v1Trade, v2Trade)
? Version.v2
: undefined
const parsedAmounts = {
[Field.INPUT]: independentField === Field.INPUT ? parsedAmount : bestTrade?.inputAmount,
[Field.OUTPUT]: independentField === Field.OUTPUT ? parsedAmount : bestTrade?.outputAmount
}
const parsedAmounts = showWrap
? {
[Field.INPUT]: parsedAmount,
[Field.OUTPUT]: parsedAmount
}
: {
[Field.INPUT]: independentField === Field.INPUT ? parsedAmount : trade?.inputAmount,
[Field.OUTPUT]: independentField === Field.OUTPUT ? parsedAmount : trade?.outputAmount
}
const { onSwitchTokens, onTokenSelection, onUserInput } = useSwapActionHandlers()
const { onSwitchTokens, onCurrencySelection, onUserInput, onChangeRecipient } = useSwapActionHandlers()
const isValid = !error
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
const handleTypeInput = useCallback(
(value: string) => {
onUserInput(Field.INPUT, value)
},
[onUserInput]
)
const handleTypeOutput = useCallback(
(value: string) => {
onUserInput(Field.OUTPUT, value)
},
[onUserInput]
)
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false) // show confirmation modal
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // waiting for user confirmaion/rejection
@@ -88,19 +120,19 @@ export default function Swap() {
const formattedAmounts = {
[independentField]: typedValue,
[dependentField]: parsedAmounts[dependentField] ? parsedAmounts[dependentField].toSignificant(6) : ''
[dependentField]: showWrap
? parsedAmounts[independentField]?.toExact() ?? ''
: parsedAmounts[dependentField]?.toSignificant(6) ?? ''
}
const route = bestTrade?.route
const userHasSpecifiedInputOutput =
!!tokens[Field.INPUT] &&
!!tokens[Field.OUTPUT] &&
!!parsedAmounts[independentField] &&
parsedAmounts[independentField].greaterThan(JSBI.BigInt(0))
const route = trade?.route
const userHasSpecifiedInputOutput = Boolean(
currencies[Field.INPUT] && currencies[Field.OUTPUT] && parsedAmounts[independentField]?.greaterThan(JSBI.BigInt(0))
)
const noRoute = !route
// check whether the user has approved the router on the input token
const [approval, approveCallback] = useApproveCallbackFromTrade(bestTrade, allowedSlippage)
const [approval, approveCallback] = useApproveCallbackFromTrade(trade, 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)
@@ -112,31 +144,23 @@ export default function Swap() {
}
}, [approval, approvalSubmitted])
const maxAmountInput: TokenAmount =
!!tokenBalances[Field.INPUT] &&
!!tokens[Field.INPUT] &&
!!WETH[chainId] &&
tokenBalances[Field.INPUT].greaterThan(
new TokenAmount(tokens[Field.INPUT], tokens[Field.INPUT].equals(WETH[chainId]) ? MIN_ETH : '0')
)
? tokens[Field.INPUT].equals(WETH[chainId])
? tokenBalances[Field.INPUT].subtract(new TokenAmount(WETH[chainId], MIN_ETH))
: tokenBalances[Field.INPUT]
: undefined
const atMaxAmountInput: boolean =
maxAmountInput && parsedAmounts[Field.INPUT] ? maxAmountInput.equalTo(parsedAmounts[Field.INPUT]) : undefined
const maxAmountInput: CurrencyAmount | undefined = maxAmountSpend(currencyBalances[Field.INPUT])
const atMaxAmountInput = Boolean(maxAmountInput && parsedAmounts[Field.INPUT]?.equalTo(maxAmountInput))
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(bestTrade, allowedSlippage)
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(trade, allowedSlippage)
// the callback to execute the swap
const swapCallback = useSwapCallback(bestTrade, allowedSlippage, deadline)
const swapCallback = useSwapCallback(trade, allowedSlippage, deadline, recipient)
const { priceImpactWithoutFee, realizedLPFee } = computeTradePriceBreakdown(bestTrade)
const { priceImpactWithoutFee, realizedLPFee } = computeTradePriceBreakdown(trade)
function onSwap() {
if (priceImpactWithoutFee && !confirmPriceImpactWithoutFee(priceImpactWithoutFee)) {
return
}
if (!swapCallback) {
return
}
setAttemptingTxn(true)
swapCallback()
.then(hash => {
@@ -145,11 +169,16 @@ export default function Swap() {
ReactGA.event({
category: 'Swap',
action: 'Swap w/o Send',
action:
recipient === null
? 'Swap w/o Send'
: (recipientAddress ?? recipient) === account
? 'Swap w/o Send + recipient'
: 'Swap w/ Send',
label: [
bestTrade.inputAmount.token.symbol,
bestTrade.outputAmount.token.symbol,
getTradeVersion(bestTrade)
trade?.inputAmount?.currency?.symbol,
trade?.outputAmount?.currency?.symbol,
getTradeVersion(trade)
].join('/')
})
})
@@ -180,11 +209,12 @@ export default function Swap() {
function modalHeader() {
return (
<SwapModalHeader
tokens={tokens}
currencies={currencies}
formattedAmounts={formattedAmounts}
slippageAdjustedAmounts={slippageAdjustedAmounts}
priceImpactSeverity={priceImpactSeverity}
independentField={independentField}
recipient={recipient}
/>
)
}
@@ -201,20 +231,21 @@ export default function Swap() {
parsedAmounts={parsedAmounts}
priceImpactWithoutFee={priceImpactWithoutFee}
slippageAdjustedAmounts={slippageAdjustedAmounts}
trade={bestTrade}
trade={trade}
/>
)
}
// text to show while loading
const pendingText = `Swapping ${parsedAmounts[Field.INPUT]?.toSignificant(6)} ${
tokens[Field.INPUT]?.symbol
} for ${parsedAmounts[Field.OUTPUT]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol}`
currencies[Field.INPUT]?.symbol
} for ${parsedAmounts[Field.OUTPUT]?.toSignificant(6)} ${currencies[Field.OUTPUT]?.symbol}`
return (
<>
<TokenWarningCards tokens={tokens} />
<TokenWarningCards currencies={currencies} />
<AppBody>
<SwapPoolTabs active={'swap'} />
<Wrapper id="swap-page">
<ConfirmationModal
isOpen={showConfirm}
@@ -235,83 +266,106 @@ export default function Swap() {
/>
<AutoColumn gap={'md'}>
<>
<CurrencyInputPanel
field={Field.INPUT}
label={independentField === Field.OUTPUT ? 'From (estimated)' : 'From'}
value={formattedAmounts[Field.INPUT]}
showMaxButton={!atMaxAmountInput}
token={tokens[Field.INPUT]}
onUserInput={onUserInput}
onMax={() => {
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
}}
onTokenSelection={address => {
setApprovalSubmitted(false) // reset 2 step UI for approvals
onTokenSelection(Field.INPUT, address)
}}
otherSelectedTokenAddress={tokens[Field.OUTPUT]?.address}
id="swap-currency-input"
/>
<CurrencyInputPanel
label={independentField === Field.OUTPUT && !showWrap ? 'From (estimated)' : 'From'}
value={formattedAmounts[Field.INPUT]}
showMaxButton={!atMaxAmountInput}
currency={currencies[Field.INPUT]}
onUserInput={handleTypeInput}
onMax={() => {
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
}}
onCurrencySelect={currency => {
setApprovalSubmitted(false) // reset 2 step UI for approvals
onCurrencySelection(Field.INPUT, currency)
}}
otherCurrency={currencies[Field.OUTPUT]}
id="swap-currency-input"
/>
<CursorPointer>
<AutoColumn style={{ padding: '0 1rem' }}>
<ArrowWrapper>
<CursorPointer>
<AutoColumn justify="space-between">
<AutoRow justify="space-between" style={{ padding: '0 1rem' }}>
<ArrowWrapper clickable>
<ArrowDown
size="16"
onClick={() => {
setApprovalSubmitted(false) // reset 2 step UI for approvals
onSwitchTokens()
}}
color={tokens[Field.INPUT] && tokens[Field.OUTPUT] ? theme.primary1 : theme.text2}
color={currencies[Field.INPUT] && currencies[Field.OUTPUT] ? theme.primary1 : theme.text2}
/>
</ArrowWrapper>
</AutoColumn>
</CursorPointer>
<CurrencyInputPanel
field={Field.OUTPUT}
value={formattedAmounts[Field.OUTPUT]}
onUserInput={onUserInput}
label={independentField === Field.INPUT ? 'To (estimated)' : 'To'}
showMaxButton={false}
token={tokens[Field.OUTPUT]}
onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)}
otherSelectedTokenAddress={tokens[Field.INPUT]?.address}
id="swap-currency-output"
/>
</>
<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>
{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>
)}
{recipient === null && !showWrap ? (
<LinkStyledButton id="add-recipient-button" onClick={() => onChangeRecipient('')}>
+ Add a send (optional)
</LinkStyledButton>
) : null}
</AutoRow>
</AutoColumn>
</Card>
</CursorPointer>
<CurrencyInputPanel
value={formattedAmounts[Field.OUTPUT]}
onUserInput={handleTypeOutput}
label={independentField === Field.INPUT && !showWrap ? 'To (estimated)' : 'To'}
showMaxButton={false}
currency={currencies[Field.OUTPUT]}
onCurrencySelect={address => onCurrencySelection(Field.OUTPUT, address)}
otherCurrency={currencies[Field.INPUT]}
id="swap-currency-output"
/>
{recipient !== null && !showWrap ? (
<>
<AutoRow justify="space-between" style={{ padding: '0 1rem' }}>
<ArrowWrapper clickable={false}>
<ArrowDown size="16" color={theme.text2} />
</ArrowWrapper>
<LinkStyledButton id="remove-recipient-button" onClick={() => onChangeRecipient(null)}>
- Remove send
</LinkStyledButton>
</AutoRow>
<AddressInputPanel id="recipient" value={recipient} onChange={onChangeRecipient} />
</>
) : null}
{showWrap ? null : (
<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
inputCurrency={currencies[Field.INPUT]}
outputCurrency={currencies[Field.OUTPUT]}
price={trade?.executionPrice}
showInverted={showInverted}
setShowInverted={setShowInverted}
/>
</RowBetween>
{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 ? (
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
) : showWrap ? (
<ButtonPrimary disabled={Boolean(wrapError)} onClick={onWrap}>
{wrapError ?? (wrapType === WrapType.WRAP ? 'Wrap' : wrapType === WrapType.UNWRAP ? 'Unwrap' : null)}
</ButtonPrimary>
) : noRoute && userHasSpecifiedInputOutput ? (
<GreyCard style={{ textAlign: 'center' }}>
<TYPE.main mb="4px">Insufficient liquidity for this trade.</TYPE.main>
@@ -329,7 +383,7 @@ export default function Swap() {
) : approvalSubmitted && approval === ApprovalState.APPROVED ? (
'Approved'
) : (
'Approve ' + tokens[Field.INPUT]?.symbol
'Approve ' + currencies[Field.INPUT]?.symbol
)}
</ButtonPrimary>
<ButtonError
@@ -371,7 +425,7 @@ export default function Swap() {
</Wrapper>
</AppBody>
<AdvancedSwapDetailsDropdown trade={bestTrade} />
<AdvancedSwapDetailsDropdown trade={trade} />
</>
)
}

View File

@@ -1,7 +1,7 @@
// Redirects to swap but only replace the pathname
import React from 'react'
import { Redirect, RouteComponentProps } from 'react-router-dom'
// Redirects to swap but only replace the pathname
export function RedirectPathToSwapOnly({ location }: RouteComponentProps) {
return <Redirect to={{ ...location, pathname: '/swap' }} />
}

View File

@@ -0,0 +1,4 @@
{
"extends": "../../../tsconfig.strict.json",
"include": ["**/*"]
}

View File

@@ -1,25 +1,12 @@
import { createAction } from '@reduxjs/toolkit'
export type PopupContent =
| {
txn: {
hash: string
success?: boolean
summary?: string
}
}
| {
poolAdded: {
token0?: {
address?: string
symbol?: string
}
token1: {
address?: string
symbol?: string
}
}
}
export type PopupContent = {
txn: {
hash: string
success: boolean
summary?: string
}
}
export const updateBlockNumber = createAction<{ chainId: number; blockNumber: number }>('updateBlockNumber')
export const toggleWalletModal = createAction<void>('toggleWalletModal')

View File

@@ -3,8 +3,8 @@ import { createAction } from '@reduxjs/toolkit'
export enum Field {
LIQUIDITY_PERCENT = 'LIQUIDITY_PERCENT',
LIQUIDITY = 'LIQUIDITY',
TOKEN_A = 'TOKEN_A',
TOKEN_B = 'TOKEN_B'
CURRENCY_A = 'CURRENCY_A',
CURRENCY_B = 'CURRENCY_B'
}
export const typeInput = createAction<{ field: Field; typedValue: string }>('typeInputBurn')

View File

@@ -1,95 +1,74 @@
import { useEffect, useCallback, useMemo } from 'react'
import { Currency, CurrencyAmount, JSBI, Pair, Percent, TokenAmount } from '@uniswap/sdk'
import { useCallback } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useActiveWeb3React } from '../../hooks'
import { AppDispatch, AppState } from '../index'
import { Field, typeInput } from './actions'
import { setDefaultsFromURLMatchParams } from '../mint/actions'
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'
import { tryParseAmount } from '../swap/hooks'
import { useTotalSupply } from '../../data/TotalSupply'
const ZERO = JSBI.BigInt(0)
import { useActiveWeb3React } from '../../hooks'
import { wrappedCurrency } from '../../utils/wrappedCurrency'
import { AppDispatch, AppState } from '../index'
import { tryParseAmount } from '../swap/hooks'
import { useTokenBalances } from '../wallet/hooks'
import { Field, typeInput } from './actions'
export function useBurnState(): AppState['burn'] {
return useSelector<AppState, AppState['burn']>(state => state.burn)
}
export function useDerivedBurnInfo(): {
tokens: { [field in Extract<Field, Field.TOKEN_A | Field.TOKEN_B>]?: Token }
export function useDerivedBurnInfo(
currencyA: Currency | undefined,
currencyB: Currency | undefined
): {
pair?: Pair | null
route?: Route
parsedAmounts: {
[Field.LIQUIDITY_PERCENT]: Percent
[Field.LIQUIDITY]?: TokenAmount
[Field.TOKEN_A]?: TokenAmount
[Field.TOKEN_B]?: TokenAmount
[Field.CURRENCY_A]?: CurrencyAmount
[Field.CURRENCY_B]?: CurrencyAmount
}
error?: string
} {
const { account } = useActiveWeb3React()
const { account, chainId } = useActiveWeb3React()
const {
independentField,
typedValue,
[Field.TOKEN_A]: { address: tokenAAddress },
[Field.TOKEN_B]: { address: tokenBAddress }
} = useBurnState()
// tokens
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 ?? undefined,
[Field.TOKEN_B]: tokenB ?? undefined
}),
[tokenA, tokenB]
)
const { independentField, typedValue } = useBurnState()
// pair + totalsupply
const pair = usePair(tokens[Field.TOKEN_A], tokens[Field.TOKEN_B])
const noLiquidity =
pair === null || (!!pair && JSBI.equal(pair.reserve0.raw, ZERO) && JSBI.equal(pair.reserve1.raw, ZERO))
// route
const route =
!noLiquidity && pair && tokens[Field.TOKEN_A] ? new Route([pair], tokens[Field.TOKEN_A] as Token) : undefined
const [, pair] = usePair(currencyA, currencyB)
// balances
const relevantTokenBalances = useTokenBalances(account ?? undefined, [pair?.liquidityToken])
const userLiquidity: undefined | TokenAmount = relevantTokenBalances?.[pair?.liquidityToken?.address ?? '']
const [tokenA, tokenB] = [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)]
const tokens = {
[Field.CURRENCY_A]: tokenA,
[Field.CURRENCY_B]: tokenB,
[Field.LIQUIDITY]: pair?.liquidityToken
}
// liquidity values
const totalSupply = useTotalSupply(pair?.liquidityToken)
const liquidityValues: { [field in Extract<Field, Field.TOKEN_A | Field.TOKEN_B>]?: TokenAmount } = {
[Field.TOKEN_A]:
pair &&
tokens[Field.TOKEN_A] &&
totalSupply &&
userLiquidity &&
// this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
JSBI.greaterThanOrEqual(totalSupply.raw, userLiquidity.raw)
? new TokenAmount(
tokens[Field.TOKEN_A] as Token,
pair.getLiquidityValue(tokens[Field.TOKEN_A] as Token, totalSupply, userLiquidity, false).raw
)
: undefined,
[Field.TOKEN_B]:
pair &&
tokens[Field.TOKEN_B] &&
totalSupply &&
userLiquidity &&
// this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
JSBI.greaterThanOrEqual(totalSupply.raw, userLiquidity.raw)
? new TokenAmount(
tokens[Field.TOKEN_B] as Token,
pair.getLiquidityValue(tokens[Field.TOKEN_B] as Token, totalSupply, userLiquidity, false).raw
)
: undefined
const liquidityValueA =
pair &&
totalSupply &&
userLiquidity &&
tokenA &&
// this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
JSBI.greaterThanOrEqual(totalSupply.raw, userLiquidity.raw)
? new TokenAmount(tokenA, pair.getLiquidityValue(tokenA, totalSupply, userLiquidity, false).raw)
: undefined
const liquidityValueB =
pair &&
totalSupply &&
userLiquidity &&
tokenB &&
// this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
JSBI.greaterThanOrEqual(totalSupply.raw, userLiquidity.raw)
? new TokenAmount(tokenB, pair.getLiquidityValue(tokenB, totalSupply, userLiquidity, false).raw)
: undefined
const liquidityValues: { [Field.CURRENCY_A]?: TokenAmount; [Field.CURRENCY_B]?: TokenAmount } = {
[Field.CURRENCY_A]: liquidityValueA,
[Field.CURRENCY_B]: liquidityValueB
}
let percentToRemove: Percent = new Percent('0', '100')
@@ -110,12 +89,9 @@ export function useDerivedBurnInfo(): {
else {
if (tokens[independentField]) {
const independentAmount = tryParseAmount(typedValue, tokens[independentField])
if (
independentAmount &&
liquidityValues[independentField] &&
!independentAmount.greaterThan(liquidityValues[independentField] as TokenAmount)
) {
percentToRemove = new Percent(independentAmount.raw, (liquidityValues[independentField] as TokenAmount).raw)
const liquidityValue = liquidityValues[independentField]
if (independentAmount && liquidityValue && !independentAmount.greaterThan(liquidityValue)) {
percentToRemove = new Percent(independentAmount.raw, liquidityValue.raw)
}
}
}
@@ -123,27 +99,21 @@ export function useDerivedBurnInfo(): {
const parsedAmounts: {
[Field.LIQUIDITY_PERCENT]: Percent
[Field.LIQUIDITY]?: TokenAmount
[Field.TOKEN_A]?: TokenAmount
[Field.TOKEN_B]?: TokenAmount
[Field.CURRENCY_A]?: TokenAmount
[Field.CURRENCY_B]?: TokenAmount
} = {
[Field.LIQUIDITY_PERCENT]: percentToRemove,
[Field.LIQUIDITY]:
userLiquidity && percentToRemove && percentToRemove.greaterThan('0')
? new TokenAmount(userLiquidity.token, percentToRemove.multiply(userLiquidity.raw).quotient)
: undefined,
[Field.TOKEN_A]:
tokens[Field.TOKEN_A] && percentToRemove && percentToRemove.greaterThan('0') && liquidityValues[Field.TOKEN_A]
? new TokenAmount(
tokens[Field.TOKEN_A] as Token,
percentToRemove.multiply((liquidityValues[Field.TOKEN_A] as TokenAmount).raw).quotient
)
[Field.CURRENCY_A]:
tokenA && percentToRemove && percentToRemove.greaterThan('0') && liquidityValueA
? new TokenAmount(tokenA, percentToRemove.multiply(liquidityValueA.raw).quotient)
: undefined,
[Field.TOKEN_B]:
tokens[Field.TOKEN_B] && percentToRemove && percentToRemove.greaterThan('0') && liquidityValues[Field.TOKEN_B]
? new TokenAmount(
tokens[Field.TOKEN_B] as Token,
percentToRemove.multiply((liquidityValues[Field.TOKEN_B] as TokenAmount).raw).quotient
)
[Field.CURRENCY_B]:
tokenB && percentToRemove && percentToRemove.greaterThan('0') && liquidityValueB
? new TokenAmount(tokenB, percentToRemove.multiply(liquidityValueB.raw).quotient)
: undefined
}
@@ -152,11 +122,11 @@ export function useDerivedBurnInfo(): {
error = 'Connect Wallet'
}
if (!parsedAmounts[Field.LIQUIDITY] || !parsedAmounts[Field.TOKEN_A] || !parsedAmounts[Field.TOKEN_B]) {
if (!parsedAmounts[Field.LIQUIDITY] || !parsedAmounts[Field.CURRENCY_A] || !parsedAmounts[Field.CURRENCY_B]) {
error = error ?? 'Enter an amount'
}
return { tokens, pair, route, parsedAmounts, error }
return { pair, parsedAmounts, error }
}
export function useBurnActionHandlers(): {
@@ -175,13 +145,3 @@ export function useBurnActionHandlers(): {
onUserInput
}
}
// updates the burn state to use the appropriate tokens, given the route
export function useDefaultsFromURLMatchParams(params: { [k: string]: string }) {
const { chainId } = useActiveWeb3React()
const dispatch = useDispatch<AppDispatch>()
useEffect(() => {
if (!chainId) return
dispatch(setDefaultsFromURLMatchParams({ chainId, params }))
}, [dispatch, chainId, params])
}

View File

@@ -1,51 +1,22 @@
import { createReducer } from '@reduxjs/toolkit'
import { Field, typeInput } from './actions'
import { setDefaultsFromURLMatchParams } from '../mint/actions'
import { parseTokens } from '../mint/reducer'
export interface MintState {
export interface BurnState {
readonly independentField: Field
readonly typedValue: string
readonly [Field.TOKEN_A]: {
readonly address: string
}
readonly [Field.TOKEN_B]: {
readonly address: string
}
}
const initialState: MintState = {
const initialState: BurnState = {
independentField: Field.LIQUIDITY_PERCENT,
typedValue: '0',
[Field.TOKEN_A]: {
address: ''
},
[Field.TOKEN_B]: {
address: ''
}
typedValue: '0'
}
export default createReducer<MintState>(initialState, builder =>
builder
.addCase(setDefaultsFromURLMatchParams, (state, { payload: { chainId, params } }) => {
const tokens = parseTokens(chainId, params?.tokens ?? '')
return {
independentField: Field.LIQUIDITY_PERCENT,
typedValue: '0',
[Field.TOKEN_A]: {
address: tokens[0]
},
[Field.TOKEN_B]: {
address: tokens[1]
}
}
})
.addCase(typeInput, (state, { payload: { field, typedValue } }) => {
return {
...state,
independentField: field,
typedValue
}
})
export default createReducer<BurnState>(initialState, builder =>
builder.addCase(typeInput, (state, { payload: { field, typedValue } }) => {
return {
...state,
independentField: field,
typedValue
}
})
)

View File

@@ -1,13 +1,9 @@
import { createAction } from '@reduxjs/toolkit'
import { RouteComponentProps } from 'react-router-dom'
export enum Field {
TOKEN_A = 'TOKEN_A',
TOKEN_B = 'TOKEN_B'
CURRENCY_A = 'CURRENCY_A',
CURRENCY_B = 'CURRENCY_B'
}
export const setDefaultsFromURLMatchParams = createAction<{
chainId: number
params: RouteComponentProps<{ [k: string]: string }>['match']['params']
}>('setDefaultsFromMatch')
export const typeInput = createAction<{ field: Field; typedValue: string; noLiquidity: boolean }>('typeInputMint')
export const resetMintState = createAction<void>('resetMintState')

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