Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d6eddf9d4 | ||
|
|
aadf43efc3 | ||
|
|
227f729ecd | ||
|
|
a5b15e37f6 | ||
|
|
2408b2966e | ||
|
|
dc391d1bea | ||
|
|
e2d0514344 | ||
|
|
98d25dd2af | ||
|
|
f289dec684 | ||
|
|
73d3df05f2 | ||
|
|
83554f44f8 | ||
|
|
320b2e384b | ||
|
|
9492e7375a | ||
|
|
8a6a10be9d | ||
|
|
5e486fca7f | ||
|
|
87d24c404b | ||
|
|
d4011f73d1 | ||
|
|
6fc3157977 | ||
|
|
9c1fe53e4b | ||
|
|
28c916ff45 | ||
|
|
7adb4b6bd6 | ||
|
|
b2f0236ee8 | ||
|
|
4b57059353 | ||
|
|
6926f9a4ae | ||
|
|
7dec580944 | ||
|
|
5cf95680ef | ||
|
|
f8d6bab4ae | ||
|
|
c9721c42bf | ||
|
|
4414134bb2 | ||
|
|
44ba54e44a | ||
|
|
9ec3109f72 | ||
|
|
e75793676a | ||
|
|
32006ded21 | ||
|
|
d4f1c579d8 | ||
|
|
95f3541807 | ||
|
|
da4ca73a1d | ||
|
|
e75bf8d003 | ||
|
|
236f68a459 | ||
|
|
9f07baaad2 | ||
|
|
c75464e1aa | ||
|
|
bc80585bb4 | ||
|
|
ad45b2b7bb | ||
|
|
63ac89e9f3 | ||
|
|
1b6ae0d3db | ||
|
|
7d67819604 | ||
|
|
7b9b332c42 | ||
|
|
01feae978a | ||
|
|
2452d51e14 | ||
|
|
bbdc258083 | ||
|
|
27b103e3f7 | ||
|
|
2a751b9892 | ||
|
|
175e93fbba | ||
|
|
0b5fc07ee5 | ||
|
|
a0d4710a11 |
2
.env
2
.env
@@ -1,2 +1,2 @@
|
||||
REACT_APP_CHAIN_ID="1"
|
||||
REACT_APP_NETWORK_URL="https://mainnet.infura.io/v3/b8800ce81b8c451698081d269b86692b"
|
||||
REACT_APP_NETWORK_URL="https://mainnet.infura.io/v3/acb7e55995d04c49bfb52b7141599467"
|
||||
@@ -1,5 +1,5 @@
|
||||
REACT_APP_CHAIN_ID="1"
|
||||
REACT_APP_NETWORK_URL="https://mainnet.infura.io/v3/2acb2baa4c06402792e0c701a3697d10"
|
||||
REACT_APP_NETWORK_URL="https://mainnet.infura.io/v3/febcb10ca2754433a61e0805bc6c047d"
|
||||
REACT_APP_PORTIS_ID="c0e2bf01-4b08-4fd5-ac7b-8e26b58cd236"
|
||||
REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF"
|
||||
REACT_APP_GOOGLE_ANALYTICS_ID="UA-128182339-4"
|
||||
|
||||
77
.github/workflows/release.yaml
vendored
77
.github/workflows/release.yaml
vendored
@@ -1,36 +1,49 @@
|
||||
name: Release
|
||||
# every morning
|
||||
on:
|
||||
# every morning
|
||||
schedule:
|
||||
- cron: '0 12 * * *'
|
||||
|
||||
# releases are triggered on changes to this file
|
||||
push:
|
||||
branches:
|
||||
- v2
|
||||
paths:
|
||||
- '.github/workflows/release.yaml'
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
name: Create Release
|
||||
bump_version:
|
||||
name: Bump Version
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
new_tag: ${{ steps.github_tag_action.outputs.new_tag }}
|
||||
changelog: ${{ steps.github_tag_action.outputs.changelog }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Bump version and push tag
|
||||
id: bump_version
|
||||
uses: mathieudutour/github-tag-action@v4
|
||||
id: github_tag_action
|
||||
uses: mathieudutour/github-tag-action@v4.5
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
release_branches: .*
|
||||
|
||||
- name: Cancel this build if no new commits
|
||||
if: ${{ steps.bump_version.outputs.new_tag == null }}
|
||||
uses: andymckay/cancel-action@0.2
|
||||
create_release:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: bump_version
|
||||
if: ${{ needs.bump_version.outputs.new_tag != null }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12'
|
||||
|
||||
- name: Install yarn
|
||||
run: npm install -g yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
run: yarn install --ignore-scripts --frozen-lockfile
|
||||
|
||||
- name: Build the IPFS bundle
|
||||
run: yarn ipfs-build
|
||||
@@ -39,14 +52,26 @@ jobs:
|
||||
id: upload
|
||||
uses: anantaramdas/ipfs-pinata-deploy-action@v1.5.2
|
||||
with:
|
||||
pin-name: Uniswap ${{ steps.bump_version.outputs.new_tag }}
|
||||
pin-name: Uniswap ${{ needs.bump_version.outputs.new_tag }}
|
||||
path: './build'
|
||||
pinata-api-key: ${{ secrets.PINATA_API_KEY }}
|
||||
pinata-secret-api-key: ${{ secrets.PINATA_API_SECRET_KEY }}
|
||||
|
||||
- name: Convert CIDv0 to CIDv1
|
||||
id: convert_cidv0
|
||||
uses: uniswap/convert-cidv0-cidv1@v1.0.0
|
||||
with:
|
||||
cidv0: ${{ steps.upload.outputs.hash }}
|
||||
|
||||
- name: Update DNS with new IPFS hash
|
||||
id: update_dns
|
||||
run: npx vercel --token ${{ secrets.VERCEL_TOKEN }} --scope uniswap dns add uniswap.org _dnslink.app TXT "dnslink=/ipfs/${{ steps.upload.outputs.hash }}"
|
||||
uses: uniswap/replace-vercel-dns-records@v1.0.0
|
||||
with:
|
||||
domain: 'uniswap.org'
|
||||
subdomain: '_dnslink.app'
|
||||
record-type: 'TXT'
|
||||
value: dnslink=/ipfs/${{ steps.upload.outputs.hash }}
|
||||
token: ${{ secrets.VERCEL_TOKEN }}
|
||||
team-name: 'uniswap'
|
||||
|
||||
- name: Create GitHub Release
|
||||
id: create_release
|
||||
@@ -54,17 +79,27 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ steps.bump_version.outputs.new_tag }}
|
||||
release_name: Release ${{ steps.bump_version.outputs.new_tag }}
|
||||
tag_name: ${{ needs.bump_version.outputs.new_tag }}
|
||||
release_name: Release ${{ needs.bump_version.outputs.new_tag }}
|
||||
body: |
|
||||
Release built from commit [`${{ github.sha }}`](https://github.com/Uniswap/uniswap-frontend/tree/${{ github.sha }})
|
||||
|
||||
The IPFS hash of the bundle is `${{ steps.upload.outputs.hash }}`
|
||||
The IPFS hash of the bundle is:
|
||||
- CIDv0: `${{ steps.upload.outputs.hash }}`
|
||||
- CIDv1: `${{ steps.convert_cidv0.outputs.cidv1 }}`
|
||||
|
||||
The following IPFS Gateways can be used to access the release:
|
||||
Uniswap uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to store your settings.
|
||||
**Beware** that other sites you access via the _same_ IPFS gateway can read and modify your settings on Uniswap without your permission.
|
||||
You can avoid this issue by using a subdomain IPFS gateway. The preferred gateway URLs below utilize the CIDv1 of the release in the subdomain, and are relatively safer.
|
||||
|
||||
Preferred URLs:
|
||||
- https://${{ steps.convert_cidv0.outputs.cidv1 }}.ipfs.dweb.link/
|
||||
- https://${{ steps.convert_cidv0.outputs.cidv1 }}.cf-ipfs.com/
|
||||
- [ipfs://${{ steps.upload.outputs.hash }}/](ipfs://${{ steps.upload.outputs.hash }}/)
|
||||
|
||||
Other IPFS gateways:
|
||||
- https://cloudflare-ipfs.com/ipfs/${{ steps.upload.outputs.hash }}/
|
||||
- https://ipfs.infura.io/ipfs/${{ steps.upload.outputs.hash }}/
|
||||
- https://ipfs.io/ipfs/${{ steps.upload.outputs.hash }}/
|
||||
- https://dweb.link/ipfs/${{ steps.upload.outputs.hash }}/
|
||||
|
||||
${{ steps.bump_version.outputs.changelog }}
|
||||
${{ needs.bump_version.outputs.changelog }}
|
||||
6
.github/workflows/tests.yaml
vendored
6
.github/workflows/tests.yaml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- run: yarn
|
||||
- run: yarn install
|
||||
- run: yarn integration-test
|
||||
|
||||
unit-tests:
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- run: yarn
|
||||
- run: yarn install --ignore-scripts --frozen-lockfile
|
||||
- run: yarn test
|
||||
|
||||
lint:
|
||||
@@ -70,6 +70,6 @@ jobs:
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- run: yarn
|
||||
- run: yarn install --ignore-scripts --frozen-lockfile
|
||||
- run: yarn lint
|
||||
|
||||
|
||||
18
README.md
18
README.md
@@ -1,9 +1,7 @@
|
||||
# Uniswap Frontend
|
||||
|
||||
[](https://app.netlify.com/sites/uniswap/deploys)
|
||||
[](https://github.com/Uniswap/uniswap-frontend/actions?query=workflow%3ATests)
|
||||
[](https://github.com/Uniswap/uniswap-frontend/actions?query=workflow%3ATests)
|
||||
[](https://prettier.io/)
|
||||
[](https://github.com/Uniswap/uniswap-frontend/actions?query=workflow%3ARelease)
|
||||
|
||||
An open source interface for Uniswap -- a protocol for decentralized exchange of Ethereum tokens.
|
||||
|
||||
@@ -17,10 +15,9 @@ An open source interface for Uniswap -- a protocol for decentralized exchange of
|
||||
|
||||
## Accessing the frontend
|
||||
|
||||
The front end is deployed to IPFS as well as to [uniswap.exchange](https://uniswap.exchange).
|
||||
|
||||
To access the front end via IPFS, use a link from the
|
||||
[latest release](https://github.com/Uniswap/uniswap-frontend/releases/latest).
|
||||
To access the front end, use an IPFS gateway link from the
|
||||
[latest release](https://github.com/Uniswap/uniswap-frontend/releases/latest)
|
||||
or visit [uniswap.exchange](https://uniswap.exchange).
|
||||
|
||||
## Development
|
||||
|
||||
@@ -46,10 +43,15 @@ change `REACT_APP_NETWORK_ID` to `"{yourNetworkId}"`, and change `REACT_APP_NETW
|
||||
|
||||
Note that the front end only works properly on testnets where both
|
||||
[Uniswap V2](https://uniswap.org/docs/v2/smart-contracts/factory/) and
|
||||
[eth-scan](https://github.com/MyCryptoHQ/eth-scan) are deployed.
|
||||
[multicall](https://github.com/makerdao/multicall) are deployed.
|
||||
The frontend will not work on other networks.
|
||||
|
||||
## Contributions
|
||||
|
||||
**Please open all pull requests against the `v2` branch.**
|
||||
CI checks will run against all PRs.
|
||||
|
||||
## Accessing Uniswap V1 interface
|
||||
|
||||
The Uniswap V1 interface for mainnet and testnets is accessible via IPFS gateways linked
|
||||
from the [v1.0.0 release](https://github.com/Uniswap/uniswap-frontend/releases/tag/v1.0.0).
|
||||
@@ -1,19 +1,19 @@
|
||||
describe('Add Liquidity', () => {
|
||||
it('loads the two correct tokens', () => {
|
||||
cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85-0xc778417E063141139Fce010982780140Aa0cD5Ab')
|
||||
cy.get('#add-liquidity-input-token0 .token-symbol-container').should('contain.text', 'MKR')
|
||||
cy.get('#add-liquidity-input-token1 .token-symbol-container').should('contain.text', 'ETH')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'MKR')
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'ETH')
|
||||
})
|
||||
|
||||
it('does not crash if ETH is duplicated', () => {
|
||||
cy.visit('/add/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xc778417E063141139Fce010982780140Aa0cD5Ab')
|
||||
cy.get('#add-liquidity-input-token0 .token-symbol-container').should('contain.text', 'ETH')
|
||||
cy.get('#add-liquidity-input-token1 .token-symbol-container').should('not.contain.text', 'ETH')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'ETH')
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('not.contain.text', 'ETH')
|
||||
})
|
||||
|
||||
it('token not in storage is loaded', () => {
|
||||
cy.visit('/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
|
||||
cy.get('#add-liquidity-input-token0 .token-symbol-container').should('contain.text', 'SKL')
|
||||
cy.get('#add-liquidity-input-token1 .token-symbol-container').should('contain.text', 'MKR')
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,9 +5,9 @@ describe('Pool', () => {
|
||||
cy.get('#token-search-input').type('DAI')
|
||||
})
|
||||
|
||||
it.skip('can import a pool', () => {
|
||||
it('can import a pool', () => {
|
||||
cy.get('#join-pool-button').click()
|
||||
cy.get('#import-pool-link').click() // blocked by the grid element in the search box
|
||||
cy.get('#import-pool-link').click({ force: true }) // blocked by the grid element in the search box
|
||||
cy.url().should('include', '/find')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
describe('Remove Liquidity', () => {
|
||||
it('loads the two correct tokens', () => {
|
||||
cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
|
||||
cy.get('#remove-liquidity-token0-symbol').should('contain.text', 'ETH')
|
||||
cy.get('#remove-liquidity-token1-symbol').should('contain.text', 'MKR')
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'MKR')
|
||||
})
|
||||
|
||||
it('does not crash if ETH is duplicated', () => {
|
||||
cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xc778417E063141139Fce010982780140Aa0cD5Ab')
|
||||
cy.get('#remove-liquidity-token0-symbol').should('contain.text', 'ETH')
|
||||
cy.get('#remove-liquidity-token1-symbol').should('not.contain.text', 'ETH')
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('not.contain.text', 'ETH')
|
||||
})
|
||||
|
||||
it('token not in storage is loaded', () => {
|
||||
cy.visit('/remove/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
|
||||
cy.get('#remove-liquidity-token0-symbol').should('contain.text', 'SKL')
|
||||
cy.get('#remove-liquidity-token1-symbol').should('contain.text', 'MKR')
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'SKL')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'MKR')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -32,8 +32,10 @@ describe('Swap', () => {
|
||||
|
||||
it('can swap ETH for DAI', () => {
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').click()
|
||||
cy.get('#swap-currency-input .token-amount-input').type('0.001')
|
||||
cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').should('be.visible')
|
||||
cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').click({ force: true })
|
||||
cy.get('#swap-currency-input .token-amount-input').should('be.visible')
|
||||
cy.get('#swap-currency-input .token-amount-input').type('0.001', { force: true })
|
||||
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
|
||||
cy.get('#show-advanced').click()
|
||||
cy.get('#swap-button').click()
|
||||
|
||||
@@ -71,10 +71,9 @@ Cypress.Commands.overwrite('visit', (original, url, options) => {
|
||||
...options,
|
||||
onBeforeLoad(win) {
|
||||
options && options.onBeforeLoad && options.onBeforeLoad(win)
|
||||
const provider = new JsonRpcProvider('https://rinkeby.infura.io/v3/b8800ce81b8c451698081d269b86692b', 4)
|
||||
const provider = new JsonRpcProvider('https://rinkeby.infura.io/v3/acb7e55995d04c49bfb52b7141599467', 4)
|
||||
const signer = new Wallet(PRIVATE_KEY_TEST_NEVER_USE, provider)
|
||||
const bridge = new CustomizedBridge(signer, provider)
|
||||
win.ethereum = bridge
|
||||
win.ethereum = new CustomizedBridge(signer, provider)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
15
package.json
15
package.json
@@ -13,19 +13,19 @@
|
||||
"@ethersproject/strings": "^5.0.0-beta.136",
|
||||
"@ethersproject/units": "^5.0.0-beta.132",
|
||||
"@ethersproject/wallet": "^5.0.0-beta.141",
|
||||
"@material-ui/core": "^4.9.5",
|
||||
"@mycrypto/eth-scan": "^2.1.0",
|
||||
"@popperjs/core": "^2.4.0",
|
||||
"@reach/dialog": "^0.2.8",
|
||||
"@reach/tooltip": "^0.2.0",
|
||||
"@reach/dialog": "^0.10.3",
|
||||
"@reach/portal": "^0.10.3",
|
||||
"@reduxjs/toolkit": "^1.3.5",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/lodash.flatmap": "^4.5.6",
|
||||
"@types/node": "^13.13.5",
|
||||
"@types/qs": "^6.9.2",
|
||||
"@types/react": "^16.9.34",
|
||||
"@types/react-dom": "^16.9.7",
|
||||
"@types/react-redux": "^7.1.8",
|
||||
"@types/react-router-dom": "^5.0.0",
|
||||
"@types/react-window": "^1.8.2",
|
||||
"@types/rebass": "^4.0.5",
|
||||
"@types/styled-components": "^4.2.0",
|
||||
"@types/testing-library__cypress": "^5.0.5",
|
||||
@@ -37,7 +37,6 @@
|
||||
"@web3-react/core": "^6.0.9",
|
||||
"@web3-react/fortmatic-connector": "^6.0.9",
|
||||
"@web3-react/injected-connector": "^6.0.7",
|
||||
"@web3-react/network-connector": "^6.0.9",
|
||||
"@web3-react/portis-connector": "^6.0.9",
|
||||
"@web3-react/walletconnect-connector": "^6.0.9",
|
||||
"@web3-react/walletlink-connector": "^6.0.9",
|
||||
@@ -49,11 +48,11 @@
|
||||
"eslint-plugin-prettier": "^3.1.3",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"eslint-plugin-react-hooks": "^4.0.0",
|
||||
"history": "^4.9.0",
|
||||
"i18next": "^15.0.9",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
"i18next-xhr-backend": "^2.0.1",
|
||||
"jazzicon": "^1.5.0",
|
||||
"lodash.flatmap": "^4.5.0",
|
||||
"polished": "^3.3.2",
|
||||
"prettier": "^1.17.0",
|
||||
"qrcode.react": "^0.9.3",
|
||||
@@ -70,12 +69,12 @@
|
||||
"react-scripts": "^3.4.1",
|
||||
"react-spring": "^8.0.27",
|
||||
"react-use-gesture": "^6.0.14",
|
||||
"react-window": "^1.8.5",
|
||||
"rebass": "^4.0.7",
|
||||
"redux-localstorage-simple": "^2.2.0",
|
||||
"serve": "^11.3.0",
|
||||
"start-server-and-test": "^1.11.0",
|
||||
"styled-components": "^4.2.0",
|
||||
"swr": "0.1.18",
|
||||
"typescript": "^3.8.3",
|
||||
"use-media": "^1.4.0"
|
||||
},
|
||||
@@ -107,4 +106,4 @@
|
||||
]
|
||||
},
|
||||
"license": "GPL-3.0-or-later"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
src/assets/images/ethereum-logo.png
Normal file
BIN
src/assets/images/ethereum-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
@@ -1,9 +0,0 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<rect width="20" height="20" fill="url(#pattern0)"/>
|
||||
<defs>
|
||||
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
|
||||
<use xlink:href="#image0" transform="scale(0.0078125)"/>
|
||||
</pattern>
|
||||
<image id="image0" width="128" height="128" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAAAXNSR0IArs4c6QAADd9JREFUeNrtXdtzE+cV38kweWpm+tI8JZP8EZ0pL+UhferkfyAzFqEpadq0hbRNUlsYQ2ycBEgCgQQIgVxMbC6BAHbEJZg7dW4OpeCWwDTNDOyuLFuybMsXfT2/tWVkWavd1e5K58g6M9+Q8cTS+ju//b5z/R1Nq0F5Ppr46cpm8xcroubyFU3m+oYmY39Do97d0KRfoH/76d/b9K9JP89Yy/pv62f9s/9PN34Hv4vPwGfhM7W68JNVUfWTp9fqv440Ge2RRqOXFHiPFKhCWfTZ+A58F74T313XQIUlGlUP0lv5RCRqtkQa9YukjMnQFO6w8N3WM9Cz4JnwbHUNhSQrovGlkSZ9K216vFoKdwGIOJ4Rz1rXWADyTDTxeCSqv0wbe5Or0kuA4SaeHX9DXZMepSFq/Jw28FBDo5GVpviFy8hafwv9TXXNOh3zzfov6Qjtka90u1NB78HfWNd0gUTWmr+yLPgaVfwCIMCToL+5/sZH44+Qa9W5WBRfxK3sxB4sRlduSSRqrKYjMbVolX//WkhhL7Ani+O4j+rL6Ai8ttgVX+RauIa9qfG3Xm+tDcs+RI+B9qjmToNVLfFHKWJ2vq5gt6eBfr5mbIPIWuNJK9lSV6zXZWLvJB/5D1AApK1+5PsOIrVhL8UlbMiy7agrMKArgfZSTKKpoc14iHLpMUkb/OZHQ6pxa5x5zID2lPaWefIm9TC5M32SlP/MOl0Zg1PqWG9aUY6fu6vY99yG1M94GnsticfozhqQdryeOJ9W09NZ9cU/Rq2TQIBdMIC9ZvfmS1T+39+Kq6mp7BwATl8dVX/caIoAAfaczZ0v7djPrYE7EwqSAwDW/u6UlMhhX9VtAlim0gy+3Np9KKlykg8ArPXvDApJJhmxqnkHlp8v1NV77hVDJUembQHQfSFtGYeCXMTKxwlmgjwy/erevlGVL4UAwHqna1hS+VlbFcK7MiN8698dVNls1hEAWC+8bsqJGFYqbIzEjtTY/oqorn64O6kKxQ4AB0+NIF8vJncA3YSf0hWc1YOFX0zsAIDVvichKosYaip5Jp8vU/l/ftVUY+NZzwCIXRpVz24wBOUN9NbQKnkkZ/a+vD6u7KQUALDeP5KUVlSyLISjX24Z16Z9Q6qUOAEA6+U344KuAuNaoFcBoWqNVOX/pllXOiV7/ALg6NkR9XRUVC3BmsBKtyVX70JxTuIGAFhvfDgkKDagpwIpKyPLskuq8l98I64mJ7OBAeD0lVH1fJshySvo8t2xI7mS5vr3GeVG3AIA6+MTKVF74KsDSXK7FkK5bsULALCatw9KMgh7y27UlKp8+O1DqenQAIAiEhiXYiKg5TSkSu7SxV3tRbwCAGt7p6Rkkd7jze2jXnapysfxDIWGDQCs1a+ZcvbGCz/BDDmDyEZLdefHCeVVygXAgZicZBF06rK+L/G41JDvh8eSqhwpFwBYbbsTYkLEruhqwGsjUfko5hwdm644AGKX0mrVehmxAejWTdh3QCIArvSPqXLFDwCwdh9OSrkGbjqFfZdKVP6rlLP3I34BgPXiFhnJopIUduC4k6b8ldTNc9ecrDoAjpwZsSqOBBjKW+0bOhmTMNqtw6dHlF8JAgBYSDsLuAbiRUvJQXUqTfl/3WyqiYksGwCcogDU71sNAdeA+UQR699skQaA7/6dUUFIUADAgivK3xswW4qlfS9KUv62/cMqKAkSAFjRbbyTRdD1/FJvoj2vJuu217WqxVCDw1NsAXD8XNoyThnbAZPzqO7BfS/p7f/8YloFKUEDAGtrB2+DEDrPj/23S1F+07a452RPNQBwhlrNUYrO+BRoF1n4ceuHiUCVjzax67cy6pOeVOAg6Pw8xdgOyCsUCXXMSoAL9flBSYbcR/QKwHWDsta+PahadyXU3qNJq54gKBBs2MnUICSdW8rHMCQJyod/PTI67VvxQ8lpdfGbsQVKBgByC/wAOw8Mqx6yNfwCAPbKb1t47qk1CAsTsSQA4MLXY74U/6M+qc5SW/gZG0XlAyC31lFxyVtkzKG03A8Idh7kWT0E3dP9bz7FXfk4msuVgTsZetud3+RiAMhfm/YlrDu9XBAgaskwIrgc+f8NrN0VSrDg7fUiE9QL8PUNut8vuz/CnQCQWxupAOSDz5IWoZQXACBnwa16CLrXZocqsgVAVyzlWvGgfbn07ZirN75cAMzZCUQ0sYuO9pgHO+G195lVD5HuWVf/riGGjvGMs89/15hUvV/a3+9hACC3mncMqm1kJxxzYSecujxq8ROxqhbmnAP45sZ4ScX/578ZdSYgl61cAOSvzWQnHIiVthP2HU3yygk0YF4uU/7eYjJJxI7f3oT/ng40aBMEAHKr/b2ElRW0sxMY8RL3a7NDk9nx95qJ+ckexACufDcWaJAmLADk1gayE3YfGlYnL80HKxteYtK9xpHwCdm0nNyLT6lzX83E1sNQfJgAmIsnUGBp+/4hdbz3PhCY8BKb2uz4dHb8vbf/NxG60isFgDmDkQJLW4hj4OCpFA9eYtI9OwAcOzcyF5+v5KoEAObZCXvIaPxgiAEAmF0BSKGCzq3WAfA3KiNn0GFssjQCc02ex3rTNQcAXHG/40I7N2sE9nMOA2+hYxIBFOkAQJ0g7nxm4WDLDbwggd17z6dJsQAA5zDLcTSke5wA3XL6AOIWf68UALxEJFVcawFmVze7ZJBT2hRH6Cs7E6rnAt9IICJ9bhpE/lBtxjErGcQsHbyRwqiXKaPnxL6BaCF6A05f5QOAKC14MU73fO5KA41dVesBmsz1aAlbzu1ogvWPmj3k0J1cJRhWHx1PVR0AuJ5WNjuWYltkk4hzcIgEWgUhHEvCYP3nqn+RE8Cb7vQ7OHaPfjFScQDArXvWBUEE3FqEuPFdqHHg4A1YJWFci0L/sokYP8bvF4HeuJ2xegKcBkKg6OLk5fABALfODWsorrL8knPQz3NpILWKQjmXhReSPaKBA/kBpw3EG7mLsnBhAQCFKk7E0Whfe/fAwmdYt2OQiwt4T0RjCEq4CyVNqWHc+06EDPC/u2KpwAAAo81pmhieCYwlsUsLvRSMqmPZGMK5NQxvkh6fsi31dlNnhzp/sHuWCwDYF25KuWAPgCnEjm5+JSN20XmtYdybQ3FsIkVsJ6gAdoofYPNhedsVlNi5dX9qd3brYAugAsiWZfwqvwlk85pDJbSHoya/lIAWHlY2TgynDiPU5TkBAEaoU4s3XFR0ATvFIl7fy6saeEF7uASCCLyFbujf0fq1y0Unzks0AubTvON6LnxLP3cCERbKvbpdRCM7GNLLLyCIkEIRg+M4lXbXH/g9VRS1OMwABqgQeUT/HlxMN6FZXDUHTrqLNyBczXHiWFGKGCkkUYikeWn9Rk8hgOMUVnbyKJDDf++wt4wko+pfZ5IoSTRxqA/wIpgXCHewHNoW+Pub93mvSeBKJ29LEyeJKBIWfbERsE4Cd9JLDB5eQDlVSchhcJ0wZksUKY0qFj53uRyB/yRGkFJzAJHR6yizLhGJHqcrhy1V7GxQ6KYUEMCd88MLhJxBfn0eijd2dA77yiqilV0sWbREuviv/jXuizgCXgVoYXLegB/lc+r7K5suXtrACIRo/XIGBsEShiCUU55AxMAIiSNjQNrghzpuMdDFux4ZI3Vo1Gdn01UDAJM+v9LLy9AoiWPjVkTL5xD0AwDkKLgPjvI8Nk7q4MgXCqqIwgYAcv4i6OHLGRwpdXSsl5GxfgHAprrHbeHHYhke7ZVTsBwAoORMwl74Gh49Wy/YKQ0ASOfei0+FBgBUH6+UMDuYdKf5FQodPkJGREoaCHA8T05lAwcAij/WCBgZC51Bd1oQEokaqyVeBU5VROUAgB3Xn23Uz1itBSWUPlxCxsQ1aQCwqohuZQIDwMcnUjL+btIVdKYFKRRHXiZxpjAyc2AQ9QsAq7pHxIhYIwtdaWEIfXCrxKsAJBN+AYD0s4yjX2/VwpKZq0A/LxEEpaqInADw9idCXD7STeBHf6Gsaok/ypFb0E8VUSkAHDrFt7qnkPAJutEqIZG1xpMS7QFUAWWKVBHZAQCnRtW5/Nze+6QTrZJC6cU2iVfB3iJVRHYAABOJDG/HaNMqLXTXPEC+ZodEEGBYlBMA9h5JSvH3O6ALrRqC8mLimYlJA0BhFVEhAPhX98xx/MRsS7wrJQ1txkMUeOiTBoK2vCqiQgCA7kVAsKcPe69xkGeiqYfJEBmQBgK0bBcCAJ1HAoy+Aey5xkkiLYnHpIEgV0WUA4CE6h7sMfZa4yhApbTrAH37YBxBdQ+neT52xz67N7+YTSDNMEQfH1i8uBt8bO58N96BVBeRsav3oCZJrDiBFSySFzFkFeGjPayanx9c2Fhe7oBDbL/i4d0wE0hSs4jVyupVLLFTwSthyUw9Qf1KcCjmaA09pVvVK4GqVSSWl1WijCu0Sh6OpwGhfY3EauMwqnexFzX91pcsOW/UuxbxXd8VWOm26GuBulcktqH5adfy3bFTkycCNTFK60r22qVbdqPmYhL0ss+QVNSCx2AFcw557s+vywxdzQxnkbxUs5W1o2d3TctSF0eDcSk47jiTWeLZ8IyOVGx18ZdoAtUp+G5BelxNlnN8t/UM9Cx4JnEJm5oIMxPtObjvMQDB8iTCHH9Dn21Z8PRd+M4FlOt14SEYhoSJWJFG8ylrLiINSIQFPkuJ3z87MNvE+HRrWf9t/ax/5lQhTwS/Q7+Lz8BnzQ1YqjH5P29N0rBVv2N5AAAAAElFTkSuQmCC"/>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.2 KiB |
@@ -1,11 +1,11 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { useCopyClipboard } from '../../hooks'
|
||||
import useCopyClipboard from '../../hooks/useCopyClipboard'
|
||||
|
||||
import { Link } from '../../theme'
|
||||
import { LinkStyledButton } from '../../theme'
|
||||
import { CheckCircle, Copy } from 'react-feather'
|
||||
|
||||
const CopyIcon = styled(Link)`
|
||||
const CopyIcon = styled(LinkStyledButton)`
|
||||
color: ${({ theme }) => theme.text4};
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Check, Triangle } from 'react-feather'
|
||||
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import { Link, Spinner } from '../../theme'
|
||||
import { ExternalLink, Spinner } from '../../theme'
|
||||
import Circle from '../../assets/images/circle.svg'
|
||||
|
||||
import { transparentize } from 'polished'
|
||||
@@ -18,7 +18,7 @@ const TransactionStatusText = styled.div`
|
||||
margin-right: 0.5rem;
|
||||
`
|
||||
|
||||
const TransactionState = styled(Link)<{ pending: boolean; success?: boolean }>`
|
||||
const TransactionState = styled(ExternalLink)<{ pending: boolean; success?: boolean }>`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
text-decoration: none !important;
|
||||
|
||||
@@ -21,7 +21,7 @@ import Identicon from '../Identicon'
|
||||
|
||||
import { ButtonEmpty } from '../Button'
|
||||
|
||||
import { Link, TYPE } from '../../theme'
|
||||
import { ExternalLink, LinkStyledButton, TYPE } from '../../theme'
|
||||
|
||||
const HeaderRow = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
@@ -160,7 +160,7 @@ const ConnectButtonRow = styled.div`
|
||||
margin: 10px 0;
|
||||
`
|
||||
|
||||
const StyledLink = styled(Link)<{ hasENS: boolean; isENS: boolean }>`
|
||||
const AddressLink = styled(ExternalLink)<{ hasENS: boolean; isENS: boolean }>`
|
||||
color: ${({ hasENS, isENS, theme }) => (hasENS ? (isENS ? theme.primary1 : theme.text3) : theme.primary1)};
|
||||
`
|
||||
|
||||
@@ -357,17 +357,21 @@ export default function AccountDetails({
|
||||
{ENSName ? (
|
||||
<>
|
||||
<AccountControl hasENS={!!ENSName} isENS={false}>
|
||||
<StyledLink hasENS={!!ENSName} isENS={true} href={getEtherscanLink(chainId, ENSName, 'address')}>
|
||||
<AddressLink hasENS={!!ENSName} isENS={true} href={getEtherscanLink(chainId, ENSName, 'address')}>
|
||||
View on Etherscan ↗
|
||||
</StyledLink>
|
||||
</AddressLink>
|
||||
</AccountControl>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<AccountControl hasENS={!!ENSName} isENS={false}>
|
||||
<StyledLink hasENS={!!ENSName} isENS={false} href={getEtherscanLink(chainId, account, 'address')}>
|
||||
<AddressLink
|
||||
hasENS={!!ENSName}
|
||||
isENS={false}
|
||||
href={getEtherscanLink(chainId, account, 'address')}
|
||||
>
|
||||
View on Etherscan ↗
|
||||
</StyledLink>
|
||||
</AddressLink>
|
||||
</AccountControl>
|
||||
</>
|
||||
)}
|
||||
@@ -395,7 +399,7 @@ export default function AccountDetails({
|
||||
<LowerSection>
|
||||
<AutoRow style={{ justifyContent: 'space-between' }}>
|
||||
<TYPE.body>Recent Transactions</TYPE.body>
|
||||
<Link onClick={clearAllTransactionsCallback}>(clear all)</Link>
|
||||
<LinkStyledButton onClick={clearAllTransactionsCallback}>(clear all)</LinkStyledButton>
|
||||
</AutoRow>
|
||||
{renderTransactions(pendingTransactions)}
|
||||
{renderTransactions(confirmedTransactions)}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useState, useEffect, useContext } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import useDebounce from '../../hooks/useDebounce'
|
||||
|
||||
import { isAddress } from '../../utils'
|
||||
import { useActiveWeb3React, useDebounce } from '../../hooks'
|
||||
import { Link, TYPE } from '../../theme'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { ExternalLink, TYPE } from '../../theme'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { RowBetween } from '../Row'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
@@ -159,12 +160,12 @@ export default function AddressInputPanel({
|
||||
Recipient
|
||||
</TYPE.black>
|
||||
{data.address && (
|
||||
<Link
|
||||
<ExternalLink
|
||||
href={getEtherscanLink(chainId, data.name || data.address, 'address')}
|
||||
style={{ fontSize: '14px' }}
|
||||
>
|
||||
(View on Etherscan)
|
||||
</Link>
|
||||
</ExternalLink>
|
||||
)}
|
||||
</RowBetween>
|
||||
<Input
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import React, { useCallback, useContext } from 'react'
|
||||
import React, { useContext } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom'
|
||||
|
||||
import Modal from '../Modal'
|
||||
import Loader from '../Loader'
|
||||
import { Link } from '../../theme'
|
||||
import { ExternalLink } from '../../theme'
|
||||
import { Text } from 'rebass'
|
||||
import { CloseIcon } from '../../theme/components'
|
||||
import { RowBetween } from '../Row'
|
||||
@@ -32,7 +30,7 @@ const ConfirmedIcon = styled(ColumnCenter)`
|
||||
padding: 60px 0;
|
||||
`
|
||||
|
||||
interface ConfirmationModalProps extends RouteComponentProps<{}> {
|
||||
interface ConfirmationModalProps {
|
||||
isOpen: boolean
|
||||
onDismiss: () => void
|
||||
hash: string
|
||||
@@ -44,8 +42,7 @@ interface ConfirmationModalProps extends RouteComponentProps<{}> {
|
||||
title?: string
|
||||
}
|
||||
|
||||
function ConfirmationModal({
|
||||
history,
|
||||
export default function ConfirmationModal({
|
||||
isOpen,
|
||||
onDismiss,
|
||||
hash,
|
||||
@@ -59,13 +56,6 @@ function ConfirmationModal({
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const dismissAndReturn = useCallback(() => {
|
||||
if (history.location.pathname.match('/add') || history.location.pathname.match('/remove')) {
|
||||
history.push('/pool')
|
||||
}
|
||||
onDismiss()
|
||||
}, [onDismiss, history])
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90}>
|
||||
{!attemptingTxn ? (
|
||||
@@ -106,12 +96,12 @@ function ConfirmationModal({
|
||||
</AutoColumn>
|
||||
{!pendingConfirmation && (
|
||||
<>
|
||||
<Link href={getEtherscanLink(chainId, hash, 'transaction')}>
|
||||
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>
|
||||
<Text fontWeight={500} fontSize={14} color={theme.primary1}>
|
||||
View on Etherscan
|
||||
</Text>
|
||||
</Link>
|
||||
<ButtonPrimary onClick={dismissAndReturn} style={{ margin: '20px 0 0 0' }}>
|
||||
</ExternalLink>
|
||||
<ButtonPrimary onClick={onDismiss} style={{ margin: '20px 0 0 0' }}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Close
|
||||
</Text>
|
||||
@@ -131,5 +121,3 @@ function ConfirmationModal({
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(ConfirmationModal)
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Pair, Token } from '@uniswap/sdk'
|
||||
import React, { useState, useContext } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import '@reach/tooltip/styles.css'
|
||||
import { darken } from 'polished'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import DoubleLogo from '../DoubleLogo'
|
||||
import SearchModal from '../SearchModal'
|
||||
|
||||
@@ -8,7 +8,7 @@ import Row from '../Row'
|
||||
import Menu from '../Menu'
|
||||
import Web3Status from '../Web3Status'
|
||||
|
||||
import { Link } from '../../theme'
|
||||
import { ExternalLink } from '../../theme'
|
||||
import { Text } from 'rebass'
|
||||
import { WETH, ChainId } from '@uniswap/sdk'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
@@ -159,13 +159,13 @@ export default function Header() {
|
||||
<HeaderFrame>
|
||||
<MigrateBanner>
|
||||
Uniswap V2 is live! Read the
|
||||
<Link href="https://uniswap.org/blog/launch-uniswap-v2/">
|
||||
<ExternalLink href="https://uniswap.org/blog/launch-uniswap-v2/">
|
||||
<b>blog post ↗</b>
|
||||
</Link>
|
||||
</ExternalLink>
|
||||
or
|
||||
<Link href="https://migrate.uniswap.exchange/">
|
||||
<ExternalLink href="https://migrate.uniswap.exchange/">
|
||||
<b>migrate your liquidity ↗</b>
|
||||
</Link>
|
||||
</ExternalLink>
|
||||
.
|
||||
</MigrateBanner>
|
||||
<RowBetween padding="1rem">
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useRef, useEffect } from 'react'
|
||||
import { Info, BookOpen, Code, PieChart, MessageCircle } from 'react-feather'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { ReactComponent as MenuIcon } from '../../assets/images/menu.svg'
|
||||
import useToggle from '../../hooks/useToggle'
|
||||
|
||||
import { Link } from '../../theme'
|
||||
import { useToggle } from '../../hooks'
|
||||
import { ExternalLink } from '../../theme'
|
||||
|
||||
const StyledMenuIcon = styled(MenuIcon)`
|
||||
path {
|
||||
@@ -63,13 +63,17 @@ const MenuFlyout = styled.span`
|
||||
z-index: 100;
|
||||
`
|
||||
|
||||
const MenuItem = styled(Link)`
|
||||
const MenuItem = styled(ExternalLink)`
|
||||
flex: 1;
|
||||
padding: 0.5rem 0.5rem;
|
||||
color: ${({ theme }) => theme.text2};
|
||||
:hover {
|
||||
color: ${({ theme }) => theme.text1};
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
> svg {
|
||||
margin-right: 8px;
|
||||
}
|
||||
`
|
||||
|
||||
@@ -102,26 +106,32 @@ export default function Menu() {
|
||||
|
||||
return (
|
||||
<StyledMenu ref={node}>
|
||||
<StyledMenuButton onClick={() => toggle()}>
|
||||
<StyledMenuButton onClick={toggle}>
|
||||
<StyledMenuIcon />
|
||||
</StyledMenuButton>
|
||||
{open ? (
|
||||
{open && (
|
||||
<MenuFlyout>
|
||||
<MenuItem id="link" href="https://uniswap.org/">
|
||||
<Info size={14} />
|
||||
About
|
||||
</MenuItem>
|
||||
<MenuItem id="link" href="https://uniswap.org/docs/v2">
|
||||
<BookOpen size={14} />
|
||||
Docs
|
||||
</MenuItem>
|
||||
<MenuItem id="link" href={CODE_LINK}>
|
||||
<Code size={14} />
|
||||
Code
|
||||
</MenuItem>
|
||||
<MenuItem id="link" href="https://discord.gg/vXCdddD">
|
||||
<MessageCircle size={14} />
|
||||
Discord
|
||||
</MenuItem>
|
||||
<MenuItem id="link" href="https://uniswap.info/">
|
||||
<PieChart size={14} />
|
||||
Analytics
|
||||
</MenuItem>
|
||||
</MenuFlyout>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</StyledMenu>
|
||||
)
|
||||
|
||||
@@ -9,9 +9,9 @@ import '@reach/dialog/styles.css'
|
||||
import { transparentize } from 'polished'
|
||||
import { useGesture } from 'react-use-gesture'
|
||||
|
||||
// errors emitted, fix with https://github.com/styled-components/styled-components/pull/3006
|
||||
const AnimatedDialogOverlay = animated(DialogOverlay)
|
||||
const StyledDialogOverlay = styled(AnimatedDialogOverlay)`
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const StyledDialogOverlay = styled(({ mobile, ...rest }) => <AnimatedDialogOverlay {...rest} />)<{ mobile: boolean }>`
|
||||
&[data-reach-dialog-overlay] {
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
@@ -41,7 +41,9 @@ const StyledDialogOverlay = styled(AnimatedDialogOverlay)`
|
||||
|
||||
// destructure to not pass custom props to Dialog DOM element
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...rest }) => <DialogContent {...rest} />)`
|
||||
const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...rest }) => (
|
||||
<DialogContent aria-label="content" {...rest} />
|
||||
))`
|
||||
&[data-reach-dialog-content] {
|
||||
margin: 0 0 2rem 0;
|
||||
border: 1px solid ${({ theme }) => theme.bg1};
|
||||
@@ -95,7 +97,7 @@ interface ModalProps {
|
||||
onDismiss: () => void
|
||||
minHeight?: number | false
|
||||
maxHeight?: number
|
||||
initialFocusRef?: React.Ref<any>
|
||||
initialFocusRef?: React.RefObject<any>
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
@@ -145,7 +147,7 @@ export default function Modal({
|
||||
style={props}
|
||||
onDismiss={onDismiss}
|
||||
initialFocusRef={initialFocusRef}
|
||||
mobile={isMobile}
|
||||
mobile={true}
|
||||
>
|
||||
<Spring // animation for entrance and exit
|
||||
from={{
|
||||
@@ -163,6 +165,7 @@ export default function Modal({
|
||||
}}
|
||||
>
|
||||
<StyledDialogContent
|
||||
ariaLabel="test"
|
||||
style={props}
|
||||
hidden={true}
|
||||
minHeight={minHeight}
|
||||
@@ -191,15 +194,9 @@ export default function Modal({
|
||||
style={props}
|
||||
onDismiss={onDismiss}
|
||||
initialFocusRef={initialFocusRef}
|
||||
mobile={isMobile ? isMobile : undefined}
|
||||
mobile={false}
|
||||
>
|
||||
<StyledDialogContent
|
||||
hidden={true}
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
isOpen={isOpen}
|
||||
mobile={isMobile ? isMobile : undefined}
|
||||
>
|
||||
<StyledDialogContent hidden={true} minHeight={minHeight} maxHeight={maxHeight} isOpen={isOpen}>
|
||||
<HiddenCloseButton onClick={onDismiss} />
|
||||
{children}
|
||||
</StyledDialogContent>
|
||||
|
||||
@@ -3,13 +3,12 @@ import styled from 'styled-components'
|
||||
import { darken } from 'polished'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { withRouter, NavLink, Link as HistoryLink, RouteComponentProps } from 'react-router-dom'
|
||||
import useBodyKeyDown from '../../hooks/useBodyKeyDown'
|
||||
|
||||
import { CursorPointer } from '../../theme'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import { RowBetween } from '../Row'
|
||||
import QuestionHelper from '../Question'
|
||||
|
||||
import { useBodyKeyDown } from '../../hooks'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
|
||||
const tabOrder = [
|
||||
{
|
||||
@@ -110,8 +109,8 @@ function NavigationTabs({ location: { pathname }, history }: RouteComponentProps
|
||||
<QuestionHelper
|
||||
text={
|
||||
adding
|
||||
? 'When you add liquidity, you are given pool tokens that represent your position in this pool. These tokens automatically earn fees proportional to your pool share and can be redeemed at any time.'
|
||||
: 'Your liquidity is represented by a pool token (ERC20). Removing will convert your position back into tokens at the current rate and proportional to the amount of each token in the pool. Any fees you accrued are included in the token amounts you receive.'
|
||||
? '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>
|
||||
|
||||
135
src/components/Popover/index.tsx
Normal file
135
src/components/Popover/index.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { Placement } from '@popperjs/core'
|
||||
import { transparentize } from 'polished'
|
||||
import React, { useState } from 'react'
|
||||
import { usePopper } from 'react-popper'
|
||||
import styled, { keyframes } from 'styled-components'
|
||||
import useInterval from '../../hooks/useInterval'
|
||||
import Portal from '@reach/portal'
|
||||
|
||||
const fadeIn = keyframes`
|
||||
from {
|
||||
opacity : 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity : 1;
|
||||
}
|
||||
`
|
||||
|
||||
const fadeOut = keyframes`
|
||||
from {
|
||||
opacity : 1;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity : 0;
|
||||
}
|
||||
`
|
||||
|
||||
const PopoverContainer = styled.div<{ show: boolean }>`
|
||||
z-index: 9999;
|
||||
|
||||
visibility: ${props => (!props.show ? 'hidden' : 'visible')};
|
||||
animation: ${props => (!props.show ? fadeOut : fadeIn)} 150ms linear;
|
||||
transition: visibility 150ms linear;
|
||||
|
||||
background: ${({ theme }) => theme.bg2};
|
||||
border: 1px solid ${({ theme }) => theme.bg3};
|
||||
box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.9, theme.shadow1)};
|
||||
color: ${({ theme }) => theme.text2};
|
||||
border-radius: 8px;
|
||||
`
|
||||
|
||||
const ReferenceElement = styled.div`
|
||||
display: inline-block;
|
||||
`
|
||||
|
||||
const Arrow = styled.div`
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
z-index: 9998;
|
||||
|
||||
::before {
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
z-index: 9998;
|
||||
|
||||
content: '';
|
||||
border: 1px solid ${({ theme }) => theme.bg3};
|
||||
transform: rotate(45deg);
|
||||
background: ${({ theme }) => theme.bg2};
|
||||
}
|
||||
|
||||
&.arrow-top {
|
||||
bottom: -5px;
|
||||
::before {
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.arrow-bottom {
|
||||
top: -5px;
|
||||
::before {
|
||||
border-bottom: none;
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.arrow-left {
|
||||
right: -5px;
|
||||
|
||||
::before {
|
||||
border-bottom: none;
|
||||
border-left: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.arrow-right {
|
||||
left: -5px;
|
||||
::before {
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export interface PopoverProps {
|
||||
content: React.ReactNode
|
||||
show: boolean
|
||||
children: React.ReactNode
|
||||
placement?: Placement
|
||||
}
|
||||
|
||||
export default function Popover({ content, show, children, placement = 'auto' }: PopoverProps) {
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLDivElement>(null)
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement>(null)
|
||||
const [arrowElement, setArrowElement] = useState<HTMLDivElement>(null)
|
||||
const { styles, update, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement,
|
||||
strategy: 'fixed',
|
||||
modifiers: [
|
||||
{ name: 'offset', options: { offset: [8, 8] } },
|
||||
{ name: 'arrow', options: { element: arrowElement } }
|
||||
]
|
||||
})
|
||||
useInterval(update, show ? 100 : null)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ReferenceElement ref={setReferenceElement}>{children}</ReferenceElement>
|
||||
<Portal>
|
||||
<PopoverContainer show={show} ref={setPopperElement} style={styles.popper} {...attributes.popper}>
|
||||
{content}
|
||||
<Arrow
|
||||
className={`arrow-${attributes.popper?.['data-popper-placement'] ?? ''}`}
|
||||
ref={setArrowElement}
|
||||
style={styles.arrow}
|
||||
{...attributes.arrow}
|
||||
/>
|
||||
</PopoverContainer>
|
||||
</Portal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { useContext } from 'react'
|
||||
import { ChainId, Pair, Token } from '@uniswap/sdk'
|
||||
import React, { useContext, useMemo } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import { useMediaLayout } from 'use-media'
|
||||
|
||||
import { X } from 'react-feather'
|
||||
import { PopupContent } from '../../state/application/actions'
|
||||
import { useActivePopups, useRemovePopup } from '../../state/application/hooks'
|
||||
import { Link } from '../../theme'
|
||||
import { ExternalLink } from '../../theme'
|
||||
import { AutoColumn } from '../Column'
|
||||
import DoubleTokenLogo from '../DoubleLogo'
|
||||
import Row from '../Row'
|
||||
@@ -71,6 +72,40 @@ const Popup = styled.div`
|
||||
`}
|
||||
`
|
||||
|
||||
function PoolPopup({
|
||||
token0,
|
||||
token1
|
||||
}: {
|
||||
token0: { address?: string; symbol?: string }
|
||||
token1: { address?: string; symbol?: string }
|
||||
}) {
|
||||
const pairAddress: string | null = useMemo(() => {
|
||||
if (!token0 || !token1) return null
|
||||
// just mock it out
|
||||
return Pair.getAddress(
|
||||
new Token(ChainId.MAINNET, token0.address, 18),
|
||||
new Token(ChainId.MAINNET, token1.address, 18)
|
||||
)
|
||||
}, [token0, token1])
|
||||
|
||||
return (
|
||||
<AutoColumn gap={'10px'}>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
Pool Imported
|
||||
</Text>
|
||||
<Row>
|
||||
<DoubleTokenLogo a0={token0?.address ?? ''} a1={token1?.address ?? ''} margin={true} />
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
UNI {token0?.symbol} / {token1?.symbol}
|
||||
</Text>
|
||||
</Row>
|
||||
{pairAddress ? (
|
||||
<ExternalLink href={`https://uniswap.info/pair/${pairAddress}`}>View on Uniswap Info.</ExternalLink>
|
||||
) : null}
|
||||
</AutoColumn>
|
||||
)
|
||||
}
|
||||
|
||||
function PopupItem({ content, popKey }: { content: PopupContent; popKey: string }) {
|
||||
if ('txn' in content) {
|
||||
const {
|
||||
@@ -81,24 +116,12 @@ function PopupItem({ content, popKey }: { content: PopupContent; popKey: string
|
||||
const {
|
||||
poolAdded: { token0, token1 }
|
||||
} = content
|
||||
return (
|
||||
<AutoColumn gap={'10px'}>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
Pool Imported
|
||||
</Text>
|
||||
<Row>
|
||||
<DoubleTokenLogo a0={token0?.address ?? ''} a1={token1?.address ?? ''} margin={true} />
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
UNI {token0?.symbol} / {token1?.symbol}
|
||||
</Text>
|
||||
</Row>
|
||||
<Link>View on Uniswap Info.</Link>
|
||||
</AutoColumn>
|
||||
)
|
||||
|
||||
return <PoolPopup token0={token0} token1={token1} />
|
||||
}
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
export default function Popups() {
|
||||
const theme = useContext(ThemeContext)
|
||||
// get all popups
|
||||
const activePopups = useActivePopups()
|
||||
|
||||
@@ -12,7 +12,7 @@ import Card, { GreyCard } from '../Card'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import DoubleLogo from '../DoubleLogo'
|
||||
import { Text } from 'rebass'
|
||||
import { Link } from '../../theme/components'
|
||||
import { ExternalLink } from '../../theme/components'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { ChevronDown, ChevronUp } from 'react-feather'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
@@ -204,7 +204,9 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
|
||||
)}
|
||||
|
||||
<AutoRow justify="center" marginTop={'10px'}>
|
||||
<Link href={`https://uniswap.info/pair/${pair?.liquidityToken.address}`}>View pool information ↗</Link>
|
||||
<ExternalLink href={`https://uniswap.info/pair/${pair?.liquidityToken.address}`}>
|
||||
View pool information ↗
|
||||
</ExternalLink>
|
||||
</AutoRow>
|
||||
<RowBetween marginTop="10px">
|
||||
<ButtonSecondary
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
import React, { useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import styled, { keyframes } from 'styled-components'
|
||||
import { HelpCircle as Question } from 'react-feather'
|
||||
import { usePopper } from 'react-popper'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const QuestionWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 0.4rem;
|
||||
padding: 0.2rem;
|
||||
border: none;
|
||||
background: none;
|
||||
outline: none;
|
||||
cursor: default;
|
||||
border-radius: 36px;
|
||||
background-color: ${({ theme }) => theme.bg2};
|
||||
color: ${({ theme }) => theme.text2};
|
||||
|
||||
:hover,
|
||||
:focus {
|
||||
opacity: 0.7;
|
||||
}
|
||||
`
|
||||
const fadeIn = keyframes`
|
||||
from {
|
||||
opacity : 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity : 1;
|
||||
}
|
||||
`
|
||||
|
||||
const Popup = styled.div`
|
||||
width: 228px;
|
||||
z-index: 9999;
|
||||
padding: 0.6rem 1rem;
|
||||
line-height: 150%;
|
||||
background: ${({ theme }) => theme.bg1};
|
||||
border: 1px solid ${({ theme }) => theme.bg3};
|
||||
|
||||
border-radius: 8px;
|
||||
|
||||
animation: ${fadeIn} 0.15s linear;
|
||||
|
||||
color: ${({ theme }) => theme.text2};
|
||||
font-weight: 400;
|
||||
`
|
||||
|
||||
export default function QuestionHelper({ text }: { text: string }) {
|
||||
const [showPopup, setShowPopup] = useState<boolean>(false)
|
||||
const [referenceElement, setReferenceElement] = useState(null)
|
||||
const [popperElement, setPopperElement] = useState(null)
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: 'auto',
|
||||
strategy: 'fixed',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [6, 6]
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const portal = createPortal(
|
||||
showPopup && (
|
||||
<Popup ref={setPopperElement} style={styles.popper} {...attributes.popper}>
|
||||
{text}
|
||||
</Popup>
|
||||
),
|
||||
document.getElementById('popover-container')
|
||||
)
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<QuestionWrapper
|
||||
onClick={() => {
|
||||
setShowPopup(true)
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setShowPopup(true)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setShowPopup(false)
|
||||
}}
|
||||
ref={setReferenceElement}
|
||||
>
|
||||
<Question size={16} />
|
||||
</QuestionWrapper>
|
||||
{portal}
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
40
src/components/QuestionHelper/index.tsx
Normal file
40
src/components/QuestionHelper/index.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { HelpCircle as Question } from 'react-feather'
|
||||
import styled from 'styled-components'
|
||||
import Tooltip from '../Tooltip'
|
||||
|
||||
const QuestionWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.2rem;
|
||||
border: none;
|
||||
background: none;
|
||||
outline: none;
|
||||
cursor: default;
|
||||
border-radius: 36px;
|
||||
background-color: ${({ theme }) => theme.bg2};
|
||||
color: ${({ theme }) => theme.text2};
|
||||
|
||||
:hover,
|
||||
:focus {
|
||||
opacity: 0.7;
|
||||
}
|
||||
`
|
||||
|
||||
export default function QuestionHelper({ text, disabled }: { text: string; disabled?: boolean }) {
|
||||
const [show, setShow] = useState<boolean>(false)
|
||||
|
||||
const open = useCallback(() => setShow(true), [setShow])
|
||||
const close = useCallback(() => setShow(false), [setShow])
|
||||
|
||||
return (
|
||||
<span style={{ marginLeft: 4 }}>
|
||||
<Tooltip text={text} show={show && !disabled}>
|
||||
<QuestionWrapper onClick={open} onMouseEnter={open} onMouseLeave={close}>
|
||||
<Question size={16} />
|
||||
</QuestionWrapper>
|
||||
</Tooltip>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
48
src/components/SearchModal/CommonBases.tsx
Normal file
48
src/components/SearchModal/CommonBases.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import { Token } from '@uniswap/sdk'
|
||||
|
||||
import { SUGGESTED_BASES } from '../../constants'
|
||||
import { AutoColumn } from '../Column'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { AutoRow } from '../Row'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import { BaseWrapper } from './styleds'
|
||||
|
||||
export default function CommonBases({
|
||||
chainId,
|
||||
onSelect,
|
||||
selectedTokenAddress
|
||||
}: {
|
||||
chainId: number
|
||||
selectedTokenAddress: string
|
||||
onSelect: (tokenAddress: string) => void
|
||||
}) {
|
||||
return (
|
||||
<AutoColumn gap="md">
|
||||
<AutoRow>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
Common Bases
|
||||
</Text>
|
||||
<QuestionHelper text="These tokens are commonly used in pairs." />
|
||||
</AutoRow>
|
||||
<AutoRow gap="10px">
|
||||
{(SUGGESTED_BASES[chainId] ?? []).map((token: Token) => {
|
||||
return (
|
||||
<BaseWrapper
|
||||
gap="6px"
|
||||
onClick={() => selectedTokenAddress !== token.address && onSelect(token.address)}
|
||||
disable={selectedTokenAddress === token.address}
|
||||
key={token.address}
|
||||
>
|
||||
<TokenLogo address={token.address} />
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
{token.symbol}
|
||||
</Text>
|
||||
</BaseWrapper>
|
||||
)
|
||||
})}
|
||||
</AutoRow>
|
||||
</AutoColumn>
|
||||
)
|
||||
}
|
||||
64
src/components/SearchModal/PairList.tsx
Normal file
64
src/components/SearchModal/PairList.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
34
src/components/SearchModal/SortButton.tsx
Normal file
34
src/components/SearchModal/SortButton.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import styled from 'styled-components'
|
||||
import { RowFixed } from '../Row'
|
||||
|
||||
export const FilterWrapper = styled(RowFixed)`
|
||||
padding: 8px;
|
||||
background-color: ${({ theme }) => theme.bg2};
|
||||
color: ${({ theme }) => theme.text1};
|
||||
border-radius: 8px;
|
||||
user-select: none;
|
||||
& > * {
|
||||
user-select: none;
|
||||
}
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`
|
||||
|
||||
export default function SortButton({
|
||||
toggleSortOrder,
|
||||
ascending
|
||||
}: {
|
||||
toggleSortOrder: () => void
|
||||
ascending: boolean
|
||||
}) {
|
||||
return (
|
||||
<FilterWrapper onClick={toggleSortOrder}>
|
||||
<Text fontSize={14} fontWeight={500}>
|
||||
{ascending ? '↑' : '↓'}
|
||||
</Text>
|
||||
</FilterWrapper>
|
||||
)
|
||||
}
|
||||
122
src/components/SearchModal/TokenList.tsx
Normal file
122
src/components/SearchModal/TokenList.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { ChainId, JSBI, Token, TokenAmount } from '@uniswap/sdk'
|
||||
import React, { useContext } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FixedSizeList } from 'react-window'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import Circle from '../../assets/images/circle.svg'
|
||||
import { ALL_TOKENS } from '../../constants/tokens'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { LinkStyledButton, TYPE } from '../../theme'
|
||||
import { isAddress } from '../../utils'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
import Column, { AutoColumn } from '../Column'
|
||||
import { RowFixed } from '../Row'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import { FadedSpan, GreySpan, MenuItem, SpinnerWrapper, ModalInfo } from './styleds'
|
||||
|
||||
function isDefaultToken(tokenAddress: string, chainId?: number): boolean {
|
||||
const address = isAddress(tokenAddress)
|
||||
return Boolean(chainId && address && ALL_TOKENS[chainId as ChainId]?.[tokenAddress])
|
||||
}
|
||||
|
||||
export default function TokenList({
|
||||
tokens,
|
||||
allTokenBalances,
|
||||
selectedToken,
|
||||
onTokenSelect,
|
||||
otherToken,
|
||||
showSendWithSwap,
|
||||
onRemoveAddedToken,
|
||||
otherSelectedText
|
||||
}: {
|
||||
tokens: Token[]
|
||||
selectedToken: string
|
||||
allTokenBalances: { [tokenAddress: string]: TokenAmount }
|
||||
onTokenSelect: (tokenAddress: string) => void
|
||||
onRemoveAddedToken: (chainId: number, tokenAddress: string) => void
|
||||
otherToken: string
|
||||
showSendWithSwap?: boolean
|
||||
otherSelectedText: string
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
if (tokens.length === 0) {
|
||||
return <ModalInfo>{t('noToken')}</ModalInfo>
|
||||
}
|
||||
return (
|
||||
<FixedSizeList
|
||||
width="100%"
|
||||
height={500}
|
||||
itemCount={tokens.length}
|
||||
itemSize={50}
|
||||
style={{ flex: '1', minHeight: 200 }}
|
||||
>
|
||||
{({ index, style }) => {
|
||||
const { address, symbol } = tokens[index]
|
||||
|
||||
const customAdded = !isDefaultToken(address, chainId)
|
||||
const balance = allTokenBalances[address]
|
||||
|
||||
const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw)
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
style={style}
|
||||
key={address}
|
||||
className={`token-item-${address}`}
|
||||
onClick={() => (selectedToken && selectedToken === address ? null : onTokenSelect(address))}
|
||||
disabled={selectedToken && selectedToken === address}
|
||||
selected={otherToken === address}
|
||||
>
|
||||
<RowFixed>
|
||||
<TokenLogo address={address} size={'24px'} style={{ marginRight: '14px' }} />
|
||||
<Column>
|
||||
<Text fontWeight={500}>
|
||||
{symbol}
|
||||
{otherToken === address && <GreySpan> ({otherSelectedText})</GreySpan>}
|
||||
</Text>
|
||||
<FadedSpan>
|
||||
<TYPE.main fontWeight={500}>{customAdded && 'Added by user'}</TYPE.main>
|
||||
{customAdded && (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import { FilterWrapper } from './styleds'
|
||||
|
||||
export function TokenSortButton({
|
||||
title,
|
||||
toggleSortOrder,
|
||||
invertSearchOrder
|
||||
}: {
|
||||
title: string
|
||||
toggleSortOrder: () => void
|
||||
invertSearchOrder: boolean
|
||||
}) {
|
||||
return (
|
||||
<FilterWrapper onClick={toggleSortOrder}>
|
||||
<Text fontSize={14} fontWeight={500}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text fontSize={14} fontWeight={500}>
|
||||
{!invertSearchOrder ? '↓' : '↑'}
|
||||
</Text>
|
||||
</FilterWrapper>
|
||||
)
|
||||
}
|
||||
50
src/components/SearchModal/filtering.ts
Normal file
50
src/components/SearchModal/filtering.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { isAddress } from '../../utils'
|
||||
import { Pair, Token } from '@uniswap/sdk'
|
||||
|
||||
export function filterTokens(tokens: Token[], search: string): Token[] {
|
||||
if (search.length === 0) return tokens
|
||||
|
||||
const searchingAddress = isAddress(search)
|
||||
|
||||
if (searchingAddress) {
|
||||
return tokens.filter(token => token.address === searchingAddress)
|
||||
}
|
||||
|
||||
const lowerSearchParts = searchingAddress ? [] : search.toLowerCase().split(/\s+/)
|
||||
|
||||
const matchesSearch = (s: string): boolean => {
|
||||
const sParts = s.toLowerCase().split(/\s+/)
|
||||
|
||||
return lowerSearchParts.every(p => p.length === 0 || sParts.some(sp => sp.startsWith(p)))
|
||||
}
|
||||
|
||||
return tokens.filter(token => {
|
||||
const { symbol, name } = token
|
||||
|
||||
return matchesSearch(symbol) || matchesSearch(name)
|
||||
})
|
||||
}
|
||||
|
||||
export function filterPairs(pairs: Pair[], search: string): Pair[] {
|
||||
if (search.trim().length === 0) return pairs
|
||||
|
||||
const addressSearch = isAddress(search)
|
||||
if (addressSearch) {
|
||||
return pairs.filter(p => {
|
||||
return (
|
||||
p.token0.address === addressSearch ||
|
||||
p.token1.address === addressSearch ||
|
||||
p.liquidityToken.address === addressSearch
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const lowerSearch = search.toLowerCase()
|
||||
return pairs.filter(pair => {
|
||||
const pairExpressionA = `${pair.token0.symbol}/${pair.token1.symbol}`.toLowerCase()
|
||||
if (pairExpressionA.startsWith(lowerSearch)) return true
|
||||
const pairExpressionB = `${pair.token1.symbol}/${pair.token0.symbol}`.toLowerCase()
|
||||
if (pairExpressionB.startsWith(lowerSearch)) return true
|
||||
return filterTokens([pair.token0, pair.token1], search).length > 0
|
||||
})
|
||||
}
|
||||
@@ -1,43 +1,29 @@
|
||||
import '@reach/tooltip/styles.css'
|
||||
import { ChainId, JSBI, Token, WETH } from '@uniswap/sdk'
|
||||
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Pair, Token } from '@uniswap/sdk'
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import Circle from '../../assets/images/circle.svg'
|
||||
import Card from '../../components/Card'
|
||||
import { COMMON_BASES } from '../../constants'
|
||||
import { ALL_TOKENS } from '../../constants/tokens'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens, useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
|
||||
import { useAllDummyPairs, useRemoveUserAddedToken } from '../../state/user/hooks'
|
||||
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||
import { CursorPointer, TYPE } from '../../theme'
|
||||
import { CloseIcon, Link as StyledLink } from '../../theme/components'
|
||||
import { escapeRegExp, isAddress } from '../../utils'
|
||||
import { ButtonPrimary, ButtonSecondary } from '../Button'
|
||||
import Column, { AutoColumn } from '../Column'
|
||||
import DoubleTokenLogo from '../DoubleLogo'
|
||||
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 '../Question'
|
||||
import { AutoRow, RowBetween, RowFixed } from '../Row'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import { useTokenComparator } from './sorting'
|
||||
import {
|
||||
BaseWrapper,
|
||||
FadedSpan,
|
||||
GreySpan,
|
||||
Input,
|
||||
ItemList,
|
||||
MenuItem,
|
||||
PaddedColumn,
|
||||
SpinnerWrapper,
|
||||
TokenModalInfo
|
||||
} from './styleds'
|
||||
import { TokenSortButton } from './TokenSortButton'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { AutoRow, RowBetween } from '../Row'
|
||||
import Tooltip from '../Tooltip'
|
||||
import CommonBases from './CommonBases'
|
||||
import { filterPairs, filterTokens } from './filtering'
|
||||
import PairList from './PairList'
|
||||
import { useTokenComparator, pairComparator } from './sorting'
|
||||
import { PaddedColumn, SearchInput } from './styleds'
|
||||
import TokenList from './TokenList'
|
||||
import SortButton from './SortButton'
|
||||
|
||||
interface SearchModalProps extends RouteComponentProps {
|
||||
isOpen?: boolean
|
||||
@@ -51,11 +37,6 @@ interface SearchModalProps extends RouteComponentProps {
|
||||
showCommonBases?: boolean
|
||||
}
|
||||
|
||||
function isDefaultToken(tokenAddress: string, chainId?: number): boolean {
|
||||
const address = isAddress(tokenAddress)
|
||||
return Boolean(chainId && address && ALL_TOKENS[chainId as ChainId]?.[tokenAddress])
|
||||
}
|
||||
|
||||
function SearchModal({
|
||||
history,
|
||||
isOpen,
|
||||
@@ -72,421 +53,169 @@ function SearchModal({
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const isTokenView = filterType === 'tokens'
|
||||
|
||||
const allTokens = useAllTokens()
|
||||
const allPairs = useAllDummyPairs()
|
||||
const allBalances = useAllTokenBalancesTreatingWETHasETH()
|
||||
const allTokenBalances = useAllTokenBalancesTreatingWETHasETH() ?? {}
|
||||
const allPairBalances = useTokenBalances(
|
||||
account,
|
||||
allPairs.map(p => p.liquidityToken)
|
||||
)
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [invertSearchOrder, setInvertSearchOrder] = useState(false)
|
||||
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
|
||||
const searchQueryToken = useTokenByAddressAndAutomaticallyAdd(searchQuery)
|
||||
|
||||
// toggle specific token import view
|
||||
const [showTokenImport, setShowTokenImport] = useState(false)
|
||||
|
||||
// used to help scanning on results, put token found from input on left
|
||||
const [identifiedToken, setIdentifiedToken] = useState<Token>()
|
||||
|
||||
// reset view on close
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setShowTokenImport(false)
|
||||
}
|
||||
}, [isOpen])
|
||||
// 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 sortedTokenList = useMemo(() => {
|
||||
return Object.values(allTokens)
|
||||
.sort(tokenComparator)
|
||||
.map(token => {
|
||||
return {
|
||||
name: token.name,
|
||||
symbol: token.symbol,
|
||||
address: token.address,
|
||||
balance: allBalances[account]?.[token.address]
|
||||
}
|
||||
})
|
||||
}, [allTokens, tokenComparator, allBalances, account])
|
||||
const sortedTokens: Token[] = useMemo(() => {
|
||||
if (!isTokenView) return []
|
||||
return Object.values(allTokens).sort(tokenComparator)
|
||||
}, [allTokens, isTokenView, tokenComparator])
|
||||
|
||||
const filteredTokenList = useMemo(() => {
|
||||
return sortedTokenList.filter(tokenEntry => {
|
||||
const customAdded = !isDefaultToken(tokenEntry.address, chainId)
|
||||
const filteredTokens: Token[] = useMemo(() => {
|
||||
if (!isTokenView) return []
|
||||
return filterTokens(sortedTokens, searchQuery)
|
||||
}, [isTokenView, sortedTokens, searchQuery])
|
||||
|
||||
// if token import page dont show preset list, else show all
|
||||
const include = !showTokenImport || (showTokenImport && customAdded && searchQuery !== '')
|
||||
|
||||
const inputIsAddress = searchQuery.slice(0, 2) === '0x'
|
||||
const regexMatches = Object.keys(tokenEntry).map(tokenEntryKey => {
|
||||
if (tokenEntryKey === 'address') {
|
||||
return (
|
||||
include &&
|
||||
inputIsAddress &&
|
||||
typeof tokenEntry[tokenEntryKey] === 'string' &&
|
||||
!!tokenEntry[tokenEntryKey].match(new RegExp(escapeRegExp(searchQuery), 'i'))
|
||||
)
|
||||
}
|
||||
return (
|
||||
include &&
|
||||
typeof tokenEntry[tokenEntryKey] === 'string' &&
|
||||
!!tokenEntry[tokenEntryKey].match(new RegExp(escapeRegExp(searchQuery), 'i'))
|
||||
)
|
||||
})
|
||||
return regexMatches.some(m => m)
|
||||
})
|
||||
}, [sortedTokenList, chainId, showTokenImport, searchQuery])
|
||||
|
||||
function _onTokenSelect(address) {
|
||||
setSearchQuery('')
|
||||
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()
|
||||
const inputRef = useRef<HTMLInputElement>()
|
||||
function onInput(event) {
|
||||
const input = event.target.value
|
||||
const checksummedInput = isAddress(input)
|
||||
setSearchQuery(checksummedInput || input)
|
||||
}
|
||||
|
||||
function clearInputAndDismiss() {
|
||||
setSearchQuery('')
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
// make an effort to identify the specific token a user is searching for
|
||||
useEffect(() => {
|
||||
const searchQueryIsAddress = !!isAddress(searchQuery)
|
||||
|
||||
// try to find an exact match by address
|
||||
if (searchQueryIsAddress) {
|
||||
const identifiedTokenByAddress = Object.values(allTokens).filter(token => {
|
||||
return searchQueryIsAddress && token.address === isAddress(searchQuery)
|
||||
})
|
||||
if (identifiedTokenByAddress.length > 0) setIdentifiedToken(identifiedTokenByAddress[0])
|
||||
}
|
||||
// try to find an exact match by symbol
|
||||
else {
|
||||
const identifiedTokenBySymbol = Object.values(allTokens).filter(token => {
|
||||
return token.symbol.slice(0, searchQuery.length).toLowerCase() === searchQuery.toLowerCase()
|
||||
})
|
||||
if (identifiedTokenBySymbol.length > 0) setIdentifiedToken(identifiedTokenBySymbol[0])
|
||||
}
|
||||
|
||||
return () => {
|
||||
setIdentifiedToken(undefined)
|
||||
}
|
||||
}, [allTokens, searchQuery])
|
||||
|
||||
const sortedPairList = useMemo(() => {
|
||||
if (isTokenView) return []
|
||||
return allPairs.sort((a, b): number => {
|
||||
// sort by balance
|
||||
const balanceA = allBalances[account]?.[a.liquidityToken.address]
|
||||
const balanceB = allBalances[account]?.[b.liquidityToken.address]
|
||||
if (balanceA?.greaterThan('0') && !balanceB?.greaterThan('0')) return !invertSearchOrder ? -1 : 1
|
||||
if (!balanceA?.greaterThan('0') && balanceB?.greaterThan('0')) return !invertSearchOrder ? 1 : -1
|
||||
if (balanceA?.greaterThan('0') && balanceB?.greaterThan('0')) {
|
||||
return balanceA.greaterThan(balanceB) ? (!invertSearchOrder ? -1 : 1) : !invertSearchOrder ? 1 : -1
|
||||
}
|
||||
return 0
|
||||
const balanceA = allPairBalances[a.liquidityToken.address]
|
||||
const balanceB = allPairBalances[b.liquidityToken.address]
|
||||
return pairComparator(a, b, balanceA, balanceB)
|
||||
})
|
||||
}, [allPairs, allBalances, account, invertSearchOrder])
|
||||
}, [isTokenView, allPairs, allPairBalances])
|
||||
|
||||
const filteredPairList = useMemo(() => {
|
||||
const searchQueryIsAddress = !!isAddress(searchQuery)
|
||||
return sortedPairList.filter(pair => {
|
||||
// if there's no search query, hide non-ETH pairs
|
||||
if (searchQuery === '') return pair.token0.equals(WETH[chainId]) || pair.token1.equals(WETH[chainId])
|
||||
const filteredPairs = useMemo(() => {
|
||||
if (isTokenView) return []
|
||||
return filterPairs(sortedPairList, searchQuery)
|
||||
}, [isTokenView, searchQuery, sortedPairList])
|
||||
|
||||
const token0 = pair.token0
|
||||
const token1 = pair.token1
|
||||
const selectPair = useCallback(
|
||||
(pair: Pair) => {
|
||||
history.push(`/add/${pair.token0.address}-${pair.token1.address}`)
|
||||
},
|
||||
[history]
|
||||
)
|
||||
|
||||
if (searchQueryIsAddress) {
|
||||
if (token0.address === isAddress(searchQuery)) return true
|
||||
if (token1.address === isAddress(searchQuery)) return true
|
||||
} else {
|
||||
const identifier0 = `${token0.symbol}/${token1.symbol}`
|
||||
const identifier1 = `${token1.symbol}/${token0.symbol}`
|
||||
if (identifier0.slice(0, searchQuery.length).toLowerCase() === searchQuery.toLowerCase()) return true
|
||||
if (identifier1.slice(0, searchQuery.length).toLowerCase() === searchQuery.toLowerCase()) return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}, [searchQuery, sortedPairList, chainId])
|
||||
const focusedToken = Object.values(allTokens ?? {}).filter(token => {
|
||||
return token.symbol.toLowerCase() === searchQuery || searchQuery === token.address
|
||||
})[0]
|
||||
|
||||
function renderPairsList() {
|
||||
if (filteredPairList?.length === 0) {
|
||||
return (
|
||||
<PaddedColumn justify="center">
|
||||
<Text>No Pools Found</Text>
|
||||
</PaddedColumn>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
filteredPairList &&
|
||||
filteredPairList.map((pair, i) => {
|
||||
// reset ordering to help scan search results
|
||||
const token0 = identifiedToken ? (identifiedToken.equals(pair.token0) ? pair.token0 : pair.token1) : pair.token0
|
||||
const token1 = identifiedToken ? (identifiedToken.equals(pair.token0) ? pair.token1 : pair.token0) : pair.token1
|
||||
const pairAddress = pair.liquidityToken.address
|
||||
const balance = allBalances?.[account]?.[pairAddress]?.toSignificant(6)
|
||||
const zeroBalance =
|
||||
allBalances?.[account]?.[pairAddress]?.raw &&
|
||||
JSBI.equal(allBalances?.[account]?.[pairAddress].raw, JSBI.BigInt(0))
|
||||
return (
|
||||
<MenuItem
|
||||
key={i}
|
||||
onClick={() => {
|
||||
history.push('/add/' + token0.address + '-' + token1.address)
|
||||
onDismiss()
|
||||
}}
|
||||
>
|
||||
<RowFixed>
|
||||
<DoubleTokenLogo a0={token0?.address || ''} a1={token1?.address || ''} size={24} margin={true} />
|
||||
<Text fontWeight={500} fontSize={16}>{`${token0?.symbol}/${token1?.symbol}`}</Text>
|
||||
</RowFixed>
|
||||
|
||||
<ButtonPrimary
|
||||
padding={'6px 8px'}
|
||||
width={'fit-content'}
|
||||
borderRadius={'12px'}
|
||||
onClick={() => {
|
||||
history.push('/add/' + token0.address + '-' + token1.address)
|
||||
onDismiss()
|
||||
}}
|
||||
>
|
||||
{balance ? (zeroBalance ? 'Join' : 'Add Liquidity') : 'Join'}
|
||||
</ButtonPrimary>
|
||||
</MenuItem>
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function renderTokenList() {
|
||||
if (filteredTokenList.length === 0) {
|
||||
if (isAddress(searchQuery)) {
|
||||
if (!searchQueryToken) {
|
||||
return <TokenModalInfo>Searching...</TokenModalInfo>
|
||||
} else {
|
||||
// a user found a token by search that isn't yet added to localstorage
|
||||
return (
|
||||
<MenuItem
|
||||
key={searchQueryToken.address}
|
||||
className={`temporary-token-${searchQueryToken.address}`}
|
||||
onClick={() => {
|
||||
_onTokenSelect(searchQueryToken.address)
|
||||
}}
|
||||
>
|
||||
<RowFixed>
|
||||
<TokenLogo address={searchQueryToken.address} size={'24px'} style={{ marginRight: '14px' }} />
|
||||
<Column>
|
||||
<Text fontWeight={500}>{searchQueryToken.symbol}</Text>
|
||||
<FadedSpan>(Found by search)</FadedSpan>
|
||||
</Column>
|
||||
</RowFixed>
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
} else {
|
||||
return <TokenModalInfo>{t('noToken')}</TokenModalInfo>
|
||||
}
|
||||
} else {
|
||||
return filteredTokenList.map(({ address, symbol, balance }) => {
|
||||
const customAdded = !isDefaultToken(address, chainId)
|
||||
|
||||
const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw)
|
||||
|
||||
// if token import page dont show preset list, else show all
|
||||
return (
|
||||
<MenuItem
|
||||
key={address}
|
||||
className={`token-item-${address}`}
|
||||
onClick={() => (hiddenToken && hiddenToken === address ? null : _onTokenSelect(address))}
|
||||
disabled={hiddenToken && hiddenToken === address}
|
||||
selected={otherSelectedTokenAddress === address}
|
||||
>
|
||||
<RowFixed>
|
||||
<TokenLogo address={address} size={'24px'} style={{ marginRight: '14px' }} />
|
||||
<Column>
|
||||
<Text fontWeight={500}>
|
||||
{symbol}
|
||||
{otherSelectedTokenAddress === address && <GreySpan> ({otherSelectedText})</GreySpan>}
|
||||
</Text>
|
||||
<FadedSpan>
|
||||
<TYPE.main fontWeight={500}>{customAdded && 'Added by user'}</TYPE.main>
|
||||
{customAdded && (
|
||||
<div
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
if (searchQuery === address) {
|
||||
setSearchQuery('')
|
||||
}
|
||||
removeTokenByAddress(chainId, address)
|
||||
}}
|
||||
>
|
||||
<StyledLink style={{ marginLeft: '4px', fontWeight: 400 }}>(Remove)</StyledLink>
|
||||
</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>
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
const openTooltip = useCallback(() => {
|
||||
setTooltipOpen(true)
|
||||
inputRef.current?.focus()
|
||||
}, [setTooltipOpen])
|
||||
const closeTooltip = useCallback(() => setTooltipOpen(false), [setTooltipOpen])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onDismiss={clearInputAndDismiss}
|
||||
maxHeight={70}
|
||||
initialFocusRef={isMobile ? undefined : inputRef}
|
||||
>
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={70} initialFocusRef={isMobile ? undefined : inputRef}>
|
||||
<Column style={{ width: '100%' }}>
|
||||
{showTokenImport ? (
|
||||
<PaddedColumn gap="lg">
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<CursorPointer>
|
||||
<ArrowLeft
|
||||
onClick={() => {
|
||||
setShowTokenImport(false)
|
||||
}}
|
||||
/>
|
||||
</CursorPointer>
|
||||
<Text fontWeight={500} fontSize={16} marginLeft={'10px'}>
|
||||
Import A Token
|
||||
</Text>
|
||||
</RowFixed>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
<TYPE.body style={{ marginTop: '10px' }}>
|
||||
To import a custom token, paste token address in the search bar.
|
||||
</TYPE.body>
|
||||
<Input type={'text'} placeholder={'0x000000...'} value={searchQuery} ref={inputRef} onChange={onInput} />
|
||||
{renderTokenList()}
|
||||
</PaddedColumn>
|
||||
) : (
|
||||
<PaddedColumn gap="20px">
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
{filterType === 'tokens' ? 'Select a token' : 'Select a pool'}
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
<Input
|
||||
<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}
|
||||
/>
|
||||
{showCommonBases && (
|
||||
<AutoColumn gap="md">
|
||||
<AutoRow>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
Common Bases
|
||||
</Text>
|
||||
<QuestionHelper text="These tokens are commonly used in pairs." />
|
||||
</AutoRow>
|
||||
<AutoRow gap="10px">
|
||||
{COMMON_BASES[chainId]?.map(token => {
|
||||
return (
|
||||
<BaseWrapper
|
||||
gap="6px"
|
||||
onClick={() => hiddenToken !== token.address && _onTokenSelect(token.address)}
|
||||
disable={hiddenToken === token.address}
|
||||
key={token.address}
|
||||
>
|
||||
<TokenLogo address={token.address} />
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
{token.symbol}
|
||||
</Text>
|
||||
</BaseWrapper>
|
||||
)
|
||||
})}
|
||||
</AutoRow>
|
||||
</AutoColumn>
|
||||
</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>
|
||||
<Text fontSize={14} fontWeight={500}>
|
||||
{filterType === 'tokens' ? 'Token Name' : 'Pool Name'}
|
||||
</Text>
|
||||
{filterType === 'tokens' && (
|
||||
<TokenSortButton
|
||||
invertSearchOrder={invertSearchOrder}
|
||||
toggleSortOrder={() => setInvertSearchOrder(iso => !iso)}
|
||||
title={filterType === 'tokens' ? 'Your Balances' : ' '}
|
||||
/>
|
||||
</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>
|
||||
)}
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
)}
|
||||
{!showTokenImport && <div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />}
|
||||
{!showTokenImport && <ItemList>{filterType === 'tokens' ? renderTokenList() : renderPairsList()}</ItemList>}
|
||||
{!showTokenImport && <div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />}
|
||||
{!showTokenImport && (
|
||||
<Card>
|
||||
<AutoRow justify={'center'}>
|
||||
<div>
|
||||
{filterType !== 'tokens' && (
|
||||
<Text fontWeight={500}>
|
||||
{!isMobile && "Don't see a pool? "}
|
||||
<StyledLink
|
||||
onClick={() => {
|
||||
history.push('/find')
|
||||
}}
|
||||
>
|
||||
{!isMobile ? 'Import it.' : 'Import pool.'}
|
||||
</StyledLink>
|
||||
</Text>
|
||||
)}
|
||||
{filterType === 'tokens' && (
|
||||
<Text fontWeight={500} color={theme.text2} fontSize={14}>
|
||||
{!isMobile && "Don't see a token? "}
|
||||
|
||||
<StyledLink
|
||||
onClick={() => {
|
||||
setShowTokenImport(true)
|
||||
}}
|
||||
>
|
||||
{!isMobile ? 'Import it.' : 'Import custom token.'}
|
||||
</StyledLink>
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</AutoRow>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</AutoRow>
|
||||
</Card>
|
||||
</Column>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
@@ -1,12 +1,44 @@
|
||||
import { Token, TokenAmount, WETH } from '@uniswap/sdk'
|
||||
import { Token, TokenAmount, WETH, Pair } from '@uniswap/sdk'
|
||||
import { useMemo } from 'react'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||
import { DUMMY_PAIRS_TO_PIN } from '../../constants'
|
||||
|
||||
// compare two token amounts with highest one coming first
|
||||
function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount) {
|
||||
if (balanceA && balanceB) {
|
||||
return balanceA.greaterThan(balanceB) ? -1 : balanceA.equalTo(balanceB) ? 0 : 1
|
||||
} else if (balanceA && balanceA.greaterThan('0')) {
|
||||
return -1
|
||||
} else if (balanceB && balanceB.greaterThan('0')) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// compare two pairs, favoring "pinned" pairs, and falling back to balances
|
||||
export function pairComparator(pairA: Pair, pairB: Pair, balanceA?: TokenAmount, balanceB?: TokenAmount) {
|
||||
const aShouldBePinned =
|
||||
DUMMY_PAIRS_TO_PIN[pairA?.token0?.chainId]?.some(
|
||||
dummyPairToPin => dummyPairToPin.liquidityToken.address === pairA?.liquidityToken?.address
|
||||
) ?? false
|
||||
const bShouldBePinned =
|
||||
DUMMY_PAIRS_TO_PIN[pairB?.token0?.chainId]?.some(
|
||||
dummyPairToPin => dummyPairToPin.liquidityToken.address === pairB?.liquidityToken?.address
|
||||
) ?? false
|
||||
|
||||
if (aShouldBePinned && !bShouldBePinned) {
|
||||
return -1
|
||||
} else if (!aShouldBePinned && bShouldBePinned) {
|
||||
return 1
|
||||
} else {
|
||||
return balanceComparator(balanceA, balanceB)
|
||||
}
|
||||
}
|
||||
|
||||
function getTokenComparator(
|
||||
weth: Token | undefined,
|
||||
balances: { [tokenAddress: string]: TokenAmount },
|
||||
invertSearchOrder: boolean
|
||||
balances: { [tokenAddress: string]: TokenAmount }
|
||||
): (tokenA: Token, tokenB: Token) => number {
|
||||
return function sortTokens(tokenA: Token, tokenB: Token): number {
|
||||
// -1 = a is first
|
||||
@@ -22,11 +54,8 @@ function getTokenComparator(
|
||||
const balanceA = balances[tokenA.address]
|
||||
const balanceB = balances[tokenB.address]
|
||||
|
||||
if (balanceA?.greaterThan('0') && !balanceB?.greaterThan('0')) return !invertSearchOrder ? -1 : 1
|
||||
if (!balanceA?.greaterThan('0') && balanceB?.greaterThan('0')) return !invertSearchOrder ? 1 : -1
|
||||
if (balanceA?.greaterThan('0') && balanceB?.greaterThan('0')) {
|
||||
return balanceA.greaterThan(balanceB) ? (!invertSearchOrder ? -1 : 1) : !invertSearchOrder ? 1 : -1
|
||||
}
|
||||
const balanceComp = balanceComparator(balanceA, balanceB)
|
||||
if (balanceComp !== 0) return balanceComp
|
||||
|
||||
// sort by symbol
|
||||
return tokenA.symbol.toLowerCase() < tokenB.symbol.toLowerCase() ? -1 : 1
|
||||
@@ -34,8 +63,15 @@ function getTokenComparator(
|
||||
}
|
||||
|
||||
export function useTokenComparator(inverted: boolean): (tokenA: Token, tokenB: Token) => number {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const weth = WETH[chainId]
|
||||
const balances = useAllTokenBalancesTreatingWETHasETH()
|
||||
return useMemo(() => getTokenComparator(weth, balances[account] ?? {}, inverted), [account, balances, inverted, weth])
|
||||
const comparator = useMemo(() => getTokenComparator(weth, balances ?? {}), [balances, weth])
|
||||
return useMemo(() => {
|
||||
if (inverted) {
|
||||
return (tokenA: Token, tokenB: Token) => comparator(tokenA, tokenB) * -1
|
||||
} else {
|
||||
return comparator
|
||||
}
|
||||
}, [inverted, comparator])
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Spinner } from '../../theme'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { AutoRow, RowBetween, RowFixed } from '../Row'
|
||||
|
||||
export const TokenModalInfo = styled.div`
|
||||
export const ModalInfo = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
padding: 1rem 1rem;
|
||||
@@ -13,13 +13,6 @@ export const TokenModalInfo = styled.div`
|
||||
min-height: 200px;
|
||||
`
|
||||
|
||||
export const ItemList = styled.div`
|
||||
flex-grow: 1;
|
||||
height: 254px;
|
||||
overflow-y: scroll;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
`
|
||||
|
||||
export const FadedSpan = styled(RowFixed)`
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
font-size: 14px;
|
||||
@@ -59,20 +52,6 @@ export const Input = styled.input`
|
||||
}
|
||||
`
|
||||
|
||||
export const FilterWrapper = styled(RowFixed)`
|
||||
padding: 8px;
|
||||
background-color: ${({ selected, theme }) => selected && theme.bg2};
|
||||
color: ${({ selected, theme }) => (selected ? theme.text1 : theme.text2)};
|
||||
border-radius: 8px;
|
||||
user-select: none;
|
||||
& > * {
|
||||
user-select: none;
|
||||
}
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`
|
||||
|
||||
export const PaddedColumn = styled(AutoColumn)`
|
||||
padding: 20px;
|
||||
padding-bottom: 12px;
|
||||
@@ -106,3 +85,11 @@ export const BaseWrapper = styled(AutoRow)<{ disable?: boolean }>`
|
||||
background-color: ${({ theme, disable }) => disable && theme.bg3};
|
||||
opacity: ${({ disable }) => disable && '0.4'};
|
||||
`
|
||||
|
||||
export const SearchInput = styled(Input)`
|
||||
transition: border 100ms;
|
||||
:focus {
|
||||
border: 1px solid ${({ theme }) => theme.primary1};
|
||||
outline: none;
|
||||
}
|
||||
`
|
||||
|
||||
@@ -1,86 +1,127 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import Slider from '@material-ui/core/Slider'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { useDebounce } from '../../hooks'
|
||||
import React, { useCallback } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const StyledSlider = withStyles({
|
||||
root: {
|
||||
width: '90%',
|
||||
color: '#565A69',
|
||||
height: 4,
|
||||
marginLeft: '15px',
|
||||
marginRight: '15px',
|
||||
padding: '15px 0'
|
||||
},
|
||||
thumb: {
|
||||
height: 28,
|
||||
width: 28,
|
||||
backgroundColor: '#565A69',
|
||||
marginTop: -14,
|
||||
marginLeft: -14,
|
||||
'&:focus,&:hover,&$active': {
|
||||
boxShadow:
|
||||
'0px 0px 1px rgba(0, 0, 0, 0.04), 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.04)',
|
||||
// Reset on touch devices, it doesn't add specificity
|
||||
'@media (hover: none)': {}
|
||||
}
|
||||
},
|
||||
active: {},
|
||||
track: {
|
||||
height: 4
|
||||
},
|
||||
rail: {
|
||||
height: 2,
|
||||
opacity: 0.5,
|
||||
backgroundColor: '#C3C5CB'
|
||||
},
|
||||
mark: {
|
||||
backgroundColor: '#C3C5CB',
|
||||
height: 12,
|
||||
width: 2,
|
||||
marginTop: -4
|
||||
},
|
||||
markActive: {
|
||||
opacity: 1,
|
||||
backgroundColor: 'currentColor',
|
||||
height: 12,
|
||||
width: 2,
|
||||
marginTop: -4
|
||||
const StyledRangeInput = styled.input<{ value: number }>`
|
||||
-webkit-appearance: none; /* Hides the slider so that custom slider can be made */
|
||||
width: 100%; /* Specific width is required for Firefox. */
|
||||
background: transparent; /* Otherwise white in Chrome */
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
})(Slider)
|
||||
|
||||
&::-moz-focus-outer {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
background-color: #565a69;
|
||||
border-radius: 100%;
|
||||
border: none;
|
||||
transform: translateY(-50%);
|
||||
color: ${({ theme }) => theme.bg1};
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.1), 0px 4px 8px rgba(0, 0, 0, 0.08), 0px 16px 24px rgba(0, 0, 0, 0.06),
|
||||
0px 24px 32px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
background-color: #565a69;
|
||||
border-radius: 100%;
|
||||
border: none;
|
||||
color: ${({ theme }) => theme.bg1};
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.1), 0px 4px 8px rgba(0, 0, 0, 0.08), 0px 16px 24px rgba(0, 0, 0, 0.06),
|
||||
0px 24px 32px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
&::-ms-thumb {
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
background-color: #565a69;
|
||||
border-radius: 100%;
|
||||
color: ${({ theme }) => theme.bg1};
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.1), 0px 4px 8px rgba(0, 0, 0, 0.08), 0px 16px 24px rgba(0, 0, 0, 0.06),
|
||||
0px 24px 32px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-slider-runnable-track {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
${({ theme }) => theme.bg5},
|
||||
${({ theme }) => theme.bg5} ${({ value }) => value}%,
|
||||
${({ theme }) => theme.bg3} ${({ value }) => value}%,
|
||||
${({ theme }) => theme.bg3}
|
||||
);
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
&::-moz-range-track {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
${({ theme }) => theme.bg5},
|
||||
${({ theme }) => theme.bg5} ${({ value }) => value}%,
|
||||
${({ theme }) => theme.bg3} ${({ value }) => value}%,
|
||||
${({ theme }) => theme.bg3}
|
||||
);
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
&::-ms-track {
|
||||
width: 100%;
|
||||
border-color: transparent;
|
||||
color: transparent;
|
||||
|
||||
background: ${({ theme }) => theme.bg5};
|
||||
height: 2px;
|
||||
}
|
||||
&::-ms-fill-lower {
|
||||
background: ${({ theme }) => theme.bg5};
|
||||
}
|
||||
&::-ms-fill-upper {
|
||||
background: ${({ theme }) => theme.bg3};
|
||||
}
|
||||
`
|
||||
|
||||
interface InputSliderProps {
|
||||
value: number
|
||||
onChange: (val: number) => void
|
||||
override?: boolean
|
||||
onChange: (value: number) => void
|
||||
}
|
||||
|
||||
export default function InputSlider({ value, onChange, override }: InputSliderProps) {
|
||||
const [internalVal, setInternalVal] = useState<number>(value)
|
||||
const debouncedInternalValue = useDebounce(internalVal, 100)
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e, val) => {
|
||||
setInternalVal(val)
|
||||
if (val !== debouncedInternalValue) {
|
||||
onChange(val)
|
||||
}
|
||||
export default function InputSlider({ value, onChange }: InputSliderProps) {
|
||||
const changeCallback = useCallback(
|
||||
e => {
|
||||
onChange(e.target.value)
|
||||
},
|
||||
[setInternalVal, onChange, debouncedInternalValue]
|
||||
[onChange]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (override) {
|
||||
setInternalVal(value)
|
||||
}
|
||||
}, [override, value])
|
||||
|
||||
return (
|
||||
<StyledSlider
|
||||
value={typeof internalVal === 'number' ? internalVal : 0}
|
||||
onChange={handleChange}
|
||||
<StyledRangeInput
|
||||
type="range"
|
||||
value={value}
|
||||
style={{ width: '90%', marginLeft: 15, marginRight: 15, padding: '15px 0' }}
|
||||
onChange={changeCallback}
|
||||
aria-labelledby="input-slider"
|
||||
step={1}
|
||||
min={0}
|
||||
max={100}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import React, { useState, useEffect, useRef, useCallback, useContext } from 'react'
|
||||
import React, { useState, useRef, useContext } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
|
||||
import QuestionHelper from '../Question'
|
||||
import { Text } from 'rebass'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { TYPE } from '../../theme'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
|
||||
import { darken } from 'polished'
|
||||
import { useDebounce } from '../../hooks'
|
||||
|
||||
const WARNING_TYPE = Object.freeze({
|
||||
none: 'none',
|
||||
emptyInput: 'emptyInput',
|
||||
invalidEntryBound: 'invalidEntryBound',
|
||||
riskyEntryHigh: 'riskyEntryHigh',
|
||||
riskyEntryLow: 'riskyEntryLow'
|
||||
})
|
||||
enum SlippageError {
|
||||
InvalidInput = 'InvalidInput',
|
||||
RiskyLow = 'RiskyLow',
|
||||
RiskyHigh = 'RiskyHigh'
|
||||
}
|
||||
|
||||
enum DeadlineError {
|
||||
InvalidInput = 'InvalidInput'
|
||||
}
|
||||
|
||||
const FancyButton = styled.button`
|
||||
color: ${({ theme }) => theme.text1};
|
||||
@@ -46,7 +46,7 @@ const Option = styled(FancyButton)<{ active: boolean }>`
|
||||
color: ${({ active, theme }) => (active ? theme.white : theme.text1)};
|
||||
`
|
||||
|
||||
const Input = styled.input<{ active?: boolean }>`
|
||||
const Input = styled.input`
|
||||
background: ${({ theme }) => theme.bg1};
|
||||
flex-grow: 1;
|
||||
font-size: 12px;
|
||||
@@ -56,15 +56,8 @@ const Input = styled.input<{ active?: boolean }>`
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
color: ${({ active, theme, color }) => (color === 'red' ? theme.red1 : active ? 'initial' : theme.text1)};
|
||||
cursor: ${({ active }) => (active ? 'initial' : 'inherit')};
|
||||
text-align: ${({ active }) => (active ? 'right' : 'left')};
|
||||
`
|
||||
|
||||
const BottomError = styled(Text)<{ show?: boolean }>`
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
padding-top: ${({ show }) => (show ? '12px' : '')};
|
||||
color: ${({ theme, color }) => (color === 'red' ? theme.red1 : theme.text1)};
|
||||
text-align: right;
|
||||
`
|
||||
|
||||
const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean }>`
|
||||
@@ -89,12 +82,6 @@ const SlippageSelector = styled.div`
|
||||
padding: 0 20px;
|
||||
`
|
||||
|
||||
const Percent = styled.div`
|
||||
color: ${({ color, theme }) => (color === 'faded' ? theme.bg1 : color === 'red' ? theme.red1 : 'inherit')};
|
||||
font-size: 0, 8rem;
|
||||
flex-grow: 0;
|
||||
`
|
||||
|
||||
export interface SlippageTabsProps {
|
||||
rawSlippage: number
|
||||
setRawSlippage: (rawSlippage: number) => void
|
||||
@@ -102,247 +89,154 @@ export interface SlippageTabsProps {
|
||||
setDeadline: (deadline: number) => void
|
||||
}
|
||||
|
||||
export default function SlippageTabs({ setRawSlippage, rawSlippage, deadline, setDeadline }: SlippageTabsProps) {
|
||||
export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, setDeadline }: SlippageTabsProps) {
|
||||
const theme = useContext(ThemeContext)
|
||||
const [activeIndex, setActiveIndex] = useState(2)
|
||||
|
||||
const [warningType, setWarningType] = useState(WARNING_TYPE.none)
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>()
|
||||
|
||||
const [userInput, setUserInput] = useState('')
|
||||
const debouncedInput = useDebounce(userInput, 150)
|
||||
const [slippageInput, setSlippageInput] = useState('')
|
||||
const [deadlineInput, setDeadlineInput] = useState('')
|
||||
|
||||
const [initialSlippage] = useState(rawSlippage)
|
||||
const slippageInputIsValid =
|
||||
slippageInput === '' || (rawSlippage / 100).toFixed(2) === Number.parseFloat(slippageInput).toFixed(2)
|
||||
const deadlineInputIsValid = deadlineInput === '' || (deadline / 60).toString() === deadlineInput
|
||||
|
||||
const [deadlineInput, setDeadlineInput] = useState(deadline / 60)
|
||||
|
||||
const updateSlippage = useCallback(
|
||||
newSlippage => {
|
||||
// round to 2 decimals to prevent ethers error
|
||||
const numParsed = newSlippage * 100
|
||||
|
||||
// set both slippage values in parents
|
||||
setRawSlippage(numParsed)
|
||||
},
|
||||
[setRawSlippage]
|
||||
)
|
||||
|
||||
const checkBounds = useCallback(
|
||||
slippageValue => {
|
||||
setWarningType(WARNING_TYPE.none)
|
||||
|
||||
if (slippageValue === '' || slippageValue === '.') {
|
||||
return setWarningType(WARNING_TYPE.emptyInput)
|
||||
}
|
||||
|
||||
// check bounds and set errors
|
||||
if (Number(slippageValue) < 0 || Number(slippageValue) > 50) {
|
||||
return setWarningType(WARNING_TYPE.invalidEntryBound)
|
||||
}
|
||||
if (Number(slippageValue) >= 0 && Number(slippageValue) < 0.1) {
|
||||
setWarningType(WARNING_TYPE.riskyEntryLow)
|
||||
}
|
||||
if (Number(slippageValue) > 5) {
|
||||
setWarningType(WARNING_TYPE.riskyEntryHigh)
|
||||
}
|
||||
//update the actual slippage value in parent
|
||||
updateSlippage(Number(slippageValue))
|
||||
},
|
||||
[updateSlippage]
|
||||
)
|
||||
|
||||
function parseCustomDeadline(e) {
|
||||
const val = e.target.value
|
||||
const acceptableValues = [/^$/, /^\d+$/]
|
||||
if (acceptableValues.some(re => re.test(val))) {
|
||||
setDeadlineInput(val)
|
||||
setDeadline(val * 60)
|
||||
}
|
||||
}
|
||||
const setFromCustom = () => {
|
||||
setActiveIndex(4)
|
||||
inputRef.current.focus()
|
||||
// if there's a value, evaluate the bounds
|
||||
checkBounds(debouncedInput)
|
||||
let slippageError: SlippageError
|
||||
if (slippageInput !== '' && !slippageInputIsValid) {
|
||||
slippageError = SlippageError.InvalidInput
|
||||
} else if (slippageInputIsValid && rawSlippage < 50) {
|
||||
slippageError = SlippageError.RiskyLow
|
||||
} else if (slippageInputIsValid && rawSlippage > 500) {
|
||||
slippageError = SlippageError.RiskyHigh
|
||||
}
|
||||
|
||||
// used for slippage presets
|
||||
const setFromFixed = useCallback(
|
||||
(index, slippage) => {
|
||||
// update slippage in parent, reset errors and input state
|
||||
updateSlippage(slippage)
|
||||
setWarningType(WARNING_TYPE.none)
|
||||
setActiveIndex(index)
|
||||
},
|
||||
[updateSlippage]
|
||||
)
|
||||
let deadlineError: DeadlineError
|
||||
if (deadlineInput !== '' && !deadlineInputIsValid) {
|
||||
deadlineError = DeadlineError.InvalidInput
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
switch (initialSlippage) {
|
||||
case 10:
|
||||
setFromFixed(1, 0.1)
|
||||
break
|
||||
case 50:
|
||||
setFromFixed(2, 0.5)
|
||||
break
|
||||
case 100:
|
||||
setFromFixed(3, 1)
|
||||
break
|
||||
default:
|
||||
// restrict to 2 decimal places
|
||||
const acceptableValues = [/^$/, /^\d{1,2}$/, /^\d{0,2}\.\d{0,2}$/]
|
||||
// if its within accepted decimal limit, update the input state
|
||||
if (acceptableValues.some(val => val.test('' + initialSlippage / 100))) {
|
||||
setUserInput('' + initialSlippage / 100)
|
||||
setActiveIndex(4)
|
||||
}
|
||||
}
|
||||
}, [initialSlippage, setFromFixed])
|
||||
function parseCustomSlippage(event) {
|
||||
setSlippageInput(event.target.value)
|
||||
|
||||
// check that the theyve entered number and correct decimal
|
||||
const parseInput = e => {
|
||||
const input = e.target.value
|
||||
let valueAsIntFromRoundedFloat: number
|
||||
try {
|
||||
valueAsIntFromRoundedFloat = Number.parseInt((Number.parseFloat(event.target.value) * 100).toString())
|
||||
} catch {}
|
||||
|
||||
// restrict to 2 decimal places
|
||||
const acceptableValues = [/^$/, /^\d{1,2}$/, /^\d{0,2}\.\d{0,2}$/]
|
||||
// if its within accepted decimal limit, update the input state
|
||||
if (acceptableValues.some(a => a.test(input))) {
|
||||
setUserInput(input)
|
||||
if (
|
||||
typeof valueAsIntFromRoundedFloat === 'number' &&
|
||||
!Number.isNaN(valueAsIntFromRoundedFloat) &&
|
||||
valueAsIntFromRoundedFloat < 5000
|
||||
) {
|
||||
setRawSlippage(valueAsIntFromRoundedFloat)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (activeIndex === 4) {
|
||||
checkBounds(debouncedInput)
|
||||
function parseCustomDeadline(event) {
|
||||
setDeadlineInput(event.target.value)
|
||||
|
||||
let valueAsInt: number
|
||||
try {
|
||||
valueAsInt = Number.parseInt(event.target.value) * 60
|
||||
} catch {}
|
||||
|
||||
if (typeof valueAsInt === 'number' && !Number.isNaN(valueAsInt) && valueAsInt > 0) {
|
||||
setDeadline(valueAsInt)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<RowFixed padding={'0 20px'}>
|
||||
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
|
||||
Set slippage tolerance
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Your transaction will revert if the price changes unfavorably by more than this percentage." />
|
||||
</RowFixed>
|
||||
|
||||
<SlippageSelector>
|
||||
<RowBetween>
|
||||
<Option
|
||||
onClick={() => {
|
||||
setFromFixed(1, 0.1)
|
||||
setSlippageInput('')
|
||||
setRawSlippage(10)
|
||||
}}
|
||||
active={activeIndex === 1}
|
||||
active={rawSlippage === 10}
|
||||
>
|
||||
0.1%
|
||||
</Option>
|
||||
<Option
|
||||
onClick={() => {
|
||||
setFromFixed(2, 0.5)
|
||||
setSlippageInput('')
|
||||
setRawSlippage(50)
|
||||
}}
|
||||
active={activeIndex === 2}
|
||||
active={rawSlippage === 50}
|
||||
>
|
||||
0.5%
|
||||
</Option>
|
||||
<Option
|
||||
onClick={() => {
|
||||
setFromFixed(3, 1)
|
||||
setSlippageInput('')
|
||||
setRawSlippage(100)
|
||||
}}
|
||||
active={activeIndex === 3}
|
||||
active={rawSlippage === 100}
|
||||
>
|
||||
1%
|
||||
</Option>
|
||||
<OptionCustom
|
||||
active={activeIndex === 4}
|
||||
warning={
|
||||
warningType !== WARNING_TYPE.none &&
|
||||
warningType !== WARNING_TYPE.emptyInput &&
|
||||
warningType !== WARNING_TYPE.riskyEntryLow
|
||||
}
|
||||
onClick={() => {
|
||||
setFromCustom()
|
||||
}}
|
||||
>
|
||||
<OptionCustom active={![10, 50, 100].includes(rawSlippage)} warning={!slippageInputIsValid} tabIndex={-1}>
|
||||
<RowBetween>
|
||||
{!(warningType === WARNING_TYPE.none || warningType === WARNING_TYPE.emptyInput) && (
|
||||
<span
|
||||
role="img"
|
||||
aria-label="warning"
|
||||
style={{
|
||||
color:
|
||||
warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
|
||||
? 'red'
|
||||
: warningType === WARNING_TYPE.riskyEntryLow
|
||||
? '#F3841E'
|
||||
: ''
|
||||
}}
|
||||
>
|
||||
{!!slippageInput &&
|
||||
(slippageError === SlippageError.RiskyLow || slippageError === SlippageError.RiskyHigh) ? (
|
||||
<span role="img" aria-label="warning" style={{ color: '#F3841E' }}>
|
||||
⚠️
|
||||
</span>
|
||||
)}
|
||||
) : null}
|
||||
<Input
|
||||
tabIndex={-1}
|
||||
ref={inputRef}
|
||||
active={activeIndex === 4}
|
||||
placeholder={
|
||||
activeIndex === 4
|
||||
? !!userInput
|
||||
? ''
|
||||
: '0'
|
||||
: activeIndex !== 4 && userInput !== ''
|
||||
? userInput
|
||||
: 'Custom'
|
||||
}
|
||||
value={activeIndex === 4 ? userInput : ''}
|
||||
onChange={parseInput}
|
||||
color={
|
||||
warningType === WARNING_TYPE.emptyInput
|
||||
? ''
|
||||
: warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
|
||||
? 'red'
|
||||
: ''
|
||||
}
|
||||
placeholder={(rawSlippage / 100).toFixed(2)}
|
||||
value={slippageInput}
|
||||
onBlur={() => {
|
||||
parseCustomSlippage({ target: { value: (rawSlippage / 100).toFixed(2) } })
|
||||
}}
|
||||
onChange={parseCustomSlippage}
|
||||
color={!slippageInputIsValid ? 'red' : ''}
|
||||
/>
|
||||
<Percent
|
||||
color={
|
||||
activeIndex !== 4
|
||||
? 'faded'
|
||||
: warningType === WARNING_TYPE.riskyEntryHigh || warningType === WARNING_TYPE.invalidEntryBound
|
||||
? 'red'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
%
|
||||
</Percent>
|
||||
%
|
||||
</RowBetween>
|
||||
</OptionCustom>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<BottomError
|
||||
show={activeIndex === 4}
|
||||
color={
|
||||
warningType === WARNING_TYPE.emptyInput
|
||||
? '#565A69'
|
||||
: warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
|
||||
? 'red'
|
||||
: warningType === WARNING_TYPE.riskyEntryLow
|
||||
? '#F3841E'
|
||||
: ''
|
||||
}
|
||||
{!!slippageError && (
|
||||
<RowBetween
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
paddingTop: '7px',
|
||||
color: slippageError === SlippageError.InvalidInput ? 'red' : '#F3841E'
|
||||
}}
|
||||
>
|
||||
{warningType === WARNING_TYPE.emptyInput && 'Enter a slippage percentage'}
|
||||
{warningType === WARNING_TYPE.invalidEntryBound && 'Please select a value no greater than 50%'}
|
||||
{warningType === WARNING_TYPE.riskyEntryHigh && 'Your transaction may be frontrun'}
|
||||
{warningType === WARNING_TYPE.riskyEntryLow && 'Your transaction may fail'}
|
||||
</BottomError>
|
||||
</RowBetween>
|
||||
{slippageError === SlippageError.InvalidInput
|
||||
? 'Enter a valid slippage percentage'
|
||||
: slippageError === SlippageError.RiskyLow
|
||||
? 'Your transaction may fail'
|
||||
: 'Your transaction may be frontrun'}
|
||||
</RowBetween>
|
||||
)}
|
||||
</SlippageSelector>
|
||||
|
||||
<AutoColumn gap="sm">
|
||||
<RowFixed padding={'0 20px'}>
|
||||
<TYPE.black fontSize={14} color={theme.text2}>
|
||||
Deadline
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Deadline in minutes. If your transaction takes longer than this it will revert." />
|
||||
<QuestionHelper text="Your transaction will revert if it is pending for more than this long." />
|
||||
</RowFixed>
|
||||
<RowFixed padding={'0 20px'}>
|
||||
<OptionCustom style={{ width: '80px' }}>
|
||||
<OptionCustom style={{ width: '80px' }} tabIndex={-1}>
|
||||
<Input
|
||||
tabIndex={-1}
|
||||
placeholder={'' + deadlineInput}
|
||||
color={!!deadlineError ? 'red' : undefined}
|
||||
onBlur={() => {
|
||||
parseCustomDeadline({ target: { value: (deadline / 60).toString() } })
|
||||
}}
|
||||
placeholder={(deadline / 60).toString()}
|
||||
value={deadlineInput}
|
||||
onChange={parseCustomDeadline}
|
||||
/>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { isAddress } from '../../utils'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { WETH } from '@uniswap/sdk'
|
||||
|
||||
import { ReactComponent as EthereumLogo } from '../../assets/images/ethereum-logo.svg'
|
||||
import EthereumLogo from '../../assets/images/ethereum-logo.png'
|
||||
|
||||
const TOKEN_ICON_API = address =>
|
||||
`https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${address}/logo.png`
|
||||
@@ -28,7 +28,7 @@ const Emoji = styled.span<{ size?: string }>`
|
||||
margin-bottom: -4px;
|
||||
`
|
||||
|
||||
const StyledEthereumLogo = styled(EthereumLogo)<{ size: string }>`
|
||||
const StyledEthereumLogo = styled.img<{ size: string }>`
|
||||
width: ${({ size }) => size};
|
||||
height: ${({ size }) => size};
|
||||
box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075);
|
||||
@@ -55,7 +55,7 @@ export default function TokenLogo({
|
||||
let path = ''
|
||||
// hard code to show ETH instead of WETH in UI
|
||||
if (address === WETH[chainId].address) {
|
||||
return <StyledEthereumLogo size={size} {...rest} />
|
||||
return <StyledEthereumLogo src={EthereumLogo} size={size} {...rest} />
|
||||
} else if (!error && !BAD_IMAGES[address] && isAddress(address)) {
|
||||
path = TOKEN_ICON_API(address)
|
||||
} else {
|
||||
|
||||
@@ -8,10 +8,10 @@ import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens } from '../../hooks/Tokens'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { useTokenWarningDismissal } from '../../state/user/hooks'
|
||||
import { Link, TYPE } from '../../theme'
|
||||
import { ExternalLink, TYPE } from '../../theme'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import PropsOfExcluding from '../../utils/props-of-excluding'
|
||||
import QuestionHelper from '../Question'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
|
||||
const Wrapper = styled.div<{ error: boolean }>`
|
||||
@@ -111,9 +111,9 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro
|
||||
? `${token.name} (${token.symbol})`
|
||||
: token.name || token.symbol}
|
||||
</div>
|
||||
<Link style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'address')}>
|
||||
<ExternalLink style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'address')}>
|
||||
(View on Etherscan)
|
||||
</Link>
|
||||
</ExternalLink>
|
||||
</Row>
|
||||
<Row>
|
||||
<TYPE.italic>Verify this is the correct token before making any transactions.</TYPE.italic>
|
||||
|
||||
18
src/components/Tooltip/index.tsx
Normal file
18
src/components/Tooltip/index.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Popover, { PopoverProps } from '../Popover'
|
||||
|
||||
const TooltipContainer = styled.div`
|
||||
width: 228px;
|
||||
padding: 0.6rem 1rem;
|
||||
line-height: 150%;
|
||||
font-weight: 400;
|
||||
`
|
||||
|
||||
interface TooltipProps extends Omit<PopoverProps, 'content'> {
|
||||
text: string
|
||||
}
|
||||
|
||||
export default function Tooltip({ text, ...rest }: TooltipProps) {
|
||||
return <Popover content={<TooltipContainer>{text}</TooltipContainer>} {...rest} />
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import useInterval from '../../hooks/useInterval'
|
||||
import { useRemovePopup } from '../../state/application/hooks'
|
||||
import { TYPE } from '../../theme'
|
||||
|
||||
import { Link } from '../../theme/components'
|
||||
import { ExternalLink } from '../../theme/components'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { AutoRow } from '../Row'
|
||||
@@ -62,7 +62,7 @@ export default function TxnPopup({
|
||||
<TYPE.body fontWeight={500}>
|
||||
{summary ? summary : 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}
|
||||
</TYPE.body>
|
||||
<Link href={getEtherscanLink(chainId, hash, 'transaction')}>View on Etherscan</Link>
|
||||
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>View on Etherscan</ExternalLink>
|
||||
</AutoColumn>
|
||||
<Fader count={count} />
|
||||
</AutoRow>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { Link } from '../../theme'
|
||||
import { ExternalLink } from '../../theme'
|
||||
|
||||
const InfoCard = styled.button<{ active?: boolean }>`
|
||||
background-color: ${({ theme, active }) => (active ? theme.bg3 : theme.bg2)};
|
||||
@@ -134,7 +134,7 @@ export default function Option({
|
||||
</OptionCardClickable>
|
||||
)
|
||||
if (link) {
|
||||
return <Link href={link}>{content}</Link>
|
||||
return <ExternalLink href={link}>{content}</ExternalLink>
|
||||
}
|
||||
|
||||
return content
|
||||
|
||||
@@ -4,6 +4,7 @@ import styled from 'styled-components'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { UnsupportedChainIdError, useWeb3React } from '@web3-react/core'
|
||||
import { URI_AVAILABLE } from '@web3-react/walletconnect-connector'
|
||||
import usePrevious from '../../hooks/usePrevious'
|
||||
import { useWalletModalOpen, useWalletModalToggle } from '../../state/application/hooks'
|
||||
|
||||
import Modal from '../Modal'
|
||||
@@ -11,8 +12,7 @@ import AccountDetails from '../AccountDetails'
|
||||
import PendingView from './PendingView'
|
||||
import Option from './Option'
|
||||
import { SUPPORTED_WALLETS } from '../../constants'
|
||||
import { usePrevious } from '../../hooks'
|
||||
import { Link } from '../../theme'
|
||||
import { ExternalLink } from '../../theme'
|
||||
import MetamaskIcon from '../../assets/images/metamask.png'
|
||||
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
||||
import { injected, walletconnect, fortmatic, portis } from '../../connectors'
|
||||
@@ -358,9 +358,9 @@ export default function WalletModal({
|
||||
{walletView !== WALLET_VIEWS.PENDING && (
|
||||
<Blurb>
|
||||
<span>New to Ethereum? </span>{' '}
|
||||
<Link href="https://ethereum.org/use/#3-what-is-a-wallet-and-which-one-should-i-use">
|
||||
<ExternalLink href="https://ethereum.org/use/#3-what-is-a-wallet-and-which-one-should-i-use">
|
||||
Learn more about wallets
|
||||
</Link>
|
||||
</ExternalLink>
|
||||
</Blurb>
|
||||
)}
|
||||
</ContentWrapper>
|
||||
|
||||
@@ -45,20 +45,6 @@ export default function Web3ReactManager({ children }) {
|
||||
}
|
||||
}, [triedEager, networkActive, networkError, activateNetwork, active])
|
||||
|
||||
// 'pause' the network connector if we're ever connected to an account and it's active
|
||||
useEffect(() => {
|
||||
if (active && networkActive) {
|
||||
network.pause()
|
||||
}
|
||||
}, [active, networkActive])
|
||||
|
||||
// 'resume' the network connector if we're ever not connected to an account and it's active
|
||||
useEffect(() => {
|
||||
if (!active && networkActive) {
|
||||
network.resume()
|
||||
}
|
||||
}, [active, networkActive])
|
||||
|
||||
// when there's no account connected, react to logins (broadly speaking) on the injected provider, if it exists
|
||||
useInactiveListener(!triedEager)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useWeb3React, UnsupportedChainIdError } from '@web3-react/core'
|
||||
import { darken, lighten } from 'polished'
|
||||
import { Activity } from 'react-feather'
|
||||
import useENSName from '../../hooks/useENSName'
|
||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||
import { TransactionDetails } from '../../state/transactions/reducer'
|
||||
|
||||
@@ -19,7 +20,6 @@ import { Spinner } from '../../theme'
|
||||
import LightCircle from '../../assets/svg/lightcircle.svg'
|
||||
|
||||
import { RowBetween } from '../Row'
|
||||
import { useENSName } from '../../hooks'
|
||||
import { shortenAddress } from '../../utils'
|
||||
import { useAllTransactions } from '../../state/transactions/hooks'
|
||||
import { NetworkContextName } from '../../constants'
|
||||
|
||||
@@ -8,47 +8,28 @@ import { CursorPointer, TYPE } from '../../theme'
|
||||
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown } from '../../utils/prices'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { SectionBreak } from './styleds'
|
||||
import QuestionHelper from '../Question'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import SlippageTabs, { SlippageTabsProps } from '../SlippageTabs'
|
||||
import FormattedPriceImpact from './FormattedPriceImpact'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import flatMap from 'lodash.flatmap'
|
||||
|
||||
export interface AdvancedSwapDetailsProps extends SlippageTabsProps {
|
||||
trade: Trade
|
||||
onDismiss: () => void
|
||||
}
|
||||
|
||||
export function AdvancedSwapDetails({ trade, onDismiss, ...slippageTabProps }: AdvancedSwapDetailsProps) {
|
||||
const { priceImpactWithoutFee, realizedLPFee } = computeTradePriceBreakdown(trade)
|
||||
function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippage: number }) {
|
||||
const theme = useContext(ThemeContext)
|
||||
const { priceImpactWithoutFee, realizedLPFee } = computeTradePriceBreakdown(trade)
|
||||
const isExactIn = trade.tradeType === TradeType.EXACT_INPUT
|
||||
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(trade, slippageTabProps.rawSlippage)
|
||||
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(trade, allowedSlippage)
|
||||
|
||||
return (
|
||||
<AutoColumn gap="md">
|
||||
<CursorPointer>
|
||||
<RowBetween onClick={onDismiss} padding={'8px 20px'}>
|
||||
<Text fontSize={16} color={theme.text2} fontWeight={500} style={{ userSelect: 'none' }}>
|
||||
Hide Advanced
|
||||
</Text>
|
||||
<ChevronUp color={theme.text2} />
|
||||
</RowBetween>
|
||||
</CursorPointer>
|
||||
<SectionBreak />
|
||||
<>
|
||||
<AutoColumn style={{ padding: '0 20px' }}>
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
|
||||
{isExactIn ? 'Minimum received' : 'Maximum sold'}
|
||||
</TYPE.black>
|
||||
<QuestionHelper
|
||||
text={
|
||||
isExactIn
|
||||
? 'Price can change between when a transaction is submitted and when it is executed. This is the minimum amount you will receive. A worse rate will cause your transaction to revert.'
|
||||
: 'Price can change between when a transaction is submitted and when it is executed. This is the maximum amount you will pay. A worse rate will cause your transaction to revert.'
|
||||
}
|
||||
/>
|
||||
<QuestionHelper text="Your transaction will revert if there is a large, unfavorable price movement before it is confirmed." />
|
||||
</RowFixed>
|
||||
<RowFixed>
|
||||
<TYPE.black color={theme.text1} fontSize={14}>
|
||||
@@ -82,16 +63,36 @@ export function AdvancedSwapDetails({ trade, onDismiss, ...slippageTabProps }: A
|
||||
</AutoColumn>
|
||||
|
||||
<SectionBreak />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export interface AdvancedSwapDetailsProps extends SlippageTabsProps {
|
||||
trade?: Trade
|
||||
onDismiss: () => void
|
||||
}
|
||||
|
||||
export function AdvancedSwapDetails({ trade, onDismiss, ...slippageTabProps }: AdvancedSwapDetailsProps) {
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
return (
|
||||
<AutoColumn gap="md">
|
||||
<CursorPointer>
|
||||
<RowBetween onClick={onDismiss} padding={'8px 20px'}>
|
||||
<Text fontSize={16} color={theme.text2} fontWeight={500} style={{ userSelect: 'none' }}>
|
||||
Hide Advanced
|
||||
</Text>
|
||||
<ChevronUp color={theme.text2} />
|
||||
</RowBetween>
|
||||
</CursorPointer>
|
||||
|
||||
<SectionBreak />
|
||||
|
||||
{trade && <TradeSummary trade={trade} allowedSlippage={slippageTabProps.rawSlippage} />}
|
||||
|
||||
<RowFixed padding={'0 20px'}>
|
||||
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
|
||||
Set slippage tolerance
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Your transaction will revert if the execution price changes by more than this amount after you submit your trade." />
|
||||
</RowFixed>
|
||||
<SlippageTabs {...slippageTabProps} />
|
||||
|
||||
{trade.route.path.length > 2 && (
|
||||
{trade?.route?.path?.length > 2 && (
|
||||
<AutoColumn style={{ padding: '0 20px' }}>
|
||||
<RowFixed>
|
||||
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
|
||||
@@ -109,27 +110,28 @@ export function AdvancedSwapDetails({ trade, onDismiss, ...slippageTabProps }: A
|
||||
justifyContent="space-evenly"
|
||||
alignItems="center"
|
||||
>
|
||||
{trade.route.path
|
||||
{flatMap(
|
||||
trade.route.path,
|
||||
// add a null in-between each item
|
||||
.flatMap((token, i, array) => {
|
||||
(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>
|
||||
)
|
||||
}
|
||||
})}
|
||||
}
|
||||
).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>
|
||||
)}
|
||||
|
||||
@@ -1,30 +1,23 @@
|
||||
import { Percent } from '@uniswap/sdk'
|
||||
import React, { useContext } from 'react'
|
||||
import { ChevronDown } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { CursorPointer } from '../../theme'
|
||||
import { warningServerity } from '../../utils/prices'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { RowBetween } from '../Row'
|
||||
import { AdvancedSwapDetails, AdvancedSwapDetailsProps } from './AdvancedSwapDetails'
|
||||
import { PriceSlippageWarningCard } from './PriceSlippageWarningCard'
|
||||
import { AdvancedDropwdown, FixedBottom } from './styleds'
|
||||
import { AdvancedDropdown } from './styleds'
|
||||
|
||||
export default function AdvancedSwapDetailsDropdown({
|
||||
priceImpactWithoutFee,
|
||||
showAdvanced,
|
||||
setShowAdvanced,
|
||||
...rest
|
||||
}: Omit<AdvancedSwapDetailsProps, 'onDismiss'> & {
|
||||
showAdvanced: boolean
|
||||
setShowAdvanced: (showAdvanced: boolean) => void
|
||||
priceImpactWithoutFee: Percent
|
||||
}) {
|
||||
const theme = useContext(ThemeContext)
|
||||
const severity = warningServerity(priceImpactWithoutFee)
|
||||
return (
|
||||
<AdvancedDropwdown>
|
||||
<AdvancedDropdown>
|
||||
{showAdvanced ? (
|
||||
<AdvancedSwapDetails {...rest} onDismiss={() => setShowAdvanced(false)} />
|
||||
) : (
|
||||
@@ -37,11 +30,6 @@ export default function AdvancedSwapDetailsDropdown({
|
||||
</RowBetween>
|
||||
</CursorPointer>
|
||||
)}
|
||||
<FixedBottom>
|
||||
<AutoColumn gap="lg">
|
||||
{severity > 2 && <PriceSlippageWarningCard priceSlippage={priceImpactWithoutFee} />}
|
||||
</AutoColumn>
|
||||
</FixedBottom>
|
||||
</AdvancedDropwdown>
|
||||
</AdvancedDropdown>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Percent } from '@uniswap/sdk'
|
||||
import React from 'react'
|
||||
import { ONE_BIPS } from '../../constants'
|
||||
import { warningServerity } from '../../utils/prices'
|
||||
import { warningSeverity } from '../../utils/prices'
|
||||
import { ErrorText } from './styleds'
|
||||
|
||||
export default function FormattedPriceImpact({ priceImpact }: { priceImpact?: Percent }) {
|
||||
return (
|
||||
<ErrorText fontWeight={500} fontSize={14} severity={warningServerity(priceImpact)}>
|
||||
<ErrorText fontWeight={500} fontSize={14} severity={warningSeverity(priceImpact)}>
|
||||
{priceImpact?.lessThan(ONE_BIPS) ? '<0.01%' : `${priceImpact?.toFixed(2)}%` ?? '-'}
|
||||
</ErrorText>
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ import { TYPE } from '../../theme'
|
||||
import { formatExecutionPrice } from '../../utils/prices'
|
||||
import { ButtonError } from '../Button'
|
||||
import { AutoColumn } from '../Column'
|
||||
import QuestionHelper from '../Question'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { AutoRow, RowBetween, RowFixed } from '../Row'
|
||||
import FormattedPriceImpact from './FormattedPriceImpact'
|
||||
import { StyledBalanceMaxMini } from './styleds'
|
||||
@@ -66,9 +66,9 @@ export default function SwapModalFooter({
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
|
||||
{trade?.tradeType === TradeType.EXACT_INPUT ? 'Min sent' : 'Maximum sold'}
|
||||
{trade?.tradeType === TradeType.EXACT_INPUT ? 'Minimum sent' : 'Maximum sold'}
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="A boundary is set so you are protected from large price movements after you submit your trade." />
|
||||
<QuestionHelper text="Your transaction will revert if there is a large, unfavorable price movement before it is confirmed." />
|
||||
</RowFixed>
|
||||
<RowFixed>
|
||||
<TYPE.black fontSize={14}>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { TokenAmount } from '@uniswap/sdk'
|
||||
import React from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { Link, TYPE } from '../../theme'
|
||||
import { ExternalLink, TYPE } from '../../theme'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import Copy from '../AccountDetails/Copy'
|
||||
import { AutoColumn } from '../Column'
|
||||
@@ -32,21 +32,21 @@ export function TransferModalHeader({
|
||||
<AutoColumn gap="lg">
|
||||
<TYPE.blue fontSize={36}>{ENSName}</TYPE.blue>
|
||||
<AutoRow gap="10px">
|
||||
<Link href={getEtherscanLink(chainId, ENSName, 'address')}>
|
||||
<ExternalLink href={getEtherscanLink(chainId, ENSName, 'address')}>
|
||||
<TYPE.blue fontSize={18}>
|
||||
{recipient?.slice(0, 8)}...{recipient?.slice(34, 42)}↗
|
||||
</TYPE.blue>
|
||||
</Link>
|
||||
</ExternalLink>
|
||||
<Copy toCopy={recipient} />
|
||||
</AutoRow>
|
||||
</AutoColumn>
|
||||
) : (
|
||||
<AutoRow gap="10px">
|
||||
<Link href={getEtherscanLink(chainId, recipient, 'address')}>
|
||||
<ExternalLink href={getEtherscanLink(chainId, recipient, 'address')}>
|
||||
<TYPE.blue fontSize={36}>
|
||||
{recipient?.slice(0, 6)}...{recipient?.slice(36, 42)}↗
|
||||
</TYPE.blue>
|
||||
</Link>
|
||||
</ExternalLink>
|
||||
<Copy toCopy={recipient} />
|
||||
</AutoRow>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useContext } from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
|
||||
import { Link } from '../../theme'
|
||||
import { ExternalLink } from '../../theme'
|
||||
import { YellowCard } from '../Card'
|
||||
import { AutoColumn } from '../Column'
|
||||
|
||||
@@ -12,10 +12,10 @@ export default function V1TradeLink({ v1TradeLinkIfBetter }: { v1TradeLinkIfBett
|
||||
<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
|
||||
<Link href={v1TradeLinkIfBetter}>
|
||||
<b> Uniswap V1 ↗</b>
|
||||
</Link>
|
||||
There is a better price for this trade on{' '}
|
||||
<ExternalLink href={v1TradeLinkIfBetter}>
|
||||
<b>Uniswap V1 ↗</b>
|
||||
</ExternalLink>
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
</YellowCard>
|
||||
|
||||
@@ -21,26 +21,16 @@ export const ArrowWrapper = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
export const FixedBottom = styled.div`
|
||||
position: absolute;
|
||||
margin-top: 1.5rem;
|
||||
export const AdvancedDropdown = styled.div`
|
||||
padding-top: calc(10px + 2rem);
|
||||
padding-bottom: 10px;
|
||||
margin-top: -2rem;
|
||||
width: 100%;
|
||||
margin-bottom: 40px;
|
||||
`
|
||||
|
||||
export const AdvancedDropwdown = styled.div`
|
||||
position: absolute;
|
||||
margin-top: -12px;
|
||||
max-width: 455px;
|
||||
width: 100%;
|
||||
margin-bottom: 100px;
|
||||
padding: 10px 0;
|
||||
padding-top: 36px;
|
||||
max-width: 400px;
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
color: ${({ theme }) => theme.text2};
|
||||
background-color: ${({ theme }) => theme.advancedBG};
|
||||
color: ${({ theme }) => theme.text2};
|
||||
z-index: -1;
|
||||
`
|
||||
|
||||
@@ -57,7 +47,7 @@ export const BottomGrouping = styled.div`
|
||||
|
||||
export const ErrorText = styled(Text)<{ severity?: 0 | 1 | 2 | 3 }>`
|
||||
color: ${({ theme, severity }) =>
|
||||
severity === 3 ? theme.red1 : severity === 2 ? theme.yellow2 : severity === 1 ? theme.green1 : theme.text1};
|
||||
severity === 3 ? theme.red1 : severity === 2 ? theme.yellow2 : severity === 1 ? theme.text1 : theme.green1};
|
||||
`
|
||||
|
||||
export const InputGroup = styled(AutoColumn)`
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { ChainId } from '@uniswap/sdk'
|
||||
import { FortmaticConnector as FortmaticConnectorCore } from '@web3-react/fortmatic-connector'
|
||||
|
||||
export const OVERLAY_READY = 'OVERLAY_READY'
|
||||
|
||||
const chainIdToNetwork = {
|
||||
1: 'mainnet',
|
||||
3: 'ropsten',
|
||||
4: 'rinkeby',
|
||||
42: 'kovan'
|
||||
type FormaticSupportedChains = Extract<ChainId, ChainId.MAINNET | ChainId.ROPSTEN | ChainId.RINKEBY | ChainId.KOVAN>
|
||||
|
||||
const CHAIN_ID_NETWORK_ARGUMENT: { readonly [chainId in FormaticSupportedChains]: string | undefined } = {
|
||||
[ChainId.MAINNET]: undefined,
|
||||
[ChainId.ROPSTEN]: 'ropsten',
|
||||
[ChainId.RINKEBY]: 'rinkeby',
|
||||
[ChainId.KOVAN]: 'kovan'
|
||||
}
|
||||
|
||||
export class FortmaticConnector extends FortmaticConnectorCore {
|
||||
@@ -14,7 +17,11 @@ export class FortmaticConnector extends FortmaticConnectorCore {
|
||||
if (!this.fortmatic) {
|
||||
const { default: Fortmatic } = await import('fortmatic')
|
||||
const { apiKey, chainId } = this as any
|
||||
this.fortmatic = new Fortmatic(apiKey, chainId === 1 || chainId === 4 ? undefined : chainIdToNetwork[chainId])
|
||||
if (chainId in CHAIN_ID_NETWORK_ARGUMENT) {
|
||||
this.fortmatic = new Fortmatic(apiKey, CHAIN_ID_NETWORK_ARGUMENT[chainId as FormaticSupportedChains])
|
||||
} else {
|
||||
throw new Error(`Unsupported network ID: ${chainId}`)
|
||||
}
|
||||
}
|
||||
|
||||
const provider = this.fortmatic.getProvider()
|
||||
@@ -29,7 +36,10 @@ export class FortmaticConnector extends FortmaticConnectorCore {
|
||||
}, 200)
|
||||
})
|
||||
|
||||
const [account] = await Promise.all([provider.enable().then(accounts => accounts[0]), pollForOverlayReady])
|
||||
const [account] = await Promise.all([
|
||||
provider.enable().then((accounts: string[]) => accounts[0]),
|
||||
pollForOverlayReady
|
||||
])
|
||||
|
||||
return { provider: this.fortmatic.getProvider(), chainId: (this as any).chainId, account }
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { NetworkConnector as NetworkConnectorCore } from '@web3-react/network-connector'
|
||||
|
||||
export class NetworkConnector extends NetworkConnectorCore {
|
||||
pause() {
|
||||
if ((this as any).active) {
|
||||
;(this as any).providers[(this as any).currentChainId].stop()
|
||||
}
|
||||
}
|
||||
|
||||
resume() {
|
||||
if ((this as any).active) {
|
||||
;(this as any).providers[(this as any).currentChainId].start()
|
||||
}
|
||||
}
|
||||
}
|
||||
105
src/connectors/NetworkConnector.ts
Normal file
105
src/connectors/NetworkConnector.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { ConnectorUpdate } from '@web3-react/types'
|
||||
import { AbstractConnector } from '@web3-react/abstract-connector'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
interface NetworkConnectorArguments {
|
||||
urls: { [chainId: number]: string }
|
||||
defaultChainId?: number
|
||||
}
|
||||
|
||||
// taken from ethers.js, compatible interface with web3 provider
|
||||
type AsyncSendable = {
|
||||
isMetaMask?: boolean
|
||||
host?: string
|
||||
path?: string
|
||||
sendAsync?: (request: any, callback: (error: any, response: any) => void) => void
|
||||
send?: (request: any, callback: (error: any, response: any) => void) => void
|
||||
}
|
||||
|
||||
class RequestError extends Error {
|
||||
constructor(message: string, public code: number, public data?: unknown) {
|
||||
super(message)
|
||||
}
|
||||
}
|
||||
|
||||
class MiniRpcProvider implements AsyncSendable {
|
||||
public readonly isMetaMask: false = false
|
||||
public readonly chainId: number
|
||||
public readonly url: string
|
||||
public readonly host: string
|
||||
public readonly path: string
|
||||
|
||||
constructor(chainId: number, url: string) {
|
||||
this.chainId = chainId
|
||||
this.url = url
|
||||
const parsed = new URL(url)
|
||||
this.host = parsed.host
|
||||
this.path = parsed.pathname
|
||||
}
|
||||
|
||||
public readonly sendAsync = (
|
||||
request: { jsonrpc: '2.0'; id: number | string | null; method: string; params?: unknown[] | object },
|
||||
callback: (error: any, response: any) => void
|
||||
): void => {
|
||||
this.request(request.method, request.params)
|
||||
.then(result => callback(null, { jsonrpc: '2.0', id: request.id, result }))
|
||||
.catch(error => callback(error, null))
|
||||
}
|
||||
|
||||
public readonly request = async (method: string, params?: unknown[] | object): Promise<unknown> => {
|
||||
const response = await fetch(this.url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method,
|
||||
params
|
||||
})
|
||||
})
|
||||
if (!response.ok) throw new RequestError(`${response.status}: ${response.statusText}`, -32000)
|
||||
const body = await response.json()
|
||||
if ('error' in body) {
|
||||
throw new RequestError(body?.error?.message, body?.error?.code, body?.error?.data)
|
||||
} else if ('result' in body) {
|
||||
return body.result
|
||||
} else {
|
||||
throw new RequestError(`Received unexpected JSON-RPC response to ${method} request.`, -32000, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NetworkConnector extends AbstractConnector {
|
||||
private readonly providers: { [chainId: number]: MiniRpcProvider }
|
||||
private currentChainId: number
|
||||
|
||||
constructor({ urls, defaultChainId }: NetworkConnectorArguments) {
|
||||
invariant(defaultChainId || Object.keys(urls).length === 1, 'defaultChainId is a required argument with >1 url')
|
||||
super({ supportedChainIds: Object.keys(urls).map((k): number => Number(k)) })
|
||||
|
||||
this.currentChainId = defaultChainId || Number(Object.keys(urls)[0])
|
||||
this.providers = Object.keys(urls).reduce<{ [chainId: number]: MiniRpcProvider }>((accumulator, chainId) => {
|
||||
accumulator[Number(chainId)] = new MiniRpcProvider(Number(chainId), urls[Number(chainId)])
|
||||
return accumulator
|
||||
}, {})
|
||||
}
|
||||
|
||||
public async activate(): Promise<ConnectorUpdate> {
|
||||
return { provider: this.providers[this.currentChainId], chainId: this.currentChainId, account: null }
|
||||
}
|
||||
|
||||
public async getProvider(): Promise<MiniRpcProvider> {
|
||||
return this.providers[this.currentChainId]
|
||||
}
|
||||
|
||||
public async getChainId(): Promise<number> {
|
||||
return this.currentChainId
|
||||
}
|
||||
|
||||
public async getAccount(): Promise<null> {
|
||||
return null
|
||||
}
|
||||
|
||||
public deactivate() {
|
||||
return
|
||||
}
|
||||
}
|
||||
1
src/connectors/fortmatic.d.ts
vendored
Normal file
1
src/connectors/fortmatic.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module 'formatic'
|
||||
@@ -3,15 +3,20 @@ import { WalletConnectConnector } from '@web3-react/walletconnect-connector'
|
||||
import { WalletLinkConnector } from '@web3-react/walletlink-connector'
|
||||
import { PortisConnector } from '@web3-react/portis-connector'
|
||||
|
||||
import { NetworkConnector } from './Network'
|
||||
import { FortmaticConnector } from './Fortmatic'
|
||||
import { NetworkConnector } from './NetworkConnector'
|
||||
|
||||
const POLLING_INTERVAL = 10000
|
||||
const NETWORK_URL = process.env.REACT_APP_NETWORK_URL
|
||||
const FORMATIC_KEY = process.env.REACT_APP_FORTMATIC_KEY
|
||||
const PORTIS_ID = process.env.REACT_APP_PORTIS_ID
|
||||
|
||||
if (typeof NETWORK_URL === 'undefined') {
|
||||
throw new Error(`REACT_APP_NETWORK_URL must be a defined environment variable`)
|
||||
}
|
||||
|
||||
export const network = new NetworkConnector({
|
||||
urls: { [Number(process.env.REACT_APP_CHAIN_ID)]: NETWORK_URL },
|
||||
pollingInterval: POLLING_INTERVAL * 3
|
||||
urls: { [Number(process.env.REACT_APP_CHAIN_ID)]: NETWORK_URL }
|
||||
})
|
||||
|
||||
export const injected = new InjectedConnector({
|
||||
@@ -28,13 +33,13 @@ export const walletconnect = new WalletConnectConnector({
|
||||
|
||||
// mainnet only
|
||||
export const fortmatic = new FortmaticConnector({
|
||||
apiKey: process.env.REACT_APP_FORTMATIC_KEY,
|
||||
apiKey: FORMATIC_KEY ?? '',
|
||||
chainId: 1
|
||||
})
|
||||
|
||||
// mainnet only
|
||||
export const portis = new PortisConnector({
|
||||
dAppId: process.env.REACT_APP_PORTIS_ID,
|
||||
dAppId: PORTIS_ID ?? '',
|
||||
networks: [1]
|
||||
})
|
||||
|
||||
|
||||
4
src/connectors/tsconfig.json
Normal file
4
src/connectors/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.strict.json",
|
||||
"include": ["**/*"]
|
||||
}
|
||||
6
src/constants/abis/erc20.ts
Normal file
6
src/constants/abis/erc20.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Interface } from '@ethersproject/abi'
|
||||
import ERC20_ABI from './erc20.json'
|
||||
|
||||
const ERC20_INTERFACE = new Interface(ERC20_ABI)
|
||||
|
||||
export default ERC20_INTERFACE
|
||||
@@ -1,26 +1,63 @@
|
||||
import { ChainId, Token, WETH, JSBI, Percent } from '@uniswap/sdk'
|
||||
import { ChainId, JSBI, Percent, Token, WETH, Pair, TokenAmount } from '@uniswap/sdk'
|
||||
import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors'
|
||||
|
||||
export const V1_FACTORY_ADDRESS = '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'
|
||||
|
||||
export const ROUTER_ADDRESS = '0xf164fC0Ec4E93095b804a4795bBe1e041497b92a'
|
||||
|
||||
// used for display in the default list when adding liquidity
|
||||
export const COMMON_BASES = {
|
||||
// used to construct intermediary pairs for trading
|
||||
|
||||
export const BASES_TO_CHECK_TRADES_AGAINST: { readonly [chainId in ChainId]: Token[] } = {
|
||||
[ChainId.MAINNET]: [
|
||||
WETH[ChainId.MAINNET],
|
||||
new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'),
|
||||
new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C')
|
||||
],
|
||||
[ChainId.ROPSTEN]: [WETH[ChainId.ROPSTEN]],
|
||||
[ChainId.RINKEBY]: [
|
||||
WETH[ChainId.RINKEBY],
|
||||
new Token(ChainId.RINKEBY, '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735', 18, 'DAI', 'Dai Stablecoin')
|
||||
],
|
||||
[ChainId.RINKEBY]: [WETH[ChainId.RINKEBY]],
|
||||
[ChainId.GÖRLI]: [WETH[ChainId.GÖRLI]],
|
||||
[ChainId.KOVAN]: [WETH[ChainId.KOVAN]]
|
||||
}
|
||||
|
||||
// used for display in the default list when adding liquidity
|
||||
export const SUGGESTED_BASES = BASES_TO_CHECK_TRADES_AGAINST
|
||||
|
||||
// used to construct the list of all pairs we consider by default in the frontend
|
||||
export const BASES_TO_TRACK_LIQUIDITY_FOR = BASES_TO_CHECK_TRADES_AGAINST
|
||||
|
||||
export const 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 MAINNET_WALLETS = {
|
||||
INJECTED: {
|
||||
connector: injected,
|
||||
|
||||
143
src/constants/multicall/abi.json
Normal file
143
src/constants/multicall/abi.json
Normal file
@@ -0,0 +1,143 @@
|
||||
[
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "getCurrentBlockTimestamp",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "timestamp",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"components": [
|
||||
{
|
||||
"name": "target",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "callData",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "calls",
|
||||
"type": "tuple[]"
|
||||
}
|
||||
],
|
||||
"name": "aggregate",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "blockNumber",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "returnData",
|
||||
"type": "bytes[]"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "getLastBlockHash",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "blockHash",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "addr",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "getEthBalance",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "balance",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "getCurrentBlockDifficulty",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "difficulty",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "getCurrentBlockGasLimit",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "gaslimit",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "getCurrentBlockCoinbase",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "coinbase",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "blockNumber",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "getBlockHash",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "blockHash",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
||||
12
src/constants/multicall/index.ts
Normal file
12
src/constants/multicall/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { ChainId } from '@uniswap/sdk'
|
||||
import MULTICALL_ABI from './abi.json'
|
||||
|
||||
const MULTICALL_NETWORKS: { [chainId in ChainId]: string } = {
|
||||
[ChainId.MAINNET]: '0xeefBa1e63905eF1D7ACbA5a8513c70307C1cE441',
|
||||
[ChainId.ROPSTEN]: '0x53C43764255c17BD724F74c4eF150724AC50a3ed',
|
||||
[ChainId.KOVAN]: '0x2cc8688C5f75E365aaEEb4ea8D6a480405A48D2A',
|
||||
[ChainId.RINKEBY]: '0x42Ad527de7d4e9d9d011aC45B31D8551f8Fe9821',
|
||||
[ChainId.GÖRLI]: '0x77dCa2C955b15e9dE4dbBCf1246B4B85b651e50e'
|
||||
}
|
||||
|
||||
export { MULTICALL_ABI, MULTICALL_NETWORKS }
|
||||
@@ -7,6 +7,7 @@ export default [
|
||||
new Token(ChainId.MAINNET, '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 9, 'AMPL', 'Ampleforth'),
|
||||
new Token(ChainId.MAINNET, '0xcD62b1C403fa761BAadFC74C525ce2B51780b184', 18, 'ANJ', 'Aragon Network Juror'),
|
||||
new Token(ChainId.MAINNET, '0x960b236A07cf122663c4303350609A66A7B288C0', 18, 'ANT', 'Aragon Network Token'),
|
||||
new Token(ChainId.MAINNET, '0x27054b13b1B798B345b591a4d22e6562d47eA75a', 4, 'AST', 'AirSwap Token'),
|
||||
new Token(ChainId.MAINNET, '0xBA11D00c5f74255f56a5E366F4F77f5A186d7f55', 18, 'BAND', 'BandToken'),
|
||||
new Token(ChainId.MAINNET, '0x0D8775F648430679A709E98d2b0Cb6250d2887EF', 18, 'BAT', 'Basic Attention Token'),
|
||||
new Token(ChainId.MAINNET, '0x107c4504cd79C5d2696Ea0030a8dD4e92601B82e', 18, 'BLT', 'Bloom Token'),
|
||||
@@ -15,6 +16,7 @@ export default [
|
||||
new Token(ChainId.MAINNET, '0x4F9254C83EB525f9FCf346490bbb3ed28a81C667', 18, 'CELR', 'CelerToken'),
|
||||
new Token(ChainId.MAINNET, '0xF5DCe57282A584D2746FaF1593d3121Fcac444dC', 8, 'cSAI', 'Compound Dai'),
|
||||
new Token(ChainId.MAINNET, '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643', 8, 'cDAI', 'Compound Dai'),
|
||||
new Token(ChainId.MAINNET, '0x39AA39c021dfbaE8faC545936693aC917d5E7563', 8, 'cUSDC', 'Compound USD Coin'),
|
||||
new Token(ChainId.MAINNET, '0xaaAEBE6Fe48E54f431b0C390CfaF0b017d09D42d', 4, 'CEL', 'Celsius'),
|
||||
new Token(ChainId.MAINNET, '0x06AF07097C9Eeb7fD685c692751D5C66dB49c215', 18, 'CHAI', 'Chai'),
|
||||
new Token(ChainId.MAINNET, '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359', 18, 'SAI', 'Dai Stablecoin v1.0 (SAI)'),
|
||||
@@ -30,6 +32,7 @@ export default [
|
||||
'Decentralized Insurance Protocol'
|
||||
),
|
||||
new Token(ChainId.MAINNET, '0xC0F9bD5Fa5698B6505F643900FFA515Ea5dF54A9', 18, 'DONUT', 'Donut'),
|
||||
new Token(ChainId.MAINNET, '0x86FADb80d8D2cff3C3680819E4da99C10232Ba0F', 18, 'EBASE', 'EURBASE Stablecoin'),
|
||||
new Token(ChainId.MAINNET, '0xF629cBd94d3791C9250152BD8dfBDF380E2a3B9c', 18, 'ENJ', 'Enjin Coin'),
|
||||
new Token(ChainId.MAINNET, '0x06f65b8CfCb13a9FE37d836fE9708dA38Ecb29B2', 18, 'FAME', 'SAINT FAME: Genesis Shirt'),
|
||||
new Token(ChainId.MAINNET, '0x4946Fcea7C692606e8908002e55A582af44AC121', 18, 'FOAM', 'FOAM Token'),
|
||||
@@ -90,6 +93,7 @@ export default [
|
||||
new Token(ChainId.MAINNET, '0x42d6622deCe394b54999Fbd73D108123806f6a18', 18, 'SPANK', 'SPANK'),
|
||||
new Token(ChainId.MAINNET, '0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC', 8, 'STORJ', 'StorjToken'),
|
||||
new Token(ChainId.MAINNET, '0x57Ab1ec28D129707052df4dF418D58a2D46d5f51', 18, 'sUSD', 'Synth sUSD'),
|
||||
new Token(ChainId.MAINNET, '0x261EfCdD24CeA98652B9700800a13DfBca4103fF', 18, 'sXAU', 'Synth sXAU'),
|
||||
new Token(ChainId.MAINNET, '0x8CE9137d39326AD0cD6491fb5CC0CbA0e089b6A9', 18, 'SXP', 'Swipe'),
|
||||
new Token(ChainId.MAINNET, '0x00006100F7090010005F1bd7aE6122c3C2CF0090', 18, 'TAUD', 'TrueAUD'),
|
||||
new Token(ChainId.MAINNET, '0x00000100F2A2bd000715001920eB70D229700085', 18, 'TCAD', 'TrueCAD'),
|
||||
|
||||
9
src/constants/v1/index.ts
Normal file
9
src/constants/v1/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Interface } from '@ethersproject/abi'
|
||||
import V1_EXCHANGE_ABI from './v1_exchange.json'
|
||||
import V1_FACTORY_ABI from './v1_factory.json'
|
||||
|
||||
const V1_FACTORY_ADDRESS = '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'
|
||||
const V1_FACTORY_INTERFACE = new Interface(V1_FACTORY_ABI)
|
||||
const V1_EXCHANGE_INTERFACE = new Interface(V1_EXCHANGE_ABI)
|
||||
|
||||
export { V1_FACTORY_ADDRESS, V1_FACTORY_INTERFACE, V1_FACTORY_ABI, V1_EXCHANGE_INTERFACE, V1_EXCHANGE_ABI }
|
||||
971
src/constants/v1/v1_exchange.json
Normal file
971
src/constants/v1/v1_exchange.json
Normal file
@@ -0,0 +1,971 @@
|
||||
[
|
||||
{
|
||||
"name": "TokenPurchase",
|
||||
"inputs": [
|
||||
{
|
||||
"type": "address",
|
||||
"name": "buyer",
|
||||
"indexed": true
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "eth_sold",
|
||||
"indexed": true
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_bought",
|
||||
"indexed": true
|
||||
}
|
||||
],
|
||||
"anonymous": false,
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"name": "EthPurchase",
|
||||
"inputs": [
|
||||
{
|
||||
"type": "address",
|
||||
"name": "buyer",
|
||||
"indexed": true
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_sold",
|
||||
"indexed": true
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "eth_bought",
|
||||
"indexed": true
|
||||
}
|
||||
],
|
||||
"anonymous": false,
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"name": "AddLiquidity",
|
||||
"inputs": [
|
||||
{
|
||||
"type": "address",
|
||||
"name": "provider",
|
||||
"indexed": true
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "eth_amount",
|
||||
"indexed": true
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "token_amount",
|
||||
"indexed": true
|
||||
}
|
||||
],
|
||||
"anonymous": false,
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"name": "RemoveLiquidity",
|
||||
"inputs": [
|
||||
{
|
||||
"type": "address",
|
||||
"name": "provider",
|
||||
"indexed": true
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "eth_amount",
|
||||
"indexed": true
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "token_amount",
|
||||
"indexed": true
|
||||
}
|
||||
],
|
||||
"anonymous": false,
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"name": "Transfer",
|
||||
"inputs": [
|
||||
{
|
||||
"type": "address",
|
||||
"name": "_from",
|
||||
"indexed": true
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "_to",
|
||||
"indexed": true
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "_value",
|
||||
"indexed": false
|
||||
}
|
||||
],
|
||||
"anonymous": false,
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"name": "Approval",
|
||||
"inputs": [
|
||||
{
|
||||
"type": "address",
|
||||
"name": "_owner",
|
||||
"indexed": true
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "_spender",
|
||||
"indexed": true
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "_value",
|
||||
"indexed": false
|
||||
}
|
||||
],
|
||||
"anonymous": false,
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"name": "setup",
|
||||
"outputs": [],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "address",
|
||||
"name": "token_addr"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "addLiquidity",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_liquidity"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "max_tokens"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": true,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "removeLiquidity",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "outA"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "outB"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "amount"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_eth"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_tokens"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "__default__",
|
||||
"outputs": [],
|
||||
"inputs": [],
|
||||
"constant": false,
|
||||
"payable": true,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "ethToTokenSwapInput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_tokens"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": true,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "ethToTokenTransferInput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_tokens"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "recipient"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": true,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "ethToTokenSwapOutput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": true,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "ethToTokenTransferOutput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "recipient"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": true,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "tokenToEthSwapInput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_sold"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_eth"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "tokenToEthTransferInput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_sold"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_eth"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "recipient"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "tokenToEthSwapOutput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "eth_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "max_tokens"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "tokenToEthTransferOutput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "eth_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "max_tokens"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "recipient"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "tokenToTokenSwapInput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_sold"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_tokens_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_eth_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "token_addr"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "tokenToTokenTransferInput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_sold"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_tokens_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_eth_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "recipient"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "token_addr"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "tokenToTokenSwapOutput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "max_tokens_sold"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "max_eth_sold"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "token_addr"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "tokenToTokenTransferOutput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "max_tokens_sold"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "max_eth_sold"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "recipient"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "token_addr"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "tokenToExchangeSwapInput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_sold"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_tokens_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_eth_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "exchange_addr"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "tokenToExchangeTransferInput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_sold"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_tokens_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_eth_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "recipient"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "exchange_addr"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "tokenToExchangeSwapOutput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "max_tokens_sold"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "max_eth_sold"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "exchange_addr"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "tokenToExchangeTransferOutput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "max_tokens_sold"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "max_eth_sold"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "recipient"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "exchange_addr"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "getEthToTokenInputPrice",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "eth_sold"
|
||||
}
|
||||
],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "getEthToTokenOutputPrice",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_bought"
|
||||
}
|
||||
],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "getTokenToEthInputPrice",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_sold"
|
||||
}
|
||||
],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "getTokenToEthOutputPrice",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "eth_bought"
|
||||
}
|
||||
],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "tokenAddress",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "address",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "factoryAddress",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "address",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "balanceOf",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "address",
|
||||
"name": "_owner"
|
||||
}
|
||||
],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "transfer",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "bool",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "address",
|
||||
"name": "_to"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "_value"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "transferFrom",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "bool",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "address",
|
||||
"name": "_from"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "_to"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "_value"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "approve",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "bool",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "address",
|
||||
"name": "_spender"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "_value"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "allowance",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "address",
|
||||
"name": "_owner"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "_spender"
|
||||
}
|
||||
],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "bytes32",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "symbol",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "bytes32",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "decimals",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "totalSupply",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
}
|
||||
]
|
||||
@@ -14,8 +14,7 @@
|
||||
"inputs": [{ "type": "address", "name": "template" }],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function",
|
||||
"gas": 35725
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "createExchange",
|
||||
@@ -23,8 +22,7 @@
|
||||
"inputs": [{ "type": "address", "name": "token" }],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function",
|
||||
"gas": 187911
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "getExchange",
|
||||
@@ -32,8 +30,7 @@
|
||||
"inputs": [{ "type": "address", "name": "token" }],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function",
|
||||
"gas": 715
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "getToken",
|
||||
@@ -41,8 +38,7 @@
|
||||
"inputs": [{ "type": "address", "name": "exchange" }],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function",
|
||||
"gas": 745
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "getTokenWithId",
|
||||
@@ -50,8 +46,7 @@
|
||||
"inputs": [{ "type": "uint256", "name": "token_id" }],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function",
|
||||
"gas": 736
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "exchangeTemplate",
|
||||
@@ -59,8 +54,7 @@
|
||||
"inputs": [],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function",
|
||||
"gas": 633
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "tokenCount",
|
||||
@@ -68,7 +62,6 @@
|
||||
"inputs": [],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function",
|
||||
"gas": 663
|
||||
"type": "function"
|
||||
}
|
||||
]
|
||||
@@ -1,26 +1,17 @@
|
||||
import { Contract } from '@ethersproject/contracts'
|
||||
import { Token, TokenAmount } from '@uniswap/sdk'
|
||||
import useSWR from 'swr'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { SWRKeys, useKeepSWRDataLiveAsBlocksArrive } from '.'
|
||||
import { useTokenContract } from '../hooks'
|
||||
import { useTokenContract } from '../hooks/useContract'
|
||||
import { useSingleCallResult } from '../state/multicall/hooks'
|
||||
|
||||
function getTokenAllowance(contract: Contract, token: Token): (owner: string, spender: string) => Promise<TokenAmount> {
|
||||
return async (owner: string, spender: string): Promise<TokenAmount> =>
|
||||
contract
|
||||
.allowance(owner, spender)
|
||||
.then((balance: { toString: () => string }) => new TokenAmount(token, balance.toString()))
|
||||
}
|
||||
|
||||
export function useTokenAllowance(token?: Token, owner?: string, spender?: string): TokenAmount {
|
||||
export function useTokenAllowance(token?: Token, owner?: string, spender?: string): TokenAmount | undefined {
|
||||
const contract = useTokenContract(token?.address, false)
|
||||
|
||||
const shouldFetch = !!contract && typeof owner === 'string' && typeof spender === 'string'
|
||||
const { data, mutate } = useSWR(
|
||||
shouldFetch ? [owner, spender, token.address, token.chainId, SWRKeys.Allowances] : null,
|
||||
getTokenAllowance(contract, token)
|
||||
)
|
||||
useKeepSWRDataLiveAsBlocksArrive(mutate)
|
||||
const inputs = useMemo(() => [owner, spender], [owner, spender])
|
||||
const allowance = useSingleCallResult(contract, 'allowance', inputs).result
|
||||
|
||||
return data
|
||||
return useMemo(() => (token && allowance ? new TokenAmount(token, allowance.toString()) : undefined), [
|
||||
token,
|
||||
allowance
|
||||
])
|
||||
}
|
||||
|
||||
@@ -1,24 +1,10 @@
|
||||
import { Contract } from '@ethersproject/contracts'
|
||||
import { Token, TokenAmount, Pair } from '@uniswap/sdk'
|
||||
import useSWR from 'swr'
|
||||
import { useMemo } from 'react'
|
||||
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
|
||||
import { Interface } from '@ethersproject/abi'
|
||||
|
||||
import { SWRKeys, useKeepSWRDataLiveAsBlocksArrive } from '.'
|
||||
import { usePairContract } from '../hooks'
|
||||
|
||||
function getReserves(contract: Contract, tokenA: Token, tokenB: Token): () => Promise<Pair | null> {
|
||||
return async (): Promise<Pair | null> =>
|
||||
contract
|
||||
.getReserves()
|
||||
.then(
|
||||
({ reserve0, reserve1 }: { reserve0: { toString: () => string }; reserve1: { toString: () => string } }) => {
|
||||
const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]
|
||||
return new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString()))
|
||||
}
|
||||
)
|
||||
.catch(() => {
|
||||
return null
|
||||
})
|
||||
}
|
||||
import { usePairContract } from '../hooks/useContract'
|
||||
import { useSingleCallResult, useMultipleContractSingleData } from '../state/multicall/hooks'
|
||||
|
||||
/*
|
||||
* if loading, return undefined
|
||||
@@ -26,13 +12,42 @@ function getReserves(contract: Contract, tokenA: Token, tokenB: Token): () => Pr
|
||||
* if pair already created (even if 0 reserves), return pair
|
||||
*/
|
||||
export function usePair(tokenA?: Token, tokenB?: Token): undefined | Pair | null {
|
||||
const pairAddress = !!tokenA && !!tokenB && !tokenA.equals(tokenB) ? Pair.getAddress(tokenA, tokenB) : undefined
|
||||
const pairAddress = tokenA && tokenB && !tokenA.equals(tokenB) ? Pair.getAddress(tokenA, tokenB) : undefined
|
||||
const contract = usePairContract(pairAddress, false)
|
||||
const { result: reserves, loading } = useSingleCallResult(contract, 'getReserves')
|
||||
|
||||
const shouldFetch = !!contract
|
||||
const key = shouldFetch ? [pairAddress, tokenA.chainId, SWRKeys.Reserves] : null
|
||||
const { data, mutate } = useSWR(key, getReserves(contract, tokenA, tokenB))
|
||||
useKeepSWRDataLiveAsBlocksArrive(mutate)
|
||||
|
||||
return data
|
||||
return useMemo(() => {
|
||||
if (loading || !tokenA || !tokenB) return undefined
|
||||
if (!reserves) return null
|
||||
const { reserve0, reserve1 } = reserves
|
||||
const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]
|
||||
return new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString()))
|
||||
}, [loading, reserves, tokenA, tokenB])
|
||||
}
|
||||
|
||||
const PAIR_INTERFACE = new Interface(IUniswapV2PairABI)
|
||||
export function usePairs(tokens: [Token | undefined, Token | undefined][]): (undefined | Pair | null)[] {
|
||||
const pairAddresses = useMemo(
|
||||
() =>
|
||||
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 results.map((result, i) => {
|
||||
const { result: reserves, loading } = result
|
||||
const tokenA = tokens[i][0]
|
||||
const tokenB = tokens[i][1]
|
||||
|
||||
if (loading || !tokenA || !tokenB) return undefined
|
||||
if (!reserves) return null
|
||||
const { reserve0, reserve1 } = reserves
|
||||
const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]
|
||||
return new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString()))
|
||||
})
|
||||
}, [results, tokens])
|
||||
}
|
||||
|
||||
@@ -1,26 +1,14 @@
|
||||
import { Contract } from '@ethersproject/contracts'
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { Token, TokenAmount } from '@uniswap/sdk'
|
||||
import useSWR from 'swr'
|
||||
import { useTokenContract } from '../hooks/useContract'
|
||||
import { useSingleCallResult } from '../state/multicall/hooks'
|
||||
|
||||
import { SWRKeys, useKeepSWRDataLiveAsBlocksArrive } from '.'
|
||||
import { useTokenContract } from '../hooks'
|
||||
|
||||
function getTotalSupply(contract: Contract, token: Token): () => Promise<TokenAmount> {
|
||||
return async (): Promise<TokenAmount> =>
|
||||
contract
|
||||
.totalSupply()
|
||||
.then((totalSupply: { toString: () => string }) => new TokenAmount(token, totalSupply.toString()))
|
||||
}
|
||||
|
||||
export function useTotalSupply(token?: Token): TokenAmount {
|
||||
// returns undefined if input token is undefined, or fails to get token contract,
|
||||
// or contract total supply cannot be fetched
|
||||
export function useTotalSupply(token?: Token): TokenAmount | undefined {
|
||||
const contract = useTokenContract(token?.address, false)
|
||||
|
||||
const shouldFetch = !!contract
|
||||
const { data, mutate } = useSWR(
|
||||
shouldFetch ? [token.address, token.chainId, SWRKeys.TotalSupply] : null,
|
||||
getTotalSupply(contract, token)
|
||||
)
|
||||
useKeepSWRDataLiveAsBlocksArrive(mutate)
|
||||
const totalSupply: BigNumber = useSingleCallResult(contract, 'totalSupply')?.result?.[0]
|
||||
|
||||
return data
|
||||
return token && totalSupply ? new TokenAmount(token, totalSupply.toString()) : undefined
|
||||
}
|
||||
|
||||
117
src/data/V1.ts
117
src/data/V1.ts
@@ -1,66 +1,103 @@
|
||||
import { Contract } from '@ethersproject/contracts'
|
||||
import { Token, TokenAmount, Pair, Trade, ChainId, WETH, Route, TradeType, Percent } from '@uniswap/sdk'
|
||||
import useSWR from 'swr'
|
||||
import { ChainId, JSBI, Pair, Percent, Route, Token, TokenAmount, Trade, TradeType, WETH } from '@uniswap/sdk'
|
||||
import { useMemo } from 'react'
|
||||
import { useActiveWeb3React } from '../hooks'
|
||||
import { useV1FactoryContract } from '../hooks'
|
||||
import { SWRKeys } from '.'
|
||||
import { useETHBalances, useTokenBalances } from '../state/wallet/hooks'
|
||||
|
||||
function getV1PairAddress(contract: Contract): (tokenAddress: string) => Promise<string> {
|
||||
return async (tokenAddress: string): Promise<string> => contract.getExchange(tokenAddress)
|
||||
}
|
||||
|
||||
function useV1PairAddress(tokenAddress: string) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
import { useAllTokens } from '../hooks/Tokens'
|
||||
import { useV1FactoryContract } from '../hooks/useContract'
|
||||
import { NEVER_RELOAD, useSingleCallResult, useSingleContractMultipleData } from '../state/multicall/hooks'
|
||||
import { useETHBalances, useTokenBalance, useTokenBalances } from '../state/wallet/hooks'
|
||||
|
||||
function useV1PairAddress(tokenAddress?: string): string | undefined {
|
||||
const contract = useV1FactoryContract()
|
||||
|
||||
const shouldFetch = chainId === ChainId.MAINNET && typeof tokenAddress === 'string' && !!contract
|
||||
const { data } = useSWR(shouldFetch ? [tokenAddress, SWRKeys.V1PairAddress] : null, getV1PairAddress(contract), {
|
||||
// don't need to update this data
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false
|
||||
})
|
||||
|
||||
return data
|
||||
const inputs = useMemo(() => [tokenAddress], [tokenAddress])
|
||||
return useSingleCallResult(contract, 'getExchange', inputs)?.result?.[0]
|
||||
}
|
||||
|
||||
function useMockV1Pair(token?: Token) {
|
||||
class MockV1Pair extends Pair {
|
||||
readonly isV1: true = true
|
||||
}
|
||||
|
||||
function useMockV1Pair(token?: Token): MockV1Pair | undefined {
|
||||
const isWETH = token?.equals(WETH[token?.chainId])
|
||||
|
||||
// will only return an address on mainnet, and not for WETH
|
||||
const v1PairAddress = useV1PairAddress(isWETH ? undefined : token?.address)
|
||||
const tokenBalance = useTokenBalances(v1PairAddress, [token])[token?.address]
|
||||
const ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress]
|
||||
const tokenBalance = useTokenBalance(v1PairAddress, token)
|
||||
const ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress ?? '']
|
||||
|
||||
return tokenBalance && ETHBalance
|
||||
? new Pair(tokenBalance, new TokenAmount(WETH[token?.chainId], ETHBalance.toString()))
|
||||
return tokenBalance && ETHBalance && token
|
||||
? new MockV1Pair(tokenBalance, new TokenAmount(WETH[token.chainId], ETHBalance.toString()))
|
||||
: undefined
|
||||
}
|
||||
|
||||
// returns ALL v1 exchange addresses
|
||||
export function useAllV1ExchangeAddresses(): string[] {
|
||||
const factory = useV1FactoryContract()
|
||||
const exchangeCount = useSingleCallResult(factory, 'tokenCount')?.result
|
||||
|
||||
const parsedCount = parseInt(exchangeCount?.toString() ?? '0')
|
||||
|
||||
const indices = useMemo(() => [...Array(parsedCount).keys()].map(ix => [ix]), [parsedCount])
|
||||
const data = useSingleContractMultipleData(factory, 'getTokenWithId', indices, NEVER_RELOAD)
|
||||
|
||||
return useMemo(() => data?.map(({ result }) => result?.[0])?.filter(x => x) ?? [], [data])
|
||||
}
|
||||
|
||||
// returns all v1 exchange addresses in the user's token list
|
||||
export function useAllTokenV1ExchangeAddresses(): string[] {
|
||||
const allTokens = useAllTokens()
|
||||
const factory = useV1FactoryContract()
|
||||
const args = useMemo(() => Object.keys(allTokens).map(tokenAddress => [tokenAddress]), [allTokens])
|
||||
|
||||
const data = useSingleContractMultipleData(factory, 'getExchange', args, NEVER_RELOAD)
|
||||
|
||||
return useMemo(() => data?.map(({ result }) => result?.[0])?.filter(x => x) ?? [], [data])
|
||||
}
|
||||
|
||||
// returns whether any of the tokens in the user's token list have liquidity on v1
|
||||
export function useUserProbablyHasV1Liquidity(): boolean | undefined {
|
||||
const exchangeAddresses = useAllTokenV1ExchangeAddresses()
|
||||
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
|
||||
const fakeTokens = useMemo(
|
||||
() => (chainId ? exchangeAddresses.map(address => new Token(chainId, address, 18, 'UNI-V1')) : []),
|
||||
[chainId, exchangeAddresses]
|
||||
)
|
||||
|
||||
const balances = useTokenBalances(account ?? undefined, fakeTokens)
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
Object.keys(balances).some(tokenAddress => {
|
||||
const b = balances[tokenAddress]?.raw
|
||||
return b && JSBI.greaterThan(b, JSBI.BigInt(0))
|
||||
}),
|
||||
[balances]
|
||||
)
|
||||
}
|
||||
|
||||
export function useV1TradeLinkIfBetter(
|
||||
isExactIn?: boolean,
|
||||
inputToken?: Token,
|
||||
outputToken?: Token,
|
||||
input?: Token,
|
||||
output?: Token,
|
||||
exactAmount?: TokenAmount,
|
||||
v2Trade?: Trade,
|
||||
minimumDelta: Percent = new Percent('0')
|
||||
): string {
|
||||
): string | undefined {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
const input = inputToken
|
||||
const output = outputToken
|
||||
const mainnet = chainId === ChainId.MAINNET
|
||||
const isMainnet: boolean = chainId === ChainId.MAINNET
|
||||
|
||||
// get the mock v1 pairs
|
||||
const inputPair = useMockV1Pair(input)
|
||||
const outputPair = useMockV1Pair(output)
|
||||
|
||||
const inputIsWETH = mainnet && input?.equals(WETH[ChainId.MAINNET])
|
||||
const outputIsWETH = mainnet && output?.equals(WETH[ChainId.MAINNET])
|
||||
const inputIsWETH = isMainnet && input?.equals(WETH[ChainId.MAINNET])
|
||||
const outputIsWETH = isMainnet && output?.equals(WETH[ChainId.MAINNET])
|
||||
|
||||
// construct a direct or through ETH v1 route
|
||||
let pairs: Pair[]
|
||||
let pairs: Pair[] = []
|
||||
if (inputIsWETH && outputPair) {
|
||||
pairs = [outputPair]
|
||||
} else if (outputIsWETH && inputPair) {
|
||||
@@ -71,8 +108,8 @@ export function useV1TradeLinkIfBetter(
|
||||
pairs = [inputPair, outputPair]
|
||||
}
|
||||
|
||||
const route = pairs && new Route(pairs, input)
|
||||
let v1Trade: Trade
|
||||
const route = input && pairs && pairs.length > 0 && new Route(pairs, input)
|
||||
let v1Trade: Trade | undefined
|
||||
try {
|
||||
v1Trade =
|
||||
route && exactAmount
|
||||
@@ -86,16 +123,16 @@ export function useV1TradeLinkIfBetter(
|
||||
// discount the v1 output amount by minimumDelta
|
||||
const discountedV1Output = v1Trade?.outputAmount.multiply(new Percent('1').subtract(minimumDelta))
|
||||
// check if the discounted v1 amount is still greater than v2, short-circuiting if no v2 trade exists
|
||||
v1HasBetterTrade = !!!v2Trade || discountedV1Output.greaterThan(v2Trade.outputAmount)
|
||||
v1HasBetterTrade = !v2Trade || discountedV1Output.greaterThan(v2Trade.outputAmount)
|
||||
} else {
|
||||
// inflate the v1 amount by minimumDelta
|
||||
const inflatedV1Input = v1Trade?.inputAmount.multiply(new Percent('1').add(minimumDelta))
|
||||
// check if the inflated v1 amount is still less than v2, short-circuiting if no v2 trade exists
|
||||
v1HasBetterTrade = !!!v2Trade || inflatedV1Input.lessThan(v2Trade.inputAmount)
|
||||
v1HasBetterTrade = !v2Trade || inflatedV1Input.lessThan(v2Trade.inputAmount)
|
||||
}
|
||||
}
|
||||
|
||||
return v1HasBetterTrade
|
||||
return v1HasBetterTrade && input && output
|
||||
? `https://v1.uniswap.exchange/swap?inputCurrency=${inputIsWETH ? 'ETH' : input.address}&outputCurrency=${
|
||||
outputIsWETH ? 'ETH' : output.address
|
||||
}`
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { responseInterface } from 'swr'
|
||||
|
||||
import { useBlockNumber } from '../state/application/hooks'
|
||||
|
||||
export enum SWRKeys {
|
||||
Allowances,
|
||||
Reserves,
|
||||
TotalSupply,
|
||||
V1PairAddress
|
||||
}
|
||||
|
||||
export function useKeepSWRDataLiveAsBlocksArrive(mutate: responseInterface<any, any>['mutate']) {
|
||||
// because we don't care about the referential identity of mutate, just bind it to a ref
|
||||
const mutateRef = useRef(mutate)
|
||||
useEffect(() => {
|
||||
mutateRef.current = mutate
|
||||
})
|
||||
// then, whenever a new block arrives, trigger a mutation
|
||||
const blockNumber = useBlockNumber()
|
||||
useEffect(() => {
|
||||
mutateRef.current()
|
||||
}, [blockNumber])
|
||||
}
|
||||
4
src/data/tsconfig.json
Normal file
4
src/data/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.strict.json",
|
||||
"include": ["**/*"]
|
||||
}
|
||||
@@ -1,47 +1,38 @@
|
||||
import { useMemo } from 'react'
|
||||
import { WETH, Token, TokenAmount, Trade, ChainId, Pair } from '@uniswap/sdk'
|
||||
import { useActiveWeb3React } from './index'
|
||||
import { usePair } from '../data/Reserves'
|
||||
import { Token, TokenAmount, Trade, ChainId, Pair } from '@uniswap/sdk'
|
||||
import flatMap from 'lodash.flatmap'
|
||||
|
||||
import { useActiveWeb3React } from './index'
|
||||
import { usePairs } from '../data/Reserves'
|
||||
import { BASES_TO_CHECK_TRADES_AGAINST } from '../constants'
|
||||
|
||||
const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin')
|
||||
const USDC = new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C')
|
||||
function useAllCommonPairs(tokenA?: Token, tokenB?: Token): Pair[] {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
// check for direct pair between tokens
|
||||
const pairBetween = usePair(tokenA, tokenB)
|
||||
const bases = useMemo(() => BASES_TO_CHECK_TRADES_AGAINST[chainId as ChainId] ?? [], [chainId])
|
||||
|
||||
// get token<->WETH pairs
|
||||
const aToETH = usePair(tokenA, WETH[chainId as ChainId])
|
||||
const bToETH = usePair(tokenB, WETH[chainId as ChainId])
|
||||
|
||||
// get token<->DAI pairs
|
||||
const aToDAI = usePair(tokenA, chainId === ChainId.MAINNET ? DAI : undefined)
|
||||
const bToDAI = usePair(tokenB, chainId === ChainId.MAINNET ? DAI : undefined)
|
||||
|
||||
// get token<->USDC pairs
|
||||
const aToUSDC = usePair(tokenA, chainId === ChainId.MAINNET ? USDC : undefined)
|
||||
const bToUSDC = usePair(tokenB, chainId === ChainId.MAINNET ? USDC : undefined)
|
||||
|
||||
// get connecting pairs
|
||||
const DAIToETH = usePair(chainId === ChainId.MAINNET ? DAI : undefined, WETH[chainId as ChainId])
|
||||
const USDCToETH = usePair(chainId === ChainId.MAINNET ? USDC : undefined, WETH[chainId as ChainId])
|
||||
const DAIToUSDC = usePair(
|
||||
chainId === ChainId.MAINNET ? DAI : undefined,
|
||||
chainId === ChainId.MAINNET ? USDC : undefined
|
||||
)
|
||||
const allPairs = usePairs([
|
||||
// the direct pair
|
||||
[tokenA, tokenB],
|
||||
// token A against all bases
|
||||
...bases.map((base): [Token | undefined, Token | undefined] => [tokenA, base]),
|
||||
// token B against all bases
|
||||
...bases.map((base): [Token | undefined, Token | undefined] => [tokenB, base]),
|
||||
// each base against all bases
|
||||
...flatMap(bases, (base): [Token, Token][] => bases.map(otherBase => [base, otherBase]))
|
||||
])
|
||||
|
||||
// only pass along valid pairs, non-duplicated pairs
|
||||
return useMemo(
|
||||
() =>
|
||||
[pairBetween, aToETH, bToETH, aToDAI, bToDAI, aToUSDC, bToUSDC, DAIToETH, USDCToETH, DAIToUSDC]
|
||||
allPairs
|
||||
// filter out invalid pairs
|
||||
.filter((p): p is Pair => !!p)
|
||||
// filter out duplicated pairs
|
||||
.filter(
|
||||
(p, i, pairs) => i === pairs.findIndex(pair => pair?.liquidityToken.address === p.liquidityToken.address)
|
||||
),
|
||||
[pairBetween, aToETH, bToETH, aToDAI, bToDAI, aToUSDC, bToUSDC, DAIToETH, USDCToETH, DAIToUSDC]
|
||||
[allPairs]
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
import { Contract } from '@ethersproject/contracts'
|
||||
import { Web3Provider } from '@ethersproject/providers'
|
||||
import { useState, useMemo, useCallback, useEffect, useRef } from 'react'
|
||||
import { useWeb3React as useWeb3ReactCore } from '@web3-react/core'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import copy from 'copy-to-clipboard'
|
||||
|
||||
import IUniswapV1Factory from '../constants/abis/v1_factory.json'
|
||||
import ERC20_ABI from '../constants/abis/erc20.json'
|
||||
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
|
||||
import { injected } from '../connectors'
|
||||
import { NetworkContextName, V1_FACTORY_ADDRESS } from '../constants'
|
||||
import { getContract, isAddress } from '../utils'
|
||||
import { NetworkContextName } from '../constants'
|
||||
|
||||
export function useActiveWeb3React() {
|
||||
const context = useWeb3ReactCore<Web3Provider>()
|
||||
@@ -64,7 +57,7 @@ export function useInactiveListener(suppress = false) {
|
||||
const handleChainChanged = () => {
|
||||
// eat errors
|
||||
activate(injected, undefined, true).catch(error => {
|
||||
console.log(error)
|
||||
console.error('Failed to activate after chain changed', error)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -72,7 +65,7 @@ export function useInactiveListener(suppress = false) {
|
||||
if (accounts.length > 0) {
|
||||
// eat errors
|
||||
activate(injected, undefined, true).catch(error => {
|
||||
console.log(error)
|
||||
console.error('Failed to activate after accounts changed', error)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -80,7 +73,7 @@ export function useInactiveListener(suppress = false) {
|
||||
const handleNetworkChanged = () => {
|
||||
// eat errors
|
||||
activate(injected, undefined, true).catch(error => {
|
||||
console.log(error)
|
||||
console.error('Failed to activate after networks changed', error)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -99,157 +92,3 @@ export function useInactiveListener(suppress = false) {
|
||||
return
|
||||
}, [active, error, suppress, activate])
|
||||
}
|
||||
|
||||
// modified from https://usehooks.com/useDebounce/
|
||||
export function useDebounce<T>(value: T, delay: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value)
|
||||
|
||||
useEffect(() => {
|
||||
// Update debounced value after delay
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value)
|
||||
}, delay)
|
||||
|
||||
// Cancel the timeout if value changes (also on delay change or unmount)
|
||||
// This is how we prevent debounced value from updating if value is changed ...
|
||||
// .. within the delay period. Timeout gets cleared and restarted.
|
||||
return () => {
|
||||
clearTimeout(handler)
|
||||
}
|
||||
}, [value, delay])
|
||||
|
||||
return debouncedValue
|
||||
}
|
||||
|
||||
// modified from https://usehooks.com/useKeyPress/
|
||||
export function useBodyKeyDown(targetKey: string, onKeyDown: () => void, suppressOnKeyDown = false) {
|
||||
const downHandler = useCallback(
|
||||
event => {
|
||||
const {
|
||||
target: { tagName },
|
||||
key
|
||||
} = event
|
||||
if (key === targetKey && tagName === 'BODY' && !suppressOnKeyDown) {
|
||||
event.preventDefault()
|
||||
onKeyDown()
|
||||
}
|
||||
},
|
||||
[targetKey, onKeyDown, suppressOnKeyDown]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', downHandler)
|
||||
return () => {
|
||||
window.removeEventListener('keydown', downHandler)
|
||||
}
|
||||
}, [downHandler])
|
||||
}
|
||||
|
||||
export function useENSName(address?: string): string | null {
|
||||
const { library } = useActiveWeb3React()
|
||||
|
||||
const [ENSName, setENSName] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!library || !address) return
|
||||
if (isAddress(address)) {
|
||||
let stale = false
|
||||
library
|
||||
.lookupAddress(address)
|
||||
.then(name => {
|
||||
if (!stale) {
|
||||
if (name) {
|
||||
setENSName(name)
|
||||
} else {
|
||||
setENSName(null)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!stale) {
|
||||
setENSName(null)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
stale = true
|
||||
setENSName(null)
|
||||
}
|
||||
}
|
||||
return
|
||||
}, [library, address])
|
||||
|
||||
return ENSName
|
||||
}
|
||||
|
||||
// returns null on errors
|
||||
function useContract(address?: string, ABI?: any, withSignerIfPossible = true): Contract | null {
|
||||
const { library, account } = useActiveWeb3React()
|
||||
|
||||
return useMemo(() => {
|
||||
if (!address || !ABI || !library) return null
|
||||
try {
|
||||
return getContract(address, ABI, library, withSignerIfPossible && account ? account : undefined)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}, [address, ABI, library, withSignerIfPossible, account])
|
||||
}
|
||||
|
||||
export function useV1FactoryContract(): Contract | null {
|
||||
return useContract(V1_FACTORY_ADDRESS, IUniswapV1Factory, false)
|
||||
}
|
||||
|
||||
// returns null on errors
|
||||
export function useTokenContract(tokenAddress?: string, withSignerIfPossible = true): Contract | null {
|
||||
return useContract(tokenAddress, ERC20_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
export function usePairContract(pairAddress?: string, withSignerIfPossible = true): Contract | null {
|
||||
return useContract(pairAddress, IUniswapV2PairABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
export function useCopyClipboard(timeout = 500): [boolean, (toCopy: string) => void] {
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
|
||||
const staticCopy = useCallback(text => {
|
||||
const didCopy = copy(text)
|
||||
setIsCopied(didCopy)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (isCopied) {
|
||||
const hide = setTimeout(() => {
|
||||
setIsCopied(false)
|
||||
}, timeout)
|
||||
|
||||
return () => {
|
||||
clearTimeout(hide)
|
||||
}
|
||||
}
|
||||
return
|
||||
}, [isCopied, setIsCopied, timeout])
|
||||
|
||||
return [isCopied, staticCopy]
|
||||
}
|
||||
|
||||
// modified from https://usehooks.com/usePrevious/
|
||||
export function usePrevious<T>(value: T) {
|
||||
// The ref object is a generic container whose current property is mutable ...
|
||||
// ... and can hold any value, similar to an instance property on a class
|
||||
const ref = useRef<T>()
|
||||
|
||||
// Store current value in ref
|
||||
useEffect(() => {
|
||||
ref.current = value
|
||||
}, [value]) // Only re-run if value changes
|
||||
|
||||
// Return previous value (happens before update in useEffect above)
|
||||
return ref.current
|
||||
}
|
||||
|
||||
export function useToggle(initialState = false): [boolean, () => void] {
|
||||
const [state, setState] = useState(initialState)
|
||||
const toggle = useCallback(() => setState(state => !state), [])
|
||||
return [state, toggle]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ import { Field } from '../state/swap/actions'
|
||||
import { useTransactionAdder, useHasPendingApproval } from '../state/transactions/hooks'
|
||||
import { computeSlippageAdjustedAmounts } from '../utils/prices'
|
||||
import { calculateGasMargin } from '../utils'
|
||||
import { useTokenContract, useActiveWeb3React } from './index'
|
||||
import { useTokenContract } from './useContract'
|
||||
import { useActiveWeb3React } from './index'
|
||||
|
||||
export enum ApprovalState {
|
||||
UNKNOWN,
|
||||
|
||||
25
src/hooks/useBodyKeyDown.ts
Normal file
25
src/hooks/useBodyKeyDown.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useCallback, useEffect } from 'react'
|
||||
|
||||
// modified from https://usehooks.com/useKeyPress/
|
||||
export default function useBodyKeyDown(targetKey: string, onKeyDown: () => void, suppressOnKeyDown = false) {
|
||||
const downHandler = useCallback(
|
||||
event => {
|
||||
const {
|
||||
target: { tagName },
|
||||
key
|
||||
} = event
|
||||
if (key === targetKey && tagName === 'BODY' && !suppressOnKeyDown) {
|
||||
event.preventDefault()
|
||||
onKeyDown()
|
||||
}
|
||||
},
|
||||
[targetKey, onKeyDown, suppressOnKeyDown]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', downHandler)
|
||||
return () => {
|
||||
window.removeEventListener('keydown', downHandler)
|
||||
}
|
||||
}, [downHandler])
|
||||
}
|
||||
46
src/hooks/useContract.ts
Normal file
46
src/hooks/useContract.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Contract } from '@ethersproject/contracts'
|
||||
import { ChainId } from '@uniswap/sdk'
|
||||
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
|
||||
import { useMemo } from 'react'
|
||||
import ERC20_ABI from '../constants/abis/erc20.json'
|
||||
import { V1_EXCHANGE_ABI, V1_FACTORY_ABI, V1_FACTORY_ADDRESS } from '../constants/v1'
|
||||
import { MULTICALL_ABI, MULTICALL_NETWORKS } from '../constants/multicall'
|
||||
import { getContract } from '../utils'
|
||||
import { useActiveWeb3React } from './index'
|
||||
|
||||
// returns null on errors
|
||||
function useContract(address?: string, ABI?: any, withSignerIfPossible = true): Contract | null {
|
||||
const { library, account } = useActiveWeb3React()
|
||||
|
||||
return useMemo(() => {
|
||||
if (!address || !ABI || !library) return null
|
||||
try {
|
||||
return getContract(address, ABI, library, withSignerIfPossible && account ? account : undefined)
|
||||
} catch (error) {
|
||||
console.error('Failed to get contract', error)
|
||||
return null
|
||||
}
|
||||
}, [address, ABI, library, withSignerIfPossible, account])
|
||||
}
|
||||
|
||||
export function useV1FactoryContract(): Contract | null {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
return useContract(chainId === 1 ? V1_FACTORY_ADDRESS : undefined, V1_FACTORY_ABI, false)
|
||||
}
|
||||
|
||||
export function useV1ExchangeContract(address: string): Contract | null {
|
||||
return useContract(address, V1_EXCHANGE_ABI, false)
|
||||
}
|
||||
|
||||
export function useTokenContract(tokenAddress?: string, withSignerIfPossible = true): Contract | null {
|
||||
return useContract(tokenAddress, ERC20_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
export function usePairContract(pairAddress?: string, withSignerIfPossible = true): Contract | null {
|
||||
return useContract(pairAddress, IUniswapV2PairABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
export function useMulticallContract(): Contract | null {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
return useContract(MULTICALL_NETWORKS[chainId as ChainId], MULTICALL_ABI, false)
|
||||
}
|
||||
26
src/hooks/useCopyClipboard.ts
Normal file
26
src/hooks/useCopyClipboard.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
export default function useCopyClipboard(timeout = 500): [boolean, (toCopy: string) => void] {
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
|
||||
const staticCopy = useCallback(text => {
|
||||
const didCopy = copy(text)
|
||||
setIsCopied(didCopy)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (isCopied) {
|
||||
const hide = setTimeout(() => {
|
||||
setIsCopied(false)
|
||||
}, timeout)
|
||||
|
||||
return () => {
|
||||
clearTimeout(hide)
|
||||
}
|
||||
}
|
||||
return
|
||||
}, [isCopied, setIsCopied, timeout])
|
||||
|
||||
return [isCopied, staticCopy]
|
||||
}
|
||||
22
src/hooks/useDebounce.ts
Normal file
22
src/hooks/useDebounce.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
// modified from https://usehooks.com/useDebounce/
|
||||
export default function useDebounce<T>(value: T, delay: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value)
|
||||
|
||||
useEffect(() => {
|
||||
// Update debounced value after delay
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value)
|
||||
}, delay)
|
||||
|
||||
// Cancel the timeout if value changes (also on delay change or unmount)
|
||||
// This is how we prevent debounced value from updating if value is changed ...
|
||||
// .. within the delay period. Timeout gets cleared and restarted.
|
||||
return () => {
|
||||
clearTimeout(handler)
|
||||
}
|
||||
}, [value, delay])
|
||||
|
||||
return debouncedValue
|
||||
}
|
||||
45
src/hooks/useENSName.ts
Normal file
45
src/hooks/useENSName.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { isAddress } from '../utils'
|
||||
import { useActiveWeb3React } from './index'
|
||||
|
||||
/**
|
||||
* Does a reverse lookup for an address to find its ENS name.
|
||||
* Note this is not the same as looking up an ENS name to find an address.
|
||||
*/
|
||||
export default function useENSName(address?: string): string | null {
|
||||
const { library } = useActiveWeb3React()
|
||||
|
||||
const [ENSName, setENSName] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!library || !address) return
|
||||
const validated = isAddress(address)
|
||||
if (validated) {
|
||||
let stale = false
|
||||
library
|
||||
.lookupAddress(validated)
|
||||
.then(name => {
|
||||
if (!stale) {
|
||||
if (name) {
|
||||
setENSName(name)
|
||||
} else {
|
||||
setENSName(null)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!stale) {
|
||||
setENSName(null)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
stale = true
|
||||
setENSName(null)
|
||||
}
|
||||
}
|
||||
return
|
||||
}, [library, address])
|
||||
|
||||
return ENSName
|
||||
}
|
||||
@@ -16,6 +16,7 @@ export default function useInterval(callback: () => void, delay: null | number)
|
||||
}
|
||||
|
||||
if (delay !== null) {
|
||||
tick()
|
||||
const id = setInterval(tick, delay)
|
||||
return () => clearInterval(id)
|
||||
}
|
||||
|
||||
20
src/hooks/useIsWindowVisible.ts
Normal file
20
src/hooks/useIsWindowVisible.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
/**
|
||||
* Returns whether the window is currently visible to the user.
|
||||
*/
|
||||
export default function useIsWindowVisible(): boolean {
|
||||
const [focused, setFocused] = useState<boolean>(true)
|
||||
const listener = useCallback(() => {
|
||||
setFocused(document.visibilityState !== 'hidden')
|
||||
}, [setFocused])
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('visibilitychange', listener)
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', listener)
|
||||
}
|
||||
}, [listener])
|
||||
|
||||
return focused
|
||||
}
|
||||
16
src/hooks/usePrevious.ts
Normal file
16
src/hooks/usePrevious.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
// modified from https://usehooks.com/usePrevious/
|
||||
export default function usePrevious<T>(value: T) {
|
||||
// The ref object is a generic container whose current property is mutable ...
|
||||
// ... and can hold any value, similar to an instance property on a class
|
||||
const ref = useRef<T>()
|
||||
|
||||
// Store current value in ref
|
||||
useEffect(() => {
|
||||
ref.current = value
|
||||
}, [value]) // Only re-run if value changes
|
||||
|
||||
// Return previous value (happens before update in useEffect above)
|
||||
return ref.current
|
||||
}
|
||||
@@ -6,7 +6,9 @@ import { useTransactionAdder } from '../state/transactions/hooks'
|
||||
import { useTokenBalanceTreatingWETHasETH } from '../state/wallet/hooks'
|
||||
|
||||
import { calculateGasMargin, getSigner, isAddress } from '../utils'
|
||||
import { useENSName, useTokenContract, useActiveWeb3React } from './index'
|
||||
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
|
||||
|
||||
@@ -8,7 +8,8 @@ import { Field } from '../state/swap/actions'
|
||||
import { useTransactionAdder } from '../state/transactions/hooks'
|
||||
import { computeSlippageAdjustedAmounts } from '../utils/prices'
|
||||
import { calculateGasMargin, getRouterContract, isAddress } from '../utils'
|
||||
import { useENSName, useActiveWeb3React } from './index'
|
||||
import { useActiveWeb3React } from './index'
|
||||
import useENSName from './useENSName'
|
||||
|
||||
enum SwapType {
|
||||
EXACT_TOKENS_FOR_TOKENS,
|
||||
|
||||
7
src/hooks/useToggle.ts
Normal file
7
src/hooks/useToggle.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
|
||||
export default function useToggle(initialState = false): [boolean, () => void] {
|
||||
const [state, setState] = useState(initialState)
|
||||
const toggle = useCallback(() => setState(state => !state), [])
|
||||
return [state, toggle]
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import store from './state'
|
||||
import ApplicationUpdater from './state/application/updater'
|
||||
import TransactionUpdater from './state/transactions/updater'
|
||||
import UserUpdater from './state/user/updater'
|
||||
import WalletUpdater from './state/wallet/updater'
|
||||
import MulticallUpdater from './state/multicall/updater'
|
||||
import ThemeProvider, { FixedGlobalStyle, ThemedGlobalStyle } from './theme'
|
||||
|
||||
const Web3ProviderNetwork = createWeb3ReactRoot(NetworkContextName)
|
||||
@@ -37,7 +37,7 @@ function Updaters() {
|
||||
<UserUpdater />
|
||||
<ApplicationUpdater />
|
||||
<TransactionUpdater />
|
||||
<WalletUpdater />
|
||||
<MulticallUpdater />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,6 @@ const AppWrapper = styled.div`
|
||||
flex-flow: column;
|
||||
align-items: flex-start;
|
||||
overflow-x: hidden;
|
||||
height: 100vh;
|
||||
`
|
||||
|
||||
const HeaderWrapper = styled.div`
|
||||
@@ -67,6 +66,10 @@ const BackgroundGradient = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const Marginer = styled.div`
|
||||
margin-top: 5rem;
|
||||
`
|
||||
|
||||
let Router: React.ComponentType
|
||||
if (process.env.PUBLIC_URL === '.') {
|
||||
Router = HashRouter
|
||||
@@ -99,12 +102,12 @@ export default function App() {
|
||||
<Route component={RedirectPathToSwapOnly} />
|
||||
</Switch>
|
||||
</Web3ReactManager>
|
||||
<Marginer />
|
||||
<Footer />
|
||||
</BodyWrapper>
|
||||
<BackgroundGradient />
|
||||
</AppWrapper>
|
||||
</Router>
|
||||
<div id="popover-container" />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,17 +2,15 @@ import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import NavigationTabs from '../components/NavigationTabs'
|
||||
|
||||
export const Body = styled.div`
|
||||
const Body = styled.div`
|
||||
position: relative;
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
/* min-height: 340px; */
|
||||
background: ${({ 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: 30px;
|
||||
padding: 1rem;
|
||||
position: relative;
|
||||
margin-bottom: 10rem;
|
||||
`
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,7 @@ import TokenLogo from '../../components/TokenLogo'
|
||||
import SearchModal from '../../components/SearchModal'
|
||||
import { Text } from 'rebass'
|
||||
import { Plus } from 'react-feather'
|
||||
import { TYPE, Link } from '../../theme'
|
||||
import { TYPE, StyledInternalLink } from '../../theme'
|
||||
import { AutoColumn, ColumnCenter } from '../../components/Column'
|
||||
import { ButtonPrimary, ButtonDropwdown, ButtonDropwdownLight } from '../../components/Button'
|
||||
|
||||
@@ -27,7 +27,7 @@ enum STEP {
|
||||
SHOW_CREATE_PAGE = 'SHOW_CREATE_PAGE' // show create page
|
||||
}
|
||||
|
||||
export default function CreatePool({ history, location }: RouteComponentProps) {
|
||||
export default function CreatePool({ location }: RouteComponentProps) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const [showSearch, setShowSearch] = useState<boolean>(false)
|
||||
const [activeField, setActiveField] = useState<number>(Fields.TOKEN0)
|
||||
@@ -116,8 +116,8 @@ export default function CreatePool({ history, location }: RouteComponentProps) {
|
||||
{pair ? ( // pair already exists - prompt to add liquidity to existing pool
|
||||
<AutoRow padding="10px" justify="center">
|
||||
<TYPE.body textAlign="center">
|
||||
Pool already exists!
|
||||
<Link onClick={() => history.push('/add/' + token0Address + '-' + token1Address)}> Join the pool.</Link>
|
||||
Pool already exists!{' '}
|
||||
<StyledInternalLink to={`/add/${token0Address}-${token1Address}`}>Join the pool.</StyledInternalLink>
|
||||
</TYPE.body>
|
||||
</AutoRow>
|
||||
) : (
|
||||
|
||||
40
src/pages/MigrateV1/index.tsx
Normal file
40
src/pages/MigrateV1/index.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { JSBI, Token } from '@uniswap/sdk'
|
||||
import React, { useMemo } from 'react'
|
||||
import { RouteComponentProps } from 'react-router'
|
||||
import { useAllV1ExchangeAddresses } from '../../data/V1'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useTokenBalances } from '../../state/wallet/hooks'
|
||||
|
||||
const PLACEHOLDER_ACCOUNT = (
|
||||
<div>
|
||||
<h1>You must connect a wallet to use this tool.</h1>
|
||||
</div>
|
||||
)
|
||||
|
||||
/**
|
||||
* Page component for migrating liquidity from V1
|
||||
*/
|
||||
export default function MigrateV1({}: RouteComponentProps) {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const v1ExchangeAddresses = useAllV1ExchangeAddresses()
|
||||
|
||||
const v1ExchangeTokens: Token[] = useMemo(() => {
|
||||
return v1ExchangeAddresses.map(exchangeAddress => new Token(chainId, exchangeAddress, 18))
|
||||
}, [chainId, v1ExchangeAddresses])
|
||||
|
||||
const tokenBalances = useTokenBalances(account, v1ExchangeTokens)
|
||||
|
||||
const unmigratedExchangeAddresses = useMemo(
|
||||
() =>
|
||||
Object.keys(tokenBalances).filter(tokenAddress =>
|
||||
tokenBalances[tokenAddress] ? JSBI.greaterThan(tokenBalances[tokenAddress]?.raw, JSBI.BigInt(0)) : false
|
||||
),
|
||||
[tokenBalances]
|
||||
)
|
||||
|
||||
if (!account) {
|
||||
return PLACEHOLDER_ACCOUNT
|
||||
}
|
||||
|
||||
return <div>{unmigratedExchangeAddresses?.join('\n')}</div>
|
||||
}
|
||||
@@ -3,11 +3,12 @@ import styled, { ThemeContext } from 'styled-components'
|
||||
import { JSBI, Pair } from '@uniswap/sdk'
|
||||
import { RouteComponentProps } from 'react-router-dom'
|
||||
|
||||
import Question from '../../components/Question'
|
||||
import Question from '../../components/QuestionHelper'
|
||||
import SearchModal from '../../components/SearchModal'
|
||||
import PositionCard from '../../components/PositionCard'
|
||||
import { useUserProbablyHasV1Liquidity } from '../../data/V1'
|
||||
import { useTokenBalances } from '../../state/wallet/hooks'
|
||||
import { Link, TYPE } from '../../theme'
|
||||
import { LinkStyledButton, StyledInternalLink, TYPE } from '../../theme'
|
||||
import { Text } from 'rebass'
|
||||
import { LightCard } from '../../components/Card'
|
||||
import { RowBetween } from '../../components/Row'
|
||||
@@ -58,6 +59,8 @@ export default function Pool({ history }: RouteComponentProps) {
|
||||
return <PositionCardWrapper key={i} dummyPair={pair} />
|
||||
})
|
||||
|
||||
const hasV1Liquidity = useUserProbablyHasV1Liquidity()
|
||||
|
||||
return (
|
||||
<AppBody>
|
||||
<AutoColumn gap="lg" justify="center">
|
||||
@@ -92,15 +95,16 @@ export default function Pool({ history }: RouteComponentProps) {
|
||||
)}
|
||||
{filteredExchangeList}
|
||||
<Text textAlign="center" fontSize={14} style={{ padding: '.5rem 0 .5rem 0' }}>
|
||||
{filteredExchangeList?.length !== 0 ? `Don't see a pool you joined? ` : 'Already joined a pool? '}{' '}
|
||||
<Link
|
||||
id="import-pool-link"
|
||||
onClick={() => {
|
||||
history.push('/find')
|
||||
}}
|
||||
>
|
||||
Import it.
|
||||
</Link>
|
||||
{!hasV1Liquidity ? (
|
||||
<>
|
||||
{filteredExchangeList?.length !== 0 ? `Don't see a pool you joined? ` : 'Already joined a pool? '}{' '}
|
||||
<StyledInternalLink id="import-pool-link" to="/find">
|
||||
Import it.
|
||||
</StyledInternalLink>
|
||||
</>
|
||||
) : (
|
||||
<LinkStyledButton id="migrate-v1-liquidity-link">Migrate your V1 liquidity.</LinkStyledButton>
|
||||
)}
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
<FixedBottom>
|
||||
|
||||
@@ -4,12 +4,7 @@ import styled from 'styled-components'
|
||||
export const Wrapper = styled.div`
|
||||
position: relative;
|
||||
`
|
||||
export const FixedBottom = styled.div`
|
||||
position: absolute;
|
||||
top: 100px;
|
||||
width: 100%;
|
||||
margin-bottom: 80px;
|
||||
`
|
||||
|
||||
export const ClickableText = styled(Text)`
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useActiveWeb3React } from '../../hooks'
|
||||
import { useToken } from '../../hooks/Tokens'
|
||||
import { usePairAdder } from '../../state/user/hooks'
|
||||
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||
import { Link } from '../../theme'
|
||||
import { StyledInternalLink } from '../../theme'
|
||||
import AppBody from '../AppBody'
|
||||
|
||||
enum Fields {
|
||||
@@ -119,13 +119,9 @@ export default function PoolFinder({ history }: RouteComponentProps) {
|
||||
<LightCard padding="45px 10px">
|
||||
<AutoColumn gap="sm" justify="center">
|
||||
<Text textAlign="center">Pool found, you don’t have liquidity on this pair yet.</Text>
|
||||
<Link
|
||||
onClick={() => {
|
||||
history.push('/add/' + token0Address + '-' + token1Address)
|
||||
}}
|
||||
>
|
||||
<StyledInternalLink to={`/add/${token0Address}-${token1Address}`}>
|
||||
<Text textAlign="center">Add liquidity to this pair instead.</Text>
|
||||
</Link>
|
||||
</StyledInternalLink>
|
||||
</AutoColumn>
|
||||
</LightCard>
|
||||
)
|
||||
@@ -133,13 +129,7 @@ export default function PoolFinder({ history }: RouteComponentProps) {
|
||||
<LightCard padding="45px">
|
||||
<AutoColumn gap="sm" justify="center">
|
||||
<Text color="">No pool found.</Text>
|
||||
<Link
|
||||
onClick={() => {
|
||||
history.push('/add/' + token0Address + '-' + token1Address)
|
||||
}}
|
||||
>
|
||||
Create pool?
|
||||
</Link>
|
||||
<StyledInternalLink to={`/add/${token0Address}-${token1Address}`}>Create pool?</StyledInternalLink>
|
||||
</AutoColumn>
|
||||
</LightCard>
|
||||
) : (
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@ import Card, { BlueCard, GreyCard } from '../../components/Card'
|
||||
import { AutoColumn, ColumnCenter } from '../../components/Column'
|
||||
import ConfirmationModal from '../../components/ConfirmationModal'
|
||||
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
||||
import QuestionHelper from '../../components/Question'
|
||||
import QuestionHelper from '../../components/QuestionHelper'
|
||||
import { AutoRow, RowBetween, RowFixed } from '../../components/Row'
|
||||
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
|
||||
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
|
||||
@@ -30,14 +30,20 @@ import { useSendCallback } from '../../hooks/useSendCallback'
|
||||
import { useSwapCallback } from '../../hooks/useSwapCallback'
|
||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { useDefaultsFromURL, useDerivedSwapInfo, useSwapActionHandlers, useSwapState } from '../../state/swap/hooks'
|
||||
import {
|
||||
useDefaultsFromURLSearch,
|
||||
useDerivedSwapInfo,
|
||||
useSwapActionHandlers,
|
||||
useSwapState
|
||||
} from '../../state/swap/hooks'
|
||||
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||
import { CursorPointer, TYPE } from '../../theme'
|
||||
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningServerity } from '../../utils/prices'
|
||||
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
|
||||
import AppBody from '../AppBody'
|
||||
import { PriceSlippageWarningCard } from '../../components/swap/PriceSlippageWarningCard'
|
||||
|
||||
export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
useDefaultsFromURL(search)
|
||||
useDefaultsFromURLSearch(search)
|
||||
|
||||
// text translation
|
||||
// const { t } = useTranslation()
|
||||
@@ -173,7 +179,7 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
const [showInverted, setShowInverted] = useState<boolean>(false)
|
||||
|
||||
// warnings on slippage
|
||||
const severity = !sendingWithSwap ? 0 : warningServerity(priceImpactWithoutFee)
|
||||
const severity = !sendingWithSwap ? 0 : warningSeverity(priceImpactWithoutFee)
|
||||
|
||||
function modalHeader() {
|
||||
if (!sendingWithSwap) {
|
||||
@@ -245,7 +251,7 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
const swapState = useSwapState()
|
||||
function _onTokenSelect(address: string) {
|
||||
// if no user balance - switch view to a send with swap
|
||||
const hasBalance = allBalances?.[account]?.[address]?.greaterThan('0') ?? false
|
||||
const hasBalance = allBalances?.[address]?.greaterThan('0') ?? false
|
||||
if (!hasBalance) {
|
||||
onTokenSelection(
|
||||
Field.INPUT,
|
||||
@@ -370,7 +376,7 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
</ArrowWrapper>
|
||||
<ButtonSecondary
|
||||
onClick={() => setSendingWithSwap(false)}
|
||||
style={{ marginRight: '0px', width: 'fit-content', fontSize: '14px' }}
|
||||
style={{ marginRight: '0px', width: 'auto', fontSize: '14px' }}
|
||||
padding={'4px 6px'}
|
||||
>
|
||||
Remove Swap
|
||||
@@ -492,20 +498,26 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
)}
|
||||
<V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} />
|
||||
</BottomGrouping>
|
||||
{bestTrade && (
|
||||
<AdvancedSwapDetailsDropdown
|
||||
trade={bestTrade}
|
||||
rawSlippage={allowedSlippage}
|
||||
deadline={deadline}
|
||||
showAdvanced={showAdvanced}
|
||||
setShowAdvanced={setShowAdvanced}
|
||||
priceImpactWithoutFee={priceImpactWithoutFee}
|
||||
setDeadline={setDeadline}
|
||||
setRawSlippage={setAllowedSlippage}
|
||||
/>
|
||||
)}
|
||||
</Wrapper>
|
||||
</AppBody>
|
||||
|
||||
{bestTrade && (
|
||||
<AdvancedSwapDetailsDropdown
|
||||
trade={bestTrade}
|
||||
rawSlippage={allowedSlippage}
|
||||
deadline={deadline}
|
||||
showAdvanced={showAdvanced}
|
||||
setShowAdvanced={setShowAdvanced}
|
||||
setDeadline={setDeadline}
|
||||
setRawSlippage={setAllowedSlippage}
|
||||
/>
|
||||
)}
|
||||
|
||||
{priceImpactWithoutFee && severity > 2 && (
|
||||
<AutoColumn gap="lg" style={{ marginTop: '1rem' }}>
|
||||
<PriceSlippageWarningCard priceSlippage={priceImpactWithoutFee} />
|
||||
</AutoColumn>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import Card, { GreyCard } from '../../components/Card'
|
||||
import { AutoColumn } from '../../components/Column'
|
||||
import ConfirmationModal from '../../components/ConfirmationModal'
|
||||
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
||||
import QuestionHelper from '../../components/Question'
|
||||
import QuestionHelper from '../../components/QuestionHelper'
|
||||
import { RowBetween, RowFixed } from '../../components/Row'
|
||||
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
|
||||
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
|
||||
@@ -27,23 +27,30 @@ import { useApproveCallbackFromTrade, ApprovalState } from '../../hooks/useAppro
|
||||
import { useSwapCallback } from '../../hooks/useSwapCallback'
|
||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { useDefaultsFromURL, useDerivedSwapInfo, useSwapActionHandlers, useSwapState } from '../../state/swap/hooks'
|
||||
import {
|
||||
useDefaultsFromURLSearch,
|
||||
useDerivedSwapInfo,
|
||||
useSwapActionHandlers,
|
||||
useSwapState
|
||||
} from '../../state/swap/hooks'
|
||||
import { CursorPointer, TYPE } from '../../theme'
|
||||
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningServerity } from '../../utils/prices'
|
||||
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
|
||||
import AppBody from '../AppBody'
|
||||
import { PriceSlippageWarningCard } from '../../components/swap/PriceSlippageWarningCard'
|
||||
|
||||
export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
useDefaultsFromURL(search)
|
||||
// text translation
|
||||
// const { t } = useTranslation()
|
||||
useDefaultsFromURLSearch(search)
|
||||
|
||||
const { chainId, account } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
// toggle wallet when disconnected
|
||||
const toggleWalletModal = useWalletModalToggle()
|
||||
|
||||
// swap state
|
||||
const { independentField, typedValue } = useSwapState()
|
||||
const { bestTrade, tokenBalances, parsedAmounts, tokens, error, v1TradeLinkIfBetter } = useDerivedSwapInfo()
|
||||
const { onSwitchTokens, onTokenSelection, onUserInput } = useSwapActionHandlers()
|
||||
const isValid = !error
|
||||
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
|
||||
|
||||
@@ -58,6 +65,11 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
|
||||
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
|
||||
|
||||
const formattedAmounts = {
|
||||
[independentField]: typedValue,
|
||||
[dependentField]: parsedAmounts[dependentField] ? parsedAmounts[dependentField].toSignificant(6) : ''
|
||||
}
|
||||
|
||||
const route = bestTrade?.route
|
||||
const userHasSpecifiedInputOutput =
|
||||
!!tokens[Field.INPUT] &&
|
||||
@@ -69,13 +81,6 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
// check whether the user has approved the router on the input token
|
||||
const [approval, approveCallback] = useApproveCallbackFromTrade(bestTrade, allowedSlippage)
|
||||
|
||||
const formattedAmounts = {
|
||||
[independentField]: typedValue,
|
||||
[dependentField]: parsedAmounts[dependentField] ? parsedAmounts[dependentField].toSignificant(6) : ''
|
||||
}
|
||||
|
||||
const { onSwitchTokens, onTokenSelection, onUserInput } = useSwapActionHandlers()
|
||||
|
||||
const maxAmountInput: TokenAmount =
|
||||
!!tokenBalances[Field.INPUT] &&
|
||||
!!tokens[Field.INPUT] &&
|
||||
@@ -88,7 +93,7 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
: tokenBalances[Field.INPUT]
|
||||
: undefined
|
||||
const atMaxAmountInput: boolean =
|
||||
!!maxAmountInput && !!parsedAmounts[Field.INPUT] ? maxAmountInput.equalTo(parsedAmounts[Field.INPUT]) : undefined
|
||||
maxAmountInput && parsedAmounts[Field.INPUT] ? maxAmountInput.equalTo(parsedAmounts[Field.INPUT]) : undefined
|
||||
|
||||
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(bestTrade, allowedSlippage)
|
||||
|
||||
@@ -130,7 +135,7 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
const [showInverted, setShowInverted] = useState<boolean>(false)
|
||||
|
||||
// warnings on slippage
|
||||
const priceImpactSeverity = warningServerity(priceImpactWithoutFee)
|
||||
const priceImpactSeverity = warningSeverity(priceImpactWithoutFee)
|
||||
|
||||
function modalHeader() {
|
||||
return (
|
||||
@@ -259,13 +264,7 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
</AutoColumn>
|
||||
<BottomGrouping>
|
||||
{!account ? (
|
||||
<ButtonLight
|
||||
onClick={() => {
|
||||
toggleWalletModal()
|
||||
}}
|
||||
>
|
||||
Connect Wallet
|
||||
</ButtonLight>
|
||||
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
|
||||
) : noRoute && userHasSpecifiedInputOutput ? (
|
||||
<GreyCard style={{ textAlign: 'center' }}>
|
||||
<TYPE.main mb="4px">Insufficient liquidity for this trade.</TYPE.main>
|
||||
@@ -294,20 +293,26 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
)}
|
||||
<V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} />
|
||||
</BottomGrouping>
|
||||
{bestTrade && (
|
||||
<AdvancedSwapDetailsDropdown
|
||||
trade={bestTrade}
|
||||
rawSlippage={allowedSlippage}
|
||||
deadline={deadline}
|
||||
showAdvanced={showAdvanced}
|
||||
setShowAdvanced={setShowAdvanced}
|
||||
priceImpactWithoutFee={priceImpactWithoutFee}
|
||||
setDeadline={setDeadline}
|
||||
setRawSlippage={setAllowedSlippage}
|
||||
/>
|
||||
)}
|
||||
</Wrapper>
|
||||
</AppBody>
|
||||
|
||||
{bestTrade && (
|
||||
<AdvancedSwapDetailsDropdown
|
||||
trade={bestTrade}
|
||||
rawSlippage={allowedSlippage}
|
||||
deadline={deadline}
|
||||
showAdvanced={showAdvanced}
|
||||
setShowAdvanced={setShowAdvanced}
|
||||
setDeadline={setDeadline}
|
||||
setRawSlippage={setAllowedSlippage}
|
||||
/>
|
||||
)}
|
||||
|
||||
{priceImpactWithoutFee && priceImpactSeverity > 2 && (
|
||||
<AutoColumn gap="lg" style={{ marginTop: '1rem' }}>
|
||||
<PriceSlippageWarningCard priceSlippage={priceImpactWithoutFee} />
|
||||
</AutoColumn>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user