Compare commits
106 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
848c7b418b | ||
|
|
f619cf4353 | ||
|
|
877db71e2a | ||
|
|
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 | ||
|
|
e667615449 | ||
|
|
4ab61faeae | ||
|
|
0004db3d4a | ||
|
|
c133c472be | ||
|
|
0019ccdf51 | ||
|
|
5a1a469f35 | ||
|
|
4c28f34803 | ||
|
|
104be830fc | ||
|
|
24c70791cd | ||
|
|
216fdea290 | ||
|
|
40e4ce2ed3 | ||
|
|
b2508fc6f2 | ||
|
|
f73b37287f | ||
|
|
c09eb738c3 | ||
|
|
6de3a6ec28 | ||
|
|
c1d35cc8b3 | ||
|
|
f279b2bea2 | ||
|
|
6ffbf756f8 | ||
|
|
10837d7ba1 | ||
|
|
2d6eddf9d4 | ||
|
|
aadf43efc3 | ||
|
|
227f729ecd | ||
|
|
a5b15e37f6 |
50
.github/workflows/release.yaml
vendored
50
.github/workflows/release.yaml
vendored
@@ -2,14 +2,10 @@ name: Release
|
|||||||
on:
|
on:
|
||||||
# every morning
|
# every morning
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 12 * * *'
|
- cron: '0 12 * * 1-4'
|
||||||
|
|
||||||
# releases are triggered on changes to this file
|
# manual trigger
|
||||||
push:
|
workflow_dispatch:
|
||||||
branches:
|
|
||||||
- v2
|
|
||||||
paths:
|
|
||||||
- '.github/workflows/release.yaml'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
bump_version:
|
bump_version:
|
||||||
@@ -43,14 +39,14 @@ jobs:
|
|||||||
node-version: '12'
|
node-version: '12'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn install --ignore-scripts --frozen-lockfile
|
run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build the IPFS bundle
|
- name: Build the IPFS bundle
|
||||||
run: yarn ipfs-build
|
run: yarn build
|
||||||
|
|
||||||
- name: Pin to IPFS
|
- name: Pin to IPFS
|
||||||
id: upload
|
id: upload
|
||||||
uses: anantaramdas/ipfs-pinata-deploy-action@v1.5.2
|
uses: anantaramdas/ipfs-pinata-deploy-action@39bbda1ce1fe24c69c6f57861b8038278d53688d
|
||||||
with:
|
with:
|
||||||
pin-name: Uniswap ${{ needs.bump_version.outputs.new_tag }}
|
pin-name: Uniswap ${{ needs.bump_version.outputs.new_tag }}
|
||||||
path: './build'
|
path: './build'
|
||||||
@@ -63,15 +59,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
cidv0: ${{ steps.upload.outputs.hash }}
|
cidv0: ${{ steps.upload.outputs.hash }}
|
||||||
|
|
||||||
- name: Update DNS with new IPFS hash
|
# - name: Update DNS with new IPFS hash
|
||||||
uses: uniswap/replace-vercel-dns-records@v1.0.0
|
# env:
|
||||||
with:
|
# CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_TOKEN }}
|
||||||
domain: 'uniswap.org'
|
# RECORD_DOMAIN: 'uniswap.org'
|
||||||
subdomain: '_dnslink.app'
|
# RECORD_NAME: '_dnslink.app'
|
||||||
record-type: 'TXT'
|
# CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}
|
||||||
value: dnslink=/ipfs/${{ steps.upload.outputs.hash }}
|
# uses: textileio/cloudflare-update-dnslink@0fe7b7a1ffc865db3a4da9773f0f987447ad5848
|
||||||
token: ${{ secrets.VERCEL_TOKEN }}
|
# with:
|
||||||
team-name: 'uniswap'
|
# cid: ${{ steps.upload.outputs.hash }}
|
||||||
|
|
||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
id: create_release
|
id: create_release
|
||||||
@@ -82,19 +78,21 @@ jobs:
|
|||||||
tag_name: ${{ needs.bump_version.outputs.new_tag }}
|
tag_name: ${{ needs.bump_version.outputs.new_tag }}
|
||||||
release_name: Release ${{ needs.bump_version.outputs.new_tag }}
|
release_name: Release ${{ needs.bump_version.outputs.new_tag }}
|
||||||
body: |
|
body: |
|
||||||
Release built from commit [`${{ github.sha }}`](https://github.com/Uniswap/uniswap-frontend/tree/${{ github.sha }})
|
IPFS hash of the deployment:
|
||||||
|
|
||||||
The IPFS hash of the bundle is:
|
|
||||||
- CIDv0: `${{ steps.upload.outputs.hash }}`
|
- CIDv0: `${{ steps.upload.outputs.hash }}`
|
||||||
- CIDv1: `${{ steps.convert_cidv0.outputs.cidv1 }}`
|
- CIDv1: `${{ steps.convert_cidv0.outputs.cidv1 }}`
|
||||||
|
|
||||||
Uniswap uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to store your settings.
|
The latest release is always accessible via our alias to the Cloudflare IPFS gateway at [app.uniswap.org](https://app.uniswap.org).
|
||||||
**Beware** that other sites you access via the _same_ IPFS gateway can read and modify your settings on Uniswap without your permission.
|
|
||||||
You can avoid this issue by using a subdomain IPFS gateway. The preferred gateway URLs below utilize the CIDv1 of the release in the subdomain, and are relatively safer.
|
You can also access the Uniswap Interface directly from an IPFS gateway.
|
||||||
|
The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to store your settings.
|
||||||
|
**Beware** that other sites you access via the _same_ IPFS gateway can read and modify your settings on the Uniswap interface without your permission.
|
||||||
|
You can avoid this issue by using a subdomain IPFS gateway, or our alias to the latest release at [app.uniswap.org](https://app.uniswap.org).
|
||||||
|
The preferred URLs below are safe to use to access this specific release.
|
||||||
|
|
||||||
Preferred URLs:
|
Preferred URLs:
|
||||||
- https://${{ steps.convert_cidv0.outputs.cidv1 }}.ipfs.dweb.link/
|
- https://${{ steps.convert_cidv0.outputs.cidv1 }}.ipfs.dweb.link/
|
||||||
- https://${{ steps.convert_cidv0.outputs.cidv1 }}.cf-ipfs.com/
|
- https://${{ steps.convert_cidv0.outputs.cidv1 }}.ipfs.cf-ipfs.com/
|
||||||
- [ipfs://${{ steps.upload.outputs.hash }}/](ipfs://${{ steps.upload.outputs.hash }}/)
|
- [ipfs://${{ steps.upload.outputs.hash }}/](ipfs://${{ steps.upload.outputs.hash }}/)
|
||||||
|
|
||||||
Other IPFS gateways:
|
Other IPFS gateways:
|
||||||
|
|||||||
12
.github/workflows/tests.yaml
vendored
12
.github/workflows/tests.yaml
vendored
@@ -2,10 +2,10 @@ name: Tests
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- v2
|
- master
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- v2
|
- master
|
||||||
jobs:
|
jobs:
|
||||||
integration-tests:
|
integration-tests:
|
||||||
name: Integration tests
|
name: Integration tests
|
||||||
@@ -26,7 +26,9 @@ jobs:
|
|||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-yarn-
|
${{ runner.os }}-yarn-
|
||||||
- run: yarn install
|
- run: yarn install --frozen-lockfile
|
||||||
|
- run: yarn cypress install
|
||||||
|
- run: yarn build
|
||||||
- run: yarn integration-test
|
- run: yarn integration-test
|
||||||
|
|
||||||
unit-tests:
|
unit-tests:
|
||||||
@@ -48,7 +50,7 @@ jobs:
|
|||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-yarn-
|
${{ runner.os }}-yarn-
|
||||||
- run: yarn install --ignore-scripts --frozen-lockfile
|
- run: yarn install --frozen-lockfile
|
||||||
- run: yarn test
|
- run: yarn test
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
@@ -70,6 +72,6 @@ jobs:
|
|||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-yarn-
|
${{ runner.os }}-yarn-
|
||||||
- run: yarn install --ignore-scripts --frozen-lockfile
|
- run: yarn install --frozen-lockfile
|
||||||
- run: yarn lint
|
- run: yarn lint
|
||||||
|
|
||||||
|
|||||||
41
README.md
41
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/)
|
[](https://prettier.io/)
|
||||||
|
|
||||||
An open source interface for Uniswap -- a protocol for decentralized exchange of Ethereum tokens.
|
An open source interface for Uniswap -- a protocol for decentralized exchange of Ethereum tokens.
|
||||||
|
|
||||||
- Website: [uniswap.org](https://uniswap.org/)
|
- Website: [uniswap.org](https://uniswap.org/)
|
||||||
|
- Interface: [app.uniswap.org](https://app.uniswap.org)
|
||||||
- Docs: [uniswap.org/docs/](https://uniswap.org/docs/)
|
- Docs: [uniswap.org/docs/](https://uniswap.org/docs/)
|
||||||
- Twitter: [@UniswapProtocol](https://twitter.com/UniswapProtocol)
|
- Twitter: [@UniswapProtocol](https://twitter.com/UniswapProtocol)
|
||||||
- Reddit: [/r/Uniswap](https://www.reddit.com/r/Uniswap/)
|
- Reddit: [/r/Uniswap](https://www.reddit.com/r/Uniswap/)
|
||||||
@@ -13,11 +14,11 @@ An open source interface for Uniswap -- a protocol for decentralized exchange of
|
|||||||
- Discord: [Uniswap](https://discord.gg/Y7TF6QA)
|
- Discord: [Uniswap](https://discord.gg/Y7TF6QA)
|
||||||
- Whitepaper: [Link](https://hackmd.io/C-DvwDSfSxuh-Gd4WKE_ig)
|
- Whitepaper: [Link](https://hackmd.io/C-DvwDSfSxuh-Gd4WKE_ig)
|
||||||
|
|
||||||
## Accessing the frontend
|
## Accessing the Uniswap Interface
|
||||||
|
|
||||||
To access the front end, use an IPFS gateway link from the
|
To access the Uniswap Interface, use an IPFS gateway link from the
|
||||||
[latest release](https://github.com/Uniswap/uniswap-frontend/releases/latest)
|
[latest release](https://github.com/Uniswap/uniswap-interface/releases/latest),
|
||||||
or visit [uniswap.exchange](https://uniswap.exchange).
|
or visit [app.uniswap.org](https://app.uniswap.org).
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@@ -27,26 +28,32 @@ or visit [uniswap.exchange](https://uniswap.exchange).
|
|||||||
yarn
|
yarn
|
||||||
```
|
```
|
||||||
|
|
||||||
### Configure Environment (optional)
|
|
||||||
|
|
||||||
Copy `.env` to `.env.local` and change the appropriate variables.
|
|
||||||
|
|
||||||
### Run
|
### Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn start
|
yarn start
|
||||||
```
|
```
|
||||||
|
|
||||||
To have the frontend default to a different network, make a copy of `.env` named `.env.local`,
|
### Configuring the environment (optional)
|
||||||
change `REACT_APP_NETWORK_ID` to `"{yourNetworkId}"`, and change `REACT_APP_NETWORK_URL` to e.g.
|
|
||||||
`"https://{yourNetwork}.infura.io/v3/{yourKey}"`.
|
|
||||||
|
|
||||||
Note that the front end only works properly on testnets where both
|
To have the interface default to a different network when a wallet is not connected:
|
||||||
|
|
||||||
|
1. Make a copy of `.env` named `.env.local`
|
||||||
|
2. Change `REACT_APP_NETWORK_ID` to `"{YOUR_NETWORK_ID}"`
|
||||||
|
3. Change `REACT_APP_NETWORK_URL` to e.g. `"https://{YOUR_NETWORK_ID}.infura.io/v3/{YOUR_INFURA_KEY}"`
|
||||||
|
|
||||||
|
Note that the interface only works on testnets where both
|
||||||
[Uniswap V2](https://uniswap.org/docs/v2/smart-contracts/factory/) and
|
[Uniswap V2](https://uniswap.org/docs/v2/smart-contracts/factory/) and
|
||||||
[multicall](https://github.com/makerdao/multicall) are deployed.
|
[multicall](https://github.com/makerdao/multicall) are deployed.
|
||||||
The frontend will not work on other networks.
|
The interface will not work on other networks.
|
||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
|
|
||||||
**Please open all pull requests against the `v2` branch.**
|
**Please open all pull requests against the `master` branch.**
|
||||||
CI checks will run against all PRs.
|
CI checks will run against all PRs.
|
||||||
|
|
||||||
|
## Accessing Uniswap Interface V1
|
||||||
|
|
||||||
|
The Uniswap Interface supports swapping against, and migrating or removing liquidity from Uniswap V1. However,
|
||||||
|
if you would like to use Uniswap V1, the Uniswap V1 interface for mainnet and testnets is accessible via IPFS gateways
|
||||||
|
linked from the [v1.0.0 release](https://github.com/Uniswap/uniswap-interface/releases/tag/v1.0.0).
|
||||||
|
|||||||
@@ -16,4 +16,29 @@ describe('Add Liquidity', () => {
|
|||||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'SKL')
|
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'SKL')
|
||||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'MKR')
|
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'MKR')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('single token can be selected', () => {
|
||||||
|
cy.visit('/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d')
|
||||||
|
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'SKL')
|
||||||
|
cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
|
||||||
|
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'MKR')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('redirects /add/token-token to add/token/token', () => {
|
||||||
|
cy.visit('/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
|
||||||
|
cy.url().should(
|
||||||
|
'contain',
|
||||||
|
'/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('redirects /add/WETH-token to /add/ETH/token', () => {
|
||||||
|
cy.visit('/add/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
|
||||||
|
cy.url().should('contain', '/add/ETH/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('redirects /add/token-WETH to /add/token/ETH', () => {
|
||||||
|
cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85-0xc778417E063141139Fce010982780140Aa0cD5Ab')
|
||||||
|
cy.url().should('contain', '/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85/ETH')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TEST_ADDRESS_NEVER_USE } from '../support/commands'
|
import { TEST_ADDRESS_NEVER_USE_SHORTENED } from '../support/commands'
|
||||||
|
|
||||||
describe('Landing Page', () => {
|
describe('Landing Page', () => {
|
||||||
beforeEach(() => cy.visit('/'))
|
beforeEach(() => cy.visit('/'))
|
||||||
@@ -10,11 +10,6 @@ describe('Landing Page', () => {
|
|||||||
cy.url().should('include', '/swap')
|
cy.url().should('include', '/swap')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('allows navigation to send', () => {
|
|
||||||
cy.get('#send-nav-link').click()
|
|
||||||
cy.url().should('include', '/send')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('allows navigation to pool', () => {
|
it('allows navigation to pool', () => {
|
||||||
cy.get('#pool-nav-link').click()
|
cy.get('#pool-nav-link').click()
|
||||||
cy.url().should('include', '/pool')
|
cy.url().should('include', '/pool')
|
||||||
@@ -22,6 +17,6 @@ describe('Landing Page', () => {
|
|||||||
|
|
||||||
it('is connected', () => {
|
it('is connected', () => {
|
||||||
cy.get('#web3-status-connected').click()
|
cy.get('#web3-status-connected').click()
|
||||||
cy.get('#web3-account-identifier-row').contains(TEST_ADDRESS_NEVER_USE)
|
cy.get('#web3-account-identifier-row').contains(TEST_ADDRESS_NEVER_USE_SHORTENED)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
describe('Pool', () => {
|
describe('Pool', () => {
|
||||||
beforeEach(() => cy.visit('/pool'))
|
beforeEach(() => cy.visit('/pool'))
|
||||||
it('can search for a pool', () => {
|
it('add liquidity links to /add/ETH', () => {
|
||||||
cy.get('#join-pool-button').click()
|
cy.get('#join-pool-button').click()
|
||||||
cy.get('#token-search-input').type('DAI')
|
cy.url().should('contain', '/add/ETH')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can import a pool', () => {
|
it('import pool links to /import', () => {
|
||||||
cy.get('#join-pool-button').click()
|
cy.get('#import-pool-link').click()
|
||||||
cy.get('#import-pool-link').click({ force: true }) // blocked by the grid element in the search box
|
cy.url().should('contain', '/find')
|
||||||
cy.url().should('include', '/find')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
describe('Send', () => {
|
describe('Send', () => {
|
||||||
beforeEach(() => cy.visit('/send'))
|
it('should redirect', () => {
|
||||||
|
cy.visit('/send')
|
||||||
|
cy.url().should('include', '/swap')
|
||||||
|
})
|
||||||
|
|
||||||
it('can enter an amount into input', () => {
|
it('should redirect with url params', () => {
|
||||||
cy.get('#sending-no-swap-input')
|
cy.visit('/send?outputCurrency=ETH&recipient=bob.argent.xyz')
|
||||||
.type('0.001')
|
cy.url().should('contain', '/swap?outputCurrency=ETH&recipient=bob.argent.xyz')
|
||||||
.should('have.value', '0.001')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,31 +2,31 @@ describe('Swap', () => {
|
|||||||
beforeEach(() => cy.visit('/swap'))
|
beforeEach(() => cy.visit('/swap'))
|
||||||
it('can enter an amount into input', () => {
|
it('can enter an amount into input', () => {
|
||||||
cy.get('#swap-currency-input .token-amount-input')
|
cy.get('#swap-currency-input .token-amount-input')
|
||||||
.type('0.001')
|
.type('0.001', { delay: 200 })
|
||||||
.should('have.value', '0.001')
|
.should('have.value', '0.001')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('zero swap amount', () => {
|
it('zero swap amount', () => {
|
||||||
cy.get('#swap-currency-input .token-amount-input')
|
cy.get('#swap-currency-input .token-amount-input')
|
||||||
.type('0.0')
|
.type('0.0', { delay: 200 })
|
||||||
.should('have.value', '0.0')
|
.should('have.value', '0.0')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('invalid swap amount', () => {
|
it('invalid swap amount', () => {
|
||||||
cy.get('#swap-currency-input .token-amount-input')
|
cy.get('#swap-currency-input .token-amount-input')
|
||||||
.type('\\')
|
.type('\\', { delay: 200 })
|
||||||
.should('have.value', '')
|
.should('have.value', '')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can enter an amount into output', () => {
|
it('can enter an amount into output', () => {
|
||||||
cy.get('#swap-currency-output .token-amount-input')
|
cy.get('#swap-currency-output .token-amount-input')
|
||||||
.type('0.001')
|
.type('0.001', { delay: 200 })
|
||||||
.should('have.value', '0.001')
|
.should('have.value', '0.001')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('zero output amount', () => {
|
it('zero output amount', () => {
|
||||||
cy.get('#swap-currency-output .token-amount-input')
|
cy.get('#swap-currency-output .token-amount-input')
|
||||||
.type('0.0')
|
.type('0.0', { delay: 200 })
|
||||||
.should('have.value', '0.0')
|
.should('have.value', '0.0')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -35,10 +35,20 @@ describe('Swap', () => {
|
|||||||
cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').should('be.visible')
|
cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').should('be.visible')
|
||||||
cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').click({ force: true })
|
cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').click({ force: true })
|
||||||
cy.get('#swap-currency-input .token-amount-input').should('be.visible')
|
cy.get('#swap-currency-input .token-amount-input').should('be.visible')
|
||||||
cy.get('#swap-currency-input .token-amount-input').type('0.001', { force: true })
|
cy.get('#swap-currency-input .token-amount-input').type('0.001', { force: true, delay: 200 })
|
||||||
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
|
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
|
||||||
cy.get('#show-advanced').click()
|
|
||||||
cy.get('#swap-button').click()
|
cy.get('#swap-button').click()
|
||||||
cy.get('#confirm-swap-or-send').should('contain', 'Confirm Swap')
|
cy.get('#confirm-swap-or-send').should('contain', 'Confirm Swap')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('add a recipient', () => {
|
||||||
|
cy.get('#add-recipient-button').click()
|
||||||
|
cy.get('#recipient').should('exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('remove recipient', () => {
|
||||||
|
cy.get('#add-recipient-button').click()
|
||||||
|
cy.get('#remove-recipient-button').click()
|
||||||
|
cy.get('#recipient').should('not.exist')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
2
cypress/support/commands.d.ts
vendored
2
cypress/support/commands.d.ts
vendored
@@ -1,5 +1,7 @@
|
|||||||
export const TEST_ADDRESS_NEVER_USE: string
|
export const TEST_ADDRESS_NEVER_USE: string
|
||||||
|
|
||||||
|
export const TEST_ADDRESS_NEVER_USE_SHORTENED: string
|
||||||
|
|
||||||
// declare namespace Cypress {
|
// declare namespace Cypress {
|
||||||
// // eslint-disable-next-line @typescript-eslint/class-name-casing
|
// // eslint-disable-next-line @typescript-eslint/class-name-casing
|
||||||
// interface cy {
|
// interface cy {
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ const PRIVATE_KEY_TEST_NEVER_USE = '0xad20c82497421e9784f18460ad2fe84f73569068e9
|
|||||||
// address of the above key
|
// address of the above key
|
||||||
export const TEST_ADDRESS_NEVER_USE = '0x0fF2D1eFd7A57B7562b2bf27F3f37899dB27F4a5'
|
export const TEST_ADDRESS_NEVER_USE = '0x0fF2D1eFd7A57B7562b2bf27F3f37899dB27F4a5'
|
||||||
|
|
||||||
|
export const TEST_ADDRESS_NEVER_USE_SHORTENED = '0x0fF2...F4a5'
|
||||||
|
|
||||||
class CustomizedBridge extends _Eip1193Bridge {
|
class CustomizedBridge extends _Eip1193Bridge {
|
||||||
async sendAsync(...args) {
|
async sendAsync(...args) {
|
||||||
console.debug('sendAsync called', ...args)
|
console.debug('sendAsync called', ...args)
|
||||||
@@ -67,13 +69,13 @@ class CustomizedBridge extends _Eip1193Bridge {
|
|||||||
|
|
||||||
// sets up the injected provider to be a mock ethereum provider with the given mnemonic/index
|
// sets up the injected provider to be a mock ethereum provider with the given mnemonic/index
|
||||||
Cypress.Commands.overwrite('visit', (original, url, options) => {
|
Cypress.Commands.overwrite('visit', (original, url, options) => {
|
||||||
return original(url, {
|
return original(url.startsWith('/') && url.length > 2 && !url.startsWith('/#') ? `/#${url}` : url, {
|
||||||
...options,
|
...options,
|
||||||
onBeforeLoad(win) {
|
onBeforeLoad(win) {
|
||||||
options && options.onBeforeLoad && options.onBeforeLoad(win)
|
options && options.onBeforeLoad && options.onBeforeLoad(win)
|
||||||
const provider = new JsonRpcProvider('https://rinkeby.infura.io/v3/acb7e55995d04c49bfb52b7141599467', 4)
|
const provider = new JsonRpcProvider('https://rinkeby.infura.io/v3/acb7e55995d04c49bfb52b7141599467', 4)
|
||||||
const signer = new Wallet(PRIVATE_KEY_TEST_NEVER_USE, provider)
|
const signer = new Wallet(PRIVATE_KEY_TEST_NEVER_USE, provider)
|
||||||
win.ethereum = new CustomizedBridge(signer, provider)
|
win.ethereum = new CustomizedBridge(signer, provider)
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
20
netlify.toml
20
netlify.toml
@@ -1,20 +0,0 @@
|
|||||||
# block some countries
|
|
||||||
[[redirects]]
|
|
||||||
from = "/*"
|
|
||||||
to = "/451.html"
|
|
||||||
status = 451
|
|
||||||
force = true
|
|
||||||
conditions = {Country=["BY","CU","IR","IQ","CI","LR","KP","SD","SY","ZW"]}
|
|
||||||
headers = {Link="<https://uniswap.exchange>"}
|
|
||||||
|
|
||||||
# forward v2 subdomain to apex
|
|
||||||
[[redirects]]
|
|
||||||
from = "https://v2.uniswap.exchange/*"
|
|
||||||
to = "https://uniswap.exchange/:splat"
|
|
||||||
status = 301
|
|
||||||
|
|
||||||
# support SPA setup
|
|
||||||
[[redirects]]
|
|
||||||
from = "/*"
|
|
||||||
to = "/index.html"
|
|
||||||
status = 200
|
|
||||||
17
package.json
17
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@uniswap/interface",
|
"name": "@uniswap/interface",
|
||||||
"description": "Uniswap Interface",
|
"description": "Uniswap Interface",
|
||||||
"homepage": "https://uniswap.exchange",
|
"homepage": ".",
|
||||||
"private": true,
|
"private": true,
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ethersproject/address": "^5.0.0-beta.134",
|
"@ethersproject/address": "^5.0.0-beta.134",
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"@ethersproject/strings": "^5.0.0-beta.136",
|
"@ethersproject/strings": "^5.0.0-beta.136",
|
||||||
"@ethersproject/units": "^5.0.0-beta.132",
|
"@ethersproject/units": "^5.0.0-beta.132",
|
||||||
"@ethersproject/wallet": "^5.0.0-beta.141",
|
"@ethersproject/wallet": "^5.0.0-beta.141",
|
||||||
"@popperjs/core": "^2.4.0",
|
"@popperjs/core": "^2.4.4",
|
||||||
"@reach/dialog": "^0.10.3",
|
"@reach/dialog": "^0.10.3",
|
||||||
"@reach/portal": "^0.10.3",
|
"@reach/portal": "^0.10.3",
|
||||||
"@reduxjs/toolkit": "^1.3.5",
|
"@reduxjs/toolkit": "^1.3.5",
|
||||||
@@ -33,12 +33,12 @@
|
|||||||
"@typescript-eslint/parser": "^2.31.0",
|
"@typescript-eslint/parser": "^2.31.0",
|
||||||
"@uniswap/sdk": "^2.0.5",
|
"@uniswap/sdk": "^2.0.5",
|
||||||
"@uniswap/v2-core": "1.0.0",
|
"@uniswap/v2-core": "1.0.0",
|
||||||
"@uniswap/v2-periphery": "1.0.0-beta.0",
|
"@uniswap/v2-periphery": "^1.1.0-beta.0",
|
||||||
"@web3-react/core": "^6.0.9",
|
"@web3-react/core": "^6.0.9",
|
||||||
"@web3-react/fortmatic-connector": "^6.0.9",
|
"@web3-react/fortmatic-connector": "^6.0.9",
|
||||||
"@web3-react/injected-connector": "^6.0.7",
|
"@web3-react/injected-connector": "^6.0.7",
|
||||||
"@web3-react/portis-connector": "^6.0.9",
|
"@web3-react/portis-connector": "^6.0.9",
|
||||||
"@web3-react/walletconnect-connector": "^6.0.9",
|
"@web3-react/walletconnect-connector": "^6.1.1",
|
||||||
"@web3-react/walletlink-connector": "^6.0.9",
|
"@web3-react/walletlink-connector": "^6.0.9",
|
||||||
"copy-to-clipboard": "^3.2.0",
|
"copy-to-clipboard": "^3.2.0",
|
||||||
"cross-env": "^7.0.2",
|
"cross-env": "^7.0.2",
|
||||||
@@ -51,6 +51,7 @@
|
|||||||
"i18next": "^15.0.9",
|
"i18next": "^15.0.9",
|
||||||
"i18next-browser-languagedetector": "^3.0.1",
|
"i18next-browser-languagedetector": "^3.0.1",
|
||||||
"i18next-xhr-backend": "^2.0.1",
|
"i18next-xhr-backend": "^2.0.1",
|
||||||
|
"inter-ui": "^3.13.1",
|
||||||
"jazzicon": "^1.5.0",
|
"jazzicon": "^1.5.0",
|
||||||
"lodash.flatmap": "^4.5.0",
|
"lodash.flatmap": "^4.5.0",
|
||||||
"polished": "^3.3.2",
|
"polished": "^3.3.2",
|
||||||
@@ -80,15 +81,11 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
"build": "cross-env REACT_APP_GIT_COMMIT_HASH=$(git show -s --format=%H) react-scripts build",
|
"build": "react-scripts build",
|
||||||
"ipfs-build": "cross-env PUBLIC_URL=\".\" react-scripts build",
|
|
||||||
"test": "react-scripts test --env=jsdom",
|
"test": "react-scripts test --env=jsdom",
|
||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject",
|
||||||
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
|
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
|
||||||
"lint:fix": "eslint 'src/**/*.{js,jsx,ts,tsx}' --fix",
|
"integration-test": "start-server-and-test 'serve build -l 3000' http://localhost:3000 'cypress run'"
|
||||||
"cy:run": "cypress run",
|
|
||||||
"serve:build": "serve -s build -l 3000",
|
|
||||||
"integration-test": "yarn build && start-server-and-test 'yarn run serve:build' http://localhost:3000 cy:run"
|
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "react-app"
|
"extends": "react-app"
|
||||||
|
|||||||
75
public/locales/iw.json
Normal file
75
public/locales/iw.json
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"noWallet": "לא נמצא ארנק",
|
||||||
|
"wrongNetwork": "נבחרה רשת לא נכונה",
|
||||||
|
"switchNetwork": "{{ correctNetwork }} יש צורך לשנות את הרשת ל",
|
||||||
|
"installWeb3MobileBrowser": "יש צורך בארנק ווב3.0, תתקין מטאמאסק או ארנק דומה",
|
||||||
|
"installMetamask": " Metamask יש צורך להתקין תוסף מטאמאסק לדפדפן, חפשו בגוגל ",
|
||||||
|
"disconnected": "מנותק",
|
||||||
|
"swap": "המרה",
|
||||||
|
"send": "שליחה",
|
||||||
|
"pool": "להפקיד",
|
||||||
|
"betaWarning": "הפרויקט נמצא בשלב בטא, השתמשו באחריות",
|
||||||
|
"input": "מוכר",
|
||||||
|
"output": "אקבל",
|
||||||
|
"estimated": "הערכה",
|
||||||
|
"balance": "בארנק שלי {{ balanceInput }}",
|
||||||
|
"unlock": "שחרור נעילת ארנק",
|
||||||
|
"pending": "ממתין לאישור",
|
||||||
|
"selectToken": "בחרו את הטוקן להמרה",
|
||||||
|
"searchOrPaste": "הכניסו שם או כתובת של טוקן לחיפוש",
|
||||||
|
"noExchange": "לא מתאפשרת המרה",
|
||||||
|
"exchangeRate": "שער המרה",
|
||||||
|
"enterValueCont": "כדי להמשיך {{ missingCurrencyValue }} הזינו ",
|
||||||
|
"selectTokenCont": "בחרו טוקן כדי להמשיך",
|
||||||
|
"noLiquidity": "אין נזילות",
|
||||||
|
"unlockTokenCont": "יש צורך לאשר את הטוקן למסחר",
|
||||||
|
"transactionDetails": "פרטי הטרנזקציה",
|
||||||
|
"hideDetails": "הסתר פרטים נוספים",
|
||||||
|
"youAreSelling": "למכירה",
|
||||||
|
"orTransFail": "או שהטרנזקציה תיכשל",
|
||||||
|
"youWillReceive": "תוצר המרה מינימלי",
|
||||||
|
"youAreBuying": "קונה",
|
||||||
|
"itWillCost": "זה יעלה",
|
||||||
|
"insufficientBalance": "אין בחשבון מספיק מטבעות",
|
||||||
|
"inputNotValid": "קלט לא תקין",
|
||||||
|
"differentToken": "יש צורך בטוקנים שונים",
|
||||||
|
"noRecipient": "לא הוכנסה כתובת ארנק יעד",
|
||||||
|
"invalidRecipient": "לא הוכנסה כתובת תקינה",
|
||||||
|
"recipientAddress": "כתובת יעד",
|
||||||
|
"youAreSending": "כמות לשליחה",
|
||||||
|
"willReceive": "יתקבל לכל הפחות",
|
||||||
|
"to": "אל",
|
||||||
|
"addLiquidity": "להוספת נזילות למאגר",
|
||||||
|
"deposit": "הפקדה",
|
||||||
|
"currentPoolSize": "גודל מאגר הנזילות הכולל",
|
||||||
|
"yourPoolShare": "חלקך במאגר הנזילות",
|
||||||
|
"noZero": "אפס אינו ערך תקין",
|
||||||
|
"mustBeETH": "ETH חייב להופיע באחד מהצדדים",
|
||||||
|
"enterCurrencyOrLabelCont": "כדי להמשיך {{ inputCurrency }} או {{ label }} הכנס",
|
||||||
|
"youAreAdding": "מתווספים למאגר",
|
||||||
|
"and": "וגם",
|
||||||
|
"intoPool": "לתוך הנזילות",
|
||||||
|
"outPool": "מתוך",
|
||||||
|
"youWillMint": "יונפקו לכם",
|
||||||
|
"liquidityTokens": "טוקנים של נזילות",
|
||||||
|
"totalSupplyIs": "חלקך במאגר הנזילות",
|
||||||
|
"youAreSettingExRate": "שער ההמרה יקבע על ידך",
|
||||||
|
"totalSupplyIs0": "אין לך טוקנים של נזילות",
|
||||||
|
"tokenWorth": "שווי כל טוקן נזילות הינו",
|
||||||
|
"firstLiquidity": "אתה הראשוןה שמזרים נזילות למאגר",
|
||||||
|
"initialExchangeRate": "ושל האית'ר הינן בערך שווה {{ label }} תוודאו שההפקדה של הטוקן",
|
||||||
|
"removeLiquidity": "הוצאה של נזילות",
|
||||||
|
"poolTokens": "טוקנים של מאגר הנזילות",
|
||||||
|
"enterLabelCont": "כדי להמשיך {{ label }} הכנס ",
|
||||||
|
"youAreRemoving": "יוסרו",
|
||||||
|
"youWillRemove": "יוסרו",
|
||||||
|
"createExchange": "ליצירת זוג מסחר",
|
||||||
|
"invalidTokenAddress": "כתובת טוקן לא נכונה",
|
||||||
|
"exchangeExists": "{{ label }} כבר קיים זוג המרה עבור",
|
||||||
|
"invalidSymbol": "תו שגוי",
|
||||||
|
"invalidDecimals": "ספרות עשרוניות שגויות",
|
||||||
|
"tokenAddress": "כתובת הטוקן",
|
||||||
|
"label": "שם",
|
||||||
|
"decimals": "ספרות עשרויות",
|
||||||
|
"enterTokenCont": "הכניסו כתובת טוקן כדי להמשיך"
|
||||||
|
}
|
||||||
@@ -6,21 +6,21 @@ import { LinkStyledButton } from '../../theme'
|
|||||||
import { CheckCircle, Copy } from 'react-feather'
|
import { CheckCircle, Copy } from 'react-feather'
|
||||||
|
|
||||||
const CopyIcon = styled(LinkStyledButton)`
|
const CopyIcon = styled(LinkStyledButton)`
|
||||||
color: ${({ theme }) => theme.text4};
|
color: ${({ theme }) => theme.text3};
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-right: 1rem;
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
font-size: 0.825rem;
|
||||||
:hover,
|
:hover,
|
||||||
:active,
|
:active,
|
||||||
:focus {
|
:focus {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: ${({ theme }) => theme.text3};
|
color: ${({ theme }) => theme.text2};
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
const TransactionStatusText = styled.span`
|
const TransactionStatusText = styled.span`
|
||||||
margin-left: 0.25rem;
|
margin-left: 0.25rem;
|
||||||
|
font-size: 0.825rem;
|
||||||
${({ theme }) => theme.flexRowNoWrap};
|
${({ theme }) => theme.flexRowNoWrap};
|
||||||
align-items: center;
|
align-items: center;
|
||||||
`
|
`
|
||||||
@@ -30,7 +30,6 @@ export default function CopyHelper(props: { toCopy: string; children?: React.Rea
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<CopyIcon onClick={() => setCopied(props.toCopy)}>
|
<CopyIcon onClick={() => setCopied(props.toCopy)}>
|
||||||
{props.children}
|
|
||||||
{isCopied ? (
|
{isCopied ? (
|
||||||
<TransactionStatusText>
|
<TransactionStatusText>
|
||||||
<CheckCircle size={'16'} />
|
<CheckCircle size={'16'} />
|
||||||
@@ -41,6 +40,7 @@ export default function CopyHelper(props: { toCopy: string; children?: React.Rea
|
|||||||
<Copy size={'16'} />
|
<Copy size={'16'} />
|
||||||
</TransactionStatusText>
|
</TransactionStatusText>
|
||||||
)}
|
)}
|
||||||
|
{isCopied ? '' : props.children}
|
||||||
</CopyIcon>
|
</CopyIcon>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +1,39 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { Check, Triangle } from 'react-feather'
|
import { CheckCircle, Triangle } from 'react-feather'
|
||||||
|
|
||||||
import { useActiveWeb3React } from '../../hooks'
|
import { useActiveWeb3React } from '../../hooks'
|
||||||
import { getEtherscanLink } from '../../utils'
|
import { getEtherscanLink } from '../../utils'
|
||||||
import { ExternalLink, Spinner } from '../../theme'
|
import { ExternalLink } from '../../theme'
|
||||||
import Circle from '../../assets/images/circle.svg'
|
|
||||||
|
|
||||||
import { transparentize } from 'polished'
|
|
||||||
import { useAllTransactions } from '../../state/transactions/hooks'
|
import { useAllTransactions } from '../../state/transactions/hooks'
|
||||||
|
import { RowFixed } from '../Row'
|
||||||
|
import Loader from '../Loader'
|
||||||
|
|
||||||
const TransactionWrapper = styled.div`
|
const TransactionWrapper = styled.div``
|
||||||
margin-top: 0.75rem;
|
|
||||||
`
|
|
||||||
|
|
||||||
const TransactionStatusText = styled.div`
|
const TransactionStatusText = styled.div`
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const TransactionState = styled(ExternalLink)<{ pending: boolean; success?: boolean }>`
|
const TransactionState = styled(ExternalLink)<{ pending: boolean; success?: boolean }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
text-decoration: none !important;
|
text-decoration: none !important;
|
||||||
|
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.25rem 0rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 0.75rem;
|
font-size: 0.825rem;
|
||||||
border: 1px solid;
|
color: ${({ theme }) => theme.primary1};
|
||||||
|
|
||||||
color: ${({ pending, success, theme }) => (pending ? theme.primary1 : success ? theme.green1 : theme.red1)};
|
|
||||||
|
|
||||||
border-color: ${({ pending, success, theme }) =>
|
|
||||||
pending
|
|
||||||
? transparentize(0.75, theme.primary1)
|
|
||||||
: success
|
|
||||||
? transparentize(0.75, theme.green1)
|
|
||||||
: transparentize(0.75, theme.red1)};
|
|
||||||
|
|
||||||
:hover {
|
|
||||||
border-color: ${({ pending, success, theme }) =>
|
|
||||||
pending
|
|
||||||
? transparentize(0, theme.primary1)
|
|
||||||
: success
|
|
||||||
? transparentize(0, theme.green1)
|
|
||||||
: transparentize(0, theme.red1)};
|
|
||||||
}
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const IconWrapper = styled.div`
|
const IconWrapper = styled.div<{ pending: boolean; success?: boolean }>`
|
||||||
flex-shrink: 0;
|
color: ${({ pending, success, theme }) => (pending ? theme.primary1 : success ? theme.green1 : theme.red1)};
|
||||||
`
|
`
|
||||||
|
|
||||||
export default function Transaction({ hash }: { hash: string }) {
|
export default function Transaction({ hash }: { hash: string }) {
|
||||||
@@ -65,9 +49,11 @@ export default function Transaction({ hash }: { hash: string }) {
|
|||||||
return (
|
return (
|
||||||
<TransactionWrapper>
|
<TransactionWrapper>
|
||||||
<TransactionState href={getEtherscanLink(chainId, hash, 'transaction')} pending={pending} success={success}>
|
<TransactionState href={getEtherscanLink(chainId, hash, 'transaction')} pending={pending} success={success}>
|
||||||
<TransactionStatusText>{summary ? summary : hash}</TransactionStatusText>
|
<RowFixed>
|
||||||
<IconWrapper>
|
<TransactionStatusText>{summary ?? hash} ↗</TransactionStatusText>
|
||||||
{pending ? <Spinner src={Circle} /> : success ? <Check size="16" /> : <Triangle size="16" />}
|
</RowFixed>
|
||||||
|
<IconWrapper pending={pending} success={success}>
|
||||||
|
{pending ? <Loader /> : success ? <CheckCircle size="16" /> : <Triangle size="16" />}
|
||||||
</IconWrapper>
|
</IconWrapper>
|
||||||
</TransactionState>
|
</TransactionState>
|
||||||
</TransactionWrapper>
|
</TransactionWrapper>
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import React, { useCallback, useContext } from 'react'
|
|||||||
import { useDispatch } from 'react-redux'
|
import { useDispatch } from 'react-redux'
|
||||||
import styled, { ThemeContext } from 'styled-components'
|
import styled, { ThemeContext } from 'styled-components'
|
||||||
import { useActiveWeb3React } from '../../hooks'
|
import { useActiveWeb3React } from '../../hooks'
|
||||||
import { isMobile } from 'react-device-detect'
|
|
||||||
import { AppDispatch } from '../../state'
|
import { AppDispatch } from '../../state'
|
||||||
import { clearAllTransactions } from '../../state/transactions/actions'
|
import { clearAllTransactions } from '../../state/transactions/actions'
|
||||||
|
import { shortenAddress } from '../../utils'
|
||||||
import { AutoRow } from '../Row'
|
import { AutoRow } from '../Row'
|
||||||
import Copy from './Copy'
|
import Copy from './Copy'
|
||||||
import Transaction from './Transaction'
|
import Transaction from './Transaction'
|
||||||
@@ -18,9 +18,8 @@ import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
|
|||||||
import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
|
import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
|
||||||
import PortisIcon from '../../assets/images/portisIcon.png'
|
import PortisIcon from '../../assets/images/portisIcon.png'
|
||||||
import Identicon from '../Identicon'
|
import Identicon from '../Identicon'
|
||||||
|
import { ButtonSecondary } from '../Button'
|
||||||
import { ButtonEmpty } from '../Button'
|
import { ExternalLink as LinkIcon } from 'react-feather'
|
||||||
|
|
||||||
import { ExternalLink, LinkStyledButton, TYPE } from '../../theme'
|
import { ExternalLink, LinkStyledButton, TYPE } from '../../theme'
|
||||||
|
|
||||||
const HeaderRow = styled.div`
|
const HeaderRow = styled.div`
|
||||||
@@ -55,31 +54,31 @@ const UpperSection = styled.div`
|
|||||||
|
|
||||||
const InfoCard = styled.div`
|
const InfoCard = styled.div`
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
background-color: ${({ theme }) => theme.bg2};
|
border: 1px solid ${({ theme }) => theme.bg3};
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-row-gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const AccountGroupingRow = styled.div`
|
const AccountGroupingRow = styled.div`
|
||||||
${({ theme }) => theme.flexRowNoWrap};
|
${({ theme }) => theme.flexRowNoWrap};
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-weight: 500;
|
font-weight: 400;
|
||||||
color: ${({ theme }) => theme.text1};
|
color: ${({ theme }) => theme.text1};
|
||||||
|
|
||||||
div {
|
div {
|
||||||
${({ theme }) => theme.flexRowNoWrap}
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:first-of-type {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const AccountSection = styled.div`
|
const AccountSection = styled.div`
|
||||||
background-color: ${({ theme }) => theme.bg1};
|
background-color: ${({ theme }) => theme.bg1};
|
||||||
padding: 0rem 1rem;
|
padding: 0rem 1rem;
|
||||||
${({ theme }) => theme.mediaWidth.upToMedium`padding: 0rem 1rem 1rem 1rem;`};
|
${({ theme }) => theme.mediaWidth.upToMedium`padding: 0rem 1rem 1.5rem 1rem;`};
|
||||||
`
|
`
|
||||||
|
|
||||||
const YourAccount = styled.div`
|
const YourAccount = styled.div`
|
||||||
@@ -94,28 +93,6 @@ const YourAccount = styled.div`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const GreenCircle = styled.div`
|
|
||||||
${({ theme }) => theme.flexRowNoWrap}
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
height: 8px;
|
|
||||||
width: 8px;
|
|
||||||
margin-left: 12px;
|
|
||||||
margin-right: 2px;
|
|
||||||
background-color: ${({ theme }) => theme.green1};
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const CircleWrapper = styled.div`
|
|
||||||
color: ${({ theme }) => theme.green1};
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
`
|
|
||||||
|
|
||||||
const LowerSection = styled.div`
|
const LowerSection = styled.div`
|
||||||
${({ theme }) => theme.flexColumnNoWrap}
|
${({ theme }) => theme.flexColumnNoWrap}
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
@@ -132,13 +109,14 @@ const LowerSection = styled.div`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const AccountControl = styled.div<{ hasENS: boolean; isENS: boolean }>`
|
const AccountControl = styled.div`
|
||||||
${({ theme }) => theme.flexRowNoWrap};
|
display: flex;
|
||||||
align-items: center;
|
justify-content: space-between;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
font-weight: ${({ hasENS, isENS }) => (hasENS ? (isENS ? 500 : 400) : 500)};
|
font-weight: 500;
|
||||||
font-size: ${({ hasENS, isENS }) => (hasENS ? (isENS ? '1rem' : '0.8rem') : '1rem')};
|
font-size: 1.25rem;
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
@@ -146,22 +124,22 @@ const AccountControl = styled.div<{ hasENS: boolean; isENS: boolean }>`
|
|||||||
|
|
||||||
p {
|
p {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
margin: 0.5rem 0;
|
margin: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const ConnectButtonRow = styled.div`
|
|
||||||
${({ theme }) => theme.flexRowNoWrap}
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 10px 0;
|
|
||||||
`
|
|
||||||
|
|
||||||
const AddressLink = styled(ExternalLink)<{ hasENS: boolean; isENS: boolean }>`
|
const AddressLink = styled(ExternalLink)<{ hasENS: boolean; isENS: boolean }>`
|
||||||
color: ${({ hasENS, isENS, theme }) => (hasENS ? (isENS ? theme.primary1 : theme.text3) : theme.primary1)};
|
font-size: 0.825rem;
|
||||||
|
color: ${({ theme }) => theme.text3};
|
||||||
|
margin-left: 1rem;
|
||||||
|
font-size: 0.825rem;
|
||||||
|
display: flex;
|
||||||
|
:hover {
|
||||||
|
color: ${({ theme }) => theme.text2};
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const CloseIcon = styled.div`
|
const CloseIcon = styled.div`
|
||||||
@@ -181,14 +159,17 @@ const CloseColor = styled(Close)`
|
|||||||
`
|
`
|
||||||
|
|
||||||
const WalletName = styled.div`
|
const WalletName = styled.div`
|
||||||
padding-left: 0.5rem;
|
|
||||||
width: initial;
|
width: initial;
|
||||||
|
font-size: 0.825rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: ${({ theme }) => theme.text3};
|
||||||
`
|
`
|
||||||
|
|
||||||
const IconWrapper = styled.div<{ size?: number }>`
|
const IconWrapper = styled.div<{ size?: number }>`
|
||||||
${({ theme }) => theme.flexColumnNoWrap};
|
${({ theme }) => theme.flexColumnNoWrap};
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
margin-right: 8px;
|
||||||
& > img,
|
& > img,
|
||||||
span {
|
span {
|
||||||
height: ${({ size }) => (size ? size + 'px' : '32px')};
|
height: ${({ size }) => (size ? size + 'px' : '32px')};
|
||||||
@@ -203,10 +184,12 @@ const TransactionListWrapper = styled.div`
|
|||||||
${({ theme }) => theme.flexColumnNoWrap};
|
${({ theme }) => theme.flexColumnNoWrap};
|
||||||
`
|
`
|
||||||
|
|
||||||
const WalletAction = styled.div`
|
const WalletAction = styled(ButtonSecondary)`
|
||||||
color: ${({ theme }) => theme.text4};
|
width: fit-content;
|
||||||
margin-left: 16px;
|
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 0.825rem;
|
||||||
|
padding: 4px 6px;
|
||||||
:hover {
|
:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
@@ -255,39 +238,39 @@ export default function AccountDetails({
|
|||||||
SUPPORTED_WALLETS[k].connector === connector && (connector !== injected || isMetaMask === (k === 'METAMASK'))
|
SUPPORTED_WALLETS[k].connector === connector && (connector !== injected || isMetaMask === (k === 'METAMASK'))
|
||||||
)
|
)
|
||||||
.map(k => SUPPORTED_WALLETS[k].name)[0]
|
.map(k => SUPPORTED_WALLETS[k].name)[0]
|
||||||
return <WalletName>{name}</WalletName>
|
return <WalletName>Connected with {name}</WalletName>
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatusIcon() {
|
function getStatusIcon() {
|
||||||
if (connector === injected) {
|
if (connector === injected) {
|
||||||
return (
|
return (
|
||||||
<IconWrapper size={16}>
|
<IconWrapper size={16}>
|
||||||
<Identicon /> {formatConnectorName()}
|
<Identicon />
|
||||||
</IconWrapper>
|
</IconWrapper>
|
||||||
)
|
)
|
||||||
} else if (connector === walletconnect) {
|
} else if (connector === walletconnect) {
|
||||||
return (
|
return (
|
||||||
<IconWrapper size={16}>
|
<IconWrapper size={16}>
|
||||||
<img src={WalletConnectIcon} alt={''} /> {formatConnectorName()}
|
<img src={WalletConnectIcon} alt={''} />
|
||||||
</IconWrapper>
|
</IconWrapper>
|
||||||
)
|
)
|
||||||
} else if (connector === walletlink) {
|
} else if (connector === walletlink) {
|
||||||
return (
|
return (
|
||||||
<IconWrapper size={16}>
|
<IconWrapper size={16}>
|
||||||
<img src={CoinbaseWalletIcon} alt={''} /> {formatConnectorName()}
|
<img src={CoinbaseWalletIcon} alt={''} />
|
||||||
</IconWrapper>
|
</IconWrapper>
|
||||||
)
|
)
|
||||||
} else if (connector === fortmatic) {
|
} else if (connector === fortmatic) {
|
||||||
return (
|
return (
|
||||||
<IconWrapper size={16}>
|
<IconWrapper size={16}>
|
||||||
<img src={FortmaticIcon} alt={''} /> {formatConnectorName()}
|
<img src={FortmaticIcon} alt={''} />
|
||||||
</IconWrapper>
|
</IconWrapper>
|
||||||
)
|
)
|
||||||
} else if (connector === portis) {
|
} else if (connector === portis) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<IconWrapper size={16}>
|
<IconWrapper size={16}>
|
||||||
<img src={PortisIcon} alt={''} /> {formatConnectorName()}
|
<img src={PortisIcon} alt={''} />
|
||||||
<MainWalletAction
|
<MainWalletAction
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
portis.portis.showPortis()
|
portis.portis.showPortis()
|
||||||
@@ -320,10 +303,11 @@ export default function AccountDetails({
|
|||||||
<YourAccount>
|
<YourAccount>
|
||||||
<InfoCard>
|
<InfoCard>
|
||||||
<AccountGroupingRow>
|
<AccountGroupingRow>
|
||||||
{getStatusIcon()}
|
{formatConnectorName()}
|
||||||
<div>
|
<div>
|
||||||
{connector !== injected && connector !== walletlink && (
|
{connector !== injected && connector !== walletlink && (
|
||||||
<WalletAction
|
<WalletAction
|
||||||
|
style={{ fontSize: '.825rem', fontWeight: 400, marginRight: '8px' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
;(connector as any).close()
|
;(connector as any).close()
|
||||||
}}
|
}}
|
||||||
@@ -331,73 +315,82 @@ export default function AccountDetails({
|
|||||||
Disconnect
|
Disconnect
|
||||||
</WalletAction>
|
</WalletAction>
|
||||||
)}
|
)}
|
||||||
<CircleWrapper>
|
<WalletAction
|
||||||
<GreenCircle>
|
style={{ fontSize: '.825rem', fontWeight: 400 }}
|
||||||
<div />
|
onClick={() => {
|
||||||
</GreenCircle>
|
openOptions()
|
||||||
</CircleWrapper>
|
}}
|
||||||
|
>
|
||||||
|
Change
|
||||||
|
</WalletAction>
|
||||||
</div>
|
</div>
|
||||||
</AccountGroupingRow>
|
</AccountGroupingRow>
|
||||||
<AccountGroupingRow id="web3-account-identifier-row">
|
<AccountGroupingRow id="web3-account-identifier-row">
|
||||||
{ENSName ? (
|
<AccountControl>
|
||||||
<>
|
{ENSName ? (
|
||||||
<AccountControl hasENS={!!ENSName} isENS={true}>
|
<>
|
||||||
<p>{ENSName}</p> <Copy toCopy={account} />
|
<div>
|
||||||
</AccountControl>
|
{getStatusIcon()}
|
||||||
</>
|
<p> {ENSName}</p>
|
||||||
) : (
|
</div>
|
||||||
<>
|
</>
|
||||||
<AccountControl hasENS={!!ENSName} isENS={false}>
|
) : (
|
||||||
<p>{account}</p> <Copy toCopy={account} />
|
<>
|
||||||
</AccountControl>
|
<div>
|
||||||
</>
|
{getStatusIcon()}
|
||||||
)}
|
<p> {shortenAddress(account)}</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AccountControl>
|
||||||
</AccountGroupingRow>
|
</AccountGroupingRow>
|
||||||
<AccountGroupingRow>
|
<AccountGroupingRow>
|
||||||
{ENSName ? (
|
{ENSName ? (
|
||||||
<>
|
<>
|
||||||
<AccountControl hasENS={!!ENSName} isENS={false}>
|
<AccountControl>
|
||||||
<AddressLink hasENS={!!ENSName} isENS={true} href={getEtherscanLink(chainId, ENSName, 'address')}>
|
<div>
|
||||||
View on Etherscan ↗
|
<Copy toCopy={account}>
|
||||||
</AddressLink>
|
<span style={{ marginLeft: '4px' }}>Copy Address</span>
|
||||||
|
</Copy>
|
||||||
|
<AddressLink
|
||||||
|
hasENS={!!ENSName}
|
||||||
|
isENS={true}
|
||||||
|
href={getEtherscanLink(chainId, ENSName, 'address')}
|
||||||
|
>
|
||||||
|
<LinkIcon size={16} />
|
||||||
|
<span style={{ marginLeft: '4px' }}>View on Etherscan</span>
|
||||||
|
</AddressLink>
|
||||||
|
</div>
|
||||||
</AccountControl>
|
</AccountControl>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<AccountControl hasENS={!!ENSName} isENS={false}>
|
<AccountControl>
|
||||||
<AddressLink
|
<div>
|
||||||
hasENS={!!ENSName}
|
<Copy toCopy={account}>
|
||||||
isENS={false}
|
<span style={{ marginLeft: '4px' }}>Copy Address</span>
|
||||||
href={getEtherscanLink(chainId, account, 'address')}
|
</Copy>
|
||||||
>
|
<AddressLink
|
||||||
View on Etherscan ↗
|
hasENS={!!ENSName}
|
||||||
</AddressLink>
|
isENS={false}
|
||||||
|
href={getEtherscanLink(chainId, account, 'address')}
|
||||||
|
>
|
||||||
|
<LinkIcon size={16} />
|
||||||
|
<span style={{ marginLeft: '4px' }}>View on Etherscan</span>
|
||||||
|
</AddressLink>
|
||||||
|
</div>
|
||||||
</AccountControl>
|
</AccountControl>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{/* {formatConnectorName()} */}
|
||||||
</AccountGroupingRow>
|
</AccountGroupingRow>
|
||||||
</InfoCard>
|
</InfoCard>
|
||||||
</YourAccount>
|
</YourAccount>
|
||||||
|
|
||||||
{!(isMobile && (window.web3 || window.ethereum)) && (
|
|
||||||
<ConnectButtonRow>
|
|
||||||
<ButtonEmpty
|
|
||||||
style={{ fontWeight: 400 }}
|
|
||||||
padding={'12px'}
|
|
||||||
width={'260px'}
|
|
||||||
onClick={() => {
|
|
||||||
openOptions()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Connect to a different wallet
|
|
||||||
</ButtonEmpty>
|
|
||||||
</ConnectButtonRow>
|
|
||||||
)}
|
|
||||||
</AccountSection>
|
</AccountSection>
|
||||||
</UpperSection>
|
</UpperSection>
|
||||||
{!!pendingTransactions.length || !!confirmedTransactions.length ? (
|
{!!pendingTransactions.length || !!confirmedTransactions.length ? (
|
||||||
<LowerSection>
|
<LowerSection>
|
||||||
<AutoRow style={{ justifyContent: 'space-between' }}>
|
<AutoRow mb={'1rem'} style={{ justifyContent: 'space-between' }}>
|
||||||
<TYPE.body>Recent Transactions</TYPE.body>
|
<TYPE.body>Recent Transactions</TYPE.body>
|
||||||
<LinkStyledButton onClick={clearAllTransactionsCallback}>(clear all)</LinkStyledButton>
|
<LinkStyledButton onClick={clearAllTransactionsCallback}>(clear all)</LinkStyledButton>
|
||||||
</AutoRow>
|
</AutoRow>
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import React, { useState, useEffect, useContext } from 'react'
|
import React, { useContext, useCallback } from 'react'
|
||||||
import styled, { ThemeContext } from 'styled-components'
|
import styled, { ThemeContext } from 'styled-components'
|
||||||
import useDebounce from '../../hooks/useDebounce'
|
import useENS from '../../hooks/useENS'
|
||||||
|
|
||||||
import { isAddress } from '../../utils'
|
|
||||||
import { useActiveWeb3React } from '../../hooks'
|
import { useActiveWeb3React } from '../../hooks'
|
||||||
import { ExternalLink, TYPE } from '../../theme'
|
import { ExternalLink, TYPE } from '../../theme'
|
||||||
import { AutoColumn } from '../Column'
|
import { AutoColumn } from '../Column'
|
||||||
@@ -24,6 +22,8 @@ const ContainerRow = styled.div<{ error: boolean }>`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 1.25rem;
|
border-radius: 1.25rem;
|
||||||
border: 1px solid ${({ error, theme }) => (error ? theme.red1 : theme.bg2)};
|
border: 1px solid ${({ error, theme }) => (error ? theme.red1 : theme.bg2)};
|
||||||
|
transition: border-color 300ms ${({ error }) => (error ? 'step-end' : 'step-start')},
|
||||||
|
color 500ms ${({ error }) => (error ? 'step-end' : 'step-start')};
|
||||||
background-color: ${({ theme }) => theme.bg1};
|
background-color: ${({ theme }) => theme.bg1};
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -39,6 +39,7 @@ const Input = styled.input<{ error?: boolean }>`
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
width: 0;
|
width: 0;
|
||||||
background-color: ${({ theme }) => theme.bg1};
|
background-color: ${({ theme }) => theme.bg1};
|
||||||
|
transition: color 300ms ${({ error }) => (error ? 'step-end' : 'step-start')};
|
||||||
color: ${({ error, theme }) => (error ? theme.red1 : theme.primary1)};
|
color: ${({ error, theme }) => (error ? theme.red1 : theme.primary1)};
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -64,120 +65,65 @@ const Input = styled.input<{ error?: boolean }>`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
interface Value {
|
||||||
|
address: string
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
|
||||||
export default function AddressInputPanel({
|
export default function AddressInputPanel({
|
||||||
initialInput = '',
|
id,
|
||||||
onChange,
|
value,
|
||||||
onError
|
onChange
|
||||||
}: {
|
}: {
|
||||||
initialInput?: string
|
id?: string
|
||||||
onChange: (val: { address: string; name?: string }) => void
|
// the typed string value
|
||||||
onError: (error: boolean, input: string) => void
|
value: string
|
||||||
|
// triggers whenever the typed value changes
|
||||||
|
onChange: (value: string) => void
|
||||||
}) {
|
}) {
|
||||||
const { chainId, library } = useActiveWeb3React()
|
const { chainId } = useActiveWeb3React()
|
||||||
const theme = useContext(ThemeContext)
|
const theme = useContext(ThemeContext)
|
||||||
|
|
||||||
const [input, setInput] = useState(initialInput ? initialInput : '')
|
const { address, loading, name } = useENS(value)
|
||||||
const debouncedInput = useDebounce(input, 200)
|
|
||||||
|
|
||||||
const [data, setData] = useState<{ address: string; name: string }>({ address: undefined, name: undefined })
|
const handleInput = useCallback(
|
||||||
const [error, setError] = useState<boolean>(false)
|
event => {
|
||||||
|
const input = event.target.value
|
||||||
|
const withoutSpaces = input.replace(/\s+/g, '')
|
||||||
|
onChange(withoutSpaces)
|
||||||
|
},
|
||||||
|
[onChange]
|
||||||
|
)
|
||||||
|
|
||||||
// keep data and errors in sync
|
const error = Boolean(value.length > 0 && !loading && !address)
|
||||||
useEffect(() => {
|
|
||||||
onChange({ address: data.address, name: data.name })
|
|
||||||
}, [onChange, data.address, data.name])
|
|
||||||
useEffect(() => {
|
|
||||||
onError(error, input)
|
|
||||||
}, [onError, error, input])
|
|
||||||
|
|
||||||
// run parser on debounced input
|
|
||||||
useEffect(() => {
|
|
||||||
let stale = false
|
|
||||||
// if the input is an address, try to look up its name
|
|
||||||
if (isAddress(debouncedInput)) {
|
|
||||||
library
|
|
||||||
.lookupAddress(debouncedInput)
|
|
||||||
.then(name => {
|
|
||||||
if (stale) return
|
|
||||||
// if an ENS name exists, set it as the destination
|
|
||||||
if (name) {
|
|
||||||
setInput(name)
|
|
||||||
} else {
|
|
||||||
setData({ address: debouncedInput, name: '' })
|
|
||||||
setError(null)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
if (stale) return
|
|
||||||
setData({ address: debouncedInput, name: '' })
|
|
||||||
setError(null)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// otherwise try to look up the address of the input, treated as an ENS name
|
|
||||||
else {
|
|
||||||
if (debouncedInput !== '') {
|
|
||||||
library
|
|
||||||
.resolveName(debouncedInput)
|
|
||||||
.then(address => {
|
|
||||||
if (stale) return
|
|
||||||
// if the debounced input name resolves to an address
|
|
||||||
if (address) {
|
|
||||||
setData({ address: address, name: debouncedInput })
|
|
||||||
setError(null)
|
|
||||||
} else {
|
|
||||||
setError(true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
if (stale) return
|
|
||||||
setError(true)
|
|
||||||
})
|
|
||||||
} else if (debouncedInput === '') {
|
|
||||||
setError(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
stale = true
|
|
||||||
}
|
|
||||||
}, [debouncedInput, library])
|
|
||||||
|
|
||||||
function onInput(event) {
|
|
||||||
setData({ address: undefined, name: undefined })
|
|
||||||
setError(false)
|
|
||||||
const input = event.target.value
|
|
||||||
const checksummedInput = isAddress(input.replace(/\s/g, '')) // delete whitespace
|
|
||||||
setInput(checksummedInput || input)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputPanel>
|
<InputPanel id={id}>
|
||||||
<ContainerRow error={input !== '' && error}>
|
<ContainerRow error={error}>
|
||||||
<InputContainer>
|
<InputContainer>
|
||||||
<AutoColumn gap="md">
|
<AutoColumn gap="md">
|
||||||
<RowBetween>
|
<RowBetween>
|
||||||
<TYPE.black color={theme.text2} fontWeight={500} fontSize={14}>
|
<TYPE.black color={theme.text2} fontWeight={500} fontSize={14}>
|
||||||
Recipient
|
Recipient
|
||||||
</TYPE.black>
|
</TYPE.black>
|
||||||
{data.address && (
|
{address && (
|
||||||
<ExternalLink
|
<ExternalLink href={getEtherscanLink(chainId, name ?? address, 'address')} style={{ fontSize: '14px' }}>
|
||||||
href={getEtherscanLink(chainId, data.name || data.address, 'address')}
|
|
||||||
style={{ fontSize: '14px' }}
|
|
||||||
>
|
|
||||||
(View on Etherscan)
|
(View on Etherscan)
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
)}
|
)}
|
||||||
</RowBetween>
|
</RowBetween>
|
||||||
<Input
|
<Input
|
||||||
|
className="recipient-address-input"
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
placeholder="Wallet Address or ENS name"
|
placeholder="Wallet Address or ENS name"
|
||||||
error={input !== '' && error}
|
error={error}
|
||||||
onChange={onInput}
|
pattern="^(0x[a-fA-F0-9]{40})$"
|
||||||
value={input}
|
onChange={handleInput}
|
||||||
|
value={value}
|
||||||
/>
|
/>
|
||||||
</AutoColumn>
|
</AutoColumn>
|
||||||
</InputContainer>
|
</InputContainer>
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ import { RowBetween } from '../Row'
|
|||||||
import { ChevronDown } from 'react-feather'
|
import { ChevronDown } from 'react-feather'
|
||||||
import { Button as RebassButton, ButtonProps } from 'rebass/styled-components'
|
import { Button as RebassButton, ButtonProps } from 'rebass/styled-components'
|
||||||
|
|
||||||
const Base = styled(RebassButton)<{ padding?: string; width?: string; borderRadius?: string }>`
|
const Base = styled(RebassButton)<{
|
||||||
|
padding?: string
|
||||||
|
width?: string
|
||||||
|
borderRadius?: string
|
||||||
|
altDisbaledStyle?: boolean
|
||||||
|
}>`
|
||||||
padding: ${({ padding }) => (padding ? padding : '18px')};
|
padding: ${({ padding }) => (padding ? padding : '18px')};
|
||||||
width: ${({ width }) => (width ? width : '100%')};
|
width: ${({ width }) => (width ? width : '100%')};
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -16,6 +21,7 @@ const Base = styled(RebassButton)<{ padding?: string; width?: string; borderRadi
|
|||||||
outline: none;
|
outline: none;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
color: white;
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
@@ -45,10 +51,12 @@ export const ButtonPrimary = styled(Base)`
|
|||||||
background-color: ${({ theme }) => darken(0.1, theme.primary1)};
|
background-color: ${({ theme }) => darken(0.1, theme.primary1)};
|
||||||
}
|
}
|
||||||
&:disabled {
|
&:disabled {
|
||||||
background-color: ${({ theme }) => theme.bg3};
|
background-color: ${({ theme, altDisbaledStyle }) => (altDisbaledStyle ? theme.primary1 : theme.bg3)};
|
||||||
color: ${({ theme }) => theme.text3}
|
color: ${({ theme, altDisbaledStyle }) => (altDisbaledStyle ? 'white' : theme.text3)};
|
||||||
cursor: auto;
|
cursor: auto;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -68,6 +76,16 @@ export const ButtonLight = styled(Base)`
|
|||||||
box-shadow: 0 0 0 1pt ${({ theme, disabled }) => !disabled && darken(0.05, theme.primary5)};
|
box-shadow: 0 0 0 1pt ${({ theme, disabled }) => !disabled && darken(0.05, theme.primary5)};
|
||||||
background-color: ${({ theme, disabled }) => !disabled && darken(0.05, theme.primary5)};
|
background-color: ${({ theme, disabled }) => !disabled && darken(0.05, theme.primary5)};
|
||||||
}
|
}
|
||||||
|
:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
:hover {
|
||||||
|
cursor: auto;
|
||||||
|
background-color: ${({ theme }) => theme.primary5};
|
||||||
|
box-shadow: none;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export const ButtonGray = styled(Base)`
|
export const ButtonGray = styled(Base)`
|
||||||
@@ -180,7 +198,6 @@ export const ButtonEmpty = styled(Base)`
|
|||||||
export const ButtonWhite = styled(Base)`
|
export const ButtonWhite = styled(Base)`
|
||||||
border: 1px solid #edeef2;
|
border: 1px solid #edeef2;
|
||||||
background-color: ${({ theme }) => theme.bg1};
|
background-color: ${({ theme }) => theme.bg1};
|
||||||
};
|
|
||||||
color: black;
|
color: black;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
@@ -228,6 +245,9 @@ const ButtonErrorStyle = styled(Base)`
|
|||||||
&:disabled {
|
&:disabled {
|
||||||
opacity: 50%;
|
opacity: 50%;
|
||||||
cursor: auto;
|
cursor: auto;
|
||||||
|
box-shadow: none;
|
||||||
|
background-color: ${({ theme }) => theme.red1};
|
||||||
|
border: 1px solid ${({ theme }) => theme.red1};
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -247,7 +267,7 @@ export function ButtonError({ error, ...rest }: { error?: boolean } & ButtonProp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ButtonDropwdown({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
|
export function ButtonDropdown({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
|
||||||
return (
|
return (
|
||||||
<ButtonPrimary {...rest} disabled={disabled}>
|
<ButtonPrimary {...rest} disabled={disabled}>
|
||||||
<RowBetween>
|
<RowBetween>
|
||||||
@@ -258,7 +278,7 @@ export function ButtonDropwdown({ disabled = false, children, ...rest }: { disab
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ButtonDropwdownLight({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
|
export function ButtonDropdownLight({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
|
||||||
return (
|
return (
|
||||||
<ButtonOutlined {...rest} disabled={disabled}>
|
<ButtonOutlined {...rest} disabled={disabled}>
|
||||||
<RowBetween>
|
<RowBetween>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import React, { useContext } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import styled, { ThemeContext } from 'styled-components'
|
import styled, { ThemeContext } from 'styled-components'
|
||||||
import Modal from '../Modal'
|
import Modal from '../Modal'
|
||||||
import Loader from '../Loader'
|
|
||||||
import { ExternalLink } from '../../theme'
|
import { ExternalLink } from '../../theme'
|
||||||
import { Text } from 'rebass'
|
import { Text } from 'rebass'
|
||||||
import { CloseIcon } from '../../theme/components'
|
import { CloseIcon, Spinner } from '../../theme/components'
|
||||||
import { RowBetween } from '../Row'
|
import { RowBetween } from '../Row'
|
||||||
import { ArrowUpCircle } from 'react-feather'
|
import { ArrowUpCircle } from 'react-feather'
|
||||||
import { ButtonPrimary } from '../Button'
|
import { ButtonPrimary } from '../Button'
|
||||||
import { AutoColumn, ColumnCenter } from '../Column'
|
import { AutoColumn, ColumnCenter } from '../Column'
|
||||||
|
import Circle from '../../assets/images/blue-loader.svg'
|
||||||
|
|
||||||
import { useActiveWeb3React } from '../../hooks'
|
|
||||||
import { getEtherscanLink } from '../../utils'
|
import { getEtherscanLink } from '../../utils'
|
||||||
|
import { useActiveWeb3React } from '../../hooks'
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -30,6 +30,11 @@ const ConfirmedIcon = styled(ColumnCenter)`
|
|||||||
padding: 60px 0;
|
padding: 60px 0;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const CustomLightSpinner = styled(Spinner)<{ size: string }>`
|
||||||
|
height: ${({ size }) => size};
|
||||||
|
width: ${({ size }) => size};
|
||||||
|
`
|
||||||
|
|
||||||
interface ConfirmationModalProps {
|
interface ConfirmationModalProps {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
onDismiss: () => void
|
onDismiss: () => void
|
||||||
@@ -37,7 +42,6 @@ interface ConfirmationModalProps {
|
|||||||
topContent: () => React.ReactChild
|
topContent: () => React.ReactChild
|
||||||
bottomContent: () => React.ReactChild
|
bottomContent: () => React.ReactChild
|
||||||
attemptingTxn: boolean
|
attemptingTxn: boolean
|
||||||
pendingConfirmation: boolean
|
|
||||||
pendingText: string
|
pendingText: string
|
||||||
title?: string
|
title?: string
|
||||||
}
|
}
|
||||||
@@ -45,33 +49,22 @@ interface ConfirmationModalProps {
|
|||||||
export default function ConfirmationModal({
|
export default function ConfirmationModal({
|
||||||
isOpen,
|
isOpen,
|
||||||
onDismiss,
|
onDismiss,
|
||||||
hash,
|
|
||||||
topContent,
|
topContent,
|
||||||
bottomContent,
|
bottomContent,
|
||||||
attemptingTxn,
|
attemptingTxn,
|
||||||
pendingConfirmation,
|
hash,
|
||||||
pendingText,
|
pendingText,
|
||||||
title = ''
|
title = ''
|
||||||
}: ConfirmationModalProps) {
|
}: ConfirmationModalProps) {
|
||||||
const { chainId } = useActiveWeb3React()
|
const { chainId } = useActiveWeb3React()
|
||||||
const theme = useContext(ThemeContext)
|
const theme = useContext(ThemeContext)
|
||||||
|
|
||||||
return (
|
const transactionBroadcast = !!hash
|
||||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90}>
|
|
||||||
{!attemptingTxn ? (
|
// waiting for user to confirm/reject tx _or_ showing info on a tx that has been broadcast
|
||||||
<Wrapper>
|
if (attemptingTxn || transactionBroadcast) {
|
||||||
<Section>
|
return (
|
||||||
<RowBetween>
|
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90}>
|
||||||
<Text fontWeight={500} fontSize={20}>
|
|
||||||
{title}
|
|
||||||
</Text>
|
|
||||||
<CloseIcon onClick={onDismiss} />
|
|
||||||
</RowBetween>
|
|
||||||
{topContent()}
|
|
||||||
</Section>
|
|
||||||
<BottomSection gap="12px">{bottomContent()}</BottomSection>
|
|
||||||
</Wrapper>
|
|
||||||
) : (
|
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Section>
|
<Section>
|
||||||
<RowBetween>
|
<RowBetween>
|
||||||
@@ -79,22 +72,23 @@ export default function ConfirmationModal({
|
|||||||
<CloseIcon onClick={onDismiss} />
|
<CloseIcon onClick={onDismiss} />
|
||||||
</RowBetween>
|
</RowBetween>
|
||||||
<ConfirmedIcon>
|
<ConfirmedIcon>
|
||||||
{pendingConfirmation ? (
|
{transactionBroadcast ? (
|
||||||
<Loader size="90px" />
|
|
||||||
) : (
|
|
||||||
<ArrowUpCircle strokeWidth={0.5} size={90} color={theme.primary1} />
|
<ArrowUpCircle strokeWidth={0.5} size={90} color={theme.primary1} />
|
||||||
|
) : (
|
||||||
|
<CustomLightSpinner src={Circle} alt="loader" size={'90px'} />
|
||||||
)}
|
)}
|
||||||
</ConfirmedIcon>
|
</ConfirmedIcon>
|
||||||
<AutoColumn gap="12px" justify={'center'}>
|
<AutoColumn gap="12px" justify={'center'}>
|
||||||
<Text fontWeight={500} fontSize={20}>
|
<Text fontWeight={500} fontSize={20}>
|
||||||
{!pendingConfirmation ? 'Transaction Submitted' : 'Waiting For Confirmation'}
|
{transactionBroadcast ? 'Transaction Submitted' : 'Waiting For Confirmation'}
|
||||||
</Text>
|
</Text>
|
||||||
<AutoColumn gap="12px" justify={'center'}>
|
<AutoColumn gap="12px" justify={'center'}>
|
||||||
<Text fontWeight={600} fontSize={14} color="" textAlign="center">
|
<Text fontWeight={600} fontSize={14} color="" textAlign="center">
|
||||||
{pendingText}
|
{pendingText}
|
||||||
</Text>
|
</Text>
|
||||||
</AutoColumn>
|
</AutoColumn>
|
||||||
{!pendingConfirmation && (
|
|
||||||
|
{transactionBroadcast ? (
|
||||||
<>
|
<>
|
||||||
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>
|
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>
|
||||||
<Text fontWeight={500} fontSize={14} color={theme.primary1}>
|
<Text fontWeight={500} fontSize={14} color={theme.primary1}>
|
||||||
@@ -107,9 +101,7 @@ export default function ConfirmationModal({
|
|||||||
</Text>
|
</Text>
|
||||||
</ButtonPrimary>
|
</ButtonPrimary>
|
||||||
</>
|
</>
|
||||||
)}
|
) : (
|
||||||
|
|
||||||
{pendingConfirmation && (
|
|
||||||
<Text fontSize={12} color="#565A69" textAlign="center">
|
<Text fontSize={12} color="#565A69" textAlign="center">
|
||||||
Confirm this transaction in your wallet
|
Confirm this transaction in your wallet
|
||||||
</Text>
|
</Text>
|
||||||
@@ -117,7 +109,25 @@ export default function ConfirmationModal({
|
|||||||
</AutoColumn>
|
</AutoColumn>
|
||||||
</Section>
|
</Section>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
)}
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// confirmation screen
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90}>
|
||||||
|
<Wrapper>
|
||||||
|
<Section>
|
||||||
|
<RowBetween>
|
||||||
|
<Text fontWeight={500} fontSize={20}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<CloseIcon onClick={onDismiss} />
|
||||||
|
</RowBetween>
|
||||||
|
{topContent()}
|
||||||
|
</Section>
|
||||||
|
<BottomSection gap="12px">{bottomContent()}</BottomSection>
|
||||||
|
</Wrapper>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Pair, Token } from '@uniswap/sdk'
|
import { Pair, Token } from '@uniswap/sdk'
|
||||||
import React, { useState, useContext } from 'react'
|
import React, { useState, useContext, useCallback } from 'react'
|
||||||
import styled, { ThemeContext } from 'styled-components'
|
import styled, { ThemeContext } from 'styled-components'
|
||||||
import { darken } from 'polished'
|
import { darken } from 'polished'
|
||||||
import { Field } from '../../state/swap/actions'
|
import { Field } from '../../state/swap/actions'
|
||||||
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
|
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||||
|
import TokenSearchModal from '../SearchModal/TokenSearchModal'
|
||||||
import TokenLogo from '../TokenLogo'
|
import TokenLogo from '../TokenLogo'
|
||||||
import DoubleLogo from '../DoubleLogo'
|
import DoubleLogo from '../DoubleLogo'
|
||||||
import SearchModal from '../SearchModal'
|
|
||||||
import { RowBetween } from '../Row'
|
import { RowBetween } from '../Row'
|
||||||
import { TYPE, CursorPointer } from '../../theme'
|
import { TYPE, CursorPointer } from '../../theme'
|
||||||
import { Input as NumericalInput } from '../NumericalInput'
|
import { Input as NumericalInput } from '../NumericalInput'
|
||||||
@@ -49,7 +49,6 @@ const LabelRow = styled.div`
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
line-height: 1rem;
|
line-height: 1rem;
|
||||||
padding: 0.75rem 1rem 0 1rem;
|
padding: 0.75rem 1rem 0 1rem;
|
||||||
height: 20px;
|
|
||||||
span:hover {
|
span:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: ${({ theme }) => darken(0.2, theme.text2)};
|
color: ${({ theme }) => darken(0.2, theme.text2)};
|
||||||
@@ -133,6 +132,7 @@ interface CurrencyInputPanelProps {
|
|||||||
showSendWithSwap?: boolean
|
showSendWithSwap?: boolean
|
||||||
otherSelectedTokenAddress?: string | null
|
otherSelectedTokenAddress?: string | null
|
||||||
id: string
|
id: string
|
||||||
|
showCommonBases?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CurrencyInputPanel({
|
export default function CurrencyInputPanel({
|
||||||
@@ -151,7 +151,8 @@ export default function CurrencyInputPanel({
|
|||||||
hideInput = false,
|
hideInput = false,
|
||||||
showSendWithSwap = false,
|
showSendWithSwap = false,
|
||||||
otherSelectedTokenAddress = null,
|
otherSelectedTokenAddress = null,
|
||||||
id
|
id,
|
||||||
|
showCommonBases
|
||||||
}: CurrencyInputPanelProps) {
|
}: CurrencyInputPanelProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
@@ -160,6 +161,10 @@ export default function CurrencyInputPanel({
|
|||||||
const userTokenBalance = useTokenBalanceTreatingWETHasETH(account, token)
|
const userTokenBalance = useTokenBalanceTreatingWETHasETH(account, token)
|
||||||
const theme = useContext(ThemeContext)
|
const theme = useContext(ThemeContext)
|
||||||
|
|
||||||
|
const handleDismissSearch = useCallback(() => {
|
||||||
|
setModalOpen(false)
|
||||||
|
}, [setModalOpen])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputPanel id={id}>
|
<InputPanel id={id}>
|
||||||
<Container hideInput={hideInput}>
|
<Container hideInput={hideInput}>
|
||||||
@@ -236,17 +241,15 @@ export default function CurrencyInputPanel({
|
|||||||
</InputRow>
|
</InputRow>
|
||||||
</Container>
|
</Container>
|
||||||
{!disableTokenSelect && (
|
{!disableTokenSelect && (
|
||||||
<SearchModal
|
<TokenSearchModal
|
||||||
isOpen={modalOpen}
|
isOpen={modalOpen}
|
||||||
onDismiss={() => {
|
onDismiss={handleDismissSearch}
|
||||||
setModalOpen(false)
|
|
||||||
}}
|
|
||||||
filterType="tokens"
|
|
||||||
onTokenSelect={onTokenSelection}
|
onTokenSelect={onTokenSelection}
|
||||||
showSendWithSwap={showSendWithSwap}
|
showSendWithSwap={showSendWithSwap}
|
||||||
hiddenToken={token?.address}
|
hiddenToken={token?.address}
|
||||||
otherSelectedTokenAddress={otherSelectedTokenAddress}
|
otherSelectedTokenAddress={otherSelectedTokenAddress}
|
||||||
otherSelectedText={field === Field.INPUT ? 'Selected as output' : 'Selected as input'}
|
otherSelectedText={field === Field.INPUT ? 'Selected as output' : 'Selected as input'}
|
||||||
|
showCommonBases={showCommonBases}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</InputPanel>
|
</InputPanel>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ const TokenWrapper = styled.div<{ margin: boolean; sizeraw: number }>`
|
|||||||
interface DoubleTokenLogoProps {
|
interface DoubleTokenLogoProps {
|
||||||
margin?: boolean
|
margin?: boolean
|
||||||
size?: number
|
size?: number
|
||||||
a0: string
|
a0?: string
|
||||||
a1: string
|
a1?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const HigherLogo = styled(TokenLogo)`
|
const HigherLogo = styled(TokenLogo)`
|
||||||
@@ -27,8 +27,8 @@ const CoveredLogo = styled(TokenLogo)<{ sizeraw: number }>`
|
|||||||
export default function DoubleTokenLogo({ a0, a1, size = 16, margin = false }: DoubleTokenLogoProps) {
|
export default function DoubleTokenLogo({ a0, a1, size = 16, margin = false }: DoubleTokenLogoProps) {
|
||||||
return (
|
return (
|
||||||
<TokenWrapper sizeraw={size} margin={margin}>
|
<TokenWrapper sizeraw={size} margin={margin}>
|
||||||
<HigherLogo address={a0} size={size.toString() + 'px'} />
|
{a0 && <HigherLogo address={a0} size={size.toString() + 'px'} />}
|
||||||
<CoveredLogo address={a1} size={size.toString() + 'px'} sizeraw={size} />
|
{a1 && <CoveredLogo address={a1} size={size.toString() + 'px'} sizeraw={size} />}
|
||||||
</TokenWrapper>
|
</TokenWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 React from 'react'
|
||||||
import { Link as HistoryLink } from 'react-router-dom'
|
import { isMobile } from 'react-device-detect'
|
||||||
|
import { Text } from 'rebass'
|
||||||
|
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
|
|
||||||
|
|
||||||
import Row from '../Row'
|
|
||||||
import Menu from '../Menu'
|
|
||||||
import Web3Status from '../Web3Status'
|
|
||||||
|
|
||||||
import { ExternalLink } from '../../theme'
|
|
||||||
import { Text } from 'rebass'
|
|
||||||
import { WETH, ChainId } from '@uniswap/sdk'
|
|
||||||
import { isMobile } from 'react-device-detect'
|
|
||||||
import { YellowCard } from '../Card'
|
|
||||||
import { useActiveWeb3React } from '../../hooks'
|
|
||||||
import { useDarkModeManager } from '../../state/user/hooks'
|
|
||||||
|
|
||||||
import Logo from '../../assets/svg/logo.svg'
|
import Logo from '../../assets/svg/logo.svg'
|
||||||
import Wordmark from '../../assets/svg/wordmark.svg'
|
|
||||||
import LogoDark from '../../assets/svg/logo_white.svg'
|
import LogoDark from '../../assets/svg/logo_white.svg'
|
||||||
|
import Wordmark from '../../assets/svg/wordmark.svg'
|
||||||
import WordmarkDark from '../../assets/svg/wordmark_white.svg'
|
import WordmarkDark from '../../assets/svg/wordmark_white.svg'
|
||||||
import { AutoColumn } from '../Column'
|
import { useActiveWeb3React } from '../../hooks'
|
||||||
import { RowBetween } from '../Row'
|
import { useDarkModeManager } from '../../state/user/hooks'
|
||||||
|
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||||
|
|
||||||
|
import { YellowCard } from '../Card'
|
||||||
|
import Settings from '../Settings'
|
||||||
|
import Menu from '../Menu'
|
||||||
|
|
||||||
|
import Row, { RowBetween } from '../Row'
|
||||||
|
import Web3Status from '../Web3Status'
|
||||||
|
import VersionSwitch from './VersionSwitch'
|
||||||
|
|
||||||
const HeaderFrame = styled.div`
|
const HeaderFrame = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -31,15 +29,12 @@ const HeaderFrame = styled.div`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||||
padding: 12px 0 0 0;
|
padding: 12px 0 0 0;
|
||||||
width: calc(100%);
|
width: calc(100%);
|
||||||
position: relative;
|
position: relative;
|
||||||
`};
|
`};
|
||||||
z-index: 2;
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const HeaderElement = styled.div`
|
const HeaderElement = styled.div`
|
||||||
@@ -47,7 +42,16 @@ const HeaderElement = styled.div`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Title = styled.div`
|
const HeaderElementWrap = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
`};
|
||||||
|
`
|
||||||
|
|
||||||
|
const Title = styled.a`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
@@ -72,6 +76,7 @@ const AccountElement = styled.div<{ active: boolean }>`
|
|||||||
background-color: ${({ theme, active }) => (!active ? theme.bg1 : theme.bg3)};
|
background-color: ${({ theme, active }) => (!active ? theme.bg1 : theme.bg3)};
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
:focus {
|
:focus {
|
||||||
border: 1px solid blue;
|
border: 1px solid blue;
|
||||||
@@ -82,10 +87,7 @@ const TestnetWrapper = styled.div`
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
|
pointer-events: auto;
|
||||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
|
||||||
display: none;
|
|
||||||
`};
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const NetworkCard = styled(YellowCard)`
|
const NetworkCard = styled(YellowCard)`
|
||||||
@@ -95,59 +97,42 @@ const NetworkCard = styled(YellowCard)`
|
|||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const UniIcon = styled(HistoryLink)<{ to: string }>`
|
const UniIcon = styled.div`
|
||||||
transition: transform 0.3s ease;
|
transition: transform 0.3s ease;
|
||||||
:hover {
|
:hover {
|
||||||
transform: rotate(-5deg);
|
transform: rotate(-5deg);
|
||||||
}
|
}
|
||||||
|
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||||
|
img {
|
||||||
|
width: 4.5rem;
|
||||||
|
}
|
||||||
|
`};
|
||||||
`
|
`
|
||||||
|
|
||||||
const MigrateBanner = styled(AutoColumn)`
|
const HeaderControls = styled.div`
|
||||||
width: 100%;
|
|
||||||
padding: 12px 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
flex-direction: row;
|
||||||
background-color: ${({ theme }) => theme.primary5};
|
align-items: center;
|
||||||
color: ${({ theme }) => theme.primaryText1};
|
|
||||||
font-weight: 400;
|
|
||||||
text-align: center;
|
|
||||||
pointer-events: auto;
|
|
||||||
a {
|
|
||||||
color: ${({ theme }) => theme.primaryText1};
|
|
||||||
}
|
|
||||||
|
|
||||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||||
padding: 0;
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
`};
|
||||||
|
`
|
||||||
|
|
||||||
|
const BalanceText = styled(Text)`
|
||||||
|
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||||
display: none;
|
display: none;
|
||||||
`};
|
`};
|
||||||
`
|
`
|
||||||
|
|
||||||
const VersionLabel = styled.span<{ isV2?: boolean }>`
|
const NETWORK_LABELS: { [chainId in ChainId]: string | null } = {
|
||||||
padding: ${({ isV2 }) => (isV2 ? '0.15rem 0.5rem 0.16rem 0.45rem' : '0.15rem 0.5rem 0.16rem 0.35rem')};
|
[ChainId.MAINNET]: null,
|
||||||
border-radius: 14px;
|
[ChainId.RINKEBY]: 'Rinkeby',
|
||||||
background: ${({ theme, isV2 }) => (isV2 ? theme.primary1 : 'none')};
|
[ChainId.ROPSTEN]: 'Ropsten',
|
||||||
color: ${({ theme, isV2 }) => (isV2 ? theme.white : theme.primary1)};
|
[ChainId.GÖRLI]: 'Görli',
|
||||||
font-size: 0.825rem;
|
[ChainId.KOVAN]: 'Kovan'
|
||||||
font-weight: 400;
|
}
|
||||||
:hover {
|
|
||||||
user-select: ${({ isV2 }) => (isV2 ? 'none' : 'initial')};
|
|
||||||
background: ${({ theme, isV2 }) => (isV2 ? theme.primary1 : 'none')};
|
|
||||||
color: ${({ theme, isV2 }) => (isV2 ? theme.white : theme.primary3)};
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const VersionToggle = styled.a`
|
|
||||||
border-radius: 16px;
|
|
||||||
border: 1px solid ${({ theme }) => theme.primary1};
|
|
||||||
color: ${({ theme }) => theme.primary1};
|
|
||||||
display: flex;
|
|
||||||
width: fit-content;
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: none;
|
|
||||||
:hover {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const { account, chainId } = useActiveWeb3React()
|
const { account, chainId } = useActiveWeb3React()
|
||||||
@@ -157,63 +142,37 @@ export default function Header() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<HeaderFrame>
|
<HeaderFrame>
|
||||||
<MigrateBanner>
|
<RowBetween style={{ alignItems: 'flex-start' }} padding="1rem 1rem 0 1rem">
|
||||||
Uniswap V2 is live! Read the
|
|
||||||
<ExternalLink href="https://uniswap.org/blog/launch-uniswap-v2/">
|
|
||||||
<b>blog post ↗</b>
|
|
||||||
</ExternalLink>
|
|
||||||
or
|
|
||||||
<ExternalLink href="https://migrate.uniswap.exchange/">
|
|
||||||
<b>migrate your liquidity ↗</b>
|
|
||||||
</ExternalLink>
|
|
||||||
.
|
|
||||||
</MigrateBanner>
|
|
||||||
<RowBetween padding="1rem">
|
|
||||||
<HeaderElement>
|
<HeaderElement>
|
||||||
<Title>
|
<Title href=".">
|
||||||
<UniIcon id="link" to="/">
|
<UniIcon>
|
||||||
<img src={isDark ? LogoDark : Logo} alt="logo" />
|
<img src={isDark ? LogoDark : Logo} alt="logo" />
|
||||||
</UniIcon>
|
</UniIcon>
|
||||||
{!isMobile && (
|
<TitleText>
|
||||||
<TitleText>
|
<img style={{ marginLeft: '4px', marginTop: '4px' }} src={isDark ? WordmarkDark : Wordmark} alt="logo" />
|
||||||
<HistoryLink id="link" to="/">
|
</TitleText>
|
||||||
<img
|
|
||||||
style={{ marginLeft: '4px', marginTop: '4px' }}
|
|
||||||
src={isDark ? WordmarkDark : Wordmark}
|
|
||||||
alt="logo"
|
|
||||||
/>
|
|
||||||
</HistoryLink>
|
|
||||||
</TitleText>
|
|
||||||
)}
|
|
||||||
</Title>
|
</Title>
|
||||||
<TestnetWrapper style={{ pointerEvents: 'auto' }}>
|
|
||||||
{!isMobile && (
|
|
||||||
<VersionToggle target="_self" href="https://v1.uniswap.exchange">
|
|
||||||
<VersionLabel isV2={true}>V2</VersionLabel>
|
|
||||||
<VersionLabel isV2={false}>V1</VersionLabel>
|
|
||||||
</VersionToggle>
|
|
||||||
)}
|
|
||||||
</TestnetWrapper>
|
|
||||||
</HeaderElement>
|
</HeaderElement>
|
||||||
<HeaderElement>
|
<HeaderControls>
|
||||||
<TestnetWrapper>
|
<HeaderElement>
|
||||||
{!isMobile && chainId === ChainId.ROPSTEN && <NetworkCard>Ropsten</NetworkCard>}
|
<TestnetWrapper>
|
||||||
{!isMobile && chainId === ChainId.RINKEBY && <NetworkCard>Rinkeby</NetworkCard>}
|
{!isMobile && NETWORK_LABELS[chainId] && <NetworkCard>{NETWORK_LABELS[chainId]}</NetworkCard>}
|
||||||
{!isMobile && chainId === ChainId.GÖRLI && <NetworkCard>Görli</NetworkCard>}
|
</TestnetWrapper>
|
||||||
{!isMobile && chainId === ChainId.KOVAN && <NetworkCard>Kovan</NetworkCard>}
|
<AccountElement active={!!account} style={{ pointerEvents: 'auto' }}>
|
||||||
</TestnetWrapper>
|
{account && userEthBalance ? (
|
||||||
<AccountElement active={!!account} style={{ pointerEvents: 'auto' }}>
|
<BalanceText style={{ flexShrink: 0 }} pl="0.75rem" pr="0.5rem" fontWeight={500}>
|
||||||
{account && userEthBalance ? (
|
{userEthBalance?.toSignificant(4)} ETH
|
||||||
<Text style={{ flexShrink: 0 }} px="0.5rem" fontWeight={500}>
|
</BalanceText>
|
||||||
{userEthBalance?.toSignificant(4)} ETH
|
) : null}
|
||||||
</Text>
|
<Web3Status />
|
||||||
) : null}
|
</AccountElement>
|
||||||
<Web3Status />
|
</HeaderElement>
|
||||||
</AccountElement>
|
<HeaderElementWrap>
|
||||||
<div style={{ pointerEvents: 'auto' }}>
|
<VersionSwitch />
|
||||||
|
<Settings />
|
||||||
<Menu />
|
<Menu />
|
||||||
</div>
|
</HeaderElementWrap>
|
||||||
</HeaderElement>
|
</HeaderControls>
|
||||||
</RowBetween>
|
</RowBetween>
|
||||||
</HeaderFrame>
|
</HeaderFrame>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,15 +1,38 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import styled from 'styled-components'
|
import styled, { keyframes } from 'styled-components'
|
||||||
|
|
||||||
import { Spinner } from '../../theme'
|
const rotate = keyframes`
|
||||||
import Circle from '../../assets/images/blue-loader.svg'
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
const SpinnerWrapper = styled(Spinner)<{ size: string }>`
|
}
|
||||||
height: ${({ size }) => size};
|
to {
|
||||||
width: ${({ size }) => size};
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export default function Loader({ size }: { size: string }) {
|
const StyledSVG = styled.svg<{ size: string; stroke?: string }>`
|
||||||
return <SpinnerWrapper src={Circle} alt="loader" size={size} />
|
animation: 2s ${rotate} linear infinite;
|
||||||
|
height: ${({ size }) => size};
|
||||||
|
width: ${({ size }) => size};
|
||||||
|
path {
|
||||||
|
stroke: ${({ stroke, theme }) => stroke ?? theme.primary1};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes in custom size and stroke for circle color, default to primary color as fill,
|
||||||
|
* need ...rest for layered styles on top
|
||||||
|
*/
|
||||||
|
export default function Loader({ size = '16px', stroke = null, ...rest }: { size?: string; stroke?: string }) {
|
||||||
|
return (
|
||||||
|
<StyledSVG viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" size={size} stroke={stroke} {...rest}>
|
||||||
|
<path
|
||||||
|
d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 9.27455 20.9097 6.80375 19.1414 5"
|
||||||
|
strokeWidth="2.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</StyledSVG>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,9 +77,7 @@ const MenuItem = styled(ExternalLink)`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const CODE_LINK = !!process.env.REACT_APP_GIT_COMMIT_HASH
|
const CODE_LINK = 'https://github.com/Uniswap/uniswap-interface'
|
||||||
? `https://github.com/Uniswap/uniswap-frontend/tree/${process.env.REACT_APP_GIT_COMMIT_HASH}`
|
|
||||||
: 'https://github.com/Uniswap/uniswap-frontend'
|
|
||||||
|
|
||||||
export default function Menu() {
|
export default function Menu() {
|
||||||
const node = useRef<HTMLDivElement>()
|
const node = useRef<HTMLDivElement>()
|
||||||
@@ -123,7 +121,7 @@ export default function Menu() {
|
|||||||
<Code size={14} />
|
<Code size={14} />
|
||||||
Code
|
Code
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem id="link" href="https://discord.gg/vXCdddD">
|
<MenuItem id="link" href="https://discord.gg/EwFs3Pp">
|
||||||
<MessageCircle size={14} />
|
<MessageCircle size={14} />
|
||||||
Discord
|
Discord
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const StyledDialogOverlay = styled(({ mobile, ...rest }) => <AnimatedDialogOverl
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
${({ mobile }) =>
|
${({ mobile }) =>
|
||||||
mobile &&
|
mobile &&
|
||||||
@@ -42,8 +43,10 @@ const StyledDialogOverlay = styled(({ mobile, ...rest }) => <AnimatedDialogOverl
|
|||||||
// destructure to not pass custom props to Dialog DOM element
|
// destructure to not pass custom props to Dialog DOM element
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...rest }) => (
|
const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...rest }) => (
|
||||||
<DialogContent aria-label="content" {...rest} />
|
<DialogContent {...rest} />
|
||||||
))`
|
)).attrs({
|
||||||
|
'aria-label': 'dialog'
|
||||||
|
})`
|
||||||
&[data-reach-dialog-content] {
|
&[data-reach-dialog-content] {
|
||||||
margin: 0 0 2rem 0;
|
margin: 0 0 2rem 0;
|
||||||
border: 1px solid ${({ theme }) => theme.bg1};
|
border: 1px solid ${({ theme }) => theme.bg1};
|
||||||
@@ -67,12 +70,10 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...r
|
|||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||||
width: 65vw;
|
width: 65vw;
|
||||||
max-height: 65vh;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
`}
|
`}
|
||||||
${({ theme, mobile }) => theme.mediaWidth.upToSmall`
|
${({ theme, mobile }) => theme.mediaWidth.upToSmall`
|
||||||
width: 85vw;
|
width: 85vw;
|
||||||
max-height: 66vh;
|
|
||||||
${mobile &&
|
${mobile &&
|
||||||
css`
|
css`
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
@@ -84,14 +85,6 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...r
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const HiddenCloseButton = styled.button`
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border: none;
|
|
||||||
`
|
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
onDismiss: () => void
|
onDismiss: () => void
|
||||||
@@ -116,21 +109,13 @@ export default function Modal({
|
|||||||
leave: { opacity: 0 }
|
leave: { opacity: 0 }
|
||||||
})
|
})
|
||||||
|
|
||||||
const [{ xy }, set] = useSpring(() => ({ xy: [0, 0] }))
|
const [{ y }, set] = useSpring(() => ({ y: 0, config: { mass: 1, tension: 210, friction: 20 } }))
|
||||||
const bind = useGesture({
|
const bind = useGesture({
|
||||||
onDrag: state => {
|
onDrag: state => {
|
||||||
let velocity = state.velocity
|
|
||||||
if (velocity < 1) {
|
|
||||||
velocity = 1
|
|
||||||
}
|
|
||||||
if (velocity > 8) {
|
|
||||||
velocity = 8
|
|
||||||
}
|
|
||||||
set({
|
set({
|
||||||
xy: state.down ? state.movement : [0, 0],
|
y: state.down ? state.movement[1] : 0
|
||||||
config: { mass: 1, tension: 210, friction: 20 }
|
|
||||||
})
|
})
|
||||||
if (velocity > 3 && state.direction[1] > 0) {
|
if (state.velocity > 3 && state.direction[1] > 0) {
|
||||||
onDismiss()
|
onDismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,6 +134,8 @@ export default function Modal({
|
|||||||
initialFocusRef={initialFocusRef}
|
initialFocusRef={initialFocusRef}
|
||||||
mobile={true}
|
mobile={true}
|
||||||
>
|
>
|
||||||
|
{/* prevents the automatic focusing of inputs on mobile by the reach dialog */}
|
||||||
|
{initialFocusRef ? null : <div tabIndex={1} />}
|
||||||
<Spring // animation for entrance and exit
|
<Spring // animation for entrance and exit
|
||||||
from={{
|
from={{
|
||||||
transform: isOpen ? 'translateY(200px)' : 'translateY(100px)'
|
transform: isOpen ? 'translateY(200px)' : 'translateY(100px)'
|
||||||
@@ -161,18 +148,17 @@ export default function Modal({
|
|||||||
<animated.div
|
<animated.div
|
||||||
{...bind()}
|
{...bind()}
|
||||||
style={{
|
style={{
|
||||||
transform: (xy as any).interpolate((x, y) => `translate3d(${0}px,${y > 0 ? y : 0}px,0)`)
|
transform: y.interpolate(y => `translateY(${y > 0 ? y : 0}px)`)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<StyledDialogContent
|
<StyledDialogContent
|
||||||
ariaLabel="test"
|
aria-label="dialog content"
|
||||||
style={props}
|
style={props}
|
||||||
hidden={true}
|
hidden={true}
|
||||||
minHeight={minHeight}
|
minHeight={minHeight}
|
||||||
maxHeight={maxHeight}
|
maxHeight={maxHeight}
|
||||||
mobile={isMobile}
|
mobile={isMobile}
|
||||||
>
|
>
|
||||||
<HiddenCloseButton onClick={onDismiss} />
|
|
||||||
{children}
|
{children}
|
||||||
</StyledDialogContent>
|
</StyledDialogContent>
|
||||||
</animated.div>
|
</animated.div>
|
||||||
@@ -189,15 +175,14 @@ export default function Modal({
|
|||||||
{transitions.map(
|
{transitions.map(
|
||||||
({ item, key, props }) =>
|
({ item, key, props }) =>
|
||||||
item && (
|
item && (
|
||||||
<StyledDialogOverlay
|
<StyledDialogOverlay key={key} style={props} onDismiss={onDismiss} initialFocusRef={initialFocusRef}>
|
||||||
key={key}
|
<StyledDialogContent
|
||||||
style={props}
|
aria-label="dialog content"
|
||||||
onDismiss={onDismiss}
|
hidden={true}
|
||||||
initialFocusRef={initialFocusRef}
|
minHeight={minHeight}
|
||||||
mobile={false}
|
maxHeight={maxHeight}
|
||||||
>
|
isOpen={isOpen}
|
||||||
<StyledDialogContent hidden={true} minHeight={minHeight} maxHeight={maxHeight} isOpen={isOpen}>
|
>
|
||||||
<HiddenCloseButton onClick={onDismiss} />
|
|
||||||
{children}
|
{children}
|
||||||
</StyledDialogContent>
|
</StyledDialogContent>
|
||||||
</StyledDialogOverlay>
|
</StyledDialogOverlay>
|
||||||
|
|||||||
@@ -1,37 +1,18 @@
|
|||||||
import React, { useCallback } from 'react'
|
import React from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { darken } from 'polished'
|
import { darken } from 'polished'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { withRouter, NavLink, Link as HistoryLink, RouteComponentProps } from 'react-router-dom'
|
import { NavLink, Link as HistoryLink } from 'react-router-dom'
|
||||||
import useBodyKeyDown from '../../hooks/useBodyKeyDown'
|
|
||||||
|
|
||||||
import { CursorPointer } from '../../theme'
|
|
||||||
import { ArrowLeft } from 'react-feather'
|
import { ArrowLeft } from 'react-feather'
|
||||||
import { RowBetween } from '../Row'
|
import { RowBetween } from '../Row'
|
||||||
import QuestionHelper from '../QuestionHelper'
|
import QuestionHelper from '../QuestionHelper'
|
||||||
|
|
||||||
const tabOrder = [
|
|
||||||
{
|
|
||||||
path: '/swap',
|
|
||||||
textKey: 'swap',
|
|
||||||
regex: /\/swap/
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/send',
|
|
||||||
textKey: 'send',
|
|
||||||
regex: /\/send/
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/pool',
|
|
||||||
textKey: 'pool',
|
|
||||||
regex: /\/pool/
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const Tabs = styled.div`
|
const Tabs = styled.div`
|
||||||
${({ theme }) => theme.flexRowNoWrap}
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 3rem;
|
border-radius: 3rem;
|
||||||
|
justify-content: space-evenly;
|
||||||
`
|
`
|
||||||
|
|
||||||
const activeClassName = 'ACTIVE'
|
const activeClassName = 'ACTIVE'
|
||||||
@@ -43,7 +24,6 @@ const StyledNavLink = styled(NavLink).attrs({
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
flex: 1 0 auto;
|
|
||||||
border-radius: 3rem;
|
border-radius: 3rem;
|
||||||
outline: none;
|
outline: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -68,89 +48,68 @@ const ActiveText = styled.div`
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const ArrowLink = styled(ArrowLeft)`
|
const StyledArrowLeft = styled(ArrowLeft)`
|
||||||
color: ${({ theme }) => theme.text1};
|
color: ${({ theme }) => theme.text1};
|
||||||
`
|
`
|
||||||
|
|
||||||
function NavigationTabs({ location: { pathname }, history }: RouteComponentProps<{}>) {
|
export function SwapPoolTabs({ active }: { active: 'swap' | 'pool' }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const navigate = useCallback(
|
|
||||||
direction => {
|
|
||||||
const tabIndex = tabOrder.findIndex(({ regex }) => pathname.match(regex))
|
|
||||||
history.push(tabOrder[(tabIndex + tabOrder.length + direction) % tabOrder.length].path)
|
|
||||||
},
|
|
||||||
[pathname, history]
|
|
||||||
)
|
|
||||||
const navigateRight = useCallback(() => {
|
|
||||||
navigate(1)
|
|
||||||
}, [navigate])
|
|
||||||
const navigateLeft = useCallback(() => {
|
|
||||||
navigate(-1)
|
|
||||||
}, [navigate])
|
|
||||||
|
|
||||||
useBodyKeyDown('ArrowRight', navigateRight)
|
|
||||||
useBodyKeyDown('ArrowLeft', navigateLeft)
|
|
||||||
|
|
||||||
const adding = pathname.match('/add')
|
|
||||||
const removing = pathname.match('/remove')
|
|
||||||
const finding = pathname.match('/find')
|
|
||||||
const creating = pathname.match('/create')
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Tabs style={{ marginBottom: '20px' }}>
|
||||||
{adding || removing ? (
|
<StyledNavLink id={`swap-nav-link`} to={'/swap'} isActive={() => active === 'swap'}>
|
||||||
<Tabs>
|
{t('swap')}
|
||||||
<RowBetween style={{ padding: '1rem' }}>
|
</StyledNavLink>
|
||||||
<CursorPointer onClick={() => history.push('/pool')}>
|
<StyledNavLink id={`pool-nav-link`} to={'/pool'} isActive={() => active === 'pool'}>
|
||||||
<ArrowLink />
|
{t('pool')}
|
||||||
</CursorPointer>
|
</StyledNavLink>
|
||||||
<ActiveText>{adding ? 'Add' : 'Remove'} Liquidity</ActiveText>
|
</Tabs>
|
||||||
<QuestionHelper
|
|
||||||
text={
|
|
||||||
adding
|
|
||||||
? 'When you add liquidity, you are given pool tokens representing your position. These tokens automatically earn fees proportional to your share of the pool, and can be redeemed at any time.'
|
|
||||||
: 'Removing pool tokens converts your position back into underlying tokens at the current rate, proportional to your share of the pool. Accrued fees are included in the amounts you receive.'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</RowBetween>
|
|
||||||
</Tabs>
|
|
||||||
) : finding ? (
|
|
||||||
<Tabs>
|
|
||||||
<RowBetween style={{ padding: '1rem' }}>
|
|
||||||
<HistoryLink to="/pool">
|
|
||||||
<ArrowLink />
|
|
||||||
</HistoryLink>
|
|
||||||
<ActiveText>Import Pool</ActiveText>
|
|
||||||
<QuestionHelper text={"Use this tool to find pairs that don't automatically appear in the interface."} />
|
|
||||||
</RowBetween>
|
|
||||||
</Tabs>
|
|
||||||
) : creating ? (
|
|
||||||
<Tabs>
|
|
||||||
<RowBetween style={{ padding: '1rem' }}>
|
|
||||||
<HistoryLink to="/pool">
|
|
||||||
<ArrowLink />
|
|
||||||
</HistoryLink>
|
|
||||||
<ActiveText>Create Pool</ActiveText>
|
|
||||||
<QuestionHelper text={'Use this interface to create a new pool.'} />
|
|
||||||
</RowBetween>
|
|
||||||
</Tabs>
|
|
||||||
) : (
|
|
||||||
<Tabs style={{ marginBottom: '20px' }}>
|
|
||||||
{tabOrder.map(({ path, textKey, regex }) => (
|
|
||||||
<StyledNavLink
|
|
||||||
id={`${textKey}-nav-link`}
|
|
||||||
key={path}
|
|
||||||
to={path}
|
|
||||||
isActive={(_, { pathname }) => !!pathname.match(regex)}
|
|
||||||
>
|
|
||||||
{t(textKey)}
|
|
||||||
</StyledNavLink>
|
|
||||||
))}
|
|
||||||
</Tabs>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(NavigationTabs)
|
export function CreatePoolTabs() {
|
||||||
|
return (
|
||||||
|
<Tabs>
|
||||||
|
<RowBetween style={{ padding: '1rem' }}>
|
||||||
|
<HistoryLink to="/pool">
|
||||||
|
<StyledArrowLeft />
|
||||||
|
</HistoryLink>
|
||||||
|
<ActiveText>Create Pool</ActiveText>
|
||||||
|
<QuestionHelper text={'Use this interface to create a new pool.'} />
|
||||||
|
</RowBetween>
|
||||||
|
</Tabs>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FindPoolTabs() {
|
||||||
|
return (
|
||||||
|
<Tabs>
|
||||||
|
<RowBetween style={{ padding: '1rem' }}>
|
||||||
|
<HistoryLink to="/pool">
|
||||||
|
<StyledArrowLeft />
|
||||||
|
</HistoryLink>
|
||||||
|
<ActiveText>Import Pool</ActiveText>
|
||||||
|
<QuestionHelper text={"Use this tool to find pairs that don't automatically appear in the interface."} />
|
||||||
|
</RowBetween>
|
||||||
|
</Tabs>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AddRemoveTabs({ adding }: { adding: boolean }) {
|
||||||
|
return (
|
||||||
|
<Tabs>
|
||||||
|
<RowBetween style={{ padding: '1rem' }}>
|
||||||
|
<HistoryLink to="/pool">
|
||||||
|
<StyledArrowLeft />
|
||||||
|
</HistoryLink>
|
||||||
|
<ActiveText>{adding ? 'Add' : 'Remove'} Liquidity</ActiveText>
|
||||||
|
<QuestionHelper
|
||||||
|
text={
|
||||||
|
adding
|
||||||
|
? 'When you add liquidity, you are given pool tokens representing your position. These tokens automatically earn fees proportional to your share of the pool, and can be redeemed at any time.'
|
||||||
|
: 'Removing pool tokens converts your position back into underlying tokens at the current rate, proportional to your share of the pool. Accrued fees are included in the amounts you receive.'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</RowBetween>
|
||||||
|
</Tabs>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,36 +2,16 @@ import { Placement } from '@popperjs/core'
|
|||||||
import { transparentize } from 'polished'
|
import { transparentize } from 'polished'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { usePopper } from 'react-popper'
|
import { usePopper } from 'react-popper'
|
||||||
import styled, { keyframes } from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import useInterval from '../../hooks/useInterval'
|
import useInterval from '../../hooks/useInterval'
|
||||||
import Portal from '@reach/portal'
|
import Portal from '@reach/portal'
|
||||||
|
|
||||||
const fadeIn = keyframes`
|
|
||||||
from {
|
|
||||||
opacity : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity : 1;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const fadeOut = keyframes`
|
|
||||||
from {
|
|
||||||
opacity : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity : 0;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const PopoverContainer = styled.div<{ show: boolean }>`
|
const PopoverContainer = styled.div<{ show: boolean }>`
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
|
|
||||||
visibility: ${props => (!props.show ? 'hidden' : 'visible')};
|
visibility: ${props => (props.show ? 'visible' : 'hidden')};
|
||||||
animation: ${props => (!props.show ? fadeOut : fadeIn)} 150ms linear;
|
opacity: ${props => (props.show ? 1 : 0)};
|
||||||
transition: visibility 150ms linear;
|
transition: visibility 150ms linear, opacity 150ms linear;
|
||||||
|
|
||||||
background: ${({ theme }) => theme.bg2};
|
background: ${({ theme }) => theme.bg2};
|
||||||
border: 1px solid ${({ theme }) => theme.bg3};
|
border: 1px solid ${({ theme }) => theme.bg3};
|
||||||
|
|||||||
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 React, { useState } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { darken } from 'polished'
|
import { darken } from 'polished'
|
||||||
import { RouteComponentProps, withRouter } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { Percent, Pair, JSBI } from '@uniswap/sdk'
|
import { Percent, Pair, JSBI } from '@uniswap/sdk'
|
||||||
|
|
||||||
import { useActiveWeb3React } from '../../hooks'
|
import { useActiveWeb3React } from '../../hooks'
|
||||||
import { useTotalSupply } from '../../data/TotalSupply'
|
import { useTotalSupply } from '../../data/TotalSupply'
|
||||||
|
import { currencyId } from '../../pages/AddLiquidity/currencyId'
|
||||||
import { useTokenBalance } from '../../state/wallet/hooks'
|
import { useTokenBalance } from '../../state/wallet/hooks'
|
||||||
|
|
||||||
import Card, { GreyCard } from '../Card'
|
import Card, { GreyCard } from '../Card'
|
||||||
import TokenLogo from '../TokenLogo'
|
import TokenLogo from '../TokenLogo'
|
||||||
import DoubleLogo from '../DoubleLogo'
|
import DoubleLogo from '../DoubleLogo'
|
||||||
import { Text } from 'rebass'
|
import { Text } from 'rebass'
|
||||||
import { ExternalLink } from '../../theme/components'
|
import { ExternalLink } from '../../theme'
|
||||||
import { AutoColumn } from '../Column'
|
import { AutoColumn } from '../Column'
|
||||||
import { ChevronDown, ChevronUp } from 'react-feather'
|
import { ChevronDown, ChevronUp } from 'react-feather'
|
||||||
import { ButtonSecondary } from '../Button'
|
import { ButtonSecondary } from '../Button'
|
||||||
import { RowBetween, RowFixed, AutoRow } from '../Row'
|
import { RowBetween, RowFixed, AutoRow } from '../Row'
|
||||||
|
import { Dots } from '../swap/styleds'
|
||||||
|
|
||||||
const FixedHeightRow = styled(RowBetween)`
|
export const FixedHeightRow = styled(RowBetween)`
|
||||||
height: 24px;
|
height: 24px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const HoverCard = styled(Card)`
|
export const HoverCard = styled(Card)`
|
||||||
border: 1px solid ${({ theme }) => theme.bg2};
|
border: 1px solid ${({ theme }) => theme.bg2};
|
||||||
:hover {
|
:hover {
|
||||||
border: 1px solid ${({ theme }) => darken(0.06, theme.bg2)};
|
border: 1px solid ${({ theme }) => darken(0.06, theme.bg2)};
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
interface PositionCardProps extends RouteComponentProps<{}> {
|
interface PositionCardProps {
|
||||||
pair: Pair
|
pair: Pair | undefined | null
|
||||||
minimal?: boolean
|
|
||||||
border?: string
|
border?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function PositionCard({ pair, history, border, minimal = false }: PositionCardProps) {
|
export function MinimalPositionCard({ pair, border }: PositionCardProps) {
|
||||||
|
const { account } = useActiveWeb3React()
|
||||||
|
|
||||||
|
const token0 = pair?.token0
|
||||||
|
const token1 = pair?.token1
|
||||||
|
|
||||||
|
const [showMore, setShowMore] = useState(false)
|
||||||
|
|
||||||
|
const userPoolBalance = useTokenBalance(account, pair?.liquidityToken)
|
||||||
|
const totalPoolTokens = useTotalSupply(pair?.liquidityToken)
|
||||||
|
|
||||||
|
const [token0Deposited, token1Deposited] =
|
||||||
|
!!pair &&
|
||||||
|
!!totalPoolTokens &&
|
||||||
|
!!userPoolBalance &&
|
||||||
|
// this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
|
||||||
|
JSBI.greaterThanOrEqual(totalPoolTokens.raw, userPoolBalance.raw)
|
||||||
|
? [
|
||||||
|
pair.getLiquidityValue(token0, totalPoolTokens, userPoolBalance, false),
|
||||||
|
pair.getLiquidityValue(token1, totalPoolTokens, userPoolBalance, false)
|
||||||
|
]
|
||||||
|
: [undefined, undefined]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{userPoolBalance && (
|
||||||
|
<GreyCard border={border}>
|
||||||
|
<AutoColumn gap="12px">
|
||||||
|
<FixedHeightRow>
|
||||||
|
<RowFixed>
|
||||||
|
<Text fontWeight={500} fontSize={16}>
|
||||||
|
Your position
|
||||||
|
</Text>
|
||||||
|
</RowFixed>
|
||||||
|
</FixedHeightRow>
|
||||||
|
<FixedHeightRow onClick={() => setShowMore(!showMore)}>
|
||||||
|
<RowFixed>
|
||||||
|
<DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={20} />
|
||||||
|
<Text fontWeight={500} fontSize={20}>
|
||||||
|
{token0?.symbol}/{token1?.symbol}
|
||||||
|
</Text>
|
||||||
|
</RowFixed>
|
||||||
|
<RowFixed>
|
||||||
|
<Text fontWeight={500} fontSize={20}>
|
||||||
|
{userPoolBalance ? userPoolBalance.toSignificant(4) : '-'}
|
||||||
|
</Text>
|
||||||
|
</RowFixed>
|
||||||
|
</FixedHeightRow>
|
||||||
|
<AutoColumn gap="4px">
|
||||||
|
<FixedHeightRow>
|
||||||
|
<Text color="#888D9B" fontSize={16} fontWeight={500}>
|
||||||
|
{token0?.symbol}:
|
||||||
|
</Text>
|
||||||
|
{token0Deposited ? (
|
||||||
|
<RowFixed>
|
||||||
|
<Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||||
|
{token0Deposited?.toSignificant(6)}
|
||||||
|
</Text>
|
||||||
|
</RowFixed>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
)}
|
||||||
|
</FixedHeightRow>
|
||||||
|
<FixedHeightRow>
|
||||||
|
<Text color="#888D9B" fontSize={16} fontWeight={500}>
|
||||||
|
{token1?.symbol}:
|
||||||
|
</Text>
|
||||||
|
{token1Deposited ? (
|
||||||
|
<RowFixed>
|
||||||
|
<Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||||
|
{token1Deposited?.toSignificant(6)}
|
||||||
|
</Text>
|
||||||
|
</RowFixed>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
)}
|
||||||
|
</FixedHeightRow>
|
||||||
|
</AutoColumn>
|
||||||
|
</AutoColumn>
|
||||||
|
</GreyCard>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FullPositionCard({ pair, border }: PositionCardProps) {
|
||||||
const { account } = useActiveWeb3React()
|
const { account } = useActiveWeb3React()
|
||||||
|
|
||||||
const token0 = pair?.token0
|
const token0 = pair?.token0
|
||||||
@@ -63,174 +149,94 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
|
|||||||
]
|
]
|
||||||
: [undefined, undefined]
|
: [undefined, undefined]
|
||||||
|
|
||||||
if (minimal) {
|
return (
|
||||||
return (
|
<HoverCard border={border}>
|
||||||
<>
|
<AutoColumn gap="12px">
|
||||||
{userPoolBalance && (
|
<FixedHeightRow onClick={() => setShowMore(!showMore)} style={{ cursor: 'pointer' }}>
|
||||||
<GreyCard border={border}>
|
<RowFixed>
|
||||||
<AutoColumn gap="12px">
|
<DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={20} />
|
||||||
<FixedHeightRow>
|
<Text fontWeight={500} fontSize={20}>
|
||||||
|
{!token0 || !token1 ? <Dots>Loading</Dots> : `${token0.symbol}/${token1.symbol}`}
|
||||||
|
</Text>
|
||||||
|
</RowFixed>
|
||||||
|
<RowFixed>
|
||||||
|
{showMore ? (
|
||||||
|
<ChevronUp size="20" style={{ marginLeft: '10px' }} />
|
||||||
|
) : (
|
||||||
|
<ChevronDown size="20" style={{ marginLeft: '10px' }} />
|
||||||
|
)}
|
||||||
|
</RowFixed>
|
||||||
|
</FixedHeightRow>
|
||||||
|
{showMore && (
|
||||||
|
<AutoColumn gap="8px">
|
||||||
|
<FixedHeightRow>
|
||||||
|
<RowFixed>
|
||||||
|
<Text fontSize={16} fontWeight={500}>
|
||||||
|
Pooled {token0?.symbol}:
|
||||||
|
</Text>
|
||||||
|
</RowFixed>
|
||||||
|
{token0Deposited ? (
|
||||||
<RowFixed>
|
<RowFixed>
|
||||||
<Text fontWeight={500} fontSize={16}>
|
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||||
Your current position
|
{token0Deposited?.toSignificant(6)}
|
||||||
</Text>
|
</Text>
|
||||||
|
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token0?.address} />
|
||||||
</RowFixed>
|
</RowFixed>
|
||||||
</FixedHeightRow>
|
|
||||||
<FixedHeightRow onClick={() => setShowMore(!showMore)}>
|
|
||||||
<RowFixed>
|
|
||||||
<DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={20} />
|
|
||||||
<Text fontWeight={500} fontSize={20}>
|
|
||||||
{token0?.symbol}/{token1?.symbol}
|
|
||||||
</Text>
|
|
||||||
</RowFixed>
|
|
||||||
<RowFixed>
|
|
||||||
<Text fontWeight={500} fontSize={20}>
|
|
||||||
{userPoolBalance ? userPoolBalance.toSignificant(4) : '-'}
|
|
||||||
</Text>
|
|
||||||
</RowFixed>
|
|
||||||
</FixedHeightRow>
|
|
||||||
<AutoColumn gap="4px">
|
|
||||||
<FixedHeightRow>
|
|
||||||
<Text color="#888D9B" fontSize={16} fontWeight={500}>
|
|
||||||
{token0?.symbol}:
|
|
||||||
</Text>
|
|
||||||
{token0Deposited ? (
|
|
||||||
<RowFixed>
|
|
||||||
{!minimal && <TokenLogo address={token0?.address} />}
|
|
||||||
<Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
|
||||||
{token0Deposited?.toSignificant(6)}
|
|
||||||
</Text>
|
|
||||||
</RowFixed>
|
|
||||||
) : (
|
|
||||||
'-'
|
|
||||||
)}
|
|
||||||
</FixedHeightRow>
|
|
||||||
<FixedHeightRow>
|
|
||||||
<Text color="#888D9B" fontSize={16} fontWeight={500}>
|
|
||||||
{token1?.symbol}:
|
|
||||||
</Text>
|
|
||||||
{token1Deposited ? (
|
|
||||||
<RowFixed>
|
|
||||||
{!minimal && <TokenLogo address={token1?.address} />}
|
|
||||||
<Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
|
||||||
{token1Deposited?.toSignificant(6)}
|
|
||||||
</Text>
|
|
||||||
</RowFixed>
|
|
||||||
) : (
|
|
||||||
'-'
|
|
||||||
)}
|
|
||||||
</FixedHeightRow>
|
|
||||||
</AutoColumn>
|
|
||||||
</AutoColumn>
|
|
||||||
</GreyCard>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
} else
|
|
||||||
return (
|
|
||||||
<HoverCard border={border}>
|
|
||||||
<AutoColumn gap="12px">
|
|
||||||
<FixedHeightRow onClick={() => setShowMore(!showMore)} style={{ cursor: 'pointer' }}>
|
|
||||||
<RowFixed>
|
|
||||||
<DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={20} />
|
|
||||||
<Text fontWeight={500} fontSize={20}>
|
|
||||||
{token0?.symbol}/{token1?.symbol}
|
|
||||||
</Text>
|
|
||||||
</RowFixed>
|
|
||||||
<RowFixed>
|
|
||||||
{showMore ? (
|
|
||||||
<ChevronUp size="20" style={{ marginLeft: '10px' }} />
|
|
||||||
) : (
|
) : (
|
||||||
<ChevronDown size="20" style={{ marginLeft: '10px' }} />
|
'-'
|
||||||
)}
|
)}
|
||||||
</RowFixed>
|
</FixedHeightRow>
|
||||||
</FixedHeightRow>
|
|
||||||
{showMore && (
|
|
||||||
<AutoColumn gap="8px">
|
|
||||||
<FixedHeightRow>
|
|
||||||
<RowFixed>
|
|
||||||
<Text fontSize={16} fontWeight={500}>
|
|
||||||
Pooled {token0?.symbol}:
|
|
||||||
</Text>
|
|
||||||
</RowFixed>
|
|
||||||
{token0Deposited ? (
|
|
||||||
<RowFixed>
|
|
||||||
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
|
||||||
{token0Deposited?.toSignificant(6)}
|
|
||||||
</Text>
|
|
||||||
{!minimal && <TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token0?.address} />}
|
|
||||||
</RowFixed>
|
|
||||||
) : (
|
|
||||||
'-'
|
|
||||||
)}
|
|
||||||
</FixedHeightRow>
|
|
||||||
|
|
||||||
<FixedHeightRow>
|
<FixedHeightRow>
|
||||||
|
<RowFixed>
|
||||||
|
<Text fontSize={16} fontWeight={500}>
|
||||||
|
Pooled {token1?.symbol}:
|
||||||
|
</Text>
|
||||||
|
</RowFixed>
|
||||||
|
{token1Deposited ? (
|
||||||
<RowFixed>
|
<RowFixed>
|
||||||
<Text fontSize={16} fontWeight={500}>
|
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||||
Pooled {token1?.symbol}:
|
{token1Deposited?.toSignificant(6)}
|
||||||
</Text>
|
</Text>
|
||||||
|
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token1?.address} />
|
||||||
</RowFixed>
|
</RowFixed>
|
||||||
{token1Deposited ? (
|
) : (
|
||||||
<RowFixed>
|
'-'
|
||||||
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
|
||||||
{token1Deposited?.toSignificant(6)}
|
|
||||||
</Text>
|
|
||||||
{!minimal && <TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token1?.address} />}
|
|
||||||
</RowFixed>
|
|
||||||
) : (
|
|
||||||
'-'
|
|
||||||
)}
|
|
||||||
</FixedHeightRow>
|
|
||||||
{!minimal && (
|
|
||||||
<FixedHeightRow>
|
|
||||||
<Text fontSize={16} fontWeight={500}>
|
|
||||||
Your pool tokens:
|
|
||||||
</Text>
|
|
||||||
<Text fontSize={16} fontWeight={500}>
|
|
||||||
{userPoolBalance ? userPoolBalance.toSignificant(4) : '-'}
|
|
||||||
</Text>
|
|
||||||
</FixedHeightRow>
|
|
||||||
)}
|
|
||||||
{!minimal && (
|
|
||||||
<FixedHeightRow>
|
|
||||||
<Text fontSize={16} fontWeight={500}>
|
|
||||||
Your pool share
|
|
||||||
</Text>
|
|
||||||
<Text fontSize={16} fontWeight={500}>
|
|
||||||
{poolTokenPercentage ? poolTokenPercentage.toFixed(2) + '%' : '-'}
|
|
||||||
</Text>
|
|
||||||
</FixedHeightRow>
|
|
||||||
)}
|
)}
|
||||||
|
</FixedHeightRow>
|
||||||
|
<FixedHeightRow>
|
||||||
|
<Text fontSize={16} fontWeight={500}>
|
||||||
|
Your pool tokens:
|
||||||
|
</Text>
|
||||||
|
<Text fontSize={16} fontWeight={500}>
|
||||||
|
{userPoolBalance ? userPoolBalance.toSignificant(4) : '-'}
|
||||||
|
</Text>
|
||||||
|
</FixedHeightRow>
|
||||||
|
<FixedHeightRow>
|
||||||
|
<Text fontSize={16} fontWeight={500}>
|
||||||
|
Your pool share:
|
||||||
|
</Text>
|
||||||
|
<Text fontSize={16} fontWeight={500}>
|
||||||
|
{poolTokenPercentage ? poolTokenPercentage.toFixed(2) + '%' : '-'}
|
||||||
|
</Text>
|
||||||
|
</FixedHeightRow>
|
||||||
|
|
||||||
<AutoRow justify="center" marginTop={'10px'}>
|
<AutoRow justify="center" marginTop={'10px'}>
|
||||||
<ExternalLink href={`https://uniswap.info/pair/${pair?.liquidityToken.address}`}>
|
<ExternalLink href={`https://uniswap.info/pair/${pair?.liquidityToken.address}`}>
|
||||||
View pool information ↗
|
View pool information ↗
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
</AutoRow>
|
</AutoRow>
|
||||||
<RowBetween marginTop="10px">
|
<RowBetween marginTop="10px">
|
||||||
<ButtonSecondary
|
<ButtonSecondary as={Link} to={`/add/${currencyId(token0)}/${currencyId(token1)}`} width="48%">
|
||||||
width="48%"
|
Add
|
||||||
onClick={() => {
|
</ButtonSecondary>
|
||||||
history.push('/add/' + token0?.address + '-' + token1?.address)
|
<ButtonSecondary as={Link} width="48%" to={`/remove/${token0?.address}-${token1?.address}`}>
|
||||||
}}
|
Remove
|
||||||
>
|
</ButtonSecondary>
|
||||||
Add
|
</RowBetween>
|
||||||
</ButtonSecondary>
|
</AutoColumn>
|
||||||
<ButtonSecondary
|
)}
|
||||||
width="48%"
|
</AutoColumn>
|
||||||
onClick={() => {
|
</HoverCard>
|
||||||
history.push('/remove/' + token0?.address + '-' + token1?.address)
|
)
|
||||||
}}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</ButtonSecondary>
|
|
||||||
</RowBetween>
|
|
||||||
</AutoColumn>
|
|
||||||
)}
|
|
||||||
</AutoColumn>
|
|
||||||
</HoverCard>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(PositionCard)
|
|
||||||
|
|||||||
@@ -1,39 +1,56 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Text } from 'rebass'
|
import { Text } from 'rebass'
|
||||||
import { COMMON_BASES } from '../../constants'
|
import { ChainId, Token } from '@uniswap/sdk'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import { SUGGESTED_BASES } from '../../constants'
|
||||||
import { AutoColumn } from '../Column'
|
import { AutoColumn } from '../Column'
|
||||||
import QuestionHelper from '../QuestionHelper'
|
import QuestionHelper from '../QuestionHelper'
|
||||||
import { AutoRow } from '../Row'
|
import { AutoRow } from '../Row'
|
||||||
import TokenLogo from '../TokenLogo'
|
import TokenLogo from '../TokenLogo'
|
||||||
import { BaseWrapper } from './styleds'
|
|
||||||
|
const BaseWrapper = styled.div<{ disable?: boolean }>`
|
||||||
|
border: 1px solid ${({ theme, disable }) => (disable ? 'transparent' : theme.bg3)};
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
padding: 6px;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
:hover {
|
||||||
|
cursor: ${({ disable }) => !disable && 'pointer'};
|
||||||
|
background-color: ${({ theme, disable }) => !disable && theme.bg2};
|
||||||
|
}
|
||||||
|
|
||||||
|
background-color: ${({ theme, disable }) => disable && theme.bg3};
|
||||||
|
opacity: ${({ disable }) => disable && '0.4'};
|
||||||
|
`
|
||||||
|
|
||||||
export default function CommonBases({
|
export default function CommonBases({
|
||||||
chainId,
|
chainId,
|
||||||
onSelect,
|
onSelect,
|
||||||
selectedTokenAddress
|
selectedTokenAddress
|
||||||
}: {
|
}: {
|
||||||
chainId: number
|
chainId: ChainId
|
||||||
selectedTokenAddress: string
|
selectedTokenAddress: string
|
||||||
onSelect: (tokenAddress: string) => void
|
onSelect: (tokenAddress: string) => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<AutoColumn gap="md">
|
<AutoColumn gap="md">
|
||||||
<AutoRow>
|
<AutoRow>
|
||||||
<Text fontWeight={500} fontSize={16}>
|
<Text fontWeight={500} fontSize={14}>
|
||||||
Common Bases
|
Common bases
|
||||||
</Text>
|
</Text>
|
||||||
<QuestionHelper text="These tokens are commonly used in pairs." />
|
<QuestionHelper text="These tokens are commonly paired with other tokens." />
|
||||||
</AutoRow>
|
</AutoRow>
|
||||||
<AutoRow gap="10px">
|
<AutoRow gap="4px">
|
||||||
{COMMON_BASES[chainId]?.map(token => {
|
{(SUGGESTED_BASES[chainId as ChainId] ?? []).map((token: Token) => {
|
||||||
return (
|
return (
|
||||||
<BaseWrapper
|
<BaseWrapper
|
||||||
gap="6px"
|
|
||||||
onClick={() => selectedTokenAddress !== token.address && onSelect(token.address)}
|
onClick={() => selectedTokenAddress !== token.address && onSelect(token.address)}
|
||||||
disable={selectedTokenAddress === token.address}
|
disable={selectedTokenAddress === token.address}
|
||||||
key={token.address}
|
key={token.address}
|
||||||
>
|
>
|
||||||
<TokenLogo address={token.address} />
|
<TokenLogo address={token.address} style={{ marginRight: 8 }} />
|
||||||
<Text fontWeight={500} fontSize={16}>
|
<Text fontWeight={500} fontSize={16}>
|
||||||
{token.symbol}
|
{token.symbol}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -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 { JSBI, Token, TokenAmount } from '@uniswap/sdk'
|
||||||
import React, { useContext } from 'react'
|
import React, { CSSProperties, memo, useContext, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { FixedSizeList } from 'react-window'
|
import { FixedSizeList } from 'react-window'
|
||||||
import { Text } from 'rebass'
|
import { Text } from 'rebass'
|
||||||
import { ThemeContext } from 'styled-components'
|
import { ThemeContext } from 'styled-components'
|
||||||
import Circle from '../../assets/images/circle.svg'
|
|
||||||
import { ALL_TOKENS } from '../../constants/tokens'
|
|
||||||
import { useActiveWeb3React } from '../../hooks'
|
import { useActiveWeb3React } from '../../hooks'
|
||||||
|
import { useAllTokens } from '../../hooks/Tokens'
|
||||||
|
import { useAddUserToken, useRemoveUserAddedToken } from '../../state/user/hooks'
|
||||||
import { LinkStyledButton, TYPE } from '../../theme'
|
import { LinkStyledButton, TYPE } from '../../theme'
|
||||||
import { isAddress } from '../../utils'
|
|
||||||
import { ButtonSecondary } from '../Button'
|
import { ButtonSecondary } from '../Button'
|
||||||
import Column, { AutoColumn } from '../Column'
|
import Column, { AutoColumn } from '../Column'
|
||||||
import { RowFixed } from '../Row'
|
import { RowFixed } from '../Row'
|
||||||
import TokenLogo from '../TokenLogo'
|
import TokenLogo from '../TokenLogo'
|
||||||
import { FadedSpan, GreySpan, MenuItem, SpinnerWrapper, ModalInfo } from './styleds'
|
import { FadedSpan, GreySpan, MenuItem, ModalInfo } from './styleds'
|
||||||
|
import Loader from '../Loader'
|
||||||
function isDefaultToken(tokenAddress: string, chainId?: number): boolean {
|
import { isDefaultToken, isCustomAddedToken } from '../../utils'
|
||||||
const address = isAddress(tokenAddress)
|
|
||||||
return Boolean(chainId && address && ALL_TOKENS[chainId as ChainId]?.[tokenAddress])
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TokenList({
|
export default function TokenList({
|
||||||
tokens,
|
tokens,
|
||||||
@@ -27,14 +23,12 @@ export default function TokenList({
|
|||||||
onTokenSelect,
|
onTokenSelect,
|
||||||
otherToken,
|
otherToken,
|
||||||
showSendWithSwap,
|
showSendWithSwap,
|
||||||
onRemoveAddedToken,
|
|
||||||
otherSelectedText
|
otherSelectedText
|
||||||
}: {
|
}: {
|
||||||
tokens: Token[]
|
tokens: Token[]
|
||||||
selectedToken: string
|
selectedToken: string
|
||||||
allTokenBalances: { [tokenAddress: string]: TokenAmount }
|
allTokenBalances: { [tokenAddress: string]: TokenAmount }
|
||||||
onTokenSelect: (tokenAddress: string) => void
|
onTokenSelect: (tokenAddress: string) => void
|
||||||
onRemoveAddedToken: (chainId: number, tokenAddress: string) => void
|
|
||||||
otherToken: string
|
otherToken: string
|
||||||
showSendWithSwap?: boolean
|
showSendWithSwap?: boolean
|
||||||
otherSelectedText: string
|
otherSelectedText: string
|
||||||
@@ -42,81 +36,121 @@ export default function TokenList({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { account, chainId } = useActiveWeb3React()
|
const { account, chainId } = useActiveWeb3React()
|
||||||
const theme = useContext(ThemeContext)
|
const theme = useContext(ThemeContext)
|
||||||
|
const allTokens = useAllTokens()
|
||||||
|
const addToken = useAddUserToken()
|
||||||
|
const removeToken = useRemoveUserAddedToken()
|
||||||
|
|
||||||
|
const TokenRow = useMemo(() => {
|
||||||
|
return memo(function TokenRow({ index, style }: { index: number; style: CSSProperties }) {
|
||||||
|
const token = tokens[index]
|
||||||
|
const { address, symbol } = token
|
||||||
|
|
||||||
|
const isDefault = isDefaultToken(token)
|
||||||
|
const customAdded = isCustomAddedToken(allTokens, token)
|
||||||
|
const balance = allTokenBalances[address]
|
||||||
|
|
||||||
|
const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
style={style}
|
||||||
|
key={address}
|
||||||
|
className={`token-item-${address}`}
|
||||||
|
onClick={() => (selectedToken && selectedToken === address ? null : onTokenSelect(address))}
|
||||||
|
disabled={selectedToken && selectedToken === address}
|
||||||
|
selected={otherToken === address}
|
||||||
|
>
|
||||||
|
<RowFixed>
|
||||||
|
<TokenLogo address={address} size={'24px'} style={{ marginRight: '14px' }} />
|
||||||
|
<Column>
|
||||||
|
<Text fontWeight={500}>
|
||||||
|
{symbol}
|
||||||
|
{otherToken === address && <GreySpan> ({otherSelectedText})</GreySpan>}
|
||||||
|
</Text>
|
||||||
|
<FadedSpan>
|
||||||
|
{customAdded ? (
|
||||||
|
<TYPE.main fontWeight={500}>
|
||||||
|
Added by user
|
||||||
|
<LinkStyledButton
|
||||||
|
onClick={event => {
|
||||||
|
event.stopPropagation()
|
||||||
|
removeToken(chainId, address)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
(Remove)
|
||||||
|
</LinkStyledButton>
|
||||||
|
</TYPE.main>
|
||||||
|
) : null}
|
||||||
|
{!isDefault && !customAdded ? (
|
||||||
|
<TYPE.main fontWeight={500}>
|
||||||
|
Found by address
|
||||||
|
<LinkStyledButton
|
||||||
|
onClick={event => {
|
||||||
|
event.stopPropagation()
|
||||||
|
addToken(token)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
(Add)
|
||||||
|
</LinkStyledButton>
|
||||||
|
</TYPE.main>
|
||||||
|
) : null}
|
||||||
|
</FadedSpan>
|
||||||
|
</Column>
|
||||||
|
</RowFixed>
|
||||||
|
<AutoColumn>
|
||||||
|
{balance ? (
|
||||||
|
<Text>
|
||||||
|
{zeroBalance && showSendWithSwap ? (
|
||||||
|
<ButtonSecondary padding={'4px 8px'}>
|
||||||
|
<Text textAlign="center" fontWeight={500} fontSize={14} color={theme.primary1}>
|
||||||
|
Send With Swap
|
||||||
|
</Text>
|
||||||
|
</ButtonSecondary>
|
||||||
|
) : balance ? (
|
||||||
|
balance.toSignificant(6)
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
) : account ? (
|
||||||
|
<Loader />
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
)}
|
||||||
|
</AutoColumn>
|
||||||
|
</MenuItem>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}, [
|
||||||
|
account,
|
||||||
|
addToken,
|
||||||
|
allTokenBalances,
|
||||||
|
allTokens,
|
||||||
|
chainId,
|
||||||
|
onTokenSelect,
|
||||||
|
otherSelectedText,
|
||||||
|
otherToken,
|
||||||
|
removeToken,
|
||||||
|
selectedToken,
|
||||||
|
showSendWithSwap,
|
||||||
|
theme.primary1,
|
||||||
|
tokens
|
||||||
|
])
|
||||||
|
|
||||||
if (tokens.length === 0) {
|
if (tokens.length === 0) {
|
||||||
return <ModalInfo>{t('noToken')}</ModalInfo>
|
return <ModalInfo>{t('noToken')}</ModalInfo>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FixedSizeList
|
<FixedSizeList
|
||||||
width="100%"
|
width="100%"
|
||||||
height={500}
|
height={500}
|
||||||
itemCount={tokens.length}
|
itemCount={tokens.length}
|
||||||
itemSize={50}
|
itemSize={56}
|
||||||
style={{ flex: '1', minHeight: 200 }}
|
style={{ flex: '1' }}
|
||||||
|
itemKey={index => tokens[index].address}
|
||||||
>
|
>
|
||||||
{({ index, style }) => {
|
{TokenRow}
|
||||||
const { address, symbol } = tokens[index]
|
|
||||||
|
|
||||||
const customAdded = !isDefaultToken(address, chainId)
|
|
||||||
const balance = allTokenBalances[address]
|
|
||||||
|
|
||||||
const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MenuItem
|
|
||||||
style={style}
|
|
||||||
key={address}
|
|
||||||
className={`token-item-${address}`}
|
|
||||||
onClick={() => (selectedToken && selectedToken === address ? null : onTokenSelect(address))}
|
|
||||||
disabled={selectedToken && selectedToken === address}
|
|
||||||
selected={otherToken === address}
|
|
||||||
>
|
|
||||||
<RowFixed>
|
|
||||||
<TokenLogo address={address} size={'24px'} style={{ marginRight: '14px' }} />
|
|
||||||
<Column>
|
|
||||||
<Text fontWeight={500}>
|
|
||||||
{symbol}
|
|
||||||
{otherToken === address && <GreySpan> ({otherSelectedText})</GreySpan>}
|
|
||||||
</Text>
|
|
||||||
<FadedSpan>
|
|
||||||
<TYPE.main fontWeight={500}>{customAdded && 'Added by user'}</TYPE.main>
|
|
||||||
{customAdded && (
|
|
||||||
<div
|
|
||||||
onClick={event => {
|
|
||||||
event.stopPropagation()
|
|
||||||
onRemoveAddedToken(chainId, address)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LinkStyledButton style={{ marginLeft: '4px', fontWeight: 400 }}>(Remove)</LinkStyledButton>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</FadedSpan>
|
|
||||||
</Column>
|
|
||||||
</RowFixed>
|
|
||||||
<AutoColumn gap="4px" justify="end">
|
|
||||||
{balance ? (
|
|
||||||
<Text>
|
|
||||||
{zeroBalance && showSendWithSwap ? (
|
|
||||||
<ButtonSecondary padding={'4px 8px'}>
|
|
||||||
<Text textAlign="center" fontWeight={500} fontSize={14} color={theme.primary1}>
|
|
||||||
Send With Swap
|
|
||||||
</Text>
|
|
||||||
</ButtonSecondary>
|
|
||||||
) : balance ? (
|
|
||||||
balance.toSignificant(6)
|
|
||||||
) : (
|
|
||||||
'-'
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
) : account ? (
|
|
||||||
<SpinnerWrapper src={Circle} alt="loader" />
|
|
||||||
) : (
|
|
||||||
'-'
|
|
||||||
)}
|
|
||||||
</AutoColumn>
|
|
||||||
</MenuItem>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</FixedSizeList>
|
</FixedSizeList>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -10,12 +10,22 @@ export function filterTokens(tokens: Token[], search: string): Token[] {
|
|||||||
return tokens.filter(token => token.address === searchingAddress)
|
return tokens.filter(token => token.address === searchingAddress)
|
||||||
}
|
}
|
||||||
|
|
||||||
const lowerSearchParts = searchingAddress ? [] : search.toLowerCase().split(/\s+/)
|
const lowerSearchParts = search
|
||||||
|
.toLowerCase()
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter(s => s.length > 0)
|
||||||
|
|
||||||
|
if (lowerSearchParts.length === 0) {
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
|
||||||
const matchesSearch = (s: string): boolean => {
|
const matchesSearch = (s: string): boolean => {
|
||||||
const sParts = s.toLowerCase().split(/\s+/)
|
const sParts = s
|
||||||
|
.toLowerCase()
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter(s => s.length > 0)
|
||||||
|
|
||||||
return lowerSearchParts.every(p => p.length === 0 || sParts.some(sp => sp.startsWith(p)))
|
return lowerSearchParts.every(p => p.length === 0 || sParts.some(sp => sp.startsWith(p) || sp.endsWith(p)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return tokens.filter(token => {
|
return tokens.filter(token => {
|
||||||
|
|||||||
@@ -1,226 +0,0 @@
|
|||||||
import { Pair, Token } from '@uniswap/sdk'
|
|
||||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
|
||||||
import { isMobile } from 'react-device-detect'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { RouteComponentProps, withRouter } from 'react-router-dom'
|
|
||||||
import { Text } from 'rebass'
|
|
||||||
import { ThemeContext } from 'styled-components'
|
|
||||||
import Card from '../../components/Card'
|
|
||||||
import { useActiveWeb3React } from '../../hooks'
|
|
||||||
import { useAllTokens, useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
|
|
||||||
import { useAllDummyPairs, useRemoveUserAddedToken } from '../../state/user/hooks'
|
|
||||||
import { useAllTokenBalancesTreatingWETHasETH, useTokenBalances } from '../../state/wallet/hooks'
|
|
||||||
import { CloseIcon, LinkStyledButton, StyledInternalLink } from '../../theme/components'
|
|
||||||
import { isAddress } from '../../utils'
|
|
||||||
import Column from '../Column'
|
|
||||||
import Modal from '../Modal'
|
|
||||||
import QuestionHelper from '../QuestionHelper'
|
|
||||||
import { AutoRow, RowBetween } from '../Row'
|
|
||||||
import Tooltip from '../Tooltip'
|
|
||||||
import CommonBases from './CommonBases'
|
|
||||||
import { filterPairs, filterTokens } from './filtering'
|
|
||||||
import PairList from './PairList'
|
|
||||||
import { balanceComparator, useTokenComparator } from './sorting'
|
|
||||||
import { PaddedColumn, SearchInput } from './styleds'
|
|
||||||
import TokenList from './TokenList'
|
|
||||||
import SortButton from './SortButton'
|
|
||||||
|
|
||||||
interface SearchModalProps extends RouteComponentProps {
|
|
||||||
isOpen?: boolean
|
|
||||||
onDismiss?: () => void
|
|
||||||
filterType?: 'tokens'
|
|
||||||
hiddenToken?: string
|
|
||||||
showSendWithSwap?: boolean
|
|
||||||
onTokenSelect?: (address: string) => void
|
|
||||||
otherSelectedTokenAddress?: string
|
|
||||||
otherSelectedText?: string
|
|
||||||
showCommonBases?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
function SearchModal({
|
|
||||||
history,
|
|
||||||
isOpen,
|
|
||||||
onDismiss,
|
|
||||||
onTokenSelect,
|
|
||||||
filterType,
|
|
||||||
hiddenToken,
|
|
||||||
showSendWithSwap,
|
|
||||||
otherSelectedTokenAddress,
|
|
||||||
otherSelectedText,
|
|
||||||
showCommonBases = false
|
|
||||||
}: SearchModalProps) {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { account, chainId } = useActiveWeb3React()
|
|
||||||
const theme = useContext(ThemeContext)
|
|
||||||
|
|
||||||
const isTokenView = filterType === 'tokens'
|
|
||||||
|
|
||||||
const allTokens = useAllTokens()
|
|
||||||
const allPairs = useAllDummyPairs()
|
|
||||||
const allTokenBalances = useAllTokenBalancesTreatingWETHasETH() ?? {}
|
|
||||||
const allPairBalances = useTokenBalances(
|
|
||||||
account,
|
|
||||||
allPairs.map(p => p.liquidityToken)
|
|
||||||
)
|
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
|
||||||
const [tooltipOpen, setTooltipOpen] = useState<boolean>(false)
|
|
||||||
const [invertSearchOrder, setInvertSearchOrder] = useState<boolean>(false)
|
|
||||||
|
|
||||||
const removeTokenByAddress = useRemoveUserAddedToken()
|
|
||||||
|
|
||||||
// if the current input is an address, and we don't have the token in context, try to fetch it and import
|
|
||||||
useTokenByAddressAndAutomaticallyAdd(searchQuery)
|
|
||||||
|
|
||||||
const tokenComparator = useTokenComparator(invertSearchOrder)
|
|
||||||
|
|
||||||
const sortedTokens: Token[] = useMemo(() => {
|
|
||||||
if (!isTokenView) return []
|
|
||||||
return Object.values(allTokens).sort(tokenComparator)
|
|
||||||
}, [allTokens, isTokenView, tokenComparator])
|
|
||||||
|
|
||||||
const filteredTokens: Token[] = useMemo(() => {
|
|
||||||
if (!isTokenView) return []
|
|
||||||
return filterTokens(sortedTokens, searchQuery)
|
|
||||||
}, [isTokenView, sortedTokens, searchQuery])
|
|
||||||
|
|
||||||
function _onTokenSelect(address: string) {
|
|
||||||
onTokenSelect(address)
|
|
||||||
onDismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
// clear the input on open
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen) setSearchQuery('')
|
|
||||||
}, [isOpen, setSearchQuery])
|
|
||||||
|
|
||||||
// manage focus on modal show
|
|
||||||
const inputRef = useRef<HTMLInputElement>()
|
|
||||||
function onInput(event) {
|
|
||||||
const input = event.target.value
|
|
||||||
const checksummedInput = isAddress(input)
|
|
||||||
setSearchQuery(checksummedInput || input)
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortedPairList = useMemo(() => {
|
|
||||||
if (isTokenView) return []
|
|
||||||
return allPairs.sort((a, b): number => {
|
|
||||||
// sort by balance
|
|
||||||
const balanceA = allPairBalances[a.liquidityToken.address]
|
|
||||||
const balanceB = allPairBalances[b.liquidityToken.address]
|
|
||||||
|
|
||||||
return balanceComparator(balanceA, balanceB)
|
|
||||||
})
|
|
||||||
}, [isTokenView, allPairs, allPairBalances])
|
|
||||||
|
|
||||||
const filteredPairs = useMemo(() => {
|
|
||||||
if (isTokenView) return []
|
|
||||||
return filterPairs(sortedPairList, searchQuery)
|
|
||||||
}, [isTokenView, searchQuery, sortedPairList])
|
|
||||||
|
|
||||||
const selectPair = useCallback(
|
|
||||||
(pair: Pair) => {
|
|
||||||
history.push(`/add/${pair.token0.address}-${pair.token1.address}`)
|
|
||||||
},
|
|
||||||
[history]
|
|
||||||
)
|
|
||||||
|
|
||||||
const focusedToken = Object.values(allTokens ?? {}).filter(token => {
|
|
||||||
return token.symbol.toLowerCase() === searchQuery || searchQuery === token.address
|
|
||||||
})[0]
|
|
||||||
|
|
||||||
const openTooltip = useCallback(() => {
|
|
||||||
setTooltipOpen(true)
|
|
||||||
inputRef.current?.focus()
|
|
||||||
}, [setTooltipOpen])
|
|
||||||
const closeTooltip = useCallback(() => setTooltipOpen(false), [setTooltipOpen])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={70} initialFocusRef={isMobile ? undefined : inputRef}>
|
|
||||||
<Column style={{ width: '100%' }}>
|
|
||||||
<PaddedColumn gap="20px">
|
|
||||||
<RowBetween>
|
|
||||||
<Text fontWeight={500} fontSize={16}>
|
|
||||||
{isTokenView ? 'Select a token' : 'Select a pool'}
|
|
||||||
<QuestionHelper
|
|
||||||
disabled={tooltipOpen}
|
|
||||||
text={
|
|
||||||
isTokenView
|
|
||||||
? 'Find a token by searching for its name or symbol or by pasting its address below.'
|
|
||||||
: 'Find a pair by searching for its name below.'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Text>
|
|
||||||
<CloseIcon onClick={onDismiss} />
|
|
||||||
</RowBetween>
|
|
||||||
<Tooltip
|
|
||||||
text="Import any token into your list by pasting the token address into the search field."
|
|
||||||
show={tooltipOpen}
|
|
||||||
placement="bottom"
|
|
||||||
>
|
|
||||||
<SearchInput
|
|
||||||
type={'text'}
|
|
||||||
id="token-search-input"
|
|
||||||
placeholder={t('tokenSearchPlaceholder')}
|
|
||||||
value={searchQuery}
|
|
||||||
ref={inputRef}
|
|
||||||
onChange={onInput}
|
|
||||||
onBlur={closeTooltip}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
{showCommonBases && (
|
|
||||||
<CommonBases chainId={chainId} onSelect={_onTokenSelect} selectedTokenAddress={hiddenToken} />
|
|
||||||
)}
|
|
||||||
<RowBetween>
|
|
||||||
<Text fontSize={14} fontWeight={500}>
|
|
||||||
{isTokenView ? 'Token Name' : 'Pool Name'}
|
|
||||||
</Text>
|
|
||||||
{isTokenView && (
|
|
||||||
<SortButton ascending={invertSearchOrder} toggleSortOrder={() => setInvertSearchOrder(iso => !iso)} />
|
|
||||||
)}
|
|
||||||
</RowBetween>
|
|
||||||
</PaddedColumn>
|
|
||||||
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
|
||||||
{isTokenView ? (
|
|
||||||
<TokenList
|
|
||||||
tokens={filteredTokens}
|
|
||||||
allTokenBalances={allTokenBalances}
|
|
||||||
onRemoveAddedToken={removeTokenByAddress}
|
|
||||||
onTokenSelect={_onTokenSelect}
|
|
||||||
otherSelectedText={otherSelectedText}
|
|
||||||
otherToken={otherSelectedTokenAddress}
|
|
||||||
selectedToken={hiddenToken}
|
|
||||||
showSendWithSwap={showSendWithSwap}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<PairList
|
|
||||||
pairs={filteredPairs}
|
|
||||||
focusTokenAddress={focusedToken?.address}
|
|
||||||
onAddLiquidity={selectPair}
|
|
||||||
onSelectPair={selectPair}
|
|
||||||
pairBalances={allPairBalances}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
|
||||||
<Card>
|
|
||||||
<AutoRow justify={'center'}>
|
|
||||||
<div>
|
|
||||||
{isTokenView ? (
|
|
||||||
<LinkStyledButton style={{ fontWeight: 500, color: theme.text2, fontSize: 16 }} onClick={openTooltip}>
|
|
||||||
Having trouble finding a token?
|
|
||||||
</LinkStyledButton>
|
|
||||||
) : (
|
|
||||||
<Text fontWeight={500}>
|
|
||||||
{!isMobile && "Don't see a pool? "}
|
|
||||||
<StyledInternalLink to="/find">{!isMobile ? 'Import it.' : 'Import pool.'}</StyledInternalLink>
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</AutoRow>
|
|
||||||
</Card>
|
|
||||||
</Column>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withRouter(SearchModal)
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Token, TokenAmount, WETH } from '@uniswap/sdk'
|
import { Token, TokenAmount, WETH, Pair } from '@uniswap/sdk'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useActiveWeb3React } from '../../hooks'
|
import { useActiveWeb3React } from '../../hooks'
|
||||||
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
|
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||||
|
import { DUMMY_PAIRS_TO_PIN } from '../../constants'
|
||||||
|
|
||||||
// compare two token amounts with highest one coming first
|
// compare two token amounts with highest one coming first
|
||||||
export function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount) {
|
function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount) {
|
||||||
if (balanceA && balanceB) {
|
if (balanceA && balanceB) {
|
||||||
return balanceA.greaterThan(balanceB) ? -1 : balanceA.equalTo(balanceB) ? 0 : 1
|
return balanceA.greaterThan(balanceB) ? -1 : balanceA.equalTo(balanceB) ? 0 : 1
|
||||||
} else if (balanceA && balanceA.greaterThan('0')) {
|
} else if (balanceA && balanceA.greaterThan('0')) {
|
||||||
@@ -15,6 +16,26 @@ export function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// compare two pairs, favoring "pinned" pairs, and falling back to balances
|
||||||
|
export function pairComparator(pairA: Pair, pairB: Pair, balanceA?: TokenAmount, balanceB?: TokenAmount) {
|
||||||
|
const aShouldBePinned =
|
||||||
|
DUMMY_PAIRS_TO_PIN[pairA?.token0?.chainId]?.some(
|
||||||
|
dummyPairToPin => dummyPairToPin.liquidityToken.address === pairA?.liquidityToken?.address
|
||||||
|
) ?? false
|
||||||
|
const bShouldBePinned =
|
||||||
|
DUMMY_PAIRS_TO_PIN[pairB?.token0?.chainId]?.some(
|
||||||
|
dummyPairToPin => dummyPairToPin.liquidityToken.address === pairB?.liquidityToken?.address
|
||||||
|
) ?? false
|
||||||
|
|
||||||
|
if (aShouldBePinned && !bShouldBePinned) {
|
||||||
|
return -1
|
||||||
|
} else if (!aShouldBePinned && bShouldBePinned) {
|
||||||
|
return 1
|
||||||
|
} else {
|
||||||
|
return balanceComparator(balanceA, balanceB)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getTokenComparator(
|
function getTokenComparator(
|
||||||
weth: Token | undefined,
|
weth: Token | undefined,
|
||||||
balances: { [tokenAddress: string]: TokenAmount }
|
balances: { [tokenAddress: string]: TokenAmount }
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { Spinner } from '../../theme'
|
|
||||||
import { AutoColumn } from '../Column'
|
import { AutoColumn } from '../Column'
|
||||||
import { AutoRow, RowBetween, RowFixed } from '../Row'
|
import { RowBetween, RowFixed } from '../Row'
|
||||||
|
|
||||||
export const ModalInfo = styled.div`
|
export const ModalInfo = styled.div`
|
||||||
${({ theme }) => theme.flexRowNoWrap}
|
${({ theme }) => theme.flexRowNoWrap}
|
||||||
@@ -9,8 +8,8 @@ export const ModalInfo = styled.div`
|
|||||||
padding: 1rem 1rem;
|
padding: 1rem 1rem;
|
||||||
margin: 0.25rem 0.5rem;
|
margin: 0.25rem 0.5rem;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
flex: 1;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
min-height: 200px;
|
|
||||||
`
|
`
|
||||||
|
|
||||||
export const FadedSpan = styled(RowFixed)`
|
export const FadedSpan = styled(RowFixed)`
|
||||||
@@ -23,12 +22,6 @@ export const GreySpan = styled.span`
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
`
|
`
|
||||||
|
|
||||||
export const SpinnerWrapper = styled(Spinner)`
|
|
||||||
margin: 0 0.25rem 0 0.25rem;
|
|
||||||
color: ${({ theme }) => theme.text4};
|
|
||||||
opacity: 0.6;
|
|
||||||
`
|
|
||||||
|
|
||||||
export const Input = styled.input`
|
export const Input = styled.input`
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -57,12 +50,9 @@ export const PaddedColumn = styled(AutoColumn)`
|
|||||||
padding-bottom: 12px;
|
padding-bottom: 12px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const PaddedItem = styled(RowBetween)`
|
export const MenuItem = styled(RowBetween)`
|
||||||
padding: 4px 20px;
|
padding: 4px 20px;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
`
|
|
||||||
|
|
||||||
export const MenuItem = styled(PaddedItem)`
|
|
||||||
cursor: ${({ disabled }) => !disabled && 'pointer'};
|
cursor: ${({ disabled }) => !disabled && 'pointer'};
|
||||||
pointer-events: ${({ disabled }) => disabled && 'none'};
|
pointer-events: ${({ disabled }) => disabled && 'none'};
|
||||||
:hover {
|
:hover {
|
||||||
@@ -71,21 +61,6 @@ export const MenuItem = styled(PaddedItem)`
|
|||||||
opacity: ${({ disabled, selected }) => (disabled || selected ? 0.5 : 1)};
|
opacity: ${({ disabled, selected }) => (disabled || selected ? 0.5 : 1)};
|
||||||
`
|
`
|
||||||
|
|
||||||
export const BaseWrapper = styled(AutoRow)<{ disable?: boolean }>`
|
|
||||||
border: 1px solid ${({ theme, disable }) => (disable ? 'transparent' : theme.bg3)};
|
|
||||||
padding: 0 6px;
|
|
||||||
border-radius: 10px;
|
|
||||||
width: 120px;
|
|
||||||
|
|
||||||
:hover {
|
|
||||||
cursor: ${({ disable }) => !disable && 'pointer'};
|
|
||||||
background-color: ${({ theme, disable }) => !disable && theme.bg2};
|
|
||||||
}
|
|
||||||
|
|
||||||
background-color: ${({ theme, disable }) => disable && theme.bg3};
|
|
||||||
opacity: ${({ disable }) => disable && '0.4'};
|
|
||||||
`
|
|
||||||
|
|
||||||
export const SearchInput = styled(Input)`
|
export const SearchInput = styled(Input)`
|
||||||
transition: border 100ms;
|
transition: border 100ms;
|
||||||
:focus {
|
:focus {
|
||||||
|
|||||||
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`
|
const FancyButton = styled.button`
|
||||||
color: ${({ theme }) => theme.text1};
|
color: ${({ theme }) => theme.text1};
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-width: 55px;
|
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
border-radius: 36px;
|
border-radius: 36px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
width: auto;
|
||||||
|
min-width: 3rem;
|
||||||
border: 1px solid ${({ theme }) => theme.bg3};
|
border: 1px solid ${({ theme }) => theme.bg3};
|
||||||
outline: none;
|
outline: none;
|
||||||
background: ${({ theme }) => theme.bg1};
|
background: ${({ theme }) => theme.bg1};
|
||||||
:hover {
|
:hover {
|
||||||
cursor: inherit;
|
|
||||||
border: 1px solid ${({ theme }) => theme.bg4};
|
border: 1px solid ${({ theme }) => theme.bg4};
|
||||||
}
|
}
|
||||||
:focus {
|
:focus {
|
||||||
@@ -48,9 +48,8 @@ const Option = styled(FancyButton)<{ active: boolean }>`
|
|||||||
|
|
||||||
const Input = styled.input`
|
const Input = styled.input`
|
||||||
background: ${({ theme }) => theme.bg1};
|
background: ${({ theme }) => theme.bg1};
|
||||||
flex-grow: 1;
|
font-size: 16px;
|
||||||
font-size: 12px;
|
width: auto;
|
||||||
min-width: 20px;
|
|
||||||
outline: none;
|
outline: none;
|
||||||
&::-webkit-outer-spin-button,
|
&::-webkit-outer-spin-button,
|
||||||
&::-webkit-inner-spin-button {
|
&::-webkit-inner-spin-button {
|
||||||
@@ -64,6 +63,7 @@ const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean }
|
|||||||
height: 2rem;
|
height: 2rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0 0.75rem;
|
padding: 0 0.75rem;
|
||||||
|
flex: 1;
|
||||||
border: ${({ theme, active, warning }) => active && `1px solid ${warning ? theme.red1 : theme.primary1}`};
|
border: ${({ theme, active, warning }) => active && `1px solid ${warning ? theme.red1 : theme.primary1}`};
|
||||||
:hover {
|
:hover {
|
||||||
border: ${({ theme, active, warning }) =>
|
border: ${({ theme, active, warning }) =>
|
||||||
@@ -78,8 +78,11 @@ const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean }
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const SlippageSelector = styled.div`
|
const SlippageEmojiContainer = styled.span`
|
||||||
padding: 0 20px;
|
color: #f3841e;
|
||||||
|
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||||
|
display: none;
|
||||||
|
`}
|
||||||
`
|
`
|
||||||
|
|
||||||
export interface SlippageTabsProps {
|
export interface SlippageTabsProps {
|
||||||
@@ -146,15 +149,14 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<AutoColumn gap="md">
|
||||||
<RowFixed padding={'0 20px'}>
|
<AutoColumn gap="sm">
|
||||||
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
|
<RowFixed>
|
||||||
Set slippage tolerance
|
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
|
||||||
</TYPE.black>
|
Slippage tolerance
|
||||||
<QuestionHelper text="Your transaction will revert if the price changes unfavorably by more than this percentage." />
|
</TYPE.black>
|
||||||
</RowFixed>
|
<QuestionHelper text="Your transaction will revert if the price changes unfavorably by more than this percentage." />
|
||||||
|
</RowFixed>
|
||||||
<SlippageSelector>
|
|
||||||
<RowBetween>
|
<RowBetween>
|
||||||
<Option
|
<Option
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -187,9 +189,11 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
|
|||||||
<RowBetween>
|
<RowBetween>
|
||||||
{!!slippageInput &&
|
{!!slippageInput &&
|
||||||
(slippageError === SlippageError.RiskyLow || slippageError === SlippageError.RiskyHigh) ? (
|
(slippageError === SlippageError.RiskyLow || slippageError === SlippageError.RiskyHigh) ? (
|
||||||
<span role="img" aria-label="warning" style={{ color: '#F3841E' }}>
|
<SlippageEmojiContainer>
|
||||||
⚠️
|
<span role="img" aria-label="warning">
|
||||||
</span>
|
⚠️
|
||||||
|
</span>
|
||||||
|
</SlippageEmojiContainer>
|
||||||
) : null}
|
) : null}
|
||||||
<Input
|
<Input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
@@ -220,16 +224,16 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
|
|||||||
: 'Your transaction may be frontrun'}
|
: 'Your transaction may be frontrun'}
|
||||||
</RowBetween>
|
</RowBetween>
|
||||||
)}
|
)}
|
||||||
</SlippageSelector>
|
</AutoColumn>
|
||||||
|
|
||||||
<AutoColumn gap="sm">
|
<AutoColumn gap="sm">
|
||||||
<RowFixed padding={'0 20px'}>
|
<RowFixed>
|
||||||
<TYPE.black fontSize={14} color={theme.text2}>
|
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
|
||||||
Deadline
|
Transaction deadline
|
||||||
</TYPE.black>
|
</TYPE.black>
|
||||||
<QuestionHelper text="Your transaction will revert if it is pending for more than this long." />
|
<QuestionHelper text="Your transaction will revert if it is pending for more than this long." />
|
||||||
</RowFixed>
|
</RowFixed>
|
||||||
<RowFixed padding={'0 20px'}>
|
<RowFixed>
|
||||||
<OptionCustom style={{ width: '80px' }} tabIndex={-1}>
|
<OptionCustom style={{ width: '80px' }} tabIndex={-1}>
|
||||||
<Input
|
<Input
|
||||||
color={!!deadlineError ? 'red' : undefined}
|
color={!!deadlineError ? 'red' : undefined}
|
||||||
@@ -246,6 +250,6 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
|
|||||||
</TYPE.body>
|
</TYPE.body>
|
||||||
</RowFixed>
|
</RowFixed>
|
||||||
</AutoColumn>
|
</AutoColumn>
|
||||||
</>
|
</AutoColumn>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
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'
|
import EthereumLogo from '../../assets/images/ethereum-logo.png'
|
||||||
|
|
||||||
const TOKEN_ICON_API = address =>
|
const getTokenLogoURL = address =>
|
||||||
`https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${address}/logo.png`
|
`https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${address}/logo.png`
|
||||||
const BAD_IMAGES = {}
|
const NO_LOGO_ADDRESSES: { [tokenAddress: string]: true } = {}
|
||||||
|
|
||||||
const Image = styled.img<{ size: string }>`
|
const Image = styled.img<{ size: string }>`
|
||||||
width: ${({ size }) => size};
|
width: ${({ size }) => size};
|
||||||
@@ -44,20 +44,16 @@ export default function TokenLogo({
|
|||||||
size?: string
|
size?: string
|
||||||
style?: React.CSSProperties
|
style?: React.CSSProperties
|
||||||
}) {
|
}) {
|
||||||
const [error, setError] = useState(false)
|
const [, refresh] = useState<number>(0)
|
||||||
const { chainId } = useActiveWeb3React()
|
const { chainId } = useActiveWeb3React()
|
||||||
|
|
||||||
// mock rinkeby DAI
|
|
||||||
if (chainId === 4 && address === '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735') {
|
|
||||||
address = '0x6B175474E89094C44Da98b954EedeAC495271d0F'
|
|
||||||
}
|
|
||||||
|
|
||||||
let path = ''
|
let path = ''
|
||||||
|
const validated = isAddress(address)
|
||||||
// hard code to show ETH instead of WETH in UI
|
// hard code to show ETH instead of WETH in UI
|
||||||
if (address === WETH[chainId].address) {
|
if (validated === WETH[chainId].address) {
|
||||||
return <StyledEthereumLogo src={EthereumLogo} size={size} {...rest} />
|
return <StyledEthereumLogo src={EthereumLogo} size={size} {...rest} />
|
||||||
} else if (!error && !BAD_IMAGES[address] && isAddress(address)) {
|
} else if (!NO_LOGO_ADDRESSES[address] && validated) {
|
||||||
path = TOKEN_ICON_API(address)
|
path = getTokenLogoURL(validated)
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Emoji {...rest} size={size}>
|
<Emoji {...rest} size={size}>
|
||||||
@@ -75,8 +71,8 @@ export default function TokenLogo({
|
|||||||
src={path}
|
src={path}
|
||||||
size={size}
|
size={size}
|
||||||
onError={() => {
|
onError={() => {
|
||||||
BAD_IMAGES[address] = true
|
NO_LOGO_ADDRESSES[address] = true
|
||||||
setError(true)
|
refresh(i => i + 1)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,13 +3,12 @@ import { transparentize } from 'polished'
|
|||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
||||||
import { ALL_TOKENS } from '../../constants/tokens'
|
|
||||||
import { useActiveWeb3React } from '../../hooks'
|
import { useActiveWeb3React } from '../../hooks'
|
||||||
import { useAllTokens } from '../../hooks/Tokens'
|
import { useAllTokens } from '../../hooks/Tokens'
|
||||||
import { Field } from '../../state/swap/actions'
|
import { Field } from '../../state/swap/actions'
|
||||||
import { useTokenWarningDismissal } from '../../state/user/hooks'
|
import { useTokenWarningDismissal } from '../../state/user/hooks'
|
||||||
import { ExternalLink, TYPE } from '../../theme'
|
import { ExternalLink, TYPE } from '../../theme'
|
||||||
import { getEtherscanLink } from '../../utils'
|
import { getEtherscanLink, isDefaultToken } from '../../utils'
|
||||||
import PropsOfExcluding from '../../utils/props-of-excluding'
|
import PropsOfExcluding from '../../utils/props-of-excluding'
|
||||||
import QuestionHelper from '../QuestionHelper'
|
import QuestionHelper from '../QuestionHelper'
|
||||||
import TokenLogo from '../TokenLogo'
|
import TokenLogo from '../TokenLogo'
|
||||||
@@ -18,11 +17,11 @@ const Wrapper = styled.div<{ error: boolean }>`
|
|||||||
background: ${({ theme, error }) => transparentize(0.9, error ? theme.red1 : theme.yellow1)};
|
background: ${({ theme, error }) => transparentize(0.9, error ? theme.red1 : theme.yellow1)};
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border: 0.5px solid ${({ theme, error }) => transparentize(0.4, error ? theme.red1 : theme.yellow1)};
|
/* border: 0.5px solid ${({ theme, error }) => transparentize(0.4, error ? theme.red1 : theme.yellow1)}; */
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto auto auto;
|
grid-template-rows: 14px auto auto;
|
||||||
grid-row-gap: 14px;
|
grid-row-gap: 14px;
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -42,15 +41,15 @@ const CloseColor = styled(Close)`
|
|||||||
const CloseIcon = styled.div`
|
const CloseIcon = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
top: 14px;
|
top: 12px;
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
height: 14px;
|
height: 16px;
|
||||||
width: 14px;
|
width: 16px;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -68,9 +67,8 @@ interface TokenWarningCardProps extends PropsOfExcluding<typeof Wrapper, 'error'
|
|||||||
|
|
||||||
export default function TokenWarningCard({ token, ...rest }: TokenWarningCardProps) {
|
export default function TokenWarningCard({ token, ...rest }: TokenWarningCardProps) {
|
||||||
const { chainId } = useActiveWeb3React()
|
const { chainId } = useActiveWeb3React()
|
||||||
const isDefaultToken = Boolean(
|
|
||||||
token && token.address && chainId && ALL_TOKENS[chainId] && ALL_TOKENS[chainId][token.address]
|
const isDefault = isDefaultToken(token)
|
||||||
)
|
|
||||||
|
|
||||||
const tokenSymbol = token?.symbol?.toLowerCase() ?? ''
|
const tokenSymbol = token?.symbol?.toLowerCase() ?? ''
|
||||||
const tokenName = token?.name?.toLowerCase() ?? ''
|
const tokenName = token?.name?.toLowerCase() ?? ''
|
||||||
@@ -80,7 +78,7 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro
|
|||||||
const allTokens = useAllTokens()
|
const allTokens = useAllTokens()
|
||||||
|
|
||||||
const duplicateNameOrSymbol = useMemo(() => {
|
const duplicateNameOrSymbol = useMemo(() => {
|
||||||
if (isDefaultToken || !token || !chainId) return false
|
if (isDefault || !token || !chainId) return false
|
||||||
|
|
||||||
return Object.keys(allTokens).some(tokenAddress => {
|
return Object.keys(allTokens).some(tokenAddress => {
|
||||||
const userToken = allTokens[tokenAddress]
|
const userToken = allTokens[tokenAddress]
|
||||||
@@ -89,9 +87,9 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro
|
|||||||
}
|
}
|
||||||
return userToken.symbol.toLowerCase() === tokenSymbol || userToken.name.toLowerCase() === tokenName
|
return userToken.symbol.toLowerCase() === tokenSymbol || userToken.name.toLowerCase() === tokenName
|
||||||
})
|
})
|
||||||
}, [isDefaultToken, token, chainId, allTokens, tokenSymbol, tokenName])
|
}, [isDefault, token, chainId, allTokens, tokenSymbol, tokenName])
|
||||||
|
|
||||||
if (isDefaultToken || !token || dismissed) return null
|
if (isDefault || !token || dismissed) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper error={duplicateNameOrSymbol} {...rest}>
|
<Wrapper error={duplicateNameOrSymbol} {...rest}>
|
||||||
@@ -111,7 +109,7 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro
|
|||||||
? `${token.name} (${token.symbol})`
|
? `${token.name} (${token.symbol})`
|
||||||
: token.name || token.symbol}
|
: token.name || token.symbol}
|
||||||
</div>
|
</div>
|
||||||
<ExternalLink style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'address')}>
|
<ExternalLink style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'token')}>
|
||||||
(View on Etherscan)
|
(View on Etherscan)
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useCallback, useState } from 'react'
|
import React, { useCallback, useContext, useState } from 'react'
|
||||||
import { AlertCircle, CheckCircle } from 'react-feather'
|
import { AlertCircle, CheckCircle } from 'react-feather'
|
||||||
|
|
||||||
import styled from 'styled-components'
|
import styled, { ThemeContext } from 'styled-components'
|
||||||
|
|
||||||
import { useActiveWeb3React } from '../../hooks'
|
import { useActiveWeb3React } from '../../hooks'
|
||||||
import useInterval from '../../hooks/useInterval'
|
import useInterval from '../../hooks/useInterval'
|
||||||
@@ -51,17 +51,18 @@ export default function TxnPopup({
|
|||||||
isRunning ? delay : null
|
isRunning ? delay : null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleMouseEnter = useCallback(() => setIsRunning(false), [])
|
||||||
|
const handleMouseLeave = useCallback(() => setIsRunning(true), [])
|
||||||
|
|
||||||
|
const theme = useContext(ThemeContext)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AutoRow onMouseEnter={() => setIsRunning(false)} onMouseLeave={() => setIsRunning(true)}>
|
<AutoRow onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||||
{success ? (
|
<div style={{ paddingRight: 16 }}>
|
||||||
<CheckCircle color={'#27AE60'} size={24} style={{ paddingRight: '24px' }} />
|
{success ? <CheckCircle color={theme.green1} size={24} /> : <AlertCircle color={theme.red1} size={24} />}
|
||||||
) : (
|
</div>
|
||||||
<AlertCircle color={'#FF6871'} size={24} style={{ paddingRight: '24px' }} />
|
|
||||||
)}
|
|
||||||
<AutoColumn gap="8px">
|
<AutoColumn gap="8px">
|
||||||
<TYPE.body fontWeight={500}>
|
<TYPE.body fontWeight={500}>{summary ?? 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}</TYPE.body>
|
||||||
{summary ? summary : 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}
|
|
||||||
</TYPE.body>
|
|
||||||
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>View on Etherscan</ExternalLink>
|
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>View on Etherscan</ExternalLink>
|
||||||
</AutoColumn>
|
</AutoColumn>
|
||||||
<Fader count={count} />
|
<Fader count={count} />
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const InfoCard = styled.button<{ active?: boolean }>`
|
|||||||
border-color: ${({ theme, active }) => (active ? 'transparent' : theme.bg3)};
|
border-color: ${({ theme, active }) => (active ? 'transparent' : theme.bg3)};
|
||||||
`
|
`
|
||||||
|
|
||||||
const OptionCard = styled(InfoCard)`
|
const OptionCard = styled(InfoCard as any)`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -30,7 +30,7 @@ const OptionCardLeft = styled.div`
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
`
|
`
|
||||||
|
|
||||||
const OptionCardClickable = styled(OptionCard)<{ clickable?: boolean }>`
|
const OptionCardClickable = styled(OptionCard as any)<{ clickable?: boolean }>`
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: ${({ clickable }) => (clickable ? 'pointer' : '')};
|
cursor: ${({ clickable }) => (clickable ? 'pointer' : '')};
|
||||||
@@ -114,7 +114,6 @@ export default function Option({
|
|||||||
<OptionCardClickable id={id} onClick={onClick} clickable={clickable && !active} active={active}>
|
<OptionCardClickable id={id} onClick={onClick} clickable={clickable && !active} active={active}>
|
||||||
<OptionCardLeft>
|
<OptionCardLeft>
|
||||||
<HeaderText color={color}>
|
<HeaderText color={color}>
|
||||||
{' '}
|
|
||||||
{active ? (
|
{active ? (
|
||||||
<CircleWrapper>
|
<CircleWrapper>
|
||||||
<GreenCircle>
|
<GreenCircle>
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ import React from 'react'
|
|||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import Option from './Option'
|
import Option from './Option'
|
||||||
import { SUPPORTED_WALLETS } from '../../constants'
|
import { SUPPORTED_WALLETS } from '../../constants'
|
||||||
import WalletConnectData from './WalletConnectData'
|
import { injected } from '../../connectors'
|
||||||
import { walletconnect, injected } from '../../connectors'
|
|
||||||
import { Spinner } from '../../theme'
|
|
||||||
import Circle from '../../assets/images/circle.svg'
|
|
||||||
import { darken } from 'polished'
|
import { darken } from 'polished'
|
||||||
|
import Loader from '../Loader'
|
||||||
|
|
||||||
const PendingSection = styled.div`
|
const PendingSection = styled.div`
|
||||||
${({ theme }) => theme.flexColumnNoWrap};
|
${({ theme }) => theme.flexColumnNoWrap};
|
||||||
@@ -19,14 +17,8 @@ const PendingSection = styled.div`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const SpinnerWrapper = styled(Spinner)`
|
const StyledLoader = styled(Loader)`
|
||||||
font-size: 4rem;
|
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
svg {
|
|
||||||
path {
|
|
||||||
color: ${({ theme }) => theme.bg4};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const LoadingMessage = styled.div<{ error?: boolean }>`
|
const LoadingMessage = styled.div<{ error?: boolean }>`
|
||||||
@@ -72,28 +64,22 @@ const LoadingWrapper = styled.div`
|
|||||||
`
|
`
|
||||||
|
|
||||||
export default function PendingView({
|
export default function PendingView({
|
||||||
uri = '',
|
|
||||||
size,
|
|
||||||
connector,
|
connector,
|
||||||
error = false,
|
error = false,
|
||||||
setPendingError,
|
setPendingError,
|
||||||
tryActivation
|
tryActivation
|
||||||
}: {
|
}: {
|
||||||
uri?: string
|
|
||||||
size?: number
|
|
||||||
connector?: AbstractConnector
|
connector?: AbstractConnector
|
||||||
error?: boolean
|
error?: boolean
|
||||||
setPendingError: (error: boolean) => void
|
setPendingError: (error: boolean) => void
|
||||||
tryActivation: (connector: AbstractConnector) => void
|
tryActivation: (connector: AbstractConnector) => void
|
||||||
}) {
|
}) {
|
||||||
const isMetamask = window.ethereum && window.ethereum.isMetaMask
|
const isMetamask = window?.ethereum?.isMetaMask
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PendingSection>
|
<PendingSection>
|
||||||
{!error && connector === walletconnect && <WalletConnectData size={size} uri={uri} />}
|
|
||||||
<LoadingMessage error={error}>
|
<LoadingMessage error={error}>
|
||||||
<LoadingWrapper>
|
<LoadingWrapper>
|
||||||
{!error && <SpinnerWrapper src={Circle} />}
|
|
||||||
{error ? (
|
{error ? (
|
||||||
<ErrorGroup>
|
<ErrorGroup>
|
||||||
<div>Error connecting.</div>
|
<div>Error connecting.</div>
|
||||||
@@ -106,10 +92,11 @@ export default function PendingView({
|
|||||||
Try Again
|
Try Again
|
||||||
</ErrorButton>
|
</ErrorButton>
|
||||||
</ErrorGroup>
|
</ErrorGroup>
|
||||||
) : connector === walletconnect ? (
|
|
||||||
'Scan QR code with a compatible wallet...'
|
|
||||||
) : (
|
) : (
|
||||||
'Initializing...'
|
<>
|
||||||
|
<StyledLoader />
|
||||||
|
Initializing...
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</LoadingWrapper>
|
</LoadingWrapper>
|
||||||
</LoadingMessage>
|
</LoadingMessage>
|
||||||
|
|||||||
@@ -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 styled from 'styled-components'
|
||||||
import { isMobile } from 'react-device-detect'
|
import { isMobile } from 'react-device-detect'
|
||||||
import { UnsupportedChainIdError, useWeb3React } from '@web3-react/core'
|
import { UnsupportedChainIdError, useWeb3React } from '@web3-react/core'
|
||||||
import { URI_AVAILABLE } from '@web3-react/walletconnect-connector'
|
|
||||||
import usePrevious from '../../hooks/usePrevious'
|
import usePrevious from '../../hooks/usePrevious'
|
||||||
import { useWalletModalOpen, useWalletModalToggle } from '../../state/application/hooks'
|
import { useWalletModalOpen, useWalletModalToggle } from '../../state/application/hooks'
|
||||||
|
|
||||||
@@ -15,8 +14,9 @@ import { SUPPORTED_WALLETS } from '../../constants'
|
|||||||
import { ExternalLink } from '../../theme'
|
import { ExternalLink } from '../../theme'
|
||||||
import MetamaskIcon from '../../assets/images/metamask.png'
|
import MetamaskIcon from '../../assets/images/metamask.png'
|
||||||
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
||||||
import { injected, walletconnect, fortmatic, portis } from '../../connectors'
|
import { injected, fortmatic, portis } from '../../connectors'
|
||||||
import { OVERLAY_READY } from '../../connectors/Fortmatic'
|
import { OVERLAY_READY } from '../../connectors/Fortmatic'
|
||||||
|
import { WalletConnectConnector } from '@web3-react/walletconnect-connector'
|
||||||
|
|
||||||
const CloseIcon = styled.div`
|
const CloseIcon = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -152,19 +152,6 @@ export default function WalletModal({
|
|||||||
}
|
}
|
||||||
}, [walletModalOpen])
|
}, [walletModalOpen])
|
||||||
|
|
||||||
// set up uri listener for walletconnect
|
|
||||||
const [uri, setUri] = useState()
|
|
||||||
useEffect(() => {
|
|
||||||
const activateWC = uri => {
|
|
||||||
setUri(uri)
|
|
||||||
// setWalletView(WALLET_VIEWS.PENDING)
|
|
||||||
}
|
|
||||||
walletconnect.on(URI_AVAILABLE, activateWC)
|
|
||||||
return () => {
|
|
||||||
walletconnect.off(URI_AVAILABLE, activateWC)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// close modal when a connection is successful
|
// close modal when a connection is successful
|
||||||
const activePrevious = usePrevious(active)
|
const activePrevious = usePrevious(active)
|
||||||
const connectorPrevious = usePrevious(connector)
|
const connectorPrevious = usePrevious(connector)
|
||||||
@@ -190,6 +177,12 @@ export default function WalletModal({
|
|||||||
})
|
})
|
||||||
setPendingWallet(connector) // set wallet for pending view
|
setPendingWallet(connector) // set wallet for pending view
|
||||||
setWalletView(WALLET_VIEWS.PENDING)
|
setWalletView(WALLET_VIEWS.PENDING)
|
||||||
|
|
||||||
|
// if the connector is walletconnect and the user has already tried to connect, manually reset the connector
|
||||||
|
if (connector instanceof WalletConnectConnector && connector.walletConnectProvider?.wc?.uri) {
|
||||||
|
connector.walletConnectProvider = undefined
|
||||||
|
}
|
||||||
|
|
||||||
activate(connector, undefined, true).catch(error => {
|
activate(connector, undefined, true).catch(error => {
|
||||||
if (error instanceof UnsupportedChainIdError) {
|
if (error instanceof UnsupportedChainIdError) {
|
||||||
activate(connector) // a little janky...can't use setError because the connector isn't set
|
activate(connector) // a little janky...can't use setError because the connector isn't set
|
||||||
@@ -345,8 +338,6 @@ export default function WalletModal({
|
|||||||
<ContentWrapper>
|
<ContentWrapper>
|
||||||
{walletView === WALLET_VIEWS.PENDING ? (
|
{walletView === WALLET_VIEWS.PENDING ? (
|
||||||
<PendingView
|
<PendingView
|
||||||
uri={uri}
|
|
||||||
size={220}
|
|
||||||
connector={pendingWallet}
|
connector={pendingWallet}
|
||||||
error={pendingError}
|
error={pendingError}
|
||||||
setPendingError={setPendingError}
|
setPendingError={setPendingError}
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import { useTranslation } from 'react-i18next'
|
|||||||
|
|
||||||
import { network } from '../../connectors'
|
import { network } from '../../connectors'
|
||||||
import { useEagerConnect, useInactiveListener } from '../../hooks'
|
import { useEagerConnect, useInactiveListener } from '../../hooks'
|
||||||
import { Spinner } from '../../theme'
|
|
||||||
import Circle from '../../assets/images/circle.svg'
|
|
||||||
import { NetworkContextName } from '../../constants'
|
import { NetworkContextName } from '../../constants'
|
||||||
|
import Loader from '../Loader'
|
||||||
|
|
||||||
const MessageWrapper = styled.div`
|
const MessageWrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -20,16 +19,6 @@ const Message = styled.h2`
|
|||||||
color: ${({ theme }) => theme.secondary1};
|
color: ${({ theme }) => theme.secondary1};
|
||||||
`
|
`
|
||||||
|
|
||||||
const SpinnerWrapper = styled(Spinner)`
|
|
||||||
font-size: 4rem;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
path {
|
|
||||||
color: ${({ theme }) => theme.secondary1};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export default function Web3ReactManager({ children }) {
|
export default function Web3ReactManager({ children }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { active } = useWeb3React()
|
const { active } = useWeb3React()
|
||||||
@@ -78,7 +67,7 @@ export default function Web3ReactManager({ children }) {
|
|||||||
if (!active && !networkActive) {
|
if (!active && !networkActive) {
|
||||||
return showLoader ? (
|
return showLoader ? (
|
||||||
<MessageWrapper>
|
<MessageWrapper>
|
||||||
<SpinnerWrapper src={Circle} />
|
<Loader />
|
||||||
</MessageWrapper>
|
</MessageWrapper>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useWeb3React, UnsupportedChainIdError } from '@web3-react/core'
|
|||||||
import { darken, lighten } from 'polished'
|
import { darken, lighten } from 'polished'
|
||||||
import { Activity } from 'react-feather'
|
import { Activity } from 'react-feather'
|
||||||
import useENSName from '../../hooks/useENSName'
|
import useENSName from '../../hooks/useENSName'
|
||||||
|
import { useHasSocks } from '../../hooks/useSocksBalance'
|
||||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||||
import { TransactionDetails } from '../../state/transactions/reducer'
|
import { TransactionDetails } from '../../state/transactions/reducer'
|
||||||
|
|
||||||
@@ -16,18 +17,12 @@ import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
|
|||||||
import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
|
import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
|
||||||
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
|
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
|
||||||
|
|
||||||
import { Spinner } from '../../theme'
|
|
||||||
import LightCircle from '../../assets/svg/lightcircle.svg'
|
|
||||||
|
|
||||||
import { RowBetween } from '../Row'
|
import { RowBetween } from '../Row'
|
||||||
import { shortenAddress } from '../../utils'
|
import { shortenAddress } from '../../utils'
|
||||||
import { useAllTransactions } from '../../state/transactions/hooks'
|
import { useAllTransactions } from '../../state/transactions/hooks'
|
||||||
import { NetworkContextName } from '../../constants'
|
import { NetworkContextName } from '../../constants'
|
||||||
import { injected, walletconnect, walletlink, fortmatic, portis } from '../../connectors'
|
import { injected, walletconnect, walletlink, fortmatic, portis } from '../../connectors'
|
||||||
|
import Loader from '../Loader'
|
||||||
const SpinnerWrapper = styled(Spinner)`
|
|
||||||
margin: 0 0.25rem 0 0.25rem;
|
|
||||||
`
|
|
||||||
|
|
||||||
const IconWrapper = styled.div<{ size?: number }>`
|
const IconWrapper = styled.div<{ size?: number }>`
|
||||||
${({ theme }) => theme.flexColumnNoWrap};
|
${({ theme }) => theme.flexColumnNoWrap};
|
||||||
@@ -136,7 +131,7 @@ export default function Web3Status() {
|
|||||||
const { active, account, connector, error } = useWeb3React()
|
const { active, account, connector, error } = useWeb3React()
|
||||||
const contextNetwork = useWeb3React(NetworkContextName)
|
const contextNetwork = useWeb3React(NetworkContextName)
|
||||||
|
|
||||||
const ENSName = useENSName(account)
|
const { ENSName } = useENSName(account)
|
||||||
|
|
||||||
const allTransactions = useAllTransactions()
|
const allTransactions = useAllTransactions()
|
||||||
|
|
||||||
@@ -149,7 +144,7 @@ export default function Web3Status() {
|
|||||||
const confirmed = sortedRecentTransactions.filter(tx => tx.receipt).map(tx => tx.hash)
|
const confirmed = sortedRecentTransactions.filter(tx => tx.receipt).map(tx => tx.hash)
|
||||||
|
|
||||||
const hasPendingTransactions = !!pending.length
|
const hasPendingTransactions = !!pending.length
|
||||||
|
const hasSocks = useHasSocks()
|
||||||
const toggleWalletModal = useWalletModalToggle()
|
const toggleWalletModal = useWalletModalToggle()
|
||||||
|
|
||||||
// handle the logo we want to show with the account
|
// handle the logo we want to show with the account
|
||||||
@@ -189,10 +184,12 @@ export default function Web3Status() {
|
|||||||
<Web3StatusConnected id="web3-status-connected" onClick={toggleWalletModal} pending={hasPendingTransactions}>
|
<Web3StatusConnected id="web3-status-connected" onClick={toggleWalletModal} pending={hasPendingTransactions}>
|
||||||
{hasPendingTransactions ? (
|
{hasPendingTransactions ? (
|
||||||
<RowBetween>
|
<RowBetween>
|
||||||
<Text>{pending?.length} Pending</Text> <SpinnerWrapper src={LightCircle} alt="loader" />
|
<Text>{pending?.length} Pending</Text> <Loader stroke="white" />
|
||||||
</RowBetween>
|
</RowBetween>
|
||||||
) : (
|
) : (
|
||||||
<Text>{ENSName || shortenAddress(account)}</Text>
|
<Text>
|
||||||
|
{hasSocks ? '🧦' : ''} {ENSName || shortenAddress(account)}
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
{!hasPendingTransactions && getStatusIcon()}
|
{!hasPendingTransactions && getStatusIcon()}
|
||||||
</Web3StatusConnected>
|
</Web3StatusConnected>
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
import { Trade, TradeType } from '@uniswap/sdk'
|
import { Trade, TradeType } from '@uniswap/sdk'
|
||||||
import React, { useContext } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { ChevronUp, ChevronRight } from 'react-feather'
|
|
||||||
import { Text, Flex } from 'rebass'
|
|
||||||
import { ThemeContext } from 'styled-components'
|
import { ThemeContext } from 'styled-components'
|
||||||
import { Field } from '../../state/swap/actions'
|
import { Field } from '../../state/swap/actions'
|
||||||
import { CursorPointer, TYPE } from '../../theme'
|
import { useUserSlippageTolerance } from '../../state/user/hooks'
|
||||||
|
import { TYPE } from '../../theme'
|
||||||
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown } from '../../utils/prices'
|
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown } from '../../utils/prices'
|
||||||
import { AutoColumn } from '../Column'
|
import { AutoColumn } from '../Column'
|
||||||
import { SectionBreak } from './styleds'
|
|
||||||
import QuestionHelper from '../QuestionHelper'
|
import QuestionHelper from '../QuestionHelper'
|
||||||
import { RowBetween, RowFixed } from '../Row'
|
import { RowBetween, RowFixed } from '../Row'
|
||||||
import SlippageTabs, { SlippageTabsProps } from '../SlippageTabs'
|
|
||||||
import FormattedPriceImpact from './FormattedPriceImpact'
|
import FormattedPriceImpact from './FormattedPriceImpact'
|
||||||
import TokenLogo from '../TokenLogo'
|
import { SectionBreak } from './styleds'
|
||||||
import flatMap from 'lodash.flatmap'
|
import SwapRoute from './SwapRoute'
|
||||||
|
|
||||||
function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippage: number }) {
|
function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippage: number }) {
|
||||||
const theme = useContext(ThemeContext)
|
const theme = useContext(ThemeContext)
|
||||||
@@ -61,79 +58,37 @@ function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippag
|
|||||||
</TYPE.black>
|
</TYPE.black>
|
||||||
</RowBetween>
|
</RowBetween>
|
||||||
</AutoColumn>
|
</AutoColumn>
|
||||||
|
|
||||||
<SectionBreak />
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdvancedSwapDetailsProps extends SlippageTabsProps {
|
export interface AdvancedSwapDetailsProps {
|
||||||
trade?: Trade
|
trade?: Trade
|
||||||
onDismiss: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AdvancedSwapDetails({ trade, onDismiss, ...slippageTabProps }: AdvancedSwapDetailsProps) {
|
export function AdvancedSwapDetails({ trade }: AdvancedSwapDetailsProps) {
|
||||||
const theme = useContext(ThemeContext)
|
const theme = useContext(ThemeContext)
|
||||||
|
|
||||||
|
const [allowedSlippage] = useUserSlippageTolerance()
|
||||||
|
|
||||||
|
const showRoute = trade?.route?.path?.length > 2
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AutoColumn gap="md">
|
<AutoColumn gap="md">
|
||||||
<CursorPointer>
|
{trade && <TradeSummary trade={trade} allowedSlippage={allowedSlippage} />}
|
||||||
<RowBetween onClick={onDismiss} padding={'8px 20px'}>
|
{showRoute && (
|
||||||
<Text fontSize={16} color={theme.text2} fontWeight={500} style={{ userSelect: 'none' }}>
|
<>
|
||||||
Hide Advanced
|
<SectionBreak />
|
||||||
</Text>
|
<AutoColumn style={{ padding: '0 24px' }}>
|
||||||
<ChevronUp color={theme.text2} />
|
<RowFixed>
|
||||||
</RowBetween>
|
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
|
||||||
</CursorPointer>
|
Route
|
||||||
|
</TYPE.black>
|
||||||
<SectionBreak />
|
<QuestionHelper text="Routing through these tokens resulted in the best price for your trade." />
|
||||||
|
</RowFixed>
|
||||||
{trade && <TradeSummary trade={trade} allowedSlippage={slippageTabProps.rawSlippage} />}
|
<SwapRoute trade={trade} />
|
||||||
|
</AutoColumn>
|
||||||
<SlippageTabs {...slippageTabProps} />
|
</>
|
||||||
|
|
||||||
{trade?.route?.path?.length > 2 && (
|
|
||||||
<AutoColumn style={{ padding: '0 20px' }}>
|
|
||||||
<RowFixed>
|
|
||||||
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
|
|
||||||
Route
|
|
||||||
</TYPE.black>
|
|
||||||
<QuestionHelper text="Routing through these tokens resulted in the best price for your trade." />
|
|
||||||
</RowFixed>
|
|
||||||
<Flex
|
|
||||||
px="1rem"
|
|
||||||
py="0.5rem"
|
|
||||||
my="0.5rem"
|
|
||||||
style={{ border: `1px solid ${theme.bg3}`, borderRadius: '1rem' }}
|
|
||||||
flexWrap="wrap"
|
|
||||||
width="100%"
|
|
||||||
justifyContent="space-evenly"
|
|
||||||
alignItems="center"
|
|
||||||
>
|
|
||||||
{flatMap(
|
|
||||||
trade.route.path,
|
|
||||||
// add a null in-between each item
|
|
||||||
(token, i, array) => {
|
|
||||||
const lastItem = i === array.length - 1
|
|
||||||
return lastItem ? [token] : [token, null]
|
|
||||||
}
|
|
||||||
).map((token, i) => {
|
|
||||||
// use null as an indicator to insert chevrons
|
|
||||||
if (token === null) {
|
|
||||||
return <ChevronRight key={i} color={theme.text2} />
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<Flex my="0.5rem" alignItems="center" key={token.address} style={{ flexShrink: 0 }}>
|
|
||||||
<TokenLogo address={token.address} size="1.5rem" />
|
|
||||||
<TYPE.black fontSize={14} color={theme.text1} ml="0.5rem">
|
|
||||||
{token.symbol}
|
|
||||||
</TYPE.black>
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</Flex>
|
|
||||||
</AutoColumn>
|
|
||||||
)}
|
)}
|
||||||
</AutoColumn>
|
</AutoColumn>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,35 +1,30 @@
|
|||||||
import React, { useContext } from 'react'
|
import React from 'react'
|
||||||
import { ChevronDown } from 'react-feather'
|
import styled from 'styled-components'
|
||||||
import { Text } from 'rebass'
|
import useLast from '../../hooks/useLast'
|
||||||
import { ThemeContext } from 'styled-components'
|
|
||||||
import { CursorPointer } from '../../theme'
|
|
||||||
import { RowBetween } from '../Row'
|
|
||||||
import { AdvancedSwapDetails, AdvancedSwapDetailsProps } from './AdvancedSwapDetails'
|
import { AdvancedSwapDetails, AdvancedSwapDetailsProps } from './AdvancedSwapDetails'
|
||||||
import { AdvancedDropdown } from './styleds'
|
|
||||||
|
|
||||||
export default function AdvancedSwapDetailsDropdown({
|
const AdvancedDetailsFooter = styled.div<{ show: boolean }>`
|
||||||
showAdvanced,
|
padding-top: calc(16px + 2rem);
|
||||||
setShowAdvanced,
|
padding-bottom: 20px;
|
||||||
...rest
|
margin-top: -2rem;
|
||||||
}: Omit<AdvancedSwapDetailsProps, 'onDismiss'> & {
|
width: 100%;
|
||||||
showAdvanced: boolean
|
max-width: 400px;
|
||||||
setShowAdvanced: (showAdvanced: boolean) => void
|
border-bottom-left-radius: 20px;
|
||||||
}) {
|
border-bottom-right-radius: 20px;
|
||||||
const theme = useContext(ThemeContext)
|
color: ${({ theme }) => theme.text2};
|
||||||
|
background-color: ${({ theme }) => theme.advancedBG};
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
|
transform: ${({ show }) => (show ? 'translateY(0%)' : 'translateY(-100%)')};
|
||||||
|
transition: transform 300ms ease-in-out;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default function AdvancedSwapDetailsDropdown({ trade, ...rest }: AdvancedSwapDetailsProps) {
|
||||||
|
const lastTrade = useLast(trade)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdvancedDropdown>
|
<AdvancedDetailsFooter show={Boolean(trade)}>
|
||||||
{showAdvanced ? (
|
<AdvancedSwapDetails {...rest} trade={trade ?? lastTrade} />
|
||||||
<AdvancedSwapDetails {...rest} onDismiss={() => setShowAdvanced(false)} />
|
</AdvancedDetailsFooter>
|
||||||
) : (
|
|
||||||
<CursorPointer>
|
|
||||||
<RowBetween onClick={() => setShowAdvanced(true)} padding={'8px 20px'} id="show-advanced">
|
|
||||||
<Text fontSize={16} fontWeight={500} style={{ userSelect: 'none' }}>
|
|
||||||
Show Advanced
|
|
||||||
</Text>
|
|
||||||
<ChevronDown color={theme.text2} />
|
|
||||||
</RowBetween>
|
|
||||||
</CursorPointer>
|
|
||||||
)}
|
|
||||||
</AdvancedDropdown>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -37,6 +37,11 @@ export default function SwapModalFooter({
|
|||||||
confirmText: string
|
confirmText: string
|
||||||
}) {
|
}) {
|
||||||
const theme = useContext(ThemeContext)
|
const theme = useContext(ThemeContext)
|
||||||
|
|
||||||
|
if (!trade) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AutoColumn gap="0px">
|
<AutoColumn gap="0px">
|
||||||
|
|||||||
@@ -5,35 +5,39 @@ import { Text } from 'rebass'
|
|||||||
import { ThemeContext } from 'styled-components'
|
import { ThemeContext } from 'styled-components'
|
||||||
import { Field } from '../../state/swap/actions'
|
import { Field } from '../../state/swap/actions'
|
||||||
import { TYPE } from '../../theme'
|
import { TYPE } from '../../theme'
|
||||||
|
import { isAddress, shortenAddress } from '../../utils'
|
||||||
import { AutoColumn } from '../Column'
|
import { AutoColumn } from '../Column'
|
||||||
import { RowBetween, RowFixed } from '../Row'
|
import { RowBetween, RowFixed } from '../Row'
|
||||||
import TokenLogo from '../TokenLogo'
|
import TokenLogo from '../TokenLogo'
|
||||||
import { TruncatedText } from './styleds'
|
import { TruncatedText } from './styleds'
|
||||||
|
|
||||||
export default function SwapModalHeader({
|
export default function SwapModalHeader({
|
||||||
formattedAmounts,
|
|
||||||
tokens,
|
tokens,
|
||||||
|
formattedAmounts,
|
||||||
slippageAdjustedAmounts,
|
slippageAdjustedAmounts,
|
||||||
priceImpactSeverity,
|
priceImpactSeverity,
|
||||||
independentField
|
independentField,
|
||||||
|
recipient
|
||||||
}: {
|
}: {
|
||||||
formattedAmounts?: { [field in Field]?: string }
|
tokens: { [field in Field]?: Token }
|
||||||
tokens?: { [field in Field]?: Token }
|
formattedAmounts: { [field in Field]?: string }
|
||||||
slippageAdjustedAmounts?: { [field in Field]?: TokenAmount }
|
slippageAdjustedAmounts: { [field in Field]?: TokenAmount }
|
||||||
priceImpactSeverity: number
|
priceImpactSeverity: number
|
||||||
independentField: Field
|
independentField: Field
|
||||||
|
recipient: string | null
|
||||||
}) {
|
}) {
|
||||||
const theme = useContext(ThemeContext)
|
const theme = useContext(ThemeContext)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AutoColumn gap={'md'} style={{ marginTop: '20px' }}>
|
<AutoColumn gap={'md'} style={{ marginTop: '20px' }}>
|
||||||
<RowBetween align="flex-end">
|
<RowBetween align="flex-end">
|
||||||
<TruncatedText fontSize={24} fontWeight={500}>
|
<TruncatedText fontSize={24} fontWeight={500}>
|
||||||
{!!formattedAmounts[Field.INPUT] && formattedAmounts[Field.INPUT]}
|
{formattedAmounts[Field.INPUT]}
|
||||||
</TruncatedText>
|
</TruncatedText>
|
||||||
<RowFixed gap="4px">
|
<RowFixed gap="4px">
|
||||||
<TokenLogo address={tokens[Field.INPUT]?.address} size={'24px'} />
|
<TokenLogo address={tokens[Field.INPUT]?.address} size={'24px'} />
|
||||||
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
|
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
|
||||||
{tokens[Field.INPUT]?.symbol || ''}
|
{tokens[Field.INPUT]?.symbol}
|
||||||
</Text>
|
</Text>
|
||||||
</RowFixed>
|
</RowFixed>
|
||||||
</RowBetween>
|
</RowBetween>
|
||||||
@@ -42,12 +46,12 @@ export default function SwapModalHeader({
|
|||||||
</RowFixed>
|
</RowFixed>
|
||||||
<RowBetween align="flex-end">
|
<RowBetween align="flex-end">
|
||||||
<TruncatedText fontSize={24} fontWeight={500} color={priceImpactSeverity > 2 ? theme.red1 : ''}>
|
<TruncatedText fontSize={24} fontWeight={500} color={priceImpactSeverity > 2 ? theme.red1 : ''}>
|
||||||
{!!formattedAmounts[Field.OUTPUT] && formattedAmounts[Field.OUTPUT]}
|
{formattedAmounts[Field.OUTPUT]}
|
||||||
</TruncatedText>
|
</TruncatedText>
|
||||||
<RowFixed gap="4px">
|
<RowFixed gap="4px">
|
||||||
<TokenLogo address={tokens[Field.OUTPUT]?.address} size={'24px'} />
|
<TokenLogo address={tokens[Field.OUTPUT]?.address} size={'24px'} />
|
||||||
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
|
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
|
||||||
{tokens[Field.OUTPUT]?.symbol || ''}
|
{tokens[Field.OUTPUT]?.symbol}
|
||||||
</Text>
|
</Text>
|
||||||
</RowFixed>
|
</RowFixed>
|
||||||
</RowBetween>
|
</RowBetween>
|
||||||
@@ -56,7 +60,7 @@ export default function SwapModalHeader({
|
|||||||
<TYPE.italic textAlign="left" style={{ width: '100%' }}>
|
<TYPE.italic textAlign="left" style={{ width: '100%' }}>
|
||||||
{`Output is estimated. You will receive at least `}
|
{`Output is estimated. You will receive at least `}
|
||||||
<b>
|
<b>
|
||||||
{slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} {tokens[Field.OUTPUT]?.symbol}{' '}
|
{slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} {tokens[Field.OUTPUT]?.symbol}
|
||||||
</b>
|
</b>
|
||||||
{' or the transaction will revert.'}
|
{' or the transaction will revert.'}
|
||||||
</TYPE.italic>
|
</TYPE.italic>
|
||||||
@@ -70,6 +74,14 @@ export default function SwapModalHeader({
|
|||||||
</TYPE.italic>
|
</TYPE.italic>
|
||||||
)}
|
)}
|
||||||
</AutoColumn>
|
</AutoColumn>
|
||||||
|
{recipient !== null ? (
|
||||||
|
<AutoColumn justify="flex-start" gap="sm" style={{ padding: '12px 0 0 0px' }}>
|
||||||
|
<TYPE.main>
|
||||||
|
Output will be sent to{' '}
|
||||||
|
<b title={recipient}>{isAddress(recipient) ? shortenAddress(recipient) : recipient}</b>
|
||||||
|
</TYPE.main>
|
||||||
|
</AutoColumn>
|
||||||
|
) : null}
|
||||||
</AutoColumn>
|
</AutoColumn>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
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 React from 'react'
|
||||||
import { Trade } from '@uniswap/sdk'
|
import { Price, Token } from '@uniswap/sdk'
|
||||||
import { useContext } from 'react'
|
import { useContext } from 'react'
|
||||||
import { Repeat } from 'react-feather'
|
import { Repeat } from 'react-feather'
|
||||||
import { Text } from 'rebass'
|
import { Text } from 'rebass'
|
||||||
@@ -7,20 +7,19 @@ import { ThemeContext } from 'styled-components'
|
|||||||
import { StyledBalanceMaxMini } from './styleds'
|
import { StyledBalanceMaxMini } from './styleds'
|
||||||
|
|
||||||
interface TradePriceProps {
|
interface TradePriceProps {
|
||||||
trade?: Trade
|
price?: Price
|
||||||
|
inputToken?: Token
|
||||||
|
outputToken?: Token
|
||||||
showInverted: boolean
|
showInverted: boolean
|
||||||
setShowInverted: (showInverted: boolean) => void
|
setShowInverted: (showInverted: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TradePrice({ trade, showInverted, setShowInverted }: TradePriceProps) {
|
export default function TradePrice({ price, inputToken, outputToken, showInverted, setShowInverted }: TradePriceProps) {
|
||||||
const theme = useContext(ThemeContext)
|
const theme = useContext(ThemeContext)
|
||||||
const inputToken = trade?.inputAmount?.token
|
|
||||||
const outputToken = trade?.outputAmount?.token
|
|
||||||
|
|
||||||
const price = showInverted
|
const formattedPrice = showInverted ? price?.toSignificant(6) : price?.invert()?.toSignificant(6)
|
||||||
? trade?.executionPrice?.toSignificant(6)
|
|
||||||
: trade?.executionPrice?.invert()?.toSignificant(6)
|
|
||||||
|
|
||||||
|
const show = Boolean(inputToken && outputToken)
|
||||||
const label = showInverted
|
const label = showInverted
|
||||||
? `${outputToken?.symbol} per ${inputToken?.symbol}`
|
? `${outputToken?.symbol} per ${inputToken?.symbol}`
|
||||||
: `${inputToken?.symbol} per ${outputToken?.symbol}`
|
: `${inputToken?.symbol} per ${outputToken?.symbol}`
|
||||||
@@ -32,10 +31,16 @@ export default function TradePrice({ trade, showInverted, setShowInverted }: Tra
|
|||||||
color={theme.text2}
|
color={theme.text2}
|
||||||
style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }}
|
style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }}
|
||||||
>
|
>
|
||||||
{price && `${price} ${label}`}
|
{show ? (
|
||||||
<StyledBalanceMaxMini onClick={() => setShowInverted(!showInverted)}>
|
<>
|
||||||
<Repeat size={14} />
|
{formattedPrice ?? '-'} {label}
|
||||||
</StyledBalanceMaxMini>
|
<StyledBalanceMaxMini onClick={() => setShowInverted(!showInverted)}>
|
||||||
|
<Repeat size={14} />
|
||||||
|
</StyledBalanceMaxMini>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { AutoColumn } from '../Column'
|
||||||
import { Text } from 'rebass'
|
import { Text } from 'rebass'
|
||||||
|
|
||||||
@@ -8,30 +8,18 @@ export const Wrapper = styled.div`
|
|||||||
position: relative;
|
position: relative;
|
||||||
`
|
`
|
||||||
|
|
||||||
export const ArrowWrapper = styled.div`
|
export const ArrowWrapper = styled.div<{ clickable: boolean }>`
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
border-radius: 12px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
:hover {
|
${({ clickable }) =>
|
||||||
cursor: pointer;
|
clickable
|
||||||
opacity: 0.8;
|
? css`
|
||||||
}
|
:hover {
|
||||||
`
|
cursor: pointer;
|
||||||
|
opacity: 0.8;
|
||||||
export const AdvancedDropdown = styled.div`
|
}
|
||||||
padding-top: calc(10px + 2rem);
|
`
|
||||||
padding-bottom: 10px;
|
: null}
|
||||||
margin-top: -2rem;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 400px;
|
|
||||||
border-bottom-left-radius: 20px;
|
|
||||||
border-bottom-right-radius: 20px;
|
|
||||||
color: ${({ theme }) => theme.text2};
|
|
||||||
background-color: ${({ theme }) => theme.advancedBG};
|
|
||||||
z-index: -1;
|
|
||||||
`
|
`
|
||||||
|
|
||||||
export const SectionBreak = styled.div`
|
export const SectionBreak = styled.div`
|
||||||
@@ -45,9 +33,15 @@ export const BottomGrouping = styled.div`
|
|||||||
position: relative;
|
position: relative;
|
||||||
`
|
`
|
||||||
|
|
||||||
export const ErrorText = styled(Text)<{ severity?: 0 | 1 | 2 | 3 }>`
|
export const ErrorText = styled(Text)<{ severity?: 0 | 1 | 2 | 3 | 4 }>`
|
||||||
color: ${({ theme, severity }) =>
|
color: ${({ theme, severity }) =>
|
||||||
severity === 3 ? theme.red1 : severity === 2 ? theme.yellow2 : severity === 1 ? theme.text1 : theme.green1};
|
severity === 3 || severity === 4
|
||||||
|
? theme.red1
|
||||||
|
: severity === 2
|
||||||
|
? theme.yellow2
|
||||||
|
: severity === 1
|
||||||
|
? theme.text1
|
||||||
|
: theme.green1};
|
||||||
`
|
`
|
||||||
|
|
||||||
export const InputGroup = styled(AutoColumn)`
|
export const InputGroup = styled(AutoColumn)`
|
||||||
@@ -65,7 +59,7 @@ export const StyledNumerical = styled(NumericalInput)`
|
|||||||
color: ${({ theme }) => theme.text4};
|
color: ${({ theme }) => theme.text4};
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
export const StyledBalanceMaxMini = styled.button<{ active?: boolean }>`
|
export const StyledBalanceMaxMini = styled.button`
|
||||||
height: 22px;
|
height: 22px;
|
||||||
width: 22px;
|
width: 22px;
|
||||||
background-color: ${({ theme }) => theme.bg2};
|
background-color: ${({ theme }) => theme.bg2};
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const injected = new InjectedConnector({
|
|||||||
export const walletconnect = new WalletConnectConnector({
|
export const walletconnect = new WalletConnectConnector({
|
||||||
rpc: { 1: NETWORK_URL },
|
rpc: { 1: NETWORK_URL },
|
||||||
bridge: 'https://bridge.walletconnect.org',
|
bridge: 'https://bridge.walletconnect.org',
|
||||||
qrcode: false,
|
qrcode: true,
|
||||||
pollingInterval: POLLING_INTERVAL
|
pollingInterval: POLLING_INTERVAL
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { Interface } from '@ethersproject/abi'
|
import { Interface } from '@ethersproject/abi'
|
||||||
import ERC20_ABI from './erc20.json'
|
import ERC20_ABI from './erc20.json'
|
||||||
|
import ERC20_BYTES32_ABI from './erc20_bytes32.json'
|
||||||
|
|
||||||
const ERC20_INTERFACE = new Interface(ERC20_ABI)
|
const ERC20_INTERFACE = new Interface(ERC20_ABI)
|
||||||
|
|
||||||
|
const ERC20_BYTES32_INTERFACE = new Interface(ERC20_BYTES32_ABI)
|
||||||
|
|
||||||
export default ERC20_INTERFACE
|
export default ERC20_INTERFACE
|
||||||
|
export { ERC20_ABI, ERC20_BYTES32_INTERFACE, ERC20_BYTES32_ABI }
|
||||||
|
|||||||
@@ -3,56 +3,12 @@
|
|||||||
"constant": true,
|
"constant": true,
|
||||||
"inputs": [],
|
"inputs": [],
|
||||||
"name": "name",
|
"name": "name",
|
||||||
"outputs": [{ "name": "", "type": "bytes32" }],
|
"outputs": [
|
||||||
"payable": false,
|
{
|
||||||
"stateMutability": "view",
|
"name": "",
|
||||||
"type": "function"
|
"type": "bytes32"
|
||||||
},
|
}
|
||||||
{
|
|
||||||
"constant": false,
|
|
||||||
"inputs": [{ "name": "_spender", "type": "address" }, { "name": "_value", "type": "uint256" }],
|
|
||||||
"name": "approve",
|
|
||||||
"outputs": [{ "name": "", "type": "bool" }],
|
|
||||||
"payable": false,
|
|
||||||
"stateMutability": "nonpayable",
|
|
||||||
"type": "function"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"constant": true,
|
|
||||||
"inputs": [],
|
|
||||||
"name": "totalSupply",
|
|
||||||
"outputs": [{ "name": "", "type": "uint256" }],
|
|
||||||
"payable": false,
|
|
||||||
"stateMutability": "view",
|
|
||||||
"type": "function"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"constant": false,
|
|
||||||
"inputs": [
|
|
||||||
{ "name": "_from", "type": "address" },
|
|
||||||
{ "name": "_to", "type": "address" },
|
|
||||||
{ "name": "_value", "type": "uint256" }
|
|
||||||
],
|
],
|
||||||
"name": "transferFrom",
|
|
||||||
"outputs": [{ "name": "", "type": "bool" }],
|
|
||||||
"payable": false,
|
|
||||||
"stateMutability": "nonpayable",
|
|
||||||
"type": "function"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"constant": true,
|
|
||||||
"inputs": [],
|
|
||||||
"name": "decimals",
|
|
||||||
"outputs": [{ "name": "", "type": "uint8" }],
|
|
||||||
"payable": false,
|
|
||||||
"stateMutability": "view",
|
|
||||||
"type": "function"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"constant": true,
|
|
||||||
"inputs": [{ "name": "_owner", "type": "address" }],
|
|
||||||
"name": "balanceOf",
|
|
||||||
"outputs": [{ "name": "balance", "type": "uint256" }],
|
|
||||||
"payable": false,
|
"payable": false,
|
||||||
"stateMutability": "view",
|
"stateMutability": "view",
|
||||||
"type": "function"
|
"type": "function"
|
||||||
@@ -61,48 +17,14 @@
|
|||||||
"constant": true,
|
"constant": true,
|
||||||
"inputs": [],
|
"inputs": [],
|
||||||
"name": "symbol",
|
"name": "symbol",
|
||||||
"outputs": [{ "name": "", "type": "bytes32" }],
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "bytes32"
|
||||||
|
}
|
||||||
|
],
|
||||||
"payable": false,
|
"payable": false,
|
||||||
"stateMutability": "view",
|
"stateMutability": "view",
|
||||||
"type": "function"
|
"type": "function"
|
||||||
},
|
|
||||||
{
|
|
||||||
"constant": false,
|
|
||||||
"inputs": [{ "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" }],
|
|
||||||
"name": "transfer",
|
|
||||||
"outputs": [{ "name": "", "type": "bool" }],
|
|
||||||
"payable": false,
|
|
||||||
"stateMutability": "nonpayable",
|
|
||||||
"type": "function"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"constant": true,
|
|
||||||
"inputs": [{ "name": "_owner", "type": "address" }, { "name": "_spender", "type": "address" }],
|
|
||||||
"name": "allowance",
|
|
||||||
"outputs": [{ "name": "", "type": "uint256" }],
|
|
||||||
"payable": false,
|
|
||||||
"stateMutability": "view",
|
|
||||||
"type": "function"
|
|
||||||
},
|
|
||||||
{ "payable": true, "stateMutability": "payable", "type": "fallback" },
|
|
||||||
{
|
|
||||||
"anonymous": false,
|
|
||||||
"inputs": [
|
|
||||||
{ "indexed": true, "name": "owner", "type": "address" },
|
|
||||||
{ "indexed": true, "name": "spender", "type": "address" },
|
|
||||||
{ "indexed": false, "name": "value", "type": "uint256" }
|
|
||||||
],
|
|
||||||
"name": "Approval",
|
|
||||||
"type": "event"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"anonymous": false,
|
|
||||||
"inputs": [
|
|
||||||
{ "indexed": true, "name": "from", "type": "address" },
|
|
||||||
{ "indexed": true, "name": "to", "type": "address" },
|
|
||||||
{ "indexed": false, "name": "value", "type": "uint256" }
|
|
||||||
],
|
|
||||||
"name": "Transfer",
|
|
||||||
"type": "event"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
55
src/constants/abis/migrator.json
Normal file
55
src/constants/abis/migrator.json
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "_factoryV1",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "_router",
|
||||||
|
"type": "address"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "constructor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "token",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "uint256",
|
||||||
|
"name": "amountTokenMin",
|
||||||
|
"type": "uint256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "uint256",
|
||||||
|
"name": "amountETHMin",
|
||||||
|
"type": "uint256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "to",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "uint256",
|
||||||
|
"name": "deadline",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "migrate",
|
||||||
|
"outputs": [],
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stateMutability": "payable",
|
||||||
|
"type": "receive"
|
||||||
|
}
|
||||||
|
]
|
||||||
5
src/constants/abis/migrator.ts
Normal file
5
src/constants/abis/migrator.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import MIGRATOR_ABI from './migrator.json'
|
||||||
|
|
||||||
|
const MIGRATOR_ADDRESS = '0x16D4F26C15f3658ec65B1126ff27DD3dF2a2996b'
|
||||||
|
|
||||||
|
export { MIGRATOR_ADDRESS, MIGRATOR_ABI }
|
||||||
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,25 +1,77 @@
|
|||||||
import { ChainId, JSBI, Percent, Token, WETH } from '@uniswap/sdk'
|
import { ChainId, JSBI, Percent, Token, WETH, Pair, TokenAmount } from '@uniswap/sdk'
|
||||||
|
|
||||||
import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors'
|
import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors'
|
||||||
|
import { COMP, DAI, MKR, USDC, USDT } from './tokens/mainnet'
|
||||||
|
|
||||||
export const ROUTER_ADDRESS = '0xf164fC0Ec4E93095b804a4795bBe1e041497b92a'
|
export const ROUTER_ADDRESS = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'
|
||||||
|
|
||||||
// used for display in the default list when adding liquidity
|
// a list of tokens by chain
|
||||||
export const COMMON_BASES = {
|
type ChainTokenList = {
|
||||||
[ChainId.MAINNET]: [
|
readonly [chainId in ChainId]: Token[]
|
||||||
WETH[ChainId.MAINNET],
|
}
|
||||||
new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'),
|
|
||||||
new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C')
|
const WETH_ONLY: ChainTokenList = {
|
||||||
],
|
[ChainId.MAINNET]: [WETH[ChainId.MAINNET]],
|
||||||
[ChainId.ROPSTEN]: [WETH[ChainId.ROPSTEN]],
|
[ChainId.ROPSTEN]: [WETH[ChainId.ROPSTEN]],
|
||||||
[ChainId.RINKEBY]: [
|
[ChainId.RINKEBY]: [WETH[ChainId.RINKEBY]],
|
||||||
WETH[ChainId.RINKEBY],
|
|
||||||
new Token(ChainId.RINKEBY, '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735', 18, 'DAI', 'Dai Stablecoin')
|
|
||||||
],
|
|
||||||
[ChainId.GÖRLI]: [WETH[ChainId.GÖRLI]],
|
[ChainId.GÖRLI]: [WETH[ChainId.GÖRLI]],
|
||||||
[ChainId.KOVAN]: [WETH[ChainId.KOVAN]]
|
[ChainId.KOVAN]: [WETH[ChainId.KOVAN]]
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAINNET_WALLETS = {
|
// used to construct intermediary pairs for trading
|
||||||
|
export const BASES_TO_CHECK_TRADES_AGAINST: ChainTokenList = {
|
||||||
|
...WETH_ONLY,
|
||||||
|
[ChainId.MAINNET]: [...WETH_ONLY[ChainId.MAINNET], DAI, USDC, USDT, COMP, MKR]
|
||||||
|
}
|
||||||
|
|
||||||
|
// used for display in the default list when adding liquidity
|
||||||
|
export const SUGGESTED_BASES: ChainTokenList = {
|
||||||
|
...WETH_ONLY,
|
||||||
|
[ChainId.MAINNET]: [...WETH_ONLY[ChainId.MAINNET], DAI, USDC, USDT]
|
||||||
|
}
|
||||||
|
|
||||||
|
// used to construct the list of all pairs we consider by default in the frontend
|
||||||
|
export const BASES_TO_TRACK_LIQUIDITY_FOR: ChainTokenList = {
|
||||||
|
...WETH_ONLY,
|
||||||
|
[ChainId.MAINNET]: [...WETH_ONLY[ChainId.MAINNET], DAI, USDC, USDT]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DUMMY_PAIRS_TO_PIN: { readonly [chainId in ChainId]?: Pair[] } = {
|
||||||
|
[ChainId.MAINNET]: [
|
||||||
|
new Pair(
|
||||||
|
new TokenAmount(
|
||||||
|
new Token(ChainId.MAINNET, '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643', 8, 'cDAI', 'Compound Dai'),
|
||||||
|
'0'
|
||||||
|
),
|
||||||
|
new TokenAmount(
|
||||||
|
new Token(ChainId.MAINNET, '0x39AA39c021dfbaE8faC545936693aC917d5E7563', 8, 'cUSDC', 'Compound USD Coin'),
|
||||||
|
'0'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new Pair(
|
||||||
|
new TokenAmount(
|
||||||
|
new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C'),
|
||||||
|
'0'
|
||||||
|
),
|
||||||
|
new TokenAmount(
|
||||||
|
new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'Tether USD'),
|
||||||
|
'0'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new Pair(
|
||||||
|
new TokenAmount(
|
||||||
|
new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'),
|
||||||
|
'0'
|
||||||
|
),
|
||||||
|
new TokenAmount(
|
||||||
|
new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'Tether USD'),
|
||||||
|
'0'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const TESTNET_CAPABLE_WALLETS = {
|
||||||
INJECTED: {
|
INJECTED: {
|
||||||
connector: injected,
|
connector: injected,
|
||||||
name: 'Injected',
|
name: 'Injected',
|
||||||
@@ -41,9 +93,9 @@ const MAINNET_WALLETS = {
|
|||||||
|
|
||||||
export const SUPPORTED_WALLETS =
|
export const SUPPORTED_WALLETS =
|
||||||
process.env.REACT_APP_CHAIN_ID !== '1'
|
process.env.REACT_APP_CHAIN_ID !== '1'
|
||||||
? MAINNET_WALLETS
|
? TESTNET_CAPABLE_WALLETS
|
||||||
: {
|
: {
|
||||||
...MAINNET_WALLETS,
|
...TESTNET_CAPABLE_WALLETS,
|
||||||
...{
|
...{
|
||||||
WALLET_CONNECT: {
|
WALLET_CONNECT: {
|
||||||
connector: walletconnect,
|
connector: walletconnect,
|
||||||
@@ -51,7 +103,8 @@ export const SUPPORTED_WALLETS =
|
|||||||
iconName: 'walletConnectIcon.svg',
|
iconName: 'walletConnectIcon.svg',
|
||||||
description: 'Connect to Trust Wallet, Rainbow Wallet and more...',
|
description: 'Connect to Trust Wallet, Rainbow Wallet and more...',
|
||||||
href: null,
|
href: null,
|
||||||
color: '#4196FC'
|
color: '#4196FC',
|
||||||
|
mobile: true
|
||||||
},
|
},
|
||||||
WALLET_LINK: {
|
WALLET_LINK: {
|
||||||
connector: walletlink,
|
connector: walletlink,
|
||||||
@@ -70,15 +123,6 @@ export const SUPPORTED_WALLETS =
|
|||||||
mobile: true,
|
mobile: true,
|
||||||
mobileOnly: true
|
mobileOnly: true
|
||||||
},
|
},
|
||||||
TRUST_WALLET_LINK: {
|
|
||||||
name: 'Open in Trust Wallet',
|
|
||||||
iconName: 'trustWallet.png',
|
|
||||||
description: 'iOS and Android app.',
|
|
||||||
href: 'https://link.trustwallet.com/open_url?coin_id=60&url=https://uniswap.exchange/swap',
|
|
||||||
color: '#1C74CC',
|
|
||||||
mobile: true,
|
|
||||||
mobileOnly: true
|
|
||||||
},
|
|
||||||
FORTMATIC: {
|
FORTMATIC: {
|
||||||
connector: fortmatic,
|
connector: fortmatic,
|
||||||
name: 'Fortmatic',
|
name: 'Fortmatic',
|
||||||
@@ -112,12 +156,13 @@ export const ONE_BIPS = new Percent(JSBI.BigInt(1), JSBI.BigInt(10000))
|
|||||||
export const BIPS_BASE = JSBI.BigInt(10000)
|
export const BIPS_BASE = JSBI.BigInt(10000)
|
||||||
// used for warning states
|
// used for warning states
|
||||||
export const ALLOWED_PRICE_IMPACT_LOW: Percent = new Percent(JSBI.BigInt(100), BIPS_BASE) // 1%
|
export const ALLOWED_PRICE_IMPACT_LOW: Percent = new Percent(JSBI.BigInt(100), BIPS_BASE) // 1%
|
||||||
export const ALLOWED_PRICE_IMPACT_MEDIUM: Percent = new Percent(JSBI.BigInt(500), BIPS_BASE) // 5%
|
export const ALLOWED_PRICE_IMPACT_MEDIUM: Percent = new Percent(JSBI.BigInt(300), BIPS_BASE) // 3%
|
||||||
export const ALLOWED_PRICE_IMPACT_HIGH: Percent = new Percent(JSBI.BigInt(1000), BIPS_BASE) // 10%
|
export const ALLOWED_PRICE_IMPACT_HIGH: Percent = new Percent(JSBI.BigInt(500), BIPS_BASE) // 5%
|
||||||
|
|
||||||
// if the price slippage exceeds this number, force the user to type 'confirm' to execute
|
// if the price slippage exceeds this number, force the user to type 'confirm' to execute
|
||||||
export const PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN: Percent = new Percent(JSBI.BigInt(2500), BIPS_BASE) // 25%
|
export const PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN: Percent = new Percent(JSBI.BigInt(1000), BIPS_BASE) // 10%
|
||||||
|
// for non expert mode disable swaps above this
|
||||||
|
export const BLOCKED_PRICE_IMPACT_NON_EXPERT: Percent = new Percent(JSBI.BigInt(1500), BIPS_BASE) // 15%
|
||||||
|
|
||||||
// used to ensure the user doesn't send so much ETH so they end up with <.01
|
// used to ensure the user doesn't send so much ETH so they end up with <.01
|
||||||
export const MIN_ETH: JSBI = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(16)) // .01 ETH
|
export const MIN_ETH: JSBI = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(16)) // .01 ETH
|
||||||
export const V1_TRADE_LINK_THRESHOLD = new Percent(JSBI.BigInt(75), JSBI.BigInt(10000))
|
export const BETTER_TRADE_LINK_THRESHOLD = new Percent(JSBI.BigInt(75), JSBI.BigInt(10000))
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { Token, ChainId } from '@uniswap/sdk'
|
import { Token, ChainId } from '@uniswap/sdk'
|
||||||
|
|
||||||
|
export const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin')
|
||||||
|
export const USDC = new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C')
|
||||||
|
export const USDT = new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'Tether USD')
|
||||||
|
export const COMP = new Token(ChainId.MAINNET, '0xc00e94Cb662C3520282E6f5717214004A7f26888', 18, 'COMP', 'Compound')
|
||||||
|
export const MKR = new Token(ChainId.MAINNET, '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', 18, 'MKR', 'Maker')
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
new Token(ChainId.MAINNET, '0xB6eD7644C69416d67B522e20bC294A9a9B405B31', 8, '0xBTC', '0xBitcoin Token'),
|
new Token(ChainId.MAINNET, '0xB6eD7644C69416d67B522e20bC294A9a9B405B31', 8, '0xBTC', '0xBitcoin Token'),
|
||||||
new Token(ChainId.MAINNET, '0xfC1E690f61EFd961294b3e1Ce3313fBD8aa4f85d', 18, 'aDAI', 'Aave Interest bearing DAI'),
|
new Token(ChainId.MAINNET, '0xfC1E690f61EFd961294b3e1Ce3313fBD8aa4f85d', 18, 'aDAI', 'Aave Interest bearing DAI'),
|
||||||
@@ -7,19 +13,23 @@ export default [
|
|||||||
new Token(ChainId.MAINNET, '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 9, 'AMPL', 'Ampleforth'),
|
new Token(ChainId.MAINNET, '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 9, 'AMPL', 'Ampleforth'),
|
||||||
new Token(ChainId.MAINNET, '0xcD62b1C403fa761BAadFC74C525ce2B51780b184', 18, 'ANJ', 'Aragon Network Juror'),
|
new Token(ChainId.MAINNET, '0xcD62b1C403fa761BAadFC74C525ce2B51780b184', 18, 'ANJ', 'Aragon Network Juror'),
|
||||||
new Token(ChainId.MAINNET, '0x960b236A07cf122663c4303350609A66A7B288C0', 18, 'ANT', 'Aragon Network Token'),
|
new Token(ChainId.MAINNET, '0x960b236A07cf122663c4303350609A66A7B288C0', 18, 'ANT', 'Aragon Network Token'),
|
||||||
|
new Token(ChainId.MAINNET, '0x27054b13b1B798B345b591a4d22e6562d47eA75a', 4, 'AST', 'AirSwap Token'),
|
||||||
new Token(ChainId.MAINNET, '0xBA11D00c5f74255f56a5E366F4F77f5A186d7f55', 18, 'BAND', 'BandToken'),
|
new Token(ChainId.MAINNET, '0xBA11D00c5f74255f56a5E366F4F77f5A186d7f55', 18, 'BAND', 'BandToken'),
|
||||||
new Token(ChainId.MAINNET, '0x0D8775F648430679A709E98d2b0Cb6250d2887EF', 18, 'BAT', 'Basic Attention Token'),
|
new Token(ChainId.MAINNET, '0x0D8775F648430679A709E98d2b0Cb6250d2887EF', 18, 'BAT', 'Basic Attention Token'),
|
||||||
|
new Token(ChainId.MAINNET, '0xba100000625a3754423978a60c9317c58a424e3D', 18, 'BAL', 'Balancer'),
|
||||||
new Token(ChainId.MAINNET, '0x107c4504cd79C5d2696Ea0030a8dD4e92601B82e', 18, 'BLT', 'Bloom Token'),
|
new Token(ChainId.MAINNET, '0x107c4504cd79C5d2696Ea0030a8dD4e92601B82e', 18, 'BLT', 'Bloom Token'),
|
||||||
new Token(ChainId.MAINNET, '0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C', 18, 'BNT', 'Bancor Network Token'),
|
new Token(ChainId.MAINNET, '0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C', 18, 'BNT', 'Bancor Network Token'),
|
||||||
new Token(ChainId.MAINNET, '0x0327112423F3A68efdF1fcF402F6c5CB9f7C33fd', 18, 'BTC++', 'PieDAO BTC++'),
|
new Token(ChainId.MAINNET, '0x0327112423F3A68efdF1fcF402F6c5CB9f7C33fd', 18, 'BTC++', 'PieDAO BTC++'),
|
||||||
|
new Token(ChainId.MAINNET, '0x56d811088235F11C8920698a204A5010a788f4b3', 18, 'BZRX', 'bZx Protocol Token'),
|
||||||
new Token(ChainId.MAINNET, '0x4F9254C83EB525f9FCf346490bbb3ed28a81C667', 18, 'CELR', 'CelerToken'),
|
new Token(ChainId.MAINNET, '0x4F9254C83EB525f9FCf346490bbb3ed28a81C667', 18, 'CELR', 'CelerToken'),
|
||||||
new Token(ChainId.MAINNET, '0xF5DCe57282A584D2746FaF1593d3121Fcac444dC', 8, 'cSAI', 'Compound Dai'),
|
new Token(ChainId.MAINNET, '0xF5DCe57282A584D2746FaF1593d3121Fcac444dC', 8, 'cSAI', 'Compound Dai'),
|
||||||
new Token(ChainId.MAINNET, '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643', 8, 'cDAI', 'Compound Dai'),
|
new Token(ChainId.MAINNET, '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643', 8, 'cDAI', 'Compound Dai'),
|
||||||
new Token(ChainId.MAINNET, '0x39AA39c021dfbaE8faC545936693aC917d5E7563', 8, 'cUSDC', 'Compound USD Coin'),
|
new Token(ChainId.MAINNET, '0x39AA39c021dfbaE8faC545936693aC917d5E7563', 8, 'cUSDC', 'Compound USD Coin'),
|
||||||
new Token(ChainId.MAINNET, '0xaaAEBE6Fe48E54f431b0C390CfaF0b017d09D42d', 4, 'CEL', 'Celsius'),
|
new Token(ChainId.MAINNET, '0xaaAEBE6Fe48E54f431b0C390CfaF0b017d09D42d', 4, 'CEL', 'Celsius'),
|
||||||
new Token(ChainId.MAINNET, '0x06AF07097C9Eeb7fD685c692751D5C66dB49c215', 18, 'CHAI', 'Chai'),
|
new Token(ChainId.MAINNET, '0x06AF07097C9Eeb7fD685c692751D5C66dB49c215', 18, 'CHAI', 'Chai'),
|
||||||
|
COMP,
|
||||||
new Token(ChainId.MAINNET, '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359', 18, 'SAI', 'Dai Stablecoin v1.0 (SAI)'),
|
new Token(ChainId.MAINNET, '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359', 18, 'SAI', 'Dai Stablecoin v1.0 (SAI)'),
|
||||||
new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'),
|
DAI,
|
||||||
new Token(ChainId.MAINNET, '0x0Cf0Ee63788A0849fE5297F3407f701E122cC023', 18, 'DATA', 'Streamr DATAcoin'),
|
new Token(ChainId.MAINNET, '0x0Cf0Ee63788A0849fE5297F3407f701E122cC023', 18, 'DATA', 'Streamr DATAcoin'),
|
||||||
new Token(ChainId.MAINNET, '0xE0B7927c4aF23765Cb51314A0E0521A9645F0E2A', 9, 'DGD', 'DigixDAO'),
|
new Token(ChainId.MAINNET, '0xE0B7927c4aF23765Cb51314A0E0521A9645F0E2A', 9, 'DGD', 'DigixDAO'),
|
||||||
new Token(ChainId.MAINNET, '0x4f3AfEC4E5a3F2A6a1A411DEF7D7dFe50eE057bF', 9, 'DGX', 'Digix Gold Token'),
|
new Token(ChainId.MAINNET, '0x4f3AfEC4E5a3F2A6a1A411DEF7D7dFe50eE057bF', 9, 'DGX', 'Digix Gold Token'),
|
||||||
@@ -31,6 +41,7 @@ export default [
|
|||||||
'Decentralized Insurance Protocol'
|
'Decentralized Insurance Protocol'
|
||||||
),
|
),
|
||||||
new Token(ChainId.MAINNET, '0xC0F9bD5Fa5698B6505F643900FFA515Ea5dF54A9', 18, 'DONUT', 'Donut'),
|
new Token(ChainId.MAINNET, '0xC0F9bD5Fa5698B6505F643900FFA515Ea5dF54A9', 18, 'DONUT', 'Donut'),
|
||||||
|
new Token(ChainId.MAINNET, '0x86FADb80d8D2cff3C3680819E4da99C10232Ba0F', 18, 'EBASE', 'EURBASE Stablecoin'),
|
||||||
new Token(ChainId.MAINNET, '0xF629cBd94d3791C9250152BD8dfBDF380E2a3B9c', 18, 'ENJ', 'Enjin Coin'),
|
new Token(ChainId.MAINNET, '0xF629cBd94d3791C9250152BD8dfBDF380E2a3B9c', 18, 'ENJ', 'Enjin Coin'),
|
||||||
new Token(ChainId.MAINNET, '0x06f65b8CfCb13a9FE37d836fE9708dA38Ecb29B2', 18, 'FAME', 'SAINT FAME: Genesis Shirt'),
|
new Token(ChainId.MAINNET, '0x06f65b8CfCb13a9FE37d836fE9708dA38Ecb29B2', 18, 'FAME', 'SAINT FAME: Genesis Shirt'),
|
||||||
new Token(ChainId.MAINNET, '0x4946Fcea7C692606e8908002e55A582af44AC121', 18, 'FOAM', 'FOAM Token'),
|
new Token(ChainId.MAINNET, '0x4946Fcea7C692606e8908002e55A582af44AC121', 18, 'FOAM', 'FOAM Token'),
|
||||||
@@ -59,9 +70,10 @@ export default [
|
|||||||
new Token(ChainId.MAINNET, '0xd15eCDCF5Ea68e3995b2D0527A0aE0a3258302F8', 18, 'MCX', 'MachiX Token'),
|
new Token(ChainId.MAINNET, '0xd15eCDCF5Ea68e3995b2D0527A0aE0a3258302F8', 18, 'MCX', 'MachiX Token'),
|
||||||
new Token(ChainId.MAINNET, '0xa3d58c4E56fedCae3a7c43A725aeE9A71F0ece4e', 18, 'MET', 'Metronome'),
|
new Token(ChainId.MAINNET, '0xa3d58c4E56fedCae3a7c43A725aeE9A71F0ece4e', 18, 'MET', 'Metronome'),
|
||||||
new Token(ChainId.MAINNET, '0x80f222a749a2e18Eb7f676D371F19ad7EFEEe3b7', 18, 'MGN', 'Magnolia Token'),
|
new Token(ChainId.MAINNET, '0x80f222a749a2e18Eb7f676D371F19ad7EFEEe3b7', 18, 'MGN', 'Magnolia Token'),
|
||||||
new Token(ChainId.MAINNET, '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', 18, 'MKR', 'Maker'),
|
MKR,
|
||||||
new Token(ChainId.MAINNET, '0xec67005c4E498Ec7f55E092bd1d35cbC47C91892', 18, 'MLN', 'Melon Token'),
|
new Token(ChainId.MAINNET, '0xec67005c4E498Ec7f55E092bd1d35cbC47C91892', 18, 'MLN', 'Melon Token'),
|
||||||
new Token(ChainId.MAINNET, '0x957c30aB0426e0C93CD8241E2c60392d08c6aC8e', 0, 'MOD', 'Modum Token'),
|
new Token(ChainId.MAINNET, '0x957c30aB0426e0C93CD8241E2c60392d08c6aC8e', 0, 'MOD', 'Modum Token'),
|
||||||
|
new Token(ChainId.MAINNET, '0xe2f2a5C287993345a840Db3B0845fbC70f5935a5', 18, 'mUSD', 'mStable USD'),
|
||||||
new Token(ChainId.MAINNET, '0xB62132e35a6c13ee1EE0f84dC5d40bad8d815206', 18, 'NEXO', 'Nexo'),
|
new Token(ChainId.MAINNET, '0xB62132e35a6c13ee1EE0f84dC5d40bad8d815206', 18, 'NEXO', 'Nexo'),
|
||||||
new Token(ChainId.MAINNET, '0x1776e1F26f98b1A5dF9cD347953a26dd3Cb46671', 18, 'NMR', 'Numeraire'),
|
new Token(ChainId.MAINNET, '0x1776e1F26f98b1A5dF9cD347953a26dd3Cb46671', 18, 'NMR', 'Numeraire'),
|
||||||
new Token(ChainId.MAINNET, '0x985dd3D42De1e256d09e1c10F112bCCB8015AD41', 18, 'OCEAN', 'OceanToken'),
|
new Token(ChainId.MAINNET, '0x985dd3D42De1e256d09e1c10F112bCCB8015AD41', 18, 'OCEAN', 'OceanToken'),
|
||||||
@@ -77,7 +89,10 @@ export default [
|
|||||||
new Token(ChainId.MAINNET, '0xF970b8E36e23F7fC3FD752EeA86f8Be8D83375A6', 18, 'RCN', 'Ripio Credit Network Token'),
|
new Token(ChainId.MAINNET, '0xF970b8E36e23F7fC3FD752EeA86f8Be8D83375A6', 18, 'RCN', 'Ripio Credit Network Token'),
|
||||||
new Token(ChainId.MAINNET, '0x255Aa6DF07540Cb5d3d297f0D0D4D84cb52bc8e6', 18, 'RDN', 'Raiden Token'),
|
new Token(ChainId.MAINNET, '0x255Aa6DF07540Cb5d3d297f0D0D4D84cb52bc8e6', 18, 'RDN', 'Raiden Token'),
|
||||||
new Token(ChainId.MAINNET, '0x408e41876cCCDC0F92210600ef50372656052a38', 18, 'REN', 'Republic Token'),
|
new Token(ChainId.MAINNET, '0x408e41876cCCDC0F92210600ef50372656052a38', 18, 'REN', 'Republic Token'),
|
||||||
new Token(ChainId.MAINNET, '0x1985365e9f78359a9B6AD760e32412f4a445E862', 18, 'REP', 'Reputation'),
|
new Token(ChainId.MAINNET, '0x459086F2376525BdCebA5bDDA135e4E9d3FeF5bf', 8, 'renBCH', 'renBCH'),
|
||||||
|
new Token(ChainId.MAINNET, '0xEB4C2781e4ebA804CE9a9803C67d0893436bB27D', 8, 'renBTC', 'renBTC'),
|
||||||
|
new Token(ChainId.MAINNET, '0x1C5db575E2Ff833E46a2E9864C22F4B22E0B37C2', 8, 'renZEC', 'renZEC'),
|
||||||
|
new Token(ChainId.MAINNET, '0x1985365e9f78359a9B6AD760e32412f4a445E862', 18, 'REPv1', 'Augur v1 Reputation'),
|
||||||
new Token(ChainId.MAINNET, '0x9469D013805bFfB7D3DEBe5E7839237e535ec483', 18, 'RING', 'Darwinia Network Native Token'),
|
new Token(ChainId.MAINNET, '0x9469D013805bFfB7D3DEBe5E7839237e535ec483', 18, 'RING', 'Darwinia Network Native Token'),
|
||||||
new Token(ChainId.MAINNET, '0x607F4C5BB672230e8672085532f7e901544a7375', 9, 'RLC', 'iEx.ec Network Token'),
|
new Token(ChainId.MAINNET, '0x607F4C5BB672230e8672085532f7e901544a7375', 9, 'RLC', 'iEx.ec Network Token'),
|
||||||
new Token(ChainId.MAINNET, '0xB4EFd85c19999D84251304bDA99E90B92300Bd93', 18, 'RPL', 'Rocket Pool'),
|
new Token(ChainId.MAINNET, '0xB4EFd85c19999D84251304bDA99E90B92300Bd93', 18, 'RPL', 'Rocket Pool'),
|
||||||
@@ -89,6 +104,7 @@ export default [
|
|||||||
new Token(ChainId.MAINNET, '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F', 18, 'SNX', 'Synthetix Network Token'),
|
new Token(ChainId.MAINNET, '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F', 18, 'SNX', 'Synthetix Network Token'),
|
||||||
new Token(ChainId.MAINNET, '0x23B608675a2B2fB1890d3ABBd85c5775c51691d5', 18, 'SOCKS', 'Unisocks Edition 0'),
|
new Token(ChainId.MAINNET, '0x23B608675a2B2fB1890d3ABBd85c5775c51691d5', 18, 'SOCKS', 'Unisocks Edition 0'),
|
||||||
new Token(ChainId.MAINNET, '0x42d6622deCe394b54999Fbd73D108123806f6a18', 18, 'SPANK', 'SPANK'),
|
new Token(ChainId.MAINNET, '0x42d6622deCe394b54999Fbd73D108123806f6a18', 18, 'SPANK', 'SPANK'),
|
||||||
|
new Token(ChainId.MAINNET, '0x0Ae055097C6d159879521C384F1D2123D1f195e6', 18, 'STAKE', 'STAKE'),
|
||||||
new Token(ChainId.MAINNET, '0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC', 8, 'STORJ', 'StorjToken'),
|
new Token(ChainId.MAINNET, '0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC', 8, 'STORJ', 'StorjToken'),
|
||||||
new Token(ChainId.MAINNET, '0x57Ab1ec28D129707052df4dF418D58a2D46d5f51', 18, 'sUSD', 'Synth sUSD'),
|
new Token(ChainId.MAINNET, '0x57Ab1ec28D129707052df4dF418D58a2D46d5f51', 18, 'sUSD', 'Synth sUSD'),
|
||||||
new Token(ChainId.MAINNET, '0x261EfCdD24CeA98652B9700800a13DfBca4103fF', 18, 'sXAU', 'Synth sXAU'),
|
new Token(ChainId.MAINNET, '0x261EfCdD24CeA98652B9700800a13DfBca4103fF', 18, 'sXAU', 'Synth sXAU'),
|
||||||
@@ -104,10 +120,11 @@ export default [
|
|||||||
new Token(ChainId.MAINNET, '0x0000000000085d4780B73119b644AE5ecd22b376', 18, 'TUSD', 'TrueUSD'),
|
new Token(ChainId.MAINNET, '0x0000000000085d4780B73119b644AE5ecd22b376', 18, 'TUSD', 'TrueUSD'),
|
||||||
new Token(ChainId.MAINNET, '0x8400D94A5cb0fa0D041a3788e395285d61c9ee5e', 8, 'UBT', 'UniBright'),
|
new Token(ChainId.MAINNET, '0x8400D94A5cb0fa0D041a3788e395285d61c9ee5e', 8, 'UBT', 'UniBright'),
|
||||||
new Token(ChainId.MAINNET, '0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828', 18, 'UMA', 'UMA Voting Token v1'),
|
new Token(ChainId.MAINNET, '0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828', 18, 'UMA', 'UMA Voting Token v1'),
|
||||||
new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C'),
|
USDC,
|
||||||
new Token(ChainId.MAINNET, '0xA4Bdb11dc0a2bEC88d24A3aa1E6Bb17201112eBe', 6, 'USDS', 'StableUSD'),
|
new Token(ChainId.MAINNET, '0xA4Bdb11dc0a2bEC88d24A3aa1E6Bb17201112eBe', 6, 'USDS', 'StableUSD'),
|
||||||
new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'Tether USD'),
|
USDT,
|
||||||
new Token(ChainId.MAINNET, '0xeb269732ab75A6fD61Ea60b06fE994cD32a83549', 18, 'USDx', 'dForce'),
|
new Token(ChainId.MAINNET, '0xeb269732ab75A6fD61Ea60b06fE994cD32a83549', 18, 'USDx', 'dForce'),
|
||||||
|
new Token(ChainId.MAINNET, '0x9A48BD0EC040ea4f1D3147C025cd4076A2e71e3e', 18, 'USD++', 'PieDAO USD++'),
|
||||||
new Token(ChainId.MAINNET, '0x8f3470A7388c05eE4e7AF3d01D8C722b0FF52374', 18, 'VERI', 'Veritaseum'),
|
new Token(ChainId.MAINNET, '0x8f3470A7388c05eE4e7AF3d01D8C722b0FF52374', 18, 'VERI', 'Veritaseum'),
|
||||||
new Token(ChainId.MAINNET, '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', 8, 'WBTC', 'Wrapped BTC'),
|
new Token(ChainId.MAINNET, '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', 8, 'WBTC', 'Wrapped BTC'),
|
||||||
new Token(ChainId.MAINNET, '0x09fE5f0236F0Ea5D930197DCE254d77B04128075', 18, 'WCK', 'Wrapped CryptoKitties'),
|
new Token(ChainId.MAINNET, '0x09fE5f0236F0Ea5D930197DCE254d77B04128075', 18, 'WCK', 'Wrapped CryptoKitties'),
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
import { Interface } from '@ethersproject/abi'
|
import { Interface } from '@ethersproject/abi'
|
||||||
|
import { ChainId } from '@uniswap/sdk'
|
||||||
import V1_EXCHANGE_ABI from './v1_exchange.json'
|
import V1_EXCHANGE_ABI from './v1_exchange.json'
|
||||||
import V1_FACTORY_ABI from './v1_factory.json'
|
import V1_FACTORY_ABI from './v1_factory.json'
|
||||||
|
|
||||||
const V1_FACTORY_ADDRESS = '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'
|
const V1_FACTORY_ADDRESSES: { [chainId in ChainId]: string } = {
|
||||||
|
[ChainId.MAINNET]: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95',
|
||||||
|
[ChainId.ROPSTEN]: '0x9c83dCE8CA20E9aAF9D3efc003b2ea62aBC08351',
|
||||||
|
[ChainId.RINKEBY]: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36',
|
||||||
|
[ChainId.GÖRLI]: '0x6Ce570d02D73d4c384b46135E87f8C592A8c86dA',
|
||||||
|
[ChainId.KOVAN]: '0xD3E51Ef092B2845f10401a0159B2B96e8B6c3D30'
|
||||||
|
}
|
||||||
|
|
||||||
const V1_FACTORY_INTERFACE = new Interface(V1_FACTORY_ABI)
|
const V1_FACTORY_INTERFACE = new Interface(V1_FACTORY_ABI)
|
||||||
const V1_EXCHANGE_INTERFACE = new Interface(V1_EXCHANGE_ABI)
|
const V1_EXCHANGE_INTERFACE = new Interface(V1_EXCHANGE_ABI)
|
||||||
|
|
||||||
export { V1_FACTORY_ADDRESS, V1_FACTORY_INTERFACE, V1_FACTORY_ABI, V1_EXCHANGE_INTERFACE, V1_EXCHANGE_ABI }
|
export { V1_FACTORY_ADDRESSES, V1_FACTORY_INTERFACE, V1_FACTORY_ABI, V1_EXCHANGE_INTERFACE, V1_EXCHANGE_ABI }
|
||||||
|
|||||||
@@ -1,24 +1,43 @@
|
|||||||
import { Token, TokenAmount, Pair } from '@uniswap/sdk'
|
import { Token, TokenAmount, Pair } from '@uniswap/sdk'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
|
||||||
|
import { Interface } from '@ethersproject/abi'
|
||||||
|
|
||||||
import { usePairContract } from '../hooks/useContract'
|
import { useMultipleContractSingleData } from '../state/multicall/hooks'
|
||||||
import { useSingleCallResult } from '../state/multicall/hooks'
|
|
||||||
|
const PAIR_INTERFACE = new Interface(IUniswapV2PairABI)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* if loading, return undefined
|
* if loading, return undefined
|
||||||
* if no pair created yet, return null
|
* if no pair created yet, return null
|
||||||
* if pair already created (even if 0 reserves), return pair
|
* if pair already created (even if 0 reserves), return pair
|
||||||
*/
|
*/
|
||||||
export function usePair(tokenA?: Token, tokenB?: Token): undefined | Pair | null {
|
export function usePairs(tokens: [Token | undefined, Token | undefined][]): (undefined | Pair | null)[] {
|
||||||
const pairAddress = tokenA && tokenB && !tokenA.equals(tokenB) ? Pair.getAddress(tokenA, tokenB) : undefined
|
const pairAddresses = useMemo(
|
||||||
const contract = usePairContract(pairAddress, false)
|
() =>
|
||||||
const { result: reserves, loading } = useSingleCallResult(contract, 'getReserves')
|
tokens.map(([tokenA, tokenB]) => {
|
||||||
|
return tokenA && tokenB && !tokenA.equals(tokenB) ? Pair.getAddress(tokenA, tokenB) : undefined
|
||||||
|
}),
|
||||||
|
[tokens]
|
||||||
|
)
|
||||||
|
|
||||||
|
const results = useMultipleContractSingleData(pairAddresses, PAIR_INTERFACE, 'getReserves')
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
if (loading || !tokenA || !tokenB) return undefined
|
return results.map((result, i) => {
|
||||||
if (!reserves) return null
|
const { result: reserves, loading } = result
|
||||||
const { reserve0, reserve1 } = reserves
|
const tokenA = tokens[i][0]
|
||||||
const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]
|
const tokenB = tokens[i][1]
|
||||||
return new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString()))
|
|
||||||
}, [loading, reserves, tokenA, tokenB])
|
if (loading || !tokenA || !tokenB) return undefined
|
||||||
|
if (!reserves) return null
|
||||||
|
const { reserve0, reserve1 } = reserves
|
||||||
|
const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]
|
||||||
|
return new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString()))
|
||||||
|
})
|
||||||
|
}, [results, tokens])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePair(tokenA?: Token, tokenB?: Token): undefined | Pair | null {
|
||||||
|
return usePairs([[tokenA, tokenB]])[0]
|
||||||
}
|
}
|
||||||
|
|||||||
148
src/data/V1.ts
148
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 { useMemo } from 'react'
|
||||||
import { useActiveWeb3React } from '../hooks'
|
import { useActiveWeb3React } from '../hooks'
|
||||||
import { useAllTokens } from '../hooks/Tokens'
|
import { useAllTokens } from '../hooks/Tokens'
|
||||||
import { useV1FactoryContract } from '../hooks/useContract'
|
import { useV1FactoryContract } from '../hooks/useContract'
|
||||||
|
import { Version } from '../hooks/useToggledVersion'
|
||||||
import { NEVER_RELOAD, useSingleCallResult, useSingleContractMultipleData } from '../state/multicall/hooks'
|
import { NEVER_RELOAD, useSingleCallResult, useSingleContractMultipleData } from '../state/multicall/hooks'
|
||||||
import { useETHBalances, useTokenBalance, useTokenBalances } from '../state/wallet/hooks'
|
import { useETHBalances, useTokenBalance, useTokenBalances } from '../state/wallet/hooks'
|
||||||
|
import { AddressZero } from '@ethersproject/constants'
|
||||||
|
|
||||||
function useV1PairAddress(tokenAddress?: string): string | undefined {
|
export function useV1ExchangeAddress(tokenAddress?: string): string | undefined {
|
||||||
const contract = useV1FactoryContract()
|
const contract = useV1FactoryContract()
|
||||||
|
|
||||||
const inputs = useMemo(() => [tokenAddress], [tokenAddress])
|
const inputs = useMemo(() => [tokenAddress], [tokenAddress])
|
||||||
return useSingleCallResult(contract, 'getExchange', inputs)?.result?.[0]
|
return useSingleCallResult(contract, 'getExchange', inputs)?.result?.[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
class MockV1Pair extends Pair {
|
class MockV1Pair extends Pair {}
|
||||||
readonly isV1: true = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function useMockV1Pair(token?: Token): MockV1Pair | undefined {
|
function useMockV1Pair(token?: Token): MockV1Pair | undefined {
|
||||||
const isWETH = token?.equals(WETH[token?.chainId])
|
const isWETH: boolean = token && WETH[token.chainId] ? token.equals(WETH[token.chainId]) : false
|
||||||
|
|
||||||
// will only return an address on mainnet, and not for WETH
|
const v1PairAddress = useV1ExchangeAddress(isWETH ? undefined : token?.address)
|
||||||
const v1PairAddress = useV1PairAddress(isWETH ? undefined : token?.address)
|
|
||||||
const tokenBalance = useTokenBalance(v1PairAddress, token)
|
const tokenBalance = useTokenBalance(v1PairAddress, token)
|
||||||
const ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress ?? '']
|
const ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress ?? '']
|
||||||
|
|
||||||
@@ -30,42 +29,40 @@ function useMockV1Pair(token?: Token): MockV1Pair | undefined {
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns ALL v1 exchange addresses
|
|
||||||
export function useAllV1ExchangeAddresses(): string[] {
|
|
||||||
const factory = useV1FactoryContract()
|
|
||||||
const exchangeCount = useSingleCallResult(factory, 'tokenCount')?.result
|
|
||||||
|
|
||||||
const parsedCount = parseInt(exchangeCount?.toString() ?? '0')
|
|
||||||
|
|
||||||
const indices = useMemo(() => [...Array(parsedCount).keys()].map(ix => [ix]), [parsedCount])
|
|
||||||
const data = useSingleContractMultipleData(factory, 'getTokenWithId', indices, NEVER_RELOAD)
|
|
||||||
|
|
||||||
return useMemo(() => data?.map(({ result }) => result?.[0])?.filter(x => x) ?? [], [data])
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns all v1 exchange addresses in the user's token list
|
// returns all v1 exchange addresses in the user's token list
|
||||||
export function useAllTokenV1ExchangeAddresses(): string[] {
|
export function useAllTokenV1Exchanges(): { [exchangeAddress: string]: Token } {
|
||||||
const allTokens = useAllTokens()
|
const allTokens = useAllTokens()
|
||||||
const factory = useV1FactoryContract()
|
const factory = useV1FactoryContract()
|
||||||
const args = useMemo(() => Object.keys(allTokens).map(tokenAddress => [tokenAddress]), [allTokens])
|
const args = useMemo(() => Object.keys(allTokens).map(tokenAddress => [tokenAddress]), [allTokens])
|
||||||
|
|
||||||
const data = useSingleContractMultipleData(factory, 'getExchange', args, NEVER_RELOAD)
|
const data = useSingleContractMultipleData(factory, 'getExchange', args, NEVER_RELOAD)
|
||||||
|
|
||||||
return useMemo(() => data?.map(({ result }) => result?.[0])?.filter(x => x) ?? [], [data])
|
return useMemo(
|
||||||
|
() =>
|
||||||
|
data?.reduce<{ [exchangeAddress: string]: Token }>((memo, { result }, ix) => {
|
||||||
|
if (result?.[0] && result[0] !== AddressZero) {
|
||||||
|
const token = allTokens[args[ix][0]]
|
||||||
|
memo[result[0]] = token
|
||||||
|
}
|
||||||
|
return memo
|
||||||
|
}, {}) ?? {},
|
||||||
|
[allTokens, args, data]
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns whether any of the tokens in the user's token list have liquidity on v1
|
// returns whether any of the tokens in the user's token list have liquidity on v1
|
||||||
export function useUserProbablyHasV1Liquidity(): boolean | undefined {
|
export function useUserHasLiquidityInAllTokens(): boolean | undefined {
|
||||||
const exchangeAddresses = useAllTokenV1ExchangeAddresses()
|
|
||||||
|
|
||||||
const { account, chainId } = useActiveWeb3React()
|
const { account, chainId } = useActiveWeb3React()
|
||||||
|
|
||||||
const fakeTokens = useMemo(
|
const exchanges = useAllTokenV1Exchanges()
|
||||||
() => (chainId ? exchangeAddresses.map(address => new Token(chainId, address, 18, 'UNI-V1')) : []),
|
|
||||||
[chainId, exchangeAddresses]
|
const fakeLiquidityTokens = useMemo(
|
||||||
|
() =>
|
||||||
|
chainId ? Object.keys(exchanges).map(address => new Token(chainId, address, 18, 'UNI-V1', 'Uniswap V1')) : [],
|
||||||
|
[chainId, exchanges]
|
||||||
)
|
)
|
||||||
|
|
||||||
const balances = useTokenBalances(account ?? undefined, fakeTokens)
|
const balances = useTokenBalances(account ?? undefined, fakeLiquidityTokens)
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -77,24 +74,23 @@ export function useUserProbablyHasV1Liquidity(): boolean | undefined {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useV1TradeLinkIfBetter(
|
/**
|
||||||
|
* Returns the trade to execute on V1 to go between input and output token
|
||||||
|
*/
|
||||||
|
export function useV1Trade(
|
||||||
isExactIn?: boolean,
|
isExactIn?: boolean,
|
||||||
input?: Token,
|
inputToken?: Token,
|
||||||
output?: Token,
|
outputToken?: Token,
|
||||||
exactAmount?: TokenAmount,
|
exactAmount?: TokenAmount
|
||||||
v2Trade?: Trade,
|
): Trade | undefined {
|
||||||
minimumDelta: Percent = new Percent('0')
|
|
||||||
): string | undefined {
|
|
||||||
const { chainId } = useActiveWeb3React()
|
const { chainId } = useActiveWeb3React()
|
||||||
|
|
||||||
const isMainnet: boolean = chainId === ChainId.MAINNET
|
|
||||||
|
|
||||||
// get the mock v1 pairs
|
// get the mock v1 pairs
|
||||||
const inputPair = useMockV1Pair(input)
|
const inputPair = useMockV1Pair(inputToken)
|
||||||
const outputPair = useMockV1Pair(output)
|
const outputPair = useMockV1Pair(outputToken)
|
||||||
|
|
||||||
const inputIsWETH = isMainnet && input?.equals(WETH[ChainId.MAINNET])
|
const inputIsWETH = (inputToken && chainId && WETH[chainId] && inputToken.equals(WETH[chainId])) ?? false
|
||||||
const outputIsWETH = isMainnet && output?.equals(WETH[ChainId.MAINNET])
|
const outputIsWETH = (outputToken && chainId && WETH[chainId] && outputToken.equals(WETH[chainId])) ?? false
|
||||||
|
|
||||||
// construct a direct or through ETH v1 route
|
// construct a direct or through ETH v1 route
|
||||||
let pairs: Pair[] = []
|
let pairs: Pair[] = []
|
||||||
@@ -108,7 +104,7 @@ export function useV1TradeLinkIfBetter(
|
|||||||
pairs = [inputPair, outputPair]
|
pairs = [inputPair, outputPair]
|
||||||
}
|
}
|
||||||
|
|
||||||
const route = input && pairs && pairs.length > 0 && new Route(pairs, input)
|
const route = inputToken && pairs && pairs.length > 0 && new Route(pairs, inputToken)
|
||||||
let v1Trade: Trade | undefined
|
let v1Trade: Trade | undefined
|
||||||
try {
|
try {
|
||||||
v1Trade =
|
v1Trade =
|
||||||
@@ -116,25 +112,53 @@ export function useV1TradeLinkIfBetter(
|
|||||||
? new Trade(route, exactAmount, isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT)
|
? new Trade(route, exactAmount, isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT)
|
||||||
: undefined
|
: undefined
|
||||||
} catch {}
|
} catch {}
|
||||||
|
return v1Trade
|
||||||
|
}
|
||||||
|
|
||||||
let v1HasBetterTrade = false
|
export function getTradeVersion(trade?: Trade): Version | undefined {
|
||||||
if (v1Trade) {
|
const isV1 = trade?.route?.pairs?.some(pair => pair instanceof MockV1Pair)
|
||||||
if (isExactIn) {
|
if (isV1) return Version.v1
|
||||||
// discount the v1 output amount by minimumDelta
|
if (isV1 === false) return Version.v2
|
||||||
const discountedV1Output = v1Trade?.outputAmount.multiply(new Percent('1').subtract(minimumDelta))
|
return undefined
|
||||||
// check if the discounted v1 amount is still greater than v2, short-circuiting if no v2 trade exists
|
}
|
||||||
v1HasBetterTrade = !v2Trade || discountedV1Output.greaterThan(v2Trade.outputAmount)
|
|
||||||
} else {
|
// returns the v1 exchange against which a trade should be executed
|
||||||
// inflate the v1 amount by minimumDelta
|
export function useV1TradeExchangeAddress(trade: Trade | undefined): string | undefined {
|
||||||
const inflatedV1Input = v1Trade?.inputAmount.multiply(new Percent('1').add(minimumDelta))
|
const tokenAddress: string | undefined = useMemo(() => {
|
||||||
// check if the inflated v1 amount is still less than v2, short-circuiting if no v2 trade exists
|
const tradeVersion = getTradeVersion(trade)
|
||||||
v1HasBetterTrade = !v2Trade || inflatedV1Input.lessThan(v2Trade.inputAmount)
|
const isV1 = tradeVersion === Version.v1
|
||||||
}
|
return isV1
|
||||||
|
? trade &&
|
||||||
|
WETH[trade.inputAmount.token.chainId] &&
|
||||||
|
trade.inputAmount.token.equals(WETH[trade.inputAmount.token.chainId])
|
||||||
|
? trade.outputAmount.token.address
|
||||||
|
: trade?.inputAmount?.token?.address
|
||||||
|
: undefined
|
||||||
|
}, [trade])
|
||||||
|
return useV1ExchangeAddress(tokenAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ZERO_PERCENT = new Percent('0')
|
||||||
|
const ONE_HUNDRED_PERCENT = new Percent('1')
|
||||||
|
// returns whether tradeB is better than tradeA by at least a threshold
|
||||||
|
export function isTradeBetter(
|
||||||
|
tradeA: Trade | undefined,
|
||||||
|
tradeB: Trade | undefined,
|
||||||
|
minimumDelta: Percent = ZERO_PERCENT
|
||||||
|
): boolean | undefined {
|
||||||
|
if (!tradeA || !tradeB) return undefined
|
||||||
|
|
||||||
|
if (
|
||||||
|
tradeA.tradeType !== tradeB.tradeType ||
|
||||||
|
!tradeA.inputAmount.token.equals(tradeB.inputAmount.token) ||
|
||||||
|
!tradeB.outputAmount.token.equals(tradeB.outputAmount.token)
|
||||||
|
) {
|
||||||
|
throw new Error('Trades are not comparable')
|
||||||
}
|
}
|
||||||
|
|
||||||
return v1HasBetterTrade && input && output
|
if (minimumDelta.equalTo(ZERO_PERCENT)) {
|
||||||
? `https://v1.uniswap.exchange/swap?inputCurrency=${inputIsWETH ? 'ETH' : input.address}&outputCurrency=${
|
return tradeA.executionPrice.lessThan(tradeB.executionPrice)
|
||||||
outputIsWETH ? 'ETH' : output.address
|
} else {
|
||||||
}`
|
return tradeA.executionPrice.raw.multiply(minimumDelta.add(ONE_HUNDRED_PERCENT)).lessThan(tradeB.executionPrice)
|
||||||
: undefined
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
|
import { parseBytes32String } from '@ethersproject/strings'
|
||||||
import { ChainId, Token, WETH } from '@uniswap/sdk'
|
import { ChainId, Token, WETH } from '@uniswap/sdk'
|
||||||
import { useEffect, useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { ALL_TOKENS } from '../constants/tokens'
|
import { ALL_TOKENS } from '../constants/tokens'
|
||||||
import { useAddUserToken, useFetchTokenByAddress, useUserAddedTokens } from '../state/user/hooks'
|
import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'
|
||||||
|
import { useUserAddedTokens } from '../state/user/hooks'
|
||||||
import { isAddress } from '../utils'
|
import { isAddress } from '../utils'
|
||||||
|
|
||||||
import { useActiveWeb3React } from './index'
|
import { useActiveWeb3React } from './index'
|
||||||
|
import { useBytes32TokenContract, useTokenContract } from './useContract'
|
||||||
|
|
||||||
export function useAllTokens(): { [address: string]: Token } {
|
export function useAllTokens(): { [address: string]: Token } {
|
||||||
const { chainId } = useActiveWeb3React()
|
const { chainId } = useActiveWeb3React()
|
||||||
@@ -35,36 +38,65 @@ export function useAllTokens(): { [address: string]: Token } {
|
|||||||
}, [userAddedTokens, chainId])
|
}, [userAddedTokens, chainId])
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useToken(tokenAddress?: string): Token | undefined {
|
// parse a name or symbol from a token response
|
||||||
const tokens = useAllTokens()
|
const BYTES32_REGEX = /^0x[a-fA-F0-9]{64}$/
|
||||||
return useMemo(() => {
|
function parseStringOrBytes32(str: string | undefined, bytes32: string | undefined, defaultValue: string): string {
|
||||||
const validatedAddress = isAddress(tokenAddress)
|
return str && str.length > 0
|
||||||
if (!validatedAddress) return
|
? str
|
||||||
return tokens[validatedAddress]
|
: bytes32 && BYTES32_REGEX.test(bytes32)
|
||||||
}, [tokens, tokenAddress])
|
? parseBytes32String(bytes32)
|
||||||
|
: defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
// gets token information by address (typically user input) and
|
// undefined if invalid or does not exist
|
||||||
// automatically adds it for the user if the token address is valid
|
// null if loading
|
||||||
export function useTokenByAddressAndAutomaticallyAdd(tokenAddress?: string): Token | undefined {
|
// otherwise returns the token
|
||||||
const fetchTokenByAddress = useFetchTokenByAddress()
|
export function useToken(tokenAddress?: string): Token | undefined | null {
|
||||||
const addToken = useAddUserToken()
|
|
||||||
const token = useToken(tokenAddress)
|
|
||||||
const { chainId } = useActiveWeb3React()
|
const { chainId } = useActiveWeb3React()
|
||||||
|
const tokens = useAllTokens()
|
||||||
|
|
||||||
useEffect(() => {
|
const address = isAddress(tokenAddress)
|
||||||
if (!chainId || !isAddress(tokenAddress)) return
|
|
||||||
const weth = WETH[chainId as ChainId]
|
|
||||||
if (weth && weth.address === isAddress(tokenAddress)) return
|
|
||||||
|
|
||||||
if (tokenAddress && !token) {
|
const tokenContract = useTokenContract(address ? address : undefined, false)
|
||||||
fetchTokenByAddress(tokenAddress).then(token => {
|
const tokenContractBytes32 = useBytes32TokenContract(address ? address : undefined, false)
|
||||||
if (token !== null) {
|
const token: Token | undefined = address ? tokens[address] : undefined
|
||||||
addToken(token)
|
|
||||||
}
|
const tokenName = useSingleCallResult(token ? undefined : tokenContract, 'name', undefined, NEVER_RELOAD)
|
||||||
})
|
const tokenNameBytes32 = useSingleCallResult(
|
||||||
|
token ? undefined : tokenContractBytes32,
|
||||||
|
'name',
|
||||||
|
undefined,
|
||||||
|
NEVER_RELOAD
|
||||||
|
)
|
||||||
|
const symbol = useSingleCallResult(token ? undefined : tokenContract, 'symbol', undefined, NEVER_RELOAD)
|
||||||
|
const symbolBytes32 = useSingleCallResult(token ? undefined : tokenContractBytes32, 'symbol', undefined, NEVER_RELOAD)
|
||||||
|
const decimals = useSingleCallResult(token ? undefined : tokenContract, 'decimals', undefined, NEVER_RELOAD)
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
if (token) return token
|
||||||
|
if (!chainId || !address) return undefined
|
||||||
|
if (decimals.loading || symbol.loading || tokenName.loading) return null
|
||||||
|
if (decimals.result) {
|
||||||
|
return new Token(
|
||||||
|
chainId,
|
||||||
|
address,
|
||||||
|
decimals.result[0],
|
||||||
|
parseStringOrBytes32(symbol.result?.[0], symbolBytes32.result?.[0], 'UNKNOWN'),
|
||||||
|
parseStringOrBytes32(tokenName.result?.[0], tokenNameBytes32.result?.[0], 'Unknown Token')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}, [tokenAddress, token, fetchTokenByAddress, addToken, chainId])
|
return undefined
|
||||||
|
}, [
|
||||||
return token
|
address,
|
||||||
|
chainId,
|
||||||
|
decimals.loading,
|
||||||
|
decimals.result,
|
||||||
|
symbol.loading,
|
||||||
|
symbol.result,
|
||||||
|
symbolBytes32.result,
|
||||||
|
token,
|
||||||
|
tokenName.loading,
|
||||||
|
tokenName.result,
|
||||||
|
tokenNameBytes32.result
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,47 @@
|
|||||||
|
import { Pair, Token, TokenAmount, Trade } from '@uniswap/sdk'
|
||||||
|
import flatMap from 'lodash.flatmap'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { WETH, Token, TokenAmount, Trade, ChainId, Pair } from '@uniswap/sdk'
|
|
||||||
import { useActiveWeb3React } from './index'
|
|
||||||
import { usePair } from '../data/Reserves'
|
|
||||||
|
|
||||||
const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin')
|
import { BASES_TO_CHECK_TRADES_AGAINST } from '../constants'
|
||||||
const USDC = new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C')
|
import { usePairs } from '../data/Reserves'
|
||||||
|
|
||||||
|
import { useActiveWeb3React } from './index'
|
||||||
|
|
||||||
function useAllCommonPairs(tokenA?: Token, tokenB?: Token): Pair[] {
|
function useAllCommonPairs(tokenA?: Token, tokenB?: Token): Pair[] {
|
||||||
const { chainId } = useActiveWeb3React()
|
const { chainId } = useActiveWeb3React()
|
||||||
|
|
||||||
// check for direct pair between tokens
|
const bases: Token[] = chainId ? BASES_TO_CHECK_TRADES_AGAINST[chainId] : []
|
||||||
const pairBetween = usePair(tokenA, tokenB)
|
|
||||||
|
|
||||||
// get token<->WETH pairs
|
const allPairCombinations: [Token | undefined, Token | undefined][] = useMemo(
|
||||||
const aToETH = usePair(tokenA, WETH[chainId as ChainId])
|
() => [
|
||||||
const bToETH = usePair(tokenB, WETH[chainId as ChainId])
|
// the direct pair
|
||||||
|
[tokenA, tokenB],
|
||||||
// get token<->DAI pairs
|
// token A against all bases
|
||||||
const aToDAI = usePair(tokenA, chainId === ChainId.MAINNET ? DAI : undefined)
|
...bases.map((base): [Token | undefined, Token | undefined] => [tokenA, base]),
|
||||||
const bToDAI = usePair(tokenB, chainId === ChainId.MAINNET ? DAI : undefined)
|
// token B against all bases
|
||||||
|
...bases.map((base): [Token | undefined, Token | undefined] => [tokenB, base]),
|
||||||
// get token<->USDC pairs
|
// each base against all bases
|
||||||
const aToUSDC = usePair(tokenA, chainId === ChainId.MAINNET ? USDC : undefined)
|
...flatMap(bases, (base): [Token, Token][] => bases.map(otherBase => [base, otherBase]))
|
||||||
const bToUSDC = usePair(tokenB, chainId === ChainId.MAINNET ? USDC : undefined)
|
],
|
||||||
|
[tokenA, tokenB, bases]
|
||||||
// get connecting pairs
|
|
||||||
const DAIToETH = usePair(chainId === ChainId.MAINNET ? DAI : undefined, WETH[chainId as ChainId])
|
|
||||||
const USDCToETH = usePair(chainId === ChainId.MAINNET ? USDC : undefined, WETH[chainId as ChainId])
|
|
||||||
const DAIToUSDC = usePair(
|
|
||||||
chainId === ChainId.MAINNET ? DAI : undefined,
|
|
||||||
chainId === ChainId.MAINNET ? USDC : undefined
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const allPairs = usePairs(allPairCombinations)
|
||||||
|
|
||||||
// only pass along valid pairs, non-duplicated pairs
|
// only pass along valid pairs, non-duplicated pairs
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() =>
|
() =>
|
||||||
[pairBetween, aToETH, bToETH, aToDAI, bToDAI, aToUSDC, bToUSDC, DAIToETH, USDCToETH, DAIToUSDC]
|
Object.values(
|
||||||
// filter out invalid pairs
|
allPairs
|
||||||
.filter((p): p is Pair => !!p)
|
// filter out invalid pairs
|
||||||
// filter out duplicated pairs
|
.filter((p): p is Pair => !!p)
|
||||||
.filter(
|
// filter out duplicated pairs
|
||||||
(p, i, pairs) => i === pairs.findIndex(pair => pair?.liquidityToken.address === p.liquidityToken.address)
|
.reduce<{ [pairAddress: string]: Pair }>((memo, curr) => {
|
||||||
),
|
memo[curr.liquidityToken.address] = memo[curr.liquidityToken.address] ?? curr
|
||||||
[pairBetween, aToETH, bToETH, aToDAI, bToDAI, aToUSDC, bToUSDC, DAIToETH, USDCToETH, DAIToUSDC]
|
return memo
|
||||||
|
}, {})
|
||||||
|
),
|
||||||
|
[allPairs]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,14 +49,11 @@ function useAllCommonPairs(tokenA?: Token, tokenB?: Token): Pair[] {
|
|||||||
* Returns the best trade for the exact amount of tokens in to the given token out
|
* Returns the best trade for the exact amount of tokens in to the given token out
|
||||||
*/
|
*/
|
||||||
export function useTradeExactIn(amountIn?: TokenAmount, tokenOut?: Token): Trade | null {
|
export function useTradeExactIn(amountIn?: TokenAmount, tokenOut?: Token): Trade | null {
|
||||||
const inputToken = amountIn?.token
|
const allowedPairs = useAllCommonPairs(amountIn?.token, tokenOut)
|
||||||
const outputToken = tokenOut
|
|
||||||
|
|
||||||
const allowedPairs = useAllCommonPairs(inputToken, outputToken)
|
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
if (amountIn && tokenOut && allowedPairs.length > 0) {
|
if (amountIn && tokenOut && allowedPairs.length > 0) {
|
||||||
return Trade.bestTradeExactIn(allowedPairs, amountIn, tokenOut)[0] ?? null
|
return Trade.bestTradeExactIn(allowedPairs, amountIn, tokenOut, { maxHops: 3, maxNumResults: 1 })[0] ?? null
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}, [allowedPairs, amountIn, tokenOut])
|
}, [allowedPairs, amountIn, tokenOut])
|
||||||
@@ -66,14 +63,11 @@ export function useTradeExactIn(amountIn?: TokenAmount, tokenOut?: Token): Trade
|
|||||||
* Returns the best trade for the token in to the exact amount of token out
|
* Returns the best trade for the token in to the exact amount of token out
|
||||||
*/
|
*/
|
||||||
export function useTradeExactOut(tokenIn?: Token, amountOut?: TokenAmount): Trade | null {
|
export function useTradeExactOut(tokenIn?: Token, amountOut?: TokenAmount): Trade | null {
|
||||||
const inputToken = tokenIn
|
const allowedPairs = useAllCommonPairs(tokenIn, amountOut?.token)
|
||||||
const outputToken = amountOut?.token
|
|
||||||
|
|
||||||
const allowedPairs = useAllCommonPairs(inputToken, outputToken)
|
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
if (tokenIn && amountOut && allowedPairs.length > 0) {
|
if (tokenIn && amountOut && allowedPairs.length > 0) {
|
||||||
return Trade.bestTradeExactOut(allowedPairs, tokenIn, amountOut)[0] ?? null
|
return Trade.bestTradeExactOut(allowedPairs, tokenIn, amountOut, { maxHops: 3, maxNumResults: 1 })[0] ?? null
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}, [allowedPairs, tokenIn, amountOut])
|
}, [allowedPairs, tokenIn, amountOut])
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { Web3Provider } from '@ethersproject/providers'
|
import { Web3Provider } from '@ethersproject/providers'
|
||||||
|
import { ChainId } from '@uniswap/sdk'
|
||||||
import { useWeb3React as useWeb3ReactCore } from '@web3-react/core'
|
import { useWeb3React as useWeb3ReactCore } from '@web3-react/core'
|
||||||
|
import { Web3ReactContextInterface } from '@web3-react/core/dist/types'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { isMobile } from 'react-device-detect'
|
import { isMobile } from 'react-device-detect'
|
||||||
import { injected } from '../connectors'
|
import { injected } from '../connectors'
|
||||||
import { NetworkContextName } from '../constants'
|
import { NetworkContextName } from '../constants'
|
||||||
|
|
||||||
export function useActiveWeb3React() {
|
export function useActiveWeb3React(): Web3ReactContextInterface<Web3Provider> & { chainId?: ChainId } {
|
||||||
const context = useWeb3ReactCore<Web3Provider>()
|
const context = useWeb3ReactCore<Web3Provider>()
|
||||||
const contextNetwork = useWeb3ReactCore<Web3Provider>(NetworkContextName)
|
const contextNetwork = useWeb3ReactCore<Web3Provider>(NetworkContextName)
|
||||||
return context.active ? context : contextNetwork
|
return context.active ? context : contextNetwork
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import { Trade, WETH, TokenAmount } from '@uniswap/sdk'
|
|||||||
import { useCallback, useMemo } from 'react'
|
import { useCallback, useMemo } from 'react'
|
||||||
import { ROUTER_ADDRESS } from '../constants'
|
import { ROUTER_ADDRESS } from '../constants'
|
||||||
import { useTokenAllowance } from '../data/Allowances'
|
import { useTokenAllowance } from '../data/Allowances'
|
||||||
|
import { getTradeVersion, useV1TradeExchangeAddress } from '../data/V1'
|
||||||
import { Field } from '../state/swap/actions'
|
import { Field } from '../state/swap/actions'
|
||||||
import { useTransactionAdder, useHasPendingApproval } from '../state/transactions/hooks'
|
import { useTransactionAdder, useHasPendingApproval } from '../state/transactions/hooks'
|
||||||
import { computeSlippageAdjustedAmounts } from '../utils/prices'
|
import { computeSlippageAdjustedAmounts } from '../utils/prices'
|
||||||
import { calculateGasMargin } from '../utils'
|
import { calculateGasMargin } from '../utils'
|
||||||
import { useTokenContract } from './useContract'
|
import { useTokenContract } from './useContract'
|
||||||
import { useActiveWeb3React } from './index'
|
import { useActiveWeb3React } from './index'
|
||||||
|
import { Version } from './useToggledVersion'
|
||||||
|
|
||||||
export enum ApprovalState {
|
export enum ApprovalState {
|
||||||
UNKNOWN,
|
UNKNOWN,
|
||||||
@@ -21,30 +23,34 @@ export enum ApprovalState {
|
|||||||
// returns a variable indicating the state of the approval and a function which approves if necessary or early returns
|
// returns a variable indicating the state of the approval and a function which approves if necessary or early returns
|
||||||
export function useApproveCallback(
|
export function useApproveCallback(
|
||||||
amountToApprove?: TokenAmount,
|
amountToApprove?: TokenAmount,
|
||||||
addressToApprove?: string
|
spender?: string
|
||||||
): [ApprovalState, () => Promise<void>] {
|
): [ApprovalState, () => Promise<void>] {
|
||||||
const { account } = useActiveWeb3React()
|
const { account } = useActiveWeb3React()
|
||||||
|
|
||||||
const currentAllowance = useTokenAllowance(amountToApprove?.token, account ?? undefined, addressToApprove)
|
const currentAllowance = useTokenAllowance(amountToApprove?.token, account ?? undefined, spender)
|
||||||
const pendingApproval = useHasPendingApproval(amountToApprove?.token?.address)
|
const pendingApproval = useHasPendingApproval(amountToApprove?.token?.address, spender)
|
||||||
|
|
||||||
// check the current approval status
|
// check the current approval status
|
||||||
const approval = useMemo(() => {
|
const approvalState: ApprovalState = useMemo(() => {
|
||||||
if (!amountToApprove) return ApprovalState.UNKNOWN
|
if (!amountToApprove || !spender) return ApprovalState.UNKNOWN
|
||||||
// we treat WETH as ETH which requires no approvals
|
// we treat WETH as ETH which requires no approvals
|
||||||
if (amountToApprove.token.equals(WETH[amountToApprove.token.chainId])) return ApprovalState.APPROVED
|
if (amountToApprove.token.equals(WETH[amountToApprove.token.chainId])) return ApprovalState.APPROVED
|
||||||
// we might not have enough data to know whether or not we need to approve
|
// we might not have enough data to know whether or not we need to approve
|
||||||
if (!currentAllowance) return ApprovalState.UNKNOWN
|
if (!currentAllowance) return ApprovalState.UNKNOWN
|
||||||
if (pendingApproval) return ApprovalState.PENDING
|
|
||||||
// amountToApprove will be defined if currentAllowance is
|
// amountToApprove will be defined if currentAllowance is
|
||||||
return currentAllowance.lessThan(amountToApprove) ? ApprovalState.NOT_APPROVED : ApprovalState.APPROVED
|
return currentAllowance.lessThan(amountToApprove)
|
||||||
}, [amountToApprove, currentAllowance, pendingApproval])
|
? pendingApproval
|
||||||
|
? ApprovalState.PENDING
|
||||||
|
: ApprovalState.NOT_APPROVED
|
||||||
|
: ApprovalState.APPROVED
|
||||||
|
}, [amountToApprove, currentAllowance, pendingApproval, spender])
|
||||||
|
|
||||||
const tokenContract = useTokenContract(amountToApprove?.token?.address)
|
const tokenContract = useTokenContract(amountToApprove?.token?.address)
|
||||||
const addTransaction = useTransactionAdder()
|
const addTransaction = useTransactionAdder()
|
||||||
|
|
||||||
const approve = useCallback(async (): Promise<void> => {
|
const approve = useCallback(async (): Promise<void> => {
|
||||||
if (approval !== ApprovalState.NOT_APPROVED) {
|
if (approvalState !== ApprovalState.NOT_APPROVED) {
|
||||||
console.error('approve was called unnecessarily')
|
console.error('approve was called unnecessarily')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -59,30 +65,35 @@ export function useApproveCallback(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!spender) {
|
||||||
|
console.error('no spender')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let useExact = false
|
let useExact = false
|
||||||
const estimatedGas = await tokenContract.estimateGas.approve(addressToApprove, MaxUint256).catch(() => {
|
const estimatedGas = await tokenContract.estimateGas.approve(spender, MaxUint256).catch(() => {
|
||||||
// general fallback for tokens who restrict approval amounts
|
// general fallback for tokens who restrict approval amounts
|
||||||
useExact = true
|
useExact = true
|
||||||
return tokenContract.estimateGas.approve(addressToApprove, amountToApprove.raw.toString())
|
return tokenContract.estimateGas.approve(spender, amountToApprove.raw.toString())
|
||||||
})
|
})
|
||||||
|
|
||||||
return tokenContract
|
return tokenContract
|
||||||
.approve(addressToApprove, useExact ? amountToApprove.raw.toString() : MaxUint256, {
|
.approve(spender, useExact ? amountToApprove.raw.toString() : MaxUint256, {
|
||||||
gasLimit: calculateGasMargin(estimatedGas)
|
gasLimit: calculateGasMargin(estimatedGas)
|
||||||
})
|
})
|
||||||
.then((response: TransactionResponse) => {
|
.then((response: TransactionResponse) => {
|
||||||
addTransaction(response, {
|
addTransaction(response, {
|
||||||
summary: 'Approve ' + amountToApprove?.token?.symbol,
|
summary: 'Approve ' + amountToApprove.token.symbol,
|
||||||
approvalOfToken: amountToApprove?.token?.address
|
approval: { tokenAddress: amountToApprove.token.address, spender: spender }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch((error: Error) => {
|
.catch((error: Error) => {
|
||||||
console.debug('Failed to approve token', error)
|
console.debug('Failed to approve token', error)
|
||||||
throw error
|
throw error
|
||||||
})
|
})
|
||||||
}, [approval, tokenContract, addressToApprove, amountToApprove, addTransaction])
|
}, [approvalState, tokenContract, spender, amountToApprove, addTransaction])
|
||||||
|
|
||||||
return [approval, approve]
|
return [approvalState, approve]
|
||||||
}
|
}
|
||||||
|
|
||||||
// wraps useApproveCallback in the context of a swap
|
// wraps useApproveCallback in the context of a swap
|
||||||
@@ -91,5 +102,7 @@ export function useApproveCallbackFromTrade(trade?: Trade, allowedSlippage = 0)
|
|||||||
() => (trade ? computeSlippageAdjustedAmounts(trade, allowedSlippage)[Field.INPUT] : undefined),
|
() => (trade ? computeSlippageAdjustedAmounts(trade, allowedSlippage)[Field.INPUT] : undefined),
|
||||||
[trade, allowedSlippage]
|
[trade, allowedSlippage]
|
||||||
)
|
)
|
||||||
return useApproveCallback(amountToApprove, ROUTER_ADDRESS)
|
const tradeIsV1 = getTradeVersion(trade) === Version.v1
|
||||||
|
const v1ExchangeAddress = useV1TradeExchangeAddress(trade)
|
||||||
|
return useApproveCallback(amountToApprove, tradeIsV1 ? v1ExchangeAddress : ROUTER_ADDRESS)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ import { Contract } from '@ethersproject/contracts'
|
|||||||
import { ChainId } from '@uniswap/sdk'
|
import { ChainId } from '@uniswap/sdk'
|
||||||
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
|
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
import { ERC20_BYTES32_ABI } from '../constants/abis/erc20'
|
||||||
|
import UNISOCKS_ABI from '../constants/abis/unisocks.json'
|
||||||
import ERC20_ABI from '../constants/abis/erc20.json'
|
import ERC20_ABI from '../constants/abis/erc20.json'
|
||||||
import { V1_EXCHANGE_ABI, V1_FACTORY_ABI, V1_FACTORY_ADDRESS } from '../constants/v1'
|
import { MIGRATOR_ABI, MIGRATOR_ADDRESS } from '../constants/abis/migrator'
|
||||||
import { MULTICALL_ABI, MULTICALL_NETWORKS } from '../constants/multicall'
|
import { MULTICALL_ABI, MULTICALL_NETWORKS } from '../constants/multicall'
|
||||||
|
import { V1_EXCHANGE_ABI, V1_FACTORY_ABI, V1_FACTORY_ADDRESSES } from '../constants/v1'
|
||||||
import { getContract } from '../utils'
|
import { getContract } from '../utils'
|
||||||
import { useActiveWeb3React } from './index'
|
import { useActiveWeb3React } from './index'
|
||||||
|
|
||||||
@@ -25,22 +28,39 @@ function useContract(address?: string, ABI?: any, withSignerIfPossible = true):
|
|||||||
|
|
||||||
export function useV1FactoryContract(): Contract | null {
|
export function useV1FactoryContract(): Contract | null {
|
||||||
const { chainId } = useActiveWeb3React()
|
const { chainId } = useActiveWeb3React()
|
||||||
return useContract(chainId === 1 ? V1_FACTORY_ADDRESS : undefined, V1_FACTORY_ABI, false)
|
return useContract(chainId && V1_FACTORY_ADDRESSES[chainId], V1_FACTORY_ABI, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useV1ExchangeContract(address: string): Contract | null {
|
export function useV2MigratorContract(): Contract | null {
|
||||||
return useContract(address, V1_EXCHANGE_ABI, false)
|
return useContract(MIGRATOR_ADDRESS, MIGRATOR_ABI, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTokenContract(tokenAddress?: string, withSignerIfPossible = true): Contract | null {
|
export function useV1ExchangeContract(address?: string, withSignerIfPossible?: boolean): Contract | null {
|
||||||
|
return useContract(address, V1_EXCHANGE_ABI, withSignerIfPossible)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTokenContract(tokenAddress?: string, withSignerIfPossible?: boolean): Contract | null {
|
||||||
return useContract(tokenAddress, ERC20_ABI, withSignerIfPossible)
|
return useContract(tokenAddress, ERC20_ABI, withSignerIfPossible)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePairContract(pairAddress?: string, withSignerIfPossible = true): Contract | null {
|
export function useBytes32TokenContract(tokenAddress?: string, withSignerIfPossible?: boolean): Contract | null {
|
||||||
|
return useContract(tokenAddress, ERC20_BYTES32_ABI, withSignerIfPossible)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePairContract(pairAddress?: string, withSignerIfPossible?: boolean): Contract | null {
|
||||||
return useContract(pairAddress, IUniswapV2PairABI, withSignerIfPossible)
|
return useContract(pairAddress, IUniswapV2PairABI, withSignerIfPossible)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMulticallContract(): Contract | null {
|
export function useMulticallContract(): Contract | null {
|
||||||
const { chainId } = useActiveWeb3React()
|
const { chainId } = useActiveWeb3React()
|
||||||
return useContract(MULTICALL_NETWORKS[chainId as ChainId], MULTICALL_ABI, false)
|
return useContract(chainId && MULTICALL_NETWORKS[chainId], MULTICALL_ABI, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSocksController(): Contract | null {
|
||||||
|
const { chainId } = useActiveWeb3React()
|
||||||
|
return useContract(
|
||||||
|
chainId === ChainId.MAINNET ? '0x65770b5283117639760beA3F867b69b3697a91dd' : undefined,
|
||||||
|
UNISOCKS_ABI,
|
||||||
|
false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
21
src/hooks/useENS.ts
Normal file
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.
|
* Does a reverse lookup for an address to find its ENS name.
|
||||||
* Note this is not the same as looking up an ENS name to find an address.
|
* Note this is not the same as looking up an ENS name to find an address.
|
||||||
*/
|
*/
|
||||||
export default function useENSName(address?: string): string | null {
|
export default function useENSName(address?: string): { ENSName: string | null; loading: boolean } {
|
||||||
const { library } = useActiveWeb3React()
|
const { library } = useActiveWeb3React()
|
||||||
|
|
||||||
const [ENSName, setENSName] = useState<string | null>(null)
|
const [ENSName, setENSName] = useState<{ ENSName: string | null; loading: boolean }>({
|
||||||
|
loading: false,
|
||||||
|
ENSName: null
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!library || !address) return
|
|
||||||
const validated = isAddress(address)
|
const validated = isAddress(address)
|
||||||
if (validated) {
|
if (!library || !validated) {
|
||||||
|
setENSName({ loading: false, ENSName: null })
|
||||||
|
return
|
||||||
|
} else {
|
||||||
let stale = false
|
let stale = false
|
||||||
|
setENSName({ loading: true, ENSName: null })
|
||||||
library
|
library
|
||||||
.lookupAddress(validated)
|
.lookupAddress(validated)
|
||||||
.then(name => {
|
.then(name => {
|
||||||
if (!stale) {
|
if (!stale) {
|
||||||
if (name) {
|
if (name) {
|
||||||
setENSName(name)
|
setENSName({ loading: false, ENSName: name })
|
||||||
} else {
|
} else {
|
||||||
setENSName(null)
|
setENSName({ loading: false, ENSName: null })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
if (!stale) {
|
if (!stale) {
|
||||||
setENSName(null)
|
setENSName({ loading: false, ENSName: null })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
stale = true
|
stale = true
|
||||||
setENSName(null)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}, [library, address])
|
}, [library, address])
|
||||||
|
|
||||||
return ENSName
|
return ENSName
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
export default function useInterval(callback: () => void, delay: null | number) {
|
export default function useInterval(callback: () => void, delay: null | number, leading = true) {
|
||||||
const savedCallback = useRef<() => void>()
|
const savedCallback = useRef<() => void>()
|
||||||
|
|
||||||
// Remember the latest callback.
|
// Remember the latest callback.
|
||||||
@@ -16,10 +16,10 @@ export default function useInterval(callback: () => void, delay: null | number)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (delay !== null) {
|
if (delay !== null) {
|
||||||
tick()
|
if (leading) tick()
|
||||||
const id = setInterval(tick, delay)
|
const id = setInterval(tick, delay)
|
||||||
return () => clearInterval(id)
|
return () => clearInterval(id)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}, [delay])
|
}, [delay, leading])
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/hooks/useLast.ts
Normal file
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 { BigNumber } from '@ethersproject/bignumber'
|
||||||
|
import { MaxUint256 } from '@ethersproject/constants'
|
||||||
import { Contract } from '@ethersproject/contracts'
|
import { Contract } from '@ethersproject/contracts'
|
||||||
import { ChainId, Token, Trade, TradeType, WETH } from '@uniswap/sdk'
|
import { Trade, TradeType, WETH } from '@uniswap/sdk'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, ROUTER_ADDRESS } from '../constants'
|
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, ROUTER_ADDRESS } from '../constants'
|
||||||
import { useTokenAllowance } from '../data/Allowances'
|
import { useTokenAllowance } from '../data/Allowances'
|
||||||
|
import { getTradeVersion, useV1TradeExchangeAddress } from '../data/V1'
|
||||||
import { Field } from '../state/swap/actions'
|
import { Field } from '../state/swap/actions'
|
||||||
import { useTransactionAdder } from '../state/transactions/hooks'
|
import { useTransactionAdder } from '../state/transactions/hooks'
|
||||||
|
import { calculateGasMargin, getRouterContract, shortenAddress, isAddress } from '../utils'
|
||||||
import { computeSlippageAdjustedAmounts } from '../utils/prices'
|
import { computeSlippageAdjustedAmounts } from '../utils/prices'
|
||||||
import { calculateGasMargin, getRouterContract, isAddress } from '../utils'
|
|
||||||
import { useActiveWeb3React } from './index'
|
import { useActiveWeb3React } from './index'
|
||||||
import useENSName from './useENSName'
|
import { useV1ExchangeContract } from './useContract'
|
||||||
|
import useENS from './useENS'
|
||||||
|
import { Version } from './useToggledVersion'
|
||||||
|
|
||||||
enum SwapType {
|
enum SwapType {
|
||||||
EXACT_TOKENS_FOR_TOKENS,
|
EXACT_TOKENS_FOR_TOKENS,
|
||||||
@@ -17,25 +21,37 @@ enum SwapType {
|
|||||||
EXACT_ETH_FOR_TOKENS,
|
EXACT_ETH_FOR_TOKENS,
|
||||||
TOKENS_FOR_EXACT_TOKENS,
|
TOKENS_FOR_EXACT_TOKENS,
|
||||||
TOKENS_FOR_EXACT_ETH,
|
TOKENS_FOR_EXACT_ETH,
|
||||||
ETH_FOR_EXACT_TOKENS
|
ETH_FOR_EXACT_TOKENS,
|
||||||
|
V1_EXACT_ETH_FOR_TOKENS,
|
||||||
|
V1_EXACT_TOKENS_FOR_ETH,
|
||||||
|
V1_EXACT_TOKENS_FOR_TOKENS,
|
||||||
|
V1_ETH_FOR_EXACT_TOKENS,
|
||||||
|
V1_TOKENS_FOR_EXACT_ETH,
|
||||||
|
V1_TOKENS_FOR_EXACT_TOKENS
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSwapType(tokens: { [field in Field]?: Token }, isExactIn: boolean, chainId: number): SwapType {
|
function getSwapType(trade: Trade | undefined): SwapType | undefined {
|
||||||
|
if (!trade) return undefined
|
||||||
|
const chainId = trade.inputAmount.token.chainId
|
||||||
|
const inputWETH = trade.inputAmount.token.equals(WETH[chainId])
|
||||||
|
const outputWETH = trade.outputAmount.token.equals(WETH[chainId])
|
||||||
|
const isExactIn = trade.tradeType === TradeType.EXACT_INPUT
|
||||||
|
const isV1 = getTradeVersion(trade) === Version.v1
|
||||||
if (isExactIn) {
|
if (isExactIn) {
|
||||||
if (tokens[Field.INPUT]?.equals(WETH[chainId as ChainId])) {
|
if (inputWETH) {
|
||||||
return SwapType.EXACT_ETH_FOR_TOKENS
|
return isV1 ? SwapType.V1_EXACT_ETH_FOR_TOKENS : SwapType.EXACT_ETH_FOR_TOKENS
|
||||||
} else if (tokens[Field.OUTPUT]?.equals(WETH[chainId as ChainId])) {
|
} else if (outputWETH) {
|
||||||
return SwapType.EXACT_TOKENS_FOR_ETH
|
return isV1 ? SwapType.V1_EXACT_TOKENS_FOR_ETH : SwapType.EXACT_TOKENS_FOR_ETH
|
||||||
} else {
|
} else {
|
||||||
return SwapType.EXACT_TOKENS_FOR_TOKENS
|
return isV1 ? SwapType.V1_EXACT_TOKENS_FOR_TOKENS : SwapType.EXACT_TOKENS_FOR_TOKENS
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (tokens[Field.INPUT]?.equals(WETH[chainId as ChainId])) {
|
if (inputWETH) {
|
||||||
return SwapType.ETH_FOR_EXACT_TOKENS
|
return isV1 ? SwapType.V1_ETH_FOR_EXACT_TOKENS : SwapType.ETH_FOR_EXACT_TOKENS
|
||||||
} else if (tokens[Field.OUTPUT]?.equals(WETH[chainId as ChainId])) {
|
} else if (outputWETH) {
|
||||||
return SwapType.TOKENS_FOR_EXACT_ETH
|
return isV1 ? SwapType.V1_TOKENS_FOR_EXACT_ETH : SwapType.TOKENS_FOR_EXACT_ETH
|
||||||
} else {
|
} else {
|
||||||
return SwapType.TOKENS_FOR_EXACT_TOKENS
|
return isV1 ? SwapType.V1_TOKENS_FOR_EXACT_TOKENS : SwapType.TOKENS_FOR_EXACT_TOKENS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,19 +59,27 @@ function getSwapType(tokens: { [field in Field]?: Token }, isExactIn: boolean, c
|
|||||||
// returns a function that will execute a swap, if the parameters are all valid
|
// returns a function that will execute a swap, if the parameters are all valid
|
||||||
// and the user has approved the slippage adjusted input amount for the trade
|
// and the user has approved the slippage adjusted input amount for the trade
|
||||||
export function useSwapCallback(
|
export function useSwapCallback(
|
||||||
trade?: Trade, // trade to execute, required
|
trade: Trade | undefined, // trade to execute, required
|
||||||
allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // in bips, optional
|
allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // in bips
|
||||||
deadline: number = DEFAULT_DEADLINE_FROM_NOW, // in seconds from now, optional
|
deadline: number = DEFAULT_DEADLINE_FROM_NOW, // in seconds from now
|
||||||
to?: string // recipient of output, optional
|
recipientAddressOrName: string | null // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender
|
||||||
): null | (() => Promise<string>) {
|
): null | (() => Promise<string>) {
|
||||||
const { account, chainId, library } = useActiveWeb3React()
|
const { account, chainId, library } = useActiveWeb3React()
|
||||||
const inputAllowance = useTokenAllowance(trade?.inputAmount?.token, account ?? undefined, ROUTER_ADDRESS)
|
|
||||||
const addTransaction = useTransactionAdder()
|
const addTransaction = useTransactionAdder()
|
||||||
const recipient = to ? isAddress(to) : account
|
|
||||||
const ensName = useENSName(to)
|
const { address: recipientAddress } = useENS(recipientAddressOrName)
|
||||||
|
const recipient = recipientAddressOrName === null ? account : recipientAddress
|
||||||
|
|
||||||
|
const tradeVersion = getTradeVersion(trade)
|
||||||
|
const v1Exchange = useV1ExchangeContract(useV1TradeExchangeAddress(trade), true)
|
||||||
|
const inputAllowance = useTokenAllowance(
|
||||||
|
trade?.inputAmount?.token,
|
||||||
|
account ?? undefined,
|
||||||
|
tradeVersion === Version.v1 ? v1Exchange?.address : ROUTER_ADDRESS
|
||||||
|
)
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
if (!trade || !recipient) return null
|
if (!trade || !recipient || !library || !account || !tradeVersion || !chainId) return null
|
||||||
|
|
||||||
// will always be defined
|
// will always be defined
|
||||||
const {
|
const {
|
||||||
@@ -67,34 +91,32 @@ export function useSwapCallback(
|
|||||||
|
|
||||||
// no allowance
|
// no allowance
|
||||||
if (
|
if (
|
||||||
!trade.inputAmount.token.equals(WETH[chainId as ChainId]) &&
|
!trade.inputAmount.token.equals(WETH[chainId]) &&
|
||||||
(!inputAllowance || slippageAdjustedInput.greaterThan(inputAllowance))
|
(!inputAllowance || slippageAdjustedInput.greaterThan(inputAllowance))
|
||||||
) {
|
) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return async function onSwap() {
|
return async function onSwap() {
|
||||||
if (!chainId || !library || !account) {
|
const contract: Contract | null =
|
||||||
throw new Error('missing dependencies in onSwap callback')
|
tradeVersion === Version.v2 ? getRouterContract(chainId, library, account) : v1Exchange
|
||||||
|
if (!contract) {
|
||||||
|
throw new Error('Failed to get a swap contract')
|
||||||
}
|
}
|
||||||
|
|
||||||
const routerContract: Contract = getRouterContract(chainId, library, account)
|
|
||||||
|
|
||||||
const path = trade.route.path.map(t => t.address)
|
const path = trade.route.path.map(t => t.address)
|
||||||
|
|
||||||
const deadlineFromNow: number = Math.ceil(Date.now() / 1000) + deadline
|
const deadlineFromNow: number = Math.ceil(Date.now() / 1000) + deadline
|
||||||
|
|
||||||
const swapType = getSwapType(
|
const swapType = getSwapType(trade)
|
||||||
{ [Field.INPUT]: trade.inputAmount.token, [Field.OUTPUT]: trade.outputAmount.token },
|
|
||||||
trade.tradeType === TradeType.EXACT_INPUT,
|
|
||||||
chainId as ChainId
|
|
||||||
)
|
|
||||||
|
|
||||||
let estimate, method: Function, args: Array<string | string[] | number>, value: BigNumber | null
|
// let estimate: Function, method: Function,
|
||||||
|
let methodNames: string[],
|
||||||
|
args: Array<string | string[] | number>,
|
||||||
|
value: BigNumber | null = null
|
||||||
switch (swapType) {
|
switch (swapType) {
|
||||||
case SwapType.EXACT_TOKENS_FOR_TOKENS:
|
case SwapType.EXACT_TOKENS_FOR_TOKENS:
|
||||||
estimate = routerContract.estimateGas.swapExactTokensForTokens
|
methodNames = ['swapExactTokensForTokens', 'swapExactTokensForTokensSupportingFeeOnTransferTokens']
|
||||||
method = routerContract.swapExactTokensForTokens
|
|
||||||
args = [
|
args = [
|
||||||
slippageAdjustedInput.raw.toString(),
|
slippageAdjustedInput.raw.toString(),
|
||||||
slippageAdjustedOutput.raw.toString(),
|
slippageAdjustedOutput.raw.toString(),
|
||||||
@@ -102,11 +124,9 @@ export function useSwapCallback(
|
|||||||
recipient,
|
recipient,
|
||||||
deadlineFromNow
|
deadlineFromNow
|
||||||
]
|
]
|
||||||
value = null
|
|
||||||
break
|
break
|
||||||
case SwapType.TOKENS_FOR_EXACT_TOKENS:
|
case SwapType.TOKENS_FOR_EXACT_TOKENS:
|
||||||
estimate = routerContract.estimateGas.swapTokensForExactTokens
|
methodNames = ['swapTokensForExactTokens']
|
||||||
method = routerContract.swapTokensForExactTokens
|
|
||||||
args = [
|
args = [
|
||||||
slippageAdjustedOutput.raw.toString(),
|
slippageAdjustedOutput.raw.toString(),
|
||||||
slippageAdjustedInput.raw.toString(),
|
slippageAdjustedInput.raw.toString(),
|
||||||
@@ -114,17 +134,14 @@ export function useSwapCallback(
|
|||||||
recipient,
|
recipient,
|
||||||
deadlineFromNow
|
deadlineFromNow
|
||||||
]
|
]
|
||||||
value = null
|
|
||||||
break
|
break
|
||||||
case SwapType.EXACT_ETH_FOR_TOKENS:
|
case SwapType.EXACT_ETH_FOR_TOKENS:
|
||||||
estimate = routerContract.estimateGas.swapExactETHForTokens
|
methodNames = ['swapExactETHForTokens', 'swapExactETHForTokensSupportingFeeOnTransferTokens']
|
||||||
method = routerContract.swapExactETHForTokens
|
|
||||||
args = [slippageAdjustedOutput.raw.toString(), path, recipient, deadlineFromNow]
|
args = [slippageAdjustedOutput.raw.toString(), path, recipient, deadlineFromNow]
|
||||||
value = BigNumber.from(slippageAdjustedInput.raw.toString())
|
value = BigNumber.from(slippageAdjustedInput.raw.toString())
|
||||||
break
|
break
|
||||||
case SwapType.TOKENS_FOR_EXACT_ETH:
|
case SwapType.TOKENS_FOR_EXACT_ETH:
|
||||||
estimate = routerContract.estimateGas.swapTokensForExactETH
|
methodNames = ['swapTokensForExactETH']
|
||||||
method = routerContract.swapTokensForExactETH
|
|
||||||
args = [
|
args = [
|
||||||
slippageAdjustedOutput.raw.toString(),
|
slippageAdjustedOutput.raw.toString(),
|
||||||
slippageAdjustedInput.raw.toString(),
|
slippageAdjustedInput.raw.toString(),
|
||||||
@@ -132,11 +149,9 @@ export function useSwapCallback(
|
|||||||
recipient,
|
recipient,
|
||||||
deadlineFromNow
|
deadlineFromNow
|
||||||
]
|
]
|
||||||
value = null
|
|
||||||
break
|
break
|
||||||
case SwapType.EXACT_TOKENS_FOR_ETH:
|
case SwapType.EXACT_TOKENS_FOR_ETH:
|
||||||
estimate = routerContract.estimateGas.swapExactTokensForETH
|
methodNames = ['swapExactTokensForETH', 'swapExactTokensForETHSupportingFeeOnTransferTokens']
|
||||||
method = routerContract.swapExactTokensForETH
|
|
||||||
args = [
|
args = [
|
||||||
slippageAdjustedInput.raw.toString(),
|
slippageAdjustedInput.raw.toString(),
|
||||||
slippageAdjustedOutput.raw.toString(),
|
slippageAdjustedOutput.raw.toString(),
|
||||||
@@ -144,58 +159,171 @@ export function useSwapCallback(
|
|||||||
recipient,
|
recipient,
|
||||||
deadlineFromNow
|
deadlineFromNow
|
||||||
]
|
]
|
||||||
value = null
|
|
||||||
break
|
break
|
||||||
case SwapType.ETH_FOR_EXACT_TOKENS:
|
case SwapType.ETH_FOR_EXACT_TOKENS:
|
||||||
estimate = routerContract.estimateGas.swapETHForExactTokens
|
methodNames = ['swapETHForExactTokens']
|
||||||
method = routerContract.swapETHForExactTokens
|
|
||||||
args = [slippageAdjustedOutput.raw.toString(), path, recipient, deadlineFromNow]
|
args = [slippageAdjustedOutput.raw.toString(), path, recipient, deadlineFromNow]
|
||||||
value = BigNumber.from(slippageAdjustedInput.raw.toString())
|
value = BigNumber.from(slippageAdjustedInput.raw.toString())
|
||||||
break
|
break
|
||||||
|
case SwapType.V1_EXACT_ETH_FOR_TOKENS:
|
||||||
|
methodNames = ['ethToTokenTransferInput']
|
||||||
|
args = [slippageAdjustedOutput.raw.toString(), deadlineFromNow, recipient]
|
||||||
|
value = BigNumber.from(slippageAdjustedInput.raw.toString())
|
||||||
|
break
|
||||||
|
case SwapType.V1_EXACT_TOKENS_FOR_TOKENS:
|
||||||
|
methodNames = ['tokenToTokenTransferInput']
|
||||||
|
args = [
|
||||||
|
slippageAdjustedInput.raw.toString(),
|
||||||
|
slippageAdjustedOutput.raw.toString(),
|
||||||
|
1,
|
||||||
|
deadlineFromNow,
|
||||||
|
recipient,
|
||||||
|
trade.outputAmount.token.address
|
||||||
|
]
|
||||||
|
break
|
||||||
|
case SwapType.V1_EXACT_TOKENS_FOR_ETH:
|
||||||
|
methodNames = ['tokenToEthTransferOutput']
|
||||||
|
args = [
|
||||||
|
slippageAdjustedOutput.raw.toString(),
|
||||||
|
slippageAdjustedInput.raw.toString(),
|
||||||
|
deadlineFromNow,
|
||||||
|
recipient
|
||||||
|
]
|
||||||
|
break
|
||||||
|
case SwapType.V1_ETH_FOR_EXACT_TOKENS:
|
||||||
|
methodNames = ['ethToTokenTransferOutput']
|
||||||
|
args = [slippageAdjustedOutput.raw.toString(), deadlineFromNow, recipient]
|
||||||
|
value = BigNumber.from(slippageAdjustedInput.raw.toString())
|
||||||
|
break
|
||||||
|
case SwapType.V1_TOKENS_FOR_EXACT_ETH:
|
||||||
|
methodNames = ['tokenToEthTransferOutput']
|
||||||
|
args = [
|
||||||
|
slippageAdjustedOutput.raw.toString(),
|
||||||
|
slippageAdjustedInput.raw.toString(),
|
||||||
|
deadlineFromNow,
|
||||||
|
recipient
|
||||||
|
]
|
||||||
|
break
|
||||||
|
case SwapType.V1_TOKENS_FOR_EXACT_TOKENS:
|
||||||
|
methodNames = ['tokenToTokenTransferOutput']
|
||||||
|
args = [
|
||||||
|
slippageAdjustedOutput.raw.toString(),
|
||||||
|
slippageAdjustedInput.raw.toString(),
|
||||||
|
MaxUint256.toString(),
|
||||||
|
deadlineFromNow,
|
||||||
|
recipient,
|
||||||
|
trade.outputAmount.token.address
|
||||||
|
]
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new Error(`Unhandled swap type: ${swapType}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return estimate(...args, value ? { value } : {})
|
const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all(
|
||||||
.then(estimatedGasLimit =>
|
methodNames.map(methodName =>
|
||||||
method(...args, {
|
contract.estimateGas[methodName](...args, value ? { value } : {})
|
||||||
...(value ? { value } : {}),
|
.then(calculateGasMargin)
|
||||||
gasLimit: calculateGasMargin(estimatedGasLimit)
|
.catch(error => {
|
||||||
})
|
console.error(`estimateGas failed for ${methodName}`, error)
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
)
|
)
|
||||||
.then(response => {
|
)
|
||||||
if (recipient === account) {
|
|
||||||
addTransaction(response, {
|
|
||||||
summary:
|
|
||||||
'Swap ' +
|
|
||||||
slippageAdjustedInput.toSignificant(3) +
|
|
||||||
' ' +
|
|
||||||
trade.inputAmount.token.symbol +
|
|
||||||
' for ' +
|
|
||||||
slippageAdjustedOutput.toSignificant(3) +
|
|
||||||
' ' +
|
|
||||||
trade.outputAmount.token.symbol
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
addTransaction(response, {
|
|
||||||
summary:
|
|
||||||
'Swap ' +
|
|
||||||
slippageAdjustedInput.toSignificant(3) +
|
|
||||||
' ' +
|
|
||||||
trade.inputAmount.token.symbol +
|
|
||||||
' for ' +
|
|
||||||
slippageAdjustedOutput.toSignificant(3) +
|
|
||||||
' ' +
|
|
||||||
trade.outputAmount.token.symbol +
|
|
||||||
' to ' +
|
|
||||||
(ensName ?? recipient)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.hash
|
// we expect failures from left to right, so throw if we see failures
|
||||||
})
|
// from right to left
|
||||||
.catch(error => {
|
for (let i = 0; i < safeGasEstimates.length - 1; i++) {
|
||||||
console.error(`Swap or gas estimate failed`, error)
|
// if the FoT method fails, but the regular method does not, we should not
|
||||||
throw error
|
// use the regular method. this probably means something is wrong with the fot token.
|
||||||
|
if (BigNumber.isBigNumber(safeGasEstimates[i]) && !BigNumber.isBigNumber(safeGasEstimates[i + 1])) {
|
||||||
|
throw new Error(
|
||||||
|
'An error occurred. Please try raising your slippage. If that does not work, contact support.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexOfSuccessfulEstimation = safeGasEstimates.findIndex(safeGasEstimate =>
|
||||||
|
BigNumber.isBigNumber(safeGasEstimate)
|
||||||
|
)
|
||||||
|
|
||||||
|
// all estimations failed...
|
||||||
|
if (indexOfSuccessfulEstimation === -1) {
|
||||||
|
// if only 1 method exists, either:
|
||||||
|
// a) the token is doing something weird not related to FoT (e.g. enforcing a whitelist)
|
||||||
|
// b) the token is FoT and the user specified an exact output, which is not allowed
|
||||||
|
if (methodNames.length === 1) {
|
||||||
|
throw Error(
|
||||||
|
`An error occurred. If either of the tokens you're swapping take a fee on transfer, you must specify an exact input amount.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// if 2 methods exists, either:
|
||||||
|
// a) the token is doing something weird not related to FoT (e.g. enforcing a whitelist)
|
||||||
|
// b) the token is FoT and is taking more than the specified slippage
|
||||||
|
else if (methodNames.length === 2) {
|
||||||
|
throw Error(
|
||||||
|
`An error occurred. If either of the tokens you're swapping take a fee on transfer, you must specify a slippage tolerance higher than the fee.`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
throw Error('This transaction would fail. Please contact support.')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const methodName = methodNames[indexOfSuccessfulEstimation]
|
||||||
|
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
|
||||||
|
|
||||||
|
return contract[methodName](...args, {
|
||||||
|
gasLimit: safeGasEstimate,
|
||||||
|
...(value ? { value } : {})
|
||||||
})
|
})
|
||||||
|
.then((response: any) => {
|
||||||
|
const inputSymbol = trade.inputAmount.token.symbol
|
||||||
|
const outputSymbol = trade.outputAmount.token.symbol
|
||||||
|
const inputAmount = slippageAdjustedInput.toSignificant(3)
|
||||||
|
const outputAmount = slippageAdjustedOutput.toSignificant(3)
|
||||||
|
|
||||||
|
const base = `Swap ${inputAmount} ${inputSymbol} for ${outputAmount} ${outputSymbol}`
|
||||||
|
const withRecipient =
|
||||||
|
recipient === account
|
||||||
|
? base
|
||||||
|
: `${base} to ${
|
||||||
|
recipientAddressOrName && isAddress(recipientAddressOrName)
|
||||||
|
? shortenAddress(recipientAddressOrName)
|
||||||
|
: recipientAddressOrName
|
||||||
|
}`
|
||||||
|
|
||||||
|
const withVersion =
|
||||||
|
tradeVersion === Version.v2 ? withRecipient : `${withRecipient} on ${tradeVersion.toUpperCase()}`
|
||||||
|
|
||||||
|
addTransaction(response, {
|
||||||
|
summary: withVersion
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.hash
|
||||||
|
})
|
||||||
|
.catch((error: any) => {
|
||||||
|
// if the user rejected the tx, pass this along
|
||||||
|
if (error?.code === 4001) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
// otherwise, the error was unexpected and we need to convey that
|
||||||
|
else {
|
||||||
|
console.error(`Swap failed`, error, methodName, args, value)
|
||||||
|
throw Error('An error occurred while swapping. Please contact support.')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [account, allowedSlippage, addTransaction, chainId, deadline, inputAllowance, library, trade, ensName, recipient])
|
}, [
|
||||||
|
trade,
|
||||||
|
recipient,
|
||||||
|
library,
|
||||||
|
account,
|
||||||
|
tradeVersion,
|
||||||
|
chainId,
|
||||||
|
allowedSlippage,
|
||||||
|
inputAllowance,
|
||||||
|
v1Exchange,
|
||||||
|
deadline,
|
||||||
|
recipientAddressOrName,
|
||||||
|
addTransaction
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|||||||
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 XHR from 'i18next-xhr-backend'
|
||||||
import LanguageDetector from 'i18next-browser-languagedetector'
|
import LanguageDetector from 'i18next-browser-languagedetector'
|
||||||
|
|
||||||
const LOAD_PATH: string = process.env.PUBLIC_URL === '.' ? `./locales/{{lng}}.json` : '/locales/{{lng}}.json'
|
|
||||||
|
|
||||||
i18next
|
i18next
|
||||||
.use(XHR)
|
.use(XHR)
|
||||||
.use(LanguageDetector)
|
.use(LanguageDetector)
|
||||||
.use(initReactI18next)
|
.use(initReactI18next)
|
||||||
.init({
|
.init({
|
||||||
backend: {
|
backend: {
|
||||||
loadPath: LOAD_PATH
|
loadPath: `./locales/{{lng}}.json`
|
||||||
},
|
},
|
||||||
react: {
|
react: {
|
||||||
useSuspense: true
|
useSuspense: true
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import ReactDOM from 'react-dom'
|
|||||||
import ReactGA from 'react-ga'
|
import ReactGA from 'react-ga'
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
import { NetworkContextName } from './constants'
|
import { NetworkContextName } from './constants'
|
||||||
|
import 'inter-ui'
|
||||||
import './i18n'
|
import './i18n'
|
||||||
import App from './pages/App'
|
import App from './pages/App'
|
||||||
import store from './state'
|
import store from './state'
|
||||||
@@ -18,7 +19,9 @@ import ThemeProvider, { FixedGlobalStyle, ThemedGlobalStyle } from './theme'
|
|||||||
const Web3ProviderNetwork = createWeb3ReactRoot(NetworkContextName)
|
const Web3ProviderNetwork = createWeb3ReactRoot(NetworkContextName)
|
||||||
|
|
||||||
function getLibrary(provider: any): Web3Provider {
|
function getLibrary(provider: any): Web3Provider {
|
||||||
return new Web3Provider(provider)
|
const library = new Web3Provider(provider)
|
||||||
|
library.pollingInterval = 15000
|
||||||
|
return library
|
||||||
}
|
}
|
||||||
|
|
||||||
const GOOGLE_ANALYTICS_ID: string | undefined = process.env.REACT_APP_GOOGLE_ANALYTICS_ID
|
const GOOGLE_ANALYTICS_ID: string | undefined = process.env.REACT_APP_GOOGLE_ANALYTICS_ID
|
||||||
@@ -31,6 +34,13 @@ if (typeof GOOGLE_ANALYTICS_ID === 'string') {
|
|||||||
ReactGA.initialize('test', { testMode: true, debug: true })
|
ReactGA.initialize('test', { testMode: true, debug: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.addEventListener('error', error => {
|
||||||
|
ReactGA.exception({
|
||||||
|
description: `${error.message} @ ${error.filename}:${error.lineno}:${error.colno}`,
|
||||||
|
fatal: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
function Updaters() {
|
function Updaters() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
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 { BigNumber } from '@ethersproject/bignumber'
|
||||||
import { TokenAmount, WETH } from '@uniswap/sdk'
|
import { TransactionResponse } from '@ethersproject/providers'
|
||||||
import React, { useContext, useState } from 'react'
|
import { ChainId, Token, TokenAmount, WETH } from '@uniswap/sdk'
|
||||||
|
import React, { useCallback, useContext, useState } from 'react'
|
||||||
import { Plus } from 'react-feather'
|
import { Plus } from 'react-feather'
|
||||||
import ReactGA from 'react-ga'
|
import ReactGA from 'react-ga'
|
||||||
import { RouteComponentProps } from 'react-router-dom'
|
import { RouteComponentProps } from 'react-router-dom'
|
||||||
import { Text } from 'rebass'
|
import { Text } from 'rebass'
|
||||||
import { ThemeContext } from 'styled-components'
|
import { ThemeContext } from 'styled-components'
|
||||||
import { ButtonLight, ButtonPrimary, ButtonError } from '../../components/Button'
|
import { ButtonError, ButtonLight, ButtonPrimary } from '../../components/Button'
|
||||||
import { BlueCard, GreyCard, LightCard } from '../../components/Card'
|
import { BlueCard, GreyCard, LightCard } from '../../components/Card'
|
||||||
import { AutoColumn, ColumnCenter } from '../../components/Column'
|
import { AutoColumn, ColumnCenter } from '../../components/Column'
|
||||||
import ConfirmationModal from '../../components/ConfirmationModal'
|
import ConfirmationModal from '../../components/ConfirmationModal'
|
||||||
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
||||||
import DoubleLogo from '../../components/DoubleLogo'
|
import DoubleLogo from '../../components/DoubleLogo'
|
||||||
import PositionCard from '../../components/PositionCard'
|
import { AddRemoveTabs } from '../../components/NavigationTabs'
|
||||||
import Row, { AutoRow, RowBetween, RowFixed, RowFlat } from '../../components/Row'
|
import { MinimalPositionCard } from '../../components/PositionCard'
|
||||||
|
import Row, { RowBetween, RowFlat } from '../../components/Row'
|
||||||
|
|
||||||
import TokenLogo from '../../components/TokenLogo'
|
import { ROUTER_ADDRESS } from '../../constants'
|
||||||
|
|
||||||
import { ROUTER_ADDRESS, MIN_ETH, ONE_BIPS, DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
|
|
||||||
import { useActiveWeb3React } from '../../hooks'
|
import { useActiveWeb3React } from '../../hooks'
|
||||||
|
import { useToken } from '../../hooks/Tokens'
|
||||||
|
import { ApprovalState, useApproveCallback } from '../../hooks/useApproveCallback'
|
||||||
|
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||||
|
import { Field } from '../../state/mint/actions'
|
||||||
|
import { useDerivedMintInfo, useMintActionHandlers, useMintState } from '../../state/mint/hooks'
|
||||||
|
|
||||||
import { useTransactionAdder } from '../../state/transactions/hooks'
|
import { useTransactionAdder } from '../../state/transactions/hooks'
|
||||||
|
import { useIsExpertMode, useUserDeadline, useUserSlippageTolerance } from '../../state/user/hooks'
|
||||||
import { TYPE } from '../../theme'
|
import { TYPE } from '../../theme'
|
||||||
import { calculateGasMargin, calculateSlippageAmount, getRouterContract } from '../../utils'
|
import { calculateGasMargin, calculateSlippageAmount, getRouterContract } from '../../utils'
|
||||||
|
import { maxAmountSpend } from '../../utils/maxAmountSpend'
|
||||||
import AppBody from '../AppBody'
|
import AppBody from '../AppBody'
|
||||||
import { Dots, Wrapper } from '../Pool/styleds'
|
import { Dots, Wrapper } from '../Pool/styleds'
|
||||||
import {
|
import { ConfirmAddModalBottom } from './ConfirmAddModalBottom'
|
||||||
useDefaultsFromURLMatchParams,
|
import { currencyId } from './currencyId'
|
||||||
useMintState,
|
import { PoolPriceBar } from './PoolPriceBar'
|
||||||
useDerivedMintInfo,
|
|
||||||
useMintActionHandlers
|
|
||||||
} from '../../state/mint/hooks'
|
|
||||||
import { Field } from '../../state/mint/actions'
|
|
||||||
import { useApproveCallback, ApprovalState } from '../../hooks/useApproveCallback'
|
|
||||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
|
||||||
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
|
|
||||||
|
|
||||||
export default function AddLiquidity({ match: { params }, history }: RouteComponentProps<{ tokens: string }>) {
|
function useTokenByCurrencyId(chainId: ChainId | undefined, currencyId: string | undefined): Token | undefined {
|
||||||
useDefaultsFromURLMatchParams(params)
|
const isETH = currencyId?.toUpperCase() === 'ETH'
|
||||||
|
const token = useToken(isETH ? undefined : currencyId)
|
||||||
|
return isETH && chainId ? WETH[chainId] : token ?? undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AddLiquidity({
|
||||||
|
match: {
|
||||||
|
params: { currencyIdA, currencyIdB }
|
||||||
|
},
|
||||||
|
history
|
||||||
|
}: RouteComponentProps<{ currencyIdA?: string; currencyIdB?: string }>) {
|
||||||
const { account, chainId, library } = useActiveWeb3React()
|
const { account, chainId, library } = useActiveWeb3React()
|
||||||
const theme = useContext(ThemeContext)
|
const theme = useContext(ThemeContext)
|
||||||
|
|
||||||
|
const tokenA = useTokenByCurrencyId(chainId, currencyIdA)
|
||||||
|
const tokenB = useTokenByCurrencyId(chainId, currencyIdB)
|
||||||
|
|
||||||
// toggle wallet when disconnected
|
// toggle wallet when disconnected
|
||||||
const toggleWalletModal = useWalletModalToggle()
|
const toggleWalletModal = useWalletModalToggle()
|
||||||
|
|
||||||
|
const expertMode = useIsExpertMode()
|
||||||
|
|
||||||
// mint state
|
// mint state
|
||||||
const { independentField, typedValue, otherTypedValue } = useMintState()
|
const { independentField, typedValue, otherTypedValue } = useMintState()
|
||||||
const {
|
const {
|
||||||
@@ -58,21 +72,32 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
|
|||||||
liquidityMinted,
|
liquidityMinted,
|
||||||
poolTokenPercentage,
|
poolTokenPercentage,
|
||||||
error
|
error
|
||||||
} = useDerivedMintInfo()
|
} = useDerivedMintInfo(tokenA ?? undefined, tokenB ?? undefined)
|
||||||
const { onUserInput } = useMintActionHandlers()
|
const { onUserInput } = useMintActionHandlers(noLiquidity)
|
||||||
|
|
||||||
|
const handleTokenAInput = useCallback(
|
||||||
|
(field: string, value: string) => {
|
||||||
|
return onUserInput(Field.TOKEN_A, value)
|
||||||
|
},
|
||||||
|
[onUserInput]
|
||||||
|
)
|
||||||
|
const handleTokenBInput = useCallback(
|
||||||
|
(field: string, value: string) => {
|
||||||
|
return onUserInput(Field.TOKEN_B, value)
|
||||||
|
},
|
||||||
|
[onUserInput]
|
||||||
|
)
|
||||||
|
|
||||||
const isValid = !error
|
const isValid = !error
|
||||||
|
|
||||||
// modal and loading
|
// modal and loading
|
||||||
const [showConfirm, setShowConfirm] = useState<boolean>(false)
|
const [showConfirm, setShowConfirm] = useState<boolean>(false)
|
||||||
const [showAdvanced, setShowAdvanced] = useState<boolean>(false)
|
|
||||||
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirm
|
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirm
|
||||||
const [pendingConfirmation, setPendingConfirmation] = useState<boolean>(true) // waiting for user confirmation
|
|
||||||
|
|
||||||
// txn values
|
// txn values
|
||||||
|
const [deadline] = useUserDeadline() // custom from users settings
|
||||||
|
const [allowedSlippage] = useUserSlippageTolerance() // custom from users
|
||||||
const [txHash, setTxHash] = useState<string>('')
|
const [txHash, setTxHash] = useState<string>('')
|
||||||
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
|
|
||||||
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
|
|
||||||
|
|
||||||
// get formatted amounts
|
// get formatted amounts
|
||||||
const formattedAmounts = {
|
const formattedAmounts = {
|
||||||
@@ -84,17 +109,7 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
|
|||||||
const maxAmounts: { [field in Field]?: TokenAmount } = [Field.TOKEN_A, Field.TOKEN_B].reduce((accumulator, field) => {
|
const maxAmounts: { [field in Field]?: TokenAmount } = [Field.TOKEN_A, Field.TOKEN_B].reduce((accumulator, field) => {
|
||||||
return {
|
return {
|
||||||
...accumulator,
|
...accumulator,
|
||||||
[field]:
|
[field]: maxAmountSpend(tokenBalances[field])
|
||||||
!!tokenBalances[field] &&
|
|
||||||
!!tokens[field] &&
|
|
||||||
!!WETH[chainId] &&
|
|
||||||
tokenBalances[field].greaterThan(
|
|
||||||
new TokenAmount(tokens[field], tokens[field].equals(WETH[chainId]) ? MIN_ETH : '0')
|
|
||||||
)
|
|
||||||
? tokens[field].equals(WETH[chainId])
|
|
||||||
? tokenBalances[field].subtract(new TokenAmount(WETH[chainId], MIN_ETH))
|
|
||||||
: tokenBalances[field]
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
@@ -102,7 +117,7 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
|
|||||||
(accumulator, field) => {
|
(accumulator, field) => {
|
||||||
return {
|
return {
|
||||||
...accumulator,
|
...accumulator,
|
||||||
[field]: maxAmounts[field] && parsedAmounts[field] ? maxAmounts[field].equalTo(parsedAmounts[field]) : undefined
|
[field]: maxAmounts[field]?.equalTo(parsedAmounts[field] ?? '0')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
@@ -113,40 +128,48 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
|
|||||||
const [approvalB, approveBCallback] = useApproveCallback(parsedAmounts[Field.TOKEN_B], ROUTER_ADDRESS)
|
const [approvalB, approveBCallback] = useApproveCallback(parsedAmounts[Field.TOKEN_B], ROUTER_ADDRESS)
|
||||||
|
|
||||||
const addTransaction = useTransactionAdder()
|
const addTransaction = useTransactionAdder()
|
||||||
async function onAdd() {
|
|
||||||
setAttemptingTxn(true)
|
|
||||||
|
|
||||||
|
async function onAdd() {
|
||||||
|
if (!chainId || !library || !account) return
|
||||||
const router = getRouterContract(chainId, library, account)
|
const router = getRouterContract(chainId, library, account)
|
||||||
|
|
||||||
|
const { [Field.TOKEN_A]: parsedAmountA, [Field.TOKEN_B]: parsedAmountB } = parsedAmounts
|
||||||
|
if (!parsedAmountA || !parsedAmountB || !tokenA || !tokenB) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const amountsMin = {
|
const amountsMin = {
|
||||||
[Field.TOKEN_A]: calculateSlippageAmount(parsedAmounts[Field.TOKEN_A], noLiquidity ? 0 : allowedSlippage)[0],
|
[Field.TOKEN_A]: calculateSlippageAmount(parsedAmountA, noLiquidity ? 0 : allowedSlippage)[0],
|
||||||
[Field.TOKEN_B]: calculateSlippageAmount(parsedAmounts[Field.TOKEN_B], noLiquidity ? 0 : allowedSlippage)[0]
|
[Field.TOKEN_B]: calculateSlippageAmount(parsedAmountB, noLiquidity ? 0 : allowedSlippage)[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline
|
const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline
|
||||||
|
|
||||||
let estimate, method: Function, args: Array<string | string[] | number>, value: BigNumber | null
|
let estimate,
|
||||||
if (tokens[Field.TOKEN_A].equals(WETH[chainId]) || tokens[Field.TOKEN_B].equals(WETH[chainId])) {
|
method: (...args: any) => Promise<TransactionResponse>,
|
||||||
const tokenBIsETH = tokens[Field.TOKEN_B].equals(WETH[chainId])
|
args: Array<string | string[] | number>,
|
||||||
|
value: BigNumber | null
|
||||||
|
if (tokenA.equals(WETH[chainId]) || tokenB.equals(WETH[chainId])) {
|
||||||
|
const tokenBIsETH = tokenB.equals(WETH[chainId])
|
||||||
estimate = router.estimateGas.addLiquidityETH
|
estimate = router.estimateGas.addLiquidityETH
|
||||||
method = router.addLiquidityETH
|
method = router.addLiquidityETH
|
||||||
args = [
|
args = [
|
||||||
tokens[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].address, // token
|
(tokenBIsETH ? tokenA : tokenB).address, // token
|
||||||
parsedAmounts[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].raw.toString(), // token desired
|
(tokenBIsETH ? parsedAmountA : parsedAmountB).raw.toString(), // token desired
|
||||||
amountsMin[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].toString(), // token min
|
amountsMin[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].toString(), // token min
|
||||||
amountsMin[tokenBIsETH ? Field.TOKEN_B : Field.TOKEN_A].toString(), // eth min
|
amountsMin[tokenBIsETH ? Field.TOKEN_B : Field.TOKEN_A].toString(), // eth min
|
||||||
account,
|
account,
|
||||||
deadlineFromNow
|
deadlineFromNow
|
||||||
]
|
]
|
||||||
value = BigNumber.from(parsedAmounts[tokenBIsETH ? Field.TOKEN_B : Field.TOKEN_A].raw.toString())
|
value = BigNumber.from((tokenBIsETH ? parsedAmountB : parsedAmountA).raw.toString())
|
||||||
} else {
|
} else {
|
||||||
estimate = router.estimateGas.addLiquidity
|
estimate = router.estimateGas.addLiquidity
|
||||||
method = router.addLiquidity
|
method = router.addLiquidity
|
||||||
args = [
|
args = [
|
||||||
tokens[Field.TOKEN_A].address,
|
tokenA.address,
|
||||||
tokens[Field.TOKEN_B].address,
|
tokenB.address,
|
||||||
parsedAmounts[Field.TOKEN_A].raw.toString(),
|
parsedAmountA.raw.toString(),
|
||||||
parsedAmounts[Field.TOKEN_B].raw.toString(),
|
parsedAmountB.raw.toString(),
|
||||||
amountsMin[Field.TOKEN_A].toString(),
|
amountsMin[Field.TOKEN_A].toString(),
|
||||||
amountsMin[Field.TOKEN_B].toString(),
|
amountsMin[Field.TOKEN_B].toString(),
|
||||||
account,
|
account,
|
||||||
@@ -155,12 +178,15 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
|
|||||||
value = null
|
value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setAttemptingTxn(true)
|
||||||
await estimate(...args, value ? { value } : {})
|
await estimate(...args, value ? { value } : {})
|
||||||
.then(estimatedGasLimit =>
|
.then(estimatedGasLimit =>
|
||||||
method(...args, {
|
method(...args, {
|
||||||
...(value ? { value } : {}),
|
...(value ? { value } : {}),
|
||||||
gasLimit: calculateGasMargin(estimatedGasLimit)
|
gasLimit: calculateGasMargin(estimatedGasLimit)
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
|
setAttemptingTxn(false)
|
||||||
|
|
||||||
addTransaction(response, {
|
addTransaction(response, {
|
||||||
summary:
|
summary:
|
||||||
'Add ' +
|
'Add ' +
|
||||||
@@ -174,7 +200,6 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
|
|||||||
})
|
})
|
||||||
|
|
||||||
setTxHash(response.hash)
|
setTxHash(response.hash)
|
||||||
setPendingConfirmation(false)
|
|
||||||
|
|
||||||
ReactGA.event({
|
ReactGA.event({
|
||||||
category: 'Liquidity',
|
category: 'Liquidity',
|
||||||
@@ -183,12 +208,12 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.catch((e: Error) => {
|
.catch(error => {
|
||||||
console.error(e)
|
|
||||||
setPendingConfirmation(true)
|
|
||||||
setAttemptingTxn(false)
|
setAttemptingTxn(false)
|
||||||
setShowConfirm(false)
|
// we only care if the error is something _other_ than the user rejected the tx
|
||||||
setShowAdvanced(false)
|
if (error?.code !== 4001) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,76 +252,14 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
|
|||||||
|
|
||||||
const modalBottom = () => {
|
const modalBottom = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<ConfirmAddModalBottom
|
||||||
<RowBetween>
|
price={price}
|
||||||
<TYPE.body>{tokens[Field.TOKEN_A]?.symbol} Deposited</TYPE.body>
|
tokens={tokens}
|
||||||
<RowFixed>
|
parsedAmounts={parsedAmounts}
|
||||||
<TokenLogo address={tokens[Field.TOKEN_A]?.address} style={{ marginRight: '8px' }} />
|
noLiquidity={noLiquidity}
|
||||||
<TYPE.body>{parsedAmounts[Field.TOKEN_A]?.toSignificant(6)}</TYPE.body>
|
onAdd={onAdd}
|
||||||
</RowFixed>
|
poolTokenPercentage={poolTokenPercentage}
|
||||||
</RowBetween>
|
/>
|
||||||
<RowBetween>
|
|
||||||
<TYPE.body>{tokens[Field.TOKEN_B]?.symbol} Deposited</TYPE.body>
|
|
||||||
<RowFixed>
|
|
||||||
<TokenLogo address={tokens[Field.TOKEN_B]?.address} style={{ marginRight: '8px' }} />
|
|
||||||
<TYPE.body>{parsedAmounts[Field.TOKEN_B]?.toSignificant(6)}</TYPE.body>
|
|
||||||
</RowFixed>
|
|
||||||
</RowBetween>
|
|
||||||
<RowBetween>
|
|
||||||
<TYPE.body>Rates</TYPE.body>
|
|
||||||
<TYPE.body>
|
|
||||||
{`1 ${tokens[Field.TOKEN_A]?.symbol} = ${price?.toSignificant(4)} ${tokens[Field.TOKEN_B]?.symbol}`}
|
|
||||||
</TYPE.body>
|
|
||||||
</RowBetween>
|
|
||||||
<RowBetween style={{ justifyContent: 'flex-end' }}>
|
|
||||||
<TYPE.body>
|
|
||||||
{`1 ${tokens[Field.TOKEN_B]?.symbol} = ${price?.invert().toSignificant(4)} ${
|
|
||||||
tokens[Field.TOKEN_A]?.symbol
|
|
||||||
}`}
|
|
||||||
</TYPE.body>
|
|
||||||
</RowBetween>
|
|
||||||
<RowBetween>
|
|
||||||
<TYPE.body>Share of Pool:</TYPE.body>
|
|
||||||
<TYPE.body>{noLiquidity ? '100' : poolTokenPercentage?.toSignificant(4)}%</TYPE.body>
|
|
||||||
</RowBetween>
|
|
||||||
<ButtonPrimary style={{ margin: '20px 0 0 0' }} onClick={onAdd}>
|
|
||||||
<Text fontWeight={500} fontSize={20}>
|
|
||||||
{noLiquidity ? 'Create Pool & Supply' : 'Confirm Supply'}
|
|
||||||
</Text>
|
|
||||||
</ButtonPrimary>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const PriceBar = () => {
|
|
||||||
return (
|
|
||||||
<AutoColumn gap="md" justify="space-between">
|
|
||||||
<AutoRow justify="space-between">
|
|
||||||
<AutoColumn justify="center">
|
|
||||||
<TYPE.black>{price?.toSignificant(6) ?? '0'}</TYPE.black>
|
|
||||||
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
|
|
||||||
{tokens[Field.TOKEN_B]?.symbol} per {tokens[Field.TOKEN_A]?.symbol}
|
|
||||||
</Text>
|
|
||||||
</AutoColumn>
|
|
||||||
<AutoColumn justify="center">
|
|
||||||
<TYPE.black>{price?.invert().toSignificant(6) ?? '0'}</TYPE.black>
|
|
||||||
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
|
|
||||||
{tokens[Field.TOKEN_A]?.symbol} per {tokens[Field.TOKEN_B]?.symbol}
|
|
||||||
</Text>
|
|
||||||
</AutoColumn>
|
|
||||||
<AutoColumn justify="center">
|
|
||||||
<TYPE.black>
|
|
||||||
{noLiquidity && price
|
|
||||||
? '100'
|
|
||||||
: (poolTokenPercentage?.lessThan(ONE_BIPS) ? '<0.01' : poolTokenPercentage?.toFixed(2)) ?? '0'}
|
|
||||||
%
|
|
||||||
</TYPE.black>
|
|
||||||
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
|
|
||||||
Share of Pool
|
|
||||||
</Text>
|
|
||||||
</AutoColumn>
|
|
||||||
</AutoRow>
|
|
||||||
</AutoColumn>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,24 +267,52 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
|
|||||||
tokens[Field.TOKEN_A]?.symbol
|
tokens[Field.TOKEN_A]?.symbol
|
||||||
} and ${parsedAmounts[Field.TOKEN_B]?.toSignificant(6)} ${tokens[Field.TOKEN_B]?.symbol}`
|
} and ${parsedAmounts[Field.TOKEN_B]?.toSignificant(6)} ${tokens[Field.TOKEN_B]?.symbol}`
|
||||||
|
|
||||||
|
const handleTokenASelect = useCallback(
|
||||||
|
(tokenAddress: string) => {
|
||||||
|
const [tokenAId, tokenBId] = [
|
||||||
|
currencyId(chainId, tokenAddress),
|
||||||
|
tokenB ? currencyId(chainId, tokenB.address) : undefined
|
||||||
|
]
|
||||||
|
if (tokenAId === tokenBId) {
|
||||||
|
history.push(`/add/${tokenAId}/${tokenA ? currencyId(chainId, tokenA.address) : ''}`)
|
||||||
|
} else {
|
||||||
|
history.push(`/add/${tokenAId}/${tokenBId}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[chainId, tokenB, history, tokenA]
|
||||||
|
)
|
||||||
|
const handleTokenBSelect = useCallback(
|
||||||
|
(tokenAddress: string) => {
|
||||||
|
const [tokenAId, tokenBId] = [
|
||||||
|
tokenA ? currencyId(chainId, tokenA.address) : undefined,
|
||||||
|
currencyId(chainId, tokenAddress)
|
||||||
|
]
|
||||||
|
if (tokenAId === tokenBId) {
|
||||||
|
history.push(`/add/${tokenB ? currencyId(chainId, tokenB.address) : ''}/${tokenAId}`)
|
||||||
|
} else {
|
||||||
|
history.push(`/add/${currencyIdA ? currencyIdA : 'ETH'}/${currencyId(chainId, tokenAddress)}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[tokenA, chainId, history, tokenB, currencyIdA]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppBody>
|
<AppBody>
|
||||||
|
<AddRemoveTabs adding={true} />
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
isOpen={showConfirm}
|
isOpen={showConfirm}
|
||||||
onDismiss={() => {
|
onDismiss={() => {
|
||||||
if (attemptingTxn) {
|
|
||||||
history.push('/pool')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setPendingConfirmation(true)
|
|
||||||
setAttemptingTxn(false)
|
|
||||||
setShowConfirm(false)
|
setShowConfirm(false)
|
||||||
|
// if there was a tx hash, we want to clear the input
|
||||||
|
if (txHash) {
|
||||||
|
onUserInput(Field.TOKEN_A, '')
|
||||||
|
}
|
||||||
|
setTxHash('')
|
||||||
}}
|
}}
|
||||||
attemptingTxn={attemptingTxn}
|
attemptingTxn={attemptingTxn}
|
||||||
pendingConfirmation={pendingConfirmation}
|
hash={txHash}
|
||||||
hash={txHash ? txHash : ''}
|
|
||||||
topContent={() => modalHeader()}
|
topContent={() => modalHeader()}
|
||||||
bottomContent={modalBottom}
|
bottomContent={modalBottom}
|
||||||
pendingText={pendingText}
|
pendingText={pendingText}
|
||||||
@@ -346,34 +337,35 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
|
|||||||
</ColumnCenter>
|
</ColumnCenter>
|
||||||
)}
|
)}
|
||||||
<CurrencyInputPanel
|
<CurrencyInputPanel
|
||||||
disableTokenSelect={true}
|
|
||||||
field={Field.TOKEN_A}
|
field={Field.TOKEN_A}
|
||||||
value={formattedAmounts[Field.TOKEN_A]}
|
value={formattedAmounts[Field.TOKEN_A]}
|
||||||
onUserInput={onUserInput}
|
onUserInput={handleTokenAInput}
|
||||||
onMax={() => {
|
onMax={() => {
|
||||||
maxAmounts[Field.TOKEN_A] && onUserInput(Field.TOKEN_A, maxAmounts[Field.TOKEN_A].toExact())
|
onUserInput(Field.TOKEN_A, maxAmounts[Field.TOKEN_A]?.toExact() ?? '')
|
||||||
}}
|
}}
|
||||||
|
onTokenSelection={handleTokenASelect}
|
||||||
showMaxButton={!atMaxAmounts[Field.TOKEN_A]}
|
showMaxButton={!atMaxAmounts[Field.TOKEN_A]}
|
||||||
token={tokens[Field.TOKEN_A]}
|
token={tokens[Field.TOKEN_A]}
|
||||||
pair={pair}
|
pair={pair}
|
||||||
label="Input"
|
|
||||||
id="add-liquidity-input-tokena"
|
id="add-liquidity-input-tokena"
|
||||||
|
showCommonBases
|
||||||
/>
|
/>
|
||||||
<ColumnCenter>
|
<ColumnCenter>
|
||||||
<Plus size="16" color={theme.text2} />
|
<Plus size="16" color={theme.text2} />
|
||||||
</ColumnCenter>
|
</ColumnCenter>
|
||||||
<CurrencyInputPanel
|
<CurrencyInputPanel
|
||||||
disableTokenSelect={true}
|
|
||||||
field={Field.TOKEN_B}
|
field={Field.TOKEN_B}
|
||||||
value={formattedAmounts[Field.TOKEN_B]}
|
value={formattedAmounts[Field.TOKEN_B]}
|
||||||
onUserInput={onUserInput}
|
onUserInput={handleTokenBInput}
|
||||||
|
onTokenSelection={handleTokenBSelect}
|
||||||
onMax={() => {
|
onMax={() => {
|
||||||
maxAmounts[Field.TOKEN_B] && onUserInput(Field.TOKEN_B, maxAmounts[Field.TOKEN_B].toExact())
|
onUserInput(Field.TOKEN_B, maxAmounts[Field.TOKEN_B]?.toExact() ?? '')
|
||||||
}}
|
}}
|
||||||
showMaxButton={!atMaxAmounts[Field.TOKEN_B]}
|
showMaxButton={!atMaxAmounts[Field.TOKEN_B]}
|
||||||
token={tokens[Field.TOKEN_B]}
|
token={tokens[Field.TOKEN_B]}
|
||||||
pair={pair}
|
pair={pair}
|
||||||
id="add-liquidity-input-tokenb"
|
id="add-liquidity-input-tokenb"
|
||||||
|
showCommonBases
|
||||||
/>
|
/>
|
||||||
{tokens[Field.TOKEN_A] && tokens[Field.TOKEN_B] && (
|
{tokens[Field.TOKEN_A] && tokens[Field.TOKEN_B] && (
|
||||||
<>
|
<>
|
||||||
@@ -384,7 +376,12 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
|
|||||||
</TYPE.subHeader>
|
</TYPE.subHeader>
|
||||||
</RowBetween>{' '}
|
</RowBetween>{' '}
|
||||||
<LightCard padding="1rem" borderRadius={'20px'}>
|
<LightCard padding="1rem" borderRadius={'20px'}>
|
||||||
<PriceBar />
|
<PoolPriceBar
|
||||||
|
tokens={tokens}
|
||||||
|
poolTokenPercentage={poolTokenPercentage}
|
||||||
|
noLiquidity={noLiquidity}
|
||||||
|
price={price}
|
||||||
|
/>
|
||||||
</LightCard>
|
</LightCard>
|
||||||
</GreyCard>
|
</GreyCard>
|
||||||
</>
|
</>
|
||||||
@@ -392,53 +389,62 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
|
|||||||
|
|
||||||
{!account ? (
|
{!account ? (
|
||||||
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
|
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
|
||||||
) : approvalA === ApprovalState.NOT_APPROVED || approvalA === ApprovalState.PENDING ? (
|
|
||||||
<ButtonLight onClick={approveACallback} disabled={approvalA === ApprovalState.PENDING}>
|
|
||||||
{approvalA === ApprovalState.PENDING ? (
|
|
||||||
<Dots>Approving {tokens[Field.TOKEN_A]?.symbol}</Dots>
|
|
||||||
) : (
|
|
||||||
'Approve ' + tokens[Field.TOKEN_A]?.symbol
|
|
||||||
)}
|
|
||||||
</ButtonLight>
|
|
||||||
) : approvalB === ApprovalState.NOT_APPROVED || approvalB === ApprovalState.PENDING ? (
|
|
||||||
<ButtonLight onClick={approveBCallback} disabled={approvalB === ApprovalState.PENDING}>
|
|
||||||
{approvalB === ApprovalState.PENDING ? (
|
|
||||||
<Dots>Approving {tokens[Field.TOKEN_B]?.symbol}</Dots>
|
|
||||||
) : (
|
|
||||||
'Approve ' + tokens[Field.TOKEN_B]?.symbol
|
|
||||||
)}
|
|
||||||
</ButtonLight>
|
|
||||||
) : (
|
) : (
|
||||||
<ButtonError
|
<AutoColumn gap={'md'}>
|
||||||
onClick={() => {
|
{(approvalA === ApprovalState.NOT_APPROVED ||
|
||||||
setShowConfirm(true)
|
approvalA === ApprovalState.PENDING ||
|
||||||
}}
|
approvalB === ApprovalState.NOT_APPROVED ||
|
||||||
disabled={!isValid}
|
approvalB === ApprovalState.PENDING) &&
|
||||||
error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]}
|
isValid && (
|
||||||
>
|
<RowBetween>
|
||||||
<Text fontSize={20} fontWeight={500}>
|
{approvalA !== ApprovalState.APPROVED && (
|
||||||
{error ?? 'Supply'}
|
<ButtonPrimary
|
||||||
</Text>
|
onClick={approveACallback}
|
||||||
</ButtonError>
|
disabled={approvalA === ApprovalState.PENDING}
|
||||||
|
width={approvalB !== ApprovalState.APPROVED ? '48%' : '100%'}
|
||||||
|
>
|
||||||
|
{approvalA === ApprovalState.PENDING ? (
|
||||||
|
<Dots>Approving {tokens[Field.TOKEN_A]?.symbol}</Dots>
|
||||||
|
) : (
|
||||||
|
'Approve ' + tokens[Field.TOKEN_A]?.symbol
|
||||||
|
)}
|
||||||
|
</ButtonPrimary>
|
||||||
|
)}
|
||||||
|
{approvalB !== ApprovalState.APPROVED && (
|
||||||
|
<ButtonPrimary
|
||||||
|
onClick={approveBCallback}
|
||||||
|
disabled={approvalB === ApprovalState.PENDING}
|
||||||
|
width={approvalA !== ApprovalState.APPROVED ? '48%' : '100%'}
|
||||||
|
>
|
||||||
|
{approvalB === ApprovalState.PENDING ? (
|
||||||
|
<Dots>Approving {tokens[Field.TOKEN_B]?.symbol}</Dots>
|
||||||
|
) : (
|
||||||
|
'Approve ' + tokens[Field.TOKEN_B]?.symbol
|
||||||
|
)}
|
||||||
|
</ButtonPrimary>
|
||||||
|
)}
|
||||||
|
</RowBetween>
|
||||||
|
)}
|
||||||
|
<ButtonError
|
||||||
|
onClick={() => {
|
||||||
|
expertMode ? onAdd() : setShowConfirm(true)
|
||||||
|
}}
|
||||||
|
disabled={!isValid || approvalA !== ApprovalState.APPROVED || approvalB !== ApprovalState.APPROVED}
|
||||||
|
error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]}
|
||||||
|
>
|
||||||
|
<Text fontSize={20} fontWeight={500}>
|
||||||
|
{error ?? 'Supply'}
|
||||||
|
</Text>
|
||||||
|
</ButtonError>
|
||||||
|
</AutoColumn>
|
||||||
)}
|
)}
|
||||||
</AutoColumn>
|
</AutoColumn>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
</AppBody>
|
</AppBody>
|
||||||
|
|
||||||
{isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B] ? (
|
|
||||||
<AdvancedSwapDetailsDropdown
|
|
||||||
rawSlippage={allowedSlippage}
|
|
||||||
deadline={deadline}
|
|
||||||
showAdvanced={showAdvanced}
|
|
||||||
setShowAdvanced={setShowAdvanced}
|
|
||||||
setDeadline={setDeadline}
|
|
||||||
setRawSlippage={setAllowedSlippage}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{pair && !noLiquidity ? (
|
{pair && !noLiquidity ? (
|
||||||
<AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
|
<AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
|
||||||
<PositionCard pair={pair} minimal={true} />
|
<MinimalPositionCard pair={pair} />
|
||||||
</AutoColumn>
|
</AutoColumn>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
|
|||||||
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,18 +1,23 @@
|
|||||||
import React, { Suspense } from 'react'
|
import React, { Suspense } from 'react'
|
||||||
import { BrowserRouter, HashRouter, Route, Switch } from 'react-router-dom'
|
import { HashRouter, Route, Switch } from 'react-router-dom'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import GoogleAnalyticsReporter from '../components/analytics/GoogleAnalyticsReporter'
|
import GoogleAnalyticsReporter from '../components/analytics/GoogleAnalyticsReporter'
|
||||||
import Footer from '../components/Footer'
|
|
||||||
import Header from '../components/Header'
|
import Header from '../components/Header'
|
||||||
import Popups from '../components/Popups'
|
import Popups from '../components/Popups'
|
||||||
import Web3ReactManager from '../components/Web3ReactManager'
|
import Web3ReactManager from '../components/Web3ReactManager'
|
||||||
import DarkModeQueryParamReader from '../theme/DarkModeQueryParamReader'
|
import DarkModeQueryParamReader from '../theme/DarkModeQueryParamReader'
|
||||||
import AddLiquidity from './AddLiquidity'
|
import AddLiquidity from './AddLiquidity'
|
||||||
import CreatePool from './CreatePool'
|
import {
|
||||||
|
RedirectDuplicateTokenIds,
|
||||||
|
RedirectOldAddLiquidityPathStructure,
|
||||||
|
RedirectToAddLiquidity
|
||||||
|
} from './AddLiquidity/redirects'
|
||||||
|
import MigrateV1 from './MigrateV1'
|
||||||
|
import MigrateV1Exchange from './MigrateV1/MigrateV1Exchange'
|
||||||
|
import RemoveV1Exchange from './MigrateV1/RemoveV1Exchange'
|
||||||
import Pool from './Pool'
|
import Pool from './Pool'
|
||||||
import PoolFinder from './PoolFinder'
|
import PoolFinder from './PoolFinder'
|
||||||
import RemoveLiquidity from './RemoveLiquidity'
|
import RemoveLiquidity from './RemoveLiquidity'
|
||||||
import Send from './Send'
|
|
||||||
import Swap from './Swap'
|
import Swap from './Swap'
|
||||||
import { RedirectPathToSwapOnly, RedirectToSwap } from './Swap/redirects'
|
import { RedirectPathToSwapOnly, RedirectToSwap } from './Swap/redirects'
|
||||||
|
|
||||||
@@ -47,40 +52,14 @@ const BodyWrapper = styled.div`
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
`
|
`
|
||||||
|
|
||||||
const BackgroundGradient = styled.div`
|
|
||||||
width: 100%;
|
|
||||||
height: 200vh;
|
|
||||||
background: ${({ theme }) => `radial-gradient(50% 50% at 50% 50%, ${theme.primary1} 0%, ${theme.bg1} 100%)`};
|
|
||||||
position: absolute;
|
|
||||||
top: 0px;
|
|
||||||
left: 0px;
|
|
||||||
opacity: 0.1;
|
|
||||||
z-index: -1;
|
|
||||||
|
|
||||||
transform: translateY(-70vh);
|
|
||||||
|
|
||||||
@media (max-width: 960px) {
|
|
||||||
height: 300px;
|
|
||||||
width: 100%;
|
|
||||||
transform: translateY(-150px);
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const Marginer = styled.div`
|
const Marginer = styled.div`
|
||||||
margin-top: 5rem;
|
margin-top: 5rem;
|
||||||
`
|
`
|
||||||
|
|
||||||
let Router: React.ComponentType
|
|
||||||
if (process.env.PUBLIC_URL === '.') {
|
|
||||||
Router = HashRouter
|
|
||||||
} else {
|
|
||||||
Router = BrowserRouter
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<Router>
|
<HashRouter>
|
||||||
<Route component={GoogleAnalyticsReporter} />
|
<Route component={GoogleAnalyticsReporter} />
|
||||||
<Route component={DarkModeQueryParamReader} />
|
<Route component={DarkModeQueryParamReader} />
|
||||||
<AppWrapper>
|
<AppWrapper>
|
||||||
@@ -93,21 +72,24 @@ export default function App() {
|
|||||||
<Switch>
|
<Switch>
|
||||||
<Route exact strict path="/swap" component={Swap} />
|
<Route exact strict path="/swap" component={Swap} />
|
||||||
<Route exact strict path="/swap/:outputCurrency" component={RedirectToSwap} />
|
<Route exact strict path="/swap/:outputCurrency" component={RedirectToSwap} />
|
||||||
<Route exact strict path="/send" component={Send} />
|
<Route exact strict path="/send" component={RedirectPathToSwapOnly} />
|
||||||
<Route exact strict path="/find" component={PoolFinder} />
|
<Route exact strict path="/find" component={PoolFinder} />
|
||||||
<Route exact strict path="/pool" component={Pool} />
|
<Route exact strict path="/pool" component={Pool} />
|
||||||
<Route exact strict path="/create" component={CreatePool} />
|
<Route exact strict path="/create" component={RedirectToAddLiquidity} />
|
||||||
<Route exact strict path="/add/:tokens" component={AddLiquidity} />
|
<Route exact path="/add" component={AddLiquidity} />
|
||||||
|
<Route exact path="/add/:currencyIdA" component={RedirectOldAddLiquidityPathStructure} />
|
||||||
|
<Route exact path="/add/:currencyIdA/:currencyIdB" component={RedirectDuplicateTokenIds} />
|
||||||
<Route exact strict path="/remove/:tokens" component={RemoveLiquidity} />
|
<Route exact strict path="/remove/:tokens" component={RemoveLiquidity} />
|
||||||
|
<Route exact strict path="/migrate/v1" component={MigrateV1} />
|
||||||
|
<Route exact strict path="/migrate/v1/:address" component={MigrateV1Exchange} />
|
||||||
|
<Route exact strict path="/remove/v1/:address" component={RemoveV1Exchange} />
|
||||||
<Route component={RedirectPathToSwapOnly} />
|
<Route component={RedirectPathToSwapOnly} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</Web3ReactManager>
|
</Web3ReactManager>
|
||||||
<Marginer />
|
<Marginer />
|
||||||
<Footer />
|
|
||||||
</BodyWrapper>
|
</BodyWrapper>
|
||||||
<BackgroundGradient />
|
|
||||||
</AppWrapper>
|
</AppWrapper>
|
||||||
</Router>
|
</HashRouter>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import NavigationTabs from '../components/NavigationTabs'
|
|
||||||
|
|
||||||
const Body = styled.div`
|
export const BodyWrapper = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
max-width: 420px;
|
max-width: 420px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -17,10 +16,5 @@ const Body = styled.div`
|
|||||||
* The styled container element that wraps the content of most pages and the tabs.
|
* The styled container element that wraps the content of most pages and the tabs.
|
||||||
*/
|
*/
|
||||||
export default function AppBody({ children }: { children: React.ReactNode }) {
|
export default function AppBody({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return <BodyWrapper>{children}</BodyWrapper>
|
||||||
<Body>
|
|
||||||
<NavigationTabs />
|
|
||||||
<>{children}</>
|
|
||||||
</Body>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,146 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
|
||||||
import { RouteComponentProps, Redirect } from 'react-router-dom'
|
|
||||||
import { Token, WETH } from '@uniswap/sdk'
|
|
||||||
import AppBody from '../AppBody'
|
|
||||||
|
|
||||||
import Row, { AutoRow } from '../../components/Row'
|
|
||||||
import TokenLogo from '../../components/TokenLogo'
|
|
||||||
import SearchModal from '../../components/SearchModal'
|
|
||||||
import { Text } from 'rebass'
|
|
||||||
import { Plus } from 'react-feather'
|
|
||||||
import { TYPE, StyledInternalLink } from '../../theme'
|
|
||||||
import { AutoColumn, ColumnCenter } from '../../components/Column'
|
|
||||||
import { ButtonPrimary, ButtonDropwdown, ButtonDropwdownLight } from '../../components/Button'
|
|
||||||
|
|
||||||
import { useToken } from '../../hooks/Tokens'
|
|
||||||
import { useActiveWeb3React } from '../../hooks'
|
|
||||||
import { usePair } from '../../data/Reserves'
|
|
||||||
|
|
||||||
enum Fields {
|
|
||||||
TOKEN0 = 0,
|
|
||||||
TOKEN1 = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
enum STEP {
|
|
||||||
SELECT_TOKENS = 'SELECT_TOKENS', // choose input and output tokens
|
|
||||||
READY_TO_CREATE = 'READY_TO_CREATE', // enable 'create' button
|
|
||||||
SHOW_CREATE_PAGE = 'SHOW_CREATE_PAGE' // show create page
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CreatePool({ location }: RouteComponentProps) {
|
|
||||||
const { chainId } = useActiveWeb3React()
|
|
||||||
const [showSearch, setShowSearch] = useState<boolean>(false)
|
|
||||||
const [activeField, setActiveField] = useState<number>(Fields.TOKEN0)
|
|
||||||
|
|
||||||
const [token0Address, setToken0Address] = useState<string>(WETH[chainId].address)
|
|
||||||
const [token1Address, setToken1Address] = useState<string>()
|
|
||||||
|
|
||||||
const token0: Token = useToken(token0Address)
|
|
||||||
const token1: Token = useToken(token1Address)
|
|
||||||
|
|
||||||
const [step, setStep] = useState<string>(STEP.SELECT_TOKENS)
|
|
||||||
|
|
||||||
const pair = usePair(token0, token1)
|
|
||||||
|
|
||||||
// if both tokens selected but pair doesnt exist, enable button to create pair
|
|
||||||
useEffect(() => {
|
|
||||||
if (token0Address && token1Address && pair === null) {
|
|
||||||
setStep(STEP.READY_TO_CREATE)
|
|
||||||
}
|
|
||||||
}, [pair, token0Address, token1Address])
|
|
||||||
|
|
||||||
// if theyve clicked create, show add liquidity page
|
|
||||||
if (step === STEP.SHOW_CREATE_PAGE) {
|
|
||||||
return <Redirect to={{ ...location, pathname: `/add/${token0Address}-${token1Address}` }} push={true} />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppBody>
|
|
||||||
<AutoColumn gap="20px">
|
|
||||||
<AutoColumn gap="24px">
|
|
||||||
{!token0Address ? (
|
|
||||||
<ButtonDropwdown
|
|
||||||
onClick={() => {
|
|
||||||
setShowSearch(true)
|
|
||||||
setActiveField(Fields.TOKEN0)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text fontSize={20}>Select first token</Text>
|
|
||||||
</ButtonDropwdown>
|
|
||||||
) : (
|
|
||||||
<ButtonDropwdownLight
|
|
||||||
onClick={() => {
|
|
||||||
setShowSearch(true)
|
|
||||||
setActiveField(Fields.TOKEN0)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Row align="flex-end">
|
|
||||||
<TokenLogo address={token0Address} />
|
|
||||||
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
|
|
||||||
{token0?.symbol}{' '}
|
|
||||||
</Text>
|
|
||||||
<TYPE.darkGray fontWeight={500} fontSize={16} marginLeft={'8px'}>
|
|
||||||
{token0?.address === WETH[chainId]?.address && '(default)'}
|
|
||||||
</TYPE.darkGray>
|
|
||||||
</Row>
|
|
||||||
</ButtonDropwdownLight>
|
|
||||||
)}
|
|
||||||
<ColumnCenter>
|
|
||||||
<Plus size="16" color="#888D9B" />
|
|
||||||
</ColumnCenter>
|
|
||||||
{!token1Address ? (
|
|
||||||
<ButtonDropwdown
|
|
||||||
onClick={() => {
|
|
||||||
setShowSearch(true)
|
|
||||||
setActiveField(Fields.TOKEN1)
|
|
||||||
}}
|
|
||||||
disabled={step !== STEP.SELECT_TOKENS}
|
|
||||||
>
|
|
||||||
<Text fontSize={20}>Select second token</Text>
|
|
||||||
</ButtonDropwdown>
|
|
||||||
) : (
|
|
||||||
<ButtonDropwdownLight
|
|
||||||
onClick={() => {
|
|
||||||
setShowSearch(true)
|
|
||||||
setActiveField(Fields.TOKEN1)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Row>
|
|
||||||
<TokenLogo address={token1Address} />
|
|
||||||
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
|
|
||||||
{token1?.symbol}
|
|
||||||
</Text>
|
|
||||||
</Row>
|
|
||||||
</ButtonDropwdownLight>
|
|
||||||
)}
|
|
||||||
{pair ? ( // pair already exists - prompt to add liquidity to existing pool
|
|
||||||
<AutoRow padding="10px" justify="center">
|
|
||||||
<TYPE.body textAlign="center">
|
|
||||||
Pool already exists!{' '}
|
|
||||||
<StyledInternalLink to={`/add/${token0Address}-${token1Address}`}>Join the pool.</StyledInternalLink>
|
|
||||||
</TYPE.body>
|
|
||||||
</AutoRow>
|
|
||||||
) : (
|
|
||||||
<ButtonPrimary disabled={step !== STEP.READY_TO_CREATE} onClick={() => setStep(STEP.SHOW_CREATE_PAGE)}>
|
|
||||||
<Text fontWeight={500} fontSize={20}>
|
|
||||||
Create Pool
|
|
||||||
</Text>
|
|
||||||
</ButtonPrimary>
|
|
||||||
)}
|
|
||||||
</AutoColumn>
|
|
||||||
<SearchModal
|
|
||||||
isOpen={showSearch}
|
|
||||||
filterType="tokens"
|
|
||||||
onTokenSelect={address => {
|
|
||||||
activeField === Fields.TOKEN0 ? setToken0Address(address) : setToken1Address(address)
|
|
||||||
}}
|
|
||||||
onDismiss={() => {
|
|
||||||
setShowSearch(false)
|
|
||||||
}}
|
|
||||||
hiddenToken={activeField === Fields.TOKEN0 ? token1Address : token0Address}
|
|
||||||
showCommonBases={activeField === Fields.TOKEN0}
|
|
||||||
/>
|
|
||||||
</AutoColumn>
|
|
||||||
</AppBody>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user