Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4b5727fdb | ||
|
|
1fd6b1e659 | ||
|
|
6570beef32 | ||
|
|
b57f58ab35 | ||
|
|
2f40c4f614 | ||
|
|
3f9c34d37d | ||
|
|
1d5c6530e3 | ||
|
|
78f294c340 | ||
|
|
90d24a26f3 | ||
|
|
7a3a5bd546 | ||
|
|
081ae15aa8 | ||
|
|
f5a5c5e70d | ||
|
|
e05e0206b7 | ||
|
|
344b4340ae | ||
|
|
eeef306bdd | ||
|
|
63a491d4b1 | ||
|
|
6831a73fdf | ||
|
|
a4aef02747 | ||
|
|
c26716047f | ||
|
|
0fa238af0b | ||
|
|
21c1484c0e | ||
|
|
8a845ee0e9 | ||
|
|
f5229ca838 | ||
|
|
875203f0ef | ||
|
|
91a8202737 | ||
|
|
0b4819d165 | ||
|
|
e7d3289754 | ||
|
|
0698e0f82a | ||
|
|
0350cc4701 | ||
|
|
997052869d | ||
|
|
9ec16c2ba8 | ||
|
|
e2cf8f1642 | ||
|
|
ed6952d1f7 | ||
|
|
3277d70e93 | ||
|
|
d1a31fe763 | ||
|
|
f88af029ae | ||
|
|
9f3e49b4d8 | ||
|
|
d4911d1054 | ||
|
|
90df9c4ced | ||
|
|
14f15d1fd6 | ||
|
|
69818ace1f | ||
|
|
42906d6709 | ||
|
|
2f8936a980 | ||
|
|
f5c4468c3c | ||
|
|
852e8f749f | ||
|
|
6694e5e398 | ||
|
|
2c9a50a372 | ||
|
|
0fc0cba6de | ||
|
|
041c86c04d | ||
|
|
123373e671 | ||
|
|
eb1732deee | ||
|
|
3c13321a71 | ||
|
|
58703f31a0 | ||
|
|
58721fb191 | ||
|
|
678cd1a06f | ||
|
|
a5ff3beb92 | ||
|
|
35ccf425f6 | ||
|
|
fe030412cd | ||
|
|
4d5a43351f | ||
|
|
ac1bc3b3a6 | ||
|
|
d1063d50ed | ||
|
|
46fc74e90f | ||
|
|
2c4f4092d8 | ||
|
|
aac7268dc8 | ||
|
|
fd162a72ff | ||
|
|
e20936709c | ||
|
|
2fda2c8c15 | ||
|
|
1f09757c49 | ||
|
|
7e49babff7 | ||
|
|
b35653ade1 | ||
|
|
57b53013d1 | ||
|
|
bafd3f3c05 | ||
|
|
29db0a50b3 | ||
|
|
9566fb888e | ||
|
|
13c8903e8f | ||
|
|
1d06b47e8d | ||
|
|
0089c2ee43 | ||
|
|
9869a9fcb7 | ||
|
|
631f29d66d | ||
|
|
97deebad37 |
44
.github/workflows/release.yaml
vendored
44
.github/workflows/release.yaml
vendored
@@ -2,14 +2,10 @@ name: Release
|
||||
on:
|
||||
# every morning
|
||||
schedule:
|
||||
- cron: '0 12 * * *'
|
||||
- cron: '0 12 * * 1-4'
|
||||
|
||||
# releases are triggered on changes to this file
|
||||
push:
|
||||
branches:
|
||||
- v2
|
||||
paths:
|
||||
- '.github/workflows/release.yaml'
|
||||
# manual trigger
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
bump_version:
|
||||
@@ -43,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,14 +60,14 @@ jobs:
|
||||
cidv0: ${{ steps.upload.outputs.hash }}
|
||||
|
||||
- name: Update DNS with new IPFS hash
|
||||
uses: uniswap/replace-vercel-dns-records@v1.0.0
|
||||
env:
|
||||
CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_TOKEN }}
|
||||
RECORD_DOMAIN: 'uniswap.org'
|
||||
RECORD_NAME: '_dnslink.app'
|
||||
CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}
|
||||
uses: textileio/cloudflare-update-dnslink@0fe7b7a1ffc865db3a4da9773f0f987447ad5848
|
||||
with:
|
||||
domain: 'uniswap.org'
|
||||
subdomain: '_dnslink.app'
|
||||
record-type: 'TXT'
|
||||
value: dnslink=/ipfs/${{ steps.upload.outputs.hash }}
|
||||
token: ${{ secrets.VERCEL_TOKEN }}
|
||||
team-name: 'uniswap'
|
||||
cid: ${{ steps.upload.outputs.hash }}
|
||||
|
||||
- name: Create GitHub Release
|
||||
id: create_release
|
||||
@@ -82,15 +78,17 @@ jobs:
|
||||
tag_name: ${{ needs.bump_version.outputs.new_tag }}
|
||||
release_name: Release ${{ needs.bump_version.outputs.new_tag }}
|
||||
body: |
|
||||
Release built from commit [`${{ github.sha }}`](https://github.com/Uniswap/uniswap-frontend/tree/${{ github.sha }})
|
||||
|
||||
The IPFS hash of the bundle is:
|
||||
IPFS hash of the deployment:
|
||||
- CIDv0: `${{ steps.upload.outputs.hash }}`
|
||||
- CIDv1: `${{ steps.convert_cidv0.outputs.cidv1 }}`
|
||||
|
||||
Uniswap uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to store your settings.
|
||||
**Beware** that other sites you access via the _same_ IPFS gateway can read and modify your settings on Uniswap without your permission.
|
||||
You can avoid this issue by using a subdomain IPFS gateway. The preferred gateway URLs below utilize the CIDv1 of the release in the subdomain, and are relatively safer.
|
||||
The latest release is always accessible via our alias to the Cloudflare IPFS gateway at [app.uniswap.org](https://app.uniswap.org).
|
||||
|
||||
You can also access the Uniswap Interface directly from an IPFS gateway.
|
||||
The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to store your settings.
|
||||
**Beware** that other sites you access via the _same_ IPFS gateway can read and modify your settings on the Uniswap interface without your permission.
|
||||
You can avoid this issue by using a subdomain IPFS gateway, or our alias to the latest release at [app.uniswap.org](https://app.uniswap.org).
|
||||
The preferred URLs below are safe to use to access this specific release.
|
||||
|
||||
Preferred URLs:
|
||||
- https://${{ steps.convert_cidv0.outputs.cidv1 }}.ipfs.dweb.link/
|
||||
|
||||
12
.github/workflows/tests.yaml
vendored
12
.github/workflows/tests.yaml
vendored
@@ -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
|
||||
|
||||
|
||||
42
README.md
42
README.md
@@ -1,11 +1,12 @@
|
||||
# Uniswap Frontend
|
||||
# Uniswap Interface
|
||||
|
||||
[](https://github.com/Uniswap/uniswap-frontend/actions?query=workflow%3ATests)
|
||||
[](https://github.com/Uniswap/uniswap-interface/actions?query=workflow%3ATests)
|
||||
[](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).
|
||||
|
||||
@@ -16,4 +16,29 @@ describe('Add Liquidity', () => {
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'SKL')
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'MKR')
|
||||
})
|
||||
|
||||
it('single token can be selected', () => {
|
||||
cy.visit('/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'SKL')
|
||||
cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'MKR')
|
||||
})
|
||||
|
||||
it('redirects /add/token-token to add/token/token', () => {
|
||||
cy.visit('/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
|
||||
cy.url().should(
|
||||
'contain',
|
||||
'/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85'
|
||||
)
|
||||
})
|
||||
|
||||
it('redirects /add/WETH-token to /add/ETH/token', () => {
|
||||
cy.visit('/add/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
|
||||
cy.url().should('contain', '/add/ETH/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
|
||||
})
|
||||
|
||||
it('redirects /add/token-WETH to /add/token/ETH', () => {
|
||||
cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85-0xc778417E063141139Fce010982780140Aa0cD5Ab')
|
||||
cy.url().should('contain', '/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85/ETH')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
.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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,31 +2,31 @@ describe('Swap', () => {
|
||||
beforeEach(() => cy.visit('/swap'))
|
||||
it('can enter an amount into input', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input')
|
||||
.type('0.001')
|
||||
.type('0.001', { delay: 200 })
|
||||
.should('have.value', '0.001')
|
||||
})
|
||||
|
||||
it('zero swap amount', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input')
|
||||
.type('0.0')
|
||||
.type('0.0', { delay: 200 })
|
||||
.should('have.value', '0.0')
|
||||
})
|
||||
|
||||
it('invalid swap amount', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input')
|
||||
.type('\\')
|
||||
.type('\\', { delay: 200 })
|
||||
.should('have.value', '')
|
||||
})
|
||||
|
||||
it('can enter an amount into output', () => {
|
||||
cy.get('#swap-currency-output .token-amount-input')
|
||||
.type('0.001')
|
||||
.type('0.001', { delay: 200 })
|
||||
.should('have.value', '0.001')
|
||||
})
|
||||
|
||||
it('zero output amount', () => {
|
||||
cy.get('#swap-currency-output .token-amount-input')
|
||||
.type('0.0')
|
||||
.type('0.0', { delay: 200 })
|
||||
.should('have.value', '0.0')
|
||||
})
|
||||
|
||||
@@ -35,10 +35,20 @@ describe('Swap', () => {
|
||||
cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').should('be.visible')
|
||||
cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').click({ force: true })
|
||||
cy.get('#swap-currency-input .token-amount-input').should('be.visible')
|
||||
cy.get('#swap-currency-input .token-amount-input').type('0.001', { force: true })
|
||||
cy.get('#swap-currency-input .token-amount-input').type('0.001', { force: true, delay: 200 })
|
||||
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
|
||||
cy.get('#show-advanced').click()
|
||||
cy.get('#swap-button').click()
|
||||
cy.get('#confirm-swap-or-send').should('contain', 'Confirm Swap')
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
27
netlify.toml
27
netlify.toml
@@ -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
|
||||
15
package.json
15
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@uniswap/interface",
|
||||
"description": "Uniswap Interface",
|
||||
"homepage": "https://uniswap.exchange",
|
||||
"homepage": ".",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@ethersproject/address": "^5.0.0-beta.134",
|
||||
@@ -13,7 +13,7 @@
|
||||
"@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",
|
||||
"@popperjs/core": "^2.4.4",
|
||||
"@reach/dialog": "^0.10.3",
|
||||
"@reach/portal": "^0.10.3",
|
||||
"@reduxjs/toolkit": "^1.3.5",
|
||||
@@ -38,7 +38,7 @@
|
||||
"@web3-react/fortmatic-connector": "^6.0.9",
|
||||
"@web3-react/injected-connector": "^6.0.7",
|
||||
"@web3-react/portis-connector": "^6.0.9",
|
||||
"@web3-react/walletconnect-connector": "^6.0.9",
|
||||
"@web3-react/walletconnect-connector": "^6.1.1",
|
||||
"@web3-react/walletlink-connector": "^6.0.9",
|
||||
"copy-to-clipboard": "^3.2.0",
|
||||
"cross-env": "^7.0.2",
|
||||
@@ -51,6 +51,7 @@
|
||||
"i18next": "^15.0.9",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
"i18next-xhr-backend": "^2.0.1",
|
||||
"inter-ui": "^3.13.1",
|
||||
"jazzicon": "^1.5.0",
|
||||
"lodash.flatmap": "^4.5.0",
|
||||
"polished": "^3.3.2",
|
||||
@@ -80,15 +81,11 @@
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "cross-env REACT_APP_GIT_COMMIT_HASH=$(git show -s --format=%H) react-scripts build",
|
||||
"ipfs-build": "cross-env PUBLIC_URL=\".\" react-scripts build",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
|
||||
"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"
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
"youAreSettingExRate": "שער ההמרה יקבע על ידך",
|
||||
"totalSupplyIs0": "אין לך טוקנים של נזילות",
|
||||
"tokenWorth": "שווי כל טוקן נזילות הינו",
|
||||
"firstLiquidity": "את\ה הראשון\ה שמזרים נזילות למאגר",
|
||||
"firstLiquidity": "אתה הראשוןה שמזרים נזילות למאגר",
|
||||
"initialExchangeRate": "ושל האית'ר הינן בערך שווה {{ label }} תוודאו שההפקדה של הטוקן",
|
||||
"removeLiquidity": "הוצאה של נזילות",
|
||||
"poolTokens": "טוקנים של מאגר הנזילות",
|
||||
@@ -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" />}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -51,10 +52,10 @@ export const ButtonPrimary = styled(Base)`
|
||||
}
|
||||
&:disabled {
|
||||
background-color: ${({ theme, altDisbaledStyle }) => (altDisbaledStyle ? theme.primary1 : theme.bg3)};
|
||||
color: ${({ theme, altDisbaledStyle }) => (altDisbaledStyle ? 'white' : theme.text3)}
|
||||
color: ${({ theme, altDisbaledStyle }) => (altDisbaledStyle ? 'white' : theme.text3)};
|
||||
cursor: auto;
|
||||
box-shadow: none;
|
||||
border: 1px solid transparent;;
|
||||
border: 1px solid transparent;
|
||||
outline: none;
|
||||
}
|
||||
`
|
||||
@@ -197,7 +198,6 @@ export const ButtonEmpty = styled(Base)`
|
||||
export const ButtonWhite = styled(Base)`
|
||||
border: 1px solid #edeef2;
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
};
|
||||
color: black;
|
||||
|
||||
&:focus {
|
||||
@@ -245,6 +245,9 @@ const ButtonErrorStyle = styled(Base)`
|
||||
&:disabled {
|
||||
opacity: 50%;
|
||||
cursor: auto;
|
||||
box-shadow: none;
|
||||
background-color: ${({ theme }) => theme.red1};
|
||||
border: 1px solid ${({ theme }) => theme.red1};
|
||||
}
|
||||
`
|
||||
|
||||
@@ -264,7 +267,7 @@ export function ButtonError({ error, ...rest }: { error?: boolean } & ButtonProp
|
||||
}
|
||||
}
|
||||
|
||||
export function ButtonDropwdown({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
|
||||
export function ButtonDropdown({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
|
||||
return (
|
||||
<ButtonPrimary {...rest} disabled={disabled}>
|
||||
<RowBetween>
|
||||
@@ -275,7 +278,7 @@ export function ButtonDropwdown({ disabled = false, children, ...rest }: { disab
|
||||
)
|
||||
}
|
||||
|
||||
export function ButtonDropwdownLight({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
|
||||
export function ButtonDropdownLight({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
|
||||
return (
|
||||
<ButtonOutlined {...rest} disabled={disabled}>
|
||||
<RowBetween>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Pair, Token } from '@uniswap/sdk'
|
||||
import React, { useState, useContext } from 'react'
|
||||
import React, { useState, useContext, useCallback } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import { darken } from 'polished'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||
import TokenSearchModal from '../SearchModal/TokenSearchModal'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import DoubleLogo from '../DoubleLogo'
|
||||
import SearchModal from '../SearchModal'
|
||||
import { RowBetween } from '../Row'
|
||||
import { TYPE, CursorPointer } from '../../theme'
|
||||
import { Input as NumericalInput } from '../NumericalInput'
|
||||
@@ -132,6 +132,7 @@ interface CurrencyInputPanelProps {
|
||||
showSendWithSwap?: boolean
|
||||
otherSelectedTokenAddress?: string | null
|
||||
id: string
|
||||
showCommonBases?: boolean
|
||||
}
|
||||
|
||||
export default function CurrencyInputPanel({
|
||||
@@ -150,7 +151,8 @@ export default function CurrencyInputPanel({
|
||||
hideInput = false,
|
||||
showSendWithSwap = false,
|
||||
otherSelectedTokenAddress = null,
|
||||
id
|
||||
id,
|
||||
showCommonBases
|
||||
}: CurrencyInputPanelProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -159,6 +161,10 @@ export default function CurrencyInputPanel({
|
||||
const userTokenBalance = useTokenBalanceTreatingWETHasETH(account, token)
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const handleDismissSearch = useCallback(() => {
|
||||
setModalOpen(false)
|
||||
}, [setModalOpen])
|
||||
|
||||
return (
|
||||
<InputPanel id={id}>
|
||||
<Container hideInput={hideInput}>
|
||||
@@ -235,17 +241,15 @@ export default function CurrencyInputPanel({
|
||||
</InputRow>
|
||||
</Container>
|
||||
{!disableTokenSelect && (
|
||||
<SearchModal
|
||||
<TokenSearchModal
|
||||
isOpen={modalOpen}
|
||||
onDismiss={() => {
|
||||
setModalOpen(false)
|
||||
}}
|
||||
filterType="tokens"
|
||||
onDismiss={handleDismissSearch}
|
||||
onTokenSelect={onTokenSelection}
|
||||
showSendWithSwap={showSendWithSwap}
|
||||
hiddenToken={token?.address}
|
||||
otherSelectedTokenAddress={otherSelectedTokenAddress}
|
||||
otherSelectedText={field === Field.INPUT ? 'Selected as output' : 'Selected as input'}
|
||||
showCommonBases={showCommonBases}
|
||||
/>
|
||||
)}
|
||||
</InputPanel>
|
||||
|
||||
@@ -12,8 +12,8 @@ const TokenWrapper = styled.div<{ margin: boolean; sizeraw: number }>`
|
||||
interface DoubleTokenLogoProps {
|
||||
margin?: boolean
|
||||
size?: number
|
||||
a0: string
|
||||
a1: string
|
||||
a0?: string
|
||||
a1?: string
|
||||
}
|
||||
|
||||
const HigherLogo = styled(TokenLogo)`
|
||||
@@ -27,8 +27,8 @@ const CoveredLogo = styled(TokenLogo)<{ sizeraw: number }>`
|
||||
export default function DoubleTokenLogo({ a0, a1, size = 16, margin = false }: DoubleTokenLogoProps) {
|
||||
return (
|
||||
<TokenWrapper sizeraw={size} margin={margin}>
|
||||
<HigherLogo address={a0} size={size.toString() + 'px'} />
|
||||
<CoveredLogo address={a1} size={size.toString() + 'px'} sizeraw={size} />
|
||||
{a0 && <HigherLogo address={a0} size={size.toString() + 'px'} />}
|
||||
{a1 && <CoveredLogo address={a1} size={size.toString() + 'px'} sizeraw={size} />}
|
||||
</TokenWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { Send, Sun, Moon } from 'react-feather'
|
||||
import { useDarkModeManager } from '../../state/user/hooks'
|
||||
|
||||
import { ButtonSecondary } from '../Button'
|
||||
|
||||
const FooterFrame = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
position: fixed;
|
||||
right: 1rem;
|
||||
bottom: 1rem;
|
||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||
display: none;
|
||||
`};
|
||||
`
|
||||
|
||||
export default function Footer() {
|
||||
const [darkMode, toggleDarkMode] = useDarkModeManager()
|
||||
|
||||
return (
|
||||
<FooterFrame>
|
||||
<form action="https://forms.gle/DaLuqvJsVhVaAM3J9" target="_blank">
|
||||
<ButtonSecondary p="8px 12px">
|
||||
<Send size={16} style={{ marginRight: '8px' }} /> Feedback
|
||||
</ButtonSecondary>
|
||||
</form>
|
||||
<ButtonSecondary onClick={toggleDarkMode} p="8px 12px" ml="0.5rem" width="min-content">
|
||||
{darkMode ? <Sun size={16} /> : <Moon size={16} />}
|
||||
</ButtonSecondary>
|
||||
</FooterFrame>
|
||||
)
|
||||
}
|
||||
70
src/components/Header/VersionSwitch.tsx
Normal file
70
src/components/Header/VersionSwitch.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { stringify } from 'qs'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
import useParsedQueryString from '../../hooks/useParsedQueryString'
|
||||
import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
|
||||
|
||||
const VersionLabel = styled.span<{ enabled: boolean }>`
|
||||
padding: 0.35rem 0.6rem;
|
||||
border-radius: 12px;
|
||||
background: ${({ theme, enabled }) => (enabled ? theme.primary1 : 'none')};
|
||||
color: ${({ theme, enabled }) => (enabled ? theme.white : theme.text1)};
|
||||
font-size: 1rem;
|
||||
font-weight: ${({ enabled }) => (enabled ? '500' : '400')};
|
||||
:hover {
|
||||
user-select: ${({ enabled }) => (enabled ? 'none' : 'initial')};
|
||||
background: ${({ theme, enabled }) => (enabled ? theme.primary1 : 'none')};
|
||||
color: ${({ theme, enabled }) => (enabled ? theme.white : theme.text1)};
|
||||
}
|
||||
`
|
||||
|
||||
interface VersionToggleProps extends React.ComponentProps<typeof Link> {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const VersionToggle = styled(({ enabled, ...rest }: VersionToggleProps) => <Link {...rest} />)<VersionToggleProps>`
|
||||
border-radius: 12px;
|
||||
opacity: ${({ enabled }) => (enabled ? 1 : 0.5)};
|
||||
cursor: ${({ enabled }) => (enabled ? 'pointer' : 'default')};
|
||||
background: ${({ theme }) => theme.bg3};
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
margin-left: 0.5rem;
|
||||
text-decoration: none;
|
||||
:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
|
||||
export default function VersionSwitch() {
|
||||
const version = useToggledVersion()
|
||||
const location = useLocation()
|
||||
const query = useParsedQueryString()
|
||||
const versionSwitchAvailable = location.pathname === '/swap' || location.pathname === '/send'
|
||||
|
||||
const toggleDest = useMemo(() => {
|
||||
return versionSwitchAvailable
|
||||
? {
|
||||
...location,
|
||||
search: `?${stringify({ ...query, use: version === Version.v1 ? undefined : Version.v1 })}`
|
||||
}
|
||||
: location
|
||||
}, [location, query, version, versionSwitchAvailable])
|
||||
|
||||
const handleClick = useCallback(
|
||||
e => {
|
||||
if (!versionSwitchAvailable) e.preventDefault()
|
||||
},
|
||||
[versionSwitchAvailable]
|
||||
)
|
||||
|
||||
return (
|
||||
<VersionToggle enabled={versionSwitchAvailable} to={toggleDest} onClick={handleClick}>
|
||||
<VersionLabel enabled={version === Version.v2 || !versionSwitchAvailable}>V2</VersionLabel>
|
||||
<VersionLabel enabled={version === Version.v1 && versionSwitchAvailable}>V1</VersionLabel>
|
||||
</VersionToggle>
|
||||
)
|
||||
}
|
||||
@@ -1,27 +1,25 @@
|
||||
import { ChainId, WETH } from '@uniswap/sdk'
|
||||
import React from 'react'
|
||||
import { Link as HistoryLink } from 'react-router-dom'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { Text } from 'rebass'
|
||||
|
||||
import styled from 'styled-components'
|
||||
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||
|
||||
import Row from '../Row'
|
||||
import Menu from '../Menu'
|
||||
import Web3Status from '../Web3Status'
|
||||
|
||||
import { ExternalLink, StyledInternalLink } from '../../theme'
|
||||
import { Text } from 'rebass'
|
||||
import { WETH, ChainId } from '@uniswap/sdk'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { YellowCard } from '../Card'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useDarkModeManager } from '../../state/user/hooks'
|
||||
|
||||
import Logo from '../../assets/svg/logo.svg'
|
||||
import Wordmark from '../../assets/svg/wordmark.svg'
|
||||
import LogoDark from '../../assets/svg/logo_white.svg'
|
||||
import Wordmark from '../../assets/svg/wordmark.svg'
|
||||
import WordmarkDark from '../../assets/svg/wordmark_white.svg'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { RowBetween } from '../Row'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useDarkModeManager } from '../../state/user/hooks'
|
||||
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||
|
||||
import { YellowCard } from '../Card'
|
||||
import Settings from '../Settings'
|
||||
import Menu from '../Menu'
|
||||
|
||||
import Row, { RowBetween } from '../Row'
|
||||
import Web3Status from '../Web3Status'
|
||||
import VersionSwitch from './VersionSwitch'
|
||||
|
||||
const HeaderFrame = styled.div`
|
||||
display: flex;
|
||||
@@ -31,15 +29,12 @@ const HeaderFrame = styled.div`
|
||||
width: 100%;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
z-index: 2;
|
||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||
padding: 12px 0 0 0;
|
||||
width: calc(100%);
|
||||
position: relative;
|
||||
`};
|
||||
z-index: 2;
|
||||
`
|
||||
|
||||
const HeaderElement = styled.div`
|
||||
@@ -47,7 +42,16 @@ const HeaderElement = styled.div`
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const Title = styled.div`
|
||||
const HeaderElementWrap = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
margin-top: 0.5rem;
|
||||
`};
|
||||
`
|
||||
|
||||
const Title = styled.a`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
pointer-events: auto;
|
||||
@@ -72,6 +76,7 @@ const AccountElement = styled.div<{ active: boolean }>`
|
||||
background-color: ${({ theme, active }) => (!active ? theme.bg1 : theme.bg3)};
|
||||
border-radius: 12px;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
|
||||
:focus {
|
||||
border: 1px solid blue;
|
||||
@@ -82,10 +87,7 @@ const TestnetWrapper = styled.div`
|
||||
white-space: nowrap;
|
||||
width: fit-content;
|
||||
margin-left: 10px;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
display: none;
|
||||
`};
|
||||
pointer-events: auto;
|
||||
`
|
||||
|
||||
const NetworkCard = styled(YellowCard)`
|
||||
@@ -95,60 +97,42 @@ 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);
|
||||
}
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
img {
|
||||
width: 4.5rem;
|
||||
}
|
||||
`};
|
||||
`
|
||||
|
||||
const MigrateBanner = styled(AutoColumn)`
|
||||
width: 100%;
|
||||
padding: 12px 0;
|
||||
const HeaderControls = styled.div`
|
||||
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};
|
||||
}
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
padding: 0;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
`};
|
||||
`
|
||||
|
||||
const BalanceText = styled(Text)`
|
||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||
display: none;
|
||||
`};
|
||||
`
|
||||
|
||||
const VersionLabel = styled.span<{ isV2?: boolean }>`
|
||||
padding: ${({ isV2 }) => (isV2 ? '0.15rem 0.5rem 0.16rem 0.45rem' : '0.15rem 0.5rem 0.16rem 0.35rem')};
|
||||
border-radius: 14px;
|
||||
background: ${({ theme, isV2 }) => (isV2 ? theme.primary1 : 'none')};
|
||||
color: ${({ theme, isV2 }) => (isV2 ? theme.white : theme.primary1)};
|
||||
font-size: 0.825rem;
|
||||
font-weight: 400;
|
||||
:hover {
|
||||
user-select: ${({ isV2 }) => (isV2 ? 'none' : 'initial')};
|
||||
background: ${({ theme, isV2 }) => (isV2 ? theme.primary1 : 'none')};
|
||||
color: ${({ theme, isV2 }) => (isV2 ? theme.white : theme.primary3)};
|
||||
}
|
||||
`
|
||||
|
||||
const VersionToggle = styled.a`
|
||||
border-radius: 16px;
|
||||
background: ${({ theme }) => theme.primary5};
|
||||
border: 1px solid ${({ theme }) => theme.primary4};
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
const NETWORK_LABELS: { [chainId in ChainId]: string | null } = {
|
||||
[ChainId.MAINNET]: null,
|
||||
[ChainId.RINKEBY]: 'Rinkeby',
|
||||
[ChainId.ROPSTEN]: 'Ropsten',
|
||||
[ChainId.GÖRLI]: 'Görli',
|
||||
[ChainId.KOVAN]: 'Kovan'
|
||||
}
|
||||
|
||||
export default function Header() {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
@@ -158,63 +142,37 @@ export default function Header() {
|
||||
|
||||
return (
|
||||
<HeaderFrame>
|
||||
<MigrateBanner>
|
||||
Uniswap V2 is live! Read the
|
||||
<ExternalLink href="https://uniswap.org/blog/launch-uniswap-v2/">
|
||||
<b>blog post ↗</b>
|
||||
</ExternalLink>
|
||||
or
|
||||
<StyledInternalLink to="/migrate/v1">
|
||||
<b>migrate your liquidity ↗</b>
|
||||
</StyledInternalLink>
|
||||
.
|
||||
</MigrateBanner>
|
||||
<RowBetween padding="1rem">
|
||||
<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>
|
||||
<TestnetWrapper style={{ pointerEvents: 'auto' }}>
|
||||
{!isMobile && (
|
||||
<VersionToggle target="_self" href="https://v1.uniswap.exchange">
|
||||
<VersionLabel isV2={true}>V2</VersionLabel>
|
||||
<VersionLabel isV2={false}>V1</VersionLabel>
|
||||
</VersionToggle>
|
||||
)}
|
||||
</TestnetWrapper>
|
||||
</HeaderElement>
|
||||
<HeaderElement>
|
||||
<TestnetWrapper>
|
||||
{!isMobile && chainId === ChainId.ROPSTEN && <NetworkCard>Ropsten</NetworkCard>}
|
||||
{!isMobile && chainId === ChainId.RINKEBY && <NetworkCard>Rinkeby</NetworkCard>}
|
||||
{!isMobile && chainId === ChainId.GÖRLI && <NetworkCard>Görli</NetworkCard>}
|
||||
{!isMobile && chainId === ChainId.KOVAN && <NetworkCard>Kovan</NetworkCard>}
|
||||
</TestnetWrapper>
|
||||
<AccountElement active={!!account} style={{ pointerEvents: 'auto' }}>
|
||||
{account && userEthBalance ? (
|
||||
<Text style={{ flexShrink: 0 }} pl="0.75rem" pr="0.5rem" fontWeight={500}>
|
||||
{userEthBalance?.toSignificant(4)} ETH
|
||||
</Text>
|
||||
) : null}
|
||||
<Web3Status />
|
||||
</AccountElement>
|
||||
<div style={{ pointerEvents: 'auto' }}>
|
||||
<HeaderControls>
|
||||
<HeaderElement>
|
||||
<TestnetWrapper>
|
||||
{!isMobile && NETWORK_LABELS[chainId] && <NetworkCard>{NETWORK_LABELS[chainId]}</NetworkCard>}
|
||||
</TestnetWrapper>
|
||||
<AccountElement active={!!account} style={{ pointerEvents: 'auto' }}>
|
||||
{account && userEthBalance ? (
|
||||
<BalanceText style={{ flexShrink: 0 }} pl="0.75rem" pr="0.5rem" fontWeight={500}>
|
||||
{userEthBalance?.toSignificant(4)} ETH
|
||||
</BalanceText>
|
||||
) : null}
|
||||
<Web3Status />
|
||||
</AccountElement>
|
||||
</HeaderElement>
|
||||
<HeaderElementWrap>
|
||||
<VersionSwitch />
|
||||
<Settings />
|
||||
<Menu />
|
||||
</div>
|
||||
</HeaderElement>
|
||||
</HeaderElementWrap>
|
||||
</HeaderControls>
|
||||
</RowBetween>
|
||||
</HeaderFrame>
|
||||
)
|
||||
|
||||
@@ -77,9 +77,7 @@ const MenuItem = styled(ExternalLink)`
|
||||
}
|
||||
`
|
||||
|
||||
const CODE_LINK = !!process.env.REACT_APP_GIT_COMMIT_HASH
|
||||
? `https://github.com/Uniswap/uniswap-frontend/tree/${process.env.REACT_APP_GIT_COMMIT_HASH}`
|
||||
: 'https://github.com/Uniswap/uniswap-frontend'
|
||||
const CODE_LINK = 'https://github.com/Uniswap/uniswap-interface'
|
||||
|
||||
export default function Menu() {
|
||||
const node = useRef<HTMLDivElement>()
|
||||
@@ -123,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>
|
||||
|
||||
@@ -18,6 +18,7 @@ const StyledDialogOverlay = styled(({ mobile, ...rest }) => <AnimatedDialogOverl
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: transparent;
|
||||
overflow: hidden;
|
||||
|
||||
${({ mobile }) =>
|
||||
mobile &&
|
||||
@@ -69,12 +70,10 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...r
|
||||
border-radius: 20px;
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
width: 65vw;
|
||||
max-height: 65vh;
|
||||
margin: 0;
|
||||
`}
|
||||
${({ theme, mobile }) => theme.mediaWidth.upToSmall`
|
||||
width: 85vw;
|
||||
max-height: 66vh;
|
||||
${mobile &&
|
||||
css`
|
||||
width: 100vw;
|
||||
@@ -86,14 +85,6 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...r
|
||||
}
|
||||
`
|
||||
|
||||
const HiddenCloseButton = styled.button`
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: none;
|
||||
`
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean
|
||||
onDismiss: () => void
|
||||
@@ -118,21 +109,13 @@ export default function Modal({
|
||||
leave: { opacity: 0 }
|
||||
})
|
||||
|
||||
const [{ xy }, set] = useSpring(() => ({ xy: [0, 0] }))
|
||||
const [{ y }, set] = useSpring(() => ({ y: 0, config: { mass: 1, tension: 210, friction: 20 } }))
|
||||
const bind = useGesture({
|
||||
onDrag: state => {
|
||||
let velocity = state.velocity
|
||||
if (velocity < 1) {
|
||||
velocity = 1
|
||||
}
|
||||
if (velocity > 8) {
|
||||
velocity = 8
|
||||
}
|
||||
set({
|
||||
xy: state.down ? state.movement : [0, 0],
|
||||
config: { mass: 1, tension: 210, friction: 20 }
|
||||
y: state.down ? state.movement[1] : 0
|
||||
})
|
||||
if (velocity > 3 && state.direction[1] > 0) {
|
||||
if (state.velocity > 3 && state.direction[1] > 0) {
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
@@ -151,6 +134,8 @@ export default function Modal({
|
||||
initialFocusRef={initialFocusRef}
|
||||
mobile={true}
|
||||
>
|
||||
{/* prevents the automatic focusing of inputs on mobile by the reach dialog */}
|
||||
{initialFocusRef ? null : <div tabIndex={1} />}
|
||||
<Spring // animation for entrance and exit
|
||||
from={{
|
||||
transform: isOpen ? 'translateY(200px)' : 'translateY(100px)'
|
||||
@@ -163,18 +148,17 @@ export default function Modal({
|
||||
<animated.div
|
||||
{...bind()}
|
||||
style={{
|
||||
transform: (xy as any).interpolate((x, y) => `translate3d(${0}px,${y > 0 ? y : 0}px,0)`)
|
||||
transform: y.interpolate(y => `translateY(${y > 0 ? y : 0}px)`)
|
||||
}}
|
||||
>
|
||||
<StyledDialogContent
|
||||
ariaLabel="test"
|
||||
aria-label="dialog content"
|
||||
style={props}
|
||||
hidden={true}
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
mobile={isMobile ?? undefined}
|
||||
mobile={isMobile}
|
||||
>
|
||||
<HiddenCloseButton onClick={onDismiss} />
|
||||
{children}
|
||||
</StyledDialogContent>
|
||||
</animated.div>
|
||||
@@ -192,8 +176,13 @@ export default function Modal({
|
||||
({ item, key, props }) =>
|
||||
item && (
|
||||
<StyledDialogOverlay key={key} style={props} onDismiss={onDismiss} initialFocusRef={initialFocusRef}>
|
||||
<StyledDialogContent hidden={true} minHeight={minHeight} maxHeight={maxHeight} isOpen={isOpen}>
|
||||
<HiddenCloseButton onClick={onDismiss} />
|
||||
<StyledDialogContent
|
||||
aria-label="dialog content"
|
||||
hidden={true}
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
{children}
|
||||
</StyledDialogContent>
|
||||
</StyledDialogOverlay>
|
||||
|
||||
@@ -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,68 @@ 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 CreatePoolTabs() {
|
||||
return (
|
||||
<Tabs>
|
||||
<RowBetween style={{ padding: '1rem' }}>
|
||||
<HistoryLink to="/pool">
|
||||
<StyledArrowLeft />
|
||||
</HistoryLink>
|
||||
<ActiveText>Create Pool</ActiveText>
|
||||
<QuestionHelper text={'Use this interface to create a new pool.'} />
|
||||
</RowBetween>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
||||
export function FindPoolTabs() {
|
||||
return (
|
||||
<Tabs>
|
||||
<RowBetween style={{ padding: '1rem' }}>
|
||||
<HistoryLink to="/pool">
|
||||
<StyledArrowLeft />
|
||||
</HistoryLink>
|
||||
<ActiveText>Import Pool</ActiveText>
|
||||
<QuestionHelper text={"Use this tool to find pairs that don't automatically appear in the interface."} />
|
||||
</RowBetween>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
||||
export function AddRemoveTabs({ adding }: { adding: boolean }) {
|
||||
return (
|
||||
<Tabs>
|
||||
<RowBetween style={{ padding: '1rem' }}>
|
||||
<HistoryLink to="/pool">
|
||||
<StyledArrowLeft />
|
||||
</HistoryLink>
|
||||
<ActiveText>{adding ? 'Add' : 'Remove'} Liquidity</ActiveText>
|
||||
<QuestionHelper
|
||||
text={
|
||||
adding
|
||||
? 'When you add liquidity, you are given pool tokens representing your position. These tokens automatically earn fees proportional to your share of the pool, and can be redeemed at any time.'
|
||||
: 'Removing pool tokens converts your position back into underlying tokens at the current rate, proportional to your share of the pool. Accrued fees are included in the amounts you receive.'
|
||||
}
|
||||
/>
|
||||
</RowBetween>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,36 +2,16 @@ import { Placement } from '@popperjs/core'
|
||||
import { transparentize } from 'polished'
|
||||
import React, { useState } from 'react'
|
||||
import { usePopper } from 'react-popper'
|
||||
import styled, { keyframes } from 'styled-components'
|
||||
import styled from 'styled-components'
|
||||
import useInterval from '../../hooks/useInterval'
|
||||
import Portal from '@reach/portal'
|
||||
|
||||
const fadeIn = keyframes`
|
||||
from {
|
||||
opacity : 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity : 1;
|
||||
}
|
||||
`
|
||||
|
||||
const fadeOut = keyframes`
|
||||
from {
|
||||
opacity : 1;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity : 0;
|
||||
}
|
||||
`
|
||||
|
||||
const PopoverContainer = styled.div<{ show: boolean }>`
|
||||
z-index: 9999;
|
||||
|
||||
visibility: ${props => (!props.show ? 'hidden' : 'visible')};
|
||||
animation: ${props => (!props.show ? fadeOut : fadeIn)} 150ms linear;
|
||||
transition: visibility 150ms linear;
|
||||
visibility: ${props => (props.show ? 'visible' : 'hidden')};
|
||||
opacity: ${props => (props.show ? 1 : 0)};
|
||||
transition: visibility 150ms linear, opacity 150ms linear;
|
||||
|
||||
background: ${({ theme }) => theme.bg2};
|
||||
border: 1px solid ${({ theme }) => theme.bg3};
|
||||
|
||||
69
src/components/PositionCard/V1.tsx
Normal file
69
src/components/PositionCard/V1.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React, { useContext } from 'react'
|
||||
import { Link, RouteComponentProps, withRouter } from 'react-router-dom'
|
||||
import { Token, TokenAmount, WETH } from '@uniswap/sdk'
|
||||
|
||||
import { Text } from 'rebass'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import { FixedHeightRow, HoverCard } from './index'
|
||||
import DoubleTokenLogo from '../DoubleLogo'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
|
||||
interface PositionCardProps extends RouteComponentProps<{}> {
|
||||
token: Token
|
||||
V1LiquidityBalance: TokenAmount
|
||||
}
|
||||
|
||||
function V1PositionCard({ token, V1LiquidityBalance }: PositionCardProps) {
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
return (
|
||||
<HoverCard>
|
||||
<AutoColumn gap="12px">
|
||||
<FixedHeightRow>
|
||||
<RowFixed>
|
||||
<DoubleTokenLogo a0={token.address} margin={true} size={20} />
|
||||
<Text fontWeight={500} fontSize={20} style={{ marginLeft: '' }}>
|
||||
{`${token.equals(WETH[chainId]) ? 'WETH' : token.symbol}/ETH`}
|
||||
</Text>
|
||||
<Text
|
||||
fontSize={12}
|
||||
fontWeight={500}
|
||||
ml="0.5rem"
|
||||
px="0.75rem"
|
||||
py="0.25rem"
|
||||
style={{ borderRadius: '1rem' }}
|
||||
backgroundColor={theme.yellow1}
|
||||
color={'black'}
|
||||
>
|
||||
V1
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</FixedHeightRow>
|
||||
|
||||
<AutoColumn gap="8px">
|
||||
<RowBetween marginTop="10px">
|
||||
<ButtonSecondary width="68%" as={Link} to={`/migrate/v1/${V1LiquidityBalance.token.address}`}>
|
||||
Migrate
|
||||
</ButtonSecondary>
|
||||
|
||||
<ButtonSecondary
|
||||
style={{ backgroundColor: 'transparent' }}
|
||||
width="28%"
|
||||
as={Link}
|
||||
to={`/remove/v1/${V1LiquidityBalance.token.address}`}
|
||||
>
|
||||
Remove
|
||||
</ButtonSecondary>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</AutoColumn>
|
||||
</HoverCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(V1PositionCard)
|
||||
@@ -1,41 +1,127 @@
|
||||
import React, { useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { darken } from 'polished'
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Percent, Pair, JSBI } from '@uniswap/sdk'
|
||||
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useTotalSupply } from '../../data/TotalSupply'
|
||||
import { currencyId } from '../../pages/AddLiquidity/currencyId'
|
||||
import { useTokenBalance } from '../../state/wallet/hooks'
|
||||
|
||||
import Card, { GreyCard } from '../Card'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import DoubleLogo from '../DoubleLogo'
|
||||
import { Text } from 'rebass'
|
||||
import { ExternalLink } from '../../theme/components'
|
||||
import { ExternalLink } from '../../theme'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { ChevronDown, ChevronUp } from 'react-feather'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
import { RowBetween, RowFixed, AutoRow } from '../Row'
|
||||
import { Dots } from '../swap/styleds'
|
||||
|
||||
const FixedHeightRow = styled(RowBetween)`
|
||||
export const FixedHeightRow = styled(RowBetween)`
|
||||
height: 24px;
|
||||
`
|
||||
|
||||
const HoverCard = styled(Card)`
|
||||
export const HoverCard = styled(Card)`
|
||||
border: 1px solid ${({ theme }) => theme.bg2};
|
||||
:hover {
|
||||
border: 1px solid ${({ theme }) => darken(0.06, theme.bg2)};
|
||||
}
|
||||
`
|
||||
|
||||
interface PositionCardProps extends RouteComponentProps<{}> {
|
||||
pair: Pair
|
||||
minimal?: boolean
|
||||
interface PositionCardProps {
|
||||
pair: Pair | undefined | null
|
||||
border?: string
|
||||
}
|
||||
|
||||
function PositionCard({ pair, history, border, minimal = false }: PositionCardProps) {
|
||||
export function MinimalPositionCard({ pair, border }: PositionCardProps) {
|
||||
const { account } = useActiveWeb3React()
|
||||
|
||||
const token0 = pair?.token0
|
||||
const token1 = pair?.token1
|
||||
|
||||
const [showMore, setShowMore] = useState(false)
|
||||
|
||||
const userPoolBalance = useTokenBalance(account, pair?.liquidityToken)
|
||||
const totalPoolTokens = useTotalSupply(pair?.liquidityToken)
|
||||
|
||||
const [token0Deposited, token1Deposited] =
|
||||
!!pair &&
|
||||
!!totalPoolTokens &&
|
||||
!!userPoolBalance &&
|
||||
// this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
|
||||
JSBI.greaterThanOrEqual(totalPoolTokens.raw, userPoolBalance.raw)
|
||||
? [
|
||||
pair.getLiquidityValue(token0, totalPoolTokens, userPoolBalance, false),
|
||||
pair.getLiquidityValue(token1, totalPoolTokens, userPoolBalance, false)
|
||||
]
|
||||
: [undefined, undefined]
|
||||
|
||||
return (
|
||||
<>
|
||||
{userPoolBalance && (
|
||||
<GreyCard border={border}>
|
||||
<AutoColumn gap="12px">
|
||||
<FixedHeightRow>
|
||||
<RowFixed>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
Your position
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</FixedHeightRow>
|
||||
<FixedHeightRow onClick={() => setShowMore(!showMore)}>
|
||||
<RowFixed>
|
||||
<DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={20} />
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{token0?.symbol}/{token1?.symbol}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
<RowFixed>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{userPoolBalance ? userPoolBalance.toSignificant(4) : '-'}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</FixedHeightRow>
|
||||
<AutoColumn gap="4px">
|
||||
<FixedHeightRow>
|
||||
<Text color="#888D9B" fontSize={16} fontWeight={500}>
|
||||
{token0?.symbol}:
|
||||
</Text>
|
||||
{token0Deposited ? (
|
||||
<RowFixed>
|
||||
<Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{token0Deposited?.toSignificant(6)}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</FixedHeightRow>
|
||||
<FixedHeightRow>
|
||||
<Text color="#888D9B" fontSize={16} fontWeight={500}>
|
||||
{token1?.symbol}:
|
||||
</Text>
|
||||
{token1Deposited ? (
|
||||
<RowFixed>
|
||||
<Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{token1Deposited?.toSignificant(6)}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</FixedHeightRow>
|
||||
</AutoColumn>
|
||||
</AutoColumn>
|
||||
</GreyCard>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function FullPositionCard({ pair, border }: PositionCardProps) {
|
||||
const { account } = useActiveWeb3React()
|
||||
|
||||
const token0 = pair?.token0
|
||||
@@ -63,174 +149,94 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
|
||||
]
|
||||
: [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>
|
||||
<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 fontWeight={500} fontSize={16}>
|
||||
Your current position
|
||||
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{token0Deposited?.toSignificant(6)}
|
||||
</Text>
|
||||
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token0?.address} />
|
||||
</RowFixed>
|
||||
</FixedHeightRow>
|
||||
<FixedHeightRow onClick={() => setShowMore(!showMore)}>
|
||||
<RowFixed>
|
||||
<DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={20} />
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{token0?.symbol}/{token1?.symbol}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
<RowFixed>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{userPoolBalance ? userPoolBalance.toSignificant(4) : '-'}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</FixedHeightRow>
|
||||
<AutoColumn gap="4px">
|
||||
<FixedHeightRow>
|
||||
<Text color="#888D9B" fontSize={16} fontWeight={500}>
|
||||
{token0?.symbol}:
|
||||
</Text>
|
||||
{token0Deposited ? (
|
||||
<RowFixed>
|
||||
{!minimal && <TokenLogo address={token0?.address} />}
|
||||
<Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{token0Deposited?.toSignificant(6)}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</FixedHeightRow>
|
||||
<FixedHeightRow>
|
||||
<Text color="#888D9B" fontSize={16} fontWeight={500}>
|
||||
{token1?.symbol}:
|
||||
</Text>
|
||||
{token1Deposited ? (
|
||||
<RowFixed>
|
||||
{!minimal && <TokenLogo address={token1?.address} />}
|
||||
<Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{token1Deposited?.toSignificant(6)}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</FixedHeightRow>
|
||||
</AutoColumn>
|
||||
</AutoColumn>
|
||||
</GreyCard>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
} else
|
||||
return (
|
||||
<HoverCard border={border}>
|
||||
<AutoColumn gap="12px">
|
||||
<FixedHeightRow onClick={() => setShowMore(!showMore)} style={{ cursor: 'pointer' }}>
|
||||
<RowFixed>
|
||||
<DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={20} />
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{token0?.symbol}/{token1?.symbol}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
<RowFixed>
|
||||
{showMore ? (
|
||||
<ChevronUp size="20" style={{ marginLeft: '10px' }} />
|
||||
) : (
|
||||
<ChevronDown size="20" style={{ marginLeft: '10px' }} />
|
||||
'-'
|
||||
)}
|
||||
</RowFixed>
|
||||
</FixedHeightRow>
|
||||
{showMore && (
|
||||
<AutoColumn gap="8px">
|
||||
<FixedHeightRow>
|
||||
<RowFixed>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Pooled {token0?.symbol}:
|
||||
</Text>
|
||||
</RowFixed>
|
||||
{token0Deposited ? (
|
||||
<RowFixed>
|
||||
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{token0Deposited?.toSignificant(6)}
|
||||
</Text>
|
||||
{!minimal && <TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token0?.address} />}
|
||||
</RowFixed>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</FixedHeightRow>
|
||||
</FixedHeightRow>
|
||||
|
||||
<FixedHeightRow>
|
||||
<FixedHeightRow>
|
||||
<RowFixed>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Pooled {token1?.symbol}:
|
||||
</Text>
|
||||
</RowFixed>
|
||||
{token1Deposited ? (
|
||||
<RowFixed>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Pooled {token1?.symbol}:
|
||||
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{token1Deposited?.toSignificant(6)}
|
||||
</Text>
|
||||
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token1?.address} />
|
||||
</RowFixed>
|
||||
{token1Deposited ? (
|
||||
<RowFixed>
|
||||
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{token1Deposited?.toSignificant(6)}
|
||||
</Text>
|
||||
{!minimal && <TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token1?.address} />}
|
||||
</RowFixed>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</FixedHeightRow>
|
||||
{!minimal && (
|
||||
<FixedHeightRow>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Your pool tokens:
|
||||
</Text>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{userPoolBalance ? userPoolBalance.toSignificant(4) : '-'}
|
||||
</Text>
|
||||
</FixedHeightRow>
|
||||
)}
|
||||
{!minimal && (
|
||||
<FixedHeightRow>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Your pool share
|
||||
</Text>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{poolTokenPercentage ? poolTokenPercentage.toFixed(2) + '%' : '-'}
|
||||
</Text>
|
||||
</FixedHeightRow>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</FixedHeightRow>
|
||||
<FixedHeightRow>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Your pool tokens:
|
||||
</Text>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{userPoolBalance ? userPoolBalance.toSignificant(4) : '-'}
|
||||
</Text>
|
||||
</FixedHeightRow>
|
||||
<FixedHeightRow>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Your pool share:
|
||||
</Text>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{poolTokenPercentage ? poolTokenPercentage.toFixed(2) + '%' : '-'}
|
||||
</Text>
|
||||
</FixedHeightRow>
|
||||
|
||||
<AutoRow justify="center" marginTop={'10px'}>
|
||||
<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(token0)}/${currencyId(token1)}`} width="48%">
|
||||
Add
|
||||
</ButtonSecondary>
|
||||
<ButtonSecondary as={Link} width="48%" to={`/remove/${token0?.address}-${token1?.address}`}>
|
||||
Remove
|
||||
</ButtonSecondary>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</HoverCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(PositionCard)
|
||||
|
||||
@@ -1,41 +1,56 @@
|
||||
import React from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import { Token } from '@uniswap/sdk'
|
||||
import { ChainId, 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'
|
||||
|
||||
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
|
||||
}: {
|
||||
chainId: number
|
||||
chainId: ChainId
|
||||
selectedTokenAddress: string
|
||||
onSelect: (tokenAddress: string) => 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">
|
||||
{(SUGGESTED_BASES[chainId as ChainId] ?? []).map((token: Token) => {
|
||||
return (
|
||||
<BaseWrapper
|
||||
gap="6px"
|
||||
onClick={() => selectedTokenAddress !== token.address && onSelect(token.address)}
|
||||
disable={selectedTokenAddress === token.address}
|
||||
key={token.address}
|
||||
>
|
||||
<TokenLogo address={token.address} />
|
||||
<TokenLogo address={token.address} style={{ marginRight: 8 }} />
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
{token.symbol}
|
||||
</Text>
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import { JSBI, Pair, TokenAmount } from '@uniswap/sdk'
|
||||
import React from 'react'
|
||||
import { FixedSizeList } from 'react-window'
|
||||
import { Text } from 'rebass'
|
||||
import { ButtonPrimary } from '../Button'
|
||||
import DoubleTokenLogo from '../DoubleLogo'
|
||||
import { RowFixed } from '../Row'
|
||||
import { MenuItem, ModalInfo } from './styleds'
|
||||
|
||||
export default function PairList({
|
||||
pairs,
|
||||
focusTokenAddress,
|
||||
pairBalances,
|
||||
onSelectPair,
|
||||
onAddLiquidity = onSelectPair
|
||||
}: {
|
||||
pairs: Pair[]
|
||||
focusTokenAddress?: string
|
||||
pairBalances: { [pairAddress: string]: TokenAmount }
|
||||
onSelectPair: (pair: Pair) => void
|
||||
onAddLiquidity: (pair: Pair) => void
|
||||
}) {
|
||||
if (pairs.length === 0) {
|
||||
return <ModalInfo>No Pools Found</ModalInfo>
|
||||
}
|
||||
|
||||
return (
|
||||
<FixedSizeList
|
||||
itemSize={54}
|
||||
height={500}
|
||||
itemCount={pairs.length}
|
||||
width="100%"
|
||||
style={{ flex: '1', minHeight: 200 }}
|
||||
>
|
||||
{({ index, style }) => {
|
||||
const pair = pairs[index]
|
||||
|
||||
// the focused token is shown first
|
||||
const tokenA = focusTokenAddress === pair.token1.address ? pair.token1 : pair.token0
|
||||
const tokenB = tokenA === pair.token0 ? pair.token1 : pair.token0
|
||||
|
||||
const pairAddress = pair.liquidityToken.address
|
||||
const balance = pairBalances[pairAddress]?.toSignificant(6)
|
||||
const zeroBalance = pairBalances[pairAddress]?.raw && JSBI.equal(pairBalances[pairAddress].raw, JSBI.BigInt(0))
|
||||
|
||||
const selectPair = () => onSelectPair(pair)
|
||||
const addLiquidity = () => onAddLiquidity(pair)
|
||||
|
||||
return (
|
||||
<MenuItem style={style} onClick={selectPair}>
|
||||
<RowFixed>
|
||||
<DoubleTokenLogo a0={tokenA.address} a1={tokenB.address} size={24} margin={true} />
|
||||
<Text fontWeight={500} fontSize={16}>{`${tokenA.symbol}/${tokenB.symbol}`}</Text>
|
||||
</RowFixed>
|
||||
|
||||
<ButtonPrimary padding={'6px 8px'} width={'fit-content'} borderRadius={'12px'} onClick={addLiquidity}>
|
||||
{balance ? (zeroBalance ? 'Join' : 'Add Liquidity') : 'Join'}
|
||||
</ButtonPrimary>
|
||||
</MenuItem>
|
||||
)
|
||||
}}
|
||||
</FixedSizeList>
|
||||
)
|
||||
}
|
||||
@@ -1,24 +1,20 @@
|
||||
import { ChainId, JSBI, Token, TokenAmount } from '@uniswap/sdk'
|
||||
import React, { useContext } from 'react'
|
||||
import { JSBI, Token, TokenAmount } from '@uniswap/sdk'
|
||||
import React, { CSSProperties, memo, useContext, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FixedSizeList } from 'react-window'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { ALL_TOKENS } from '../../constants/tokens'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens } from '../../hooks/Tokens'
|
||||
import { useAddUserToken, useRemoveUserAddedToken } from '../../state/user/hooks'
|
||||
import { LinkStyledButton, TYPE } from '../../theme'
|
||||
import { isAddress } from '../../utils'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
import Column, { AutoColumn } from '../Column'
|
||||
import { RowFixed } from '../Row'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import { FadedSpan, GreySpan, MenuItem, ModalInfo } from './styleds'
|
||||
import Loader from '../Loader'
|
||||
|
||||
function isDefaultToken(tokenAddress: string, chainId?: number): boolean {
|
||||
const address = isAddress(tokenAddress)
|
||||
return Boolean(chainId && address && ALL_TOKENS[chainId as ChainId]?.[tokenAddress])
|
||||
}
|
||||
import { isDefaultToken, isCustomAddedToken } from '../../utils'
|
||||
|
||||
export default function TokenList({
|
||||
tokens,
|
||||
@@ -27,99 +23,134 @@ export default function TokenList({
|
||||
onTokenSelect,
|
||||
otherToken,
|
||||
showSendWithSwap,
|
||||
onRemoveAddedToken,
|
||||
otherSelectedText,
|
||||
hideRemove
|
||||
otherSelectedText
|
||||
}: {
|
||||
tokens: Token[]
|
||||
selectedToken: string
|
||||
allTokenBalances: { [tokenAddress: string]: TokenAmount }
|
||||
onTokenSelect: (tokenAddress: string) => void
|
||||
onRemoveAddedToken: (chainId: number, tokenAddress: string) => void
|
||||
otherToken: string
|
||||
showSendWithSwap?: boolean
|
||||
otherSelectedText: string
|
||||
hideRemove?: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
const allTokens = useAllTokens()
|
||||
const addToken = useAddUserToken()
|
||||
const removeToken = useRemoveUserAddedToken()
|
||||
|
||||
const TokenRow = useMemo(() => {
|
||||
return memo(function TokenRow({ index, style }: { index: number; style: CSSProperties }) {
|
||||
const token = tokens[index]
|
||||
const { address, symbol } = token
|
||||
|
||||
const isDefault = isDefaultToken(token)
|
||||
const customAdded = isCustomAddedToken(allTokens, token)
|
||||
const balance = allTokenBalances[address]
|
||||
|
||||
const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw)
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
style={style}
|
||||
key={address}
|
||||
className={`token-item-${address}`}
|
||||
onClick={() => (selectedToken && selectedToken === address ? null : onTokenSelect(address))}
|
||||
disabled={selectedToken && selectedToken === address}
|
||||
selected={otherToken === address}
|
||||
>
|
||||
<RowFixed>
|
||||
<TokenLogo address={address} size={'24px'} style={{ marginRight: '14px' }} />
|
||||
<Column>
|
||||
<Text fontWeight={500}>
|
||||
{symbol}
|
||||
{otherToken === address && <GreySpan> ({otherSelectedText})</GreySpan>}
|
||||
</Text>
|
||||
<FadedSpan>
|
||||
{customAdded ? (
|
||||
<TYPE.main fontWeight={500}>
|
||||
Added by user
|
||||
<LinkStyledButton
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
removeToken(chainId, address)
|
||||
}}
|
||||
>
|
||||
(Remove)
|
||||
</LinkStyledButton>
|
||||
</TYPE.main>
|
||||
) : null}
|
||||
{!isDefault && !customAdded ? (
|
||||
<TYPE.main fontWeight={500}>
|
||||
Found by address
|
||||
<LinkStyledButton
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
addToken(token)
|
||||
}}
|
||||
>
|
||||
(Add)
|
||||
</LinkStyledButton>
|
||||
</TYPE.main>
|
||||
) : null}
|
||||
</FadedSpan>
|
||||
</Column>
|
||||
</RowFixed>
|
||||
<AutoColumn>
|
||||
{balance ? (
|
||||
<Text>
|
||||
{zeroBalance && showSendWithSwap ? (
|
||||
<ButtonSecondary padding={'4px 8px'}>
|
||||
<Text textAlign="center" fontWeight={500} fontSize={14} color={theme.primary1}>
|
||||
Send With Swap
|
||||
</Text>
|
||||
</ButtonSecondary>
|
||||
) : balance ? (
|
||||
balance.toSignificant(6)
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</Text>
|
||||
) : account ? (
|
||||
<Loader />
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</AutoColumn>
|
||||
</MenuItem>
|
||||
)
|
||||
})
|
||||
}, [
|
||||
account,
|
||||
addToken,
|
||||
allTokenBalances,
|
||||
allTokens,
|
||||
chainId,
|
||||
onTokenSelect,
|
||||
otherSelectedText,
|
||||
otherToken,
|
||||
removeToken,
|
||||
selectedToken,
|
||||
showSendWithSwap,
|
||||
theme.primary1,
|
||||
tokens
|
||||
])
|
||||
|
||||
if (tokens.length === 0) {
|
||||
return <ModalInfo>{t('noToken')}</ModalInfo>
|
||||
}
|
||||
|
||||
return (
|
||||
<FixedSizeList
|
||||
width="100%"
|
||||
height={500}
|
||||
itemCount={tokens.length}
|
||||
itemSize={50}
|
||||
style={{ flex: '1', minHeight: 200 }}
|
||||
itemSize={56}
|
||||
style={{ flex: '1' }}
|
||||
itemKey={index => tokens[index].address}
|
||||
>
|
||||
{({ index, style }) => {
|
||||
const { address, symbol } = tokens[index]
|
||||
|
||||
const customAdded = !isDefaultToken(address, chainId)
|
||||
const balance = allTokenBalances[address]
|
||||
|
||||
const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw)
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
style={style}
|
||||
key={address}
|
||||
className={`token-item-${address}`}
|
||||
onClick={() => (selectedToken && selectedToken === address ? null : onTokenSelect(address))}
|
||||
disabled={selectedToken && selectedToken === address}
|
||||
selected={otherToken === address}
|
||||
>
|
||||
<RowFixed>
|
||||
<TokenLogo address={address} size={'24px'} style={{ marginRight: '14px' }} />
|
||||
<Column>
|
||||
<Text fontWeight={500}>
|
||||
{symbol}
|
||||
{otherToken === address && <GreySpan> ({otherSelectedText})</GreySpan>}
|
||||
</Text>
|
||||
<FadedSpan>
|
||||
<TYPE.main fontWeight={500}>{customAdded && 'Added by user'}</TYPE.main>
|
||||
{customAdded && !hideRemove && (
|
||||
<LinkStyledButton
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
onRemoveAddedToken(chainId, address)
|
||||
}}
|
||||
style={{ marginLeft: '4px', fontWeight: 400 }}
|
||||
>
|
||||
(Remove)
|
||||
</LinkStyledButton>
|
||||
)}
|
||||
</FadedSpan>
|
||||
</Column>
|
||||
</RowFixed>
|
||||
<AutoColumn gap="4px" justify="end">
|
||||
{balance ? (
|
||||
<Text>
|
||||
{zeroBalance && showSendWithSwap ? (
|
||||
<ButtonSecondary padding={'4px 8px'}>
|
||||
<Text textAlign="center" fontWeight={500} fontSize={14} color={theme.primary1}>
|
||||
Send With Swap
|
||||
</Text>
|
||||
</ButtonSecondary>
|
||||
) : balance ? (
|
||||
balance.toSignificant(6)
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</Text>
|
||||
) : account ? (
|
||||
<Loader />
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</AutoColumn>
|
||||
</MenuItem>
|
||||
)
|
||||
}}
|
||||
{TokenRow}
|
||||
</FixedSizeList>
|
||||
)
|
||||
}
|
||||
|
||||
209
src/components/SearchModal/TokenSearchModal.tsx
Normal file
209
src/components/SearchModal/TokenSearchModal.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
import { Token } from '@uniswap/sdk'
|
||||
import React, { KeyboardEvent, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import Card from '../../components/Card'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens, useToken } from '../../hooks/Tokens'
|
||||
import useInterval from '../../hooks/useInterval'
|
||||
import { useAllTokenBalancesTreatingWETHasETH, useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||
import { CloseIcon, LinkStyledButton } from '../../theme'
|
||||
import { isAddress } from '../../utils'
|
||||
import Column from '../Column'
|
||||
import Modal from '../Modal'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { AutoRow, RowBetween } from '../Row'
|
||||
import Tooltip from '../Tooltip'
|
||||
import CommonBases from './CommonBases'
|
||||
import { filterTokens } from './filtering'
|
||||
import { useTokenComparator } from './sorting'
|
||||
import { PaddedColumn, SearchInput } from './styleds'
|
||||
import TokenList from './TokenList'
|
||||
import SortButton from './SortButton'
|
||||
|
||||
interface TokenSearchModalProps {
|
||||
isOpen?: boolean
|
||||
onDismiss?: () => void
|
||||
hiddenToken?: string
|
||||
showSendWithSwap?: boolean
|
||||
onTokenSelect?: (address: string) => void
|
||||
otherSelectedTokenAddress?: string
|
||||
otherSelectedText?: string
|
||||
showCommonBases?: boolean
|
||||
}
|
||||
|
||||
export default function TokenSearchModal({
|
||||
isOpen,
|
||||
onDismiss,
|
||||
onTokenSelect,
|
||||
hiddenToken,
|
||||
showSendWithSwap,
|
||||
otherSelectedTokenAddress,
|
||||
otherSelectedText,
|
||||
showCommonBases = false
|
||||
}: TokenSearchModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||
const [tooltipOpen, setTooltipOpen] = useState<boolean>(false)
|
||||
const [invertSearchOrder, setInvertSearchOrder] = useState<boolean>(false)
|
||||
const allTokens = useAllTokens()
|
||||
|
||||
// if the current input is an address, and we don't have the token in context, try to fetch it and import
|
||||
const searchToken = useToken(searchQuery)
|
||||
const searchTokenBalance = useTokenBalanceTreatingWETHasETH(account, searchToken)
|
||||
const allTokenBalances_ = useAllTokenBalancesTreatingWETHasETH()
|
||||
const allTokenBalances = searchToken
|
||||
? {
|
||||
[searchToken.address]: searchTokenBalance
|
||||
}
|
||||
: allTokenBalances_ ?? {}
|
||||
|
||||
const tokenComparator = useTokenComparator(invertSearchOrder)
|
||||
|
||||
const filteredTokens: Token[] = useMemo(() => {
|
||||
if (searchToken) return [searchToken]
|
||||
return filterTokens(Object.values(allTokens), searchQuery)
|
||||
}, [searchToken, allTokens, searchQuery])
|
||||
|
||||
const filteredSortedTokens: Token[] = useMemo(() => {
|
||||
if (searchToken) return [searchToken]
|
||||
const sorted = filteredTokens.sort(tokenComparator)
|
||||
const symbolMatch = searchQuery
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(s => s.length > 0)
|
||||
if (symbolMatch.length > 1) return sorted
|
||||
|
||||
return [
|
||||
...(searchToken ? [searchToken] : []),
|
||||
// sort any exact symbol matches first
|
||||
...sorted.filter(token => token.symbol.toLowerCase() === symbolMatch[0]),
|
||||
...sorted.filter(token => token.symbol.toLowerCase() !== symbolMatch[0])
|
||||
]
|
||||
}, [filteredTokens, searchQuery, searchToken, tokenComparator])
|
||||
|
||||
const handleTokenSelect = useCallback(
|
||||
(address: string) => {
|
||||
onTokenSelect(address)
|
||||
onDismiss()
|
||||
},
|
||||
[onDismiss, onTokenSelect]
|
||||
)
|
||||
|
||||
// clear the input on open
|
||||
useEffect(() => {
|
||||
if (isOpen) setSearchQuery('')
|
||||
}, [isOpen, setSearchQuery])
|
||||
|
||||
// manage focus on modal show
|
||||
const inputRef = useRef<HTMLInputElement>()
|
||||
const handleInput = useCallback(event => {
|
||||
const input = event.target.value
|
||||
const checksummedInput = isAddress(input)
|
||||
setSearchQuery(checksummedInput || input)
|
||||
setTooltipOpen(false)
|
||||
}, [])
|
||||
|
||||
const openTooltip = useCallback(() => {
|
||||
setTooltipOpen(true)
|
||||
}, [setTooltipOpen])
|
||||
const closeTooltip = useCallback(() => setTooltipOpen(false), [setTooltipOpen])
|
||||
|
||||
useInterval(
|
||||
() => {
|
||||
setTooltipOpen(false)
|
||||
},
|
||||
tooltipOpen ? 4000 : null,
|
||||
false
|
||||
)
|
||||
|
||||
const handleEnter = useCallback(
|
||||
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter' && filteredSortedTokens.length > 0) {
|
||||
if (
|
||||
filteredSortedTokens[0].symbol.toLowerCase() === searchQuery.trim().toLowerCase() ||
|
||||
filteredSortedTokens.length === 1
|
||||
) {
|
||||
handleTokenSelect(filteredSortedTokens[0].address)
|
||||
}
|
||||
}
|
||||
},
|
||||
[filteredSortedTokens, handleTokenSelect, searchQuery]
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onDismiss={onDismiss}
|
||||
maxHeight={70}
|
||||
initialFocusRef={isMobile ? undefined : inputRef}
|
||||
minHeight={70}
|
||||
>
|
||||
<Column style={{ width: '100%' }}>
|
||||
<PaddedColumn gap="14px">
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
Select a token
|
||||
<QuestionHelper
|
||||
disabled={tooltipOpen}
|
||||
text="Find a token by searching for its name or symbol or by pasting its address below."
|
||||
/>
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
<Tooltip
|
||||
text="Import any token into your list by pasting the token address into the search field."
|
||||
show={tooltipOpen}
|
||||
placement="bottom"
|
||||
>
|
||||
<SearchInput
|
||||
type="text"
|
||||
id="token-search-input"
|
||||
placeholder={t('tokenSearchPlaceholder')}
|
||||
value={searchQuery}
|
||||
ref={inputRef}
|
||||
onChange={handleInput}
|
||||
onFocus={closeTooltip}
|
||||
onBlur={closeTooltip}
|
||||
onKeyDown={handleEnter}
|
||||
/>
|
||||
</Tooltip>
|
||||
{showCommonBases && (
|
||||
<CommonBases chainId={chainId} onSelect={handleTokenSelect} selectedTokenAddress={hiddenToken} />
|
||||
)}
|
||||
<RowBetween>
|
||||
<Text fontSize={14} fontWeight={500}>
|
||||
Token Name
|
||||
</Text>
|
||||
<SortButton ascending={invertSearchOrder} toggleSortOrder={() => setInvertSearchOrder(iso => !iso)} />
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
||||
<TokenList
|
||||
tokens={filteredSortedTokens}
|
||||
allTokenBalances={allTokenBalances}
|
||||
onTokenSelect={handleTokenSelect}
|
||||
otherSelectedText={otherSelectedText}
|
||||
otherToken={otherSelectedTokenAddress}
|
||||
selectedToken={hiddenToken}
|
||||
showSendWithSwap={showSendWithSwap}
|
||||
/>
|
||||
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
||||
<Card>
|
||||
<AutoRow justify={'center'}>
|
||||
<div>
|
||||
<LinkStyledButton style={{ fontWeight: 500, color: theme.text2, fontSize: 16 }} onClick={openTooltip}>
|
||||
Having trouble finding a token?
|
||||
</LinkStyledButton>
|
||||
</div>
|
||||
</AutoRow>
|
||||
</Card>
|
||||
</Column>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
import { Pair, Token } from '@uniswap/sdk'
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import Card from '../../components/Card'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens, useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
|
||||
import { useAllDummyPairs, useRemoveUserAddedToken } from '../../state/user/hooks'
|
||||
import { useAllTokenBalancesTreatingWETHasETH, useTokenBalances } from '../../state/wallet/hooks'
|
||||
import { CloseIcon, LinkStyledButton, StyledInternalLink } from '../../theme/components'
|
||||
import { isAddress } from '../../utils'
|
||||
import Column from '../Column'
|
||||
import Modal from '../Modal'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { AutoRow, RowBetween } from '../Row'
|
||||
import Tooltip from '../Tooltip'
|
||||
import CommonBases from './CommonBases'
|
||||
import { filterPairs, filterTokens } from './filtering'
|
||||
import PairList from './PairList'
|
||||
import { useTokenComparator, pairComparator } from './sorting'
|
||||
import { PaddedColumn, SearchInput } from './styleds'
|
||||
import TokenList from './TokenList'
|
||||
import SortButton from './SortButton'
|
||||
|
||||
interface SearchModalProps extends RouteComponentProps {
|
||||
isOpen?: boolean
|
||||
onDismiss?: () => void
|
||||
filterType?: 'tokens'
|
||||
hiddenToken?: string
|
||||
showSendWithSwap?: boolean
|
||||
onTokenSelect?: (address: string) => void
|
||||
otherSelectedTokenAddress?: string
|
||||
otherSelectedText?: string
|
||||
showCommonBases?: boolean
|
||||
}
|
||||
|
||||
function SearchModal({
|
||||
history,
|
||||
isOpen,
|
||||
onDismiss,
|
||||
onTokenSelect,
|
||||
filterType,
|
||||
hiddenToken,
|
||||
showSendWithSwap,
|
||||
otherSelectedTokenAddress,
|
||||
otherSelectedText,
|
||||
showCommonBases = false
|
||||
}: SearchModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const isTokenView = filterType === 'tokens'
|
||||
|
||||
const allTokens = useAllTokens()
|
||||
const allPairs = useAllDummyPairs()
|
||||
const allTokenBalances = useAllTokenBalancesTreatingWETHasETH() ?? {}
|
||||
const allPairBalances = useTokenBalances(
|
||||
account,
|
||||
allPairs.map(p => p.liquidityToken)
|
||||
)
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||
const [tooltipOpen, setTooltipOpen] = useState<boolean>(false)
|
||||
const [invertSearchOrder, setInvertSearchOrder] = useState<boolean>(false)
|
||||
|
||||
const removeTokenByAddress = useRemoveUserAddedToken()
|
||||
|
||||
// if the current input is an address, and we don't have the token in context, try to fetch it and import
|
||||
useTokenByAddressAndAutomaticallyAdd(searchQuery)
|
||||
|
||||
const tokenComparator = useTokenComparator(invertSearchOrder)
|
||||
|
||||
const filteredTokens: Token[] = useMemo(() => {
|
||||
if (!isTokenView) return []
|
||||
return filterTokens(Object.values(allTokens), searchQuery)
|
||||
}, [isTokenView, allTokens, searchQuery])
|
||||
|
||||
const filteredSortedTokens: Token[] = useMemo(() => {
|
||||
if (!isTokenView) return []
|
||||
const sorted = filteredTokens.sort(tokenComparator)
|
||||
const symbolMatch = searchQuery
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(s => s.length > 0)
|
||||
if (symbolMatch.length > 1) return sorted
|
||||
|
||||
return [
|
||||
// sort any exact symbol matches first
|
||||
...sorted.filter(token => token.symbol.toLowerCase() === symbolMatch[0]),
|
||||
...sorted.filter(token => token.symbol.toLowerCase() !== symbolMatch[0])
|
||||
]
|
||||
}, [filteredTokens, isTokenView, searchQuery, tokenComparator])
|
||||
|
||||
function _onTokenSelect(address: string) {
|
||||
onTokenSelect(address)
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
// clear the input on open
|
||||
useEffect(() => {
|
||||
if (isOpen) setSearchQuery('')
|
||||
}, [isOpen, setSearchQuery])
|
||||
|
||||
// manage focus on modal show
|
||||
const inputRef = useRef<HTMLInputElement>()
|
||||
function onInput(event) {
|
||||
const input = event.target.value
|
||||
const checksummedInput = isAddress(input)
|
||||
setSearchQuery(checksummedInput || input)
|
||||
}
|
||||
|
||||
const sortedPairList = useMemo(() => {
|
||||
if (isTokenView) return []
|
||||
return allPairs.sort((a, b): number => {
|
||||
const balanceA = allPairBalances[a.liquidityToken.address]
|
||||
const balanceB = allPairBalances[b.liquidityToken.address]
|
||||
return pairComparator(a, b, balanceA, balanceB)
|
||||
})
|
||||
}, [isTokenView, allPairs, allPairBalances])
|
||||
|
||||
const filteredPairs = useMemo(() => {
|
||||
if (isTokenView) return []
|
||||
return filterPairs(sortedPairList, searchQuery)
|
||||
}, [isTokenView, searchQuery, sortedPairList])
|
||||
|
||||
const selectPair = useCallback(
|
||||
(pair: Pair) => {
|
||||
history.push(`/add/${pair.token0.address}-${pair.token1.address}`)
|
||||
},
|
||||
[history]
|
||||
)
|
||||
|
||||
const focusedToken = Object.values(allTokens ?? {}).filter(token => {
|
||||
return token.symbol.toLowerCase() === searchQuery || searchQuery === token.address
|
||||
})[0]
|
||||
|
||||
const openTooltip = useCallback(() => {
|
||||
setTooltipOpen(true)
|
||||
inputRef.current?.focus()
|
||||
}, [setTooltipOpen])
|
||||
const closeTooltip = useCallback(() => setTooltipOpen(false), [setTooltipOpen])
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={70} initialFocusRef={isMobile ? undefined : inputRef}>
|
||||
<Column style={{ width: '100%' }}>
|
||||
<PaddedColumn gap="20px">
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
{isTokenView ? 'Select a token' : 'Select a pool'}
|
||||
<QuestionHelper
|
||||
disabled={tooltipOpen}
|
||||
text={
|
||||
isTokenView
|
||||
? 'Find a token by searching for its name or symbol or by pasting its address below.'
|
||||
: 'Find a pair by searching for its name below.'
|
||||
}
|
||||
/>
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
<Tooltip
|
||||
text="Import any token into your list by pasting the token address into the search field."
|
||||
show={tooltipOpen}
|
||||
placement="bottom"
|
||||
>
|
||||
<SearchInput
|
||||
type="text"
|
||||
id="token-search-input"
|
||||
placeholder={t('tokenSearchPlaceholder')}
|
||||
value={searchQuery}
|
||||
ref={inputRef}
|
||||
onChange={onInput}
|
||||
onBlur={closeTooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
{showCommonBases && (
|
||||
<CommonBases chainId={chainId} onSelect={_onTokenSelect} selectedTokenAddress={hiddenToken} />
|
||||
)}
|
||||
<RowBetween>
|
||||
<Text fontSize={14} fontWeight={500}>
|
||||
{isTokenView ? 'Token Name' : 'Pool Name'}
|
||||
</Text>
|
||||
{isTokenView && (
|
||||
<SortButton ascending={invertSearchOrder} toggleSortOrder={() => setInvertSearchOrder(iso => !iso)} />
|
||||
)}
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
||||
{isTokenView ? (
|
||||
<TokenList
|
||||
tokens={filteredSortedTokens}
|
||||
allTokenBalances={allTokenBalances}
|
||||
onRemoveAddedToken={removeTokenByAddress}
|
||||
onTokenSelect={_onTokenSelect}
|
||||
otherSelectedText={otherSelectedText}
|
||||
otherToken={otherSelectedTokenAddress}
|
||||
selectedToken={hiddenToken}
|
||||
showSendWithSwap={showSendWithSwap}
|
||||
hideRemove={Boolean(isAddress(searchQuery))}
|
||||
/>
|
||||
) : (
|
||||
<PairList
|
||||
pairs={filteredPairs}
|
||||
focusTokenAddress={focusedToken?.address}
|
||||
onAddLiquidity={selectPair}
|
||||
onSelectPair={selectPair}
|
||||
pairBalances={allPairBalances}
|
||||
/>
|
||||
)}
|
||||
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
||||
<Card>
|
||||
<AutoRow justify={'center'}>
|
||||
<div>
|
||||
{isTokenView ? (
|
||||
<LinkStyledButton style={{ fontWeight: 500, color: theme.text2, fontSize: 16 }} onClick={openTooltip}>
|
||||
Having trouble finding a token?
|
||||
</LinkStyledButton>
|
||||
) : (
|
||||
<Text fontWeight={500}>
|
||||
{!isMobile && "Don't see a pool? "}
|
||||
<StyledInternalLink to="/find">{!isMobile ? 'Import it.' : 'Import pool.'}</StyledInternalLink>
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</AutoRow>
|
||||
</Card>
|
||||
</Column>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(SearchModal)
|
||||
@@ -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}
|
||||
@@ -8,8 +8,8 @@ export const ModalInfo = styled.div`
|
||||
padding: 1rem 1rem;
|
||||
margin: 0.25rem 0.5rem;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
user-select: none;
|
||||
min-height: 200px;
|
||||
`
|
||||
|
||||
export const FadedSpan = styled(RowFixed)`
|
||||
@@ -50,12 +50,9 @@ export const PaddedColumn = styled(AutoColumn)`
|
||||
padding-bottom: 12px;
|
||||
`
|
||||
|
||||
const PaddedItem = styled(RowBetween)`
|
||||
export const MenuItem = styled(RowBetween)`
|
||||
padding: 4px 20px;
|
||||
height: 56px;
|
||||
`
|
||||
|
||||
export const MenuItem = styled(PaddedItem)`
|
||||
cursor: ${({ disabled }) => !disabled && 'pointer'};
|
||||
pointer-events: ${({ disabled }) => disabled && 'none'};
|
||||
:hover {
|
||||
@@ -64,21 +61,6 @@ export const MenuItem = styled(PaddedItem)`
|
||||
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 {
|
||||
|
||||
256
src/components/Settings/index.tsx
Normal file
256
src/components/Settings/index.tsx
Normal file
@@ -0,0 +1,256 @@
|
||||
import React, { useRef, useEffect, useContext, useState } from 'react'
|
||||
import { Settings, X } from 'react-feather'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import {
|
||||
useUserSlippageTolerance,
|
||||
useExpertModeManager,
|
||||
useUserDeadline,
|
||||
useDarkModeManager
|
||||
} from '../../state/user/hooks'
|
||||
import SlippageTabs from '../SlippageTabs'
|
||||
import { RowFixed, RowBetween } from '../Row'
|
||||
import { TYPE } from '../../theme'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import Toggle from '../Toggle'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { ButtonError } from '../Button'
|
||||
import { useSettingsMenuOpen, useToggleSettingsMenu } from '../../state/application/hooks'
|
||||
import { Text } from 'rebass'
|
||||
import Modal from '../Modal'
|
||||
|
||||
const StyledMenuIcon = styled(Settings)`
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
|
||||
> * {
|
||||
stroke: ${({ theme }) => theme.text1};
|
||||
}
|
||||
`
|
||||
|
||||
const StyledCloseIcon = styled(X)`
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
> * {
|
||||
stroke: ${({ theme }) => theme.text1};
|
||||
}
|
||||
`
|
||||
|
||||
const StyledMenuButton = styled.button`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 35px;
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
:hover,
|
||||
:focus {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
background-color: ${({ theme }) => theme.bg4};
|
||||
}
|
||||
|
||||
svg {
|
||||
margin-top: 2px;
|
||||
}
|
||||
`
|
||||
const EmojiWrapper = styled.div`
|
||||
position: absolute;
|
||||
bottom: -6px;
|
||||
right: 0px;
|
||||
font-size: 14px;
|
||||
`
|
||||
|
||||
const StyledMenu = styled.div`
|
||||
margin-left: 0.5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
border: none;
|
||||
text-align: left;
|
||||
`
|
||||
|
||||
const MenuFlyout = styled.span`
|
||||
min-width: 20.125rem;
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
|
||||
0px 24px 32px rgba(0, 0, 0, 0.01);
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 1rem;
|
||||
position: absolute;
|
||||
top: 3rem;
|
||||
right: 0rem;
|
||||
z-index: 100;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||
min-width: 18.125rem;
|
||||
right: -46px;
|
||||
`};
|
||||
`
|
||||
|
||||
const Break = styled.div`
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
`
|
||||
|
||||
const ModalContentWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem 0;
|
||||
background-color: ${({ theme }) => theme.bg2};
|
||||
border-radius: 20px;
|
||||
`
|
||||
|
||||
export default function SettingsTab() {
|
||||
const node = useRef<HTMLDivElement>()
|
||||
const open = useSettingsMenuOpen()
|
||||
const toggle = useToggleSettingsMenu()
|
||||
|
||||
const theme = useContext(ThemeContext)
|
||||
const [userSlippageTolerance, setUserslippageTolerance] = useUserSlippageTolerance()
|
||||
|
||||
const [deadline, setDeadline] = useUserDeadline()
|
||||
|
||||
const [expertMode, toggleExpertMode] = useExpertModeManager()
|
||||
|
||||
const [darkMode, toggleDarkMode] = useDarkModeManager()
|
||||
|
||||
// show confirmation view before turning on
|
||||
const [showConfirmation, setShowConfirmation] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = e => {
|
||||
if (node.current?.contains(e.target) ?? false) {
|
||||
return
|
||||
}
|
||||
toggle()
|
||||
}
|
||||
|
||||
if (open) {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
} else {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [open, toggle])
|
||||
|
||||
return (
|
||||
<StyledMenu ref={node}>
|
||||
<Modal isOpen={showConfirmation} onDismiss={() => setShowConfirmation(false)} maxHeight={100}>
|
||||
<ModalContentWrapper>
|
||||
<AutoColumn gap="lg">
|
||||
<RowBetween style={{ padding: '0 2rem' }}>
|
||||
<div />
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Are you sure?
|
||||
</Text>
|
||||
<StyledCloseIcon onClick={() => setShowConfirmation(false)} />
|
||||
</RowBetween>
|
||||
<Break />
|
||||
<AutoColumn gap="lg" style={{ padding: '0 2rem' }}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Expert mode turns off the confirm transaction prompt and allows high slippage trades that often result
|
||||
in bad rates and lost funds.
|
||||
</Text>
|
||||
<Text fontWeight={600} fontSize={20}>
|
||||
ONLY USE THIS MODE IF YOU KNOW WHAT YOU ARE DOING.
|
||||
</Text>
|
||||
<ButtonError
|
||||
error={true}
|
||||
padding={'12px'}
|
||||
onClick={() => {
|
||||
if (window.prompt(`Please type the word "confirm" to enable expert mode.`) === 'confirm') {
|
||||
toggleExpertMode()
|
||||
setShowConfirmation(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
Turn On Expert Mode
|
||||
</Text>
|
||||
</ButtonError>
|
||||
</AutoColumn>
|
||||
</AutoColumn>
|
||||
</ModalContentWrapper>
|
||||
</Modal>
|
||||
<StyledMenuButton onClick={toggle}>
|
||||
<StyledMenuIcon />
|
||||
{expertMode && (
|
||||
<EmojiWrapper>
|
||||
<span role="img" aria-label="wizard-icon">
|
||||
🧙
|
||||
</span>
|
||||
</EmojiWrapper>
|
||||
)}
|
||||
</StyledMenuButton>
|
||||
{open && (
|
||||
<MenuFlyout>
|
||||
<AutoColumn gap="md" style={{ padding: '1rem' }}>
|
||||
<Text fontWeight={600} fontSize={14}>
|
||||
Transaction Settings
|
||||
</Text>
|
||||
<SlippageTabs
|
||||
rawSlippage={userSlippageTolerance}
|
||||
setRawSlippage={setUserslippageTolerance}
|
||||
deadline={deadline}
|
||||
setDeadline={setDeadline}
|
||||
/>
|
||||
<Text fontWeight={600} fontSize={14}>
|
||||
Interface Settings
|
||||
</Text>
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
|
||||
Toggle Expert Mode
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Bypasses confirmation modals and allows high slippage trades. Use at your own risk." />
|
||||
</RowFixed>
|
||||
<Toggle
|
||||
isActive={expertMode}
|
||||
toggle={
|
||||
expertMode
|
||||
? () => {
|
||||
toggleExpertMode()
|
||||
setShowConfirmation(false)
|
||||
}
|
||||
: () => {
|
||||
toggle()
|
||||
setShowConfirmation(true)
|
||||
}
|
||||
}
|
||||
/>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
|
||||
Toggle Dark Mode
|
||||
</TYPE.black>
|
||||
</RowFixed>
|
||||
<Toggle isActive={darkMode} toggle={toggleDarkMode} />
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</MenuFlyout>
|
||||
)}
|
||||
</StyledMenu>
|
||||
)
|
||||
}
|
||||
@@ -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,8 +78,11 @@ const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean }
|
||||
}
|
||||
`
|
||||
|
||||
const SlippageSelector = styled.div`
|
||||
padding: 0 20px;
|
||||
const SlippageEmojiContainer = styled.span`
|
||||
color: #f3841e;
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
display: none;
|
||||
`}
|
||||
`
|
||||
|
||||
export interface SlippageTabsProps {
|
||||
@@ -146,15 +149,14 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<RowFixed padding={'0 20px'}>
|
||||
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
|
||||
Set slippage tolerance
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Your transaction will revert if the price changes unfavorably by more than this percentage." />
|
||||
</RowFixed>
|
||||
|
||||
<SlippageSelector>
|
||||
<AutoColumn gap="md">
|
||||
<AutoColumn gap="sm">
|
||||
<RowFixed>
|
||||
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
|
||||
Slippage tolerance
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Your transaction will revert if the price changes unfavorably by more than this percentage." />
|
||||
</RowFixed>
|
||||
<RowBetween>
|
||||
<Option
|
||||
onClick={() => {
|
||||
@@ -187,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}
|
||||
@@ -220,16 +224,16 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
|
||||
: 'Your transaction may be frontrun'}
|
||||
</RowBetween>
|
||||
)}
|
||||
</SlippageSelector>
|
||||
</AutoColumn>
|
||||
|
||||
<AutoColumn gap="sm">
|
||||
<RowFixed padding={'0 20px'}>
|
||||
<RowFixed>
|
||||
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
|
||||
Deadline
|
||||
Transaction deadline
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Your transaction will revert if it is pending for more than this long." />
|
||||
</RowFixed>
|
||||
<RowFixed padding={'0 20px'}>
|
||||
<RowFixed>
|
||||
<OptionCustom style={{ width: '80px' }} tabIndex={-1}>
|
||||
<Input
|
||||
color={!!deadlineError ? 'red' : undefined}
|
||||
@@ -246,6 +250,6 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
|
||||
</TYPE.body>
|
||||
</RowFixed>
|
||||
</AutoColumn>
|
||||
</>
|
||||
</AutoColumn>
|
||||
)
|
||||
}
|
||||
|
||||
41
src/components/Toggle/index.tsx
Normal file
41
src/components/Toggle/index.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const ToggleElement = styled.span<{ isActive?: boolean; isOnSwitch?: boolean }>`
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 14px;
|
||||
background: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.primary1 : theme.text4) : 'none')};
|
||||
color: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.white : theme.text2) : theme.text3)};
|
||||
font-size: 0.825rem;
|
||||
font-weight: 400;
|
||||
`
|
||||
|
||||
const StyledToggle = styled.a<{ isActive?: boolean; activeElement?: boolean }>`
|
||||
border-radius: 16px;
|
||||
border: 1px solid ${({ theme, isActive }) => (isActive ? theme.primary5 : theme.text4)};
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
|
||||
export interface ToggleProps {
|
||||
isActive: boolean
|
||||
toggle: () => void
|
||||
}
|
||||
|
||||
export default function Toggle({ isActive, toggle }: ToggleProps) {
|
||||
return (
|
||||
<StyledToggle isActive={isActive} target="_self" onClick={toggle}>
|
||||
<ToggleElement isActive={isActive} isOnSwitch={true}>
|
||||
On
|
||||
</ToggleElement>
|
||||
<ToggleElement isActive={!isActive} isOnSwitch={false}>
|
||||
Off
|
||||
</ToggleElement>
|
||||
</StyledToggle>
|
||||
)
|
||||
}
|
||||
@@ -6,9 +6,9 @@ import { WETH } from '@uniswap/sdk'
|
||||
|
||||
import EthereumLogo from '../../assets/images/ethereum-logo.png'
|
||||
|
||||
const TOKEN_ICON_API = address =>
|
||||
const getTokenLogoURL = address =>
|
||||
`https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${address}/logo.png`
|
||||
const BAD_IMAGES = {}
|
||||
const NO_LOGO_ADDRESSES: { [tokenAddress: string]: true } = {}
|
||||
|
||||
const Image = styled.img<{ size: string }>`
|
||||
width: ${({ size }) => size};
|
||||
@@ -44,20 +44,16 @@ export default function TokenLogo({
|
||||
size?: string
|
||||
style?: React.CSSProperties
|
||||
}) {
|
||||
const [error, setError] = useState(false)
|
||||
const [, refresh] = useState<number>(0)
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
// mock rinkeby DAI
|
||||
if (chainId === 4 && address === '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735') {
|
||||
address = '0x6B175474E89094C44Da98b954EedeAC495271d0F'
|
||||
}
|
||||
|
||||
let path = ''
|
||||
const validated = isAddress(address)
|
||||
// hard code to show ETH instead of WETH in UI
|
||||
if (address === WETH[chainId].address) {
|
||||
if (validated === WETH[chainId].address) {
|
||||
return <StyledEthereumLogo src={EthereumLogo} size={size} {...rest} />
|
||||
} else if (!error && !BAD_IMAGES[address] && isAddress(address)) {
|
||||
path = TOKEN_ICON_API(address)
|
||||
} else if (!NO_LOGO_ADDRESSES[address] && validated) {
|
||||
path = getTokenLogoURL(validated)
|
||||
} else {
|
||||
return (
|
||||
<Emoji {...rest} size={size}>
|
||||
@@ -75,8 +71,8 @@ export default function TokenLogo({
|
||||
src={path}
|
||||
size={size}
|
||||
onError={() => {
|
||||
BAD_IMAGES[address] = true
|
||||
setError(true)
|
||||
NO_LOGO_ADDRESSES[address] = true
|
||||
refresh(i => i + 1)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -3,13 +3,12 @@ import { transparentize } from 'polished'
|
||||
import React, { useMemo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
||||
import { ALL_TOKENS } from '../../constants/tokens'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens } from '../../hooks/Tokens'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { useTokenWarningDismissal } from '../../state/user/hooks'
|
||||
import { ExternalLink, TYPE } from '../../theme'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import { getEtherscanLink, isDefaultToken } from '../../utils'
|
||||
import PropsOfExcluding from '../../utils/props-of-excluding'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
@@ -68,9 +67,8 @@ interface TokenWarningCardProps extends PropsOfExcluding<typeof Wrapper, 'error'
|
||||
|
||||
export default function TokenWarningCard({ token, ...rest }: TokenWarningCardProps) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const isDefaultToken = Boolean(
|
||||
token && token.address && chainId && ALL_TOKENS[chainId] && ALL_TOKENS[chainId][token.address]
|
||||
)
|
||||
|
||||
const isDefault = isDefaultToken(token)
|
||||
|
||||
const tokenSymbol = token?.symbol?.toLowerCase() ?? ''
|
||||
const tokenName = token?.name?.toLowerCase() ?? ''
|
||||
@@ -80,7 +78,7 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro
|
||||
const allTokens = useAllTokens()
|
||||
|
||||
const duplicateNameOrSymbol = useMemo(() => {
|
||||
if (isDefaultToken || !token || !chainId) return false
|
||||
if (isDefault || !token || !chainId) return false
|
||||
|
||||
return Object.keys(allTokens).some(tokenAddress => {
|
||||
const userToken = allTokens[tokenAddress]
|
||||
@@ -89,9 +87,9 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro
|
||||
}
|
||||
return userToken.symbol.toLowerCase() === tokenSymbol || userToken.name.toLowerCase() === tokenName
|
||||
})
|
||||
}, [isDefaultToken, token, chainId, allTokens, tokenSymbol, tokenName])
|
||||
}, [isDefault, token, chainId, allTokens, tokenSymbol, tokenName])
|
||||
|
||||
if (isDefaultToken || !token || dismissed) return null
|
||||
if (isDefault || !token || dismissed) return null
|
||||
|
||||
return (
|
||||
<Wrapper error={duplicateNameOrSymbol} {...rest}>
|
||||
@@ -111,7 +109,7 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro
|
||||
? `${token.name} (${token.symbol})`
|
||||
: token.name || token.symbol}
|
||||
</div>
|
||||
<ExternalLink style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'address')}>
|
||||
<ExternalLink style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'token')}>
|
||||
(View on Etherscan)
|
||||
</ExternalLink>
|
||||
</Row>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import React, { useCallback, useContext, useState } from 'react'
|
||||
import { AlertCircle, CheckCircle } from 'react-feather'
|
||||
|
||||
import styled from 'styled-components'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import useInterval from '../../hooks/useInterval'
|
||||
@@ -51,17 +51,18 @@ export default function TxnPopup({
|
||||
isRunning ? delay : null
|
||||
)
|
||||
|
||||
const handleMouseEnter = useCallback(() => setIsRunning(false), [])
|
||||
const handleMouseLeave = useCallback(() => setIsRunning(true), [])
|
||||
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
return (
|
||||
<AutoRow onMouseEnter={() => setIsRunning(false)} onMouseLeave={() => setIsRunning(true)}>
|
||||
{success ? (
|
||||
<CheckCircle color={'#27AE60'} size={24} style={{ paddingRight: '24px' }} />
|
||||
) : (
|
||||
<AlertCircle color={'#FF6871'} size={24} style={{ paddingRight: '24px' }} />
|
||||
)}
|
||||
<AutoRow onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
<div style={{ paddingRight: 16 }}>
|
||||
{success ? <CheckCircle color={theme.green1} size={24} /> : <AlertCircle color={theme.red1} size={24} />}
|
||||
</div>
|
||||
<AutoColumn gap="8px">
|
||||
<TYPE.body fontWeight={500}>
|
||||
{summary ? summary : 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}
|
||||
</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} />
|
||||
|
||||
@@ -15,7 +15,7 @@ const InfoCard = styled.button<{ active?: boolean }>`
|
||||
border-color: ${({ theme, active }) => (active ? 'transparent' : theme.bg3)};
|
||||
`
|
||||
|
||||
const OptionCard = styled(InfoCard)`
|
||||
const OptionCard = styled(InfoCard as any)`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
@@ -30,7 +30,7 @@ const OptionCardLeft = styled.div`
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
const OptionCardClickable = styled(OptionCard)<{ clickable?: boolean }>`
|
||||
const OptionCardClickable = styled(OptionCard as any)<{ clickable?: boolean }>`
|
||||
margin-top: 0;
|
||||
&:hover {
|
||||
cursor: ${({ clickable }) => (clickable ? 'pointer' : '')};
|
||||
@@ -114,7 +114,6 @@ export default function Option({
|
||||
<OptionCardClickable id={id} onClick={onClick} clickable={clickable && !active} active={active}>
|
||||
<OptionCardLeft>
|
||||
<HeaderText color={color}>
|
||||
{' '}
|
||||
{active ? (
|
||||
<CircleWrapper>
|
||||
<GreenCircle>
|
||||
|
||||
@@ -3,8 +3,7 @@ import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Option from './Option'
|
||||
import { SUPPORTED_WALLETS } from '../../constants'
|
||||
import WalletConnectData from './WalletConnectData'
|
||||
import { walletconnect, injected } from '../../connectors'
|
||||
import { injected } from '../../connectors'
|
||||
import { darken } from 'polished'
|
||||
import Loader from '../Loader'
|
||||
|
||||
@@ -65,28 +64,22 @@ const LoadingWrapper = styled.div`
|
||||
`
|
||||
|
||||
export default function PendingView({
|
||||
uri = '',
|
||||
size,
|
||||
connector,
|
||||
error = false,
|
||||
setPendingError,
|
||||
tryActivation
|
||||
}: {
|
||||
uri?: string
|
||||
size?: number
|
||||
connector?: AbstractConnector
|
||||
error?: boolean
|
||||
setPendingError: (error: boolean) => void
|
||||
tryActivation: (connector: AbstractConnector) => void
|
||||
}) {
|
||||
const isMetamask = window.ethereum && window.ethereum.isMetaMask
|
||||
const isMetamask = window?.ethereum?.isMetaMask
|
||||
|
||||
return (
|
||||
<PendingSection>
|
||||
{!error && connector === walletconnect && <WalletConnectData size={size} uri={uri} />}
|
||||
<LoadingMessage error={error}>
|
||||
<LoadingWrapper>
|
||||
{!error && <StyledLoader />}
|
||||
{error ? (
|
||||
<ErrorGroup>
|
||||
<div>Error connecting.</div>
|
||||
@@ -99,10 +92,11 @@ export default function PendingView({
|
||||
Try Again
|
||||
</ErrorButton>
|
||||
</ErrorGroup>
|
||||
) : connector === walletconnect ? (
|
||||
'Scan QR code with a compatible wallet...'
|
||||
) : (
|
||||
'Initializing...'
|
||||
<>
|
||||
<StyledLoader />
|
||||
Initializing...
|
||||
</>
|
||||
)}
|
||||
</LoadingWrapper>
|
||||
</LoadingMessage>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import QRCode from 'qrcode.react'
|
||||
|
||||
const QRCodeWrapper = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
`
|
||||
|
||||
interface WalletConnectDataProps {
|
||||
uri?: string
|
||||
size: number
|
||||
}
|
||||
|
||||
export default function WalletConnectData({ uri = '', size }: WalletConnectDataProps) {
|
||||
return <QRCodeWrapper>{uri && <QRCode size={size} value={uri} />}</QRCodeWrapper>
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import ReactGA from 'react-ga'
|
||||
import styled from 'styled-components'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { UnsupportedChainIdError, useWeb3React } from '@web3-react/core'
|
||||
import { URI_AVAILABLE } from '@web3-react/walletconnect-connector'
|
||||
import usePrevious from '../../hooks/usePrevious'
|
||||
import { useWalletModalOpen, useWalletModalToggle } from '../../state/application/hooks'
|
||||
|
||||
@@ -15,8 +14,9 @@ import { SUPPORTED_WALLETS } from '../../constants'
|
||||
import { ExternalLink } from '../../theme'
|
||||
import MetamaskIcon from '../../assets/images/metamask.png'
|
||||
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
||||
import { injected, walletconnect, fortmatic, portis } from '../../connectors'
|
||||
import { injected, fortmatic, portis } from '../../connectors'
|
||||
import { OVERLAY_READY } from '../../connectors/Fortmatic'
|
||||
import { WalletConnectConnector } from '@web3-react/walletconnect-connector'
|
||||
|
||||
const CloseIcon = styled.div`
|
||||
position: absolute;
|
||||
@@ -152,19 +152,6 @@ export default function WalletModal({
|
||||
}
|
||||
}, [walletModalOpen])
|
||||
|
||||
// set up uri listener for walletconnect
|
||||
const [uri, setUri] = useState()
|
||||
useEffect(() => {
|
||||
const activateWC = uri => {
|
||||
setUri(uri)
|
||||
// setWalletView(WALLET_VIEWS.PENDING)
|
||||
}
|
||||
walletconnect.on(URI_AVAILABLE, activateWC)
|
||||
return () => {
|
||||
walletconnect.off(URI_AVAILABLE, activateWC)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// close modal when a connection is successful
|
||||
const activePrevious = usePrevious(active)
|
||||
const connectorPrevious = usePrevious(connector)
|
||||
@@ -190,6 +177,12 @@ export default function WalletModal({
|
||||
})
|
||||
setPendingWallet(connector) // set wallet for pending view
|
||||
setWalletView(WALLET_VIEWS.PENDING)
|
||||
|
||||
// if the connector is walletconnect and the user has already tried to connect, manually reset the connector
|
||||
if (connector instanceof WalletConnectConnector && connector.walletConnectProvider?.wc?.uri) {
|
||||
connector.walletConnectProvider = undefined
|
||||
}
|
||||
|
||||
activate(connector, undefined, true).catch(error => {
|
||||
if (error instanceof UnsupportedChainIdError) {
|
||||
activate(connector) // a little janky...can't use setError because the connector isn't set
|
||||
@@ -345,8 +338,6 @@ export default function WalletModal({
|
||||
<ContentWrapper>
|
||||
{walletView === WALLET_VIEWS.PENDING ? (
|
||||
<PendingView
|
||||
uri={uri}
|
||||
size={220}
|
||||
connector={pendingWallet}
|
||||
error={pendingError}
|
||||
setPendingError={setPendingError}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -130,7 +131,7 @@ export default function Web3Status() {
|
||||
const { active, account, connector, error } = useWeb3React()
|
||||
const contextNetwork = useWeb3React(NetworkContextName)
|
||||
|
||||
const ENSName = useENSName(account)
|
||||
const { ENSName } = useENSName(account)
|
||||
|
||||
const allTransactions = useAllTransactions()
|
||||
|
||||
@@ -143,7 +144,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 +187,9 @@ export default function Web3Status() {
|
||||
<Text>{pending?.length} Pending</Text> <Loader stroke="white" />
|
||||
</RowBetween>
|
||||
) : (
|
||||
<Text>{ENSName || shortenAddress(account)}</Text>
|
||||
<Text>
|
||||
{hasSocks ? '🧦' : ''} {ENSName || shortenAddress(account)}
|
||||
</Text>
|
||||
)}
|
||||
{!hasPendingTransactions && getStatusIcon()}
|
||||
</Web3StatusConnected>
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import { Trade, TradeType } from '@uniswap/sdk'
|
||||
import React, { useContext } from 'react'
|
||||
import { ChevronUp, ChevronRight } from 'react-feather'
|
||||
import { Text, Flex } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { CursorPointer, TYPE } from '../../theme'
|
||||
import { useUserSlippageTolerance } from '../../state/user/hooks'
|
||||
import { TYPE } from '../../theme'
|
||||
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown } from '../../utils/prices'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { SectionBreak } from './styleds'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import SlippageTabs, { SlippageTabsProps } from '../SlippageTabs'
|
||||
import FormattedPriceImpact from './FormattedPriceImpact'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import flatMap from 'lodash.flatmap'
|
||||
import { SectionBreak } from './styleds'
|
||||
import SwapRoute from './SwapRoute'
|
||||
|
||||
function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippage: number }) {
|
||||
const theme = useContext(ThemeContext)
|
||||
@@ -61,79 +58,37 @@ function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippag
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
|
||||
<SectionBreak />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export interface AdvancedSwapDetailsProps extends SlippageTabsProps {
|
||||
export interface AdvancedSwapDetailsProps {
|
||||
trade?: Trade
|
||||
onDismiss: () => void
|
||||
}
|
||||
|
||||
export function AdvancedSwapDetails({ trade, onDismiss, ...slippageTabProps }: AdvancedSwapDetailsProps) {
|
||||
export function AdvancedSwapDetails({ trade }: AdvancedSwapDetailsProps) {
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const [allowedSlippage] = useUserSlippageTolerance()
|
||||
|
||||
const showRoute = trade?.route?.path?.length > 2
|
||||
|
||||
return (
|
||||
<AutoColumn gap="md">
|
||||
<CursorPointer>
|
||||
<RowBetween onClick={onDismiss} padding={'8px 20px'}>
|
||||
<Text fontSize={16} color={theme.text2} fontWeight={500} style={{ userSelect: 'none' }}>
|
||||
Hide Advanced
|
||||
</Text>
|
||||
<ChevronUp color={theme.text2} />
|
||||
</RowBetween>
|
||||
</CursorPointer>
|
||||
|
||||
<SectionBreak />
|
||||
|
||||
{trade && <TradeSummary trade={trade} allowedSlippage={slippageTabProps.rawSlippage} />}
|
||||
|
||||
<SlippageTabs {...slippageTabProps} />
|
||||
|
||||
{trade?.route?.path?.length > 2 && (
|
||||
<AutoColumn style={{ padding: '0 20px' }}>
|
||||
<RowFixed>
|
||||
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
|
||||
Route
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Routing through these tokens resulted in the best price for your trade." />
|
||||
</RowFixed>
|
||||
<Flex
|
||||
px="1rem"
|
||||
py="0.5rem"
|
||||
my="0.5rem"
|
||||
style={{ border: `1px solid ${theme.bg3}`, borderRadius: '1rem' }}
|
||||
flexWrap="wrap"
|
||||
width="100%"
|
||||
justifyContent="space-evenly"
|
||||
alignItems="center"
|
||||
>
|
||||
{flatMap(
|
||||
trade.route.path,
|
||||
// add a null in-between each item
|
||||
(token, i, array) => {
|
||||
const lastItem = i === array.length - 1
|
||||
return lastItem ? [token] : [token, null]
|
||||
}
|
||||
).map((token, i) => {
|
||||
// use null as an indicator to insert chevrons
|
||||
if (token === null) {
|
||||
return <ChevronRight key={i} color={theme.text2} />
|
||||
} else {
|
||||
return (
|
||||
<Flex my="0.5rem" alignItems="center" key={token.address} style={{ flexShrink: 0 }}>
|
||||
<TokenLogo address={token.address} size="1.5rem" />
|
||||
<TYPE.black fontSize={14} color={theme.text1} ml="0.5rem">
|
||||
{token.symbol}
|
||||
</TYPE.black>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</Flex>
|
||||
</AutoColumn>
|
||||
{trade && <TradeSummary trade={trade} allowedSlippage={allowedSlippage} />}
|
||||
{showRoute && (
|
||||
<>
|
||||
<SectionBreak />
|
||||
<AutoColumn style={{ padding: '0 24px' }}>
|
||||
<RowFixed>
|
||||
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
|
||||
Route
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Routing through these tokens resulted in the best price for your trade." />
|
||||
</RowFixed>
|
||||
<SwapRoute trade={trade} />
|
||||
</AutoColumn>
|
||||
</>
|
||||
)}
|
||||
</AutoColumn>
|
||||
)
|
||||
|
||||
@@ -1,35 +1,30 @@
|
||||
import React, { useContext } from 'react'
|
||||
import { ChevronDown } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { CursorPointer } from '../../theme'
|
||||
import { RowBetween } from '../Row'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import useLast from '../../hooks/useLast'
|
||||
import { AdvancedSwapDetails, AdvancedSwapDetailsProps } from './AdvancedSwapDetails'
|
||||
import { AdvancedDropdown } from './styleds'
|
||||
|
||||
export default function AdvancedSwapDetailsDropdown({
|
||||
showAdvanced,
|
||||
setShowAdvanced,
|
||||
...rest
|
||||
}: Omit<AdvancedSwapDetailsProps, 'onDismiss'> & {
|
||||
showAdvanced: boolean
|
||||
setShowAdvanced: (showAdvanced: boolean) => void
|
||||
}) {
|
||||
const theme = useContext(ThemeContext)
|
||||
const AdvancedDetailsFooter = styled.div<{ show: boolean }>`
|
||||
padding-top: calc(16px + 2rem);
|
||||
padding-bottom: 20px;
|
||||
margin-top: -2rem;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
color: ${({ theme }) => theme.text2};
|
||||
background-color: ${({ theme }) => theme.advancedBG};
|
||||
z-index: -1;
|
||||
|
||||
transform: ${({ show }) => (show ? 'translateY(0%)' : 'translateY(-100%)')};
|
||||
transition: transform 300ms ease-in-out;
|
||||
`
|
||||
|
||||
export default function AdvancedSwapDetailsDropdown({ trade, ...rest }: AdvancedSwapDetailsProps) {
|
||||
const lastTrade = useLast(trade)
|
||||
|
||||
return (
|
||||
<AdvancedDropdown>
|
||||
{showAdvanced ? (
|
||||
<AdvancedSwapDetails {...rest} onDismiss={() => setShowAdvanced(false)} />
|
||||
) : (
|
||||
<CursorPointer>
|
||||
<RowBetween onClick={() => setShowAdvanced(true)} padding={'8px 20px'} id="show-advanced">
|
||||
<Text fontSize={16} fontWeight={500} style={{ userSelect: 'none' }}>
|
||||
Show Advanced
|
||||
</Text>
|
||||
<ChevronDown color={theme.text2} />
|
||||
</RowBetween>
|
||||
</CursorPointer>
|
||||
)}
|
||||
</AdvancedDropdown>
|
||||
<AdvancedDetailsFooter show={Boolean(trade)}>
|
||||
<AdvancedSwapDetails {...rest} trade={trade ?? lastTrade} />
|
||||
</AdvancedDetailsFooter>
|
||||
)
|
||||
}
|
||||
|
||||
40
src/components/swap/BetterTradeLink.tsx
Normal file
40
src/components/swap/BetterTradeLink.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { stringify } from 'qs'
|
||||
import React, { useContext, useMemo } from 'react'
|
||||
import { useLocation } from 'react-router'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import useParsedQueryString from '../../hooks/useParsedQueryString'
|
||||
import { DEFAULT_VERSION, Version } from '../../hooks/useToggledVersion'
|
||||
|
||||
import { StyledInternalLink } from '../../theme'
|
||||
import { YellowCard } from '../Card'
|
||||
import { AutoColumn } from '../Column'
|
||||
|
||||
export default function BetterTradeLink({ version }: { version: Version }) {
|
||||
const theme = useContext(ThemeContext)
|
||||
const location = useLocation()
|
||||
const search = useParsedQueryString()
|
||||
|
||||
const linkDestination = useMemo(() => {
|
||||
return {
|
||||
...location,
|
||||
search: `?${stringify({
|
||||
...search,
|
||||
use: version !== DEFAULT_VERSION ? version : undefined
|
||||
})}`
|
||||
}
|
||||
}, [location, search, version])
|
||||
|
||||
return (
|
||||
<YellowCard style={{ marginTop: '12px', padding: '8px 4px' }}>
|
||||
<AutoColumn gap="sm" justify="center" style={{ alignItems: 'center', textAlign: 'center' }}>
|
||||
<Text lineHeight="145.23%;" fontSize={14} fontWeight={400} color={theme.text1}>
|
||||
There is a better price for this trade on{' '}
|
||||
<StyledInternalLink to={linkDestination}>
|
||||
<b>Uniswap {version.toUpperCase()} ↗</b>
|
||||
</StyledInternalLink>
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
</YellowCard>
|
||||
)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Percent } from '@uniswap/sdk'
|
||||
import React, { useContext } from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { YellowCard } from '../Card'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
|
||||
export function PriceSlippageWarningCard({ priceSlippage }: { priceSlippage: Percent }) {
|
||||
const theme = useContext(ThemeContext)
|
||||
return (
|
||||
<YellowCard style={{ padding: '20px', paddingTop: '10px' }}>
|
||||
<AutoColumn gap="md">
|
||||
<RowBetween>
|
||||
<RowFixed style={{ paddingTop: '8px' }}>
|
||||
<span role="img" aria-label="warning">
|
||||
⚠️
|
||||
</span>{' '}
|
||||
<Text fontWeight={500} marginLeft="4px" color={theme.text1}>
|
||||
Price Warning
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
<Text lineHeight="145.23%;" fontSize={16} fontWeight={400} color={theme.text1}>
|
||||
This trade will move the price by ~{priceSlippage.toFixed(2)}%.
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
</YellowCard>
|
||||
)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ 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'
|
||||
@@ -15,13 +16,15 @@ export default function SwapModalHeader({
|
||||
formattedAmounts,
|
||||
slippageAdjustedAmounts,
|
||||
priceImpactSeverity,
|
||||
independentField
|
||||
independentField,
|
||||
recipient
|
||||
}: {
|
||||
tokens: { [field in Field]?: Token }
|
||||
formattedAmounts: { [field in Field]?: string }
|
||||
slippageAdjustedAmounts: { [field in Field]?: TokenAmount }
|
||||
priceImpactSeverity: number
|
||||
independentField: Field
|
||||
recipient: string | null
|
||||
}) {
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
@@ -71,6 +74,14 @@ export default function SwapModalHeader({
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
38
src/components/swap/SwapRoute.tsx
Normal file
38
src/components/swap/SwapRoute.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Trade } from '@uniswap/sdk'
|
||||
import React, { Fragment, memo, useContext } from 'react'
|
||||
import { ChevronRight } from 'react-feather'
|
||||
import { Flex } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { TYPE } from '../../theme'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
|
||||
export default memo(function SwapRoute({ trade }: { trade: Trade }) {
|
||||
const theme = useContext(ThemeContext)
|
||||
return (
|
||||
<Flex
|
||||
px="1rem"
|
||||
py="0.5rem"
|
||||
my="0.5rem"
|
||||
style={{ border: `1px solid ${theme.bg3}`, borderRadius: '1rem' }}
|
||||
flexWrap="wrap"
|
||||
width="100%"
|
||||
justifyContent="space-evenly"
|
||||
alignItems="center"
|
||||
>
|
||||
{trade.route.path.map((token, i, path) => {
|
||||
const isLastItem: boolean = i === path.length - 1
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
<Flex my="0.5rem" alignItems="center" style={{ flexShrink: 0 }}>
|
||||
<TokenLogo address={token.address} size="1.5rem" />
|
||||
<TYPE.black fontSize={14} color={theme.text1} ml="0.5rem">
|
||||
{token.symbol}
|
||||
</TYPE.black>
|
||||
</Flex>
|
||||
{isLastItem ? null : <ChevronRight color={theme.text2} />}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</Flex>
|
||||
)
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { Trade } from '@uniswap/sdk'
|
||||
import { Price, Token } from '@uniswap/sdk'
|
||||
import { useContext } from 'react'
|
||||
import { Repeat } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
@@ -7,20 +7,19 @@ import { ThemeContext } from 'styled-components'
|
||||
import { StyledBalanceMaxMini } from './styleds'
|
||||
|
||||
interface TradePriceProps {
|
||||
trade?: Trade
|
||||
price?: Price
|
||||
inputToken?: Token
|
||||
outputToken?: Token
|
||||
showInverted: boolean
|
||||
setShowInverted: (showInverted: boolean) => void
|
||||
}
|
||||
|
||||
export default function TradePrice({ trade, showInverted, setShowInverted }: TradePriceProps) {
|
||||
export default function TradePrice({ price, inputToken, outputToken, showInverted, setShowInverted }: TradePriceProps) {
|
||||
const theme = useContext(ThemeContext)
|
||||
const inputToken = trade?.inputAmount?.token
|
||||
const outputToken = trade?.outputAmount?.token
|
||||
|
||||
const price = showInverted
|
||||
? trade?.executionPrice?.toSignificant(6)
|
||||
: trade?.executionPrice?.invert()?.toSignificant(6)
|
||||
const formattedPrice = showInverted ? price?.toSignificant(6) : price?.invert()?.toSignificant(6)
|
||||
|
||||
const show = Boolean(inputToken && outputToken)
|
||||
const label = showInverted
|
||||
? `${outputToken?.symbol} per ${inputToken?.symbol}`
|
||||
: `${inputToken?.symbol} per ${outputToken?.symbol}`
|
||||
@@ -32,10 +31,16 @@ export default function TradePrice({ trade, showInverted, setShowInverted }: Tra
|
||||
color={theme.text2}
|
||||
style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }}
|
||||
>
|
||||
{price && `${price} ${label}`}
|
||||
<StyledBalanceMaxMini onClick={() => setShowInverted(!showInverted)}>
|
||||
<Repeat size={14} />
|
||||
</StyledBalanceMaxMini>
|
||||
{show ? (
|
||||
<>
|
||||
{formattedPrice ?? '-'} {label}
|
||||
<StyledBalanceMaxMini onClick={() => setShowInverted(!showInverted)}>
|
||||
<Repeat size={14} />
|
||||
</StyledBalanceMaxMini>
|
||||
</>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import React, { useContext } from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
|
||||
import { ExternalLink } from '../../theme'
|
||||
import { YellowCard } from '../Card'
|
||||
import { AutoColumn } from '../Column'
|
||||
|
||||
export default function V1TradeLink({ v1TradeLinkIfBetter }: { v1TradeLinkIfBetter: string }) {
|
||||
const theme = useContext(ThemeContext)
|
||||
return v1TradeLinkIfBetter ? (
|
||||
<YellowCard style={{ marginTop: '12px', padding: '8px 4px' }}>
|
||||
<AutoColumn gap="sm" justify="center" style={{ alignItems: 'center', textAlign: 'center' }}>
|
||||
<Text lineHeight="145.23%;" fontSize={14} fontWeight={400} color={theme.text1}>
|
||||
There is a better price for this trade on{' '}
|
||||
<ExternalLink href={v1TradeLinkIfBetter}>
|
||||
<b>Uniswap V1 ↗</b>
|
||||
</ExternalLink>
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
</YellowCard>
|
||||
) : null
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import styled from 'styled-components'
|
||||
import styled, { css } from 'styled-components'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { Text } from 'rebass'
|
||||
|
||||
@@ -8,30 +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;
|
||||
}
|
||||
`
|
||||
|
||||
export const AdvancedDropdown = styled.div`
|
||||
padding-top: calc(10px + 2rem);
|
||||
padding-bottom: 10px;
|
||||
margin-top: -2rem;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
color: ${({ theme }) => theme.text2};
|
||||
background-color: ${({ theme }) => theme.advancedBG};
|
||||
z-index: -1;
|
||||
${({ clickable }) =>
|
||||
clickable
|
||||
? css`
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
}
|
||||
`
|
||||
: null}
|
||||
`
|
||||
|
||||
export const SectionBreak = styled.div`
|
||||
@@ -45,9 +33,15 @@ export const BottomGrouping = styled.div`
|
||||
position: relative;
|
||||
`
|
||||
|
||||
export const ErrorText = styled(Text)<{ severity?: 0 | 1 | 2 | 3 }>`
|
||||
export const ErrorText = styled(Text)<{ severity?: 0 | 1 | 2 | 3 | 4 }>`
|
||||
color: ${({ theme, severity }) =>
|
||||
severity === 3 ? theme.red1 : severity === 2 ? theme.yellow2 : severity === 1 ? theme.text1 : theme.green1};
|
||||
severity === 3 || severity === 4
|
||||
? theme.red1
|
||||
: severity === 2
|
||||
? theme.yellow2
|
||||
: severity === 1
|
||||
? theme.text1
|
||||
: theme.green1};
|
||||
`
|
||||
|
||||
export const InputGroup = styled(AutoColumn)`
|
||||
@@ -65,7 +59,7 @@ export const StyledNumerical = styled(NumericalInput)`
|
||||
color: ${({ theme }) => theme.text4};
|
||||
}
|
||||
`
|
||||
export const StyledBalanceMaxMini = styled.button<{ active?: boolean }>`
|
||||
export const StyledBalanceMaxMini = styled.button`
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
background-color: ${({ theme }) => theme.bg2};
|
||||
|
||||
@@ -27,7 +27,7 @@ export const injected = new InjectedConnector({
|
||||
export const walletconnect = new WalletConnectConnector({
|
||||
rpc: { 1: NETWORK_URL },
|
||||
bridge: 'https://bridge.walletconnect.org',
|
||||
qrcode: false,
|
||||
qrcode: true,
|
||||
pollingInterval: POLLING_INTERVAL
|
||||
})
|
||||
|
||||
|
||||
471
src/constants/abis/unisocks.json
Normal file
471
src/constants/abis/unisocks.json
Normal 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
|
||||
}
|
||||
]
|
||||
@@ -1,27 +1,40 @@
|
||||
import { ChainId, JSBI, Percent, Token, WETH, Pair, TokenAmount } from '@uniswap/sdk'
|
||||
|
||||
import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors'
|
||||
import { COMP, DAI, MKR, USDC, USDT } from './tokens/mainnet'
|
||||
|
||||
export const ROUTER_ADDRESS = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'
|
||||
|
||||
// used to construct intermediary pairs for trading
|
||||
export const BASES_TO_CHECK_TRADES_AGAINST: { readonly [chainId in ChainId]: Token[] } = {
|
||||
[ChainId.MAINNET]: [
|
||||
WETH[ChainId.MAINNET],
|
||||
new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'),
|
||||
new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C')
|
||||
],
|
||||
// a list of tokens by chain
|
||||
type ChainTokenList = {
|
||||
readonly [chainId in ChainId]: Token[]
|
||||
}
|
||||
|
||||
const WETH_ONLY: ChainTokenList = {
|
||||
[ChainId.MAINNET]: [WETH[ChainId.MAINNET]],
|
||||
[ChainId.ROPSTEN]: [WETH[ChainId.ROPSTEN]],
|
||||
[ChainId.RINKEBY]: [WETH[ChainId.RINKEBY]],
|
||||
[ChainId.GÖRLI]: [WETH[ChainId.GÖRLI]],
|
||||
[ChainId.KOVAN]: [WETH[ChainId.KOVAN]]
|
||||
}
|
||||
|
||||
// used to construct intermediary pairs for trading
|
||||
export const BASES_TO_CHECK_TRADES_AGAINST: ChainTokenList = {
|
||||
...WETH_ONLY,
|
||||
[ChainId.MAINNET]: [...WETH_ONLY[ChainId.MAINNET], DAI, USDC, USDT, COMP, MKR]
|
||||
}
|
||||
|
||||
// used for display in the default list when adding liquidity
|
||||
export const SUGGESTED_BASES = BASES_TO_CHECK_TRADES_AGAINST
|
||||
export const SUGGESTED_BASES: ChainTokenList = {
|
||||
...WETH_ONLY,
|
||||
[ChainId.MAINNET]: [...WETH_ONLY[ChainId.MAINNET], DAI, USDC, USDT]
|
||||
}
|
||||
|
||||
// used to construct the list of all pairs we consider by default in the frontend
|
||||
export const BASES_TO_TRACK_LIQUIDITY_FOR = BASES_TO_CHECK_TRADES_AGAINST
|
||||
export const BASES_TO_TRACK_LIQUIDITY_FOR: ChainTokenList = {
|
||||
...WETH_ONLY,
|
||||
[ChainId.MAINNET]: [...WETH_ONLY[ChainId.MAINNET], DAI, USDC, USDT]
|
||||
}
|
||||
|
||||
export const DUMMY_PAIRS_TO_PIN: { readonly [chainId in ChainId]?: Pair[] } = {
|
||||
[ChainId.MAINNET]: [
|
||||
@@ -58,7 +71,7 @@ export const DUMMY_PAIRS_TO_PIN: { readonly [chainId in ChainId]?: Pair[] } = {
|
||||
]
|
||||
}
|
||||
|
||||
const MAINNET_WALLETS = {
|
||||
const TESTNET_CAPABLE_WALLETS = {
|
||||
INJECTED: {
|
||||
connector: injected,
|
||||
name: 'Injected',
|
||||
@@ -80,9 +93,9 @@ const MAINNET_WALLETS = {
|
||||
|
||||
export const SUPPORTED_WALLETS =
|
||||
process.env.REACT_APP_CHAIN_ID !== '1'
|
||||
? MAINNET_WALLETS
|
||||
? TESTNET_CAPABLE_WALLETS
|
||||
: {
|
||||
...MAINNET_WALLETS,
|
||||
...TESTNET_CAPABLE_WALLETS,
|
||||
...{
|
||||
WALLET_CONNECT: {
|
||||
connector: walletconnect,
|
||||
@@ -90,7 +103,8 @@ export const SUPPORTED_WALLETS =
|
||||
iconName: 'walletConnectIcon.svg',
|
||||
description: 'Connect to Trust Wallet, Rainbow Wallet and more...',
|
||||
href: null,
|
||||
color: '#4196FC'
|
||||
color: '#4196FC',
|
||||
mobile: true
|
||||
},
|
||||
WALLET_LINK: {
|
||||
connector: walletlink,
|
||||
@@ -109,15 +123,6 @@ export const SUPPORTED_WALLETS =
|
||||
mobile: true,
|
||||
mobileOnly: true
|
||||
},
|
||||
TRUST_WALLET_LINK: {
|
||||
name: 'Open in Trust Wallet',
|
||||
iconName: 'trustWallet.png',
|
||||
description: 'iOS and Android app.',
|
||||
href: 'https://link.trustwallet.com/open_url?coin_id=60&url=https://uniswap.exchange/swap',
|
||||
color: '#1C74CC',
|
||||
mobile: true,
|
||||
mobileOnly: true
|
||||
},
|
||||
FORTMATIC: {
|
||||
connector: fortmatic,
|
||||
name: 'Fortmatic',
|
||||
@@ -151,12 +156,13 @@ export const ONE_BIPS = new Percent(JSBI.BigInt(1), JSBI.BigInt(10000))
|
||||
export const BIPS_BASE = JSBI.BigInt(10000)
|
||||
// used for warning states
|
||||
export const ALLOWED_PRICE_IMPACT_LOW: Percent = new Percent(JSBI.BigInt(100), BIPS_BASE) // 1%
|
||||
export const ALLOWED_PRICE_IMPACT_MEDIUM: Percent = new Percent(JSBI.BigInt(500), BIPS_BASE) // 5%
|
||||
export const ALLOWED_PRICE_IMPACT_HIGH: Percent = new Percent(JSBI.BigInt(1000), BIPS_BASE) // 10%
|
||||
|
||||
export const ALLOWED_PRICE_IMPACT_MEDIUM: Percent = new Percent(JSBI.BigInt(300), BIPS_BASE) // 3%
|
||||
export const ALLOWED_PRICE_IMPACT_HIGH: Percent = new Percent(JSBI.BigInt(500), BIPS_BASE) // 5%
|
||||
// if the price slippage exceeds this number, force the user to type 'confirm' to execute
|
||||
export const PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN: Percent = new Percent(JSBI.BigInt(2500), BIPS_BASE) // 25%
|
||||
export const PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN: Percent = new Percent(JSBI.BigInt(1000), BIPS_BASE) // 10%
|
||||
// for non expert mode disable swaps above this
|
||||
export const BLOCKED_PRICE_IMPACT_NON_EXPERT: Percent = new Percent(JSBI.BigInt(1500), BIPS_BASE) // 15%
|
||||
|
||||
// used to ensure the user doesn't send so much ETH so they end up with <.01
|
||||
export const MIN_ETH: JSBI = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(16)) // .01 ETH
|
||||
export const V1_TRADE_LINK_THRESHOLD = new Percent(JSBI.BigInt(75), JSBI.BigInt(10000))
|
||||
export const BETTER_TRADE_LINK_THRESHOLD = new Percent(JSBI.BigInt(75), JSBI.BigInt(10000))
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { Token, ChainId } from '@uniswap/sdk'
|
||||
|
||||
export const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin')
|
||||
export const USDC = new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C')
|
||||
export const USDT = new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'Tether USD')
|
||||
export const COMP = new Token(ChainId.MAINNET, '0xc00e94Cb662C3520282E6f5717214004A7f26888', 18, 'COMP', 'Compound')
|
||||
export const MKR = new Token(ChainId.MAINNET, '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', 18, 'MKR', 'Maker')
|
||||
|
||||
export default [
|
||||
new Token(ChainId.MAINNET, '0xB6eD7644C69416d67B522e20bC294A9a9B405B31', 8, '0xBTC', '0xBitcoin Token'),
|
||||
new Token(ChainId.MAINNET, '0xfC1E690f61EFd961294b3e1Ce3313fBD8aa4f85d', 18, 'aDAI', 'Aave Interest bearing DAI'),
|
||||
@@ -10,17 +16,20 @@ export default [
|
||||
new Token(ChainId.MAINNET, '0x27054b13b1B798B345b591a4d22e6562d47eA75a', 4, 'AST', 'AirSwap Token'),
|
||||
new Token(ChainId.MAINNET, '0xBA11D00c5f74255f56a5E366F4F77f5A186d7f55', 18, 'BAND', 'BandToken'),
|
||||
new Token(ChainId.MAINNET, '0x0D8775F648430679A709E98d2b0Cb6250d2887EF', 18, 'BAT', 'Basic Attention Token'),
|
||||
new Token(ChainId.MAINNET, '0xba100000625a3754423978a60c9317c58a424e3D', 18, 'BAL', 'Balancer'),
|
||||
new Token(ChainId.MAINNET, '0x107c4504cd79C5d2696Ea0030a8dD4e92601B82e', 18, 'BLT', 'Bloom Token'),
|
||||
new Token(ChainId.MAINNET, '0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C', 18, 'BNT', 'Bancor Network Token'),
|
||||
new Token(ChainId.MAINNET, '0x0327112423F3A68efdF1fcF402F6c5CB9f7C33fd', 18, 'BTC++', 'PieDAO BTC++'),
|
||||
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'),
|
||||
new Token(ChainId.MAINNET, '0x39AA39c021dfbaE8faC545936693aC917d5E7563', 8, 'cUSDC', 'Compound USD Coin'),
|
||||
new Token(ChainId.MAINNET, '0xaaAEBE6Fe48E54f431b0C390CfaF0b017d09D42d', 4, 'CEL', 'Celsius'),
|
||||
new Token(ChainId.MAINNET, '0x06AF07097C9Eeb7fD685c692751D5C66dB49c215', 18, 'CHAI', 'Chai'),
|
||||
COMP,
|
||||
new Token(ChainId.MAINNET, '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359', 18, 'SAI', 'Dai Stablecoin v1.0 (SAI)'),
|
||||
new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'),
|
||||
DAI,
|
||||
new Token(ChainId.MAINNET, '0x0Cf0Ee63788A0849fE5297F3407f701E122cC023', 18, 'DATA', 'Streamr DATAcoin'),
|
||||
new Token(ChainId.MAINNET, '0xE0B7927c4aF23765Cb51314A0E0521A9645F0E2A', 9, 'DGD', 'DigixDAO'),
|
||||
new Token(ChainId.MAINNET, '0x4f3AfEC4E5a3F2A6a1A411DEF7D7dFe50eE057bF', 9, 'DGX', 'Digix Gold Token'),
|
||||
@@ -61,9 +70,10 @@ export default [
|
||||
new Token(ChainId.MAINNET, '0xd15eCDCF5Ea68e3995b2D0527A0aE0a3258302F8', 18, 'MCX', 'MachiX Token'),
|
||||
new Token(ChainId.MAINNET, '0xa3d58c4E56fedCae3a7c43A725aeE9A71F0ece4e', 18, 'MET', 'Metronome'),
|
||||
new Token(ChainId.MAINNET, '0x80f222a749a2e18Eb7f676D371F19ad7EFEEe3b7', 18, 'MGN', 'Magnolia Token'),
|
||||
new Token(ChainId.MAINNET, '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', 18, 'MKR', 'Maker'),
|
||||
MKR,
|
||||
new Token(ChainId.MAINNET, '0xec67005c4E498Ec7f55E092bd1d35cbC47C91892', 18, 'MLN', 'Melon Token'),
|
||||
new Token(ChainId.MAINNET, '0x957c30aB0426e0C93CD8241E2c60392d08c6aC8e', 0, 'MOD', 'Modum Token'),
|
||||
new Token(ChainId.MAINNET, '0xe2f2a5C287993345a840Db3B0845fbC70f5935a5', 18, 'mUSD', 'mStable USD'),
|
||||
new Token(ChainId.MAINNET, '0xB62132e35a6c13ee1EE0f84dC5d40bad8d815206', 18, 'NEXO', 'Nexo'),
|
||||
new Token(ChainId.MAINNET, '0x1776e1F26f98b1A5dF9cD347953a26dd3Cb46671', 18, 'NMR', 'Numeraire'),
|
||||
new Token(ChainId.MAINNET, '0x985dd3D42De1e256d09e1c10F112bCCB8015AD41', 18, 'OCEAN', 'OceanToken'),
|
||||
@@ -82,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'),
|
||||
@@ -94,6 +104,7 @@ export default [
|
||||
new Token(ChainId.MAINNET, '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F', 18, 'SNX', 'Synthetix Network Token'),
|
||||
new Token(ChainId.MAINNET, '0x23B608675a2B2fB1890d3ABBd85c5775c51691d5', 18, 'SOCKS', 'Unisocks Edition 0'),
|
||||
new Token(ChainId.MAINNET, '0x42d6622deCe394b54999Fbd73D108123806f6a18', 18, 'SPANK', 'SPANK'),
|
||||
new Token(ChainId.MAINNET, '0x0Ae055097C6d159879521C384F1D2123D1f195e6', 18, 'STAKE', 'STAKE'),
|
||||
new Token(ChainId.MAINNET, '0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC', 8, 'STORJ', 'StorjToken'),
|
||||
new Token(ChainId.MAINNET, '0x57Ab1ec28D129707052df4dF418D58a2D46d5f51', 18, 'sUSD', 'Synth sUSD'),
|
||||
new Token(ChainId.MAINNET, '0x261EfCdD24CeA98652B9700800a13DfBca4103fF', 18, 'sXAU', 'Synth sXAU'),
|
||||
@@ -109,10 +120,11 @@ export default [
|
||||
new Token(ChainId.MAINNET, '0x0000000000085d4780B73119b644AE5ecd22b376', 18, 'TUSD', 'TrueUSD'),
|
||||
new Token(ChainId.MAINNET, '0x8400D94A5cb0fa0D041a3788e395285d61c9ee5e', 8, 'UBT', 'UniBright'),
|
||||
new Token(ChainId.MAINNET, '0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828', 18, 'UMA', 'UMA Voting Token v1'),
|
||||
new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C'),
|
||||
USDC,
|
||||
new Token(ChainId.MAINNET, '0xA4Bdb11dc0a2bEC88d24A3aa1E6Bb17201112eBe', 6, 'USDS', 'StableUSD'),
|
||||
new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'Tether USD'),
|
||||
USDT,
|
||||
new Token(ChainId.MAINNET, '0xeb269732ab75A6fD61Ea60b06fE994cD32a83549', 18, 'USDx', 'dForce'),
|
||||
new Token(ChainId.MAINNET, '0x9A48BD0EC040ea4f1D3147C025cd4076A2e71e3e', 18, 'USD++', 'PieDAO USD++'),
|
||||
new Token(ChainId.MAINNET, '0x8f3470A7388c05eE4e7AF3d01D8C722b0FF52374', 18, 'VERI', 'Veritaseum'),
|
||||
new Token(ChainId.MAINNET, '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', 8, 'WBTC', 'Wrapped BTC'),
|
||||
new Token(ChainId.MAINNET, '0x09fE5f0236F0Ea5D930197DCE254d77B04128075', 18, 'WCK', 'Wrapped CryptoKitties'),
|
||||
|
||||
@@ -3,29 +3,15 @@ import { useMemo } from 'react'
|
||||
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
|
||||
import { Interface } from '@ethersproject/abi'
|
||||
|
||||
import { usePairContract } from '../hooks/useContract'
|
||||
import { useSingleCallResult, useMultipleContractSingleData } from '../state/multicall/hooks'
|
||||
import { useMultipleContractSingleData } from '../state/multicall/hooks'
|
||||
|
||||
const PAIR_INTERFACE = new Interface(IUniswapV2PairABI)
|
||||
|
||||
/*
|
||||
* if loading, return undefined
|
||||
* if no pair created yet, return null
|
||||
* if pair already created (even if 0 reserves), return pair
|
||||
*/
|
||||
export function usePair(tokenA?: Token, tokenB?: Token): undefined | Pair | null {
|
||||
const pairAddress = tokenA && tokenB && !tokenA.equals(tokenB) ? Pair.getAddress(tokenA, tokenB) : undefined
|
||||
const contract = usePairContract(pairAddress, false)
|
||||
const { result: reserves, loading } = useSingleCallResult(contract, 'getReserves')
|
||||
|
||||
return useMemo(() => {
|
||||
if (loading || !tokenA || !tokenB) return undefined
|
||||
if (!reserves) return null
|
||||
const { reserve0, reserve1 } = reserves
|
||||
const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]
|
||||
return new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString()))
|
||||
}, [loading, reserves, tokenA, tokenB])
|
||||
}
|
||||
|
||||
const PAIR_INTERFACE = new Interface(IUniswapV2PairABI)
|
||||
export function usePairs(tokens: [Token | undefined, Token | undefined][]): (undefined | Pair | null)[] {
|
||||
const pairAddresses = useMemo(
|
||||
() =>
|
||||
@@ -51,3 +37,7 @@ export function usePairs(tokens: [Token | undefined, Token | undefined][]): (und
|
||||
})
|
||||
}, [results, tokens])
|
||||
}
|
||||
|
||||
export function usePair(tokenA?: Token, tokenB?: Token): undefined | Pair | null {
|
||||
return usePairs([[tokenA, tokenB]])[0]
|
||||
}
|
||||
|
||||
119
src/data/V1.ts
119
src/data/V1.ts
@@ -1,27 +1,26 @@
|
||||
import { ChainId, JSBI, Pair, Percent, Route, Token, TokenAmount, Trade, TradeType, WETH } from '@uniswap/sdk'
|
||||
import { JSBI, Pair, Percent, Route, Token, TokenAmount, Trade, TradeType, WETH } from '@uniswap/sdk'
|
||||
import { useMemo } from 'react'
|
||||
import { useActiveWeb3React } from '../hooks'
|
||||
import { useAllTokens } from '../hooks/Tokens'
|
||||
import { useV1FactoryContract } from '../hooks/useContract'
|
||||
import { Version } from '../hooks/useToggledVersion'
|
||||
import { NEVER_RELOAD, useSingleCallResult, useSingleContractMultipleData } from '../state/multicall/hooks'
|
||||
import { useETHBalances, useTokenBalance, useTokenBalances } from '../state/wallet/hooks'
|
||||
import { AddressZero } from '@ethersproject/constants'
|
||||
|
||||
function useV1PairAddress(tokenAddress?: string): string | undefined {
|
||||
export function useV1ExchangeAddress(tokenAddress?: string): string | undefined {
|
||||
const contract = useV1FactoryContract()
|
||||
|
||||
const inputs = useMemo(() => [tokenAddress], [tokenAddress])
|
||||
return useSingleCallResult(contract, 'getExchange', inputs)?.result?.[0]
|
||||
}
|
||||
|
||||
class MockV1Pair extends Pair {
|
||||
readonly isV1: true = true
|
||||
}
|
||||
class MockV1Pair extends Pair {}
|
||||
|
||||
function useMockV1Pair(token?: Token): MockV1Pair | undefined {
|
||||
const isWETH = token?.equals(WETH[token?.chainId])
|
||||
const isWETH: boolean = token && WETH[token.chainId] ? token.equals(WETH[token.chainId]) : false
|
||||
|
||||
// will only return an address on mainnet, and not for WETH
|
||||
const v1PairAddress = useV1PairAddress(isWETH ? undefined : token?.address)
|
||||
const v1PairAddress = useV1ExchangeAddress(isWETH ? undefined : token?.address)
|
||||
const tokenBalance = useTokenBalance(v1PairAddress, token)
|
||||
const ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress ?? '']
|
||||
|
||||
@@ -41,9 +40,9 @@ export function useAllTokenV1Exchanges(): { [exchangeAddress: string]: Token } {
|
||||
return useMemo(
|
||||
() =>
|
||||
data?.reduce<{ [exchangeAddress: string]: Token }>((memo, { result }, ix) => {
|
||||
const token = allTokens[args[ix][0]]
|
||||
if (result?.[0]) {
|
||||
memo[result?.[0]] = token
|
||||
if (result?.[0] && result[0] !== AddressZero) {
|
||||
const token = allTokens[args[ix][0]]
|
||||
memo[result[0]] = token
|
||||
}
|
||||
return memo
|
||||
}, {}) ?? {},
|
||||
@@ -53,12 +52,13 @@ export function useAllTokenV1Exchanges(): { [exchangeAddress: string]: Token } {
|
||||
|
||||
// returns whether any of the tokens in the user's token list have liquidity on v1
|
||||
export function useUserHasLiquidityInAllTokens(): boolean | undefined {
|
||||
const exchanges = useAllTokenV1Exchanges()
|
||||
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
|
||||
const exchanges = useAllTokenV1Exchanges()
|
||||
|
||||
const fakeLiquidityTokens = useMemo(
|
||||
() => (chainId ? Object.keys(exchanges).map(address => new Token(chainId, address, 18, 'UNI-V1')) : []),
|
||||
() =>
|
||||
chainId ? Object.keys(exchanges).map(address => new Token(chainId, address, 18, 'UNI-V1', 'Uniswap V1')) : [],
|
||||
[chainId, exchanges]
|
||||
)
|
||||
|
||||
@@ -74,24 +74,23 @@ export function useUserHasLiquidityInAllTokens(): boolean | undefined {
|
||||
)
|
||||
}
|
||||
|
||||
export function useV1TradeLinkIfBetter(
|
||||
/**
|
||||
* Returns the trade to execute on V1 to go between input and output token
|
||||
*/
|
||||
export function useV1Trade(
|
||||
isExactIn?: boolean,
|
||||
input?: Token,
|
||||
output?: Token,
|
||||
exactAmount?: TokenAmount,
|
||||
v2Trade?: Trade,
|
||||
minimumDelta: Percent = new Percent('0')
|
||||
): string | undefined {
|
||||
inputToken?: Token,
|
||||
outputToken?: Token,
|
||||
exactAmount?: TokenAmount
|
||||
): Trade | undefined {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
const isMainnet: boolean = chainId === ChainId.MAINNET
|
||||
|
||||
// get the mock v1 pairs
|
||||
const inputPair = useMockV1Pair(input)
|
||||
const outputPair = useMockV1Pair(output)
|
||||
const inputPair = useMockV1Pair(inputToken)
|
||||
const outputPair = useMockV1Pair(outputToken)
|
||||
|
||||
const inputIsWETH = isMainnet && input?.equals(WETH[ChainId.MAINNET])
|
||||
const outputIsWETH = isMainnet && output?.equals(WETH[ChainId.MAINNET])
|
||||
const inputIsWETH = (inputToken && chainId && WETH[chainId] && inputToken.equals(WETH[chainId])) ?? false
|
||||
const outputIsWETH = (outputToken && chainId && WETH[chainId] && outputToken.equals(WETH[chainId])) ?? false
|
||||
|
||||
// construct a direct or through ETH v1 route
|
||||
let pairs: Pair[] = []
|
||||
@@ -105,7 +104,7 @@ export function useV1TradeLinkIfBetter(
|
||||
pairs = [inputPair, outputPair]
|
||||
}
|
||||
|
||||
const route = input && pairs && pairs.length > 0 && new Route(pairs, input)
|
||||
const route = inputToken && pairs && pairs.length > 0 && new Route(pairs, inputToken)
|
||||
let v1Trade: Trade | undefined
|
||||
try {
|
||||
v1Trade =
|
||||
@@ -113,25 +112,53 @@ export function useV1TradeLinkIfBetter(
|
||||
? new Trade(route, exactAmount, isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT)
|
||||
: undefined
|
||||
} catch {}
|
||||
return v1Trade
|
||||
}
|
||||
|
||||
let v1HasBetterTrade = false
|
||||
if (v1Trade) {
|
||||
if (isExactIn) {
|
||||
// discount the v1 output amount by minimumDelta
|
||||
const discountedV1Output = v1Trade?.outputAmount.multiply(new Percent('1').subtract(minimumDelta))
|
||||
// check if the discounted v1 amount is still greater than v2, short-circuiting if no v2 trade exists
|
||||
v1HasBetterTrade = !v2Trade || discountedV1Output.greaterThan(v2Trade.outputAmount)
|
||||
} else {
|
||||
// inflate the v1 amount by minimumDelta
|
||||
const inflatedV1Input = v1Trade?.inputAmount.multiply(new Percent('1').add(minimumDelta))
|
||||
// check if the inflated v1 amount is still less than v2, short-circuiting if no v2 trade exists
|
||||
v1HasBetterTrade = !v2Trade || inflatedV1Input.lessThan(v2Trade.inputAmount)
|
||||
}
|
||||
export function getTradeVersion(trade?: Trade): Version | undefined {
|
||||
const isV1 = trade?.route?.pairs?.some(pair => pair instanceof MockV1Pair)
|
||||
if (isV1) return Version.v1
|
||||
if (isV1 === false) return Version.v2
|
||||
return undefined
|
||||
}
|
||||
|
||||
// returns the v1 exchange against which a trade should be executed
|
||||
export function useV1TradeExchangeAddress(trade: Trade | undefined): string | undefined {
|
||||
const tokenAddress: string | undefined = useMemo(() => {
|
||||
const tradeVersion = getTradeVersion(trade)
|
||||
const isV1 = tradeVersion === Version.v1
|
||||
return isV1
|
||||
? trade &&
|
||||
WETH[trade.inputAmount.token.chainId] &&
|
||||
trade.inputAmount.token.equals(WETH[trade.inputAmount.token.chainId])
|
||||
? trade.outputAmount.token.address
|
||||
: trade?.inputAmount?.token?.address
|
||||
: undefined
|
||||
}, [trade])
|
||||
return useV1ExchangeAddress(tokenAddress)
|
||||
}
|
||||
|
||||
const ZERO_PERCENT = new Percent('0')
|
||||
const ONE_HUNDRED_PERCENT = new Percent('1')
|
||||
// returns whether tradeB is better than tradeA by at least a threshold
|
||||
export function isTradeBetter(
|
||||
tradeA: Trade | undefined,
|
||||
tradeB: Trade | undefined,
|
||||
minimumDelta: Percent = ZERO_PERCENT
|
||||
): boolean | undefined {
|
||||
if (!tradeA || !tradeB) return undefined
|
||||
|
||||
if (
|
||||
tradeA.tradeType !== tradeB.tradeType ||
|
||||
!tradeA.inputAmount.token.equals(tradeB.inputAmount.token) ||
|
||||
!tradeB.outputAmount.token.equals(tradeB.outputAmount.token)
|
||||
) {
|
||||
throw new Error('Trades are not comparable')
|
||||
}
|
||||
|
||||
return v1HasBetterTrade && input && output
|
||||
? `https://v1.uniswap.exchange/swap?inputCurrency=${inputIsWETH ? 'ETH' : input.address}&outputCurrency=${
|
||||
outputIsWETH ? 'ETH' : output.address
|
||||
}`
|
||||
: undefined
|
||||
if (minimumDelta.equalTo(ZERO_PERCENT)) {
|
||||
return tradeA.executionPrice.lessThan(tradeB.executionPrice)
|
||||
} else {
|
||||
return tradeA.executionPrice.raw.multiply(minimumDelta.add(ONE_HUNDRED_PERCENT)).lessThan(tradeB.executionPrice)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { parseBytes32String } from '@ethersproject/strings'
|
||||
import { ChainId, Token, WETH } from '@uniswap/sdk'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { ALL_TOKENS } from '../constants/tokens'
|
||||
import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'
|
||||
import { useAddUserToken, useUserAddedTokens } from '../state/user/hooks'
|
||||
import { useUserAddedTokens } from '../state/user/hooks'
|
||||
import { isAddress } from '../utils'
|
||||
|
||||
import { useActiveWeb3React } from './index'
|
||||
@@ -100,21 +100,3 @@ export function useToken(tokenAddress?: string): Token | undefined | null {
|
||||
tokenNameBytes32.result
|
||||
])
|
||||
}
|
||||
|
||||
// gets token information by address (typically user input) and
|
||||
// automatically adds it for the user if it's a valid token address
|
||||
export function useTokenByAddressAndAutomaticallyAdd(tokenAddress?: string): Token | undefined | null {
|
||||
const addToken = useAddUserToken()
|
||||
const token = useToken(tokenAddress)
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const allTokens = useAllTokens()
|
||||
|
||||
useEffect(() => {
|
||||
if (!chainId || !token) return
|
||||
if (WETH[chainId as ChainId]?.address === token.address) return
|
||||
if (allTokens[token.address]) return
|
||||
addToken(token)
|
||||
}, [token, addToken, chainId, allTokens])
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { useMemo } from 'react'
|
||||
import { Token, TokenAmount, Trade, ChainId, Pair } from '@uniswap/sdk'
|
||||
import { Pair, Token, TokenAmount, Trade } from '@uniswap/sdk'
|
||||
import flatMap from 'lodash.flatmap'
|
||||
|
||||
import { useActiveWeb3React } from './index'
|
||||
import { usePairs } from '../data/Reserves'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { BASES_TO_CHECK_TRADES_AGAINST } from '../constants'
|
||||
import { usePairs } from '../data/Reserves'
|
||||
|
||||
import { useActiveWeb3React } from './index'
|
||||
|
||||
function useAllCommonPairs(tokenA?: Token, tokenB?: Token): Pair[] {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
const bases = useMemo(() => BASES_TO_CHECK_TRADES_AGAINST[chainId as ChainId] ?? [], [chainId])
|
||||
const bases: Token[] = chainId ? BASES_TO_CHECK_TRADES_AGAINST[chainId] : []
|
||||
|
||||
const allPairCombinations: [Token | undefined, Token | undefined][] = useMemo(
|
||||
() => [
|
||||
@@ -31,13 +31,16 @@ function useAllCommonPairs(tokenA?: Token, tokenB?: Token): Pair[] {
|
||||
// only pass along valid pairs, non-duplicated pairs
|
||||
return useMemo(
|
||||
() =>
|
||||
allPairs
|
||||
// filter out invalid pairs
|
||||
.filter((p): p is Pair => !!p)
|
||||
// filter out duplicated pairs
|
||||
.filter(
|
||||
(p, i, pairs) => i === pairs.findIndex(pair => pair?.liquidityToken.address === p.liquidityToken.address)
|
||||
),
|
||||
Object.values(
|
||||
allPairs
|
||||
// filter out invalid pairs
|
||||
.filter((p): p is Pair => !!p)
|
||||
// filter out duplicated pairs
|
||||
.reduce<{ [pairAddress: string]: Pair }>((memo, curr) => {
|
||||
memo[curr.liquidityToken.address] = memo[curr.liquidityToken.address] ?? curr
|
||||
return memo
|
||||
}, {})
|
||||
),
|
||||
[allPairs]
|
||||
)
|
||||
}
|
||||
@@ -46,14 +49,11 @@ function useAllCommonPairs(tokenA?: Token, tokenB?: Token): Pair[] {
|
||||
* Returns the best trade for the exact amount of tokens in to the given token out
|
||||
*/
|
||||
export function useTradeExactIn(amountIn?: TokenAmount, tokenOut?: Token): Trade | null {
|
||||
const inputToken = amountIn?.token
|
||||
const outputToken = tokenOut
|
||||
|
||||
const allowedPairs = useAllCommonPairs(inputToken, outputToken)
|
||||
const allowedPairs = useAllCommonPairs(amountIn?.token, tokenOut)
|
||||
|
||||
return useMemo(() => {
|
||||
if (amountIn && tokenOut && allowedPairs.length > 0) {
|
||||
return Trade.bestTradeExactIn(allowedPairs, amountIn, tokenOut)[0] ?? null
|
||||
return Trade.bestTradeExactIn(allowedPairs, amountIn, tokenOut, { maxHops: 3, maxNumResults: 1 })[0] ?? null
|
||||
}
|
||||
return null
|
||||
}, [allowedPairs, amountIn, tokenOut])
|
||||
@@ -63,14 +63,11 @@ export function useTradeExactIn(amountIn?: TokenAmount, tokenOut?: Token): Trade
|
||||
* Returns the best trade for the token in to the exact amount of token out
|
||||
*/
|
||||
export function useTradeExactOut(tokenIn?: Token, amountOut?: TokenAmount): Trade | null {
|
||||
const inputToken = tokenIn
|
||||
const outputToken = amountOut?.token
|
||||
|
||||
const allowedPairs = useAllCommonPairs(inputToken, outputToken)
|
||||
const allowedPairs = useAllCommonPairs(tokenIn, amountOut?.token)
|
||||
|
||||
return useMemo(() => {
|
||||
if (tokenIn && amountOut && allowedPairs.length > 0) {
|
||||
return Trade.bestTradeExactOut(allowedPairs, tokenIn, amountOut)[0] ?? null
|
||||
return Trade.bestTradeExactOut(allowedPairs, tokenIn, amountOut, { maxHops: 3, maxNumResults: 1 })[0] ?? null
|
||||
}
|
||||
return null
|
||||
}, [allowedPairs, tokenIn, amountOut])
|
||||
|
||||
@@ -4,12 +4,14 @@ import { Trade, WETH, TokenAmount } from '@uniswap/sdk'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { ROUTER_ADDRESS } from '../constants'
|
||||
import { useTokenAllowance } from '../data/Allowances'
|
||||
import { getTradeVersion, useV1TradeExchangeAddress } from '../data/V1'
|
||||
import { Field } from '../state/swap/actions'
|
||||
import { useTransactionAdder, useHasPendingApproval } from '../state/transactions/hooks'
|
||||
import { computeSlippageAdjustedAmounts } from '../utils/prices'
|
||||
import { calculateGasMargin } from '../utils'
|
||||
import { useTokenContract } from './useContract'
|
||||
import { useActiveWeb3React } from './index'
|
||||
import { Version } from './useToggledVersion'
|
||||
|
||||
export enum ApprovalState {
|
||||
UNKNOWN,
|
||||
@@ -21,30 +23,34 @@ export enum ApprovalState {
|
||||
// returns a variable indicating the state of the approval and a function which approves if necessary or early returns
|
||||
export function useApproveCallback(
|
||||
amountToApprove?: TokenAmount,
|
||||
addressToApprove?: string
|
||||
spender?: string
|
||||
): [ApprovalState, () => Promise<void>] {
|
||||
const { account } = useActiveWeb3React()
|
||||
|
||||
const currentAllowance = useTokenAllowance(amountToApprove?.token, account ?? undefined, addressToApprove)
|
||||
const pendingApproval = useHasPendingApproval(amountToApprove?.token?.address)
|
||||
const currentAllowance = useTokenAllowance(amountToApprove?.token, account ?? undefined, spender)
|
||||
const pendingApproval = useHasPendingApproval(amountToApprove?.token?.address, spender)
|
||||
|
||||
// check the current approval status
|
||||
const approval = useMemo(() => {
|
||||
if (!amountToApprove) return ApprovalState.UNKNOWN
|
||||
const approvalState: ApprovalState = useMemo(() => {
|
||||
if (!amountToApprove || !spender) return ApprovalState.UNKNOWN
|
||||
// we treat WETH as ETH which requires no approvals
|
||||
if (amountToApprove.token.equals(WETH[amountToApprove.token.chainId])) return ApprovalState.APPROVED
|
||||
// we might not have enough data to know whether or not we need to approve
|
||||
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
|
||||
}, [amountToApprove, currentAllowance, pendingApproval])
|
||||
return currentAllowance.lessThan(amountToApprove)
|
||||
? pendingApproval
|
||||
? ApprovalState.PENDING
|
||||
: ApprovalState.NOT_APPROVED
|
||||
: ApprovalState.APPROVED
|
||||
}, [amountToApprove, currentAllowance, pendingApproval, spender])
|
||||
|
||||
const tokenContract = useTokenContract(amountToApprove?.token?.address)
|
||||
const addTransaction = useTransactionAdder()
|
||||
|
||||
const approve = useCallback(async (): Promise<void> => {
|
||||
if (approval !== ApprovalState.NOT_APPROVED) {
|
||||
if (approvalState !== ApprovalState.NOT_APPROVED) {
|
||||
console.error('approve was called unnecessarily')
|
||||
return
|
||||
}
|
||||
@@ -59,30 +65,35 @@ export function useApproveCallback(
|
||||
return
|
||||
}
|
||||
|
||||
if (!spender) {
|
||||
console.error('no spender')
|
||||
return
|
||||
}
|
||||
|
||||
let useExact = false
|
||||
const estimatedGas = await tokenContract.estimateGas.approve(addressToApprove, MaxUint256).catch(() => {
|
||||
const estimatedGas = await tokenContract.estimateGas.approve(spender, MaxUint256).catch(() => {
|
||||
// general fallback for tokens who restrict approval amounts
|
||||
useExact = true
|
||||
return tokenContract.estimateGas.approve(addressToApprove, amountToApprove.raw.toString())
|
||||
return tokenContract.estimateGas.approve(spender, amountToApprove.raw.toString())
|
||||
})
|
||||
|
||||
return tokenContract
|
||||
.approve(addressToApprove, useExact ? amountToApprove.raw.toString() : MaxUint256, {
|
||||
.approve(spender, useExact ? amountToApprove.raw.toString() : MaxUint256, {
|
||||
gasLimit: calculateGasMargin(estimatedGas)
|
||||
})
|
||||
.then((response: TransactionResponse) => {
|
||||
addTransaction(response, {
|
||||
summary: 'Approve ' + amountToApprove?.token?.symbol,
|
||||
approvalOfToken: amountToApprove?.token?.address
|
||||
summary: 'Approve ' + amountToApprove.token.symbol,
|
||||
approval: { tokenAddress: amountToApprove.token.address, spender: spender }
|
||||
})
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
console.debug('Failed to approve token', error)
|
||||
throw error
|
||||
})
|
||||
}, [approval, tokenContract, addressToApprove, amountToApprove, addTransaction])
|
||||
}, [approvalState, tokenContract, spender, amountToApprove, addTransaction])
|
||||
|
||||
return [approval, approve]
|
||||
return [approvalState, approve]
|
||||
}
|
||||
|
||||
// wraps useApproveCallback in the context of a swap
|
||||
@@ -91,5 +102,7 @@ export function useApproveCallbackFromTrade(trade?: Trade, allowedSlippage = 0)
|
||||
() => (trade ? computeSlippageAdjustedAmounts(trade, allowedSlippage)[Field.INPUT] : undefined),
|
||||
[trade, allowedSlippage]
|
||||
)
|
||||
return useApproveCallback(amountToApprove, ROUTER_ADDRESS)
|
||||
const tradeIsV1 = getTradeVersion(trade) === Version.v1
|
||||
const v1ExchangeAddress = useV1TradeExchangeAddress(trade)
|
||||
return useApproveCallback(amountToApprove, tradeIsV1 ? v1ExchangeAddress : ROUTER_ADDRESS)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ import { ChainId } from '@uniswap/sdk'
|
||||
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
|
||||
import { useMemo } from 'react'
|
||||
import { ERC20_BYTES32_ABI } from '../constants/abis/erc20'
|
||||
import UNISOCKS_ABI from '../constants/abis/unisocks.json'
|
||||
import ERC20_ABI from '../constants/abis/erc20.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,30 +28,39 @@ 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)
|
||||
}
|
||||
|
||||
export function useV1ExchangeContract(address: string): Contract | null {
|
||||
return useContract(address, V1_EXCHANGE_ABI, false)
|
||||
return useContract(chainId && V1_FACTORY_ADDRESSES[chainId], V1_FACTORY_ABI, false)
|
||||
}
|
||||
|
||||
export function useV2MigratorContract(): Contract | null {
|
||||
return useContract(MIGRATOR_ADDRESS, MIGRATOR_ABI, true)
|
||||
}
|
||||
|
||||
export function useTokenContract(tokenAddress?: string, withSignerIfPossible = true): Contract | null {
|
||||
export function useV1ExchangeContract(address?: string, withSignerIfPossible?: boolean): Contract | null {
|
||||
return useContract(address, V1_EXCHANGE_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
export function useTokenContract(tokenAddress?: string, withSignerIfPossible?: boolean): Contract | null {
|
||||
return useContract(tokenAddress, ERC20_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
export function useBytes32TokenContract(tokenAddress?: string, withSignerIfPossible = true): Contract | null {
|
||||
export function useBytes32TokenContract(tokenAddress?: string, withSignerIfPossible?: boolean): Contract | null {
|
||||
return useContract(tokenAddress, ERC20_BYTES32_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
export function usePairContract(pairAddress?: string, withSignerIfPossible = true): Contract | null {
|
||||
export function usePairContract(pairAddress?: string, withSignerIfPossible?: boolean): Contract | null {
|
||||
return useContract(pairAddress, IUniswapV2PairABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
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
21
src/hooks/useENS.ts
Normal 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
|
||||
}
|
||||
}
|
||||
46
src/hooks/useENSAddress.ts
Normal file
46
src/hooks/useENSAddress.ts
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
13
src/hooks/useLast.ts
Normal file
13
src/hooks/useLast.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
/**
|
||||
* Returns the last truthy value of type T
|
||||
* @param value changing value
|
||||
*/
|
||||
export default function useLast<T>(value: T | undefined | null): T | null | undefined {
|
||||
const [last, setLast] = useState<T | null | undefined>(value)
|
||||
useEffect(() => {
|
||||
setLast(last => value ?? last)
|
||||
}, [value])
|
||||
return last
|
||||
}
|
||||
11
src/hooks/useParsedQueryString.ts
Normal file
11
src/hooks/useParsedQueryString.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { parse, ParsedQs } from 'qs'
|
||||
import { useMemo } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
export default function useParsedQueryString(): ParsedQs {
|
||||
const { search } = useLocation()
|
||||
return useMemo(
|
||||
() => (search && search.length > 1 ? parse(search, { parseArrays: false, ignoreQueryPrefix: true }) : {}),
|
||||
[search]
|
||||
)
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
19
src/hooks/useSocksBalance.ts
Normal file
19
src/hooks/useSocksBalance.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { JSBI } from '@uniswap/sdk'
|
||||
import { useMemo } from 'react'
|
||||
import { useSingleCallResult } from '../state/multicall/hooks'
|
||||
import { useActiveWeb3React } from './index'
|
||||
import { useSocksController } from './useContract'
|
||||
|
||||
export default function useSocksBalance(): JSBI | undefined {
|
||||
const { account } = useActiveWeb3React()
|
||||
const socksContract = useSocksController()
|
||||
|
||||
const { result } = useSingleCallResult(socksContract, 'balanceOf', [account ?? undefined], { blocksPerFetch: 100 })
|
||||
const data = result?.[0]
|
||||
return data ? JSBI.BigInt(data.toString()) : undefined
|
||||
}
|
||||
|
||||
export function useHasSocks(): boolean | undefined {
|
||||
const balance = useSocksBalance()
|
||||
return useMemo(() => balance && JSBI.greaterThan(balance, JSBI.BigInt(0)), [balance])
|
||||
}
|
||||
@@ -1,15 +1,19 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { MaxUint256 } from '@ethersproject/constants'
|
||||
import { Contract } from '@ethersproject/contracts'
|
||||
import { ChainId, Token, Trade, TradeType, WETH } from '@uniswap/sdk'
|
||||
import { Trade, TradeType, WETH } from '@uniswap/sdk'
|
||||
import { useMemo } from 'react'
|
||||
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, ROUTER_ADDRESS } from '../constants'
|
||||
import { useTokenAllowance } from '../data/Allowances'
|
||||
import { getTradeVersion, useV1TradeExchangeAddress } from '../data/V1'
|
||||
import { Field } from '../state/swap/actions'
|
||||
import { useTransactionAdder } from '../state/transactions/hooks'
|
||||
import { calculateGasMargin, getRouterContract, shortenAddress, isAddress } from '../utils'
|
||||
import { computeSlippageAdjustedAmounts } from '../utils/prices'
|
||||
import { calculateGasMargin, getRouterContract, isAddress } from '../utils'
|
||||
import { useActiveWeb3React } from './index'
|
||||
import useENSName from './useENSName'
|
||||
import { useV1ExchangeContract } from './useContract'
|
||||
import useENS from './useENS'
|
||||
import { Version } from './useToggledVersion'
|
||||
|
||||
enum SwapType {
|
||||
EXACT_TOKENS_FOR_TOKENS,
|
||||
@@ -17,25 +21,37 @@ enum SwapType {
|
||||
EXACT_ETH_FOR_TOKENS,
|
||||
TOKENS_FOR_EXACT_TOKENS,
|
||||
TOKENS_FOR_EXACT_ETH,
|
||||
ETH_FOR_EXACT_TOKENS
|
||||
ETH_FOR_EXACT_TOKENS,
|
||||
V1_EXACT_ETH_FOR_TOKENS,
|
||||
V1_EXACT_TOKENS_FOR_ETH,
|
||||
V1_EXACT_TOKENS_FOR_TOKENS,
|
||||
V1_ETH_FOR_EXACT_TOKENS,
|
||||
V1_TOKENS_FOR_EXACT_ETH,
|
||||
V1_TOKENS_FOR_EXACT_TOKENS
|
||||
}
|
||||
|
||||
function getSwapType(tokens: { [field in Field]?: Token }, isExactIn: boolean, chainId: number): SwapType {
|
||||
function getSwapType(trade: Trade | undefined): SwapType | undefined {
|
||||
if (!trade) return undefined
|
||||
const chainId = trade.inputAmount.token.chainId
|
||||
const inputWETH = trade.inputAmount.token.equals(WETH[chainId])
|
||||
const outputWETH = trade.outputAmount.token.equals(WETH[chainId])
|
||||
const isExactIn = trade.tradeType === TradeType.EXACT_INPUT
|
||||
const isV1 = getTradeVersion(trade) === Version.v1
|
||||
if (isExactIn) {
|
||||
if (tokens[Field.INPUT]?.equals(WETH[chainId as ChainId])) {
|
||||
return SwapType.EXACT_ETH_FOR_TOKENS
|
||||
} else if (tokens[Field.OUTPUT]?.equals(WETH[chainId as ChainId])) {
|
||||
return SwapType.EXACT_TOKENS_FOR_ETH
|
||||
if (inputWETH) {
|
||||
return isV1 ? SwapType.V1_EXACT_ETH_FOR_TOKENS : SwapType.EXACT_ETH_FOR_TOKENS
|
||||
} else if (outputWETH) {
|
||||
return isV1 ? SwapType.V1_EXACT_TOKENS_FOR_ETH : SwapType.EXACT_TOKENS_FOR_ETH
|
||||
} else {
|
||||
return SwapType.EXACT_TOKENS_FOR_TOKENS
|
||||
return isV1 ? SwapType.V1_EXACT_TOKENS_FOR_TOKENS : SwapType.EXACT_TOKENS_FOR_TOKENS
|
||||
}
|
||||
} else {
|
||||
if (tokens[Field.INPUT]?.equals(WETH[chainId as ChainId])) {
|
||||
return SwapType.ETH_FOR_EXACT_TOKENS
|
||||
} else if (tokens[Field.OUTPUT]?.equals(WETH[chainId as ChainId])) {
|
||||
return SwapType.TOKENS_FOR_EXACT_ETH
|
||||
if (inputWETH) {
|
||||
return isV1 ? SwapType.V1_ETH_FOR_EXACT_TOKENS : SwapType.ETH_FOR_EXACT_TOKENS
|
||||
} else if (outputWETH) {
|
||||
return isV1 ? SwapType.V1_TOKENS_FOR_EXACT_ETH : SwapType.TOKENS_FOR_EXACT_ETH
|
||||
} else {
|
||||
return SwapType.TOKENS_FOR_EXACT_TOKENS
|
||||
return isV1 ? SwapType.V1_TOKENS_FOR_EXACT_TOKENS : SwapType.TOKENS_FOR_EXACT_TOKENS
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,19 +59,27 @@ function getSwapType(tokens: { [field in Field]?: Token }, isExactIn: boolean, c
|
||||
// returns a function that will execute a swap, if the parameters are all valid
|
||||
// and the user has approved the slippage adjusted input amount for the trade
|
||||
export function useSwapCallback(
|
||||
trade?: Trade, // trade to execute, required
|
||||
allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // in bips, optional
|
||||
deadline: number = DEFAULT_DEADLINE_FROM_NOW, // in seconds from now, optional
|
||||
to?: string // recipient of output, optional
|
||||
trade: Trade | undefined, // trade to execute, required
|
||||
allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // in bips
|
||||
deadline: number = DEFAULT_DEADLINE_FROM_NOW, // in seconds from now
|
||||
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 inputAllowance = useTokenAllowance(trade?.inputAmount?.token, account ?? undefined, ROUTER_ADDRESS)
|
||||
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) return null
|
||||
if (!trade || !recipient || !library || !account || !tradeVersion || !chainId) return null
|
||||
|
||||
// will always be defined
|
||||
const {
|
||||
@@ -67,28 +91,24 @@ export function useSwapCallback(
|
||||
|
||||
// no allowance
|
||||
if (
|
||||
!trade.inputAmount.token.equals(WETH[chainId as ChainId]) &&
|
||||
!trade.inputAmount.token.equals(WETH[chainId]) &&
|
||||
(!inputAllowance || slippageAdjustedInput.greaterThan(inputAllowance))
|
||||
) {
|
||||
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 routerContract: Contract = getRouterContract(chainId, library, account)
|
||||
|
||||
const path = trade.route.path.map(t => t.address)
|
||||
|
||||
const deadlineFromNow: number = Math.ceil(Date.now() / 1000) + deadline
|
||||
|
||||
const swapType = getSwapType(
|
||||
{ [Field.INPUT]: trade.inputAmount.token, [Field.OUTPUT]: trade.outputAmount.token },
|
||||
trade.tradeType === TradeType.EXACT_INPUT,
|
||||
chainId as ChainId
|
||||
)
|
||||
const swapType = getSwapType(trade)
|
||||
|
||||
// let estimate: Function, method: Function,
|
||||
let methodNames: string[],
|
||||
@@ -145,18 +165,83 @@ export function useSwapCallback(
|
||||
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 = await Promise.all(
|
||||
const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all(
|
||||
methodNames.map(methodName =>
|
||||
routerContract.estimateGas[methodName](...args, value ? { value } : {})
|
||||
contract.estimateGas[methodName](...args, value ? { value } : {})
|
||||
.then(calculateGasMargin)
|
||||
.catch(error => {
|
||||
console.error(`estimateGas failed for ${methodName}`, error)
|
||||
return undefined
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
// we expect failures from left to right, so throw if we see failures
|
||||
// from right to left
|
||||
for (let i = 0; i < safeGasEstimates.length - 1; i++) {
|
||||
// if the FoT method fails, but the regular method does not, we should not
|
||||
// use the regular method. this probably means something is wrong with the fot token.
|
||||
if (BigNumber.isBigNumber(safeGasEstimates[i]) && !BigNumber.isBigNumber(safeGasEstimates[i + 1])) {
|
||||
throw new Error(
|
||||
'An error occurred. Please try raising your slippage. If that does not work, contact support.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const indexOfSuccessfulEstimation = safeGasEstimates.findIndex(safeGasEstimate =>
|
||||
BigNumber.isBigNumber(safeGasEstimate)
|
||||
)
|
||||
@@ -185,38 +270,32 @@ export function useSwapCallback(
|
||||
const methodName = methodNames[indexOfSuccessfulEstimation]
|
||||
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
|
||||
|
||||
return routerContract[methodName](...args, {
|
||||
return contract[methodName](...args, {
|
||||
gasLimit: safeGasEstimate,
|
||||
...(value ? { value } : {})
|
||||
})
|
||||
.then((response: any) => {
|
||||
if (recipient === account) {
|
||||
addTransaction(response, {
|
||||
summary:
|
||||
'Swap ' +
|
||||
slippageAdjustedInput.toSignificant(3) +
|
||||
' ' +
|
||||
trade.inputAmount.token.symbol +
|
||||
' for ' +
|
||||
slippageAdjustedOutput.toSignificant(3) +
|
||||
' ' +
|
||||
trade.outputAmount.token.symbol
|
||||
})
|
||||
} else {
|
||||
addTransaction(response, {
|
||||
summary:
|
||||
'Swap ' +
|
||||
slippageAdjustedInput.toSignificant(3) +
|
||||
' ' +
|
||||
trade.inputAmount.token.symbol +
|
||||
' for ' +
|
||||
slippageAdjustedOutput.toSignificant(3) +
|
||||
' ' +
|
||||
trade.outputAmount.token.symbol +
|
||||
' to ' +
|
||||
(ensName ?? recipient)
|
||||
})
|
||||
}
|
||||
const inputSymbol = trade.inputAmount.token.symbol
|
||||
const outputSymbol = trade.outputAmount.token.symbol
|
||||
const inputAmount = slippageAdjustedInput.toSignificant(3)
|
||||
const outputAmount = slippageAdjustedOutput.toSignificant(3)
|
||||
|
||||
const base = `Swap ${inputAmount} ${inputSymbol} for ${outputAmount} ${outputSymbol}`
|
||||
const withRecipient =
|
||||
recipient === account
|
||||
? base
|
||||
: `${base} to ${
|
||||
recipientAddressOrName && isAddress(recipientAddressOrName)
|
||||
? shortenAddress(recipientAddressOrName)
|
||||
: recipientAddressOrName
|
||||
}`
|
||||
|
||||
const withVersion =
|
||||
tradeVersion === Version.v2 ? withRecipient : `${withRecipient} on ${tradeVersion.toUpperCase()}`
|
||||
|
||||
addTransaction(response, {
|
||||
summary: withVersion
|
||||
})
|
||||
|
||||
return response.hash
|
||||
})
|
||||
@@ -227,11 +306,24 @@ export function useSwapCallback(
|
||||
}
|
||||
// otherwise, the error was unexpected and we need to convey that
|
||||
else {
|
||||
console.error(`swap failed for ${methodName}`, error)
|
||||
console.error(`Swap failed`, error, methodName, args, value)
|
||||
throw Error('An error occurred while swapping. Please contact support.')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [account, allowedSlippage, addTransaction, chainId, deadline, inputAllowance, library, trade, ensName, recipient])
|
||||
}, [
|
||||
trade,
|
||||
recipient,
|
||||
library,
|
||||
account,
|
||||
tradeVersion,
|
||||
chainId,
|
||||
allowedSlippage,
|
||||
inputAllowance,
|
||||
v1Exchange,
|
||||
deadline,
|
||||
recipientAddressOrName,
|
||||
addTransaction
|
||||
])
|
||||
}
|
||||
|
||||
15
src/hooks/useToggledVersion.ts
Normal file
15
src/hooks/useToggledVersion.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import useParsedQueryString from './useParsedQueryString'
|
||||
|
||||
export enum Version {
|
||||
v1 = 'v1',
|
||||
v2 = 'v2'
|
||||
}
|
||||
|
||||
export const DEFAULT_VERSION: Version = Version.v2
|
||||
|
||||
export default function useToggledVersion(): Version {
|
||||
const { use } = useParsedQueryString()
|
||||
if (!use || typeof use !== 'string') return Version.v2
|
||||
if (use.toLowerCase() === 'v1') return Version.v1
|
||||
return DEFAULT_VERSION
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -6,6 +6,7 @@ import ReactDOM from 'react-dom'
|
||||
import ReactGA from 'react-ga'
|
||||
import { Provider } from 'react-redux'
|
||||
import { NetworkContextName } from './constants'
|
||||
import 'inter-ui'
|
||||
import './i18n'
|
||||
import App from './pages/App'
|
||||
import store from './state'
|
||||
@@ -18,7 +19,9 @@ import ThemeProvider, { FixedGlobalStyle, ThemedGlobalStyle } from './theme'
|
||||
const Web3ProviderNetwork = createWeb3ReactRoot(NetworkContextName)
|
||||
|
||||
function getLibrary(provider: any): Web3Provider {
|
||||
return new Web3Provider(provider)
|
||||
const library = new Web3Provider(provider)
|
||||
library.pollingInterval = 15000
|
||||
return library
|
||||
}
|
||||
|
||||
const GOOGLE_ANALYTICS_ID: string | undefined = process.env.REACT_APP_GOOGLE_ANALYTICS_ID
|
||||
|
||||
63
src/pages/AddLiquidity/ConfirmAddModalBottom.tsx
Normal file
63
src/pages/AddLiquidity/ConfirmAddModalBottom.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Fraction, Percent, Token, TokenAmount } from '@uniswap/sdk'
|
||||
import React from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import { ButtonPrimary } from '../../components/Button'
|
||||
import { RowBetween, RowFixed } from '../../components/Row'
|
||||
import TokenLogo from '../../components/TokenLogo'
|
||||
import { Field } from '../../state/mint/actions'
|
||||
import { TYPE } from '../../theme'
|
||||
|
||||
export function ConfirmAddModalBottom({
|
||||
noLiquidity,
|
||||
price,
|
||||
tokens,
|
||||
parsedAmounts,
|
||||
poolTokenPercentage,
|
||||
onAdd
|
||||
}: {
|
||||
noLiquidity?: boolean
|
||||
price?: Fraction
|
||||
tokens: { [field in Field]?: Token }
|
||||
parsedAmounts: { [field in Field]?: TokenAmount }
|
||||
poolTokenPercentage?: Percent
|
||||
onAdd: () => void
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<RowBetween>
|
||||
<TYPE.body>{tokens[Field.TOKEN_A]?.symbol} Deposited</TYPE.body>
|
||||
<RowFixed>
|
||||
<TokenLogo address={tokens[Field.TOKEN_A]?.address} style={{ marginRight: '8px' }} />
|
||||
<TYPE.body>{parsedAmounts[Field.TOKEN_A]?.toSignificant(6)}</TYPE.body>
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<TYPE.body>{tokens[Field.TOKEN_B]?.symbol} Deposited</TYPE.body>
|
||||
<RowFixed>
|
||||
<TokenLogo address={tokens[Field.TOKEN_B]?.address} style={{ marginRight: '8px' }} />
|
||||
<TYPE.body>{parsedAmounts[Field.TOKEN_B]?.toSignificant(6)}</TYPE.body>
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<TYPE.body>Rates</TYPE.body>
|
||||
<TYPE.body>
|
||||
{`1 ${tokens[Field.TOKEN_A]?.symbol} = ${price?.toSignificant(4)} ${tokens[Field.TOKEN_B]?.symbol}`}
|
||||
</TYPE.body>
|
||||
</RowBetween>
|
||||
<RowBetween style={{ justifyContent: 'flex-end' }}>
|
||||
<TYPE.body>
|
||||
{`1 ${tokens[Field.TOKEN_B]?.symbol} = ${price?.invert().toSignificant(4)} ${tokens[Field.TOKEN_A]?.symbol}`}
|
||||
</TYPE.body>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<TYPE.body>Share of Pool:</TYPE.body>
|
||||
<TYPE.body>{noLiquidity ? '100' : poolTokenPercentage?.toSignificant(4)}%</TYPE.body>
|
||||
</RowBetween>
|
||||
<ButtonPrimary style={{ margin: '20px 0 0 0' }} onClick={onAdd}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{noLiquidity ? 'Create Pool & Supply' : 'Confirm Supply'}
|
||||
</Text>
|
||||
</ButtonPrimary>
|
||||
</>
|
||||
)
|
||||
}
|
||||
52
src/pages/AddLiquidity/PoolPriceBar.tsx
Normal file
52
src/pages/AddLiquidity/PoolPriceBar.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Fraction, Percent, Token } from '@uniswap/sdk'
|
||||
import React, { useContext } from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { AutoColumn } from '../../components/Column'
|
||||
import { AutoRow } from '../../components/Row'
|
||||
import { ONE_BIPS } from '../../constants'
|
||||
import { Field } from '../../state/mint/actions'
|
||||
import { TYPE } from '../../theme'
|
||||
|
||||
export const PoolPriceBar = ({
|
||||
tokens,
|
||||
noLiquidity,
|
||||
poolTokenPercentage,
|
||||
price
|
||||
}: {
|
||||
tokens: { [field in Field]?: Token }
|
||||
noLiquidity?: boolean
|
||||
poolTokenPercentage?: Percent
|
||||
price?: Fraction
|
||||
}) => {
|
||||
const theme = useContext(ThemeContext)
|
||||
return (
|
||||
<AutoColumn gap="md">
|
||||
<AutoRow justify="space-around" gap="4px">
|
||||
<AutoColumn justify="center">
|
||||
<TYPE.black>{price?.toSignificant(6) ?? '0'}</TYPE.black>
|
||||
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
|
||||
{tokens[Field.TOKEN_B]?.symbol} per {tokens[Field.TOKEN_A]?.symbol}
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
<AutoColumn justify="center">
|
||||
<TYPE.black>{price?.invert().toSignificant(6) ?? '0'}</TYPE.black>
|
||||
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
|
||||
{tokens[Field.TOKEN_A]?.symbol} per {tokens[Field.TOKEN_B]?.symbol}
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
<AutoColumn justify="center">
|
||||
<TYPE.black>
|
||||
{noLiquidity && price
|
||||
? '100'
|
||||
: (poolTokenPercentage?.lessThan(ONE_BIPS) ? '<0.01' : poolTokenPercentage?.toFixed(2)) ?? '0'}
|
||||
%
|
||||
</TYPE.black>
|
||||
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
|
||||
Share of Pool
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
</AutoRow>
|
||||
</AutoColumn>
|
||||
)
|
||||
}
|
||||
13
src/pages/AddLiquidity/currencyId.ts
Normal file
13
src/pages/AddLiquidity/currencyId.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Token, ChainId, WETH } from '@uniswap/sdk'
|
||||
|
||||
export function currencyId(...args: [ChainId | undefined, string] | [Token]): string {
|
||||
if (args.length === 2) {
|
||||
const [chainId, tokenAddress] = args
|
||||
return chainId && tokenAddress === WETH[chainId].address ? 'ETH' : tokenAddress
|
||||
} else if (args.length === 1) {
|
||||
const [token] = args
|
||||
return currencyId(token.chainId, token.address)
|
||||
} else {
|
||||
throw new Error('unexpected call signature')
|
||||
}
|
||||
}
|
||||
@@ -1,50 +1,64 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { TokenAmount, WETH } from '@uniswap/sdk'
|
||||
import React, { useContext, useState } from 'react'
|
||||
import { TransactionResponse } from '@ethersproject/providers'
|
||||
import { ChainId, Token, 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 { 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, DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
|
||||
import { ROUTER_ADDRESS } from '../../constants'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useToken } from '../../hooks/Tokens'
|
||||
import { ApprovalState, useApproveCallback } from '../../hooks/useApproveCallback'
|
||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||
import { Field } from '../../state/mint/actions'
|
||||
import { useDerivedMintInfo, useMintActionHandlers, useMintState } from '../../state/mint/hooks'
|
||||
|
||||
import { useTransactionAdder } from '../../state/transactions/hooks'
|
||||
import { useIsExpertMode, useUserDeadline, useUserSlippageTolerance } from '../../state/user/hooks'
|
||||
import { TYPE } from '../../theme'
|
||||
import { calculateGasMargin, calculateSlippageAmount, getRouterContract } from '../../utils'
|
||||
import { maxAmountSpend } from '../../utils/maxAmountSpend'
|
||||
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 AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
|
||||
import { ConfirmAddModalBottom } from './ConfirmAddModalBottom'
|
||||
import { currencyId } from './currencyId'
|
||||
import { PoolPriceBar } from './PoolPriceBar'
|
||||
|
||||
export default function AddLiquidity({ match: { params } }: RouteComponentProps<{ tokens: string }>) {
|
||||
useDefaultsFromURLMatchParams(params)
|
||||
function useTokenByCurrencyId(chainId: ChainId | undefined, currencyId: string | undefined): Token | undefined {
|
||||
const isETH = currencyId?.toUpperCase() === 'ETH'
|
||||
const token = useToken(isETH ? undefined : currencyId)
|
||||
return isETH && chainId ? WETH[chainId] : token ?? undefined
|
||||
}
|
||||
|
||||
export default function AddLiquidity({
|
||||
match: {
|
||||
params: { currencyIdA, currencyIdB }
|
||||
},
|
||||
history
|
||||
}: RouteComponentProps<{ currencyIdA?: string; currencyIdB?: string }>) {
|
||||
const { account, chainId, library } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const tokenA = useTokenByCurrencyId(chainId, currencyIdA)
|
||||
const tokenB = useTokenByCurrencyId(chainId, currencyIdB)
|
||||
|
||||
// toggle wallet when disconnected
|
||||
const toggleWalletModal = useWalletModalToggle()
|
||||
|
||||
const expertMode = useIsExpertMode()
|
||||
|
||||
// mint state
|
||||
const { independentField, typedValue, otherTypedValue } = useMintState()
|
||||
const {
|
||||
@@ -58,20 +72,32 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
|
||||
liquidityMinted,
|
||||
poolTokenPercentage,
|
||||
error
|
||||
} = useDerivedMintInfo()
|
||||
const { onUserInput } = useMintActionHandlers()
|
||||
} = useDerivedMintInfo(tokenA ?? undefined, tokenB ?? undefined)
|
||||
const { onUserInput } = useMintActionHandlers(noLiquidity)
|
||||
|
||||
const handleTokenAInput = useCallback(
|
||||
(field: string, value: string) => {
|
||||
return onUserInput(Field.TOKEN_A, value)
|
||||
},
|
||||
[onUserInput]
|
||||
)
|
||||
const handleTokenBInput = useCallback(
|
||||
(field: string, value: string) => {
|
||||
return onUserInput(Field.TOKEN_B, value)
|
||||
},
|
||||
[onUserInput]
|
||||
)
|
||||
|
||||
const isValid = !error
|
||||
|
||||
// modal and loading
|
||||
const [showAdvanced, setShowAdvanced] = useState<boolean>(false) // toggling slippage, deadline, etc. on and off
|
||||
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 [showConfirm, setShowConfirm] = useState<boolean>(false)
|
||||
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirm
|
||||
|
||||
// tx parameters
|
||||
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
|
||||
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
|
||||
// txn values
|
||||
const [deadline] = useUserDeadline() // custom from users settings
|
||||
const [allowedSlippage] = useUserSlippageTolerance() // custom from users
|
||||
const [txHash, setTxHash] = useState<string>('')
|
||||
|
||||
// get formatted amounts
|
||||
const formattedAmounts = {
|
||||
@@ -83,17 +109,7 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
|
||||
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
|
||||
[field]: maxAmountSpend(tokenBalances[field])
|
||||
}
|
||||
}, {})
|
||||
|
||||
@@ -101,7 +117,7 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
|
||||
(accumulator, field) => {
|
||||
return {
|
||||
...accumulator,
|
||||
[field]: maxAmounts[field] && parsedAmounts[field] ? maxAmounts[field].equalTo(parsedAmounts[field]) : undefined
|
||||
[field]: maxAmounts[field]?.equalTo(parsedAmounts[field] ?? '0')
|
||||
}
|
||||
},
|
||||
{}
|
||||
@@ -112,38 +128,48 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
|
||||
const [approvalB, approveBCallback] = useApproveCallback(parsedAmounts[Field.TOKEN_B], ROUTER_ADDRESS)
|
||||
|
||||
const addTransaction = useTransactionAdder()
|
||||
|
||||
async function onAdd() {
|
||||
if (!chainId || !library || !account) return
|
||||
const router = getRouterContract(chainId, library, account)
|
||||
|
||||
const { [Field.TOKEN_A]: parsedAmountA, [Field.TOKEN_B]: parsedAmountB } = parsedAmounts
|
||||
if (!parsedAmountA || !parsedAmountB || !tokenA || !tokenB) {
|
||||
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.TOKEN_A]: calculateSlippageAmount(parsedAmountA, noLiquidity ? 0 : allowedSlippage)[0],
|
||||
[Field.TOKEN_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 (tokenA.equals(WETH[chainId]) || tokenB.equals(WETH[chainId])) {
|
||||
const tokenBIsETH = tokenB.equals(WETH[chainId])
|
||||
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
|
||||
(tokenBIsETH ? tokenA : tokenB).address, // token
|
||||
(tokenBIsETH ? parsedAmountA : parsedAmountB).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
|
||||
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(),
|
||||
tokenA.address,
|
||||
tokenB.address,
|
||||
parsedAmountA.raw.toString(),
|
||||
parsedAmountB.raw.toString(),
|
||||
amountsMin[Field.TOKEN_A].toString(),
|
||||
amountsMin[Field.TOKEN_B].toString(),
|
||||
account,
|
||||
@@ -226,76 +252,14 @@ 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const PriceBar = () => {
|
||||
return (
|
||||
<AutoColumn gap="md" justify="space-between">
|
||||
<AutoRow justify="space-between">
|
||||
<AutoColumn justify="center">
|
||||
<TYPE.black>{price?.toSignificant(6) ?? '0'}</TYPE.black>
|
||||
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
|
||||
{tokens[Field.TOKEN_B]?.symbol} per {tokens[Field.TOKEN_A]?.symbol}
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
<AutoColumn justify="center">
|
||||
<TYPE.black>{price?.invert().toSignificant(6) ?? '0'}</TYPE.black>
|
||||
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
|
||||
{tokens[Field.TOKEN_A]?.symbol} per {tokens[Field.TOKEN_B]?.symbol}
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
<AutoColumn justify="center">
|
||||
<TYPE.black>
|
||||
{noLiquidity && price
|
||||
? '100'
|
||||
: (poolTokenPercentage?.lessThan(ONE_BIPS) ? '<0.01' : poolTokenPercentage?.toFixed(2)) ?? '0'}
|
||||
%
|
||||
</TYPE.black>
|
||||
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
|
||||
Share of Pool
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
</AutoRow>
|
||||
</AutoColumn>
|
||||
<ConfirmAddModalBottom
|
||||
price={price}
|
||||
tokens={tokens}
|
||||
parsedAmounts={parsedAmounts}
|
||||
noLiquidity={noLiquidity}
|
||||
onAdd={onAdd}
|
||||
poolTokenPercentage={poolTokenPercentage}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -303,9 +267,39 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
|
||||
tokens[Field.TOKEN_A]?.symbol
|
||||
} and ${parsedAmounts[Field.TOKEN_B]?.toSignificant(6)} ${tokens[Field.TOKEN_B]?.symbol}`
|
||||
|
||||
const handleTokenASelect = useCallback(
|
||||
(tokenAddress: string) => {
|
||||
const [tokenAId, tokenBId] = [
|
||||
currencyId(chainId, tokenAddress),
|
||||
tokenB ? currencyId(chainId, tokenB.address) : undefined
|
||||
]
|
||||
if (tokenAId === tokenBId) {
|
||||
history.push(`/add/${tokenAId}/${tokenA ? currencyId(chainId, tokenA.address) : ''}`)
|
||||
} else {
|
||||
history.push(`/add/${tokenAId}/${tokenBId}`)
|
||||
}
|
||||
},
|
||||
[chainId, tokenB, history, tokenA]
|
||||
)
|
||||
const handleTokenBSelect = useCallback(
|
||||
(tokenAddress: string) => {
|
||||
const [tokenAId, tokenBId] = [
|
||||
tokenA ? currencyId(chainId, tokenA.address) : undefined,
|
||||
currencyId(chainId, tokenAddress)
|
||||
]
|
||||
if (tokenAId === tokenBId) {
|
||||
history.push(`/add/${tokenB ? currencyId(chainId, tokenB.address) : ''}/${tokenAId}`)
|
||||
} else {
|
||||
history.push(`/add/${currencyIdA ? currencyIdA : 'ETH'}/${currencyId(chainId, tokenAddress)}`)
|
||||
}
|
||||
},
|
||||
[tokenA, chainId, history, tokenB, currencyIdA]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppBody>
|
||||
<AddRemoveTabs adding={true} />
|
||||
<Wrapper>
|
||||
<ConfirmationModal
|
||||
isOpen={showConfirm}
|
||||
@@ -343,34 +337,35 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
|
||||
</ColumnCenter>
|
||||
)}
|
||||
<CurrencyInputPanel
|
||||
disableTokenSelect={true}
|
||||
field={Field.TOKEN_A}
|
||||
value={formattedAmounts[Field.TOKEN_A]}
|
||||
onUserInput={onUserInput}
|
||||
onUserInput={handleTokenAInput}
|
||||
onMax={() => {
|
||||
maxAmounts[Field.TOKEN_A] && onUserInput(Field.TOKEN_A, maxAmounts[Field.TOKEN_A].toExact())
|
||||
onUserInput(Field.TOKEN_A, maxAmounts[Field.TOKEN_A]?.toExact() ?? '')
|
||||
}}
|
||||
onTokenSelection={handleTokenASelect}
|
||||
showMaxButton={!atMaxAmounts[Field.TOKEN_A]}
|
||||
token={tokens[Field.TOKEN_A]}
|
||||
pair={pair}
|
||||
label="Input"
|
||||
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}
|
||||
onUserInput={handleTokenBInput}
|
||||
onTokenSelection={handleTokenBSelect}
|
||||
onMax={() => {
|
||||
maxAmounts[Field.TOKEN_B] && onUserInput(Field.TOKEN_B, maxAmounts[Field.TOKEN_B].toExact())
|
||||
onUserInput(Field.TOKEN_B, maxAmounts[Field.TOKEN_B]?.toExact() ?? '')
|
||||
}}
|
||||
showMaxButton={!atMaxAmounts[Field.TOKEN_B]}
|
||||
token={tokens[Field.TOKEN_B]}
|
||||
pair={pair}
|
||||
id="add-liquidity-input-tokenb"
|
||||
showCommonBases
|
||||
/>
|
||||
{tokens[Field.TOKEN_A] && tokens[Field.TOKEN_B] && (
|
||||
<>
|
||||
@@ -381,7 +376,12 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
|
||||
</TYPE.subHeader>
|
||||
</RowBetween>{' '}
|
||||
<LightCard padding="1rem" borderRadius={'20px'}>
|
||||
<PriceBar />
|
||||
<PoolPriceBar
|
||||
tokens={tokens}
|
||||
poolTokenPercentage={poolTokenPercentage}
|
||||
noLiquidity={noLiquidity}
|
||||
price={price}
|
||||
/>
|
||||
</LightCard>
|
||||
</GreyCard>
|
||||
</>
|
||||
@@ -389,53 +389,62 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
|
||||
|
||||
{!account ? (
|
||||
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
|
||||
) : approvalA === ApprovalState.NOT_APPROVED || approvalA === ApprovalState.PENDING ? (
|
||||
<ButtonLight onClick={approveACallback} disabled={approvalA === ApprovalState.PENDING}>
|
||||
{approvalA === ApprovalState.PENDING ? (
|
||||
<Dots>Approving {tokens[Field.TOKEN_A]?.symbol}</Dots>
|
||||
) : (
|
||||
'Approve ' + tokens[Field.TOKEN_A]?.symbol
|
||||
)}
|
||||
</ButtonLight>
|
||||
) : approvalB === ApprovalState.NOT_APPROVED || approvalB === ApprovalState.PENDING ? (
|
||||
<ButtonLight onClick={approveBCallback} disabled={approvalB === ApprovalState.PENDING}>
|
||||
{approvalB === ApprovalState.PENDING ? (
|
||||
<Dots>Approving {tokens[Field.TOKEN_B]?.symbol}</Dots>
|
||||
) : (
|
||||
'Approve ' + tokens[Field.TOKEN_B]?.symbol
|
||||
)}
|
||||
</ButtonLight>
|
||||
) : (
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
setShowConfirm(true)
|
||||
}}
|
||||
disabled={!isValid}
|
||||
error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]}
|
||||
>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
{error ?? 'Supply'}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
<AutoColumn gap={'md'}>
|
||||
{(approvalA === ApprovalState.NOT_APPROVED ||
|
||||
approvalA === ApprovalState.PENDING ||
|
||||
approvalB === ApprovalState.NOT_APPROVED ||
|
||||
approvalB === ApprovalState.PENDING) &&
|
||||
isValid && (
|
||||
<RowBetween>
|
||||
{approvalA !== ApprovalState.APPROVED && (
|
||||
<ButtonPrimary
|
||||
onClick={approveACallback}
|
||||
disabled={approvalA === ApprovalState.PENDING}
|
||||
width={approvalB !== ApprovalState.APPROVED ? '48%' : '100%'}
|
||||
>
|
||||
{approvalA === ApprovalState.PENDING ? (
|
||||
<Dots>Approving {tokens[Field.TOKEN_A]?.symbol}</Dots>
|
||||
) : (
|
||||
'Approve ' + tokens[Field.TOKEN_A]?.symbol
|
||||
)}
|
||||
</ButtonPrimary>
|
||||
)}
|
||||
{approvalB !== ApprovalState.APPROVED && (
|
||||
<ButtonPrimary
|
||||
onClick={approveBCallback}
|
||||
disabled={approvalB === ApprovalState.PENDING}
|
||||
width={approvalA !== ApprovalState.APPROVED ? '48%' : '100%'}
|
||||
>
|
||||
{approvalB === ApprovalState.PENDING ? (
|
||||
<Dots>Approving {tokens[Field.TOKEN_B]?.symbol}</Dots>
|
||||
) : (
|
||||
'Approve ' + tokens[Field.TOKEN_B]?.symbol
|
||||
)}
|
||||
</ButtonPrimary>
|
||||
)}
|
||||
</RowBetween>
|
||||
)}
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
expertMode ? onAdd() : setShowConfirm(true)
|
||||
}}
|
||||
disabled={!isValid || approvalA !== ApprovalState.APPROVED || approvalB !== ApprovalState.APPROVED}
|
||||
error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]}
|
||||
>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
{error ?? 'Supply'}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
</AutoColumn>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</Wrapper>
|
||||
</AppBody>
|
||||
|
||||
{isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B] ? (
|
||||
<AdvancedSwapDetailsDropdown
|
||||
rawSlippage={allowedSlippage}
|
||||
deadline={deadline}
|
||||
showAdvanced={showAdvanced}
|
||||
setShowAdvanced={setShowAdvanced}
|
||||
setDeadline={setDeadline}
|
||||
setRawSlippage={setAllowedSlippage}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{pair && !noLiquidity ? (
|
||||
<AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
|
||||
<PositionCard pair={pair} minimal={true} />
|
||||
<MinimalPositionCard pair={pair} />
|
||||
</AutoColumn>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
42
src/pages/AddLiquidity/redirects.tsx
Normal file
42
src/pages/AddLiquidity/redirects.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { WETH } from '@uniswap/sdk'
|
||||
import React from 'react'
|
||||
import { Redirect, RouteComponentProps } from 'react-router-dom'
|
||||
import AddLiquidity from './index'
|
||||
|
||||
export function RedirectToAddLiquidity() {
|
||||
return <Redirect to="/add/" />
|
||||
}
|
||||
|
||||
function convertToCurrencyIds(address: string): string {
|
||||
if (Object.values(WETH).some(weth => weth.address === address)) {
|
||||
return 'ETH'
|
||||
}
|
||||
return address
|
||||
}
|
||||
|
||||
const OLD_PATH_STRUCTURE = /^(0x[a-fA-F0-9]{40})-(0x[a-fA-F0-9]{40})$/
|
||||
export function RedirectOldAddLiquidityPathStructure(props: RouteComponentProps<{ currencyIdA: string }>) {
|
||||
const {
|
||||
match: {
|
||||
params: { currencyIdA }
|
||||
}
|
||||
} = props
|
||||
const match = currencyIdA.match(OLD_PATH_STRUCTURE)
|
||||
if (match?.length) {
|
||||
return <Redirect to={`/add/${convertToCurrencyIds(match[1])}/${convertToCurrencyIds(match[2])}`} />
|
||||
}
|
||||
|
||||
return <AddLiquidity {...props} />
|
||||
}
|
||||
|
||||
export function RedirectDuplicateTokenIds(props: RouteComponentProps<{ currencyIdA: string; currencyIdB: string }>) {
|
||||
const {
|
||||
match: {
|
||||
params: { currencyIdA, currencyIdB }
|
||||
}
|
||||
} = props
|
||||
if (currencyIdA.toLowerCase() === currencyIdB.toLowerCase()) {
|
||||
return <Redirect to={`/add/${currencyIdA}`} />
|
||||
}
|
||||
return <AddLiquidity {...props} />
|
||||
}
|
||||
7
src/pages/AddLiquidity/tsconfig.json
Normal file
7
src/pages/AddLiquidity/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.strict.json",
|
||||
"include": [
|
||||
"**/*",
|
||||
"../../../node_modules/eslint-plugin-react/lib/types.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,20 +1,23 @@
|
||||
import React, { Suspense } from 'react'
|
||||
import { BrowserRouter, HashRouter, Route, Switch } from 'react-router-dom'
|
||||
import { HashRouter, Route, Switch } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
import GoogleAnalyticsReporter from '../components/analytics/GoogleAnalyticsReporter'
|
||||
import Footer from '../components/Footer'
|
||||
import Header from '../components/Header'
|
||||
import Popups from '../components/Popups'
|
||||
import Web3ReactManager from '../components/Web3ReactManager'
|
||||
import DarkModeQueryParamReader from '../theme/DarkModeQueryParamReader'
|
||||
import AddLiquidity from './AddLiquidity'
|
||||
import CreatePool from './CreatePool'
|
||||
import {
|
||||
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 Swap from './Swap'
|
||||
import { RedirectPathToSwapOnly, RedirectToSwap } from './Swap/redirects'
|
||||
|
||||
@@ -49,40 +52,14 @@ const BodyWrapper = styled.div`
|
||||
z-index: 1;
|
||||
`
|
||||
|
||||
const BackgroundGradient = styled.div`
|
||||
width: 100%;
|
||||
height: 170vh;
|
||||
background: ${({ theme }) => `radial-gradient(50% 50% at 50% 50%, ${theme.primary1} 0%, ${theme.bg1} 100%)`};
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
opacity: 0.1;
|
||||
z-index: -1;
|
||||
|
||||
transform: translateY(-70vh);
|
||||
|
||||
@media (max-width: 960px) {
|
||||
height: 300px;
|
||||
width: 100%;
|
||||
transform: translateY(-150px);
|
||||
}
|
||||
`
|
||||
|
||||
const Marginer = styled.div`
|
||||
margin-top: 5rem;
|
||||
`
|
||||
|
||||
let Router: React.ComponentType
|
||||
if (process.env.PUBLIC_URL === '.') {
|
||||
Router = HashRouter
|
||||
} else {
|
||||
Router = BrowserRouter
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<Router>
|
||||
<HashRouter>
|
||||
<Route component={GoogleAnalyticsReporter} />
|
||||
<Route component={DarkModeQueryParamReader} />
|
||||
<AppWrapper>
|
||||
@@ -95,23 +72,24 @@ 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="/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={RemoveLiquidity} />
|
||||
<Route exact strict path="/migrate/v1" component={MigrateV1} />
|
||||
<Route exact strict path="/migrate/v1/:address" component={MigrateV1Exchange} />
|
||||
<Route exact strict path="/remove/v1/:address" component={RemoveV1Exchange} />
|
||||
<Route component={RedirectPathToSwapOnly} />
|
||||
</Switch>
|
||||
</Web3ReactManager>
|
||||
<Marginer />
|
||||
<Footer />
|
||||
</BodyWrapper>
|
||||
<BackgroundGradient />
|
||||
</AppWrapper>
|
||||
</Router>
|
||||
</HashRouter>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { RouteComponentProps, Redirect } from 'react-router-dom'
|
||||
import { Token, WETH } from '@uniswap/sdk'
|
||||
import AppBody from '../AppBody'
|
||||
|
||||
import Row, { AutoRow } from '../../components/Row'
|
||||
import TokenLogo from '../../components/TokenLogo'
|
||||
import SearchModal from '../../components/SearchModal'
|
||||
import { Text } from 'rebass'
|
||||
import { Plus } from 'react-feather'
|
||||
import { TYPE, StyledInternalLink } from '../../theme'
|
||||
import { AutoColumn, ColumnCenter } from '../../components/Column'
|
||||
import { ButtonPrimary, ButtonDropwdown, ButtonDropwdownLight } from '../../components/Button'
|
||||
|
||||
import { useToken } from '../../hooks/Tokens'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { usePair } from '../../data/Reserves'
|
||||
|
||||
enum Fields {
|
||||
TOKEN0 = 0,
|
||||
TOKEN1 = 1
|
||||
}
|
||||
|
||||
enum STEP {
|
||||
SELECT_TOKENS = 'SELECT_TOKENS', // choose input and output tokens
|
||||
READY_TO_CREATE = 'READY_TO_CREATE', // enable 'create' button
|
||||
SHOW_CREATE_PAGE = 'SHOW_CREATE_PAGE' // show create page
|
||||
}
|
||||
|
||||
export default function CreatePool({ location }: RouteComponentProps) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const [showSearch, setShowSearch] = useState<boolean>(false)
|
||||
const [activeField, setActiveField] = useState<number>(Fields.TOKEN0)
|
||||
|
||||
const [token0Address, setToken0Address] = useState<string>(WETH[chainId].address)
|
||||
const [token1Address, setToken1Address] = useState<string>()
|
||||
|
||||
const token0: Token = useToken(token0Address)
|
||||
const token1: Token = useToken(token1Address)
|
||||
|
||||
const [step, setStep] = useState<string>(STEP.SELECT_TOKENS)
|
||||
|
||||
const pair = usePair(token0, token1)
|
||||
|
||||
// if both tokens selected but pair doesnt exist, enable button to create pair
|
||||
useEffect(() => {
|
||||
if (token0Address && token1Address && pair === null) {
|
||||
setStep(STEP.READY_TO_CREATE)
|
||||
}
|
||||
}, [pair, token0Address, token1Address])
|
||||
|
||||
// if theyve clicked create, show add liquidity page
|
||||
if (step === STEP.SHOW_CREATE_PAGE) {
|
||||
return <Redirect to={{ ...location, pathname: `/add/${token0Address}-${token1Address}` }} push={true} />
|
||||
}
|
||||
|
||||
return (
|
||||
<AppBody>
|
||||
<AutoColumn gap="20px">
|
||||
<AutoColumn gap="24px">
|
||||
{!token0Address ? (
|
||||
<ButtonDropwdown
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN0)
|
||||
}}
|
||||
>
|
||||
<Text fontSize={20}>Select first token</Text>
|
||||
</ButtonDropwdown>
|
||||
) : (
|
||||
<ButtonDropwdownLight
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN0)
|
||||
}}
|
||||
>
|
||||
<Row align="flex-end">
|
||||
<TokenLogo address={token0Address} />
|
||||
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
|
||||
{token0?.symbol}{' '}
|
||||
</Text>
|
||||
<TYPE.darkGray fontWeight={500} fontSize={16} marginLeft={'8px'}>
|
||||
{token0?.address === WETH[chainId]?.address && '(default)'}
|
||||
</TYPE.darkGray>
|
||||
</Row>
|
||||
</ButtonDropwdownLight>
|
||||
)}
|
||||
<ColumnCenter>
|
||||
<Plus size="16" color="#888D9B" />
|
||||
</ColumnCenter>
|
||||
{!token1Address ? (
|
||||
<ButtonDropwdown
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN1)
|
||||
}}
|
||||
disabled={step !== STEP.SELECT_TOKENS}
|
||||
>
|
||||
<Text fontSize={20}>Select second token</Text>
|
||||
</ButtonDropwdown>
|
||||
) : (
|
||||
<ButtonDropwdownLight
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN1)
|
||||
}}
|
||||
>
|
||||
<Row>
|
||||
<TokenLogo address={token1Address} />
|
||||
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
|
||||
{token1?.symbol}
|
||||
</Text>
|
||||
</Row>
|
||||
</ButtonDropwdownLight>
|
||||
)}
|
||||
{pair ? ( // pair already exists - prompt to add liquidity to existing pool
|
||||
<AutoRow padding="10px" justify="center">
|
||||
<TYPE.body textAlign="center">
|
||||
Pool already exists!{' '}
|
||||
<StyledInternalLink to={`/add/${token0Address}-${token1Address}`}>Join the pool.</StyledInternalLink>
|
||||
</TYPE.body>
|
||||
</AutoRow>
|
||||
) : (
|
||||
<ButtonPrimary disabled={step !== STEP.READY_TO_CREATE} onClick={() => setStep(STEP.SHOW_CREATE_PAGE)}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Create Pool
|
||||
</Text>
|
||||
</ButtonPrimary>
|
||||
)}
|
||||
</AutoColumn>
|
||||
<SearchModal
|
||||
isOpen={showSearch}
|
||||
filterType="tokens"
|
||||
onTokenSelect={address => {
|
||||
activeField === Fields.TOKEN0 ? setToken0Address(address) : setToken1Address(address)
|
||||
}}
|
||||
onDismiss={() => {
|
||||
setShowSearch(false)
|
||||
}}
|
||||
hiddenToken={activeField === Fields.TOKEN0 ? token1Address : token0Address}
|
||||
showCommonBases={activeField === Fields.TOKEN0}
|
||||
/>
|
||||
</AutoColumn>
|
||||
</AppBody>
|
||||
)
|
||||
}
|
||||
@@ -1,38 +1,103 @@
|
||||
import { TransactionResponse } from '@ethersproject/abstract-provider'
|
||||
import { ChainId, Fraction, JSBI, Percent, Token, TokenAmount, WETH } from '@uniswap/sdk'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import { Redirect, RouteComponentProps } from 'react-router'
|
||||
import { ButtonConfirmed } from '../../components/Button'
|
||||
import { PinkCard, YellowCard, LightCard } from '../../components/Card'
|
||||
import { AutoColumn } from '../../components/Column'
|
||||
import QuestionHelper from '../../components/QuestionHelper'
|
||||
import { AutoRow, RowBetween } from '../../components/Row'
|
||||
import { AutoRow, RowBetween, RowFixed } from '../../components/Row'
|
||||
import { Dots } from '../../components/swap/styleds'
|
||||
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
|
||||
import { MIGRATOR_ADDRESS } from '../../constants/abis/migrator'
|
||||
import { usePair } from '../../data/Reserves'
|
||||
import { useTotalSupply } from '../../data/TotalSupply'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
|
||||
import { useToken } from '../../hooks/Tokens'
|
||||
import { ApprovalState, useApproveCallback } from '../../hooks/useApproveCallback'
|
||||
import { useV1ExchangeContract, useV2MigratorContract } from '../../hooks/useContract'
|
||||
import { NEVER_RELOAD, useSingleCallResult } from '../../state/multicall/hooks'
|
||||
import { useIsTransactionPending, useTransactionAdder } from '../../state/transactions/hooks'
|
||||
import { useETHBalances, useTokenBalance } from '../../state/wallet/hooks'
|
||||
import { TYPE } from '../../theme'
|
||||
import { isAddress } from '../../utils'
|
||||
import { TYPE, ExternalLink, BackArrow } from '../../theme'
|
||||
import { isAddress, getEtherscanLink } from '../../utils'
|
||||
import { BodyWrapper } from '../AppBody'
|
||||
import { EmptyState } from './EmptyState'
|
||||
import TokenLogo from '../../components/TokenLogo'
|
||||
import { FormattedPoolTokenAmount } from './index'
|
||||
import { AddressZero } from '@ethersproject/constants'
|
||||
import { Text } from 'rebass'
|
||||
|
||||
const POOL_TOKEN_AMOUNT_MIN = new Fraction(JSBI.BigInt(1), JSBI.BigInt(1000000))
|
||||
const WEI_DENOM = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(18))
|
||||
const ZERO = JSBI.BigInt(0)
|
||||
const ONE = JSBI.BigInt(1)
|
||||
const ZERO_FRACTION = new Fraction(ZERO, ONE)
|
||||
const ALLOWED_OUTPUT_MIN_PERCENT = new Percent(JSBI.BigInt(10000 - INITIAL_ALLOWED_SLIPPAGE), JSBI.BigInt(10000))
|
||||
|
||||
function FormattedPoolTokenAmount({ tokenAmount }: { tokenAmount: TokenAmount }) {
|
||||
return (
|
||||
<>
|
||||
{tokenAmount.equalTo(JSBI.BigInt(0))
|
||||
? '0'
|
||||
: tokenAmount.greaterThan(POOL_TOKEN_AMOUNT_MIN)
|
||||
? tokenAmount.toSignificant(4)
|
||||
: `<${POOL_TOKEN_AMOUNT_MIN.toSignificant(1)}`}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function V1LiquidityInfo({
|
||||
token,
|
||||
liquidityTokenAmount,
|
||||
tokenWorth,
|
||||
ethWorth
|
||||
}: {
|
||||
token: Token
|
||||
liquidityTokenAmount: TokenAmount
|
||||
tokenWorth: TokenAmount
|
||||
ethWorth: Fraction
|
||||
}) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
return (
|
||||
<>
|
||||
<AutoRow style={{ justifyContent: 'flex-start', width: 'fit-content' }}>
|
||||
<TokenLogo size="24px" address={token.address} />
|
||||
<div style={{ marginLeft: '.75rem' }}>
|
||||
<TYPE.mediumHeader>
|
||||
{<FormattedPoolTokenAmount tokenAmount={liquidityTokenAmount} />}{' '}
|
||||
{token.equals(WETH[chainId]) ? 'WETH' : token.symbol}/ETH
|
||||
</TYPE.mediumHeader>
|
||||
</div>
|
||||
</AutoRow>
|
||||
|
||||
<RowBetween my="1rem">
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Pooled {token.equals(WETH[chainId]) ? 'WETH' : token.symbol}:
|
||||
</Text>
|
||||
<RowFixed>
|
||||
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{tokenWorth.toSignificant(4)}
|
||||
</Text>
|
||||
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token.address} />
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
<RowBetween mb="1rem">
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Pooled ETH:
|
||||
</Text>
|
||||
<RowFixed>
|
||||
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{ethWorth.toSignificant(4)}
|
||||
</Text>
|
||||
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={WETH[chainId].address} />
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function V1PairMigration({ liquidityTokenAmount, token }: { liquidityTokenAmount: TokenAmount; token: Token }) {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const totalSupply = useTotalSupply(liquidityTokenAmount.token)
|
||||
@@ -125,69 +190,96 @@ function V1PairMigration({ liquidityTokenAmount, token }: { liquidityTokenAmount
|
||||
})
|
||||
}, [minAmountToken, minAmountETH, migrator, token, account, addTransaction])
|
||||
|
||||
const noLiquidityTokens = liquidityTokenAmount && liquidityTokenAmount.equalTo(ZERO)
|
||||
const noLiquidityTokens = !!liquidityTokenAmount && liquidityTokenAmount.equalTo(ZERO)
|
||||
|
||||
const largePriceDifference = Boolean(priceDifferenceAbs && !priceDifferenceAbs.lessThan(JSBI.BigInt(5)))
|
||||
const largePriceDifference = !!priceDifferenceAbs && !priceDifferenceAbs.lessThan(JSBI.BigInt(5))
|
||||
|
||||
const isSuccessfullyMigrated = Boolean(noLiquidityTokens && pendingMigrationHash)
|
||||
const isSuccessfullyMigrated = !!pendingMigrationHash && !!noLiquidityTokens
|
||||
|
||||
return (
|
||||
<AutoColumn gap="20px">
|
||||
{!isFirstLiquidityProvider ? (
|
||||
largePriceDifference ? (
|
||||
<YellowCard>
|
||||
<TYPE.body style={{ marginBottom: 8, fontWeight: 400 }}>
|
||||
It is best to deposit liquidity into Uniswap V2 at a price you believe is correct. If you believe the
|
||||
price is incorrect, you can either make a swap to move the price or wait for someone else to do so.
|
||||
</TYPE.body>
|
||||
<AutoColumn gap="8px">
|
||||
<RowBetween>
|
||||
<TYPE.body>V1 Price:</TYPE.body>
|
||||
<TYPE.black>
|
||||
{v1SpotPrice?.toSignificant(6)} {token.symbol}/ETH
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<TYPE.body>V2 Price:</TYPE.body>
|
||||
<TYPE.black>
|
||||
{v2SpotPrice?.toSignificant(6)} {token.symbol}/ETH
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<div>Price Difference:</div>
|
||||
<div>{priceDifferenceAbs.toSignificant(4)}%</div>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</YellowCard>
|
||||
) : null
|
||||
) : (
|
||||
<PinkCard>
|
||||
<AutoColumn gap="10px">
|
||||
<div>
|
||||
You are the first liquidity provider for this pair on Uniswap V2. Your liquidity will be migrated at the
|
||||
current V1 price. Your transaction cost also includes the gas to create the pool.
|
||||
</div>
|
||||
<div>V1 Price</div>
|
||||
<AutoColumn>
|
||||
<div>
|
||||
{v1SpotPrice?.invert()?.toSignificant(6)} ETH/{token.symbol}
|
||||
</div>
|
||||
<div>
|
||||
<TYPE.body my={9} style={{ fontWeight: 400 }}>
|
||||
This tool will safely migrate your V1 liquidity to V2 with minimal price risk. The process is completely
|
||||
trustless thanks to the{' '}
|
||||
<ExternalLink href={getEtherscanLink(chainId, MIGRATOR_ADDRESS, 'address')}>
|
||||
<TYPE.blue display="inline">Uniswap migration contract↗</TYPE.blue>
|
||||
</ExternalLink>
|
||||
.
|
||||
</TYPE.body>
|
||||
|
||||
{!isFirstLiquidityProvider && largePriceDifference ? (
|
||||
<YellowCard>
|
||||
<TYPE.body style={{ marginBottom: 8, fontWeight: 400 }}>
|
||||
It{"'"}s best to deposit liquidity into Uniswap V2 at a price you believe is correct. If the V2 price seems
|
||||
incorrect, you can either make a swap to move the price or wait for someone else to do so.
|
||||
</TYPE.body>
|
||||
<AutoColumn gap="8px">
|
||||
<RowBetween>
|
||||
<TYPE.body>V1 Price:</TYPE.body>
|
||||
<TYPE.black>
|
||||
{v1SpotPrice?.toSignificant(6)} {token.symbol}/ETH
|
||||
</div>
|
||||
</AutoColumn>
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<div />
|
||||
<TYPE.black>
|
||||
{v1SpotPrice?.invert()?.toSignificant(6)} ETH/{token.symbol}
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
|
||||
<RowBetween>
|
||||
<TYPE.body>V2 Price:</TYPE.body>
|
||||
<TYPE.black>
|
||||
{v2SpotPrice?.toSignificant(6)} {token.symbol}/ETH
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<div />
|
||||
<TYPE.black>
|
||||
{v2SpotPrice?.invert()?.toSignificant(6)} ETH/{token.symbol}
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
|
||||
<RowBetween>
|
||||
<TYPE.body color="inherit">Price Difference:</TYPE.body>
|
||||
<TYPE.black color="inherit">{priceDifferenceAbs.toSignificant(4)}%</TYPE.black>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</YellowCard>
|
||||
) : null}
|
||||
|
||||
{isFirstLiquidityProvider && (
|
||||
<PinkCard>
|
||||
<TYPE.body style={{ marginBottom: 8, fontWeight: 400 }}>
|
||||
You are the first liquidity provider for this pair on Uniswap V2. Your liquidity will be migrated at the
|
||||
current V1 price. Your transaction cost also includes the gas to create the pool.
|
||||
</TYPE.body>
|
||||
|
||||
<AutoColumn gap="8px">
|
||||
<RowBetween>
|
||||
<TYPE.body>V1 Price:</TYPE.body>
|
||||
<TYPE.black>
|
||||
{v1SpotPrice?.toSignificant(6)} {token.symbol}/ETH
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<div />
|
||||
<TYPE.black>
|
||||
{v1SpotPrice?.invert()?.toSignificant(6)} ETH/{token.symbol}
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</PinkCard>
|
||||
)}
|
||||
|
||||
<LightCard>
|
||||
<AutoRow style={{ justifyContent: 'flex-start', width: 'fit-content' }}>
|
||||
<TokenLogo size="24px" address={token.address} />{' '}
|
||||
<div style={{ marginLeft: '.75rem' }}>
|
||||
<TYPE.mediumHeader>
|
||||
{<FormattedPoolTokenAmount tokenAmount={liquidityTokenAmount} />} {token.symbol} Pool Tokens
|
||||
</TYPE.mediumHeader>
|
||||
</div>
|
||||
</AutoRow>
|
||||
<V1LiquidityInfo
|
||||
token={token}
|
||||
liquidityTokenAmount={liquidityTokenAmount}
|
||||
tokenWorth={tokenWorth}
|
||||
ethWorth={ethWorth}
|
||||
/>
|
||||
|
||||
<div style={{ display: 'flex', marginTop: '1rem' }}>
|
||||
<AutoColumn gap="12px" style={{ flex: '1', marginRight: 12 }}>
|
||||
<ButtonConfirmed
|
||||
@@ -195,11 +287,13 @@ function V1PairMigration({ liquidityTokenAmount, token }: { liquidityTokenAmount
|
||||
disabled={approval !== ApprovalState.NOT_APPROVED}
|
||||
onClick={approve}
|
||||
>
|
||||
{approval === ApprovalState.PENDING
|
||||
? 'Approving...'
|
||||
: approval === ApprovalState.APPROVED
|
||||
? 'Approved'
|
||||
: 'Approve'}
|
||||
{approval === ApprovalState.PENDING ? (
|
||||
<Dots>Approving</Dots>
|
||||
) : approval === ApprovalState.APPROVED ? (
|
||||
'Approved'
|
||||
) : (
|
||||
'Approve'
|
||||
)}
|
||||
</ButtonConfirmed>
|
||||
</AutoColumn>
|
||||
<AutoColumn gap="12px" style={{ flex: '1' }}>
|
||||
@@ -214,13 +308,13 @@ function V1PairMigration({ liquidityTokenAmount, token }: { liquidityTokenAmount
|
||||
}
|
||||
onClick={migrate}
|
||||
>
|
||||
{isSuccessfullyMigrated ? 'Success' : isMigrationPending ? 'Migrating...' : 'Migrate'}
|
||||
{isSuccessfullyMigrated ? 'Success' : isMigrationPending ? <Dots>Migrating</Dots> : 'Migrate'}
|
||||
</ButtonConfirmed>
|
||||
</AutoColumn>
|
||||
</div>
|
||||
</LightCard>
|
||||
<TYPE.darkGray style={{ textAlign: 'center' }}>
|
||||
{'Your ' + token.symbol + ' liquidity will become Uniswap V2 ' + token.symbol + '/ETH liquidity.'}
|
||||
{`Your Uniswap V1 ${token.symbol}/ETH liquidity will become Uniswap V2 ${token.symbol}/ETH liquidity.`}
|
||||
</TYPE.darkGray>
|
||||
</AutoColumn>
|
||||
)
|
||||
@@ -232,53 +326,58 @@ export default function MigrateV1Exchange({
|
||||
params: { address }
|
||||
}
|
||||
}: RouteComponentProps<{ address: string }>) {
|
||||
const validated = isAddress(address)
|
||||
const validatedAddress = isAddress(address)
|
||||
const { chainId, account } = useActiveWeb3React()
|
||||
|
||||
const exchangeContract = useV1ExchangeContract(validated ? validated : undefined)
|
||||
|
||||
const exchangeContract = useV1ExchangeContract(validatedAddress ? validatedAddress : undefined)
|
||||
const tokenAddress = useSingleCallResult(exchangeContract, 'tokenAddress', undefined, NEVER_RELOAD)?.result?.[0]
|
||||
|
||||
const token = useTokenByAddressAndAutomaticallyAdd(tokenAddress)
|
||||
const token = useToken(tokenAddress)
|
||||
|
||||
const liquidityToken: Token | undefined = useMemo(
|
||||
() => (validated && token ? new Token(chainId, validated, 18, `UNI-V1-${token.symbol}`) : undefined),
|
||||
[chainId, token, validated]
|
||||
() =>
|
||||
validatedAddress && token
|
||||
? new Token(chainId, validatedAddress, 18, `UNI-V1-${token.symbol}`, 'Uniswap V1')
|
||||
: undefined,
|
||||
[chainId, validatedAddress, token]
|
||||
)
|
||||
|
||||
const userLiquidityBalance = useTokenBalance(account, liquidityToken)
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
history.push('/migrate/v1')
|
||||
}, [history])
|
||||
|
||||
if (!validated) {
|
||||
// redirect for invalid url params
|
||||
if (!validatedAddress || tokenAddress === AddressZero) {
|
||||
console.error('Invalid address in path', address)
|
||||
return <Redirect to="/migrate/v1" />
|
||||
}
|
||||
|
||||
if (!account) {
|
||||
return (
|
||||
<BodyWrapper>
|
||||
<TYPE.largeHeader>You must connect an account.</TYPE.largeHeader>
|
||||
</BodyWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<BodyWrapper style={{ padding: 24 }}>
|
||||
<AutoColumn gap="16px">
|
||||
<AutoRow style={{ alignItems: 'center', justifyContent: 'space-between' }} gap="8px">
|
||||
<div style={{ cursor: 'pointer' }}>
|
||||
<ArrowLeft onClick={handleBack} />
|
||||
</div>
|
||||
<TYPE.mediumHeader>Migrate {token?.symbol} Pool Tokens</TYPE.mediumHeader>
|
||||
<BackArrow to="/migrate/v1" />
|
||||
<TYPE.mediumHeader>Migrate V1 Liquidity</TYPE.mediumHeader>
|
||||
<div>
|
||||
<QuestionHelper text="Migrate your liquidity tokens from Uniswap V1 to Uniswap V2." />
|
||||
</div>
|
||||
</AutoRow>
|
||||
|
||||
{userLiquidityBalance && token ? (
|
||||
{!account ? (
|
||||
<TYPE.largeHeader>You must connect an account.</TYPE.largeHeader>
|
||||
) : validatedAddress && token?.equals(WETH[chainId]) ? (
|
||||
<>
|
||||
<TYPE.body my={9} style={{ fontWeight: 400 }}>
|
||||
Because Uniswap V2 uses WETH under the hood, your Uniswap V1 WETH/ETH liquidity cannot be migrated. You
|
||||
may want to remove your liquidity instead.
|
||||
</TYPE.body>
|
||||
|
||||
<ButtonConfirmed
|
||||
onClick={() => {
|
||||
history.push(`/remove/v1/${validatedAddress}`)
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</ButtonConfirmed>
|
||||
</>
|
||||
) : userLiquidityBalance && token ? (
|
||||
<V1PairMigration liquidityTokenAmount={userLiquidityBalance} token={token} />
|
||||
) : (
|
||||
<EmptyState message="Loading..." />
|
||||
|
||||
181
src/pages/MigrateV1/RemoveV1Exchange.tsx
Normal file
181
src/pages/MigrateV1/RemoveV1Exchange.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import { TransactionResponse } from '@ethersproject/abstract-provider'
|
||||
import { JSBI, Token, TokenAmount, WETH, Fraction, Percent } from '@uniswap/sdk'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import { Redirect, RouteComponentProps } from 'react-router'
|
||||
import { ButtonConfirmed } from '../../components/Button'
|
||||
import { LightCard } from '../../components/Card'
|
||||
import { AutoColumn } from '../../components/Column'
|
||||
import QuestionHelper from '../../components/QuestionHelper'
|
||||
import { AutoRow } from '../../components/Row'
|
||||
import { DEFAULT_DEADLINE_FROM_NOW } from '../../constants'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useToken } from '../../hooks/Tokens'
|
||||
import { useV1ExchangeContract } from '../../hooks/useContract'
|
||||
import { NEVER_RELOAD, useSingleCallResult } from '../../state/multicall/hooks'
|
||||
import { useIsTransactionPending, useTransactionAdder } from '../../state/transactions/hooks'
|
||||
import { useTokenBalance, useETHBalances } from '../../state/wallet/hooks'
|
||||
import { BackArrow, TYPE } from '../../theme'
|
||||
import { isAddress } from '../../utils'
|
||||
import { BodyWrapper } from '../AppBody'
|
||||
import { EmptyState } from './EmptyState'
|
||||
import { V1LiquidityInfo } from './MigrateV1Exchange'
|
||||
import { AddressZero } from '@ethersproject/constants'
|
||||
import { Dots } from '../../components/swap/styleds'
|
||||
import { Contract } from '@ethersproject/contracts'
|
||||
import { useTotalSupply } from '../../data/TotalSupply'
|
||||
|
||||
const WEI_DENOM = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(18))
|
||||
const ZERO = JSBI.BigInt(0)
|
||||
const ONE = JSBI.BigInt(1)
|
||||
const ZERO_FRACTION = new Fraction(ZERO, ONE)
|
||||
|
||||
function V1PairRemoval({
|
||||
exchangeContract,
|
||||
liquidityTokenAmount,
|
||||
token
|
||||
}: {
|
||||
exchangeContract: Contract
|
||||
liquidityTokenAmount: TokenAmount
|
||||
token: Token
|
||||
}) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const totalSupply = useTotalSupply(liquidityTokenAmount.token)
|
||||
const exchangeETHBalance = useETHBalances([liquidityTokenAmount.token.address])?.[liquidityTokenAmount.token.address]
|
||||
const exchangeTokenBalance = useTokenBalance(liquidityTokenAmount.token.address, token)
|
||||
|
||||
const [confirmingRemoval, setConfirmingRemoval] = useState<boolean>(false)
|
||||
const [pendingRemovalHash, setPendingRemovalHash] = useState<string | null>(null)
|
||||
|
||||
const shareFraction: Fraction = totalSupply ? new Percent(liquidityTokenAmount.raw, totalSupply.raw) : ZERO_FRACTION
|
||||
|
||||
const ethWorth: Fraction = exchangeETHBalance
|
||||
? new Fraction(shareFraction.multiply(exchangeETHBalance).quotient, WEI_DENOM)
|
||||
: ZERO_FRACTION
|
||||
|
||||
const tokenWorth: TokenAmount = exchangeTokenBalance
|
||||
? new TokenAmount(token, shareFraction.multiply(exchangeTokenBalance.raw).quotient)
|
||||
: new TokenAmount(token, ZERO)
|
||||
|
||||
const addTransaction = useTransactionAdder()
|
||||
const isRemovalPending = useIsTransactionPending(pendingRemovalHash)
|
||||
|
||||
const remove = useCallback(() => {
|
||||
if (!liquidityTokenAmount) return
|
||||
|
||||
setConfirmingRemoval(true)
|
||||
exchangeContract
|
||||
.removeLiquidity(
|
||||
liquidityTokenAmount.raw.toString(),
|
||||
1, // min_eth, this is safe because we're removing liquidity
|
||||
1, // min_tokens, this is safe because we're removing liquidity
|
||||
Math.floor(new Date().getTime() / 1000) + DEFAULT_DEADLINE_FROM_NOW
|
||||
)
|
||||
.then((response: TransactionResponse) => {
|
||||
ReactGA.event({
|
||||
category: 'Remove',
|
||||
action: 'V1',
|
||||
label: token?.symbol
|
||||
})
|
||||
|
||||
addTransaction(response, {
|
||||
summary: `Remove ${token.equals(WETH[chainId]) ? 'WETH' : token.symbol}/ETH V1 liquidity`
|
||||
})
|
||||
setPendingRemovalHash(response.hash)
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error)
|
||||
setConfirmingRemoval(false)
|
||||
})
|
||||
}, [exchangeContract, liquidityTokenAmount, token, chainId, addTransaction])
|
||||
|
||||
const noLiquidityTokens = !!liquidityTokenAmount && liquidityTokenAmount.equalTo(ZERO)
|
||||
|
||||
const isSuccessfullyRemoved = !!pendingRemovalHash && !!noLiquidityTokens
|
||||
|
||||
return (
|
||||
<AutoColumn gap="20px">
|
||||
<TYPE.body my={9} style={{ fontWeight: 400 }}>
|
||||
This tool will remove your V1 liquidity and send the underlying assets to your wallet.
|
||||
</TYPE.body>
|
||||
|
||||
<LightCard>
|
||||
<V1LiquidityInfo
|
||||
token={token}
|
||||
liquidityTokenAmount={liquidityTokenAmount}
|
||||
tokenWorth={tokenWorth}
|
||||
ethWorth={ethWorth}
|
||||
/>
|
||||
|
||||
<div style={{ display: 'flex', marginTop: '1rem' }}>
|
||||
<ButtonConfirmed
|
||||
confirmed={isSuccessfullyRemoved}
|
||||
disabled={isSuccessfullyRemoved || noLiquidityTokens || isRemovalPending || confirmingRemoval}
|
||||
onClick={remove}
|
||||
>
|
||||
{isSuccessfullyRemoved ? 'Success' : isRemovalPending ? <Dots>Removing</Dots> : 'Remove'}
|
||||
</ButtonConfirmed>
|
||||
</div>
|
||||
</LightCard>
|
||||
<TYPE.darkGray style={{ textAlign: 'center' }}>
|
||||
{`Your Uniswap V1 ${
|
||||
token.equals(WETH[chainId]) ? 'WETH' : token.symbol
|
||||
}/ETH liquidity will be redeemed for underlying assets.`}
|
||||
</TYPE.darkGray>
|
||||
</AutoColumn>
|
||||
)
|
||||
}
|
||||
|
||||
export default function RemoveV1Exchange({
|
||||
match: {
|
||||
params: { address }
|
||||
}
|
||||
}: RouteComponentProps<{ address: string }>) {
|
||||
const validatedAddress = isAddress(address)
|
||||
const { chainId, account } = useActiveWeb3React()
|
||||
|
||||
const exchangeContract = useV1ExchangeContract(validatedAddress ? validatedAddress : undefined, true)
|
||||
const tokenAddress = useSingleCallResult(exchangeContract, 'tokenAddress', undefined, NEVER_RELOAD)?.result?.[0]
|
||||
const token = useToken(tokenAddress)
|
||||
|
||||
const liquidityToken: Token | undefined = useMemo(
|
||||
() =>
|
||||
validatedAddress && token
|
||||
? new Token(chainId, validatedAddress, 18, `UNI-V1-${token.symbol}`, 'Uniswap V1')
|
||||
: undefined,
|
||||
[chainId, validatedAddress, token]
|
||||
)
|
||||
const userLiquidityBalance = useTokenBalance(account, liquidityToken)
|
||||
|
||||
// redirect for invalid url params
|
||||
if (!validatedAddress || tokenAddress === AddressZero) {
|
||||
console.error('Invalid address in path', address)
|
||||
return <Redirect to="/migrate/v1" />
|
||||
}
|
||||
|
||||
return (
|
||||
<BodyWrapper style={{ padding: 24 }}>
|
||||
<AutoColumn gap="16px">
|
||||
<AutoRow style={{ alignItems: 'center', justifyContent: 'space-between' }} gap="8px">
|
||||
<BackArrow to="/migrate/v1" />
|
||||
<TYPE.mediumHeader>Remove V1 Liquidity</TYPE.mediumHeader>
|
||||
<div>
|
||||
<QuestionHelper text="Remove your Uniswap V1 liquidity tokens." />
|
||||
</div>
|
||||
</AutoRow>
|
||||
|
||||
{!account ? (
|
||||
<TYPE.largeHeader>You must connect an account.</TYPE.largeHeader>
|
||||
) : userLiquidityBalance && token ? (
|
||||
<V1PairRemoval
|
||||
exchangeContract={exchangeContract}
|
||||
liquidityTokenAmount={userLiquidityBalance}
|
||||
token={token}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState message="Loading..." />
|
||||
)}
|
||||
</AutoColumn>
|
||||
</BodyWrapper>
|
||||
)
|
||||
}
|
||||
@@ -1,143 +1,115 @@
|
||||
import { Fraction, JSBI, Token, TokenAmount } from '@uniswap/sdk'
|
||||
import React, { useCallback, useContext, useMemo, useState } from 'react'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import { RouteComponentProps } from 'react-router'
|
||||
import { JSBI, Token } from '@uniswap/sdk'
|
||||
import React, { useCallback, useContext, useMemo, useState, useEffect } from 'react'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { ButtonPrimary } from '../../components/Button'
|
||||
import { AutoColumn } from '../../components/Column'
|
||||
import { AutoRow } from '../../components/Row'
|
||||
import { SearchInput } from '../../components/SearchModal/styleds'
|
||||
import TokenLogo from '../../components/TokenLogo'
|
||||
import { useAllTokenV1Exchanges } from '../../data/V1'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
|
||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||
import { useTokenBalances } from '../../state/wallet/hooks'
|
||||
import { TYPE } from '../../theme'
|
||||
import { GreyCard } from '../../components/Card'
|
||||
import { useToken, useAllTokens } from '../../hooks/Tokens'
|
||||
import { useTokenBalancesWithLoadingIndicator } from '../../state/wallet/hooks'
|
||||
import { BackArrow, TYPE } from '../../theme'
|
||||
import { LightCard } from '../../components/Card'
|
||||
import { BodyWrapper } from '../AppBody'
|
||||
import { EmptyState } from './EmptyState'
|
||||
import V1PositionCard from '../../components/PositionCard/V1'
|
||||
import QuestionHelper from '../../components/QuestionHelper'
|
||||
import { Dots } from '../../components/swap/styleds'
|
||||
import { useAddUserToken } from '../../state/user/hooks'
|
||||
import { isDefaultToken, isCustomAddedToken } from '../../utils'
|
||||
|
||||
const POOL_TOKEN_AMOUNT_MIN = new Fraction(JSBI.BigInt(1), JSBI.BigInt(1000000))
|
||||
|
||||
export function FormattedPoolTokenAmount({ tokenAmount }: { tokenAmount: TokenAmount }) {
|
||||
return (
|
||||
<>
|
||||
{tokenAmount.equalTo(JSBI.BigInt(0))
|
||||
? '0'
|
||||
: tokenAmount.greaterThan(POOL_TOKEN_AMOUNT_MIN)
|
||||
? tokenAmount.toSignificant(6)
|
||||
: `<${POOL_TOKEN_AMOUNT_MIN.toSignificant(1)}`}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function MigrateV1({ history }: RouteComponentProps) {
|
||||
export default function MigrateV1() {
|
||||
const theme = useContext(ThemeContext)
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const allV1Exchanges = useAllTokenV1Exchanges()
|
||||
|
||||
const v1LiquidityTokens: Token[] = useMemo(() => {
|
||||
return Object.keys(allV1Exchanges).map(exchangeAddress => new Token(chainId, exchangeAddress, 18))
|
||||
}, [chainId, allV1Exchanges])
|
||||
|
||||
const v1LiquidityBalances = useTokenBalances(account, v1LiquidityTokens)
|
||||
|
||||
const [tokenSearch, setTokenSearch] = useState<string>('')
|
||||
const handleTokenSearchChange = useCallback(e => setTokenSearch(e.target.value), [setTokenSearch])
|
||||
|
||||
const searchedToken: Token | undefined = useTokenByAddressAndAutomaticallyAdd(tokenSearch)
|
||||
// automatically add the search token
|
||||
const token = useToken(tokenSearch)
|
||||
const isDefault = isDefaultToken(token)
|
||||
const allTokens = useAllTokens()
|
||||
const isCustomAdded = isCustomAddedToken(allTokens, token)
|
||||
const addToken = useAddUserToken()
|
||||
useEffect(() => {
|
||||
if (token && !isDefault && !isCustomAdded) {
|
||||
addToken(token)
|
||||
}
|
||||
}, [token, isDefault, isCustomAdded, addToken])
|
||||
|
||||
const unmigratedLiquidityExchangeAddresses: TokenAmount[] = useMemo(
|
||||
() =>
|
||||
Object.keys(v1LiquidityBalances)
|
||||
.filter(tokenAddress =>
|
||||
v1LiquidityBalances[tokenAddress]
|
||||
? JSBI.greaterThan(v1LiquidityBalances[tokenAddress]?.raw, JSBI.BigInt(0))
|
||||
: false
|
||||
)
|
||||
.map(tokenAddress => v1LiquidityBalances[tokenAddress])
|
||||
.sort((a1, a2) => {
|
||||
if (searchedToken) {
|
||||
if (allV1Exchanges[a1.token.address].address === searchedToken.address) return -1
|
||||
if (allV1Exchanges[a2.token.address].address === searchedToken.address) return 1
|
||||
}
|
||||
return a1.token.address < a2.token.address ? -1 : 1
|
||||
}),
|
||||
[allV1Exchanges, searchedToken, v1LiquidityBalances]
|
||||
// get V1 LP balances
|
||||
const V1Exchanges = useAllTokenV1Exchanges()
|
||||
const V1LiquidityTokens: Token[] = useMemo(() => {
|
||||
return Object.keys(V1Exchanges).map(
|
||||
exchangeAddress => new Token(chainId, exchangeAddress, 18, 'UNI-V1', 'Uniswap V1')
|
||||
)
|
||||
}, [chainId, V1Exchanges])
|
||||
const [V1LiquidityBalances, V1LiquidityBalancesLoading] = useTokenBalancesWithLoadingIndicator(
|
||||
account,
|
||||
V1LiquidityTokens
|
||||
)
|
||||
const allV1PairsWithLiquidity = V1LiquidityTokens.filter(V1LiquidityToken => {
|
||||
return (
|
||||
V1LiquidityBalances?.[V1LiquidityToken.address] &&
|
||||
JSBI.greaterThan(V1LiquidityBalances[V1LiquidityToken.address].raw, JSBI.BigInt(0))
|
||||
)
|
||||
}).map(V1LiquidityToken => {
|
||||
return (
|
||||
<V1PositionCard
|
||||
key={V1LiquidityToken.address}
|
||||
token={V1Exchanges[V1LiquidityToken.address]}
|
||||
V1LiquidityBalance={V1LiquidityBalances[V1LiquidityToken.address]}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const toggleWalletModal = useWalletModalToggle()
|
||||
|
||||
const handleBackClick = useCallback(() => {
|
||||
history.push('/pool')
|
||||
}, [history])
|
||||
// should never always be false, because a V1 exhchange exists for WETH on all testnets
|
||||
const isLoading = Object.keys(V1Exchanges)?.length === 0 || V1LiquidityBalancesLoading
|
||||
|
||||
return (
|
||||
<BodyWrapper style={{ maxWidth: 450, padding: 24 }}>
|
||||
<AutoColumn gap="24px">
|
||||
<AutoRow style={{ justifyContent: 'space-between' }}>
|
||||
<BodyWrapper style={{ padding: 24 }}>
|
||||
<AutoColumn gap="16px">
|
||||
<AutoRow style={{ alignItems: 'center', justifyContent: 'space-between' }} gap="8px">
|
||||
<BackArrow to="/pool" />
|
||||
<TYPE.mediumHeader>Migrate V1 Liquidity</TYPE.mediumHeader>
|
||||
<div>
|
||||
<ArrowLeft style={{ cursor: 'pointer' }} onClick={handleBackClick} />
|
||||
<QuestionHelper text="Migrate your liquidity tokens from Uniswap V1 to Uniswap V2." />
|
||||
</div>
|
||||
<TYPE.largeHeader>Migrate Liquidity</TYPE.largeHeader>
|
||||
<div></div>
|
||||
</AutoRow>
|
||||
<GreyCard>
|
||||
<TYPE.main style={{ lineHeight: '140%' }}>
|
||||
For each pool, approve the migration helper and click migrate liquidity. Your liquidity will be withdrawn
|
||||
from Uniswap V1 and deposited into Uniswap V2.
|
||||
</TYPE.main>
|
||||
<TYPE.black padding={'1rem 0 0 0'} style={{ lineHeight: '140%' }}>
|
||||
If your liquidity does not appear below automatically, you may need to find it by pasting the token address
|
||||
into the search box below.
|
||||
</TYPE.black>
|
||||
</GreyCard>
|
||||
<AutoRow>
|
||||
<SearchInput
|
||||
value={tokenSearch}
|
||||
onChange={handleTokenSearchChange}
|
||||
placeholder="Find liquidity by pasting a token address."
|
||||
/>
|
||||
</AutoRow>
|
||||
|
||||
{unmigratedLiquidityExchangeAddresses.map(poolTokenAmount => (
|
||||
<div
|
||||
key={poolTokenAmount.token.address}
|
||||
style={{ borderRadius: '20px', padding: 16, backgroundColor: theme.bg2 }}
|
||||
>
|
||||
<AutoRow style={{ justifyContent: 'space-between' }}>
|
||||
<AutoRow style={{ justifyContent: 'flex-start', width: 'fit-content' }}>
|
||||
<TokenLogo size="32px" address={allV1Exchanges[poolTokenAmount.token.address].address} />{' '}
|
||||
<div style={{ marginLeft: '.75rem' }}>
|
||||
<TYPE.main fontWeight={600}>
|
||||
<FormattedPoolTokenAmount tokenAmount={poolTokenAmount} />
|
||||
</TYPE.main>
|
||||
<TYPE.main fontWeight={500}>
|
||||
{allV1Exchanges[poolTokenAmount.token.address].symbol} Pool Tokens
|
||||
</TYPE.main>
|
||||
</div>
|
||||
</AutoRow>
|
||||
<div>
|
||||
<ButtonPrimary
|
||||
onClick={() => {
|
||||
history.push(`/migrate/v1/${poolTokenAmount.token.address}`)
|
||||
}}
|
||||
style={{ padding: '8px 12px', borderRadius: '12px' }}
|
||||
>
|
||||
Migrate
|
||||
</ButtonPrimary>
|
||||
</div>
|
||||
<TYPE.body style={{ marginBottom: 8, fontWeight: 400 }}>
|
||||
For each pool shown below, click migrate to remove your liquidity from Uniswap V1 and deposit it into Uniswap
|
||||
V2.
|
||||
</TYPE.body>
|
||||
|
||||
{!account ? (
|
||||
<LightCard padding="40px">
|
||||
<TYPE.body color={theme.text3} textAlign="center">
|
||||
Connect to a wallet to view your V1 liquidity.
|
||||
</TYPE.body>
|
||||
</LightCard>
|
||||
) : isLoading ? (
|
||||
<LightCard padding="40px">
|
||||
<TYPE.body color={theme.text3} textAlign="center">
|
||||
<Dots>Loading</Dots>
|
||||
</TYPE.body>
|
||||
</LightCard>
|
||||
) : (
|
||||
<>
|
||||
<AutoRow>
|
||||
<SearchInput
|
||||
value={tokenSearch}
|
||||
onChange={handleTokenSearchChange}
|
||||
placeholder="Enter a token address to find liquidity"
|
||||
/>
|
||||
</AutoRow>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{account && unmigratedLiquidityExchangeAddresses.length === 0 ? (
|
||||
<EmptyState message="No V1 Liquidity found." />
|
||||
) : null}
|
||||
|
||||
{!account ? <ButtonPrimary onClick={toggleWalletModal}>Connect to a wallet</ButtonPrimary> : null}
|
||||
{allV1PairsWithLiquidity?.length > 0 ? (
|
||||
<>{allV1PairsWithLiquidity}</>
|
||||
) : (
|
||||
<EmptyState message="No V1 Liquidity found." />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</BodyWrapper>
|
||||
)
|
||||
|
||||
@@ -1,124 +1,113 @@
|
||||
import React, { useState, useContext } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import { JSBI, Pair } from '@uniswap/sdk'
|
||||
import { RouteComponentProps } from 'react-router-dom'
|
||||
import React, { useContext } 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 SearchModal from '../../components/SearchModal'
|
||||
import PositionCard from '../../components/PositionCard'
|
||||
import FullPositionCard from '../../components/PositionCard'
|
||||
import { useUserHasLiquidityInAllTokens } from '../../data/V1'
|
||||
import { useTokenBalances } from '../../state/wallet/hooks'
|
||||
import { useTokenBalancesWithLoadingIndicator } from '../../state/wallet/hooks'
|
||||
import { StyledInternalLink, TYPE } from '../../theme'
|
||||
import { Text } from 'rebass'
|
||||
import { LightCard } from '../../components/Card'
|
||||
import { RowBetween } from '../../components/Row'
|
||||
import { ButtonPrimary, ButtonSecondary } from '../../components/Button'
|
||||
import { AutoColumn, ColumnCenter } from '../../components/Column'
|
||||
import { AutoColumn } from '../../components/Column'
|
||||
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { usePair } from '../../data/Reserves'
|
||||
import { usePairs } from '../../data/Reserves'
|
||||
import { useAllDummyPairs } from '../../state/user/hooks'
|
||||
import AppBody from '../AppBody'
|
||||
import { Dots } from '../../components/swap/styleds'
|
||||
|
||||
const Positions = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const FixedBottom = styled.div`
|
||||
position: absolute;
|
||||
bottom: -80px;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
function PositionCardWrapper({ dummyPair }: { dummyPair: Pair }) {
|
||||
const pair = usePair(dummyPair.token0, dummyPair.token1)
|
||||
return <PositionCard pair={pair} />
|
||||
}
|
||||
|
||||
export default function Pool({ history }: RouteComponentProps) {
|
||||
export default function Pool() {
|
||||
const theme = useContext(ThemeContext)
|
||||
const { account } = useActiveWeb3React()
|
||||
const [showPoolSearch, setShowPoolSearch] = useState(false)
|
||||
|
||||
// initiate listener for LP balances
|
||||
const pairs = useAllDummyPairs()
|
||||
const pairBalances = useTokenBalances(
|
||||
account,
|
||||
pairs?.map(p => p.liquidityToken)
|
||||
// fetch the user's balances of all tracked V2 LP tokens
|
||||
const v2DummyPairs = useAllDummyPairs()
|
||||
const [v2PairsBalances, fetchingV2PairBalances] = useTokenBalancesWithLoadingIndicator(
|
||||
account ?? undefined,
|
||||
v2DummyPairs?.map(p => p.liquidityToken)
|
||||
)
|
||||
// fetch the reserves for all V2 pools in which the user has a balance
|
||||
const v2DummyPairsWithABalance = v2DummyPairs.filter(dummyPair =>
|
||||
v2PairsBalances[dummyPair.liquidityToken.address]?.greaterThan('0')
|
||||
)
|
||||
const v2Pairs = usePairs(
|
||||
v2DummyPairsWithABalance.map(V2DummyPairWithABalance => [
|
||||
V2DummyPairWithABalance.token0,
|
||||
V2DummyPairWithABalance.token1
|
||||
])
|
||||
)
|
||||
const v2IsLoading =
|
||||
fetchingV2PairBalances || v2Pairs?.length < v2DummyPairsWithABalance.length || v2Pairs?.some(V2Pair => !V2Pair)
|
||||
|
||||
const filteredExchangeList = pairs
|
||||
.filter(pair => {
|
||||
return (
|
||||
pairBalances?.[pair.liquidityToken.address] &&
|
||||
JSBI.greaterThan(pairBalances[pair.liquidityToken.address].raw, JSBI.BigInt(0))
|
||||
)
|
||||
})
|
||||
.map((pair, i) => {
|
||||
return <PositionCardWrapper key={i} dummyPair={pair} />
|
||||
})
|
||||
const allV2PairsWithLiquidity = v2Pairs
|
||||
.filter((v2Pair): v2Pair is Pair => Boolean(v2Pair))
|
||||
.map(V2Pair => <FullPositionCard key={V2Pair.liquidityToken.address} pair={V2Pair} />)
|
||||
|
||||
const hasV1Liquidity = useUserHasLiquidityInAllTokens()
|
||||
|
||||
return (
|
||||
<AppBody>
|
||||
<AutoColumn gap="lg" justify="center">
|
||||
<ButtonPrimary
|
||||
id="join-pool-button"
|
||||
padding="16px"
|
||||
onClick={() => {
|
||||
setShowPoolSearch(true)
|
||||
}}
|
||||
>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Join {filteredExchangeList?.length > 0 ? 'another' : 'a'} pool
|
||||
</Text>
|
||||
</ButtonPrimary>
|
||||
<Positions>
|
||||
<AutoColumn gap="12px">
|
||||
<>
|
||||
<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>
|
||||
|
||||
<AutoColumn gap="12px" style={{ width: '100%' }}>
|
||||
<RowBetween padding={'0 8px'}>
|
||||
<Text color={theme.text1} fontWeight={500}>
|
||||
Your Pooled Liquidity
|
||||
Your Liquidity
|
||||
</Text>
|
||||
<Question text="When you add liquidity, you are given pool tokens that represent your share. If you don’t see a pool you joined in this list, try importing a pool below." />
|
||||
</RowBetween>
|
||||
{filteredExchangeList?.length === 0 && (
|
||||
<LightCard
|
||||
padding="40px
|
||||
"
|
||||
>
|
||||
|
||||
{!account ? (
|
||||
<LightCard padding="40px">
|
||||
<TYPE.body color={theme.text3} textAlign="center">
|
||||
Connect to a wallet to view your liquidity.
|
||||
</TYPE.body>
|
||||
</LightCard>
|
||||
) : v2IsLoading ? (
|
||||
<LightCard padding="40px">
|
||||
<TYPE.body color={theme.text3} textAlign="center">
|
||||
<Dots>Loading</Dots>
|
||||
</TYPE.body>
|
||||
</LightCard>
|
||||
) : allV2PairsWithLiquidity?.length > 0 ? (
|
||||
<>{allV2PairsWithLiquidity}</>
|
||||
) : (
|
||||
<LightCard padding="40px">
|
||||
<TYPE.body color={theme.text3} textAlign="center">
|
||||
No liquidity found.
|
||||
</TYPE.body>
|
||||
</LightCard>
|
||||
)}
|
||||
{filteredExchangeList}
|
||||
<Text textAlign="center" fontSize={14} style={{ padding: '.5rem 0 .5rem 0' }}>
|
||||
{!hasV1Liquidity ? (
|
||||
<>
|
||||
{filteredExchangeList?.length !== 0 ? `Don't see a pool you joined? ` : 'Already joined a pool? '}{' '}
|
||||
<StyledInternalLink id="import-pool-link" to="/find">
|
||||
Import it.
|
||||
</StyledInternalLink>
|
||||
</>
|
||||
) : (
|
||||
<StyledInternalLink id="migrate-v1-liquidity-link" to="/migrate/v1">
|
||||
Migrate your V1 liquidity.
|
||||
|
||||
<div>
|
||||
<Text textAlign="center" fontSize={14} style={{ padding: '.5rem 0 .5rem 0' }}>
|
||||
{hasV1Liquidity ? 'Uniswap V1 liquidity found!' : "Don't see a pool you joined?"}{' '}
|
||||
<StyledInternalLink id="import-pool-link" to={hasV1Liquidity ? '/migrate/v1' : '/find'}>
|
||||
{hasV1Liquidity ? 'Migrate now.' : 'Import it.'}
|
||||
</StyledInternalLink>
|
||||
)}
|
||||
</Text>
|
||||
</Text>
|
||||
</div>
|
||||
</AutoColumn>
|
||||
<FixedBottom>
|
||||
<ColumnCenter>
|
||||
<ButtonSecondary width="136px" padding="8px" borderRadius="10px" onClick={() => history.push('/create')}>
|
||||
+ Create Pool
|
||||
</ButtonSecondary>
|
||||
</ColumnCenter>
|
||||
</FixedBottom>
|
||||
</Positions>
|
||||
<SearchModal isOpen={showPoolSearch} onDismiss={() => setShowPoolSearch(false)} />
|
||||
</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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,9 +17,13 @@ export const MaxButton = styled.button<{ width: string }>`
|
||||
border: 1px solid ${({ theme }) => theme.primary5};
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
padding: 0.25rem 0.5rem;
|
||||
`};
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
margin-right: 0.5rem;
|
||||
margin: 0.25rem;
|
||||
overflow: hidden;
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
:hover {
|
||||
border: 1px solid ${({ theme }) => theme.primary1};
|
||||
|
||||
4
src/pages/Pool/tsconfig.json
Normal file
4
src/pages/Pool/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.strict.json",
|
||||
"include": ["**/*"]
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { JSBI, Pair, Token, TokenAmount } from '@uniswap/sdk'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { JSBI, Pair, Token, TokenAmount, WETH } from '@uniswap/sdk'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { Plus } from 'react-feather'
|
||||
import { RouteComponentProps } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
import { ButtonDropwdown, ButtonDropwdownLight, ButtonPrimary } from '../../components/Button'
|
||||
import { ButtonDropdownLight } from '../../components/Button'
|
||||
import { LightCard } from '../../components/Card'
|
||||
import { AutoColumn, ColumnCenter } from '../../components/Column'
|
||||
import PositionCard from '../../components/PositionCard'
|
||||
import { FindPoolTabs } from '../../components/NavigationTabs'
|
||||
import { MinimalPositionCard } from '../../components/PositionCard'
|
||||
import Row from '../../components/Row'
|
||||
import SearchModal from '../../components/SearchModal'
|
||||
import TokenSearchModal from '../../components/SearchModal/TokenSearchModal'
|
||||
import TokenLogo from '../../components/TokenLogo'
|
||||
import { usePair } from '../../data/Reserves'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
@@ -23,138 +23,135 @@ enum Fields {
|
||||
TOKEN1 = 1
|
||||
}
|
||||
|
||||
export default function PoolFinder({ history }: RouteComponentProps) {
|
||||
const { account } = useActiveWeb3React()
|
||||
export default function PoolFinder() {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
|
||||
const [showSearch, setShowSearch] = useState<boolean>(false)
|
||||
const [activeField, setActiveField] = useState<number>(Fields.TOKEN0)
|
||||
const [activeField, setActiveField] = useState<number>(Fields.TOKEN1)
|
||||
|
||||
const [token0Address, setToken0Address] = useState<string>()
|
||||
const [token0Address, setToken0Address] = useState<string>(chainId ? WETH[chainId].address : '')
|
||||
const [token1Address, setToken1Address] = useState<string>()
|
||||
const token0: Token = useToken(token0Address)
|
||||
const token1: Token = useToken(token1Address)
|
||||
const token0: Token | null | undefined = useToken(token0Address)
|
||||
const token1: Token | null | undefined = useToken(token1Address)
|
||||
|
||||
const pair: Pair = usePair(token0, token1)
|
||||
const pair: Pair | null | undefined = usePair(token0 ?? undefined, token1 ?? undefined)
|
||||
const addPair = usePairAdder()
|
||||
|
||||
useEffect(() => {
|
||||
if (pair) {
|
||||
addPair(pair)
|
||||
}
|
||||
}, [pair, addPair])
|
||||
|
||||
const position: TokenAmount = useTokenBalanceTreatingWETHasETH(account, pair?.liquidityToken)
|
||||
|
||||
const newPair: boolean =
|
||||
pair === null ||
|
||||
(!!pair && JSBI.equal(pair.reserve0.raw, JSBI.BigInt(0)) && JSBI.equal(pair.reserve1.raw, JSBI.BigInt(0)))
|
||||
const allowImport: boolean = position && JSBI.greaterThan(position.raw, JSBI.BigInt(0))
|
||||
|
||||
const position: TokenAmount | undefined = useTokenBalanceTreatingWETHasETH(account ?? undefined, pair?.liquidityToken)
|
||||
const poolImported: boolean = !!position && JSBI.greaterThan(position.raw, JSBI.BigInt(0))
|
||||
|
||||
const handleTokenSelect = useCallback(
|
||||
(address: string) => {
|
||||
activeField === Fields.TOKEN0 ? setToken0Address(address) : setToken1Address(address)
|
||||
},
|
||||
[activeField]
|
||||
)
|
||||
|
||||
const handleSearchDismiss = useCallback(() => {
|
||||
setShowSearch(false)
|
||||
}, [setShowSearch])
|
||||
|
||||
return (
|
||||
<AppBody>
|
||||
<FindPoolTabs />
|
||||
<AutoColumn gap="md">
|
||||
{!token0Address ? (
|
||||
<ButtonDropwdown
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN0)
|
||||
}}
|
||||
>
|
||||
<Text fontSize={20}>Select first token</Text>
|
||||
</ButtonDropwdown>
|
||||
) : (
|
||||
<ButtonDropwdownLight
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN0)
|
||||
}}
|
||||
>
|
||||
<ButtonDropdownLight
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN0)
|
||||
}}
|
||||
>
|
||||
{token0 ? (
|
||||
<Row>
|
||||
<TokenLogo address={token0Address} />
|
||||
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
|
||||
{token0?.symbol}
|
||||
{token0.symbol}
|
||||
</Text>
|
||||
</Row>
|
||||
</ButtonDropwdownLight>
|
||||
)}
|
||||
) : (
|
||||
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
|
||||
Select a Token
|
||||
</Text>
|
||||
)}
|
||||
</ButtonDropdownLight>
|
||||
|
||||
<ColumnCenter>
|
||||
<Plus size="16" color="#888D9B" />
|
||||
</ColumnCenter>
|
||||
{!token1Address ? (
|
||||
<ButtonDropwdown
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN1)
|
||||
}}
|
||||
>
|
||||
<Text fontSize={20}>Select second token</Text>
|
||||
</ButtonDropwdown>
|
||||
) : (
|
||||
<ButtonDropwdownLight
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN1)
|
||||
}}
|
||||
>
|
||||
|
||||
<ButtonDropdownLight
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN1)
|
||||
}}
|
||||
>
|
||||
{token1 ? (
|
||||
<Row>
|
||||
<TokenLogo address={token1Address} />
|
||||
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
|
||||
{token1?.symbol}
|
||||
{token1.symbol}
|
||||
</Text>
|
||||
</Row>
|
||||
</ButtonDropwdownLight>
|
||||
)}
|
||||
{allowImport && (
|
||||
) : (
|
||||
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
|
||||
Select a Token
|
||||
</Text>
|
||||
)}
|
||||
</ButtonDropdownLight>
|
||||
|
||||
{poolImported && (
|
||||
<ColumnCenter
|
||||
style={{ justifyItems: 'center', backgroundColor: '', padding: '12px 0px', borderRadius: '12px' }}
|
||||
>
|
||||
<Text textAlign="center" fontWeight={500} color="">
|
||||
Pool Imported!
|
||||
Pool Found!
|
||||
</Text>
|
||||
</ColumnCenter>
|
||||
)}
|
||||
|
||||
{position ? (
|
||||
!JSBI.equal(position.raw, JSBI.BigInt(0)) ? (
|
||||
<PositionCard pair={pair} minimal={true} border="1px solid #CED0D9" />
|
||||
poolImported ? (
|
||||
<MinimalPositionCard pair={pair} border="1px solid #CED0D9" />
|
||||
) : (
|
||||
<LightCard padding="45px 10px">
|
||||
<AutoColumn gap="sm" justify="center">
|
||||
<Text textAlign="center">Pool found, you don’t have liquidity on this pair yet.</Text>
|
||||
<StyledInternalLink to={`/add/${token0Address}-${token1Address}`}>
|
||||
<Text textAlign="center">Add liquidity to this pair instead.</Text>
|
||||
<Text textAlign="center">You don’t have liquidity in this pool yet.</Text>
|
||||
<StyledInternalLink to={`/add/${token0?.address}/${token1?.address}`}>
|
||||
<Text textAlign="center">Add liquidity.</Text>
|
||||
</StyledInternalLink>
|
||||
</AutoColumn>
|
||||
</LightCard>
|
||||
)
|
||||
) : newPair ? (
|
||||
<LightCard padding="45px">
|
||||
<LightCard padding="45px 10px">
|
||||
<AutoColumn gap="sm" justify="center">
|
||||
<Text color="">No pool found.</Text>
|
||||
<StyledInternalLink to={`/add/${token0Address}-${token1Address}`}>Create pool?</StyledInternalLink>
|
||||
<Text textAlign="center">No pool found.</Text>
|
||||
<StyledInternalLink to={`/add/${token0Address}/${token1Address}`}>Create pool?</StyledInternalLink>
|
||||
</AutoColumn>
|
||||
</LightCard>
|
||||
) : (
|
||||
<LightCard padding={'45px'}>
|
||||
<Text color="#C3C5CB" textAlign="center">
|
||||
Select a token pair to find your liquidity.
|
||||
<LightCard padding="45px 10px">
|
||||
<Text textAlign="center">
|
||||
{!account ? 'Connect to a wallet to find pools' : 'Select a token to find your liquidity.'}
|
||||
</Text>
|
||||
</LightCard>
|
||||
)}
|
||||
|
||||
<ButtonPrimary disabled={!allowImport} onClick={() => history.goBack()}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Close
|
||||
</Text>
|
||||
</ButtonPrimary>
|
||||
</AutoColumn>
|
||||
<SearchModal
|
||||
|
||||
<TokenSearchModal
|
||||
isOpen={showSearch}
|
||||
filterType="tokens"
|
||||
onTokenSelect={address => {
|
||||
activeField === Fields.TOKEN0 ? setToken0Address(address) : setToken1Address(address)
|
||||
}}
|
||||
onDismiss={() => {
|
||||
setShowSearch(false)
|
||||
}}
|
||||
onTokenSelect={handleTokenSelect}
|
||||
onDismiss={handleSearchDismiss}
|
||||
showCommonBases
|
||||
hiddenToken={activeField === Fields.TOKEN0 ? token1Address : token0Address}
|
||||
/>
|
||||
</AppBody>
|
||||
|
||||
4
src/pages/PoolFinder/tsconfig.json
Normal file
4
src/pages/PoolFinder/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.strict.json",
|
||||
"include": ["**/*"]
|
||||
}
|
||||
@@ -7,18 +7,19 @@ import ReactGA from 'react-ga'
|
||||
import { RouteComponentProps } from 'react-router'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { ButtonConfirmed, ButtonPrimary, ButtonLight, ButtonError } from '../../components/Button'
|
||||
import { ButtonPrimary, ButtonLight, ButtonError, ButtonConfirmed } from '../../components/Button'
|
||||
import { LightCard } from '../../components/Card'
|
||||
import { AutoColumn, ColumnCenter } from '../../components/Column'
|
||||
import ConfirmationModal from '../../components/ConfirmationModal'
|
||||
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
||||
import DoubleLogo from '../../components/DoubleLogo'
|
||||
import PositionCard from '../../components/PositionCard'
|
||||
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 { ROUTER_ADDRESS, DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
|
||||
import { ROUTER_ADDRESS } from '../../constants'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { usePairContract } from '../../hooks/useContract'
|
||||
|
||||
@@ -31,9 +32,9 @@ import { useApproveCallback, ApprovalState } from '../../hooks/useApproveCallbac
|
||||
import { Dots } from '../../components/swap/styleds'
|
||||
import { useDefaultsFromURLMatchParams, useBurnActionHandlers } from '../../state/burn/hooks'
|
||||
import { useDerivedBurnInfo, useBurnState } from '../../state/burn/hooks'
|
||||
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
|
||||
import { Field } from '../../state/burn/actions'
|
||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||
import { useUserDeadline, useUserSlippageTolerance } from '../../state/user/hooks'
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
|
||||
export default function RemoveLiquidity({ match: { params } }: RouteComponentProps<{ tokens: string }>) {
|
||||
@@ -48,19 +49,18 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
|
||||
// burn state
|
||||
const { independentField, typedValue } = useBurnState()
|
||||
const { tokens, pair, route, parsedAmounts, error } = useDerivedBurnInfo()
|
||||
const { onUserInput } = useBurnActionHandlers()
|
||||
const { onUserInput: _onUserInput } = useBurnActionHandlers()
|
||||
const isValid = !error
|
||||
|
||||
// modal and loading
|
||||
const [showDetailed, setShowDetailed] = useState<boolean>(false) // toggling detailed view
|
||||
const [showAdvanced, setShowAdvanced] = useState<boolean>(false) // toggling slippage, deadline, etc. on and off
|
||||
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 [showConfirm, setShowConfirm] = useState<boolean>(false)
|
||||
const [showDetailed, setShowDetailed] = useState<boolean>(false)
|
||||
const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirm
|
||||
|
||||
// tx parameters
|
||||
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
|
||||
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
|
||||
// txn values
|
||||
const [txHash, setTxHash] = useState<string>('')
|
||||
const [deadline] = useUserDeadline()
|
||||
const [allowedSlippage] = useUserSlippageTolerance()
|
||||
|
||||
const formattedAmounts = {
|
||||
[Field.LIQUIDITY_PERCENT]: parsedAmounts[Field.LIQUIDITY_PERCENT].equalTo('0')
|
||||
@@ -144,6 +144,15 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
|
||||
})
|
||||
}
|
||||
|
||||
// wrapped onUserInput to clear signatures
|
||||
const onUserInput = useCallback(
|
||||
(field: Field, typedValue: string) => {
|
||||
setSignatureData(null)
|
||||
return _onUserInput(field, typedValue)
|
||||
},
|
||||
[_onUserInput]
|
||||
)
|
||||
|
||||
// tx sending
|
||||
const addTransaction = useTransactionAdder()
|
||||
async function onRemove() {
|
||||
@@ -359,34 +368,11 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
|
||||
</RowBetween>
|
||||
</>
|
||||
)}
|
||||
<RowBetween mt="1rem">
|
||||
<ButtonConfirmed
|
||||
onClick={onAttemptToApprove}
|
||||
confirmed={approval === ApprovalState.APPROVED || signatureData !== null}
|
||||
disabled={approval !== ApprovalState.NOT_APPROVED || signatureData !== null}
|
||||
mr="0.5rem"
|
||||
fontWeight={500}
|
||||
fontSize={20}
|
||||
>
|
||||
{approval === ApprovalState.PENDING ? (
|
||||
<Dots>Approving</Dots>
|
||||
) : approval === ApprovalState.APPROVED || signatureData !== null ? (
|
||||
'Approved'
|
||||
) : (
|
||||
'Approve'
|
||||
)}
|
||||
</ButtonConfirmed>
|
||||
|
||||
<ButtonPrimary
|
||||
disabled={!(approval === ApprovalState.APPROVED || signatureData !== null)}
|
||||
onClick={onRemove}
|
||||
ml="0.5rem"
|
||||
>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Confirm
|
||||
</Text>
|
||||
</ButtonPrimary>
|
||||
</RowBetween>
|
||||
<ButtonPrimary disabled={!(approval === ApprovalState.APPROVED || signatureData !== null)} onClick={onRemove}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Confirm
|
||||
</Text>
|
||||
</ButtonPrimary>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -405,6 +391,7 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
|
||||
return (
|
||||
<>
|
||||
<AppBody>
|
||||
<AddRemoveTabs adding={false} />
|
||||
<Wrapper>
|
||||
<ConfirmationModal
|
||||
isOpen={showConfirm}
|
||||
@@ -571,37 +558,44 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
|
||||
{!account ? (
|
||||
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
|
||||
) : (
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
setShowConfirm(true)
|
||||
}}
|
||||
disabled={!isValid}
|
||||
error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]}
|
||||
>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
{error || 'Remove'}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
<RowBetween>
|
||||
<ButtonConfirmed
|
||||
onClick={onAttemptToApprove}
|
||||
confirmed={approval === ApprovalState.APPROVED || signatureData !== null}
|
||||
disabled={approval !== ApprovalState.NOT_APPROVED || signatureData !== null}
|
||||
mr="0.5rem"
|
||||
fontWeight={500}
|
||||
fontSize={16}
|
||||
>
|
||||
{approval === ApprovalState.PENDING ? (
|
||||
<Dots>Approving</Dots>
|
||||
) : approval === ApprovalState.APPROVED || signatureData !== null ? (
|
||||
'Approved'
|
||||
) : (
|
||||
'Approve'
|
||||
)}
|
||||
</ButtonConfirmed>
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
setShowConfirm(true)
|
||||
}}
|
||||
disabled={!isValid || (signatureData === null && approval !== ApprovalState.APPROVED)}
|
||||
error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]}
|
||||
>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{error || 'Remove'}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
</RowBetween>
|
||||
)}
|
||||
</div>
|
||||
</AutoColumn>
|
||||
</Wrapper>
|
||||
</AppBody>
|
||||
|
||||
{isValid ? (
|
||||
<AdvancedSwapDetailsDropdown
|
||||
rawSlippage={allowedSlippage}
|
||||
deadline={deadline}
|
||||
showAdvanced={showAdvanced}
|
||||
setShowAdvanced={setShowAdvanced}
|
||||
setDeadline={setDeadline}
|
||||
setRawSlippage={setAllowedSlippage}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{pair ? (
|
||||
<AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
|
||||
<PositionCard pair={pair} minimal={true} />
|
||||
<MinimalPositionCard pair={pair} />
|
||||
</AutoColumn>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
@@ -1,565 +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 { RouteComponentProps } from 'react-router-dom'
|
||||
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 QuestionHelper from '../../components/QuestionHelper'
|
||||
import { AutoRow, RowBetween, RowFixed } from '../../components/Row'
|
||||
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
|
||||
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
|
||||
import FormattedPriceImpact from '../../components/swap/FormattedPriceImpact'
|
||||
import SwapModalFooter from '../../components/swap/SwapModalFooter'
|
||||
import { ArrowWrapper, BottomGrouping, Dots, InputGroup, StyledNumerical, Wrapper } from '../../components/swap/styleds'
|
||||
import TradePrice from '../../components/swap/TradePrice'
|
||||
import { TransferModalHeader } from '../../components/swap/TransferModalHeader'
|
||||
import V1TradeLink from '../../components/swap/V1TradeLink'
|
||||
import TokenLogo from '../../components/TokenLogo'
|
||||
import { TokenWarningCards } from '../../components/TokenWarningCard'
|
||||
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, MIN_ETH } from '../../constants'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useApproveCallbackFromTrade, ApprovalState } from '../../hooks/useApproveCallback'
|
||||
import { useSendCallback } from '../../hooks/useSendCallback'
|
||||
import { useSwapCallback } from '../../hooks/useSwapCallback'
|
||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||
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 { PriceSlippageWarningCard } from '../../components/swap/PriceSlippageWarningCard'
|
||||
|
||||
export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
useDefaultsFromURLSearch(search)
|
||||
|
||||
// text translation
|
||||
// const { t } = useTranslation()
|
||||
const { chainId, account } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
// toggle wallet when disconnected
|
||||
const toggleWalletModal = useWalletModalToggle()
|
||||
|
||||
// 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 {
|
||||
parsedAmounts,
|
||||
bestTrade,
|
||||
tokenBalances,
|
||||
tokens,
|
||||
error: swapError,
|
||||
v1TradeLinkIfBetter
|
||||
} = useDerivedSwapInfo()
|
||||
const isSwapValid = !swapError && !recipientError && bestTrade
|
||||
|
||||
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
|
||||
|
||||
// modal and loading
|
||||
const [showAdvanced, setShowAdvanced] = useState<boolean>(false) // toggling slippage, deadline, etc. on and off
|
||||
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>('')
|
||||
|
||||
// tx parameters
|
||||
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
|
||||
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
|
||||
|
||||
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].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))
|
||||
|
||||
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}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
label={independentField === Field.INPUT && parsedAmounts[Field.OUTPUT] ? 'To (estimated)' : 'To'}
|
||||
showMaxButton={false}
|
||||
token={tokens[Field.OUTPUT]}
|
||||
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>
|
||||
{!noRoute && tokens[Field.OUTPUT] && tokens[Field.INPUT] && (
|
||||
<Card padding={'.25rem .75rem 0 .75rem'} borderRadius={'20px'}>
|
||||
<AutoColumn gap="4px">
|
||||
<RowBetween align="center">
|
||||
<Text fontWeight={500} fontSize={14} color={theme.text2}>
|
||||
Price
|
||||
</Text>
|
||||
<TradePrice showInverted={showInverted} setShowInverted={setShowInverted} trade={bestTrade} />
|
||||
</RowBetween>
|
||||
|
||||
{bestTrade && severity > 1 && (
|
||||
<RowBetween>
|
||||
<TYPE.main
|
||||
style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }}
|
||||
fontSize={14}
|
||||
>
|
||||
Price Impact
|
||||
</TYPE.main>
|
||||
<RowFixed>
|
||||
<FormattedPriceImpact priceImpact={priceImpactWithoutFee} />
|
||||
<QuestionHelper text="The difference between the market price and estimated price due to trade size." />
|
||||
</RowFixed>
|
||||
</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={() => {
|
||||
setShowConfirm(true)
|
||||
}}
|
||||
width="48%"
|
||||
id="send-button"
|
||||
disabled={approval !== ApprovalState.APPROVED}
|
||||
error={sendingWithSwap && isSwapValid && severity > 2}
|
||||
>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{`Send${severity > 2 ? ' Anyway' : ''}`}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
</RowBetween>
|
||||
) : (
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
setShowConfirm(true)
|
||||
}}
|
||||
id="send-button"
|
||||
disabled={(sendingWithSwap && !isSwapValid) || (!sendingWithSwap && !isSendValid)}
|
||||
error={sendingWithSwap && isSwapValid && severity > 2}
|
||||
>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
{(sendingWithSwap ? swapError : null) ||
|
||||
sendAmountError ||
|
||||
recipientError ||
|
||||
`Send${severity > 2 ? ' Anyway' : ''}`}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
)}
|
||||
<V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} />
|
||||
</BottomGrouping>
|
||||
</Wrapper>
|
||||
</AppBody>
|
||||
|
||||
{bestTrade && (
|
||||
<AdvancedSwapDetailsDropdown
|
||||
trade={bestTrade}
|
||||
rawSlippage={allowedSlippage}
|
||||
deadline={deadline}
|
||||
showAdvanced={showAdvanced}
|
||||
setShowAdvanced={setShowAdvanced}
|
||||
setDeadline={setDeadline}
|
||||
setRawSlippage={setAllowedSlippage}
|
||||
/>
|
||||
)}
|
||||
|
||||
{priceImpactWithoutFee && severity > 2 && (
|
||||
<AutoColumn gap="lg" style={{ marginTop: '1rem' }}>
|
||||
<PriceSlippageWarningCard priceSlippage={priceImpactWithoutFee} />
|
||||
</AutoColumn>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,31 +1,35 @@
|
||||
import { JSBI, TokenAmount, WETH } from '@uniswap/sdk'
|
||||
import React, { useContext, useState, useEffect } from 'react'
|
||||
import { JSBI, TokenAmount } from '@uniswap/sdk'
|
||||
import React, { useContext, useState, useEffect, useCallback } from 'react'
|
||||
import { ArrowDown } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import { RouteComponentProps } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import AddressInputPanel from '../../components/AddressInputPanel'
|
||||
import { ButtonError, ButtonLight, ButtonPrimary } from '../../components/Button'
|
||||
import Card, { GreyCard } from '../../components/Card'
|
||||
import { AutoColumn } from '../../components/Column'
|
||||
import ConfirmationModal from '../../components/ConfirmationModal'
|
||||
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
||||
import QuestionHelper from '../../components/QuestionHelper'
|
||||
import { RowBetween, RowFixed } from '../../components/Row'
|
||||
import { SwapPoolTabs } from '../../components/NavigationTabs'
|
||||
import { AutoRow, RowBetween } from '../../components/Row'
|
||||
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
|
||||
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
|
||||
import FormattedPriceImpact from '../../components/swap/FormattedPriceImpact'
|
||||
import { ArrowWrapper, BottomGrouping, Dots, Wrapper } from '../../components/swap/styleds'
|
||||
import SwapModalFooter from '../../components/swap/SwapModalFooter'
|
||||
import SwapModalHeader from '../../components/swap/SwapModalHeader'
|
||||
import TradePrice from '../../components/swap/TradePrice'
|
||||
import V1TradeLink from '../../components/swap/V1TradeLink'
|
||||
import BetterTradeLink from '../../components/swap/BetterTradeLink'
|
||||
import { TokenWarningCards } from '../../components/TokenWarningCard'
|
||||
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, MIN_ETH } from '../../constants'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useApproveCallbackFromTrade, ApprovalState } from '../../hooks/useApproveCallback'
|
||||
import useENSAddress from '../../hooks/useENSAddress'
|
||||
import { useSwapCallback } from '../../hooks/useSwapCallback'
|
||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||
import { useWalletModalToggle, useToggleSettingsMenu } from '../../state/application/hooks'
|
||||
import { useExpertModeManager, useUserSlippageTolerance, useUserDeadline } from '../../state/user/hooks'
|
||||
|
||||
import { INITIAL_ALLOWED_SLIPPAGE, BETTER_TRADE_LINK_THRESHOLD } from '../../constants'
|
||||
import { getTradeVersion, isTradeBetter } from '../../data/V1'
|
||||
import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import {
|
||||
useDefaultsFromURLSearch,
|
||||
@@ -33,63 +37,91 @@ import {
|
||||
useSwapActionHandlers,
|
||||
useSwapState
|
||||
} from '../../state/swap/hooks'
|
||||
import { CursorPointer, TYPE } from '../../theme'
|
||||
import { CursorPointer, LinkStyledButton, TYPE } from '../../theme'
|
||||
import { maxAmountSpend } from '../../utils/maxAmountSpend'
|
||||
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
|
||||
import AppBody from '../AppBody'
|
||||
import { PriceSlippageWarningCard } from '../../components/swap/PriceSlippageWarningCard'
|
||||
import { ClickableText } from '../Pool/styleds'
|
||||
|
||||
export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
useDefaultsFromURLSearch(search)
|
||||
export default function Swap() {
|
||||
useDefaultsFromURLSearch()
|
||||
|
||||
const { chainId, account } = useActiveWeb3React()
|
||||
const { account } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
// toggle wallet when disconnected
|
||||
const toggleWalletModal = useWalletModalToggle()
|
||||
|
||||
// for expert mode
|
||||
const toggleSettings = useToggleSettingsMenu()
|
||||
const [expertMode] = useExpertModeManager()
|
||||
|
||||
// get custom setting values for user
|
||||
const [deadline] = useUserDeadline()
|
||||
const [allowedSlippage] = useUserSlippageTolerance()
|
||||
|
||||
// swap state
|
||||
const { independentField, typedValue } = useSwapState()
|
||||
const { bestTrade, tokenBalances, parsedAmounts, tokens, error, v1TradeLinkIfBetter } = useDerivedSwapInfo()
|
||||
const { onSwitchTokens, onTokenSelection, onUserInput } = useSwapActionHandlers()
|
||||
const { independentField, typedValue, recipient } = useSwapState()
|
||||
const { v1Trade, v2Trade, tokenBalances, parsedAmount, tokens, error } = useDerivedSwapInfo()
|
||||
const { address: recipientAddress } = useENSAddress(recipient)
|
||||
const toggledVersion = useToggledVersion()
|
||||
const trade =
|
||||
{
|
||||
[Version.v1]: v1Trade,
|
||||
[Version.v2]: v2Trade
|
||||
}[toggledVersion] ?? undefined
|
||||
|
||||
const betterTradeLinkVersion: Version | undefined =
|
||||
toggledVersion === Version.v2 && isTradeBetter(v2Trade, v1Trade, BETTER_TRADE_LINK_THRESHOLD)
|
||||
? Version.v1
|
||||
: toggledVersion === Version.v1 && isTradeBetter(v1Trade, v2Trade)
|
||||
? Version.v2
|
||||
: undefined
|
||||
|
||||
const parsedAmounts = {
|
||||
[Field.INPUT]: independentField === Field.INPUT ? parsedAmount : trade?.inputAmount,
|
||||
[Field.OUTPUT]: independentField === Field.OUTPUT ? parsedAmount : trade?.outputAmount
|
||||
}
|
||||
|
||||
const { onSwitchTokens, onTokenSelection, onUserInput, onChangeRecipient } = useSwapActionHandlers()
|
||||
const isValid = !error
|
||||
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
|
||||
|
||||
const handleTypeInput = useCallback(
|
||||
(field, value) => {
|
||||
onUserInput(Field.INPUT, value)
|
||||
},
|
||||
[onUserInput]
|
||||
)
|
||||
const handleTypeOutput = useCallback(
|
||||
(field, value) => {
|
||||
onUserInput(Field.OUTPUT, value)
|
||||
},
|
||||
[onUserInput]
|
||||
)
|
||||
|
||||
// modal and loading
|
||||
const [showAdvanced, setShowAdvanced] = useState<boolean>(false) // toggling slippage, deadline, etc. on and off
|
||||
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>('')
|
||||
|
||||
// tx parameters
|
||||
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
|
||||
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
|
||||
|
||||
const formattedAmounts = {
|
||||
[independentField]: typedValue,
|
||||
[dependentField]: parsedAmounts[dependentField] ? parsedAmounts[dependentField].toSignificant(6) : ''
|
||||
[dependentField]: 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(
|
||||
tokens[Field.INPUT] && tokens[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)
|
||||
|
||||
// show approve flow when: no error on inputs, not approved or pending, or approved in current session
|
||||
const showApproveFlow =
|
||||
!error &&
|
||||
(approval === ApprovalState.NOT_APPROVED ||
|
||||
approval === ApprovalState.PENDING ||
|
||||
(approvalSubmitted && approval === ApprovalState.APPROVED))
|
||||
|
||||
// mark when a user has submitted an approval, reset onTokenSelection for input field
|
||||
useEffect(() => {
|
||||
if (approval === ApprovalState.PENDING) {
|
||||
@@ -97,32 +129,23 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
}
|
||||
}, [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: TokenAmount | undefined = maxAmountSpend(tokenBalances[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 => {
|
||||
@@ -131,8 +154,15 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
|
||||
ReactGA.event({
|
||||
category: 'Swap',
|
||||
action: 'Swap w/o Send',
|
||||
label: [bestTrade.inputAmount.token.symbol, bestTrade.outputAmount.token.symbol].join('/')
|
||||
action:
|
||||
recipient === null
|
||||
? 'Swap w/o Send'
|
||||
: (recipientAddress ?? recipient) === account
|
||||
? 'Swap w/o Send + recipient'
|
||||
: 'Swap w/ Send',
|
||||
label: [trade?.inputAmount?.token?.symbol, trade?.outputAmount?.token?.symbol, getTradeVersion(trade)].join(
|
||||
'/'
|
||||
)
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -150,6 +180,15 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
// warnings on slippage
|
||||
const priceImpactSeverity = warningSeverity(priceImpactWithoutFee)
|
||||
|
||||
// show approve flow when: no error on inputs, not approved or pending, or approved in current session
|
||||
// never show if price impact is above threshold in non expert mode
|
||||
const showApproveFlow =
|
||||
!error &&
|
||||
(approval === ApprovalState.NOT_APPROVED ||
|
||||
approval === ApprovalState.PENDING ||
|
||||
(approvalSubmitted && approval === ApprovalState.APPROVED)) &&
|
||||
!(priceImpactSeverity > 3 && !expertMode)
|
||||
|
||||
function modalHeader() {
|
||||
return (
|
||||
<SwapModalHeader
|
||||
@@ -158,6 +197,7 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
slippageAdjustedAmounts={slippageAdjustedAmounts}
|
||||
priceImpactSeverity={priceImpactSeverity}
|
||||
independentField={independentField}
|
||||
recipient={recipient}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -174,7 +214,7 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
parsedAmounts={parsedAmounts}
|
||||
priceImpactWithoutFee={priceImpactWithoutFee}
|
||||
slippageAdjustedAmounts={slippageAdjustedAmounts}
|
||||
trade={bestTrade}
|
||||
trade={trade}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -188,6 +228,7 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
<>
|
||||
<TokenWarningCards tokens={tokens} />
|
||||
<AppBody>
|
||||
<SwapPoolTabs active={'swap'} />
|
||||
<Wrapper id="swap-page">
|
||||
<ConfirmationModal
|
||||
isOpen={showConfirm}
|
||||
@@ -208,28 +249,28 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
/>
|
||||
|
||||
<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
|
||||
field={Field.INPUT}
|
||||
label={independentField === Field.OUTPUT ? 'From (estimated)' : 'From'}
|
||||
value={formattedAmounts[Field.INPUT]}
|
||||
showMaxButton={!atMaxAmountInput}
|
||||
token={tokens[Field.INPUT]}
|
||||
onUserInput={handleTypeInput}
|
||||
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"
|
||||
/>
|
||||
|
||||
<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={() => {
|
||||
@@ -239,49 +280,67 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
color={tokens[Field.INPUT] && tokens[Field.OUTPUT] ? theme.primary1 : theme.text2}
|
||||
/>
|
||||
</ArrowWrapper>
|
||||
</AutoColumn>
|
||||
</CursorPointer>
|
||||
<CurrencyInputPanel
|
||||
field={Field.OUTPUT}
|
||||
value={formattedAmounts[Field.OUTPUT]}
|
||||
onUserInput={onUserInput}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
label={independentField === Field.INPUT ? 'To (estimated)' : 'To'}
|
||||
showMaxButton={false}
|
||||
token={tokens[Field.OUTPUT]}
|
||||
onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)}
|
||||
otherSelectedTokenAddress={tokens[Field.INPUT]?.address}
|
||||
id="swap-currency-output"
|
||||
/>
|
||||
</>
|
||||
{recipient === null ? (
|
||||
<LinkStyledButton id="add-recipient-button" onClick={() => onChangeRecipient('')}>
|
||||
+ add recipient (optional)
|
||||
</LinkStyledButton>
|
||||
) : null}
|
||||
</AutoRow>
|
||||
</AutoColumn>
|
||||
</CursorPointer>
|
||||
<CurrencyInputPanel
|
||||
field={Field.OUTPUT}
|
||||
value={formattedAmounts[Field.OUTPUT]}
|
||||
onUserInput={handleTypeOutput}
|
||||
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"
|
||||
/>
|
||||
|
||||
{!noRoute && tokens[Field.OUTPUT] && tokens[Field.INPUT] && (
|
||||
<Card padding={'.25rem .75rem 0 .75rem'} borderRadius={'20px'}>
|
||||
<AutoColumn gap="4px">
|
||||
{recipient !== null ? (
|
||||
<>
|
||||
<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 recipient
|
||||
</LinkStyledButton>
|
||||
</AutoRow>
|
||||
<AddressInputPanel id="recipient" value={recipient} onChange={onChangeRecipient} />
|
||||
</>
|
||||
) : 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
|
||||
inputToken={tokens[Field.INPUT]}
|
||||
outputToken={tokens[Field.OUTPUT]}
|
||||
price={trade?.executionPrice}
|
||||
showInverted={showInverted}
|
||||
setShowInverted={setShowInverted}
|
||||
/>
|
||||
</RowBetween>
|
||||
|
||||
{allowedSlippage !== INITIAL_ALLOWED_SLIPPAGE && (
|
||||
<RowBetween align="center">
|
||||
<Text fontWeight={500} fontSize={14} color={theme.text2}>
|
||||
Price
|
||||
</Text>
|
||||
<TradePrice trade={bestTrade} showInverted={showInverted} setShowInverted={setShowInverted} />
|
||||
<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>
|
||||
|
||||
{bestTrade && priceImpactSeverity > 1 && (
|
||||
<RowBetween>
|
||||
<TYPE.main
|
||||
style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }}
|
||||
fontSize={14}
|
||||
>
|
||||
Price Impact
|
||||
</TYPE.main>
|
||||
<RowFixed>
|
||||
<FormattedPriceImpact priceImpact={priceImpactWithoutFee} />
|
||||
<QuestionHelper text="The difference between the market price and estimated price due to trade size." />
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</Card>
|
||||
)}
|
||||
)}
|
||||
</AutoColumn>
|
||||
</Card>
|
||||
</AutoColumn>
|
||||
<BottomGrouping>
|
||||
{!account ? (
|
||||
@@ -308,54 +367,44 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
</ButtonPrimary>
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
setShowConfirm(true)
|
||||
expertMode ? onSwap() : setShowConfirm(true)
|
||||
}}
|
||||
width="48%"
|
||||
id="swap-button"
|
||||
disabled={!isValid || approval !== ApprovalState.APPROVED}
|
||||
disabled={!isValid || approval !== ApprovalState.APPROVED || (priceImpactSeverity > 3 && !expertMode)}
|
||||
error={isValid && priceImpactSeverity > 2}
|
||||
>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{`Swap${priceImpactSeverity > 2 ? ' Anyway' : ''}`}
|
||||
{priceImpactSeverity > 3 && !expertMode
|
||||
? `Price Impact High`
|
||||
: `Swap${priceImpactSeverity > 2 ? ' Anyway' : ''}`}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
</RowBetween>
|
||||
) : (
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
setShowConfirm(true)
|
||||
expertMode ? onSwap() : setShowConfirm(true)
|
||||
}}
|
||||
id="swap-button"
|
||||
disabled={!isValid}
|
||||
disabled={!isValid || (priceImpactSeverity > 3 && !expertMode)}
|
||||
error={isValid && priceImpactSeverity > 2}
|
||||
>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
{error ?? `Swap${priceImpactSeverity > 2 ? ' Anyway' : ''}`}
|
||||
{error
|
||||
? error
|
||||
: priceImpactSeverity > 3 && !expertMode
|
||||
? `Price Impact Too High`
|
||||
: `Swap${priceImpactSeverity > 2 ? ' Anyway' : ''}`}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
)}
|
||||
<V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} />
|
||||
{betterTradeLinkVersion && <BetterTradeLink version={betterTradeLinkVersion} />}
|
||||
</BottomGrouping>
|
||||
</Wrapper>
|
||||
</AppBody>
|
||||
|
||||
{bestTrade && (
|
||||
<AdvancedSwapDetailsDropdown
|
||||
trade={bestTrade}
|
||||
rawSlippage={allowedSlippage}
|
||||
deadline={deadline}
|
||||
showAdvanced={showAdvanced}
|
||||
setShowAdvanced={setShowAdvanced}
|
||||
setDeadline={setDeadline}
|
||||
setRawSlippage={setAllowedSlippage}
|
||||
/>
|
||||
)}
|
||||
|
||||
{priceImpactWithoutFee && priceImpactSeverity > 2 && (
|
||||
<AutoColumn gap="lg" style={{ marginTop: '1rem' }}>
|
||||
<PriceSlippageWarningCard priceSlippage={priceImpactWithoutFee} />
|
||||
</AutoColumn>
|
||||
)}
|
||||
<AdvancedSwapDetailsDropdown trade={trade} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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' }} />
|
||||
}
|
||||
|
||||
4
src/pages/Swap/tsconfig.json
Normal file
4
src/pages/Swap/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.strict.json",
|
||||
"include": ["**/*"]
|
||||
}
|
||||
@@ -23,5 +23,6 @@ export type PopupContent =
|
||||
|
||||
export const updateBlockNumber = createAction<{ chainId: number; blockNumber: number }>('updateBlockNumber')
|
||||
export const toggleWalletModal = createAction<void>('toggleWalletModal')
|
||||
export const toggleSettingsMenu = createAction<void>('toggleSettingsMenu')
|
||||
export const addPopup = createAction<{ key?: string; content: PopupContent }>('addPopup')
|
||||
export const removePopup = createAction<{ key: string }>('removePopup')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { addPopup, PopupContent, removePopup, toggleWalletModal } from './actions'
|
||||
import { addPopup, PopupContent, removePopup, toggleWalletModal, toggleSettingsMenu } from './actions'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { AppState } from '../index'
|
||||
|
||||
@@ -19,6 +19,15 @@ export function useWalletModalToggle(): () => void {
|
||||
return useCallback(() => dispatch(toggleWalletModal()), [dispatch])
|
||||
}
|
||||
|
||||
export function useSettingsMenuOpen(): boolean {
|
||||
return useSelector((state: AppState) => state.application.settingsMenuOpen)
|
||||
}
|
||||
|
||||
export function useToggleSettingsMenu(): () => void {
|
||||
const dispatch = useDispatch()
|
||||
return useCallback(() => dispatch(toggleSettingsMenu()), [dispatch])
|
||||
}
|
||||
|
||||
// returns a function that allows adding a popup
|
||||
export function useAddPopup(): (content: PopupContent, key?: string) => void {
|
||||
const dispatch = useDispatch()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user